Inbox, Steering, and Continuation
How mid-run input is queued, claimed, injected, or routed to child Agents
Conclusion: the Inbox Is a Replayable Scheduling Protocol, Not a Temporary Chat-Input Array
DeepSeek Harness expresses input intent through two ordered Inbox lists: next-turn holds work that should each open a later Turn, while next-step holds steering or context that should enter together at the nearest available Step boundary. followup, steer, and inject are only three fixed combinations of target list and wake behavior; claim, pre-step admission, Turn-closing checks, and cancellation convergence determine when a message actually becomes visible.
The important capability is not merely “send another message while work is running.” Pending input itself enters the Session Event Log, allowing stable MessageIds, insertion, editing, cancellation, claiming, recovery, and UI classification to share one fact. The cost is equally explicit: accepted means the Inbox admitted work, not that it ran, produced a reply, or was necessarily fsynced; running describes whole-driver activity rather than a completion handle for one message.
1. First Separate Six Mechanisms with Similar Names
| Name | Destination | Wakes? | Boundary semantics |
|---|---|---|---|
followup | next-turn | Yes | Ordinary later input; each item gets its own later Turn on the success path |
steer | next-step | Yes | Targets the current Turn's next Step when possible; opens a Turn when idle |
inject | next-step | No | Quiet model context; remains parked while idle until another wake arrives |
| Tool continuation | Tool results are logged; additional context enters next-step | The current driver continues | A later Step in the same Turn, not a new follow-up |
| Continuable-child follow-up | The child Agent's next-turn | Yes | Always a later FIFO Turn for the child; it cannot redirect the current Turn |
| Child report / settlement notice | The parent Agent's next-turn or next-step | Policy-dependent | Child-to-parent traffic, entirely separate from parent-to-child follow-up |
2. Queue Class Is Not Message Role: Identity, Source, and Scheduling Placement Are Orthogonal
An Inbox item is still the shared immutable UserMessage: it has a stable id, role: user, exact content blocks, and a required source. Plugins may extend MessageSourceMap; freezeMessage structured-clones and deep-freezes, while creation assigns a fresh UUID. INBOX-MESSAGE-IDENTITY
| Dimension | Question it answers | Examples |
|---|---|---|
| Message role | How it enters the provider-neutral conversation | user |
| Source | Who produced it and how the UI should interpret it | user, plugin notice, coordinator relay, subagent report |
| Inbox target | At which scheduling boundary it is claimed | next-turn / next-step |
| Web placement | Where pending state renders | queued / steering / context |
A “steering message” is therefore not a third message structure. It is the historical fact that a user-source message was claimed from next-step; a non-user source with the same role is instead interpreted as context by the UI.
3. One Durable Splice Rebuilds Both Lists; Commit Precedes Live Mutation
agent/inbox/spliced {
target: next-turn | next-step,
start,
removedCount?,
inserted: full UserMessage[],
outcome?: canceled
}
Insertion, editing, removal, claim, and clear all normalize to the same splice. The event carries every full inserted message. Deletion receives outcome: canceled only on user/API/cancellation paths; the loop's claim is a pure deletion without that outcome. INBOX-EVENT-SCHEMA
Inbox.mutate() validates coordinates and MessageId uniqueness across both lists, calls session.append, and mutates the in-memory array plus live notifications only after append succeeds. An append failure leaves the Inbox unchanged. A synchronous Session observer sees the pre-splice lists and can recover removed objects from the event coordinates. INBOX-COMMIT-IDENTITY
This is also why the Web projection needs no separate command queue: it reads the Agent's current lists and applies the event being emitted once to obtain authoritative post-state.
4. Claim Has a Strict Priority: Every next-step Item, Then at Most One next-turn Item
claim(target) for each proposed Step
1. Remove and take every next-step item (FIFO)
2. If target = next-turn, remove the head next-turn item
3. Emit claimed(message, turn) in that order
First Step of a Turn: target = next-turn
Later Steps in that Turn: target = next-step
The constructor replays only Inbox splices after seedLength. Claim forms one batch from all current next-step items and appends only one ordinary queued message. It never interrupts a model request already in flight; it can act only at the next pre-step boundary. INBOX-CLAIM-REPLAY
This creates an often-missed composition: a Turn's first request may include long-parked injected context, steering, and that Turn's ordinary prompt at once, ordered as the next-step batch followed by the next-turn head.
5. Follow-up Preserves the FIFO Turn Mapping, but Status Does Not Flicker Per Turn
followup(message) is send(message, next-turn, true). When idle, wake synchronously reserves the running phase and then starts the driver asynchronously. While running, enqueueing is enough: after the current Turn closes, the live driver checks hasPending, installs a fresh AbortController, and continues with another Turn. INBOX-SEND-MAP
Synchronous bursts, sequential sends, and mixed scheduling all verify that N follow-ups produce N strictly increasing Turns on the failure-free path, with exactly one ordinary message per Turn and no reordering. The entire burst still has only a running → idle status trace. INBOX-FIFO-PROPERTY
6. Steer Means “Nearest Step Plus Wake,” Not an In-Stream Append to the In-Flight Request
steer(message) writes to next-step and requests a wake. If the Agent is normally running, wakeDriver() does not start another driver: the current one should claim it at its next Step boundary. If the Agent is idle, steer opens a Turn itself. The core API therefore does not require running; an idle steer becomes the first input of a new Turn while replay still classifies it as steering. INBOX-DRIVER-WAKE
This is boundary steering, not mutation of the provider request while it is generating tokens. It changes the log prefix seen by the next model call, but cannot retract an emitted chunk and does not automatically cancel the current tool call.
The Web composer offers steer only while an ordinary session reports running, reducing the surprise that idle steer actually opens a Turn. Direct Host session.prompt(mode: steer) still exposes the underlying semantics.
7. Inject Means “Nearest Step Without Wake”; Its Turn Depends Entirely on Timing
| Injection time | Outcome |
|---|---|
| Agent idle | Park in next-step until follow-up, steer, or another waking delivery |
| Current pre-step has not claimed | Join this batch |
| Pre-step already claimed; assembly/listener/model/tool is active | Miss this request and enter the next Step |
Inside an agent/turn-stopping listener | The post-listener reread can open another Step in the same Turn |
| An aborted driver is converging | No wake latch; remain parked for a future wake |
A test pauses the first pre-step listener and calls inject then steer. Both remain in next-step, are absent from the first request, and appear in insertion order in the second. If the current batch is rejected, later next-step input survives but still needs a fresh wake. INBOX-PRESTEP-CUTOFF-TEST
8. Pre-step Consumes Before Admission: Reject Does Not Automatically Put the Original Message Back
turn/start
→ inbox.claim(target) // durable pure deletion
→ assemble system prompt
→ agent/pre-step waterfall
├─ enter(messages) → step/start → user/message* → model
└─ reject → turn/end(blocked), no Step
The loop has already removed the complete batch before listeners run. A listener may preserve, replace, or empty messages. Reject records a blocked Turn; the original batch is not marked canceled, does not become user/message, and is not restored to the queue. An empty first enter instead produces a completed Turn with no Step. INBOX-TURN-MACHINE
If assembly or a listener throws, the Turn closes with error. next-turn work not yet claimed and next-step work arriving later remain queued, but the driver stops. The design makes input claim the commit point rather than successful model dispatch.
9. Tool Continuation and turn-stopping Reuse next-step but Have Different Producers
After the model emits tool calls, results enter the log in model order. Each finalized result's additionalContexts is then spliced into next-step in order. Without concludesTurn, the loop opens another Step in the same Turn. A conclusion still cannot skip next-step input that already exists. INBOX-TOOL-CONTEXTINBOX-TOOL-CONTINUATION
When the model appears ready to end and next-step is empty, the loop awaits agent/turn-stopping and rereads next-step. A regression test proves that steer from this listener forces Step 2; if a listener-injected batch is rewritten to empty at pre-step, no extra model call occurs. INBOX-TURN-STOPPING-TEST
10. running Belongs to the Whole Driver; Maintenance Still Appears Idle
| Observation | What it means | What it does not guarantee |
|---|---|---|
status: running | A driver is reserved and may be in pre-step, model, tools, Turn close, or between drained Turns | One particular message is executing; the current Turn certainly still accepts steer |
status: idle | No turn driver; a maintenance task may still be active | The Inbox is empty |
whenIdle() | Wait for current activity and replacement activity started before retirement to stabilize | A result for one MessageId |
runMaintenance() | Exclusively own non-Turn activity from true idle; latch waking input | Expose maintenance itself to the model |
The public types define running as the complete drain interval and keep maintenance externally idle. whenIdle follows replacement work by comparing activity Promises rather than binding to a send. INBOX-ACTIVITY-CONTRACT
11. Read Cancel Across Three Layers: Signal, Pending Input, and Already-Claimed Input
| Call | Current activity | Unclaimed Inbox | Claimed batch |
|---|---|---|---|
cancel(cause) | Abort when non-idle | Clear next-step then next-turn and write canceled splices; also clears quiet parked input while idle | Never restored |
cancel(cause,{keepInbox:true}) | Abort when non-idle | Preserve, but pre-abort work is not automatically woken | Never restored |
Web session.cancel | Calls the keepInbox form | Preserves ordinary queue and steering | Never restored |
| Subagent interrupt | Calls the keepInbox form and returns immediately | Preserves and parks until a later waking send | Never restored |
Default cancel always invokes Inbox.clear and aborts only a non-idle phase; keepInbox skips clear. Tests prove that a waking send may already be claimed before cancel is called, leaving nothing for keepInbox to preserve. Default cancel drops a mid-step queued tail and steering. INBOX-CANCEL-CODEINBOX-CANCEL-BEHAVIOR-TESTINBOX-CANCEL-DROP-TEST
12. The Abort-to-Idle Wake Latch Repairs Cancellation Convergence but Leaves a Narrower Retirement Gap
Active Turn is aborted but not yet idle
├─ already queued + keepInbox → preserved but parked (no latch)
├─ new waking send afterward → force next-turn + wakeRequested=true
├─ default cancel → clear Inbox + clear latch
└─ disposed → never latch
Old driver finally: idle → if latched and pending remains → new driver
Send samples abort state before insertion. Even a post-abort steer is redirected to next-turn so it cannot join a Turn already committed to end. Replay after convergence orders the old turn/end before the new turn/start; removing the latched message suppresses an empty replay. Regression tests cover same-tick and slow convergence, maintenance, default cancel, and disposal. INBOX-CANCEL-LATCH-TEST
13. Turn Records Alone Cannot Answer “What Happened to This Work?”
A no-Step Turn may mean an empty claim, a listener rewrite to empty, pre-step rejection, cancellation, or error. Reading turn/end alone cannot distinguish “nothing ran” from “input was consumed before reaching the model.” foldConsumedWork therefore joins steps, pure-deletion claims, and canceled splices. INBOX-CONSUMED-WORK
| History shape | Accounting |
|---|---|
| Entered a Step | The Turn consumed work |
| Claim followed by blocked / aborted / error | The Turn consumed work |
| Claim followed by completed with no Step | Treat as listener-emptying, not a completed work Turn |
| Inbox canceled without replacement | droppedUnrun: true |
| One splice replaces an old message with a new one | Still pending, not dropped |
Dedicated tests cover failed claims, stopped claims, blocked claims, empty rewrites, claims outside a Turn, unrun drops, replacements, and a later Turn absorbing an older drop. INBOX-CONSUMED-WORK-TEST
14. “Logged,” “Pushed to Web,” and “Fsynced” Are Three Different Commit Points
Host prompt accepted
→ Inbox splice synchronously appended to live Session
→ API may return accepted
→ JSONL write-behind batch (fixed window by default)
First pre-step checkpoint
→ flush enqueue + turn/start + claim
Before model-adapter dispatch
→ flush step/start + user/message + request prefix
The checkpoint policy flushes at pre-step and the llm/stream boundary. The latter delays downstream-stream construction, so a flush failure prevents model dispatch. Top-level tool dispatch uses the same posture. INBOX-CHECKPOINT-POLICY
An E2E child process is killed at model dispatch. Recovery still contains insertion, turn/start, claim, step, user message, and request prefix, plus synthesized interrupted closers. This delivery reran that file's two tests and both passed. INBOX-CRASH-RECOVERY
The JSONL backend batches live events behind a fixed window; flush and teardown drain immediately. Every batch append is fsynced, and failure rolls the file back to its prior byte length. A synchronous prompt accepted is therefore Session-level admission, not an independent fsync receipt. A product promising power-loss safety before acknowledgement would need to await an explicit checkpoint. INBOX-JSONL-DURABILITY
15. Resume Rebuilds Pending Work but Does Not Run Merely Because It Rebuilt; Fork Seeds Do Not Copy Ancestor To-Dos
The resume path prepares a Session from persistence and then rejoins the same prepare/setup/publish path as creation. A new ReactLoopAgent constructor restores the last Turn number and Inbox from the log. INBOX-AGENT-RESUME
Inbox replays only session.events.slice(seedLength). Historical splices copied from a parent Session into a fork seed are child context history, not newly pending child work. Messages accepted by the child after its seed but never claimed do recover. INBOX-SEED-BOUNDARY
Construction and publication do not call wake. An ordinary Host cold resume is normally followed immediately by a waking prompt; a continuable-child cold resume likewise calls followup next, so older pending work proceeds with the new wake under the two-list claim rule. Resuming an Agent alone does not promise to consume parked input.
16. The Ordinary Host API Returns Admission, Not a Turn or Reply
session.prompt accepts mode: queue | steer. After time-zone handling, durable attachment conversion, and message-source construction, it calls agent.followup or agent.steer and returns {accepted:true} once synchronous enqueueing succeeds. It does not await pre-step, model output, or idle. INBOX-HOST-PROMPT
The public contract maps queue/steer directly to the two Agent operations. updateQueue supports edit/remove/steer, while session.cancel explicitly preserves pending work. INBOX-HOST-CONTRACT
The Host resolves or resumes a cold ordinary Session first. Direct mode: steer adds no running check, so it retains the core behavior where idle steer opens a Turn. That is a different layer from the UI's conservative submission policy.
17. Queue Edit, Remove, and Strict Steer Address MessageId; Claim Wins the Race
| Action | Admission | Implementation |
|---|---|---|
| Edit | Still pending; text blocks only | Deep-freeze a replacement while preserving MessageId |
| Remove | Still pending | Delete with a canceled splice |
| Strict steer | Item remains in next-turn and Agent currently reports running | Remove, then call steer with the same message |
The Host relocates the message across both lists. If the loop claimed it first, the operation returns queue-item-not-found. Strict steer's running check is stronger than free-form mode: steer, but it is not a capability token bound to one Turn and remains subject to the final retirement window. Subagent-owned Sessions reject these ordinary queue operations. INBOX-HOST-QUEUE-MUTATION
18. Web Uses Whole Snapshots for the Live Mirror and Durable Splices to Recover Historical Identity
Session splice (observer sees pre-state)
→ Host applies this splice to current lists
→ session/queue { complete items[] }
next-turn → queued
next-step + user → steering
next-step + non-user → context
The Host explicitly projects both lists and splits next-step into steering/context by source.kind. Its event observer applies the emitted splice to pre-state rather than waiting for live Inbox mutation. INBOX-HOST-PROJECTION
The mux sends session/subscribed first and emits a queue baseline only when pending work exists. The client clears the old generation on subscribed, so “no later queue frame” correctly means an empty queue rather than retaining disconnected phantom rows. INBOX-RECONNECT-HOSTINBOX-RECONNECT-CLIENT
The Queue mirror is a whole-snapshot last-wins value. Pending steering retires when a durable user/message with the same MessageId appears. Historical replay instead reads a non-canceled removal from next-step and then checks user source to reconstruct the same user/message as steering. INBOX-CLIENT-MIRRORINBOX-HISTORY-CLASSIFICATION
19. The UI Places Queued and Steering Input Separately, and Treats “Accelerated Send” as Policy, Not Truth
QueueDock shows only placement=queued and provides edit, remove, and per-row steer. The steer button is enabled only while running. Pending steering instead renders as a user-style bubble at the conversation tail and hands off after the durable message enters history. INBOX-QUEUE-DOCKINBOX-PENDING-STEERING-UI
A non-running or non-steer-capable transport always queues. A busy ordinary session follows its saved Queue/Steer preference, while Cmd/Ctrl+Enter chooses the opposite. Accelerated Enter on an empty draft can strict-steer every queued row. Held Enter is suppressed to prevent repeated sends. INBOX-SUBMISSION-POLICYINBOX-INPUT-GESTURES
20. A Continuable Child's Activation Is a Residency Epoch, Not a Request or Result
startContinuable establishes a durable child. Later followup hides the resident-versus-cold-resume distinction and routes every item through the child's single Inbox. The success boundary is acceptance of a message id by that Inbox; it does not await Turn start or return a child reply. INBOX-CONTINUATION-SEAM
| Activation state | Derived condition |
|---|---|
| running | Child Agent is running, or the manager recorded a waking message before synchronous send completed |
| waiting | Child Agent is quiescent but still owns undisposed descendant Activations |
| settled | Agent is quiescent with no owned children, so its handle can be disposed |
One Activation may execute many FIFO Turns and may remain resident after its own Agent becomes idle because descendants still run. The manager owns only residency and ownership; the Agent loop and Inbox remain the sole owners of Turn ordering. INBOX-ACTIVATION-MODELINBOX-ACTIVATION-STATE
21. Parent-to-Child Delivery Is Always Follow-up; a Lock Linearizes Delivery Against Disposal
For a resident child, the manager submits under a child-id lock. If disposal has begun, delivery waits for release and then cold-resumes. For an absent child, it inspects persistence, authorizes against durable parent lineage before folding, reads only the post-seed continuable descriptor, and reconstructs an Activation. INBOX-CONTINUATION-DELIVERYINBOX-COLD-RESUME
Final admission is a no-await synchronous span checking caller signal, manager drain, Activation disposal, and direct-parent authority. It adds MessageId to the accepted set before invoking child Agent.followup, then marks the Activation announced. Every parent-to-child message therefore enters next-turn; it never steers the child's current Turn. INBOX-CONTINUATION-ADMISSION
Interrupt authorizes only against a live Activation, calls cancel(...,{keepInbox:true}), and returns immediately. Pending work and descendants remain, claimed work is not restored, and an absent target is an accepted no-op. A later waking follow-up restarts the parked queue. INBOX-CHILD-INTERRUPT
22. Child-to-Parent Has Two Independent Messages: Explicit Report and Automatic Settlement Account
| Message | Source | Parent scheduling | Ends child? |
|---|---|---|---|
| Explicit report | subagent-report relay | wakeup → followup; quiet → inject | No |
| Settlement notice | subagent-settled notice | parent closing → inject; idle → followup; busy → steer | It accounts for ended residency; it is not words authored by the child |
Only the exact resident child Agent may report. The recipient is derived from durable direct-parent lineage rather than caller choice. wakeup versus quiet is a parent scheduling policy; reporting does not alter the child's Turn or Activation. INBOX-CHILD-REPORT
The manager delivers settlement after the child handle is released but before parent ownership is released, preventing the parent watcher from disposing first. A busy parent receives steer, letting several simultaneous child completions coalesce into one next-step batch. Notice failure is logged and dropped, and final flush is best-effort. INBOX-SETTLEMENT-NOTICE
23. Child Web and Model Control Surfaces Are Intentionally Narrower Than Ordinary Sessions
| Surface | Follow-up | Steer | Stop |
|---|---|---|---|
| Web continuable child | Requires live direct parent; text only; returns MessageId | None; client ignores mode and uses subagent.prompt | May interrupt by durable address while parent is offline |
| Web one-shot child | Read-only | None | None |
Model send_message | The exact live direct parent sends a later FIFO Turn to its child | None | A live ancestor can use interrupt_agent on a descendant |
The wire contract defines prompt receipt as an Inbox MessageId and interrupt receipt as signal acceptance rather than target quiescence. Host child prompt requires a live parent, while interrupt deliberately performs no parent, catalog, or persistence lookup. INBOX-SUBAGENT-WIREINBOX-SUBAGENT-HOST
The client makes one-shot children read-only. A running continuable child keeps Send and exposes Stop separately, so new input becomes a later Turn. With the parent offline, input is disabled but a live child can still be stopped. The model tool likewise states that send_message returns no reply and cannot redirect current work. INBOX-CHILD-CLIENTINBOX-CHILD-COMPOSERINBOX-CHILD-OFFLINE-UIINBOX-CONTROL-TOOLS
24. Two Reproducible Documentation Drifts Exist at the Pinned Baseline
| Adjacent documentation/comment | What it says | Current production code |
|---|---|---|
api/events.ts | A pending message becomes durable only when claimed; pending work has no durable Session event | agent/inbox/spliced is appended before live mutation and insertion carries the complete message |
| ApiProxy README | Pending next-step steering stays outside the Web projection | queueItems() projects both lists, labels user-origin next-step as steering, and ChatView renders its pending bubble |
The wire comments directly contradict the current durable event schema and Inbox implementation. The README directly contradicts the current Host projection and UI. These are comparisons within the same pinned commit, not runtime speculation. INBOX-WIRE-DOC-DRIFTINBOX-README-DRIFT
25. Guarantee Matrix and Design Assessment
| Claim | Guaranteed | Not guaranteed |
|---|---|---|
| Ordinary follow-up | FIFO in one Inbox; one Turn per item during a successful drain | An independent result Promise per item; automatic continuation after rejection/error |
| Steer | The nearest Step boundary not already crossed; can wake from idle | Mutation of an in-flight request; absolute freedom from retirement races |
| Inject | Model-facing context at the next claim | Self-wake; guaranteed membership in the current Turn |
| Cancel keepInbox | Preservation of work not yet claimed | Restoration of claimed work; automatic wake for pre-abort queued work |
| Prompt acceptance | Synchronous Inbox/Session admission and a MessageId for child delivery | Model dispatch, a produced reply, or completed physical fsync |
| Continuable child | Cold resume across residency; parent-to-child always a later FIFO Turn | A synchronous child result from send_message; current-Turn steering |
The strongest design choice is avoiding separate pending-work queues for UI, plugins, and subagents. Two lists, one splice vocabulary, stable identities, and one claim algorithm span the kernel, recovery, and product surface. The expensive complexity accumulates around “acceptance is not execution”: pre-step claims first, cancel does not roll back, driver status spans Turns, and Web snapshots plus physical persistence each add their own commit point. The system has filled most observability gaps with consumed-work folding, checkpoints, and a wake latch. Public documentation still needs correction; stronger no-stranding or crash-safe acknowledgement would require a new explicit protocol, not more optimistic wording.
Beyond branch-by-branch tracing of production source, wire contracts, READMEs, and tests at the pinned commit, this delivery executed 13 targeted Agent/Inbox/Host/client/subagent Vitest files (379 tests passed) and separately ran the hard-crash checkpoint file under the E2E configuration (2 tests passed): 14 files and 381 tests passed in total.
My Learning Notes
Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.