authorgravatar for mjc@kernel.orgMika Cohen <mjc@kernel.org> 2026-06-11 20:13:34-06:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-12 02:13:34+00:00
logae39bacf0e0a59a36477c9c9c33301908d888ee8
tree6026b0b10a5e300d6d54109a38a6b705e93d8327
parent1ea17026eedf634d208e219cbf78da7d4abe17de
signature Signed by PGP key B5690EEEBB952194

Handle non-resumable pending user input (#2766)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

6 files changed, 157 insertions(+), 7 deletions(-)

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+136
......@@ -1877,6 +1877,142 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
18771877 }),
18781878 );
18791879
1880 it.effect("clears stale pending user input from projected shell summaries", () =>
1881 Effect.gen(function* () {
1882 const projectionPipeline = yield* OrchestrationProjectionPipeline;
1883 const eventStore = yield* OrchestrationEventStore;
1884 const sql = yield* SqlClient.SqlClient;
1885 const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
1886 eventStore
1887 .append(event)
1888 .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent)));
1889
1890 yield* appendAndProject({
1891 type: "project.created",
1892 eventId: EventId.make("evt-stale-user-input-1"),
1893 aggregateKind: "project",
1894 aggregateId: ProjectId.make("project-stale-user-input"),
1895 occurredAt: "2026-02-26T12:35:00.000Z",
1896 commandId: CommandId.make("cmd-stale-user-input-1"),
1897 causationEventId: null,
1898 correlationId: CorrelationId.make("cmd-stale-user-input-1"),
1899 metadata: {},
1900 payload: {
1901 projectId: ProjectId.make("project-stale-user-input"),
1902 title: "Project Stale User Input",
1903 workspaceRoot: "/tmp/project-stale-user-input",
1904 defaultModelSelection: null,
1905 scripts: [],
1906 createdAt: "2026-02-26T12:35:00.000Z",
1907 updatedAt: "2026-02-26T12:35:00.000Z",
1908 },
1909 });
1910
1911 yield* appendAndProject({
1912 type: "thread.created",
1913 eventId: EventId.make("evt-stale-user-input-2"),
1914 aggregateKind: "thread",
1915 aggregateId: ThreadId.make("thread-stale-user-input"),
1916 occurredAt: "2026-02-26T12:35:01.000Z",
1917 commandId: CommandId.make("cmd-stale-user-input-2"),
1918 causationEventId: null,
1919 correlationId: CorrelationId.make("cmd-stale-user-input-2"),
1920 metadata: {},
1921 payload: {
1922 threadId: ThreadId.make("thread-stale-user-input"),
1923 projectId: ProjectId.make("project-stale-user-input"),
1924 title: "Thread Stale User Input",
1925 modelSelection: {
1926 instanceId: ProviderInstanceId.make("codex"),
1927 model: "gpt-5-codex",
1928 },
1929 runtimeMode: "approval-required",
1930 interactionMode: "default",
1931 branch: null,
1932 worktreePath: null,
1933 createdAt: "2026-02-26T12:35:01.000Z",
1934 updatedAt: "2026-02-26T12:35:01.000Z",
1935 },
1936 });
1937
1938 yield* appendAndProject({
1939 type: "thread.activity-appended",
1940 eventId: EventId.make("evt-stale-user-input-3"),
1941 aggregateKind: "thread",
1942 aggregateId: ThreadId.make("thread-stale-user-input"),
1943 occurredAt: "2026-02-26T12:35:02.000Z",
1944 commandId: CommandId.make("cmd-stale-user-input-3"),
1945 causationEventId: null,
1946 correlationId: CorrelationId.make("cmd-stale-user-input-3"),
1947 metadata: {},
1948 payload: {
1949 threadId: ThreadId.make("thread-stale-user-input"),
1950 activity: {
1951 id: EventId.make("activity-stale-user-input-requested"),
1952 tone: "info",
1953 kind: "user-input.requested",
1954 summary: "User input requested",
1955 payload: {
1956 requestId: "user-input-request-stale-1",
1957 questions: [
1958 {
1959 id: "sandbox_mode",
1960 header: "Sandbox",
1961 question: "Which mode should be used?",
1962 options: [
1963 {
1964 label: "workspace-write",
1965 description: "Allow workspace writes only",
1966 },
1967 ],
1968 },
1969 ],
1970 },
1971 turnId: null,
1972 createdAt: "2026-02-26T12:35:02.000Z",
1973 },
1974 },
1975 });
1976
1977 yield* appendAndProject({
1978 type: "thread.activity-appended",
1979 eventId: EventId.make("evt-stale-user-input-4"),
1980 aggregateKind: "thread",
1981 aggregateId: ThreadId.make("thread-stale-user-input"),
1982 occurredAt: "2026-02-26T12:35:03.000Z",
1983 commandId: CommandId.make("cmd-stale-user-input-4"),
1984 causationEventId: null,
1985 correlationId: CorrelationId.make("cmd-stale-user-input-4"),
1986 metadata: {},
1987 payload: {
1988 threadId: ThreadId.make("thread-stale-user-input"),
1989 activity: {
1990 id: EventId.make("activity-stale-user-input-failed"),
1991 tone: "error",
1992 kind: "provider.user-input.respond.failed",
1993 summary: "Provider user input response failed",
1994 payload: {
1995 requestId: "user-input-request-stale-1",
1996 detail:
1997 "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: user-input-request-stale-1",
1998 },
1999 turnId: null,
2000 createdAt: "2026-02-26T12:35:03.000Z",
2001 },
2002 },
2003 });
2004
2005 const threadRows = yield* sql<{
2006 readonly pendingUserInputCount: number;
2007 }>`
2008 SELECT pending_user_input_count AS "pendingUserInputCount"
2009 FROM projection_threads
2010 WHERE thread_id = 'thread-stale-user-input'
2011 `;
2012 assert.deepEqual(threadRows, [{ pendingUserInputCount: 0 }]);
2013 }),
2014 );
2015
18802016 it.effect("ignores non-stale provider approval response failures", () =>
18812017 Effect.gen(function* () {
18822018 const projectionPipeline = yield* OrchestrationProjectionPipeline;
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+3-1
......@@ -165,7 +165,9 @@ function derivePendingUserInputCountFromActivities(
165165 activity.kind === "provider.user-input.respond.failed" &&
166166 detail !== null &&
167167 (detail.includes("stale pending user-input request") ||
168 detail.includes("unknown pending user-input request"))
168 detail.includes("unknown pending user-input request") ||
169 detail.includes("unknown pending user input request") ||
170 detail.includes("unknown pending codex user input request"))
169171 ) {
170172 openRequestIds.delete(requestId);
171173 }
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+2-2
......@@ -1943,7 +1943,7 @@ describe("ProviderCommandReactor", () => {
19431943 expect(resolvedActivity).toBeUndefined();
19441944 });
19451945
1946 it("surfaces stale provider user-input failures without faking user-input resolution", async () => {
1946 it("surfaces non-resumable provider user-input callbacks as stale failures", async () => {
19471947 const harness = await createHarness();
19481948 const now = "2026-01-01T00:00:00.000Z";
19491949 harness.respondToUserInput.mockImplementation(() =>
......@@ -1951,7 +1951,7 @@ describe("ProviderCommandReactor", () => {
19511951 new ProviderAdapterRequestError({
19521952 provider: ProviderDriverKind.make("claudeAgent"),
19531953 method: "item/tool/respondToUserInput",
1954 detail: "Unknown pending user-input request: user-input-request-1",
1954 detail: "Unknown pending Codex user input request: user-input-request-1",
19551955 }),
19561956 ),
19571957 );
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+12-2
......@@ -141,9 +141,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderService
141141function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
142142 const error = findProviderAdapterRequestError(cause);
143143 if (error) {
144 return error.detail.toLowerCase().includes("unknown pending user-input request");
144 const detail = error.detail.toLowerCase();
145 return (
146 detail.includes("unknown pending user-input request") ||
147 detail.includes("unknown pending user input request") ||
148 detail.includes("unknown pending codex user input request")
149 );
145150 }
146 return Cause.pretty(cause).toLowerCase().includes("unknown pending user-input request");
151 const message = Cause.pretty(cause).toLowerCase();
152 return (
153 message.includes("unknown pending user-input request") ||
154 message.includes("unknown pending user input request") ||
155 message.includes("unknown pending codex user input request")
156 );
147157}
148158
149159function stalePendingRequestDetail(
apps/web/src/session-logic.test.ts+1-1
......@@ -300,7 +300,7 @@ describe("derivePendingUserInputs", () => {
300300 payload: {
301301 requestId: "req-user-input-stale-1",
302302 detail:
303 "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.",
303 "Provider adapter request failed (codex) for item/tool/requestUserInput: Unknown pending Codex user input request: req-user-input-stale-1",
304304 },
305305 }),
306306 ];
apps/web/src/session-logic.ts+3-1
......@@ -346,7 +346,9 @@ function isStalePendingRequestFailureDetail(detail: string | undefined): boolean
346346 normalized.includes("stale pending user-input request") ||
347347 normalized.includes("unknown pending approval request") ||
348348 normalized.includes("unknown pending permission request") ||
349 normalized.includes("unknown pending user-input request")
349 normalized.includes("unknown pending user-input request") ||
350 normalized.includes("unknown pending user input request") ||
351 normalized.includes("unknown pending codex user input request")
350352 );
351353}
352354