mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 10:59:32 +01:00
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:
1 parent
2aad93914e
commit
6368128f1e
7 files changed
+221
-32
No files matched your search
@@ -2080,15 +2080,18 @@ export function registerSessionRoutes(
|
||||
|
||||
// A guarded submit (agent-staging.ts) holds the input lease for the few
|
||||
// milliseconds of its conditional Enter, and input that arrives meanwhile is held
|
||||
// and written in arrival order after it. This route refuses for that span (the
|
||||
// lease plus the held input's drain) rather than overtake held input. Say so
|
||||
// and written in arrival order after it. A TAGGED request is refused for that span
|
||||
// (the lease plus the held input's drain) rather than overtake held input: say so
|
||||
// instead of answering 200 for bytes that were never written, and give the seq
|
||||
// back so the browser's durable queue (which keeps a frame on any 409, and whose
|
||||
// POST drainer sends one frame at a time) delivers it on its next retry. Nothing
|
||||
// from here to the write below awaits, so the lease cannot be taken in between.
|
||||
if (!duplicate && session.guardedInputHeld === true) {
|
||||
if (tagged) session.forgetInputSeq(clientId as string, seq as number);
|
||||
return reply
|
||||
// POST drainer sends one frame at a time) delivers it on its next retry. An
|
||||
// UNTAGGED request has no retry behind it (the phone accessory keys and the Stop
|
||||
// button's Ctrl+C are fire-and-forget POSTs that never read the answer), so it is
|
||||
// held instead, like a WebSocket frame: `writeViaMux` takes its own turn behind
|
||||
// the lease, and the raw write below is held explicitly. Nothing from here to the
|
||||
// write below awaits, so the lease cannot be taken in between.
|
||||
const refuseGuarded = () =>
|
||||
reply
|
||||
.code(409)
|
||||
.send(
|
||||
createErrorResponse(
|
||||
@@ -2096,6 +2099,9 @@ export function registerSessionRoutes(
|
||||
'A guarded submit is in progress on this session; nothing was written. Retry the input.'
|
||||
)
|
||||
);
|
||||
if (!duplicate && tagged && session.guardedInputHeld === true) {
|
||||
session.forgetInputSeq(clientId as string, seq as number);
|
||||
return refuseGuarded();
|
||||
}
|
||||
|
||||
// Only a waiting request pays for the tmux probe: the browser's plain input path
|
||||
@@ -2208,7 +2214,38 @@ export function registerSessionRoutes(
|
||||
// Same rollback. NOT an error response, deliberately: a session can
|
||||
// legitimately have no PTY yet (created but not started), and callers have
|
||||
// always been able to write to one without a 4xx.
|
||||
delivered = session.write(inputStr, { fromUser: true });
|
||||
//
|
||||
// During a guarded submit (untagged only: a tagged one was refused above) the
|
||||
// write is held and runs in arrival order once the lease is released. The answer
|
||||
// is a 200 once held; a send-and-wait reads `delivered` from the held write. Only
|
||||
// a full hold queue refuses, after releasing the waiter registered above.
|
||||
let heldWrite: Promise<boolean> | null = null;
|
||||
if (session.guardedInputHeld === true) {
|
||||
let settle!: (written: boolean) => void;
|
||||
const written = new Promise<boolean>((resolve) => {
|
||||
settle = resolve;
|
||||
});
|
||||
const held = session.holdForGuardedLease(
|
||||
() => {
|
||||
let ok = false;
|
||||
try {
|
||||
ok = session.write(inputStr, { fromUser: true });
|
||||
} finally {
|
||||
settle(ok);
|
||||
}
|
||||
},
|
||||
inputStr.length,
|
||||
inputStr
|
||||
);
|
||||
if (held === 'full') {
|
||||
abort.abort();
|
||||
stopDeathWatch();
|
||||
return refuseGuarded();
|
||||
}
|
||||
if (held === 'held') heldWrite = written;
|
||||
}
|
||||
if (heldWrite && !waitPromise) return {};
|
||||
delivered = heldWrite ? await heldWrite : session.write(inputStr, { fromUser: true });
|
||||
if (!delivered && tagged) {
|
||||
session.forgetInputSeq(clientId as string, seq as number);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user