Skip to content

agent-reply-channel

A send stamps WHO sent it + a reply hint into the delivered message, so the recipient can reply over the same send.

raw source

spex session send is one-way: a message arrives in the recipient's prompt with no trace of who sent it, so an agent can read a request but can't answer it. Make it bidirectional — let a supervised agent reply to whoever sent it. Not a new mechanism: reuse the existing send. Just stamp the sender and insert a reply hint into the delivered message, so the recipient can reply (or ignore), and the reply rides the same send straight back into the sender's prompt. No workflow enforcement, no ack protocol — only a prompt insert. A human running send from a plain shell has no session, so they get the bare message with no hint — the loop closes only between two agents.

expanded spec

The wrap is withSenderHint, a pure prompt insert (dispatch's sessions.ts). Given the message text and a sender { id, label }, it appends one line: — from session "<label>" (<id>). To reply: spex session send <id> "<your reply>". The reply command is a runnable spex session send at the sender's FULL id — the recipient pastes intent into it and the reply travels the identical path back.

The sender label is the board HEADLINE, not the bare prompt title. The name comes from sessionHeadline (the unified cross-surface title — the SAME chain the board card shows: a chosen name ▸ the live Claude-Code self-summary activity ▸ a fuller prompt preview ▸ node/title/branch/id), NOT the stable sessionLabel which stops at the 7-word prompt-truncation title. So the recipient recognises the sender the way it reads the board — a concise "what they're doing", not a slice of their raw launch prompt. And the label is delimited as session "<headline>" so it reads AS a session title, not as prose bleeding into the message. A sender with no richer name than its id is stamped session <id> (no empty quotes/parens); with no sender at all the message passes through unchanged — no hint, no half-built reply loop. The watch handshake greeting (comms-edge) names the watcher by the same sessionHeadline, delimited the same way.

The sender is resolved in the send command's OWN process, because only it knows who's sending. The injection itself happens in the backend (the rendezvous socket — dispatch), a different process that has no idea which agent invoked the CLI. So the sender identity must be captured and the message wrapped before it leaves the CLI: the session send verb reads its own ownSessionId (the launcher's SPEXCODE_SESSION_ID, else the harness session env var like Claude Code's CLAUDE_CODE_SESSION_ID), resolves that id against the live board through the shared remote-client resolveClientSession to get the display label (session-selectors is reused for the recipient too), wraps with withSenderHint, and only then calls clientSend. The transport stays dumb — clientSend / POST …/keys carry the already-wrapped text and learn nothing about senders, so the reply-channel is product semantics living at the compose layer, not smuggled into the socket.

Graceful when there's no sender. A human at a plain shell (no SPEXCODE_SESSION_ID, no harness session env var) yields ownSessionId() === nullsender = null → the bare message is delivered, exactly as before this node existed. A sender id that resolves to no board row still stamps that full id (label omitted), so the reply target is never lost even if the row is momentarily unlistable.