mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(agent): hold input during a guarded submit instead of refusing it
The input lease is held across the awaited tmux Enter, so WebSocket frames keep arriving while it is held. A tagged frame (seq N) was refused and its seq rolled back, but the browser's WebSocket path does not wait for ACKs: seq N+1 landed after the lease and moved the watermark past N, and the redelivered N was then dropped as a duplicate. The keystroke was lost. Input that arrives during the lease is now held and written in arrival order when the lease is released, whatever the submit's outcome. A WebSocket frame is applied and ACKed only once written; managed writes (writeViaMux) and named keys (send-key) wait for their turn the same way, and a held write that is still being sent holds everything behind it. A keystroke held before the guard runs still moves the input revision, so it still invalidates the Enter. The hold is capped per lease (256 inputs, 1 MiB): past it the WebSocket is closed with 1013 and nothing later from it is applied, so the browser resends every unACKed frame in order, and send-key answers 409 CONFLICT instead of logging a tmux failure as INTERNAL_ERROR. POST .../input keeps its 409 for the lease plus the held input's drain; its browser drainer is serialized. Docs: correct the api-reference paragraph that said these keystrokes are redelivered, and describe the capture fallback and the per-scope cursor cap from the previous two commits in api-reference, architecture-invariants and the changeset.
This commit is contained in:
1 parent
ecaddb40ae
commit
2aad93914e
9 files changed
+727
-100
No files matched your search
+22
-12
@@ -427,8 +427,9 @@ turn that never started. Verified live; this is the most common silent failure o
|
||||
this endpoint.
|
||||
|
||||
Any input, waiting or not, answers `409 CONFLICT` with nothing written while an
|
||||
experimental guarded submit holds the session's input lease (a few milliseconds;
|
||||
see [guarded input](#experimental-bounded-agent-inspection-and-guarded-input)).
|
||||
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.
|
||||
|
||||
A **plain prompt** (printable text followed by exactly one `\r`, nothing else) is
|
||||
@@ -659,9 +660,10 @@ thread, task completion or authorization.
|
||||
|
||||
Opaque server-side cursors bind caller, session, pane generation, conversation,
|
||||
opened-file identity, byte offset and bounded content anchors. Terminal resizes
|
||||
do not change this inspection scope. At most 1024 live
|
||||
cursors are retained for 15 minutes; restart, eviction or expiry requires a fresh
|
||||
tail baseline. Replacement, truncation, detected rewrites, reassociation, foreign
|
||||
do not change this inspection scope. Each owner and session keeps its newest 8
|
||||
cursors (a retry of the last call still works; older ones are retired), at most
|
||||
1024 live cursors are retained in all, each for 15 minutes; restart, eviction or
|
||||
expiry requires a fresh tail baseline. Replacement, truncation, detected rewrites, reassociation, foreign
|
||||
cursors or loss of the attributed source return `409 CONFLICT`. No transcript
|
||||
filesystem path is returned as cursor/identity metadata; evidence retains the
|
||||
owner's native record content, which can itself contain paths.
|
||||
@@ -708,13 +710,21 @@ may already be in the composer: inspect the pane and follow the stranded-draft
|
||||
recovery (Ctrl+U) in the skill reference before staging again.
|
||||
|
||||
While a guarded submit holds the input lease (the few milliseconds of its
|
||||
conditional Enter), every other Codeman write path refuses, and
|
||||
`POST /api/v1/sessions/:id/input` answers `409 CONFLICT` ("A guarded submit is in
|
||||
progress on this session; nothing was written") instead of a `200` for bytes
|
||||
that were never written. A tagged `clientId`/`seq` frame keeps its seq, so the
|
||||
browser's durable queue, which keeps a frame on a `409`, delivers it on the next
|
||||
retry. WebSocket keystrokes in that window are left unacknowledged and redelivered
|
||||
the same way.
|
||||
conditional Enter), input that reaches the session is held, not refused, and is
|
||||
written in arrival order as soon as the lease is released, whatever the submit's
|
||||
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
|
||||
nothing later from it is applied, so the browser reconnects and resends every
|
||||
unacknowledged frame in order, and `send-key` answers `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
|
||||
|
||||
@@ -1281,6 +1281,9 @@ candidate-metadata budgets, bounded path probes before canonical reads, and
|
||||
complete UTF8 JSONL records. A full page advances its cursor without claiming a
|
||||
blocked record. Opaque cursors bind caller/session/lifecycle/file identity; pane
|
||||
geometry is excluded so changing devices does not invalidate transcript paging.
|
||||
Cursors are capped per owner and session (`INSPECT_CURSORS_PER_SCOPE`, the newest 8,
|
||||
under the global 1,024), so one polling agent retires its own oldest cursors instead
|
||||
of evicting every other caller's from the shared FIFO.
|
||||
|
||||
⚠️ Codex never prunes `~/.codex/sessions`, so attribution must not cost a store
|
||||
walk per call. The first call walks (negative-filter prefixes read 1 KiB first,
|
||||
@@ -1317,9 +1320,18 @@ and the first capture is often from before them; another writer or a lifecycle c
|
||||
ends the wait at once, and every refusal after typing says the message may be in the
|
||||
composer. `capturePaneBuffer()` reads the visible frame and its caret in ONE tmux call
|
||||
(`capture-pane \; display-message`, split by `splitFrameAndCursor()`), falling back to
|
||||
the two calls on any doubt; keep the poll anyway. While a guarded submit holds the
|
||||
lease, `POST .../input` answers `409` (seq returned) instead of a `200` for unwritten
|
||||
bytes. ⚠️ Submit waits `SUBMIT_QUIET_MS` (250) after the staged keys before its
|
||||
the two calls only for output it cannot split; a combined call that THREW goes to the
|
||||
capture's catch and is never rerun (a hung tmux would block the event loop for a second
|
||||
timeout on every `GET .../terminal`). Keep the poll anyway. ⚠️ Input that arrives
|
||||
while a guarded submit holds the lease is HELD, never refused (`holdForGuardedLease()`,
|
||||
`_inGuardedTurn()`), and written in arrival order when the lease is released: the
|
||||
browser's WebSocket path does not wait for ACKs, so a refused seq N was overtaken by
|
||||
N+1 and its redelivery dropped as a duplicate. A WS frame is ACKed only once written;
|
||||
a held async write (mux write, named key) holds everything behind it until it settles
|
||||
(`guardedInputHeld`); a full hold queue (256 inputs / 1 MiB) closes the socket with
|
||||
`1013` and applies nothing later from it, so the browser resends in order. `POST
|
||||
.../input` alone answers `409` (seq returned) for the lease plus the drain, since its
|
||||
browser drainer is serialized and retries. ⚠️ Submit waits `SUBMIT_QUIET_MS` (250) after the staged keys before its
|
||||
checks: stage types plain keys, codex's paste-burst detector reads them as a paste, and
|
||||
an Enter within 120 ms of one becomes a newline while submit would report it
|
||||
delivered. Stage cancels older verifier retries and sends no Enter. Receipts contain hashes and
|
||||
@@ -1330,7 +1342,8 @@ Submission additionally binds input and geometry revisions and rechecks the
|
||||
exact composer/caret at the conditional Enter boundary. The route must retain
|
||||
the Session snapshot's staging generation rather than replacing it with the
|
||||
inspection generation. Managed input and auxiliary named keys participate in the
|
||||
same pending/lease boundary. Complete focus/mouse reports and empty writes are
|
||||
same pending/lease boundary: they wait for the lease, and only a full hold queue
|
||||
refuses them (`GuardedInputBusyError`, which `send-key` answers as `409 CONFLICT`). Complete focus/mouse reports and empty writes are
|
||||
not edits. Never retry ambiguous Enter delivery or clear an unverified composer.
|
||||
The ordinary SubmitVerifier uses the shared write boundary, so each re-press
|
||||
updates `lastSubmitAt` just like any other Enter.
|
||||
Reference in new issue
Block a user