mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(api,ws): an input whose delivery fails can be retried instead of being lost
Both input paths recorded the (clientId, seq) pair as applied and acknowledged the frame BEFORE knowing whether the write had landed: the POST route because its mux write is fire-and-forget so the response never waits on a tmux child, the WebSocket handler because it ACKed unconditionally. When the write then failed, the client dropped the frame from its durable queue and the server rejected the retry as a duplicate. The reliable-delivery layer was guaranteeing exactly-once delivery of something that had never been delivered — and `Session.write()` returned void, so a session whose PTY was gone swallowed the data with no signal at all. - `forgetInputSeq()` rolls the bookkeeping back on failure, but only when that seq is still the newest one; a later input has superseded it and must not re-open. - The WebSocket handler withholds its ACK when the write did not land, so the client redelivers. - `Session.write()` reports whether it reached a PTY. Response codes are unchanged, deliberately: a session can legitimately have no PTY yet, and turning that into a failure status would be a contract change of its own. What this does NOT do: remove the root cause. The POST still answers 200 before the mux write is attempted, so a client that treats any 2xx as final cannot learn about that failure. What closes is the narrower window — the write failed AND the ACK never reached the client — plus the whole WebSocket path. Closing the rest would mean awaiting the tmux child inside the request. 9 tests. They drive the HTTP route, not only the Session primitives: with the rollback removed from the route, 2 of them fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,12 +31,22 @@ export class MockSession extends EventEmitter {
|
||||
}
|
||||
|
||||
/** Direct PTY write (used by session.write()) */
|
||||
write(data: string): void {
|
||||
/**
|
||||
* Set to simulate a session whose PTY is gone: both write paths report failure,
|
||||
* which is the state in which input used to disappear silently.
|
||||
*/
|
||||
failWrites = false;
|
||||
|
||||
/** Direct PTY write (used by session.write()). Mirrors the real boolean return. */
|
||||
write(data: string): boolean {
|
||||
if (this.failWrites) return false;
|
||||
this.writeBuffer.push(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Write via mux (used by respawn controller) */
|
||||
async writeViaMux(data: string): Promise<boolean> {
|
||||
if (this.failWrites) return false;
|
||||
this.writeBuffer.push(data);
|
||||
return true;
|
||||
}
|
||||
@@ -44,6 +54,10 @@ export class MockSession extends EventEmitter {
|
||||
/** Exactly-once input dedup — mirrors Session.shouldApplyInput so route tests
|
||||
* exercising the reliable-delivery path behave like production. */
|
||||
private _appliedInputSeq = new Map<string, number>();
|
||||
forgetInputSeq(clientId: string, seq: number): void {
|
||||
if (this._appliedInputSeq.get(clientId) === seq) this._appliedInputSeq.set(clientId, seq - 1);
|
||||
}
|
||||
|
||||
shouldApplyInput(clientId: string, seq: number): boolean {
|
||||
const last = this._appliedInputSeq.get(clientId);
|
||||
if (last !== undefined && seq <= last) return false;
|
||||
|
||||
Reference in New Issue
Block a user