fix: apply the review items for #694 at merge

- src/web/routes/session-routes.ts:2102: the guarded-submit 409 now applies to tagged input (clientId + seq) only; untagged input is held like a WebSocket frame. The raw write (:2228) goes through holdForGuardedLease and answers 200 once held (a send-and-wait reads `delivered` from the held write; a full queue releases the waiter and answers 409); a mux write takes its own turn in writeViaMux.
- test/guarded-lease-held-input.test.ts: real-Session route tests for an untagged raw POST during the lease (written after the Enter) and a held send-and-wait (delivered: true); both fail without the fix.
- test/routes/session-routes.test.ts + test/mocks/mock-session.ts: the 409 test now covers tagged input only; new tests for untagged raw (held), untagged mux (own turn) and a full hold queue (409); MockSession gains holdForGuardedLease/releaseGuardedLease and a lease-aware writeViaMux.
- src/session.ts:4787 (guardedInputHeld JSDoc) + write() + docs/architecture-invariants.md: write() refuses during the lease only, so cron's paste mode can land ahead of input still draining.
- docs/api-reference.md: the input route's 409 is for tagged input; untagged input is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-27 02:07:39 +01:00
1 parent 2aad93914e
commit 6368128f1e
7 files changed
+221 -32

No files matched your search

+17 -13
View File
@@ -426,11 +426,12 @@ worker's prompt but never submitted, and the wait then runs its full timeout on
turn that never started. Verified live; this is the most common silent failure on
this endpoint.
Any input, waiting or not, answers `409 CONFLICT` with nothing written while an
experimental guarded submit holds the session's input lease, or input held behind
it is still being written (a few milliseconds; see
[guarded input](#experimental-bounded-agent-inspection-and-guarded-input)).
Retry it.
Input tagged with `clientId` and `seq`, waiting or not, answers `409 CONFLICT`
with nothing written while an experimental guarded submit holds the session's
input lease, or input held behind it is still being written (a few milliseconds;
see [guarded input](#experimental-bounded-agent-inspection-and-guarded-input)).
Retry it. Untagged input is held instead and written in arrival order once the
lease is released.
A **plain prompt** (printable text followed by exactly one `\r`, nothing else) is
delivered through tmux even without `useMux`: the text is typed, Enter is pressed as
@@ -716,15 +717,18 @@ outcome. That covers terminal WebSocket keystrokes (each frame is acknowledged
only once it is written, so a frame typed during the lease lands before the one
typed after it), Shift+Enter and Ctrl+Enter from `POST .../send-key`, approval
answers and Codeman's own managed writes. A held write that is still being sent
holds everything behind it. `POST /api/v1/sessions/:id/input` instead answers
`409 CONFLICT` ("A guarded submit is in progress on this session; nothing was
written") for that span, the lease plus the held input's drain, rather than a
`200` for bytes that were never written; a tagged `clientId`/`seq` frame keeps
its seq, so the browser's durable queue, whose POST fallback sends one frame at a
time and keeps it on a `409`, delivers it on the next retry. A lease holds at
most 256 inputs or 1 MiB; past that a WebSocket is closed with code `1013` and
holds everything behind it. A tagged `POST /api/v1/sessions/:id/input` (one
carrying `clientId` and `seq`) instead answers `409 CONFLICT` ("A guarded submit
is in progress on this session; nothing was written") for that span, the lease
plus the held input's drain, rather than a `200` for bytes that were never
written; it keeps its seq, so the browser's durable queue, whose POST fallback
sends one frame at a time and keeps it on a `409`, delivers it on the next retry.
Untagged input to that route is held like a WebSocket frame, because its senders
(the phone accessory keys, the Stop button) never read the answer. A lease holds
at most 256 inputs or 1 MiB; past that a WebSocket is closed with code `1013` and
nothing later from it is applied, so the browser reconnects and resends every
unacknowledged frame in order, and `send-key` answers `409 CONFLICT`.
unacknowledged frame in order, and `send-key` and an untagged raw input answer
`409 CONFLICT`.
Direct auxiliary key operations participate in the same pending-write and input
lease checks. Complete allowlisted focus/mouse reports and empty writes preserve