| author | |
| committer | |
| log | ae39bacf0e0a59a36477c9c9c33301908d888ee8 |
| tree | 6026b0b10a5e300d6d54109a38a6b705e93d8327 |
| parent | 1ea17026eedf634d208e219cbf78da7d4abe17de |
| signature | Signed by PGP key B5690EEEBB952194 |
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) => { | ... | @@ -1877,6 +1877,142 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { |
| 1877 | }), | 1877 | }), |
| 1878 | ); | 1878 | ); |
| 1879 | 1879 | ||
| 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 | |||
| 1880 | it.effect("ignores non-stale provider approval response failures", () => | 2016 | it.effect("ignores non-stale provider approval response failures", () => |
| 1881 | Effect.gen(function* () { | 2017 | Effect.gen(function* () { |
| 1882 | const projectionPipeline = yield* OrchestrationProjectionPipeline; | 2018 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+3-1| ... | @@ -165,7 +165,9 @@ function derivePendingUserInputCountFromActivities( | ... | @@ -165,7 +165,9 @@ function derivePendingUserInputCountFromActivities( |
| 165 | activity.kind === "provider.user-input.respond.failed" && | 165 | activity.kind === "provider.user-input.respond.failed" && |
| 166 | detail !== null && | 166 | detail !== null && |
| 167 | (detail.includes("stale pending user-input request") || | 167 | (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")) | ||
| 169 | ) { | 171 | ) { |
| 170 | openRequestIds.delete(requestId); | 172 | openRequestIds.delete(requestId); |
| 171 | } | 173 | } |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+2-2| ... | @@ -1943,7 +1943,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -1943,7 +1943,7 @@ describe("ProviderCommandReactor", () => { |
| 1943 | expect(resolvedActivity).toBeUndefined(); | 1943 | expect(resolvedActivity).toBeUndefined(); |
| 1944 | }); | 1944 | }); |
| 1945 | 1945 | ||
| 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 () => { |
| 1947 | const harness = await createHarness(); | 1947 | const harness = await createHarness(); |
| 1948 | const now = "2026-01-01T00:00:00.000Z"; | 1948 | const now = "2026-01-01T00:00:00.000Z"; |
| 1949 | harness.respondToUserInput.mockImplementation(() => | 1949 | harness.respondToUserInput.mockImplementation(() => |
| ... | @@ -1951,7 +1951,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -1951,7 +1951,7 @@ describe("ProviderCommandReactor", () => { |
| 1951 | new ProviderAdapterRequestError({ | 1951 | new ProviderAdapterRequestError({ |
| 1952 | provider: ProviderDriverKind.make("claudeAgent"), | 1952 | provider: ProviderDriverKind.make("claudeAgent"), |
| 1953 | method: "item/tool/respondToUserInput", | 1953 | 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", |
| 1955 | }), | 1955 | }), |
| 1956 | ), | 1956 | ), |
| 1957 | ); | 1957 | ); |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+12-2| ... | @@ -141,9 +141,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderService | ... | @@ -141,9 +141,19 @@ function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderService |
| 141 | function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { | 141 | function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { |
| 142 | const error = findProviderAdapterRequestError(cause); | 142 | const error = findProviderAdapterRequestError(cause); |
| 143 | if (error) { | 143 | 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 | ); | ||
| 145 | } | 150 | } |
| 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 | ); | ||
| 147 | } | 157 | } |
| 148 | 158 | ||
| 149 | function stalePendingRequestDetail( | 159 | function stalePendingRequestDetail( |
apps/web/src/session-logic.test.ts+1-1| ... | @@ -300,7 +300,7 @@ describe("derivePendingUserInputs", () => { | ... | @@ -300,7 +300,7 @@ describe("derivePendingUserInputs", () => { |
| 300 | payload: { | 300 | payload: { |
| 301 | requestId: "req-user-input-stale-1", | 301 | requestId: "req-user-input-stale-1", |
| 302 | detail: | 302 | 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", |
| 304 | }, | 304 | }, |
| 305 | }), | 305 | }), |
| 306 | ]; | 306 | ]; |
apps/web/src/session-logic.ts+3-1| ... | @@ -346,7 +346,9 @@ function isStalePendingRequestFailureDetail(detail: string | undefined): boolean | ... | @@ -346,7 +346,9 @@ function isStalePendingRequestFailureDetail(detail: string | undefined): boolean |
| 346 | normalized.includes("stale pending user-input request") || | 346 | normalized.includes("stale pending user-input request") || |
| 347 | normalized.includes("unknown pending approval request") || | 347 | normalized.includes("unknown pending approval request") || |
| 348 | normalized.includes("unknown pending permission request") || | 348 | 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") | ||
| 350 | ); | 352 | ); |
| 351 | } | 353 | } |
| 352 | 354 |