diff --git a/apps/server/src/routes/turns.ts b/apps/server/src/routes/turns.ts index 9a729c45..11edf65e 100644 --- a/apps/server/src/routes/turns.ts +++ b/apps/server/src/routes/turns.ts @@ -455,7 +455,7 @@ export async function executeTurn( ...(partialOutput !== undefined ? { output: partialOutput } : {}), error: { code: result.code, - message: indeterminateMessage(result.code), + message: indeterminateMessage(result.code, partialOutput !== undefined), retryable: result.code === "turn_timeout" || result.code === "turn_cancelled", ...(result.diagnostic !== "" ? { diagnostic: result.diagnostic } : {}), }, @@ -585,8 +585,6 @@ export async function handleTurnCancel( } const INDETERMINATE_MESSAGES: Record = { - turn_timeout: "the turn exceeded its time budget and was terminated; partial output was preserved", - turn_cancelled: "the turn was cancelled by the operator; partial output was preserved", turn_engine_unavailable: "the engine process could not be started; check that the engine CLI is installed and reachable", turn_protocol_invalid: "the engine output did not conform to the expected protocol; no automatic retry was attempted", turn_driver_failure: "the driver encountered an internal error; no automatic retry was attempted", @@ -594,7 +592,17 @@ const INDETERMINATE_MESSAGES: Record = { turn_process_exit_1: "the engine process exited with status 1; no automatic retry was attempted", }; -function indeterminateMessage(code: string): string { +function indeterminateMessage(code: string, hasPartialOutput: boolean): string { + if (code === "turn_timeout") { + return hasPartialOutput + ? "the turn exceeded its time budget and was terminated; partial output was preserved" + : "the turn exceeded its time budget and was terminated before any output was received"; + } + if (code === "turn_cancelled") { + return hasPartialOutput + ? "the turn was cancelled by the operator; partial output was preserved" + : "the turn was cancelled by the operator before any output was received"; + } return INDETERMINATE_MESSAGES[code] ?? `the engine process ended unexpectedly (code: ${code}); no automatic retry was attempted`; } diff --git a/apps/server/test/turns-cancel.test.ts b/apps/server/test/turns-cancel.test.ts index a74b5fda..e2933515 100644 --- a/apps/server/test/turns-cancel.test.ts +++ b/apps/server/test/turns-cancel.test.ts @@ -61,7 +61,7 @@ test("POST /turns/cancel aborts the in-flight turn as indeterminate/turn_cancell assert.equal(record.status, "indeterminate"); assert.deepEqual(record.error, { code: "turn_cancelled", - message: "the turn was cancelled by the operator; partial output was preserved", + message: "the turn was cancelled by the operator before any output was received", retryable: true, }); diff --git a/apps/server/test/turns.test.ts b/apps/server/test/turns.test.ts index 0eccb849..056eede1 100644 --- a/apps/server/test/turns.test.ts +++ b/apps/server/test/turns.test.ts @@ -815,6 +815,30 @@ test("turn_timeout message names the timeout and preserved output (AC-003)", asy } }); +test("turn_timeout does not claim to preserve output when no delta arrived", async () => { + const turnDriver = new FakeTurnDriver({ + status: "indeterminate", + events: [{ type: "run.started", runId: "run-1", timestamp: "2026-08-24T00:00:00.000Z" }], + diagnostic: "", + code: "turn_timeout", + }); + const server = await startTestServer(undefined, turnDriver); + const workspace = await copyExampleWorkspace(); + try { + await openWorkspace(server.baseUrl, server.token, workspace); + const response = await api(server.baseUrl, "/turns", { + method: "POST", + token: server.token, + body: { positionId: "repo-owner", input: "hello", engine: "qoder" }, + }); + const record = response.body as { error: { message: string }; output?: string }; + assert.match(record.error.message, /before any output was received/i); + assert.equal(record.output, undefined); + } finally { + await server.close(); + } +}); + test("diagnostic from engine stderr reaches the turn record (AC-004)", async () => { const turnDriver = new FakeTurnDriver({ status: "indeterminate",