mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(wait): a retried send-and-wait finds a screen-read dialog
`GET .../wait` answers `blocked` at once for a codex pane showing a dialog, because a screen-read dialog is a level the server is still reading. The duplicate branch of `POST .../input` (a tagged retry after a dropped connection, answered from current state) still used the plain `currentSignalFor()`, so a retry with `wait: "blocked"` on a pane the first delivery had already blocked waited out its whole timeout for an edge that had passed. It now uses `currentSignalForWait()` like the GET route; a caller that asked only for `idle` still gets `idle`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1c1658d039
commit
3ae6cde2d6
2 files changed
+40
-3
No files matched your search
@@ -685,7 +685,9 @@ function currentSignalFor(session: { pid: number | null; status: SessionStatus }
|
||||
* history, but this one is a level the server is still looking at, so a caller that
|
||||
* asks after the dialog appeared is answered at once instead of waiting out its timeout
|
||||
* for an edge that already passed. Only when `blocked` was asked for: such a pane is also
|
||||
* `idle`, and a caller that asked for `idle` alone must keep getting `idle`.
|
||||
* `idle`, and a caller that asked for `idle` alone must keep getting `idle`. Used by
|
||||
* `GET .../wait` and by a DUPLICATE send-and-wait, every place a wait answers from the
|
||||
* session's current state.
|
||||
*/
|
||||
function currentSignalForWait(
|
||||
session: { pid: number | null; status: SessionStatus; screenBlocked?: boolean },
|
||||
@@ -2017,7 +2019,9 @@ export function registerSessionRoutes(
|
||||
// A DUPLICATE has no new turn coming, so it answers from the current state
|
||||
// instead of blocking for a transition that already happened.
|
||||
requireTransition: !duplicate,
|
||||
currentSignal: duplicate ? currentSignalFor(session, workerDead) : undefined,
|
||||
// The `...ForWait` variant, as on GET .../wait: a screen-read dialog the first
|
||||
// delivery raised is a level, and a retry asking for `blocked` must find it.
|
||||
currentSignal: duplicate ? currentSignalForWait(session, workerDead, until) : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof WaitCapacityError) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||
import { ApiErrorCode, httpStatusForErrorCode } from '../../src/types.js';
|
||||
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
||||
import { sessionWaits } from '../../src/web/session-wait-registry.js';
|
||||
import { MAX_WAIT_MS } from '../../src/config/agent-wait.js';
|
||||
import { MAX_WAIT_MS, MIN_WAIT_MS } from '../../src/config/agent-wait.js';
|
||||
|
||||
// Distinct per file on purpose: the three wait suites share the process-wide
|
||||
// `sessionWaits` singleton, so a common id let one file's leftover waiter be counted
|
||||
@@ -315,6 +315,39 @@ describe('POST /api/sessions/:id/input with wait', () => {
|
||||
expect(body.data.status).toBe('busy');
|
||||
});
|
||||
|
||||
it('a duplicate on a codex pane showing a dialog answers blocked, as GET .../wait does', async () => {
|
||||
// A screen-read dialog is a level, not an edge: the original delivery may have raised
|
||||
// it before the caller's connection dropped, and the retry must not wait out its
|
||||
// timeout for an edge that already passed.
|
||||
const { app, ctx } = await harness();
|
||||
const session = ctx.sessions.get(SESSION_ID)! as unknown as {
|
||||
mode: string;
|
||||
status: string;
|
||||
screenBlocked?: boolean;
|
||||
writeBuffer: string[];
|
||||
};
|
||||
session.mode = 'codex';
|
||||
await send(app, { input: 'first', clientId: 'c-codex', seq: 1 });
|
||||
session.status = 'idle';
|
||||
session.screenBlocked = true;
|
||||
|
||||
const replay = await send(app, {
|
||||
input: 'first',
|
||||
clientId: 'c-codex',
|
||||
seq: 1,
|
||||
wait: 'blocked',
|
||||
waitTimeout: MIN_WAIT_MS,
|
||||
});
|
||||
const body = replay.json();
|
||||
|
||||
expect(body.data.duplicate).toBe(true);
|
||||
expect(body.data.wait).toMatchObject({ signal: 'blocked', immediate: true, timedOut: false });
|
||||
|
||||
// ...and only to a caller that asked for it: such a pane is idle too.
|
||||
const idle = await send(app, { input: 'first', clientId: 'c-codex', seq: 1, wait: 'idle' });
|
||||
expect(idle.json().data.wait).toMatchObject({ signal: 'idle', immediate: true });
|
||||
});
|
||||
|
||||
it('gives the dedup seq back when a full waiter pool rejects the request', async () => {
|
||||
// Otherwise the caller's retry is refused as a duplicate and the input vanishes.
|
||||
const { app, ctx } = await harness();
|
||||
|
||||
Reference in new issue
Block a user