Repository navigation
Inbox: say a sent reply is queued, and Send now delivers it into the running turn - #1968
Merged
Merged
Conversation
…running turn The line under the person's reply said "Saved for Claude Code. It sees it when it checks." while the reply waited for the session to go idle, which read as dropped. It now says where the reply is, from the session's connection: "Queued · Claude Code is working; it gets this when its turn ends." with Send now, "Sending to Claude Code…" while idle, and the truthful "Saved for … It sees it when it next reads this thread." for an agent that only reads the thread. Send now (POST /api/inbox/messages/:id/send-now, door 7.58) marks the reply deliver_now_at; the bridge command carries now: true. The Claude Code mod puts it into the running turn as a user row with $.session.append (a plugin's $.prompt.submit never folds into a running turn), with one short follow-up turn if the turn ended before any request carried it; Pi steers (deliverAs: "steer"); OpenCode 2 prompts with delivery: "steer". Connections advertise it with the poll field send_now. Also fixes the mod counting a subagent's turn.start as the session's turn: its end was never counted, so a background subagent left the session busy for good and Inbox replies never went in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…48 characters The Send now sentence in the tool's wake note pushed it to 2,122; the wake text and plannotator inbox --help already say it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # tests/e2e/playwright.config.ts
… steers Review fixes: - The mod decides into-the-turn right before the append, after the fresh check, lease and claim: a turn that ended meanwhile gets the reply as a turn; a turn that ended or changed during the append gets the pointer turn at once; rows are tied to their turn id (several per turn). - Never into this session's own Ask this session turn, running or queued (steerableTurn); the reply waits for idle there. - Send now is advertised only on an engine whose API has $.session.append (2.1.301+, from $.session.version(): validate refuses testing the noun). - OpenCode 2 counts a steer delivered only on session.inbox.delivered for its row; if the run ends first, it is sent queued after a grace. Pi tested on message_start. - The server hands `now` only to a poll that said send_now; a refused steer posts send_now_failed and the line reads queued again; urgent delivery is oldest first; the line reads Queued for a shim-sent message whose session polls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # apps/hook/hooks/mod/inbox-contract.ts # apps/hook/hooks/mod/inbox.test.ts # apps/hook/server/inbox-help.ts # packages/server/inbox.ts # packages/shared/inbox/connection.ts # tests/helpers/inbox-world.ts
…d while an Ask holds it - The fallback queued turn ends with a repeat line, and the plugin's context and generate hooks drop the later of the steer row and its fallback from every model request (OpenCode promotes the steer row with the queued turn, #1734, and cannot withdraw it). - session.inbox.delivery.changed to queue for the steer row counts as delivered: no fallback. - The steer waits at most 60 s, and the fallback goes at once when the event stream ends. - The mod's send_now (poll and state events) is true only while a reply could go in now: while this session's own Ask question holds it, Send now is not offered and a pressed one reads Queued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
# Conflicts: # apps/hook/tests/register.test.ts
# Conflicts: # AGENTS.md # apps/hook/hooks/mod/inbox-contract.ts # apps/hook/hooks/mod/inbox.test.ts # packages/shared/inbox/connection.ts
…at never empties the last message - OpenCode 2: no cap while the run goes on (a long tool call no longer sends the fallback while OpenCode will still promote the steer row); a lost end event is checked with session.wait, and after the event stream ends the row waits for idle, then counts as delivered when session.context holds it. - dropInboxDuplicates replaces a later copy that is its message's whole content with a stable pointer instead of removing it, so a request never ends on an assistant message. - Pi and OpenCode never steer into the session's own Ask question (canSteer), and InboxWake's state events carry send_now, as the mod's do. - The mod keeps alive fresh for every claim it holds while it waits. - Pi: a steer Esc moved into the editor is taken back out before the reply goes in as a follow-up. - The 👍 line uses the reply line's honest Queued / Sending words from the session's connection (no Send now for an ack).
…n is back, then is working, no Send now)
…n's process delivers it; OpenCode 2 sends no steer fallback into a deleted session or during shutdown The Claude Code mod kept a claim it won but could not use (a turn started right after the claim, its own Ask question, a refused row) and refreshed its alive while not the lease holder, so with the lease moved to another process nobody delivered the reply. It now gives the claim back (INBOX_RELEASE_SCRIPT, only while it is its own and neither delivered nor reported), and a process that lost the claim takes the reply back as pending once the folder is gone. OpenCode 2: session.deleted settles that session's steer rows (their watch stops) and dispose settles them as abandoned: no fallback is sent; the wake gives its claim back and the reply is picked up at the next start.
1 task
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.
What the person sees
The owner's report: after replying in the Inbox, the reply said "Saved for Claude Code. It sees it when it checks.", which reads as if it were dropped, or as if the agent had to poll for it. The line under the person's last reply (or New message / compose message) now says where it is, from what the Inbox knows about the session's connection right now:
The Sent row in the list reads "Queued for ; it gets this as its next turn." for a waking agent (the row's additive
sent.wakes), "Saved for . It sees it when it next reads this thread." otherwise.Can a Claude Code plugin steer into a running turn?
Yes, but not with
$.prompt.submit. From the engine's own API (2.1.301 types and the plugin-authoring reference):$.prompt.submit"queues a prompt that starts a turn of its own once the session is idle (never folded into a running turn)".$.session.append({ message: { type: 'user', content } })"REALLY appends: a user-role row … the model reads it … The row is in the list at once and in a running turn's requests from the loop's next top." So Send now uses$.session.append, no interrupt needed. The engine harness (claude plugin test) confirms the mod's call is accepted and stored as a user row.What Send now does per host
POST /api/inbox/messages/:id/send-now(same-origin,serverSession) writesdeliver_now_aton the message. Refused when the recipient's connection does not poll now or cannot deliver mid-turn (409 send_now_unavailable), when delivered (409 already_delivered), or for a message that goes to no session (422 not_addressed). The bridge command carriesnow: trueonly on a poll that sayssend_now; a held poll answers at once. A connection that could not steer postssend_now_failed, which clears the mark (the line reads Queued again). On the device door as contract 7.58.now, the connection delivers the waiting replies at once, oldest first, so an urgent reply never overtakes an older one to the same session.send_now(poll and state events) is true only while a reply could go in now, so while this session's own Ask question holds it Send now is not offered and a pressed one reads Queued. Decided right before the append, after the fresh check, lease and claim. Into the running main turn as one user row ($.session.append) plus a transcript notice, never into this session's own "Ask this session" turn (running or queued: the reply waits for idle there). A turn that ended before the append gets the reply as a turn; one that ended or changed during it gets one short pointer turn at once; otherwise the row is tied to its turn id, and a turn that ends with no model request after it gets the pointer. Advertised only on an engine at least 2.1.301 (from$.session.version():claude plugin validaterefusestypeof $.session.append), and no more after an engine refuses a row.pi.sendUserMessage(text, { deliverAs: "steer" }), delivered when Pi starts the message (message_start).session.prompt({ delivery: "steer" }), delivered only onsession.inbox.deliveredfor that row id (orsession.inbox.delivery.changedtoqueue). If the run ends first and the row is not delivered within 3 s, after 60 s, or at once if the event stream ends, it is sent as a queued turn ending with "If this same reply () already appears above, it is a repeat; answer it once." Because OpenCode cannot withdraw the steer row and promotes it with that turn (fix(opencode): a tool launch's session-URL notice never wakes an idle root #1734), the plugin'scontext/generatehooks drop the later of the two copies from every model request. Offered only while the event stream runs.send_now, so Send now is never offered.A bug this turned up
The owner's "Claude is busy constantly" had a second cause: every hook hears a subagent's
turn.start, the mod counted it as the session's running turn, and its end was filtered out atturn.complete. So once a background subagent started, the session read as busy for good and Inbox replies never went in.PlannotatorMod.onTurnStartnow ignores subagent turns. Proved in the engine harness (fails without the fix) and incontroller.test.ts.Tests
packages/server/inbox-send-now.test.ts: routes, guards, the poll'snow, a held poll answering on Send now,sent.wakes, and agent-link's steer for Pi/OpenCode (busy → steer, idle → deliver at once, no steer → refused).apps/hook/hooks/mod/inbox.test.ts12 to 17: the mod against a real Inbox (busy session, Send now → one user row, no turn, Delivered; the row landing as the turn ended → one pointer turn; the turn ending just before the append (the reviewer's repro) → a turn, no row; the turn ending during the append → the pointer at once; this session's Ask question running or queued → nothing until idle; oldest first).send-now-engine.test.ts: the version gate.apps/opencode-plugin/inbox-v2-wake.test.ts: the steer counted delivered only on itssession.inbox.delivered, and the queued fallback.apps/pi-extension/inbox-send-now.test.ts: the steer, delivered onmessage_start.apps/hook/tests/register.test.ts(engine harness): the row through$.session.append; an engine older than 2.1.301 never offers it; a background subagent's turn no longer holds a reply.packages/inbox/deliveryLine.test.ts, DOMpackages/inbox/sendNow.test.tsx(added to the test.yml DOM list).tests/e2e/inbox-send-now.spec.tson the compiled binary (desktop light/dark and a phone);inbox-compose,inbox-ended-session,inbox-new-messagespecs updated to the new lines and pass.Screenshots (light, dark, phone):
.product/drafts/inbox-queued-send-now/in the owner's checkout.Docs: AGENTS.md (Questions and Send, the mod's Inbox paragraph, Ask this session's busy, agent connections, server table),
plannotator inbox --help, the marketing reference page, and the mobile contract (7.58).