Skip to content

feat: desktop-led duo via an anchor pane - #70

Open
notdp wants to merge 12 commits into
mainfrom
anchor-pane-lead
Open

feat: desktop-led duo via an anchor pane#70
notdp wants to merge 12 commits into
mainfrom
anchor-pane-lead

Conversation

@notdp

@notdp notdp commented Aug 3, 2026

Copy link
Copy Markdown
Owner

A duo can now be led from a Claude Code desktop session: hive duo init there forms a team where the desktop session itself is the worker and a codex validator runs in a background detached tmux session (tmux attach -t hive-ccd to watch). Messages flow both ways — the desktop worker sends over the validator's app-server daemon, and the validator's replies arrive back in the desktop session's context on their own. tmux demotes from the runtime's host to an optional display.

The worker keeps a pane without living in one: every member still has a pane. The desktop worker gets an anchor pane — a plain shell carrying the member tags plus @hive-remote=channel, whose channel socket and ready marker are symlinks to the desktop session's own hive-client-<pid>.sock. Identity, routing, delivery state, reaping, and doctor keep operating on pane authority; no second identity scheme grows anywhere.

The return path, and why it isn't a channel

Claude Code delivers channel notifications only to sessions launched with --channels, and the desktop app owns its argv. Measured A/B on this machine, same binary and same stream-json transport: flag present → the notification reaches the model verbatim; flag absent → the server still gets its write receipt and the session hears nothing, exactly as the docs describe. Every way to inject the flag was tried and failed — no settings key or env var exists, .mcp.json is documented as insufficient, and replacing the app's helper binary with a wrapper works mechanically but the app restores the original before every session spawn, so a wrapper can never serve one.

Plugin hooks, however, do run in desktop sessions. So the hive plugin gained a Stop hook: at the end of a turn it drains the member's bus inbox and returns the messages as blocked-stop feedback, which Claude Code injects as context and continues on — the same effect the channel would have had, minus the flag. Inside tmux it is a silent no-op, because the native channel and app-server transports already deliver there.

Design decisions worth reviewing:

  • The agent is the discovery mechanism. Outside tmux the channel server binds a pid-keyed client socket and appends its path to its own MCP instructions; the desktop agent reads them and hands the path to hive duo init --channel. No registry, no extra protocol. Without --channel, init connect-probes hive-client-*.sock and proceeds only on exactly one live listener (corpse files without a listener are ignored).
  • Delivery stays fail-closed; the evidence changes shape. An anchor pane hosts no CLI by design, so Agent.send routes @hive-remote=channel panes to the channel transport instead of refusing on the process probe — the channel's marker and one-byte receipt are the "somebody is listening" evidence. When the desktop session exits, its server unlinks the real socket and marker, the anchor's symlinks dangle, and sends fail closed exactly like a dead pane server. Re-running init on a restarted session relinks the anchor and keeps the team ("relinked": true) instead of stranding it.
  • Waiting is scoped to an owed reply. hive collect --wait N --if-awaiting blocks only while one of the member's own messages is unanswered, so a duo waiting on a verdict holds its turn open while an idle session ends immediately. The hook uses exactly this, and never re-blocks once stop_hook_active is set.
  • Outside-tmux gates became positive-evidence checks, opt-in. Team.create_for_window / Agent.spawn take allow_outside_tmux and then require the target window/pane to actually exist on the server. The CLI root gate admits init/duo, plus any command once the saved default context names a team. In-tmux behavior and unbound outside-tmux behavior are unchanged.
  • spawn_daemon resolves a bare codex to an absolute path once. Spawner and pane otherwise resolve against different PATHs; the version-skew incident from 2026-07-14 becomes structural here, because the detached session inherits the desktop app's environment rather than a login shell's.

Validation

1219 unit + cli tests green. Two rounds of real-runtime evidence:

  • Isolated dev lane (disposable HIVE_HOME/CODEX_HOME, throwaway tmux session, checkout CLI shim): a stand-in desktop client formed the duo against a real spawned codex validator, hive send validator delivered from outside tmux, and the validator's reply came back through the anchor symlink as a channel notification carrying the exact nonce.
  • Production desktop session (the author's own Claude Code desktop app, plugin loaded from the live cache): sent a token to the validator and ended the turn with no polling of any kind; the Stop hook injected the reply mid-stop and the turn continued, with reply-to matching the sent msgId and the token verbatim.

Note for deployment: the Stop hook only reaches desktop sessions once this ships as a plugin version, since plugins load from the installed cache rather than a checkout. Sidecar ergonomics for the anchor member (notify/resume surfacing) stay out of scope here.

New concepts

Anchor pane — proxy identity for an external member

An anchor pane preserves the runtime's core invariant — every member is a pane — for a member whose process lives elsewhere. The pane is a plain shell holding the identity tags; the member's transport endpoints are symlinks pointing at the external process's real endpoints.

flowchart LR
    V[validator pane] -->|hive reply worker| S["hive-pane-%7.sock (symlink)"]
    S --> C["hive-client-11750.sock"]
    C --> D[desktop Claude session]
Loading

Why this over "paneless members": a member without a pane forks every pane-keyed authority (identity, liveness, reaping, doctor, resume) into a second code path. Anchoring inverts the cost — one placeholder pane buys back every existing mechanism, including kill-pane as the natural kick-a-member control, at the price of one honest asymmetry: pane-alive no longer implies member-alive, so delivery receipts carry that truth instead.

When not to use it: don't anchor a process that can't expose a linkable endpoint — a proxy that can't forward is just a lie with tags on it.


Compound Engineering
Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant