diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index 369eea0f7a0..e85bb5bd54f 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -1656,6 +1656,142 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { }), ); + it.effect("clears stale pending user input from projected shell summaries", () => + Effect.gen(function* () { + const projectionPipeline = yield* OrchestrationProjectionPipeline; + const eventStore = yield* OrchestrationEventStore; + const sql = yield* SqlClient.SqlClient; + const appendAndProject = (event: Parameters[0]) => + eventStore + .append(event) + .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); + + yield* appendAndProject({ + type: "project.created", + eventId: EventId.make("evt-stale-user-input-1"), + aggregateKind: "project", + aggregateId: ProjectId.make("project-stale-user-input"), + occurredAt: "2026-02-26T12:35:00.000Z", + commandId: CommandId.make("cmd-stale-user-input-1"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-stale-user-input-1"), + metadata: {}, + payload: { + projectId: ProjectId.make("project-stale-user-input"), + title: "Project Stale User Input", + workspaceRoot: "/tmp/project-stale-user-input", + defaultModelSelection: null, + scripts: [], + createdAt: "2026-02-26T12:35:00.000Z", + updatedAt: "2026-02-26T12:35:00.000Z", + }, + }); + + yield* appendAndProject({ + type: "thread.created", + eventId: EventId.make("evt-stale-user-input-2"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-stale-user-input"), + occurredAt: "2026-02-26T12:35:01.000Z", + commandId: CommandId.make("cmd-stale-user-input-2"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-stale-user-input-2"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-stale-user-input"), + projectId: ProjectId.make("project-stale-user-input"), + title: "Thread Stale User Input", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + runtimeMode: "approval-required", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt: "2026-02-26T12:35:01.000Z", + updatedAt: "2026-02-26T12:35:01.000Z", + }, + }); + + yield* appendAndProject({ + type: "thread.activity-appended", + eventId: EventId.make("evt-stale-user-input-3"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-stale-user-input"), + occurredAt: "2026-02-26T12:35:02.000Z", + commandId: CommandId.make("cmd-stale-user-input-3"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-stale-user-input-3"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-stale-user-input"), + activity: { + id: EventId.make("activity-stale-user-input-requested"), + tone: "info", + kind: "user-input.requested", + summary: "User input requested", + payload: { + requestId: "user-input-request-stale-1", + questions: [ + { + id: "sandbox_mode", + header: "Sandbox", + question: "Which mode should be used?", + options: [ + { + label: "workspace-write", + description: "Allow workspace writes only", + }, + ], + }, + ], + }, + turnId: null, + createdAt: "2026-02-26T12:35:02.000Z", + }, + }, + }); + + yield* appendAndProject({ + type: "thread.activity-appended", + eventId: EventId.make("evt-stale-user-input-4"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-stale-user-input"), + occurredAt: "2026-02-26T12:35:03.000Z", + commandId: CommandId.make("cmd-stale-user-input-4"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-stale-user-input-4"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-stale-user-input"), + activity: { + id: EventId.make("activity-stale-user-input-failed"), + tone: "error", + kind: "provider.user-input.respond.failed", + summary: "Provider user input response failed", + payload: { + requestId: "user-input-request-stale-1", + detail: + "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: user-input-request-stale-1", + }, + turnId: null, + createdAt: "2026-02-26T12:35:03.000Z", + }, + }, + }); + + const threadRows = yield* sql<{ + readonly pendingUserInputCount: number; + }>` + SELECT pending_user_input_count AS "pendingUserInputCount" + FROM projection_threads + WHERE thread_id = 'thread-stale-user-input' + `; + assert.deepEqual(threadRows, [{ pendingUserInputCount: 0 }]); + }), + ); + it.effect("ignores non-stale provider approval response failures", () => Effect.gen(function* () { const projectionPipeline = yield* OrchestrationProjectionPipeline; diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 1161ff6a7d7..4ffc08e4cd0 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -141,7 +141,9 @@ function derivePendingUserInputCountFromActivities( activity.kind === "provider.user-input.respond.failed" && detail !== null && (detail.includes("stale pending user-input request") || - detail.includes("unknown pending user-input request")) + detail.includes("unknown pending user-input request") || + detail.includes("unknown pending user input request") || + detail.includes("unknown pending codex user input request")) ) { openRequestIds.delete(requestId); } diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 571164fad93..2a2ab082394 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -1857,7 +1857,7 @@ describe("ProviderCommandReactor", () => { expect(resolvedActivity).toBeUndefined(); }); - it("surfaces stale provider user-input failures without faking user-input resolution", async () => { + it("surfaces non-resumable provider user-input callbacks as stale failures", async () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; harness.respondToUserInput.mockImplementation(() => @@ -1865,7 +1865,7 @@ describe("ProviderCommandReactor", () => { new ProviderAdapterRequestError({ provider: ProviderDriverKind.make("claudeAgent"), method: "item/tool/respondToUserInput", - detail: "Unknown pending user-input request: user-input-request-1", + detail: "Unknown pending Codex user input request: user-input-request-1", }), ), ); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 8b71a976808..9453fa9a061 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -142,9 +142,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause): boolean { const error = findProviderAdapterRequestError(cause); if (error) { - return error.detail.toLowerCase().includes("unknown pending user-input request"); + const detail = error.detail.toLowerCase(); + return ( + detail.includes("unknown pending user-input request") || + detail.includes("unknown pending user input request") || + detail.includes("unknown pending codex user input request") + ); } - return Cause.pretty(cause).toLowerCase().includes("unknown pending user-input request"); + const message = Cause.pretty(cause).toLowerCase(); + return ( + message.includes("unknown pending user-input request") || + message.includes("unknown pending user input request") || + message.includes("unknown pending codex user input request") + ); } function stalePendingRequestDetail( diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index baf384d6af2..14c1d115f98 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -297,7 +297,7 @@ describe("derivePendingUserInputs", () => { payload: { requestId: "req-user-input-stale-1", detail: - "Stale pending user-input request: req-user-input-stale-1. Provider callback state does not survive app restarts or recovered sessions. Restart the turn to continue.", + "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: req-user-input-stale-1", }, }), ]; diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index a7767672fa1..5a07575bab5 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -201,7 +201,9 @@ function isStalePendingRequestFailureDetail(detail: string | undefined): boolean normalized.includes("stale pending user-input request") || normalized.includes("unknown pending approval request") || normalized.includes("unknown pending permission request") || - normalized.includes("unknown pending user-input request") + normalized.includes("unknown pending user-input request") || + normalized.includes("unknown pending user input request") || + normalized.includes("unknown pending codex user input request") ); }