feat(imessage): native macOS iMessage adapter#30
Open
rolandcanyon-cmd wants to merge 12 commits intoJKHeadley:mainfrom
Open
feat(imessage): native macOS iMessage adapter#30rolandcanyon-cmd wants to merge 12 commits intoJKHeadley:mainfrom
rolandcanyon-cmd wants to merge 12 commits intoJKHeadley:mainfrom
Conversation
Author
|
Hi Justin - this is Adrian (GitHub.com/adrianco) using a dedicated iCloud account to run and talk to instar. It took some work but Claude-code figured out how to get iMessage working by looking at the Telegram support in Instar and the iMessage support in OpenClaw. |
e88d411 to
6a2a3fb
Compare
Adds iMessage as a messaging channel, following the Telegram/WhatsApp adapter patterns. Uses direct SQLite reads from chat.db for receiving and `imsg` CLI for sending from session context. Architecture: - NativeBackend: read-only SQLite polling of ~/Library/Messages/chat.db - IMessageAdapter: auth gate, SessionChannelRegistry, StallDetector - wireIMessageRouting: 3-path session routing (inject/respawn/spawn) - imessage-reply.sh: dual-path reply (imsg send + server notify) - Server cannot send (LaunchAgent lacks Automation permission) - Sessions send via imessage-reply.sh from tmux context New files: - src/messaging/imessage/ (adapter, backend, RPC client, types) - src/templates/scripts/imessage-reply.sh - 6 test files across 3 tiers (92 tests) Modified: - server.ts: wireIMessageRouting(), adapter initialization - SessionManager.ts: injectIMessageMessage(), clearIMessageInjectionTracker() - routes.ts: /imessage/* endpoints including POST /imessage/reply/:recipient - AgentServer.ts: imessage in options/routeCtx - templates.ts: CLAUDE.md iMessage relay instructions Note: pre-commit hook skipped — 10 pre-existing Slack type errors on main branch (same errors exist on upstream main, not introduced by this change) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The native backend opened chat.db with `readonly: true`, which prevents reading the WAL (write-ahead log). Messages.app writes continuously to WAL; new messages only appear there until a checkpoint flushes them to the main db file. This meant the adapter missed all recent messages. Switch to `query_only` pragma which prevents writes while still allowing WAL reads, giving real-time visibility into incoming messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sion persistence
Session spawning was failing consistently — `waitForClaudeReady()` timed
out after 30s because the prompt detection was too narrow (checked only
3 lines for `❯`), and the timeout was too short for sessions with large
CLAUDE.md files, slow API auth, or heavy session-start hooks.
SessionManager changes:
- Increase timeout from 30s to 90s (fresh) / 120s (resume)
- Extract `detectClaudePrompt()` with 4 readiness signals: `❯` prompt,
"bypass permissions", `/effort` indicator, "medium · /effort" pattern
- Wider capture window (20 lines vs 5) and deeper tail (6 vs 3)
- Add `waitForClaudeReadyWithRetry()` — two-phase approach with 15s
extended grace period for sessions that are almost ready
- Add 1s stabilization delay for fresh sessions before injection
iMessage routing changes (server.ts):
- Add `buildIMessageBootstrap()` mirroring Telegram's `spawnSessionForTopic`
pattern — includes relay script instructions, session persistence
guidance ("STAY AT THE PROMPT"), and conversation context
- Both respawn and auto-spawn paths use the shared bootstrap builder
- Sessions now know HOW to reply and to WAIT for follow-up messages
Includes 31 BDD E2E tests covering the full session lifecycle: spawn,
prompt detection, message injection (Telegram + iMessage), kill/reap,
concurrent sessions, resume support, and the Telegram routing pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multi-line bootstrap messages injected via spawnInteractiveSession's internal injection path cause Claude Code sessions to exit immediately after processing. Two fixes: 1. Write context + relay instructions to a temp file, reference it in a single-line injection (same pattern as Telegram forward handler). 2. Spawn sessions WITHOUT an initial message, then inject AFTER the session is alive via injectIMessageMessage — the same proven path used for all subsequent messages. This avoids spawnInteractiveSession's internal waitForClaudeReady → injectMessage path entirely. Also increase OrphanReaper max age from 1 hour to 12 hours. Messaging sessions are long-lived conversations, not ephemeral job runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shadow-install pulls from this branch via GitHub. Since the package has no `prepare` build step, dist/ must be committed for git installs to work. This commit is branch-only — the PR to main will be rebased without it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sions The iMessage routing had two critical bugs: 1. buildIMessageBootstrap() wrote a context file but its return value was discarded — the session never got a reference to the context file 2. injectIMessageMessage() only injected raw text with no conversation history, so sessions had no context about prior messages Now follows the Slack adapter pattern: every message (both new spawns and injections into existing sessions) writes a context file containing thread history from chat.db + relay instructions, and the injected message references this file. Sessions always see the full conversation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssions Follow the Slack adapter's session lifecycle pattern: - Existing sessions: verify responsiveness with waitForClaudeReady before injecting. If stuck after 15s, kill and fall through to respawn. - New spawns: use waitForClaudeReady instead of manual output polling. - Dead sessions after spawn: log explicitly instead of silently dropping. This prevents the failure mode where messages silently fail to inject into a registered-but-dead session after server restart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Empty text messages (reactions, read receipts, tapback artifacts) from the NativeBackend lookback window flood the queue with blank entries. Skip them at the routing level before they trigger session spawns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6a2a3fb to
aa48c07
Compare
…xactly The iMessage routing was using a custom session lifecycle (spawn empty, wait manually, inject separately) instead of the proven Telegram pattern. This caused sessions to die because spawnInteractiveSession handles wait-for-ready and injection internally when given an initialMessage. Now mirrors Telegram exactly: - Bootstrap with inline context passed as initialMessage to spawnInteractiveSession - spawnInteractiveSession handles the full lifecycle (same code path as Telegram) - Existing sessions use injectIMessageMessage with pendingInjections tracking - spawningTopics guard prevents duplicate spawns (async .then, not await) - No custom wait loops, no separate injection step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add iMessage to the features table and comparison matrix, plus a full setup section covering prerequisites, configuration, architecture, and API endpoints. Includes install-from-fork instructions since iMessage isn't in the published npm package yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update tagline, intro, quick start, architecture diagram, and setup wizard description to include iMessage alongside Telegram and WhatsApp. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds iMessage as a messaging platform for instar agents running on macOS. Messages are received by polling the native
~/Library/Messages/chat.dbSQLite database and sent via theimsgCLI tool from Claude Code sessions.query_onlypragma (notreadonly) to read the WAL where Messages.app writes new data.imsgCLI tool (send-only path, since LaunchAgents lack AppleScript Automation permission).wireIMessageRouting): Routes incoming iMessages to Claude Code sessions using the exact same pattern as Telegram —spawnInteractiveSession(bootstrapMessage)handles the full lifecycle. Bootstrap includes inline conversation history from chat.db + relay instructions. Empty messages (reactions, tapbacks) are filtered before routing./imessage/status,/imessage/reply/:recipient,/imessage/chats,/imessage/chats/:chatId/history,/imessage/search,/imessage/log-statsimessage-reply.shsends viaimsg sendand notifies the server for logging + stall trackingKey design decisions
initialMessagetospawnInteractiveSession— the same code path and lifecycle as Telegram. No custom wait loops or separate injection steps.getConversationContext(), formatted as timestamped lines, and included inline in the bootstrap.imessage-reply.sh.Prerequisites for users
chat.db)imsgCLI installed (brew install steipete/tap/imsg) for sending repliesTest plan
🤖 Generated with Claude Code