Skip to content

Adopt Claude Code cross-session messaging as ground-truth liveness + delivery (replaces screen-scraping) — Claude-only, needs a driver seam #667

Description

@tu11aa

Summary

Claude Code v2.1.224+ shipped cross-session messaging: sessions register themselves on disk, bind a per-session Unix inbox socket, and can list/message each other via the ListAgents and SendMessage tools. Announced 2026-08-07 (@ClaudeDevs, docs).

This is a first-party, ground-truth replacement for two things squadrant currently reverse-engineers by scraping terminal pixels:

  1. "Is this agent idle or busy?" — today: glyph matching + LivenessRegistry + pid floor.
  2. "Did my message actually get submitted?" — today: confirmedSendToPane paste → settle → Enter → re-read screen.

Both are the source of a long tail of shipped bugs: #447, #455, #466, #484, #492, #516, #566, #590, and the currently-open #514/#657.

Local evidence (verified on this machine, 2026-08-08)

claude --version2.1.226 (feature requires ≥ 2.1.224; macOS + Linux only, which matches our macOS-only stance).

Every running Claude Code session writes ~/.claude/sessions/<pid>.json:

{"pid":37511,"sessionId":"bb38bbd8-...","cwd":"/Users/q3labsadmin/me/squadrant",
 "startedAt":1786167988194,"version":"2.1.226","peerProtocol":1,
 "kind":"interactive","entrypoint":"cli",
 "messagingSocketPath":"/tmp/cc-socks/37511.sock",
 "name":"squadrant-e8","nameSource":"derived",
 "status":"busy","updatedAt":1786168376031,"statusUpdatedAt":1786168376031}

The squadrant captain session is itself registered. Captains are launched by squadrant into cmux panes, exactly the same way crews are — so squadrant-spawned Claude sessions bind an inbox socket and are addressable with no changes on our side. Feasibility for the --agent claude path is already proven; nothing needs to be built to make sessions discoverable.

ListAgents from the squadrant captain right now returns live peers with liveness state:

oneplanapp-61 [10fa68]        · interactive · idle   · started 8m ago
observer-sessions-0d [ed69b3] · interactive ·        · started 7s ago
career-stack-f6 [3effa4]      · interactive · idle   · started 6m ago

Note peerProtocol: 1 — the registry is explicitly versioned, so it is a contract rather than an incidental artifact.

Opportunity A — status field replaces liveness screen-scraping

~/.claude/sessions/<pid>.json carries status: "idle" | "busy" plus statusUpdatedAt, self-reported by the agent. That is strictly better than inferring activity from pane output, because it is the agent's own account of whether it is mid-turn rather than a guess from whether glyphs changed.

This is the cheap, high-value half: read a JSON file. No protocol reverse-engineering, no new transport. It slots straight into the existing LifecycleSource port from #333 as a fourth source alongside CmuxStore, NativeHook, and CodexAppServer.

Directly relevant to #567 (liveness trusts its own event history over observable reality) and #618 (adopt structured APIs as ground truth). See "Relationship to #618" below — these are complementary, not competing.

Opportunity B — inbox socket replaces confirmedSendToPane

packages/workspaces/src/crew-pane.ts:152 is the paste-settle-Enter machine, wired in at packages/cli/src/commands/crew.ts:70. Its own doc comment enumerates four hardening steps and three separate bug references, and the body carries #455, #484, #516 guards inline. It infers delivery from whether an input box emptied after a draft was seen.

The peer channel replaces that inference with a real answer. Delivery resolves to one of three documented outcomes — delivered, held, or refused — and, for same-machine sends, the sender is notified: a notice when the message is held, and a follow-up when the receiver later delivers, denies, or lets it expire.

That is precisely the receipt #551 asks for (ping has no delivery or read receipt) and precisely the false-negative that #514/#657 is about.

The seam already exists: crew.ts:70 injects sendToPane, so a ClaudePeerSocket delivery driver can slot in behind it without touching call sites.

⚠️ Hard limitation — this does NOT fix #514/#657

Cross-session messaging is Claude Code only. It does nothing for codex, opencode, or gemini crews.

This matters more than it first appears:

So this work would fix the delivery path we are currently using least, and leave the one we use most untouched. Adopting it naively would also re-commit the exact mistake CLAUDE.md warns about and that #657 already diagnosed once: "the v0.13.3 UserPromptSubmit fix was Claude-only and never got a cross-agent migration path." Shipping a second Claude-only delivery path without a seam would repeat that, one layer down.

Therefore: this must be built as a capability behind the existing driver seam, never as a special case. AgentDriver should expose something like supportsPeerMessaging() / peerAddress(), with the pane-scraping path retained as the fallback for agents that lack it.

Open questions (spike needed)

  1. Socket wire format. The docs explicitly say to read the inbox-socket section "when you want a script or hook to post into a session", and export CLAUDE_CODE_MESSAGING_SOCKET to hooks and Bash commands (confirmed: our captain has /tmp/cc-socks/37511.sock). But the payload format is not publicly documented and there is no claude CLI subcommand for posting. The daemon is a Node process, not a Claude session — it has no SendMessage tool. Determining whether the daemon can post directly to the socket is the single blocking unknown for Opportunity B.
  2. Sender identity / inbound controls. The daemon is not a child process of the crew session, so the "own-child" verification path does not apply (and on macOS that check only works while the posting process is still running). Delivery then falls to crossSessionInbound. Crews launch with --permission-mode auto, which counts as prompting, so the default should deliver — but we should set crossSessionInbound: "accept" explicitly in crew settings rather than rely on a default we did not choose.
  3. Rate limiting is a real hazard for us. Claude Code "drops identical repeats arriving within a short window" and caps unread messages at 50. Our retry logic re-sends the same text. A silently-dropped retry would look exactly like the false negative we are trying to eliminate. Needs explicit handling.
  4. Feature-flag fragility. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_TELEMETRY, DO_NOT_TRACK, and DISABLE_GROWTHBOOK each disable the feature. We must detect capability at runtime, never assume it.
  5. Plain text only. No structured protocol across sessions. Fine for prompt delivery; rules out using it as a typed control-plane transport.
  6. Channels as an alternative for Opportunity B. Channels are the documented way to push external events into a running session, which is literally what the daemon does. But: research preview, requires --channels at launch, requires Bun, requires an allowlisted plugin (custom needs --dangerously-load-development-channels), and the flag syntax and protocol contract may change. Heavier and less stable than the socket, but officially sanctioned. Worth evaluating against option 1 rather than assumed inferior.

Relationship to #618

#618 proposes adopting cmux structured session/feed APIs as ground truth. These are complementary rather than alternatives, and the distinction is worth stating plainly:

Source Truth it reports Coverage
cmux structured APIs (#618) terminal-level — pane activity all agents
Claude peer registry (this issue) agent-level — the agent's own idle/busy Claude only

The Claude registry is the more accurate signal where it applies, because an agent knows whether it is mid-turn and a terminal can only guess. cmux is the more universal one. A LifecycleSource that prefers the peer registry when present and falls back to cmux otherwise gets both.

Immediate usable win — captain ↔ captain chat

Independent of any crew delivery work, this is usable today with zero code: captains are Claude Code sessions, they already appear in each other's ListAgents, and they can message each other directly with real delivery receipts.

That is a better transport than what squadrant ping does today (#551: ✔ Pinged only means "sent into the void") and is the closest thing yet to the cross-captain channel #552 asks for. Worth validating with a live captain-to-captain round trip before committing to any of the above.

Proposed phasing

#514/#657 stays open and separately prioritised regardless — none of this touches the opencode path.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions