Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof eventStore.append>[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;
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Comment thread
mjc marked this conversation as resolved.
) {
openRequestIds.delete(requestId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1857,15 +1857,15 @@ 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(() =>
Effect.fail(
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",
}),
),
);
Expand Down
14 changes: 12 additions & 2 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderService
function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): 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")
);
Comment on lines +145 to +157
Copy link
Copy Markdown
Author

@mjc mjc May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a helper feels like the wrong thing, these really should not be string matching so much... there's not enough use of types in this app and state drifts constantly as a result

}

function stalePendingRequestDetail(
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}),
];
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
}

Expand Down
Loading