Skip to content

Commit e724823

Browse files
committed
Handle non-resumable pending user input
1 parent d1e85c4 commit e724823

6 files changed

Lines changed: 157 additions & 7 deletions

File tree

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,142 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
16561656
}),
16571657
);
16581658

1659+
it.effect("clears stale pending user input from projected shell summaries", () =>
1660+
Effect.gen(function* () {
1661+
const projectionPipeline = yield* OrchestrationProjectionPipeline;
1662+
const eventStore = yield* OrchestrationEventStore;
1663+
const sql = yield* SqlClient.SqlClient;
1664+
const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
1665+
eventStore
1666+
.append(event)
1667+
.pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent)));
1668+
1669+
yield* appendAndProject({
1670+
type: "project.created",
1671+
eventId: EventId.make("evt-stale-user-input-1"),
1672+
aggregateKind: "project",
1673+
aggregateId: ProjectId.make("project-stale-user-input"),
1674+
occurredAt: "2026-02-26T12:35:00.000Z",
1675+
commandId: CommandId.make("cmd-stale-user-input-1"),
1676+
causationEventId: null,
1677+
correlationId: CorrelationId.make("cmd-stale-user-input-1"),
1678+
metadata: {},
1679+
payload: {
1680+
projectId: ProjectId.make("project-stale-user-input"),
1681+
title: "Project Stale User Input",
1682+
workspaceRoot: "/tmp/project-stale-user-input",
1683+
defaultModelSelection: null,
1684+
scripts: [],
1685+
createdAt: "2026-02-26T12:35:00.000Z",
1686+
updatedAt: "2026-02-26T12:35:00.000Z",
1687+
},
1688+
});
1689+
1690+
yield* appendAndProject({
1691+
type: "thread.created",
1692+
eventId: EventId.make("evt-stale-user-input-2"),
1693+
aggregateKind: "thread",
1694+
aggregateId: ThreadId.make("thread-stale-user-input"),
1695+
occurredAt: "2026-02-26T12:35:01.000Z",
1696+
commandId: CommandId.make("cmd-stale-user-input-2"),
1697+
causationEventId: null,
1698+
correlationId: CorrelationId.make("cmd-stale-user-input-2"),
1699+
metadata: {},
1700+
payload: {
1701+
threadId: ThreadId.make("thread-stale-user-input"),
1702+
projectId: ProjectId.make("project-stale-user-input"),
1703+
title: "Thread Stale User Input",
1704+
modelSelection: {
1705+
instanceId: ProviderInstanceId.make("codex"),
1706+
model: "gpt-5-codex",
1707+
},
1708+
runtimeMode: "approval-required",
1709+
interactionMode: "default",
1710+
branch: null,
1711+
worktreePath: null,
1712+
createdAt: "2026-02-26T12:35:01.000Z",
1713+
updatedAt: "2026-02-26T12:35:01.000Z",
1714+
},
1715+
});
1716+
1717+
yield* appendAndProject({
1718+
type: "thread.activity-appended",
1719+
eventId: EventId.make("evt-stale-user-input-3"),
1720+
aggregateKind: "thread",
1721+
aggregateId: ThreadId.make("thread-stale-user-input"),
1722+
occurredAt: "2026-02-26T12:35:02.000Z",
1723+
commandId: CommandId.make("cmd-stale-user-input-3"),
1724+
causationEventId: null,
1725+
correlationId: CorrelationId.make("cmd-stale-user-input-3"),
1726+
metadata: {},
1727+
payload: {
1728+
threadId: ThreadId.make("thread-stale-user-input"),
1729+
activity: {
1730+
id: EventId.make("activity-stale-user-input-requested"),
1731+
tone: "info",
1732+
kind: "user-input.requested",
1733+
summary: "User input requested",
1734+
payload: {
1735+
requestId: "user-input-request-stale-1",
1736+
questions: [
1737+
{
1738+
id: "sandbox_mode",
1739+
header: "Sandbox",
1740+
question: "Which mode should be used?",
1741+
options: [
1742+
{
1743+
label: "workspace-write",
1744+
description: "Allow workspace writes only",
1745+
},
1746+
],
1747+
},
1748+
],
1749+
},
1750+
turnId: null,
1751+
createdAt: "2026-02-26T12:35:02.000Z",
1752+
},
1753+
},
1754+
});
1755+
1756+
yield* appendAndProject({
1757+
type: "thread.activity-appended",
1758+
eventId: EventId.make("evt-stale-user-input-4"),
1759+
aggregateKind: "thread",
1760+
aggregateId: ThreadId.make("thread-stale-user-input"),
1761+
occurredAt: "2026-02-26T12:35:03.000Z",
1762+
commandId: CommandId.make("cmd-stale-user-input-4"),
1763+
causationEventId: null,
1764+
correlationId: CorrelationId.make("cmd-stale-user-input-4"),
1765+
metadata: {},
1766+
payload: {
1767+
threadId: ThreadId.make("thread-stale-user-input"),
1768+
activity: {
1769+
id: EventId.make("activity-stale-user-input-failed"),
1770+
tone: "error",
1771+
kind: "provider.user-input.respond.failed",
1772+
summary: "Provider user input response failed",
1773+
payload: {
1774+
requestId: "user-input-request-stale-1",
1775+
detail:
1776+
"Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: user-input-request-stale-1",
1777+
},
1778+
turnId: null,
1779+
createdAt: "2026-02-26T12:35:03.000Z",
1780+
},
1781+
},
1782+
});
1783+
1784+
const threadRows = yield* sql<{
1785+
readonly pendingUserInputCount: number;
1786+
}>`
1787+
SELECT pending_user_input_count AS "pendingUserInputCount"
1788+
FROM projection_threads
1789+
WHERE thread_id = 'thread-stale-user-input'
1790+
`;
1791+
assert.deepEqual(threadRows, [{ pendingUserInputCount: 0 }]);
1792+
}),
1793+
);
1794+
16591795
it.effect("ignores non-stale provider approval response failures", () =>
16601796
Effect.gen(function* () {
16611797
const projectionPipeline = yield* OrchestrationProjectionPipeline;

apps/server/src/orchestration/Layers/ProjectionPipeline.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ function derivePendingUserInputCountFromActivities(
141141
activity.kind === "provider.user-input.respond.failed" &&
142142
detail !== null &&
143143
(detail.includes("stale pending user-input request") ||
144-
detail.includes("unknown pending user-input request"))
144+
detail.includes("unknown pending user-input request") ||
145+
detail.includes("unknown pending user input request") ||
146+
detail.includes("unknown pending codex user input request"))
145147
) {
146148
openRequestIds.delete(requestId);
147149
}

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,15 +1857,15 @@ describe("ProviderCommandReactor", () => {
18571857
expect(resolvedActivity).toBeUndefined();
18581858
});
18591859

1860-
it("surfaces stale provider user-input failures without faking user-input resolution", async () => {
1860+
it("surfaces non-resumable provider user-input callbacks as stale failures", async () => {
18611861
const harness = await createHarness();
18621862
const now = "2026-01-01T00:00:00.000Z";
18631863
harness.respondToUserInput.mockImplementation(() =>
18641864
Effect.fail(
18651865
new ProviderAdapterRequestError({
18661866
provider: ProviderDriverKind.make("claudeAgent"),
18671867
method: "item/tool/respondToUserInput",
1868-
detail: "Unknown pending user-input request: user-input-request-1",
1868+
detail: "Unknown pending Codex user input request: user-input-request-1",
18691869
}),
18701870
),
18711871
);

apps/server/src/orchestration/Layers/ProviderCommandReactor.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderService
142142
function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
143143
const error = findProviderAdapterRequestError(cause);
144144
if (error) {
145-
return error.detail.toLowerCase().includes("unknown pending user-input request");
145+
const detail = error.detail.toLowerCase();
146+
return (
147+
detail.includes("unknown pending user-input request") ||
148+
detail.includes("unknown pending user input request") ||
149+
detail.includes("unknown pending codex user input request")
150+
);
146151
}
147-
return Cause.pretty(cause).toLowerCase().includes("unknown pending user-input request");
152+
const message = Cause.pretty(cause).toLowerCase();
153+
return (
154+
message.includes("unknown pending user-input request") ||
155+
message.includes("unknown pending user input request") ||
156+
message.includes("unknown pending codex user input request")
157+
);
148158
}
149159

150160
function stalePendingRequestDetail(

apps/web/src/session-logic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ describe("derivePendingUserInputs", () => {
297297
payload: {
298298
requestId: "req-user-input-stale-1",
299299
detail:
300-
"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.",
300+
"Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: req-user-input-stale-1",
301301
},
302302
}),
303303
];

apps/web/src/session-logic.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,9 @@ function isStalePendingRequestFailureDetail(detail: string | undefined): boolean
201201
normalized.includes("stale pending user-input request") ||
202202
normalized.includes("unknown pending approval request") ||
203203
normalized.includes("unknown pending permission request") ||
204-
normalized.includes("unknown pending user-input request")
204+
normalized.includes("unknown pending user-input request") ||
205+
normalized.includes("unknown pending user input request") ||
206+
normalized.includes("unknown pending codex user input request")
205207
);
206208
}
207209

0 commit comments

Comments
 (0)