| author | |
| committer | |
| log | 7b71e234e0c7d9e937cad940a6a2474d060ca3c6 |
| tree | ff0d12284723a82a1b830151b25114b48a1b1aac |
| parent | 77f67d1766e6b2afa7e3b87a6da52cc652ceb8bf |
- migrate fixtures/tests/adapters to canonical provider runtime event payloads
- route checkpoint capture off `turn.completed` only and drop legacy `checkpoint.captured` path
- stop faking approval resolution when provider reports stale/unknown pending requests20 files changed, 765 insertions(+), 497 deletions(-)
AGENTS.md+1-1| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | # CLAUDE.md | 1 | # AGENTS.md |
| 2 | 2 | ||
| 3 | ## Task Completion Requirements | 3 | ## Task Completion Requirements |
| 4 | 4 |
apps/server/integration/TestProviderAdapter.integration.ts+165-11| ... | @@ -5,11 +5,14 @@ import { | ... | @@ -5,11 +5,14 @@ import { |
| 5 | EventId, | 5 | EventId, |
| 6 | ProviderApprovalDecision, | 6 | ProviderApprovalDecision, |
| 7 | ProviderRuntimeEvent, | 7 | ProviderRuntimeEvent, |
| 8 | RuntimeSessionId, | ||
| 8 | ProviderSession, | 9 | ProviderSession, |
| 9 | ProviderSessionId, | 10 | ProviderSessionId, |
| 10 | ProviderThreadId, | 11 | ProviderThreadId, |
| 11 | ProviderTurnId, | 12 | ProviderTurnId, |
| 12 | ProviderTurnStartResult, | 13 | ProviderTurnStartResult, |
| 14 | ThreadId, | ||
| 15 | TurnId, | ||
| 13 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 14 | import { Effect, Queue, Stream } from "effect"; | 17 | import { Effect, Queue, Stream } from "effect"; |
| 15 | 18 | ||
| ... | @@ -25,13 +28,30 @@ import type { | ... | @@ -25,13 +28,30 @@ import type { |
| 25 | } from "../src/provider/Services/ProviderAdapter.ts"; | 28 | } from "../src/provider/Services/ProviderAdapter.ts"; |
| 26 | 29 | ||
| 27 | export interface TestTurnResponse { | 30 | export interface TestTurnResponse { |
| 28 | readonly events: ReadonlyArray<ProviderRuntimeEvent>; | 31 | readonly events: ReadonlyArray<FixtureProviderRuntimeEvent>; |
| 29 | readonly mutateWorkspace?: (input: { | 32 | readonly mutateWorkspace?: (input: { |
| 30 | readonly cwd: string; | 33 | readonly cwd: string; |
| 31 | readonly turnCount: number; | 34 | readonly turnCount: number; |
| 32 | }) => Effect.Effect<void, never>; | 35 | }) => Effect.Effect<void, never>; |
| 33 | } | 36 | } |
| 34 | 37 | ||
| 38 | export type FixtureProviderRuntimeEvent = { | ||
| 39 | readonly type: string; | ||
| 40 | readonly eventId: EventId; | ||
| 41 | readonly provider: "codex" | "claudeCode" | "cursor"; | ||
| 42 | readonly sessionId: string; | ||
| 43 | readonly createdAt: string; | ||
| 44 | readonly threadId?: string | undefined; | ||
| 45 | readonly turnId?: string | undefined; | ||
| 46 | readonly itemId?: string | undefined; | ||
| 47 | readonly requestId?: string | undefined; | ||
| 48 | readonly payload?: unknown | undefined; | ||
| 49 | readonly [key: string]: unknown; | ||
| 50 | }; | ||
| 51 | |||
| 52 | // Temporary alias while fixtures migrate to the new name. | ||
| 53 | export type LegacyProviderRuntimeEvent = FixtureProviderRuntimeEvent; | ||
| 54 | |||
| 35 | interface SessionState { | 55 | interface SessionState { |
| 36 | readonly session: ProviderSession; | 56 | readonly session: ProviderSession; |
| 37 | snapshot: ProviderThreadSnapshot; | 57 | snapshot: ProviderThreadSnapshot; |
| ... | @@ -40,6 +60,126 @@ interface SessionState { | ... | @@ -40,6 +60,126 @@ interface SessionState { |
| 40 | readonly rollbackCalls: Array<number>; | 60 | readonly rollbackCalls: Array<number>; |
| 41 | } | 61 | } |
| 42 | 62 | ||
| 63 | function isRecord(value: unknown): value is Record<string, unknown> { | ||
| 64 | return typeof value === "object" && value !== null; | ||
| 65 | } | ||
| 66 | |||
| 67 | function normalizeTurnState(value: unknown): "completed" | "failed" | "interrupted" | "cancelled" { | ||
| 68 | if ( | ||
| 69 | value === "completed" || | ||
| 70 | value === "failed" || | ||
| 71 | value === "interrupted" || | ||
| 72 | value === "cancelled" | ||
| 73 | ) { | ||
| 74 | return value; | ||
| 75 | } | ||
| 76 | return "completed"; | ||
| 77 | } | ||
| 78 | |||
| 79 | function mapRequestType(requestKind: unknown): | ||
| 80 | | "command_execution_approval" | ||
| 81 | | "file_change_approval" | ||
| 82 | | "unknown" { | ||
| 83 | if (requestKind === "command") { | ||
| 84 | return "command_execution_approval"; | ||
| 85 | } | ||
| 86 | if (requestKind === "file-change") { | ||
| 87 | return "file_change_approval"; | ||
| 88 | } | ||
| 89 | return "unknown"; | ||
| 90 | } | ||
| 91 | |||
| 92 | function mapItemType(toolKind: unknown): "command_execution" | "file_change" | "unknown" { | ||
| 93 | if (toolKind === "command") { | ||
| 94 | return "command_execution"; | ||
| 95 | } | ||
| 96 | if (toolKind === "file-change") { | ||
| 97 | return "file_change"; | ||
| 98 | } | ||
| 99 | return "unknown"; | ||
| 100 | } | ||
| 101 | |||
| 102 | function normalizeFixtureEvent(rawEvent: Record<string, unknown>): ProviderRuntimeEvent { | ||
| 103 | const type = typeof rawEvent.type === "string" ? rawEvent.type : ""; | ||
| 104 | switch (type) { | ||
| 105 | case "turn.started": | ||
| 106 | return { | ||
| 107 | ...rawEvent, | ||
| 108 | type: "turn.started", | ||
| 109 | payload: isRecord(rawEvent.payload) ? rawEvent.payload : {}, | ||
| 110 | } as ProviderRuntimeEvent; | ||
| 111 | case "turn.completed": | ||
| 112 | return { | ||
| 113 | ...rawEvent, | ||
| 114 | type: "turn.completed", | ||
| 115 | payload: isRecord(rawEvent.payload) | ||
| 116 | ? rawEvent.payload | ||
| 117 | : { | ||
| 118 | state: normalizeTurnState(rawEvent.status), | ||
| 119 | }, | ||
| 120 | } as ProviderRuntimeEvent; | ||
| 121 | case "message.delta": | ||
| 122 | return { | ||
| 123 | ...rawEvent, | ||
| 124 | type: "content.delta", | ||
| 125 | payload: { | ||
| 126 | streamKind: "assistant_text", | ||
| 127 | delta: typeof rawEvent.delta === "string" ? rawEvent.delta : "", | ||
| 128 | }, | ||
| 129 | } as ProviderRuntimeEvent; | ||
| 130 | case "message.completed": | ||
| 131 | return { | ||
| 132 | ...rawEvent, | ||
| 133 | type: "item.completed", | ||
| 134 | payload: { | ||
| 135 | itemType: "assistant_message", | ||
| 136 | ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), | ||
| 137 | }, | ||
| 138 | } as ProviderRuntimeEvent; | ||
| 139 | case "tool.started": | ||
| 140 | return { | ||
| 141 | ...rawEvent, | ||
| 142 | type: "item.started", | ||
| 143 | payload: { | ||
| 144 | itemType: mapItemType(rawEvent.toolKind), | ||
| 145 | ...(typeof rawEvent.title === "string" ? { title: rawEvent.title } : {}), | ||
| 146 | ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), | ||
| 147 | }, | ||
| 148 | } as ProviderRuntimeEvent; | ||
| 149 | case "tool.completed": | ||
| 150 | return { | ||
| 151 | ...rawEvent, | ||
| 152 | type: "item.completed", | ||
| 153 | payload: { | ||
| 154 | itemType: mapItemType(rawEvent.toolKind), | ||
| 155 | status: "completed", | ||
| 156 | ...(typeof rawEvent.title === "string" ? { title: rawEvent.title } : {}), | ||
| 157 | ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), | ||
| 158 | }, | ||
| 159 | } as ProviderRuntimeEvent; | ||
| 160 | case "approval.requested": | ||
| 161 | return { | ||
| 162 | ...rawEvent, | ||
| 163 | type: "request.opened", | ||
| 164 | payload: { | ||
| 165 | requestType: mapRequestType(rawEvent.requestKind), | ||
| 166 | ...(typeof rawEvent.detail === "string" ? { detail: rawEvent.detail } : {}), | ||
| 167 | }, | ||
| 168 | } as ProviderRuntimeEvent; | ||
| 169 | case "approval.resolved": | ||
| 170 | return { | ||
| 171 | ...rawEvent, | ||
| 172 | type: "request.resolved", | ||
| 173 | payload: { | ||
| 174 | requestType: mapRequestType(rawEvent.requestKind), | ||
| 175 | ...(typeof rawEvent.decision === "string" ? { decision: rawEvent.decision } : {}), | ||
| 176 | }, | ||
| 177 | } as ProviderRuntimeEvent; | ||
| 178 | default: | ||
| 179 | return rawEvent as ProviderRuntimeEvent; | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 43 | export interface TestProviderAdapterHarness { | 183 | export interface TestProviderAdapterHarness { |
| 44 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; | 184 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; |
| 45 | readonly provider: "codex" | "claudeCode"; | 185 | readonly provider: "codex" | "claudeCode"; |
| ... | @@ -172,19 +312,28 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -172,19 +312,28 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 172 | ...(fixtureEvent as Record<string, unknown>), | 312 | ...(fixtureEvent as Record<string, unknown>), |
| 173 | eventId: randomUUID(), | 313 | eventId: randomUUID(), |
| 174 | provider, | 314 | provider, |
| 175 | sessionId: input.sessionId, | 315 | sessionId: RuntimeSessionId.makeUnsafe(input.sessionId), |
| 176 | createdAt: nowIso(), | 316 | createdAt: nowIso(), |
| 177 | }; | 317 | }; |
| 178 | if (Object.hasOwn(rawEvent, "threadId")) { | 318 | if (Object.hasOwn(rawEvent, "threadId")) { |
| 179 | rawEvent.threadId = state.snapshot.threadId; | 319 | rawEvent.threadId = ThreadId.makeUnsafe(state.snapshot.threadId); |
| 180 | } | 320 | } |
| 181 | if (Object.hasOwn(rawEvent, "turnId")) { | 321 | if (Object.hasOwn(rawEvent, "turnId")) { |
| 182 | rawEvent.turnId = turnId; | 322 | rawEvent.turnId = TurnId.makeUnsafe(turnId); |
| 183 | } | 323 | } |
| 184 | 324 | ||
| 185 | const runtimeEvent = rawEvent as ProviderRuntimeEvent; | 325 | const runtimeEvent = normalizeFixtureEvent(rawEvent); |
| 186 | if (runtimeEvent.type === "message.delta") { | 326 | const runtimeType = (runtimeEvent as { type: string }).type; |
| 187 | assistantDeltas.push(runtimeEvent.delta); | 327 | if (runtimeType === "content.delta") { |
| 328 | const payload = runtimeEvent.payload as { delta?: unknown } | undefined; | ||
| 329 | if (typeof payload?.delta === "string") { | ||
| 330 | assistantDeltas.push(payload.delta); | ||
| 331 | } | ||
| 332 | } else if (runtimeType === "message.delta") { | ||
| 333 | const legacyDelta = (runtimeEvent as { delta?: unknown }).delta; | ||
| 334 | if (typeof legacyDelta === "string") { | ||
| 335 | assistantDeltas.push(legacyDelta); | ||
| 336 | } | ||
| 188 | } | 337 | } |
| 189 | if (runtimeEvent.type === "turn.completed") { | 338 | if (runtimeEvent.type === "turn.completed") { |
| 190 | deferredTurnCompletedEvents.push(runtimeEvent); | 339 | deferredTurnCompletedEvents.push(runtimeEvent); |
| ... | @@ -223,11 +372,13 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -223,11 +372,13 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 223 | type: "turn.completed", | 372 | type: "turn.completed", |
| 224 | eventId: EventId.makeUnsafe(randomUUID()), | 373 | eventId: EventId.makeUnsafe(randomUUID()), |
| 225 | provider, | 374 | provider, |
| 226 | sessionId: input.sessionId, | 375 | sessionId: RuntimeSessionId.makeUnsafe(input.sessionId), |
| 227 | createdAt: nowIso(), | 376 | createdAt: nowIso(), |
| 228 | threadId: state.snapshot.threadId, | 377 | threadId: ThreadId.makeUnsafe(state.snapshot.threadId), |
| 229 | turnId, | 378 | turnId: TurnId.makeUnsafe(turnId), |
| 230 | status: "completed", | 379 | payload: { |
| 380 | state: "completed", | ||
| 381 | }, | ||
| 231 | }); | 382 | }); |
| 232 | } else { | 383 | } else { |
| 233 | for (const completedEvent of deferredTurnCompletedEvents) { | 384 | for (const completedEvent of deferredTurnCompletedEvents) { |
| ... | @@ -325,6 +476,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -325,6 +476,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 325 | 476 | ||
| 326 | const adapter: ProviderAdapterShape<ProviderAdapterError> = { | 477 | const adapter: ProviderAdapterShape<ProviderAdapterError> = { |
| 327 | provider, | 478 | provider, |
| 479 | capabilities: { | ||
| 480 | sessionModelSwitch: "in-session", | ||
| 481 | }, | ||
| 328 | startSession, | 482 | startSession, |
| 329 | sendTurn, | 483 | sendTurn, |
| 330 | interruptTurn, | 484 | interruptTurn, |
apps/server/integration/fixtures/providerRuntime.ts+65-41| ... | @@ -1,22 +1,16 @@ | ... | @@ -1,22 +1,16 @@ |
| 1 | import { | 1 | import { EventId, RuntimeRequestId } from "@t3tools/contracts"; |
| 2 | ApprovalRequestId, | 2 | import type { LegacyProviderRuntimeEvent } from "../TestProviderAdapter.integration.ts"; |
| 3 | EventId, | ||
| 4 | ProviderSessionId, | ||
| 5 | ProviderThreadId, | ||
| 6 | ProviderTurnId, | ||
| 7 | type ProviderRuntimeEvent, | ||
| 8 | } from "@t3tools/contracts"; | ||
| 9 | 3 | ||
| 10 | const PROVIDER = "codex" as const; | 4 | const PROVIDER = "codex" as const; |
| 11 | const SESSION_ID = ProviderSessionId.makeUnsafe("fixture-session"); | 5 | const SESSION_ID = "fixture-session"; |
| 12 | const THREAD_ID = ProviderThreadId.makeUnsafe("fixture-thread"); | 6 | const THREAD_ID = "fixture-thread"; |
| 13 | const TURN_ID = ProviderTurnId.makeUnsafe("fixture-turn"); | 7 | const TURN_ID = "fixture-turn"; |
| 14 | const REQUEST_ID = ApprovalRequestId.makeUnsafe("req-1"); | 8 | const REQUEST_ID = RuntimeRequestId.makeUnsafe("req-1"); |
| 15 | 9 | ||
| 16 | function baseEvent( | 10 | function baseEvent( |
| 17 | eventId: string, | 11 | eventId: string, |
| 18 | createdAt: string, | 12 | createdAt: string, |
| 19 | ): Pick<ProviderRuntimeEvent, "eventId" | "provider" | "sessionId" | "createdAt"> { | 13 | ): Pick<LegacyProviderRuntimeEvent, "eventId" | "provider" | "sessionId" | "createdAt"> { |
| 20 | return { | 14 | return { |
| 21 | eventId: EventId.makeUnsafe(eventId), | 15 | eventId: EventId.makeUnsafe(eventId), |
| 22 | provider: PROVIDER, | 16 | provider: PROVIDER, |
| ... | @@ -31,29 +25,38 @@ export const codexTurnTextFixture = [ | ... | @@ -31,29 +25,38 @@ export const codexTurnTextFixture = [ |
| 31 | ...baseEvent("evt-1", "2026-02-23T00:00:00.000Z"), | 25 | ...baseEvent("evt-1", "2026-02-23T00:00:00.000Z"), |
| 32 | threadId: THREAD_ID, | 26 | threadId: THREAD_ID, |
| 33 | turnId: TURN_ID, | 27 | turnId: TURN_ID, |
| 28 | payload: {}, | ||
| 34 | }, | 29 | }, |
| 35 | { | 30 | { |
| 36 | type: "message.delta", | 31 | type: "content.delta", |
| 37 | ...baseEvent("evt-2", "2026-02-23T00:00:00.100Z"), | 32 | ...baseEvent("evt-2", "2026-02-23T00:00:00.100Z"), |
| 38 | threadId: THREAD_ID, | 33 | threadId: THREAD_ID, |
| 39 | turnId: TURN_ID, | 34 | turnId: TURN_ID, |
| 40 | delta: "I will make a small update.\n", | 35 | payload: { |
| 36 | streamKind: "assistant_text", | ||
| 37 | delta: "I will make a small update.\n", | ||
| 38 | }, | ||
| 41 | }, | 39 | }, |
| 42 | { | 40 | { |
| 43 | type: "message.delta", | 41 | type: "content.delta", |
| 44 | ...baseEvent("evt-3", "2026-02-23T00:00:00.200Z"), | 42 | ...baseEvent("evt-3", "2026-02-23T00:00:00.200Z"), |
| 45 | threadId: THREAD_ID, | 43 | threadId: THREAD_ID, |
| 46 | turnId: TURN_ID, | 44 | turnId: TURN_ID, |
| 47 | delta: "Done.\n", | 45 | payload: { |
| 46 | streamKind: "assistant_text", | ||
| 47 | delta: "Done.\n", | ||
| 48 | }, | ||
| 48 | }, | 49 | }, |
| 49 | { | 50 | { |
| 50 | type: "turn.completed", | 51 | type: "turn.completed", |
| 51 | ...baseEvent("evt-4", "2026-02-23T00:00:00.300Z"), | 52 | ...baseEvent("evt-4", "2026-02-23T00:00:00.300Z"), |
| 52 | threadId: THREAD_ID, | 53 | threadId: THREAD_ID, |
| 53 | turnId: TURN_ID, | 54 | turnId: TURN_ID, |
| 54 | status: "completed", | 55 | payload: { |
| 56 | state: "completed", | ||
| 57 | }, | ||
| 55 | }, | 58 | }, |
| 56 | ] satisfies ReadonlyArray<ProviderRuntimeEvent>; | 59 | ] satisfies ReadonlyArray<LegacyProviderRuntimeEvent>; |
| 57 | 60 | ||
| 58 | export const codexTurnToolFixture = [ | 61 | export const codexTurnToolFixture = [ |
| 59 | { | 62 | { |
| ... | @@ -61,40 +64,51 @@ export const codexTurnToolFixture = [ | ... | @@ -61,40 +64,51 @@ export const codexTurnToolFixture = [ |
| 61 | ...baseEvent("evt-11", "2026-02-23T00:01:00.000Z"), | 64 | ...baseEvent("evt-11", "2026-02-23T00:01:00.000Z"), |
| 62 | threadId: THREAD_ID, | 65 | threadId: THREAD_ID, |
| 63 | turnId: TURN_ID, | 66 | turnId: TURN_ID, |
| 67 | payload: {}, | ||
| 64 | }, | 68 | }, |
| 65 | { | 69 | { |
| 66 | type: "tool.started", | 70 | type: "item.started", |
| 67 | ...baseEvent("evt-12", "2026-02-23T00:01:00.100Z"), | 71 | ...baseEvent("evt-12", "2026-02-23T00:01:00.100Z"), |
| 68 | threadId: THREAD_ID, | 72 | threadId: THREAD_ID, |
| 69 | turnId: TURN_ID, | 73 | turnId: TURN_ID, |
| 70 | toolKind: "command", | 74 | payload: { |
| 71 | title: "Command run", | 75 | itemType: "command_execution", |
| 72 | detail: "echo integration", | 76 | title: "Command run", |
| 77 | detail: "echo integration", | ||
| 78 | }, | ||
| 73 | }, | 79 | }, |
| 74 | { | 80 | { |
| 75 | type: "tool.completed", | 81 | type: "item.completed", |
| 76 | ...baseEvent("evt-13", "2026-02-23T00:01:00.200Z"), | 82 | ...baseEvent("evt-13", "2026-02-23T00:01:00.200Z"), |
| 77 | threadId: THREAD_ID, | 83 | threadId: THREAD_ID, |
| 78 | turnId: TURN_ID, | 84 | turnId: TURN_ID, |
| 79 | toolKind: "command", | 85 | payload: { |
| 80 | title: "Command run", | 86 | itemType: "command_execution", |
| 81 | detail: "echo integration", | 87 | status: "completed", |
| 88 | title: "Command run", | ||
| 89 | detail: "echo integration", | ||
| 90 | }, | ||
| 82 | }, | 91 | }, |
| 83 | { | 92 | { |
| 84 | type: "message.delta", | 93 | type: "content.delta", |
| 85 | ...baseEvent("evt-14", "2026-02-23T00:01:00.300Z"), | 94 | ...baseEvent("evt-14", "2026-02-23T00:01:00.300Z"), |
| 86 | threadId: THREAD_ID, | 95 | threadId: THREAD_ID, |
| 87 | turnId: TURN_ID, | 96 | turnId: TURN_ID, |
| 88 | delta: "Applied the requested edit.\n", | 97 | payload: { |
| 98 | streamKind: "assistant_text", | ||
| 99 | delta: "Applied the requested edit.\n", | ||
| 100 | }, | ||
| 89 | }, | 101 | }, |
| 90 | { | 102 | { |
| 91 | type: "turn.completed", | 103 | type: "turn.completed", |
| 92 | ...baseEvent("evt-15", "2026-02-23T00:01:00.400Z"), | 104 | ...baseEvent("evt-15", "2026-02-23T00:01:00.400Z"), |
| 93 | threadId: THREAD_ID, | 105 | threadId: THREAD_ID, |
| 94 | turnId: TURN_ID, | 106 | turnId: TURN_ID, |
| 95 | status: "completed", | 107 | payload: { |
| 108 | state: "completed", | ||
| 109 | }, | ||
| 96 | }, | 110 | }, |
| 97 | ] satisfies ReadonlyArray<ProviderRuntimeEvent>; | 111 | ] satisfies ReadonlyArray<LegacyProviderRuntimeEvent>; |
| 98 | 112 | ||
| 99 | export const codexTurnApprovalFixture = [ | 113 | export const codexTurnApprovalFixture = [ |
| 100 | { | 114 | { |
| ... | @@ -102,37 +116,47 @@ export const codexTurnApprovalFixture = [ | ... | @@ -102,37 +116,47 @@ export const codexTurnApprovalFixture = [ |
| 102 | ...baseEvent("evt-21", "2026-02-23T00:02:00.000Z"), | 116 | ...baseEvent("evt-21", "2026-02-23T00:02:00.000Z"), |
| 103 | threadId: THREAD_ID, | 117 | threadId: THREAD_ID, |
| 104 | turnId: TURN_ID, | 118 | turnId: TURN_ID, |
| 119 | payload: {}, | ||
| 105 | }, | 120 | }, |
| 106 | { | 121 | { |
| 107 | type: "approval.requested", | 122 | type: "request.opened", |
| 108 | ...baseEvent("evt-22", "2026-02-23T00:02:00.100Z"), | 123 | ...baseEvent("evt-22", "2026-02-23T00:02:00.100Z"), |
| 109 | threadId: THREAD_ID, | 124 | threadId: THREAD_ID, |
| 110 | turnId: TURN_ID, | 125 | turnId: TURN_ID, |
| 111 | requestId: REQUEST_ID, | 126 | requestId: REQUEST_ID, |
| 112 | requestKind: "command", | 127 | payload: { |
| 113 | detail: "Please approve command", | 128 | requestType: "command_execution_approval", |
| 129 | detail: "Please approve command", | ||
| 130 | }, | ||
| 114 | }, | 131 | }, |
| 115 | { | 132 | { |
| 116 | type: "approval.resolved", | 133 | type: "request.resolved", |
| 117 | ...baseEvent("evt-23", "2026-02-23T00:02:00.200Z"), | 134 | ...baseEvent("evt-23", "2026-02-23T00:02:00.200Z"), |
| 118 | threadId: THREAD_ID, | 135 | threadId: THREAD_ID, |
| 119 | turnId: TURN_ID, | 136 | turnId: TURN_ID, |
| 120 | requestId: REQUEST_ID, | 137 | requestId: REQUEST_ID, |
| 121 | requestKind: "command", | 138 | payload: { |
| 122 | decision: "accept", | 139 | requestType: "command_execution_approval", |
| 140 | decision: "accept", | ||
| 141 | }, | ||
| 123 | }, | 142 | }, |
| 124 | { | 143 | { |
| 125 | type: "message.delta", | 144 | type: "content.delta", |
| 126 | ...baseEvent("evt-24", "2026-02-23T00:02:00.300Z"), | 145 | ...baseEvent("evt-24", "2026-02-23T00:02:00.300Z"), |
| 127 | threadId: THREAD_ID, | 146 | threadId: THREAD_ID, |
| 128 | turnId: TURN_ID, | 147 | turnId: TURN_ID, |
| 129 | delta: "Approval received and command executed.\n", | 148 | payload: { |
| 149 | streamKind: "assistant_text", | ||
| 150 | delta: "Approval received and command executed.\n", | ||
| 151 | }, | ||
| 130 | }, | 152 | }, |
| 131 | { | 153 | { |
| 132 | type: "turn.completed", | 154 | type: "turn.completed", |
| 133 | ...baseEvent("evt-25", "2026-02-23T00:02:00.400Z"), | 155 | ...baseEvent("evt-25", "2026-02-23T00:02:00.400Z"), |
| 134 | threadId: THREAD_ID, | 156 | threadId: THREAD_ID, |
| 135 | turnId: TURN_ID, | 157 | turnId: TURN_ID, |
| 136 | status: "completed", | 158 | payload: { |
| 159 | state: "completed", | ||
| 160 | }, | ||
| 137 | }, | 161 | }, |
| 138 | ] satisfies ReadonlyArray<ProviderRuntimeEvent>; | 162 | ] satisfies ReadonlyArray<LegacyProviderRuntimeEvent>; |
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+30-17| ... | @@ -45,6 +45,20 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId | ... | @@ -45,6 +45,20 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId |
| 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); |
| 46 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | 46 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 47 | 47 | ||
| 48 | type LegacyProviderRuntimeEvent = { | ||
| 49 | readonly type: string; | ||
| 50 | readonly eventId: EventId; | ||
| 51 | readonly provider: "codex" | "claudeCode" | "cursor"; | ||
| 52 | readonly sessionId: string; | ||
| 53 | readonly createdAt: string; | ||
| 54 | readonly threadId?: string | undefined; | ||
| 55 | readonly turnId?: string | undefined; | ||
| 56 | readonly itemId?: string | undefined; | ||
| 57 | readonly requestId?: string | undefined; | ||
| 58 | readonly payload?: unknown | undefined; | ||
| 59 | readonly [key: string]: unknown; | ||
| 60 | }; | ||
| 61 | |||
| 48 | function createProviderServiceHarness( | 62 | function createProviderServiceHarness( |
| 49 | cwd: string, | 63 | cwd: string, |
| 50 | hasSession = true, | 64 | hasSession = true, |
| ... | @@ -86,8 +100,8 @@ function createProviderServiceHarness( | ... | @@ -86,8 +100,8 @@ function createProviderServiceHarness( |
| 86 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), | 100 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), |
| 87 | }; | 101 | }; |
| 88 | 102 | ||
| 89 | const emit = (event: ProviderRuntimeEvent): void => { | 103 | const emit = (event: LegacyProviderRuntimeEvent): void => { |
| 90 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event)); | 104 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent)); |
| 91 | }; | 105 | }; |
| 92 | 106 | ||
| 93 | return { | 107 | return { |
| ... | @@ -363,7 +377,7 @@ describe("CheckpointReactor", () => { | ... | @@ -363,7 +377,7 @@ describe("CheckpointReactor", () => { |
| 363 | createdAt: new Date().toISOString(), | 377 | createdAt: new Date().toISOString(), |
| 364 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 378 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 365 | turnId: asProviderTurnId("turn-1"), | 379 | turnId: asProviderTurnId("turn-1"), |
| 366 | status: "completed", | 380 | payload: { state: "completed" }, |
| 367 | }); | 381 | }); |
| 368 | 382 | ||
| 369 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); | 383 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| ... | @@ -443,7 +457,7 @@ describe("CheckpointReactor", () => { | ... | @@ -443,7 +457,7 @@ describe("CheckpointReactor", () => { |
| 443 | createdAt: new Date().toISOString(), | 457 | createdAt: new Date().toISOString(), |
| 444 | threadId: ProviderThreadId.makeUnsafe("provider-thread-aux"), | 458 | threadId: ProviderThreadId.makeUnsafe("provider-thread-aux"), |
| 445 | turnId: asProviderTurnId("turn-aux"), | 459 | turnId: asProviderTurnId("turn-aux"), |
| 446 | status: "completed", | 460 | payload: { state: "completed" }, |
| 447 | }); | 461 | }); |
| 448 | 462 | ||
| 449 | await Effect.runPromise(Effect.sleep("40 millis")); | 463 | await Effect.runPromise(Effect.sleep("40 millis")); |
| ... | @@ -461,7 +475,7 @@ describe("CheckpointReactor", () => { | ... | @@ -461,7 +475,7 @@ describe("CheckpointReactor", () => { |
| 461 | createdAt: new Date().toISOString(), | 475 | createdAt: new Date().toISOString(), |
| 462 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 476 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 463 | turnId: asProviderTurnId("turn-main"), | 477 | turnId: asProviderTurnId("turn-main"), |
| 464 | status: "completed", | 478 | payload: { state: "completed" }, |
| 465 | }); | 479 | }); |
| 466 | 480 | ||
| 467 | const thread = await waitForThread( | 481 | const thread = await waitForThread( |
| ... | @@ -522,7 +536,7 @@ describe("CheckpointReactor", () => { | ... | @@ -522,7 +536,7 @@ describe("CheckpointReactor", () => { |
| 522 | createdAt: new Date().toISOString(), | 536 | createdAt: new Date().toISOString(), |
| 523 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 537 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 524 | turnId: asProviderTurnId("turn-claude-1"), | 538 | turnId: asProviderTurnId("turn-claude-1"), |
| 525 | status: "completed", | 539 | payload: { state: "completed" }, |
| 526 | }); | 540 | }); |
| 527 | 541 | ||
| 528 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); | 542 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| ... | @@ -570,7 +584,7 @@ describe("CheckpointReactor", () => { | ... | @@ -570,7 +584,7 @@ describe("CheckpointReactor", () => { |
| 570 | createdAt: new Date().toISOString(), | 584 | createdAt: new Date().toISOString(), |
| 571 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 585 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 572 | turnId: asProviderTurnId("turn-missing-baseline"), | 586 | turnId: asProviderTurnId("turn-missing-baseline"), |
| 573 | status: "completed", | 587 | payload: { state: "completed" }, |
| 574 | }); | 588 | }); |
| 575 | 589 | ||
| 576 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); | 590 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| ... | @@ -662,7 +676,7 @@ describe("CheckpointReactor", () => { | ... | @@ -662,7 +676,7 @@ describe("CheckpointReactor", () => { |
| 662 | createdAt: new Date().toISOString(), | 676 | createdAt: new Date().toISOString(), |
| 663 | threadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), | 677 | threadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), |
| 664 | turnId: asProviderTurnId("turn-missing-cwd"), | 678 | turnId: asProviderTurnId("turn-missing-cwd"), |
| 665 | status: "completed", | 679 | payload: { state: "completed" }, |
| 666 | }); | 680 | }); |
| 667 | 681 | ||
| 668 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); | 682 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| ... | @@ -678,7 +692,7 @@ describe("CheckpointReactor", () => { | ... | @@ -678,7 +692,7 @@ describe("CheckpointReactor", () => { |
| 678 | ).toBe("v2\n"); | 692 | ).toBe("v2\n"); |
| 679 | }); | 693 | }); |
| 680 | 694 | ||
| 681 | it("maps runtime checkpoint.captured into thread.turn.diff.complete when missing in read model", async () => { | 695 | it("ignores non-v2 checkpoint.captured runtime events", async () => { |
| 682 | const harness = await createHarness(); | 696 | const harness = await createHarness(); |
| 683 | const createdAt = new Date().toISOString(); | 697 | const createdAt = new Date().toISOString(); |
| 684 | 698 | ||
| ... | @@ -715,11 +729,11 @@ describe("CheckpointReactor", () => { | ... | @@ -715,11 +729,11 @@ describe("CheckpointReactor", () => { |
| 715 | status: "completed", | 729 | status: "completed", |
| 716 | }); | 730 | }); |
| 717 | 731 | ||
| 718 | const thread = await waitForThread(harness.engine, (entry) => | 732 | await Effect.runPromise(Effect.sleep("40 millis")); |
| 719 | entry.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3), | 733 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); |
| 720 | ); | 734 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); |
| 721 | expect(thread.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3)).toBe( | 735 | expect(thread?.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3)).toBe( |
| 722 | true, | 736 | false, |
| 723 | ); | 737 | ); |
| 724 | }); | 738 | }); |
| 725 | 739 | ||
| ... | @@ -757,15 +771,14 @@ describe("CheckpointReactor", () => { | ... | @@ -757,15 +771,14 @@ describe("CheckpointReactor", () => { |
| 757 | ); | 771 | ); |
| 758 | 772 | ||
| 759 | harness.provider.emit({ | 773 | harness.provider.emit({ |
| 760 | type: "checkpoint.captured", | 774 | type: "turn.completed", |
| 761 | eventId: EventId.makeUnsafe("evt-runtime-capture-failure"), | 775 | eventId: EventId.makeUnsafe("evt-runtime-capture-failure"), |
| 762 | provider: "codex", | 776 | provider: "codex", |
| 763 | sessionId: asSessionId("sess-1"), | 777 | sessionId: asSessionId("sess-1"), |
| 764 | createdAt: new Date().toISOString(), | 778 | createdAt: new Date().toISOString(), |
| 765 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 779 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 766 | turnId: asProviderTurnId("turn-runtime-failure"), | 780 | turnId: asProviderTurnId("turn-runtime-failure"), |
| 767 | turnCount: 1, | 781 | payload: { state: "completed" }, |
| 768 | status: "completed", | ||
| 769 | }); | 782 | }); |
| 770 | 783 | ||
| 771 | harness.provider.emit({ | 784 | harness.provider.emit({ |
apps/server/src/orchestration/Layers/CheckpointReactor.ts+16-96| ... | @@ -8,6 +8,7 @@ import { | ... | @@ -8,6 +8,7 @@ import { |
| 8 | TurnId, | 8 | TurnId, |
| 9 | type OrchestrationEvent, | 9 | type OrchestrationEvent, |
| 10 | type ProviderRuntimeEvent, | 10 | type ProviderRuntimeEvent, |
| 11 | RuntimeSessionId, | ||
| 11 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 12 | import { Cause, Effect, Layer, Option, Queue, Stream } from "effect"; | 13 | import { Cause, Effect, Layer, Option, Queue, Stream } from "effect"; |
| 13 | 14 | ||
| ... | @@ -34,11 +35,15 @@ type ReactorInput = | ... | @@ -34,11 +35,15 @@ type ReactorInput = |
| 34 | }; | 35 | }; |
| 35 | 36 | ||
| 36 | function toTurnId(value: string | undefined): TurnId | null { | 37 | function toTurnId(value: string | undefined): TurnId | null { |
| 37 | return value === undefined ? null : TurnId.makeUnsafe(value); | 38 | return value === undefined ? null : TurnId.makeUnsafe(String(value)); |
| 38 | } | 39 | } |
| 39 | 40 | ||
| 40 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { | 41 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { |
| 41 | return value === undefined ? null : ProviderThreadId.makeUnsafe(value); | 42 | return value === undefined ? null : ProviderThreadId.makeUnsafe(String(value)); |
| 43 | } | ||
| 44 | |||
| 45 | function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId { | ||
| 46 | return ProviderSessionId.makeUnsafe(String(value)); | ||
| 42 | } | 47 | } |
| 43 | 48 | ||
| 44 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { | 49 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { |
| ... | @@ -172,6 +177,7 @@ const make = Effect.gen(function* () { | ... | @@ -172,6 +177,7 @@ const make = Effect.gen(function* () { |
| 172 | const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* ( | 177 | const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* ( |
| 173 | event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, | 178 | event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, |
| 174 | ) { | 179 | ) { |
| 180 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 175 | const turnId = toTurnId(event.turnId); | 181 | const turnId = toTurnId(event.turnId); |
| 176 | if (!turnId) { | 182 | if (!turnId) { |
| 177 | return; | 183 | return; |
| ... | @@ -179,7 +185,7 @@ const make = Effect.gen(function* () { | ... | @@ -179,7 +185,7 @@ const make = Effect.gen(function* () { |
| 179 | 185 | ||
| 180 | const readModel = yield* orchestrationEngine.getReadModel(); | 186 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 181 | const thread = readModel.threads.find( | 187 | const thread = readModel.threads.find( |
| 182 | (entry) => entry.session?.providerSessionId === event.sessionId, | 188 | (entry) => entry.session?.providerSessionId === providerSessionId, |
| 183 | ); | 189 | ); |
| 184 | if (!thread) { | 190 | if (!thread) { |
| 185 | return; | 191 | return; |
| ... | @@ -266,7 +272,7 @@ const make = Effect.gen(function* () { | ... | @@ -266,7 +272,7 @@ const make = Effect.gen(function* () { |
| 266 | ), | 272 | ), |
| 267 | Effect.tapError((error) => | 273 | Effect.tapError((error) => |
| 268 | appendCaptureFailureActivity({ | 274 | appendCaptureFailureActivity({ |
| 269 | sessionId: event.sessionId, | 275 | sessionId: providerSessionId, |
| 270 | turnId, | 276 | turnId, |
| 271 | detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`, | 277 | detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`, |
| 272 | createdAt: event.createdAt, | 278 | createdAt: event.createdAt, |
| ... | @@ -296,7 +302,7 @@ const make = Effect.gen(function* () { | ... | @@ -296,7 +302,7 @@ const make = Effect.gen(function* () { |
| 296 | turnId, | 302 | turnId, |
| 297 | completedAt: now, | 303 | completedAt: now, |
| 298 | checkpointRef: targetCheckpointRef, | 304 | checkpointRef: targetCheckpointRef, |
| 299 | status: checkpointStatusFromRuntime(event.status), | 305 | status: checkpointStatusFromRuntime(event.payload.state), |
| 300 | files, | 306 | files, |
| 301 | assistantMessageId, | 307 | assistantMessageId, |
| 302 | checkpointTurnCount: nextTurnCount, | 308 | checkpointTurnCount: nextTurnCount, |
| ... | @@ -314,7 +320,7 @@ const make = Effect.gen(function* () { | ... | @@ -314,7 +320,7 @@ const make = Effect.gen(function* () { |
| 314 | summary: "Checkpoint captured", | 320 | summary: "Checkpoint captured", |
| 315 | payload: { | 321 | payload: { |
| 316 | turnCount: nextTurnCount, | 322 | turnCount: nextTurnCount, |
| 317 | ...(event.status !== undefined ? { status: event.status } : {}), | 323 | status: event.payload.state, |
| 318 | }, | 324 | }, |
| 319 | turnId, | 325 | turnId, |
| 320 | createdAt: now, | 326 | createdAt: now, |
| ... | @@ -326,6 +332,7 @@ const make = Effect.gen(function* () { | ... | @@ -326,6 +332,7 @@ const make = Effect.gen(function* () { |
| 326 | const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* ( | 332 | const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* ( |
| 327 | event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>, | 333 | event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>, |
| 328 | ) { | 334 | ) { |
| 335 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 329 | const turnId = toTurnId(event.turnId); | 336 | const turnId = toTurnId(event.turnId); |
| 330 | if (!turnId) { | 337 | if (!turnId) { |
| 331 | return; | 338 | return; |
| ... | @@ -333,7 +340,7 @@ const make = Effect.gen(function* () { | ... | @@ -333,7 +340,7 @@ const make = Effect.gen(function* () { |
| 333 | 340 | ||
| 334 | const readModel = yield* orchestrationEngine.getReadModel(); | 341 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 335 | const thread = readModel.threads.find( | 342 | const thread = readModel.threads.find( |
| 336 | (entry) => entry.session?.providerSessionId === event.sessionId, | 343 | (entry) => entry.session?.providerSessionId === providerSessionId, |
| 337 | ); | 344 | ); |
| 338 | if (!thread) { | 345 | if (!thread) { |
| 339 | return; | 346 | return; |
| ... | @@ -581,7 +588,7 @@ const make = Effect.gen(function* () { | ... | @@ -581,7 +588,7 @@ const make = Effect.gen(function* () { |
| 581 | yield* captureCheckpointFromTurnCompletion(event).pipe( | 588 | yield* captureCheckpointFromTurnCompletion(event).pipe( |
| 582 | Effect.catch((error) => | 589 | Effect.catch((error) => |
| 583 | appendCaptureFailureActivity({ | 590 | appendCaptureFailureActivity({ |
| 584 | sessionId: event.sessionId, | 591 | sessionId: toProviderSessionId(event.sessionId), |
| 585 | turnId, | 592 | turnId, |
| 586 | detail: error.message, | 593 | detail: error.message, |
| 587 | createdAt: new Date().toISOString(), | 594 | createdAt: new Date().toISOString(), |
| ... | @@ -590,89 +597,6 @@ const make = Effect.gen(function* () { | ... | @@ -590,89 +597,6 @@ const make = Effect.gen(function* () { |
| 590 | ); | 597 | ); |
| 591 | return; | 598 | return; |
| 592 | } | 599 | } |
| 593 | |||
| 594 | if (event.type === "checkpoint.captured") { | ||
| 595 | const turnId = toTurnId(event.turnId); | ||
| 596 | if (!turnId) { | ||
| 597 | return; | ||
| 598 | } | ||
| 599 | |||
| 600 | const readModel = yield* orchestrationEngine.getReadModel(); | ||
| 601 | const thread = readModel.threads.find( | ||
| 602 | (entry) => entry.session?.providerSessionId === event.sessionId, | ||
| 603 | ); | ||
| 604 | if (!thread) { | ||
| 605 | return; | ||
| 606 | } | ||
| 607 | |||
| 608 | if ( | ||
| 609 | thread.checkpoints.some( | ||
| 610 | (checkpoint) => | ||
| 611 | checkpoint.turnId === turnId || checkpoint.checkpointTurnCount === event.turnCount, | ||
| 612 | ) | ||
| 613 | ) { | ||
| 614 | return; | ||
| 615 | } | ||
| 616 | |||
| 617 | const sessionRuntime = yield* resolveSessionRuntimeForThread(thread.id); | ||
| 618 | if (Option.isNone(sessionRuntime)) { | ||
| 619 | return; | ||
| 620 | } | ||
| 621 | |||
| 622 | const fromTurnCount = Math.max(0, event.turnCount - 1); | ||
| 623 | const fromCheckpointRef = checkpointRefForThreadTurn(thread.id, fromTurnCount); | ||
| 624 | const targetCheckpointRef = checkpointRefForThreadTurn(thread.id, event.turnCount); | ||
| 625 | |||
| 626 | const targetExists = yield* checkpointStore.hasCheckpointRef({ | ||
| 627 | cwd: sessionRuntime.value.cwd, | ||
| 628 | checkpointRef: targetCheckpointRef, | ||
| 629 | }); | ||
| 630 | if (!targetExists) { | ||
| 631 | yield* checkpointStore.captureCheckpoint({ | ||
| 632 | cwd: sessionRuntime.value.cwd, | ||
| 633 | checkpointRef: targetCheckpointRef, | ||
| 634 | }); | ||
| 635 | } | ||
| 636 | |||
| 637 | const files = yield* checkpointStore | ||
| 638 | .diffCheckpoints({ | ||
| 639 | cwd: sessionRuntime.value.cwd, | ||
| 640 | fromCheckpointRef, | ||
| 641 | toCheckpointRef: targetCheckpointRef, | ||
| 642 | fallbackFromToHead: false, | ||
| 643 | }) | ||
| 644 | .pipe( | ||
| 645 | Effect.map((diff) => | ||
| 646 | parseTurnDiffFilesFromUnifiedDiff(diff).map((file) => ({ | ||
| 647 | path: file.path, | ||
| 648 | kind: "modified" as const, | ||
| 649 | additions: file.additions, | ||
| 650 | deletions: file.deletions, | ||
| 651 | })), | ||
| 652 | ), | ||
| 653 | Effect.catch(() => Effect.succeed([])), | ||
| 654 | ); | ||
| 655 | |||
| 656 | const assistantMessageId = | ||
| 657 | thread.messages | ||
| 658 | .toReversed() | ||
| 659 | .find((entry) => entry.role === "assistant" && entry.turnId === turnId)?.id ?? | ||
| 660 | MessageId.makeUnsafe(`assistant:${turnId}`); | ||
| 661 | |||
| 662 | yield* orchestrationEngine.dispatch({ | ||
| 663 | type: "thread.turn.diff.complete", | ||
| 664 | commandId: serverCommandId("checkpoint-runtime-captured-diff-complete"), | ||
| 665 | threadId: thread.id, | ||
| 666 | turnId, | ||
| 667 | completedAt: event.createdAt, | ||
| 668 | checkpointRef: targetCheckpointRef, | ||
| 669 | status: checkpointStatusFromRuntime(event.status), | ||
| 670 | files, | ||
| 671 | assistantMessageId, | ||
| 672 | checkpointTurnCount: event.turnCount, | ||
| 673 | createdAt: event.createdAt, | ||
| 674 | }); | ||
| 675 | } | ||
| 676 | }); | 600 | }); |
| 677 | 601 | ||
| 678 | const processInput = ( | 602 | const processInput = ( |
| ... | @@ -717,11 +641,7 @@ const make = Effect.gen(function* () { | ... | @@ -717,11 +641,7 @@ const make = Effect.gen(function* () { |
| 717 | 641 | ||
| 718 | yield* Effect.forkScoped( | 642 | yield* Effect.forkScoped( |
| 719 | Stream.runForEach(providerService.streamEvents, (event) => { | 643 | Stream.runForEach(providerService.streamEvents, (event) => { |
| 720 | if ( | 644 | if (event.type !== "turn.started" && event.type !== "turn.completed") { |
| 721 | event.type !== "turn.started" && | ||
| 722 | event.type !== "turn.completed" && | ||
| 723 | event.type !== "checkpoint.captured" | ||
| 724 | ) { | ||
| 725 | return Effect.void; | 645 | return Effect.void; |
| 726 | } | 646 | } |
| 727 | return Queue.offer(queue, { source: "runtime", event }).pipe(Effect.asVoid); | 647 | return Queue.offer(queue, { source: "runtime", event }).pipe(Effect.asVoid); |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+3-3| ... | @@ -130,7 +130,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -130,7 +130,7 @@ describe("ProviderCommandReactor", () => { |
| 130 | }), | 130 | }), |
| 131 | ); | 131 | ); |
| 132 | const interruptTurn = vi.fn((_: unknown) => Effect.void); | 132 | const interruptTurn = vi.fn((_: unknown) => Effect.void); |
| 133 | const respondToRequest = vi.fn((_: unknown) => Effect.void); | 133 | const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void); |
| 134 | const stopSession = vi.fn((input: unknown) => | 134 | const stopSession = vi.fn((input: unknown) => |
| 135 | Effect.sync(() => { | 135 | Effect.sync(() => { |
| 136 | const sessionId = | 136 | const sessionId = |
| ... | @@ -777,7 +777,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -777,7 +777,7 @@ describe("ProviderCommandReactor", () => { |
| 777 | }); | 777 | }); |
| 778 | }); | 778 | }); |
| 779 | 779 | ||
| 780 | it("surfaces stale provider approval request failures and resolves stale pending approvals", async () => { | 780 | it("surfaces stale provider approval request failures without faking approval resolution", async () => { |
| 781 | const harness = await createHarness(); | 781 | const harness = await createHarness(); |
| 782 | const now = new Date().toISOString(); | 782 | const now = new Date().toISOString(); |
| 783 | harness.respondToRequest.mockImplementation(() => | 783 | harness.respondToRequest.mockImplementation(() => |
| ... | @@ -869,7 +869,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -869,7 +869,7 @@ describe("ProviderCommandReactor", () => { |
| 869 | activity.payload !== null && | 869 | activity.payload !== null && |
| 870 | (activity.payload as Record<string, unknown>).requestId === "approval-request-1", | 870 | (activity.payload as Record<string, unknown>).requestId === "approval-request-1", |
| 871 | ); | 871 | ); |
| 872 | expect(resolvedActivity).toBeDefined(); | 872 | expect(resolvedActivity).toBeUndefined(); |
| 873 | }); | 873 | }); |
| 874 | 874 | ||
| 875 | it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => { | 875 | it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => { |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+3-40| ... | @@ -12,7 +12,7 @@ import { | ... | @@ -12,7 +12,7 @@ import { |
| 12 | type ProviderSession, | 12 | type ProviderSession, |
| 13 | type TurnId, | 13 | type TurnId, |
| 14 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| 15 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; | 15 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Schema, Stream } from "effect"; |
| 16 | 16 | ||
| 17 | import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; | 17 | import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; |
| 18 | import { GitCore } from "../../git/Services/GitCore.ts"; | 18 | import { GitCore } from "../../git/Services/GitCore.ts"; |
| ... | @@ -84,7 +84,7 @@ function toErrorMessage(error: unknown): string { | ... | @@ -84,7 +84,7 @@ function toErrorMessage(error: unknown): string { |
| 84 | } | 84 | } |
| 85 | 85 | ||
| 86 | function isUnknownPendingApprovalRequestError(error: unknown): boolean { | 86 | function isUnknownPendingApprovalRequestError(error: unknown): boolean { |
| 87 | if (error instanceof ProviderAdapterRequestError) { | 87 | if (Schema.is(ProviderAdapterRequestError)(error)) { |
| 88 | const detail = error.detail.toLowerCase(); | 88 | const detail = error.detail.toLowerCase(); |
| 89 | return ( | 89 | return ( |
| 90 | detail.includes("unknown pending approval request") || | 90 | detail.includes("unknown pending approval request") || |
| ... | @@ -175,33 +175,6 @@ const make = Effect.gen(function* () { | ... | @@ -175,33 +175,6 @@ const make = Effect.gen(function* () { |
| 175 | createdAt: input.createdAt, | 175 | createdAt: input.createdAt, |
| 176 | }); | 176 | }); |
| 177 | 177 | ||
| 178 | const appendApprovalResolvedActivity = (input: { | ||
| 179 | readonly threadId: ThreadId; | ||
| 180 | readonly requestId: string; | ||
| 181 | readonly decision: "accept" | "acceptForSession" | "decline" | "cancel"; | ||
| 182 | readonly detail: string; | ||
| 183 | readonly createdAt: string; | ||
| 184 | }) => | ||
| 185 | orchestrationEngine.dispatch({ | ||
| 186 | type: "thread.activity.append", | ||
| 187 | commandId: serverCommandId("approval-resolved"), | ||
| 188 | threadId: input.threadId, | ||
| 189 | activity: { | ||
| 190 | id: EventId.makeUnsafe(crypto.randomUUID()), | ||
| 191 | tone: "approval", | ||
| 192 | kind: "approval.resolved", | ||
| 193 | summary: "Approval resolved", | ||
| 194 | payload: { | ||
| 195 | requestId: input.requestId, | ||
| 196 | decision: input.decision, | ||
| 197 | detail: input.detail, | ||
| 198 | }, | ||
| 199 | turnId: null, | ||
| 200 | createdAt: input.createdAt, | ||
| 201 | }, | ||
| 202 | createdAt: input.createdAt, | ||
| 203 | }); | ||
| 204 | |||
| 205 | const setThreadSession = (input: { | 178 | const setThreadSession = (input: { |
| 206 | readonly threadId: ThreadId; | 179 | readonly threadId: ThreadId; |
| 207 | readonly session: OrchestrationSession; | 180 | readonly session: OrchestrationSession; |
| ... | @@ -569,17 +542,7 @@ const make = Effect.gen(function* () { | ... | @@ -569,17 +542,7 @@ const make = Effect.gen(function* () { |
| 569 | requestId: event.payload.requestId, | 542 | requestId: event.payload.requestId, |
| 570 | }); | 543 | }); |
| 571 | 544 | ||
| 572 | if (!isUnknownPendingApprovalRequestError(error)) { | 545 | if (!isUnknownPendingApprovalRequestError(error)) return; |
| 573 | return; | ||
| 574 | } | ||
| 575 | |||
| 576 | yield* appendApprovalResolvedActivity({ | ||
| 577 | threadId: event.payload.threadId, | ||
| 578 | requestId: event.payload.requestId, | ||
| 579 | decision: event.payload.decision, | ||
| 580 | detail: "Approval request is no longer pending in the provider session.", | ||
| 581 | createdAt: event.payload.createdAt, | ||
| 582 | }); | ||
| 583 | }), | 546 | }), |
| 584 | ), | 547 | ), |
| 585 | ); | 548 | ); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+16-2| ... | @@ -40,6 +40,20 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(va | ... | @@ -40,6 +40,20 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(va |
| 40 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 40 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 41 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); | 41 | const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); |
| 42 | 42 | ||
| 43 | type LegacyProviderRuntimeEvent = { | ||
| 44 | readonly type: string; | ||
| 45 | readonly eventId: EventId; | ||
| 46 | readonly provider: "codex" | "claudeCode" | "cursor"; | ||
| 47 | readonly sessionId: string; | ||
| 48 | readonly createdAt: string; | ||
| 49 | readonly threadId?: string | undefined; | ||
| 50 | readonly turnId?: string | undefined; | ||
| 51 | readonly itemId?: string | undefined; | ||
| 52 | readonly requestId?: string | undefined; | ||
| 53 | readonly payload?: unknown | undefined; | ||
| 54 | readonly [key: string]: unknown; | ||
| 55 | }; | ||
| 56 | |||
| 43 | function createProviderServiceHarness() { | 57 | function createProviderServiceHarness() { |
| 44 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 58 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 45 | 59 | ||
| ... | @@ -57,8 +71,8 @@ function createProviderServiceHarness() { | ... | @@ -57,8 +71,8 @@ function createProviderServiceHarness() { |
| 57 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), | 71 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), |
| 58 | }; | 72 | }; |
| 59 | 73 | ||
| 60 | const emit = (event: ProviderRuntimeEvent): void => { | 74 | const emit = (event: LegacyProviderRuntimeEvent): void => { |
| 61 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event)); | 75 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent)); |
| 62 | }; | 76 | }; |
| 63 | 77 | ||
| 64 | return { | 78 | return { |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+39-23| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { | 1 | import { |
| 2 | ApprovalRequestId, | ||
| 2 | type AssistantDeliveryMode, | 3 | type AssistantDeliveryMode, |
| 3 | CommandId, | 4 | CommandId, |
| 4 | MessageId, | 5 | MessageId, |
| ... | @@ -10,7 +11,8 @@ import { | ... | @@ -10,7 +11,8 @@ import { |
| 10 | TurnId, | 11 | TurnId, |
| 11 | type OrchestrationThreadActivity, | 12 | type OrchestrationThreadActivity, |
| 12 | type ProviderRuntimeEvent, | 13 | type ProviderRuntimeEvent, |
| 13 | type ProviderSessionId, | 14 | ProviderSessionId, |
| 15 | RuntimeSessionId, | ||
| 14 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 15 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect"; | 17 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect"; |
| 16 | 18 | ||
| ... | @@ -50,12 +52,20 @@ type RuntimeIngestionInput = | ... | @@ -50,12 +52,20 @@ type RuntimeIngestionInput = |
| 50 | event: TurnStartRequestedDomainEvent; | 52 | event: TurnStartRequestedDomainEvent; |
| 51 | }; | 53 | }; |
| 52 | 54 | ||
| 53 | function toTurnId(value: string | undefined): TurnId | undefined { | 55 | function toTurnId(value: TurnId | string | undefined): TurnId | undefined { |
| 54 | return value === undefined ? undefined : TurnId.makeUnsafe(value); | 56 | return value === undefined ? undefined : TurnId.makeUnsafe(String(value)); |
| 55 | } | 57 | } |
| 56 | 58 | ||
| 57 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { | 59 | function toProviderThreadId(value: string | undefined): ProviderThreadId | null { |
| 58 | return value === undefined ? null : ProviderThreadId.makeUnsafe(value); | 60 | return value === undefined ? null : ProviderThreadId.makeUnsafe(String(value)); |
| 61 | } | ||
| 62 | |||
| 63 | function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId { | ||
| 64 | return ProviderSessionId.makeUnsafe(String(value)); | ||
| 65 | } | ||
| 66 | |||
| 67 | function toApprovalRequestId(value: string | undefined): ApprovalRequestId | undefined { | ||
| 68 | return value === undefined ? undefined : ApprovalRequestId.makeUnsafe(value); | ||
| 59 | } | 69 | } |
| 60 | 70 | ||
| 61 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { | 71 | function sameId(left: string | null | undefined, right: string | null | undefined): boolean { |
| ... | @@ -163,7 +173,12 @@ function isToolLifecycleItemType(itemType: string): boolean { | ... | @@ -163,7 +173,12 @@ function isToolLifecycleItemType(itemType: string): boolean { |
| 163 | function runtimeEventToActivities( | 173 | function runtimeEventToActivities( |
| 164 | event: ProviderRuntimeEvent, | 174 | event: ProviderRuntimeEvent, |
| 165 | ): ReadonlyArray<OrchestrationThreadActivity> { | 175 | ): ReadonlyArray<OrchestrationThreadActivity> { |
| 166 | const maybeSequence = event.sessionSequence !== undefined ? { sequence: event.sessionSequence } : {}; | 176 | const maybeSequence = (() => { |
| 177 | const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number }; | ||
| 178 | return eventWithSequence.sessionSequence !== undefined | ||
| 179 | ? { sequence: eventWithSequence.sessionSequence } | ||
| 180 | : {}; | ||
| 181 | })(); | ||
| 167 | switch (event.type) { | 182 | switch (event.type) { |
| 168 | case "request.opened": { | 183 | case "request.opened": { |
| 169 | const requestKind = requestKindFromCanonicalRequestType(event.payload.requestType); | 184 | const requestKind = requestKindFromCanonicalRequestType(event.payload.requestType); |
| ... | @@ -180,7 +195,7 @@ function runtimeEventToActivities( | ... | @@ -180,7 +195,7 @@ function runtimeEventToActivities( |
| 180 | ? "File-change approval requested" | 195 | ? "File-change approval requested" |
| 181 | : "Approval requested", | 196 | : "Approval requested", |
| 182 | payload: { | 197 | payload: { |
| 183 | requestId: event.requestId, | 198 | requestId: toApprovalRequestId(event.requestId), |
| 184 | ...(requestKind ? { requestKind } : {}), | 199 | ...(requestKind ? { requestKind } : {}), |
| 185 | requestType: event.payload.requestType, | 200 | requestType: event.payload.requestType, |
| 186 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), | 201 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), |
| ... | @@ -201,7 +216,7 @@ function runtimeEventToActivities( | ... | @@ -201,7 +216,7 @@ function runtimeEventToActivities( |
| 201 | kind: "approval.resolved", | 216 | kind: "approval.resolved", |
| 202 | summary: "Approval resolved", | 217 | summary: "Approval resolved", |
| 203 | payload: { | 218 | payload: { |
| 204 | requestId: event.requestId, | 219 | requestId: toApprovalRequestId(event.requestId), |
| 205 | ...(requestKind ? { requestKind } : {}), | 220 | ...(requestKind ? { requestKind } : {}), |
| 206 | requestType: event.payload.requestType, | 221 | requestType: event.payload.requestType, |
| 207 | ...(event.payload.decision ? { decision: event.payload.decision } : {}), | 222 | ...(event.payload.decision ? { decision: event.payload.decision } : {}), |
| ... | @@ -296,7 +311,7 @@ function runtimeEventToActivities( | ... | @@ -296,7 +311,7 @@ function runtimeEventToActivities( |
| 296 | ? "Command approval requested" | 311 | ? "Command approval requested" |
| 297 | : "File-change approval requested", | 312 | : "File-change approval requested", |
| 298 | payload: { | 313 | payload: { |
| 299 | requestId: event.requestId, | 314 | requestId: toApprovalRequestId(event.requestId), |
| 300 | ...(requestKind ? { requestKind } : {}), | 315 | ...(requestKind ? { requestKind } : {}), |
| 301 | ...(detail ? { detail: truncateDetail(detail) } : {}), | 316 | ...(detail ? { detail: truncateDetail(detail) } : {}), |
| 302 | }, | 317 | }, |
| ... | @@ -317,7 +332,7 @@ function runtimeEventToActivities( | ... | @@ -317,7 +332,7 @@ function runtimeEventToActivities( |
| 317 | kind: "approval.resolved", | 332 | kind: "approval.resolved", |
| 318 | summary: "Approval resolved", | 333 | summary: "Approval resolved", |
| 319 | payload: { | 334 | payload: { |
| 320 | requestId: event.requestId, | 335 | requestId: toApprovalRequestId(event.requestId), |
| 321 | ...(requestKind ? { requestKind } : {}), | 336 | ...(requestKind ? { requestKind } : {}), |
| 322 | ...(decision ? { decision } : {}), | 337 | ...(decision ? { decision } : {}), |
| 323 | }, | 338 | }, |
| ... | @@ -528,9 +543,10 @@ const make = Effect.gen(function* () { | ... | @@ -528,9 +543,10 @@ const make = Effect.gen(function* () { |
| 528 | 543 | ||
| 529 | const processRuntimeEvent = (event: ProviderRuntimeEvent) => | 544 | const processRuntimeEvent = (event: ProviderRuntimeEvent) => |
| 530 | Effect.gen(function* () { | 545 | Effect.gen(function* () { |
| 546 | const providerSessionId = toProviderSessionId(event.sessionId); | ||
| 531 | const readModel = yield* orchestrationEngine.getReadModel(); | 547 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 532 | const thread = readModel.threads.find( | 548 | const thread = readModel.threads.find( |
| 533 | (entry) => entry.session?.providerSessionId === event.sessionId, | 549 | (entry) => entry.session?.providerSessionId === providerSessionId, |
| 534 | ); | 550 | ); |
| 535 | if (!thread) return; | 551 | if (!thread) return; |
| 536 | 552 | ||
| ... | @@ -614,10 +630,8 @@ const make = Effect.gen(function* () { | ... | @@ -614,10 +630,8 @@ const make = Effect.gen(function* () { |
| 614 | : activeTurnId; | 630 | : activeTurnId; |
| 615 | const providerThreadIdFromEvent = | 631 | const providerThreadIdFromEvent = |
| 616 | event.type === "thread.started" | 632 | event.type === "thread.started" |
| 617 | ? ProviderThreadId.makeUnsafe(event.threadId) | 633 | ? toProviderThreadId(event.threadId) |
| 618 | : event.threadId !== undefined | 634 | : toProviderThreadId(event.threadId); |
| 619 | ? ProviderThreadId.makeUnsafe(event.threadId) | ||
| 620 | : null; | ||
| 621 | const providerThreadId = | 635 | const providerThreadId = |
| 622 | providerThreadIdFromEvent ?? scopedSessionProviderThreadId ?? null; | 636 | providerThreadIdFromEvent ?? scopedSessionProviderThreadId ?? null; |
| 623 | const status = (() => { | 637 | const status = (() => { |
| ... | @@ -651,7 +665,7 @@ const make = Effect.gen(function* () { | ... | @@ -651,7 +665,7 @@ const make = Effect.gen(function* () { |
| 651 | threadId: thread.id, | 665 | threadId: thread.id, |
| 652 | status, | 666 | status, |
| 653 | providerName: event.provider, | 667 | providerName: event.provider, |
| 654 | providerSessionId: event.sessionId, | 668 | providerSessionId, |
| 655 | providerThreadId, | 669 | providerThreadId, |
| 656 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, | 670 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, |
| 657 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, | 671 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, |
| ... | @@ -677,7 +691,7 @@ const make = Effect.gen(function* () { | ... | @@ -677,7 +691,7 @@ const make = Effect.gen(function* () { |
| 677 | ); | 691 | ); |
| 678 | const turnId = toTurnId(event.turnId); | 692 | const turnId = toTurnId(event.turnId); |
| 679 | if (turnId) { | 693 | if (turnId) { |
| 680 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 694 | yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId); |
| 681 | } | 695 | } |
| 682 | 696 | ||
| 683 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); | 697 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); |
| ... | @@ -724,7 +738,7 @@ const make = Effect.gen(function* () { | ... | @@ -724,7 +738,7 @@ const make = Effect.gen(function* () { |
| 724 | const assistantMessageId = assistantCompletion.messageId; | 738 | const assistantMessageId = assistantCompletion.messageId; |
| 725 | const turnId = toTurnId(event.turnId); | 739 | const turnId = toTurnId(event.turnId); |
| 726 | if (turnId) { | 740 | if (turnId) { |
| 727 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 741 | yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId); |
| 728 | } | 742 | } |
| 729 | 743 | ||
| 730 | yield* finalizeAssistantMessage({ | 744 | yield* finalizeAssistantMessage({ |
| ... | @@ -735,18 +749,20 @@ const make = Effect.gen(function* () { | ... | @@ -735,18 +749,20 @@ const make = Effect.gen(function* () { |
| 735 | createdAt: now, | 749 | createdAt: now, |
| 736 | commandTag: "assistant-complete", | 750 | commandTag: "assistant-complete", |
| 737 | finalDeltaCommandTag: "assistant-delta-finalize", | 751 | finalDeltaCommandTag: "assistant-delta-finalize", |
| 738 | fallbackText: assistantCompletion.fallbackText, | 752 | ...(assistantCompletion.fallbackText !== undefined |
| 753 | ? { fallbackText: assistantCompletion.fallbackText } | ||
| 754 | : {}), | ||
| 739 | }); | 755 | }); |
| 740 | 756 | ||
| 741 | if (turnId) { | 757 | if (turnId) { |
| 742 | yield* forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 758 | yield* forgetAssistantMessageId(providerSessionId, turnId, assistantMessageId); |
| 743 | } | 759 | } |
| 744 | } | 760 | } |
| 745 | 761 | ||
| 746 | if (event.type === "turn.completed") { | 762 | if (event.type === "turn.completed") { |
| 747 | const turnId = toTurnId(event.turnId); | 763 | const turnId = toTurnId(event.turnId); |
| 748 | if (turnId) { | 764 | if (turnId) { |
| 749 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(event.sessionId, turnId); | 765 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(providerSessionId, turnId); |
| 750 | yield* Effect.forEach( | 766 | yield* Effect.forEach( |
| 751 | assistantMessageIds, | 767 | assistantMessageIds, |
| 752 | (assistantMessageId) => | 768 | (assistantMessageId) => |
| ... | @@ -761,12 +777,12 @@ const make = Effect.gen(function* () { | ... | @@ -761,12 +777,12 @@ const make = Effect.gen(function* () { |
| 761 | }), | 777 | }), |
| 762 | { concurrency: 1 }, | 778 | { concurrency: 1 }, |
| 763 | ).pipe(Effect.asVoid); | 779 | ).pipe(Effect.asVoid); |
| 764 | yield* clearAssistantMessageIdsForTurn(event.sessionId, turnId); | 780 | yield* clearAssistantMessageIdsForTurn(providerSessionId, turnId); |
| 765 | } | 781 | } |
| 766 | } | 782 | } |
| 767 | 783 | ||
| 768 | if (event.type === "session.exited") { | 784 | if (event.type === "session.exited") { |
| 769 | yield* clearTurnStateForSession(event.sessionId); | 785 | yield* clearTurnStateForSession(providerSessionId); |
| 770 | } | 786 | } |
| 771 | 787 | ||
| 772 | if (event.type === "runtime.error") { | 788 | if (event.type === "runtime.error") { |
| ... | @@ -793,7 +809,7 @@ const make = Effect.gen(function* () { | ... | @@ -793,7 +809,7 @@ const make = Effect.gen(function* () { |
| 793 | threadId: thread.id, | 809 | threadId: thread.id, |
| 794 | status: "error", | 810 | status: "error", |
| 795 | providerName: event.provider, | 811 | providerName: event.provider, |
| 796 | providerSessionId: event.sessionId, | 812 | providerSessionId, |
| 797 | providerThreadId, | 813 | providerThreadId, |
| 798 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, | 814 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, |
| 799 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, | 815 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, |
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+14-9| ... | @@ -331,14 +331,14 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -331,14 +331,14 @@ describe("ClaudeCodeAdapterLive", () => { |
| 331 | const turnStarted = runtimeEvents[3]; | 331 | const turnStarted = runtimeEvents[3]; |
| 332 | assert.equal(turnStarted?.type, "turn.started"); | 332 | assert.equal(turnStarted?.type, "turn.started"); |
| 333 | if (turnStarted?.type === "turn.started") { | 333 | if (turnStarted?.type === "turn.started") { |
| 334 | assert.equal(turnStarted.turnId, turn.turnId); | 334 | assert.equal(String(turnStarted.turnId), String(turn.turnId)); |
| 335 | } | 335 | } |
| 336 | 336 | ||
| 337 | const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); | 337 | const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); |
| 338 | assert.equal(deltaEvent?.type, "content.delta"); | 338 | assert.equal(deltaEvent?.type, "content.delta"); |
| 339 | if (deltaEvent?.type === "content.delta") { | 339 | if (deltaEvent?.type === "content.delta") { |
| 340 | assert.equal(deltaEvent.payload.delta, "Hi"); | 340 | assert.equal(deltaEvent.payload.delta, "Hi"); |
| 341 | assert.equal(deltaEvent.turnId, turn.turnId); | 341 | assert.equal(String(deltaEvent.turnId), String(turn.turnId)); |
| 342 | } | 342 | } |
| 343 | 343 | ||
| 344 | const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); | 344 | const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); |
| ... | @@ -350,7 +350,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -350,7 +350,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 350 | const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; | 350 | const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; |
| 351 | assert.equal(turnCompleted?.type, "turn.completed"); | 351 | assert.equal(turnCompleted?.type, "turn.completed"); |
| 352 | if (turnCompleted?.type === "turn.completed") { | 352 | if (turnCompleted?.type === "turn.completed") { |
| 353 | assert.equal(turnCompleted.turnId, turn.turnId); | 353 | assert.equal(String(turnCompleted.turnId), String(turn.turnId)); |
| 354 | assert.equal(turnCompleted.payload.state, "completed"); | 354 | assert.equal(turnCompleted.payload.state, "completed"); |
| 355 | } | 355 | } |
| 356 | }).pipe( | 356 | }).pipe( |
| ... | @@ -438,7 +438,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -438,7 +438,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 438 | assert.equal(deltaEvent?.type, "content.delta"); | 438 | assert.equal(deltaEvent?.type, "content.delta"); |
| 439 | if (deltaEvent?.type === "content.delta") { | 439 | if (deltaEvent?.type === "content.delta") { |
| 440 | assert.equal(deltaEvent.payload.delta, "Late text"); | 440 | assert.equal(deltaEvent.payload.delta, "Late text"); |
| 441 | assert.equal(deltaEvent.turnId, turn.turnId); | 441 | assert.equal(String(deltaEvent.turnId), String(turn.turnId)); |
| 442 | } | 442 | } |
| 443 | }).pipe( | 443 | }).pipe( |
| 444 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 444 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| ... | @@ -506,7 +506,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -506,7 +506,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 506 | assert.equal(deltaEvent?.type, "content.delta"); | 506 | assert.equal(deltaEvent?.type, "content.delta"); |
| 507 | if (deltaEvent?.type === "content.delta") { | 507 | if (deltaEvent?.type === "content.delta") { |
| 508 | assert.equal(deltaEvent.payload.delta, "Fallback hello"); | 508 | assert.equal(deltaEvent.payload.delta, "Fallback hello"); |
| 509 | assert.equal(deltaEvent.turnId, turn.turnId); | 509 | assert.equal(String(deltaEvent.turnId), String(turn.turnId)); |
| 510 | } | 510 | } |
| 511 | }).pipe( | 511 | }).pipe( |
| 512 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 512 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| ... | @@ -631,10 +631,15 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -631,10 +631,15 @@ describe("ClaudeCodeAdapterLive", () => { |
| 631 | if (requested.value.type !== "request.opened") { | 631 | if (requested.value.type !== "request.opened") { |
| 632 | return; | 632 | return; |
| 633 | } | 633 | } |
| 634 | const runtimeRequestId = requested.value.requestId; | ||
| 635 | assert.equal(typeof runtimeRequestId, "string"); | ||
| 636 | if (runtimeRequestId === undefined) { | ||
| 637 | return; | ||
| 638 | } | ||
| 634 | 639 | ||
| 635 | yield* adapter.respondToRequest( | 640 | yield* adapter.respondToRequest( |
| 636 | session.sessionId, | 641 | session.sessionId, |
| 637 | ApprovalRequestId.makeUnsafe(requested.value.requestId), | 642 | ApprovalRequestId.makeUnsafe(runtimeRequestId), |
| 638 | "accept", | 643 | "accept", |
| 639 | ); | 644 | ); |
| 640 | 645 | ||
| ... | @@ -744,7 +749,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -744,7 +749,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 744 | const firstCompleted = yield* Fiber.join(firstCompletedFiber); | 749 | const firstCompleted = yield* Fiber.join(firstCompletedFiber); |
| 745 | assert.equal(firstCompleted._tag, "Some"); | 750 | assert.equal(firstCompleted._tag, "Some"); |
| 746 | if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { | 751 | if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { |
| 747 | assert.equal(firstCompleted.value.turnId, firstTurn.turnId); | 752 | assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); |
| 748 | } | 753 | } |
| 749 | 754 | ||
| 750 | const secondTurn = yield* adapter.sendTurn({ | 755 | const secondTurn = yield* adapter.sendTurn({ |
| ... | @@ -770,7 +775,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -770,7 +775,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 770 | const secondCompleted = yield* Fiber.join(secondCompletedFiber); | 775 | const secondCompleted = yield* Fiber.join(secondCompletedFiber); |
| 771 | assert.equal(secondCompleted._tag, "Some"); | 776 | assert.equal(secondCompleted._tag, "Some"); |
| 772 | if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { | 777 | if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { |
| 773 | assert.equal(secondCompleted.value.turnId, secondTurn.turnId); | 778 | assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); |
| 774 | } | 779 | } |
| 775 | 780 | ||
| 776 | const threadBeforeRollback = yield* adapter.readThread(session.sessionId); | 781 | const threadBeforeRollback = yield* adapter.readThread(session.sessionId); |
| ... | @@ -875,7 +880,7 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -875,7 +880,7 @@ describe("ClaudeCodeAdapterLive", () => { |
| 875 | 880 | ||
| 876 | assert.equal(records.some((record) => record.event?.provider === "claudeCode"), true); | 881 | assert.equal(records.some((record) => record.event?.provider === "claudeCode"), true); |
| 877 | assert.equal(records.some((record) => record.event?.sessionId === session.sessionId), true); | 882 | assert.equal(records.some((record) => record.event?.sessionId === session.sessionId), true); |
| 878 | assert.equal(records.some((record) => record.event?.turnId === turn.turnId), true); | 883 | assert.equal(records.some((record) => String(record.event?.turnId) === String(turn.turnId)), true); |
| 879 | assert.equal( | 884 | assert.equal( |
| 880 | records.some( | 885 | records.some( |
| 881 | (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", | 886 | (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", |
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+111-78| ... | @@ -31,6 +31,11 @@ import { | ... | @@ -31,6 +31,11 @@ import { |
| 31 | type ProviderSession, | 31 | type ProviderSession, |
| 32 | ProviderThreadId, | 32 | ProviderThreadId, |
| 33 | ProviderTurnId, | 33 | ProviderTurnId, |
| 34 | RuntimeItemId, | ||
| 35 | RuntimeRequestId, | ||
| 36 | RuntimeSessionId, | ||
| 37 | ThreadId, | ||
| 38 | TurnId, | ||
| 34 | } from "@t3tools/contracts"; | 39 | } from "@t3tools/contracts"; |
| 35 | import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect"; | 40 | import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect"; |
| 36 | 41 | ||
| ... | @@ -123,9 +128,7 @@ export interface ClaudeCodeAdapterLiveOptions { | ... | @@ -123,9 +128,7 @@ export interface ClaudeCodeAdapterLiveOptions { |
| 123 | } | 128 | } |
| 124 | 129 | ||
| 125 | function isUuid(value: string): boolean { | 130 | function isUuid(value: string): boolean { |
| 126 | return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( | 131 | return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); |
| 127 | value, | ||
| 128 | ); | ||
| 129 | } | 132 | } |
| 130 | 133 | ||
| 131 | function isSyntheticClaudeThreadId(value: string): boolean { | 134 | function isSyntheticClaudeThreadId(value: string): boolean { |
| ... | @@ -139,6 +142,26 @@ function toMessage(cause: unknown, fallback: string): string { | ... | @@ -139,6 +142,26 @@ function toMessage(cause: unknown, fallback: string): string { |
| 139 | return fallback; | 142 | return fallback; |
| 140 | } | 143 | } |
| 141 | 144 | ||
| 145 | function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId { | ||
| 146 | return RuntimeSessionId.makeUnsafe(value); | ||
| 147 | } | ||
| 148 | |||
| 149 | function asRuntimeThreadId(value: ProviderThreadId): ThreadId { | ||
| 150 | return ThreadId.makeUnsafe(value); | ||
| 151 | } | ||
| 152 | |||
| 153 | function asRuntimeTurnId(value: ProviderTurnId): TurnId { | ||
| 154 | return TurnId.makeUnsafe(value); | ||
| 155 | } | ||
| 156 | |||
| 157 | function asRuntimeItemId(value: string): RuntimeItemId { | ||
| 158 | return RuntimeItemId.makeUnsafe(value); | ||
| 159 | } | ||
| 160 | |||
| 161 | function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { | ||
| 162 | return RuntimeRequestId.makeUnsafe(value); | ||
| 163 | } | ||
| 164 | |||
| 142 | function toPermissionMode(value: unknown): PermissionMode | undefined { | 165 | function toPermissionMode(value: unknown): PermissionMode | undefined { |
| 143 | switch (value) { | 166 | switch (value) { |
| 144 | case "default": | 167 | case "default": |
| ... | @@ -315,7 +338,11 @@ function extractAssistantText(message: SDKMessage): string { | ... | @@ -315,7 +338,11 @@ function extractAssistantText(message: SDKMessage): string { |
| 315 | continue; | 338 | continue; |
| 316 | } | 339 | } |
| 317 | const candidate = block as { type?: unknown; text?: unknown }; | 340 | const candidate = block as { type?: unknown; text?: unknown }; |
| 318 | if (candidate.type === "text" && typeof candidate.text === "string" && candidate.text.length > 0) { | 341 | if ( |
| 342 | candidate.type === "text" && | ||
| 343 | typeof candidate.text === "string" && | ||
| 344 | candidate.text.length > 0 | ||
| 345 | ) { | ||
| 319 | fragments.push(candidate.text); | 346 | fragments.push(candidate.text); |
| 320 | } | 347 | } |
| 321 | } | 348 | } |
| ... | @@ -388,7 +415,9 @@ function sdkNativeMethod(message: SDKMessage): string { | ... | @@ -388,7 +415,9 @@ function sdkNativeMethod(message: SDKMessage): string { |
| 388 | const streamType = sdkMessageType(message.event); | 415 | const streamType = sdkMessageType(message.event); |
| 389 | if (streamType) { | 416 | if (streamType) { |
| 390 | const deltaType = | 417 | const deltaType = |
| 391 | streamType === "content_block_delta" ? sdkMessageType((message.event as { delta?: unknown }).delta) : undefined; | 418 | streamType === "content_block_delta" |
| 419 | ? sdkMessageType((message.event as { delta?: unknown }).delta) | ||
| 420 | : undefined; | ||
| 392 | if (deltaType) { | 421 | if (deltaType) { |
| 393 | return `claude/${message.type}/${streamType}/${deltaType}`; | 422 | return `claude/${message.type}/${streamType}/${deltaType}`; |
| 394 | } | 423 | } |
| ... | @@ -466,11 +495,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -466,11 +495,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 466 | : crypto.randomUUID(), | 495 | : crypto.randomUUID(), |
| 467 | kind: "notification", | 496 | kind: "notification", |
| 468 | provider: PROVIDER, | 497 | provider: PROVIDER, |
| 469 | sessionId: context.session.sessionId, | 498 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 470 | createdAt: observedAt, | 499 | createdAt: observedAt, |
| 471 | method: sdkNativeMethod(message), | 500 | method: sdkNativeMethod(message), |
| 472 | ...(typeof message.session_id === "string" ? { threadId: message.session_id } : {}), | 501 | ...(typeof message.session_id === "string" ? { threadId: message.session_id } : {}), |
| 473 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 502 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 474 | ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), | 503 | ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), |
| 475 | payload: message, | 504 | payload: message, |
| 476 | }, | 505 | }, |
| ... | @@ -485,7 +514,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -485,7 +514,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 485 | id: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 514 | id: ReturnType<typeof ProviderTurnId.makeUnsafe>; |
| 486 | items: ReadonlyArray<unknown>; | 515 | items: ReadonlyArray<unknown>; |
| 487 | }>; | 516 | }>; |
| 488 | }> => | 517 | }, ProviderAdapterValidationError> => |
| 489 | Effect.gen(function* () { | 518 | Effect.gen(function* () { |
| 490 | const threadId = context.session.threadId; | 519 | const threadId = context.session.threadId; |
| 491 | if (!threadId) { | 520 | if (!threadId) { |
| ... | @@ -552,9 +581,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -552,9 +581,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 552 | type: "thread.started", | 581 | type: "thread.started", |
| 553 | eventId: stamp.eventId, | 582 | eventId: stamp.eventId, |
| 554 | provider: PROVIDER, | 583 | provider: PROVIDER, |
| 555 | sessionId: context.session.sessionId, | 584 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 556 | createdAt: stamp.createdAt, | 585 | createdAt: stamp.createdAt, |
| 557 | threadId: nextThreadId, | 586 | threadId: asRuntimeThreadId(nextThreadId), |
| 558 | payload: { | 587 | payload: { |
| 559 | providerThreadId: nextThreadId, | 588 | providerThreadId: nextThreadId, |
| 560 | }, | 589 | }, |
| ... | @@ -588,10 +617,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -588,10 +617,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 588 | type: "runtime.error", | 617 | type: "runtime.error", |
| 589 | eventId: stamp.eventId, | 618 | eventId: stamp.eventId, |
| 590 | provider: PROVIDER, | 619 | provider: PROVIDER, |
| 591 | sessionId: context.session.sessionId, | 620 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 592 | createdAt: stamp.createdAt, | 621 | createdAt: stamp.createdAt, |
| 593 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 622 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 594 | ...(turnState ? { turnId: turnState.turnId } : {}), | 623 | ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}), |
| 595 | payload: { | 624 | payload: { |
| 596 | message, | 625 | message, |
| 597 | class: "provider_error", | 626 | class: "provider_error", |
| ... | @@ -617,10 +646,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -617,10 +646,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 617 | type: "runtime.warning", | 646 | type: "runtime.warning", |
| 618 | eventId: stamp.eventId, | 647 | eventId: stamp.eventId, |
| 619 | provider: PROVIDER, | 648 | provider: PROVIDER, |
| 620 | sessionId: context.session.sessionId, | 649 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 621 | createdAt: stamp.createdAt, | 650 | createdAt: stamp.createdAt, |
| 622 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 651 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 623 | ...(turnState ? { turnId: turnState.turnId } : {}), | 652 | ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}), |
| 624 | payload: { | 653 | payload: { |
| 625 | message, | 654 | message, |
| 626 | ...(detail !== undefined ? { detail } : {}), | 655 | ...(detail !== undefined ? { detail } : {}), |
| ... | @@ -647,9 +676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -647,9 +676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 647 | type: "turn.completed", | 676 | type: "turn.completed", |
| 648 | eventId: stamp.eventId, | 677 | eventId: stamp.eventId, |
| 649 | provider: PROVIDER, | 678 | provider: PROVIDER, |
| 650 | sessionId: context.session.sessionId, | 679 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 651 | createdAt: stamp.createdAt, | 680 | createdAt: stamp.createdAt, |
| 652 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 681 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 653 | payload: { | 682 | payload: { |
| 654 | state: status, | 683 | state: status, |
| 655 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), | 684 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| ... | @@ -675,11 +704,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -675,11 +704,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 675 | type: "content.delta", | 704 | type: "content.delta", |
| 676 | eventId: deltaStamp.eventId, | 705 | eventId: deltaStamp.eventId, |
| 677 | provider: PROVIDER, | 706 | provider: PROVIDER, |
| 678 | sessionId: context.session.sessionId, | 707 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 679 | createdAt: deltaStamp.createdAt, | 708 | createdAt: deltaStamp.createdAt, |
| 680 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 709 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 681 | turnId: turnState.turnId, | 710 | turnId: asRuntimeTurnId(turnState.turnId), |
| 682 | itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), | 711 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 683 | payload: { | 712 | payload: { |
| 684 | streamKind: "assistant_text", | 713 | streamKind: "assistant_text", |
| 685 | delta: turnState.fallbackAssistantText, | 714 | delta: turnState.fallbackAssistantText, |
| ... | @@ -698,11 +727,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -698,11 +727,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 698 | type: "item.completed", | 727 | type: "item.completed", |
| 699 | eventId: stamp.eventId, | 728 | eventId: stamp.eventId, |
| 700 | provider: PROVIDER, | 729 | provider: PROVIDER, |
| 701 | sessionId: context.session.sessionId, | 730 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 702 | createdAt: stamp.createdAt, | 731 | createdAt: stamp.createdAt, |
| 703 | itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), | 732 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 704 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 733 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 705 | turnId: turnState.turnId, | 734 | turnId: asRuntimeTurnId(turnState.turnId), |
| 706 | payload: { | 735 | payload: { |
| 707 | itemType: "assistant_message", | 736 | itemType: "assistant_message", |
| 708 | status: "completed", | 737 | status: "completed", |
| ... | @@ -727,10 +756,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -727,10 +756,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 727 | type: "turn.completed", | 756 | type: "turn.completed", |
| 728 | eventId: stamp.eventId, | 757 | eventId: stamp.eventId, |
| 729 | provider: PROVIDER, | 758 | provider: PROVIDER, |
| 730 | sessionId: context.session.sessionId, | 759 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 731 | createdAt: stamp.createdAt, | 760 | createdAt: stamp.createdAt, |
| 732 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 761 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 733 | turnId: turnState.turnId, | 762 | turnId: asRuntimeTurnId(turnState.turnId), |
| 734 | payload: { | 763 | payload: { |
| 735 | state: status, | 764 | state: status, |
| 736 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), | 765 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| ... | @@ -788,11 +817,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -788,11 +817,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 788 | type: "content.delta", | 817 | type: "content.delta", |
| 789 | eventId: stamp.eventId, | 818 | eventId: stamp.eventId, |
| 790 | provider: PROVIDER, | 819 | provider: PROVIDER, |
| 791 | sessionId: context.session.sessionId, | 820 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 792 | createdAt: stamp.createdAt, | 821 | createdAt: stamp.createdAt, |
| 793 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 822 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 794 | turnId: context.turnState.turnId, | 823 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 795 | itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), | 824 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 796 | payload: { | 825 | payload: { |
| 797 | streamKind: streamKindFromDeltaType(event.delta.type), | 826 | streamKind: streamKindFromDeltaType(event.delta.type), |
| 798 | delta: event.delta.text, | 827 | delta: event.delta.text, |
| ... | @@ -846,11 +875,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -846,11 +875,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 846 | type: "item.started", | 875 | type: "item.started", |
| 847 | eventId: stamp.eventId, | 876 | eventId: stamp.eventId, |
| 848 | provider: PROVIDER, | 877 | provider: PROVIDER, |
| 849 | sessionId: context.session.sessionId, | 878 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 850 | createdAt: stamp.createdAt, | 879 | createdAt: stamp.createdAt, |
| 851 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 880 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 852 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 881 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 853 | itemId: ProviderItemId.makeUnsafe(tool.itemId), | 882 | itemId: asRuntimeItemId(tool.itemId), |
| 854 | payload: { | 883 | payload: { |
| 855 | itemType: tool.itemType, | 884 | itemType: tool.itemType, |
| 856 | status: "inProgress", | 885 | status: "inProgress", |
| ... | @@ -889,11 +918,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -889,11 +918,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 889 | type: "item.completed", | 918 | type: "item.completed", |
| 890 | eventId: stamp.eventId, | 919 | eventId: stamp.eventId, |
| 891 | provider: PROVIDER, | 920 | provider: PROVIDER, |
| 892 | sessionId: context.session.sessionId, | 921 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 893 | createdAt: stamp.createdAt, | 922 | createdAt: stamp.createdAt, |
| 894 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 923 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 895 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 924 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 896 | itemId: ProviderItemId.makeUnsafe(tool.itemId), | 925 | itemId: asRuntimeItemId(tool.itemId), |
| 897 | payload: { | 926 | payload: { |
| 898 | itemType: tool.itemType, | 927 | itemType: tool.itemType, |
| 899 | status: "completed", | 928 | status: "completed", |
| ... | @@ -942,11 +971,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -942,11 +971,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 942 | type: "item.updated", | 971 | type: "item.updated", |
| 943 | eventId: stamp.eventId, | 972 | eventId: stamp.eventId, |
| 944 | provider: PROVIDER, | 973 | provider: PROVIDER, |
| 945 | sessionId: context.session.sessionId, | 974 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 946 | createdAt: stamp.createdAt, | 975 | createdAt: stamp.createdAt, |
| 947 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 976 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 948 | turnId: context.turnState.turnId, | 977 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 949 | itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), | 978 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 950 | payload: { | 979 | payload: { |
| 951 | itemType: "assistant_message", | 980 | itemType: "assistant_message", |
| 952 | status: "inProgress", | 981 | status: "inProgress", |
| ... | @@ -1003,10 +1032,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1003,10 +1032,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1003 | const base = { | 1032 | const base = { |
| 1004 | eventId: stamp.eventId, | 1033 | eventId: stamp.eventId, |
| 1005 | provider: PROVIDER, | 1034 | provider: PROVIDER, |
| 1006 | sessionId: context.session.sessionId, | 1035 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1007 | createdAt: stamp.createdAt, | 1036 | createdAt: stamp.createdAt, |
| 1008 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1037 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1009 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1038 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1010 | providerRefs: { | 1039 | providerRefs: { |
| 1011 | providerSessionId: context.session.sessionId, | 1040 | providerSessionId: context.session.sessionId, |
| 1012 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 1041 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), |
| ... | @@ -1164,10 +1193,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1164,10 +1193,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1164 | const base = { | 1193 | const base = { |
| 1165 | eventId: stamp.eventId, | 1194 | eventId: stamp.eventId, |
| 1166 | provider: PROVIDER, | 1195 | provider: PROVIDER, |
| 1167 | sessionId: context.session.sessionId, | 1196 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1168 | createdAt: stamp.createdAt, | 1197 | createdAt: stamp.createdAt, |
| 1169 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1198 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1170 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1199 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1171 | providerRefs: { | 1200 | providerRefs: { |
| 1172 | providerSessionId: context.session.sessionId, | 1201 | providerSessionId: context.session.sessionId, |
| 1173 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 1202 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), |
| ... | @@ -1303,11 +1332,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1303,11 +1332,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1303 | type: "request.resolved", | 1332 | type: "request.resolved", |
| 1304 | eventId: stamp.eventId, | 1333 | eventId: stamp.eventId, |
| 1305 | provider: PROVIDER, | 1334 | provider: PROVIDER, |
| 1306 | sessionId: context.session.sessionId, | 1335 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1307 | createdAt: stamp.createdAt, | 1336 | createdAt: stamp.createdAt, |
| 1308 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1337 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1309 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1338 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1310 | requestId, | 1339 | requestId: asRuntimeRequestId(requestId), |
| 1311 | payload: { | 1340 | payload: { |
| 1312 | requestType: pending.requestType, | 1341 | requestType: pending.requestType, |
| 1313 | decision: "cancel", | 1342 | decision: "cancel", |
| ... | @@ -1344,9 +1373,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1344,9 +1373,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1344 | type: "session.exited", | 1373 | type: "session.exited", |
| 1345 | eventId: stamp.eventId, | 1374 | eventId: stamp.eventId, |
| 1346 | provider: PROVIDER, | 1375 | provider: PROVIDER, |
| 1347 | sessionId: context.session.sessionId, | 1376 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1348 | createdAt: stamp.createdAt, | 1377 | createdAt: stamp.createdAt, |
| 1349 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1378 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1350 | payload: { | 1379 | payload: { |
| 1351 | reason: "Session stopped", | 1380 | reason: "Session stopped", |
| 1352 | exitKind: "graceful", | 1381 | exitKind: "graceful", |
| ... | @@ -1450,11 +1479,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1450,11 +1479,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1450 | type: "request.opened", | 1479 | type: "request.opened", |
| 1451 | eventId: requestedStamp.eventId, | 1480 | eventId: requestedStamp.eventId, |
| 1452 | provider: PROVIDER, | 1481 | provider: PROVIDER, |
| 1453 | sessionId: context.session.sessionId, | 1482 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1454 | createdAt: requestedStamp.createdAt, | 1483 | createdAt: requestedStamp.createdAt, |
| 1455 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1484 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1456 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1485 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1457 | requestId, | 1486 | requestId: asRuntimeRequestId(requestId), |
| 1458 | payload: { | 1487 | payload: { |
| 1459 | requestType, | 1488 | requestType, |
| 1460 | detail, | 1489 | detail, |
| ... | @@ -1466,7 +1495,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1466,7 +1495,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1466 | }, | 1495 | }, |
| 1467 | providerRefs: { | 1496 | providerRefs: { |
| 1468 | providerSessionId: context.session.sessionId, | 1497 | providerSessionId: context.session.sessionId, |
| 1469 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 1498 | ...(context.session.threadId |
| 1499 | ? { providerThreadId: context.session.threadId } | ||
| 1500 | : {}), | ||
| 1470 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1501 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1471 | providerRequestId: requestId, | 1502 | providerRequestId: requestId, |
| 1472 | }, | 1503 | }, |
| ... | @@ -1502,18 +1533,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1502,18 +1533,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1502 | type: "request.resolved", | 1533 | type: "request.resolved", |
| 1503 | eventId: resolvedStamp.eventId, | 1534 | eventId: resolvedStamp.eventId, |
| 1504 | provider: PROVIDER, | 1535 | provider: PROVIDER, |
| 1505 | sessionId: context.session.sessionId, | 1536 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1506 | createdAt: resolvedStamp.createdAt, | 1537 | createdAt: resolvedStamp.createdAt, |
| 1507 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1538 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1508 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1539 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1509 | requestId, | 1540 | requestId: asRuntimeRequestId(requestId), |
| 1510 | payload: { | 1541 | payload: { |
| 1511 | requestType, | 1542 | requestType, |
| 1512 | decision, | 1543 | decision, |
| 1513 | }, | 1544 | }, |
| 1514 | providerRefs: { | 1545 | providerRefs: { |
| 1515 | providerSessionId: context.session.sessionId, | 1546 | providerSessionId: context.session.sessionId, |
| 1516 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 1547 | ...(context.session.threadId |
| 1548 | ? { providerThreadId: context.session.threadId } | ||
| 1549 | : {}), | ||
| 1517 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), | 1550 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1518 | providerRequestId: requestId, | 1551 | providerRequestId: requestId, |
| 1519 | }, | 1552 | }, |
| ... | @@ -1628,9 +1661,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1628,9 +1661,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1628 | type: "session.started", | 1661 | type: "session.started", |
| 1629 | eventId: sessionStartedStamp.eventId, | 1662 | eventId: sessionStartedStamp.eventId, |
| 1630 | provider: PROVIDER, | 1663 | provider: PROVIDER, |
| 1631 | sessionId, | 1664 | sessionId: asRuntimeSessionId(sessionId), |
| 1632 | createdAt: sessionStartedStamp.createdAt, | 1665 | createdAt: sessionStartedStamp.createdAt, |
| 1633 | ...(threadId ? { threadId } : {}), | 1666 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), |
| 1634 | payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, | 1667 | payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, |
| 1635 | providerRefs: { | 1668 | providerRefs: { |
| 1636 | providerSessionId: sessionId, | 1669 | providerSessionId: sessionId, |
| ... | @@ -1643,9 +1676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1643,9 +1676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1643 | type: "session.configured", | 1676 | type: "session.configured", |
| 1644 | eventId: configuredStamp.eventId, | 1677 | eventId: configuredStamp.eventId, |
| 1645 | provider: PROVIDER, | 1678 | provider: PROVIDER, |
| 1646 | sessionId, | 1679 | sessionId: asRuntimeSessionId(sessionId), |
| 1647 | createdAt: configuredStamp.createdAt, | 1680 | createdAt: configuredStamp.createdAt, |
| 1648 | ...(threadId ? { threadId } : {}), | 1681 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), |
| 1649 | payload: { | 1682 | payload: { |
| 1650 | config: { | 1683 | config: { |
| 1651 | ...(input.model ? { model: input.model } : {}), | 1684 | ...(input.model ? { model: input.model } : {}), |
| ... | @@ -1667,9 +1700,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1667,9 +1700,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1667 | type: "session.state.changed", | 1700 | type: "session.state.changed", |
| 1668 | eventId: readyStamp.eventId, | 1701 | eventId: readyStamp.eventId, |
| 1669 | provider: PROVIDER, | 1702 | provider: PROVIDER, |
| 1670 | sessionId, | 1703 | sessionId: asRuntimeSessionId(sessionId), |
| 1671 | createdAt: readyStamp.createdAt, | 1704 | createdAt: readyStamp.createdAt, |
| 1672 | ...(threadId ? { threadId } : {}), | 1705 | ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}), |
| 1673 | payload: { | 1706 | payload: { |
| 1674 | state: "ready", | 1707 | state: "ready", |
| 1675 | }, | 1708 | }, |
| ... | @@ -1705,10 +1738,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1705,10 +1738,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1705 | }); | 1738 | }); |
| 1706 | } | 1739 | } |
| 1707 | 1740 | ||
| 1708 | const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* Random.nextUUIDv4}`); | 1741 | const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1709 | const turnState: ClaudeTurnState = { | 1742 | const turnState: ClaudeTurnState = { |
| 1710 | turnId, | 1743 | turnId, |
| 1711 | assistantItemId: `claude-message-${yield* Random.nextUUIDv4}`, | 1744 | assistantItemId: yield* Random.nextUUIDv4, |
| 1712 | startedAt: yield* nowIso, | 1745 | startedAt: yield* nowIso, |
| 1713 | items: [], | 1746 | items: [], |
| 1714 | messageCompleted: false, | 1747 | messageCompleted: false, |
| ... | @@ -1730,10 +1763,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1730,10 +1763,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1730 | type: "turn.started", | 1763 | type: "turn.started", |
| 1731 | eventId: turnStartedStamp.eventId, | 1764 | eventId: turnStartedStamp.eventId, |
| 1732 | provider: PROVIDER, | 1765 | provider: PROVIDER, |
| 1733 | sessionId: context.session.sessionId, | 1766 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1734 | createdAt: turnStartedStamp.createdAt, | 1767 | createdAt: turnStartedStamp.createdAt, |
| 1735 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1768 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1736 | turnId, | 1769 | turnId: asRuntimeTurnId(turnId), |
| 1737 | payload: { | 1770 | payload: { |
| 1738 | ...(input.model ? { model: input.model } : {}), | 1771 | ...(input.model ? { model: input.model } : {}), |
| 1739 | ...(input.effort ? { effort: input.effort } : {}), | 1772 | ...(input.effort ? { effort: input.effort } : {}), |
apps/server/src/provider/Layers/CodexAdapter.ts+33-7| ... | @@ -11,10 +11,16 @@ import { | ... | @@ -11,10 +11,16 @@ import { |
| 11 | type CanonicalRequestType, | 11 | type CanonicalRequestType, |
| 12 | type ProviderEvent, | 12 | type ProviderEvent, |
| 13 | type ProviderRuntimeEvent, | 13 | type ProviderRuntimeEvent, |
| 14 | RuntimeItemId, | ||
| 15 | RuntimeRequestId, | ||
| 16 | RuntimeSessionId, | ||
| 14 | ProviderApprovalDecision, | 17 | ProviderApprovalDecision, |
| 18 | ProviderItemId, | ||
| 15 | ProviderSessionId, | 19 | ProviderSessionId, |
| 16 | ProviderThreadId, | 20 | ProviderThreadId, |
| 17 | ProviderTurnId, | 21 | ProviderTurnId, |
| 22 | ThreadId, | ||
| 23 | TurnId, | ||
| 18 | } from "@t3tools/contracts"; | 24 | } from "@t3tools/contracts"; |
| 19 | import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect"; | 25 | import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect"; |
| 20 | 26 | ||
| ... | @@ -278,7 +284,27 @@ function contentStreamKindFromMethod( | ... | @@ -278,7 +284,27 @@ function contentStreamKindFromMethod( |
| 278 | } | 284 | } |
| 279 | } | 285 | } |
| 280 | 286 | ||
| 281 | function eventRawSource(event: ProviderEvent): ProviderRuntimeEvent["raw"]["source"] { | 287 | function asRuntimeSessionId(sessionId: ProviderSessionId): RuntimeSessionId { |
| 288 | return RuntimeSessionId.makeUnsafe(sessionId); | ||
| 289 | } | ||
| 290 | |||
| 291 | function asRuntimeThreadId(threadId: ProviderThreadId): ThreadId { | ||
| 292 | return ThreadId.makeUnsafe(threadId); | ||
| 293 | } | ||
| 294 | |||
| 295 | function asRuntimeTurnId(turnId: ProviderTurnId): TurnId { | ||
| 296 | return TurnId.makeUnsafe(turnId); | ||
| 297 | } | ||
| 298 | |||
| 299 | function asRuntimeItemId(itemId: ProviderItemId): RuntimeItemId { | ||
| 300 | return RuntimeItemId.makeUnsafe(itemId); | ||
| 301 | } | ||
| 302 | |||
| 303 | function asRuntimeRequestId(requestId: string): RuntimeRequestId { | ||
| 304 | return RuntimeRequestId.makeUnsafe(requestId); | ||
| 305 | } | ||
| 306 | |||
| 307 | function eventRawSource(event: ProviderEvent): NonNullable<ProviderRuntimeEvent["raw"]>["source"] { | ||
| 282 | return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification"; | 308 | return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification"; |
| 283 | } | 309 | } |
| 284 | 310 | ||
| ... | @@ -301,12 +327,12 @@ function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "typ | ... | @@ -301,12 +327,12 @@ function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "typ |
| 301 | return { | 327 | return { |
| 302 | eventId: event.id, | 328 | eventId: event.id, |
| 303 | provider: event.provider, | 329 | provider: event.provider, |
| 304 | sessionId: event.sessionId, | 330 | sessionId: asRuntimeSessionId(event.sessionId), |
| 305 | createdAt: event.createdAt, | 331 | createdAt: event.createdAt, |
| 306 | ...(event.threadId ? { threadId: event.threadId } : {}), | 332 | ...(event.threadId ? { threadId: asRuntimeThreadId(event.threadId) } : {}), |
| 307 | ...(event.turnId ? { turnId: event.turnId } : {}), | 333 | ...(event.turnId ? { turnId: asRuntimeTurnId(event.turnId) } : {}), |
| 308 | ...(event.itemId ? { itemId: event.itemId } : {}), | 334 | ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}), |
| 309 | ...(event.requestId ? { requestId: event.requestId } : {}), | 335 | ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}), |
| 310 | ...(refs ? { providerRefs: refs } : {}), | 336 | ...(refs ? { providerRefs: refs } : {}), |
| 311 | raw: { | 337 | raw: { |
| 312 | source: eventRawSource(event), | 338 | source: eventRawSource(event), |
| ... | @@ -541,7 +567,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime | ... | @@ -541,7 +567,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime |
| 541 | return [ | 567 | return [ |
| 542 | { | 568 | { |
| 543 | ...runtimeEventBase(event), | 569 | ...runtimeEventBase(event), |
| 544 | turnId, | 570 | turnId: asRuntimeTurnId(turnId), |
| 545 | type: "turn.started", | 571 | type: "turn.started", |
| 546 | payload: { | 572 | payload: { |
| 547 | ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}), | 573 | ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}), |
apps/server/src/provider/Layers/CursorAdapter.test.ts+7-2| ... | @@ -320,7 +320,7 @@ describe("CursorAdapterLive", () => { | ... | @@ -320,7 +320,7 @@ describe("CursorAdapterLive", () => { |
| 320 | const turnStarted = events[6]; | 320 | const turnStarted = events[6]; |
| 321 | assert.equal(turnStarted?.type, "turn.started"); | 321 | assert.equal(turnStarted?.type, "turn.started"); |
| 322 | if (turnStarted?.type === "turn.started") { | 322 | if (turnStarted?.type === "turn.started") { |
| 323 | assert.equal(turnStarted.turnId, turn.turnId); | 323 | assert.equal(String(turnStarted.turnId), String(turn.turnId)); |
| 324 | } | 324 | } |
| 325 | 325 | ||
| 326 | const completion = events[12]; | 326 | const completion = events[12]; |
| ... | @@ -448,10 +448,15 @@ describe("CursorAdapterLive", () => { | ... | @@ -448,10 +448,15 @@ describe("CursorAdapterLive", () => { |
| 448 | if (opened.value.type !== "request.opened") { | 448 | if (opened.value.type !== "request.opened") { |
| 449 | return; | 449 | return; |
| 450 | } | 450 | } |
| 451 | const runtimeRequestId = opened.value.requestId; | ||
| 452 | assert.equal(typeof runtimeRequestId, "string"); | ||
| 453 | if (runtimeRequestId === undefined) { | ||
| 454 | return; | ||
| 455 | } | ||
| 451 | 456 | ||
| 452 | yield* adapter.respondToRequest( | 457 | yield* adapter.respondToRequest( |
| 453 | session.sessionId, | 458 | session.sessionId, |
| 454 | ApprovalRequestId.makeUnsafe(opened.value.requestId), | 459 | ApprovalRequestId.makeUnsafe(runtimeRequestId), |
| 455 | "acceptForSession", | 460 | "acceptForSession", |
| 456 | ); | 461 | ); |
| 457 | 462 |
apps/server/src/provider/Layers/CursorAdapter.ts+146-86| ... | @@ -16,12 +16,18 @@ import { | ... | @@ -16,12 +16,18 @@ import { |
| 16 | type CanonicalItemType, | 16 | type CanonicalItemType, |
| 17 | type CanonicalRequestType, | 17 | type CanonicalRequestType, |
| 18 | type ProviderApprovalPolicy, | 18 | type ProviderApprovalPolicy, |
| 19 | ProviderItemId, | ||
| 19 | ProviderSessionId, | 20 | ProviderSessionId, |
| 20 | type ProviderRuntimeEvent, | 21 | type ProviderRuntimeEvent, |
| 21 | type ProviderSession, | 22 | type ProviderSession, |
| 22 | ProviderThreadId, | 23 | ProviderThreadId, |
| 23 | ProviderTurnId, | 24 | ProviderTurnId, |
| 24 | type ProviderTurnStartResult, | 25 | type ProviderTurnStartResult, |
| 26 | RuntimeItemId, | ||
| 27 | RuntimeRequestId, | ||
| 28 | RuntimeSessionId, | ||
| 29 | ThreadId, | ||
| 30 | TurnId, | ||
| 25 | } from "@t3tools/contracts"; | 31 | } from "@t3tools/contracts"; |
| 26 | import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect"; | 32 | import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect"; |
| 27 | 33 | ||
| ... | @@ -67,7 +73,7 @@ interface PendingPermission { | ... | @@ -67,7 +73,7 @@ interface PendingPermission { |
| 67 | 73 | ||
| 68 | interface CursorTurnState { | 74 | interface CursorTurnState { |
| 69 | readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>; | 75 | readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>; |
| 70 | readonly assistantItemId: string; | 76 | readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>; |
| 71 | readonly startedToolCalls: Set<string>; | 77 | readonly startedToolCalls: Set<string>; |
| 72 | readonly items: Array<unknown>; | 78 | readonly items: Array<unknown>; |
| 73 | } | 79 | } |
| ... | @@ -106,6 +112,30 @@ function toMessage(cause: unknown, fallback: string): string { | ... | @@ -106,6 +112,30 @@ function toMessage(cause: unknown, fallback: string): string { |
| 106 | return fallback; | 112 | return fallback; |
| 107 | } | 113 | } |
| 108 | 114 | ||
| 115 | function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId { | ||
| 116 | return RuntimeSessionId.makeUnsafe(value); | ||
| 117 | } | ||
| 118 | |||
| 119 | function asRuntimeThreadId(value: ProviderThreadId): ThreadId { | ||
| 120 | return ThreadId.makeUnsafe(value); | ||
| 121 | } | ||
| 122 | |||
| 123 | function asRuntimeTurnId(value: ProviderTurnId): TurnId { | ||
| 124 | return TurnId.makeUnsafe(value); | ||
| 125 | } | ||
| 126 | |||
| 127 | function asRuntimeItemId(value: string): RuntimeItemId { | ||
| 128 | return RuntimeItemId.makeUnsafe(value); | ||
| 129 | } | ||
| 130 | |||
| 131 | function asProviderItemId(value: string): ProviderItemId { | ||
| 132 | return ProviderItemId.makeUnsafe(value); | ||
| 133 | } | ||
| 134 | |||
| 135 | function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { | ||
| 136 | return RuntimeRequestId.makeUnsafe(value); | ||
| 137 | } | ||
| 138 | |||
| 109 | function toSessionError( | 139 | function toSessionError( |
| 110 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, | 140 | sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>, |
| 111 | cause: unknown, | 141 | cause: unknown, |
| ... | @@ -252,11 +282,9 @@ function summarizeToolOutput(rawOutput: unknown): string | undefined { | ... | @@ -252,11 +282,9 @@ function summarizeToolOutput(rawOutput: unknown): string | undefined { |
| 252 | return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary; | 282 | return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary; |
| 253 | } | 283 | } |
| 254 | 284 | ||
| 255 | function mapStopReasonToTurnState(stopReason: string | undefined): | 285 | function mapStopReasonToTurnState( |
| 256 | | "completed" | 286 | stopReason: string | undefined, |
| 257 | | "failed" | 287 | ): "completed" | "failed" | "interrupted" | "cancelled" { |
| 258 | | "interrupted" | ||
| 259 | | "cancelled" { | ||
| 260 | if (stopReason === "cancelled") return "cancelled"; | 288 | if (stopReason === "cancelled") return "cancelled"; |
| 261 | if (stopReason === "interrupted") return "interrupted"; | 289 | if (stopReason === "interrupted") return "interrupted"; |
| 262 | return "completed"; | 290 | return "completed"; |
| ... | @@ -357,7 +385,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -357,7 +385,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 357 | }); | 385 | }); |
| 358 | }; | 386 | }; |
| 359 | 387 | ||
| 360 | const resolvePendingRequest = (context: CursorSessionContext, message: Record<string, unknown>) => { | 388 | const resolvePendingRequest = ( |
| 389 | context: CursorSessionContext, | ||
| 390 | message: Record<string, unknown>, | ||
| 391 | ) => { | ||
| 361 | const id = message.id; | 392 | const id = message.id; |
| 362 | if (typeof id !== "string" && typeof id !== "number") { | 393 | if (typeof id !== "string" && typeof id !== "number") { |
| 363 | return; | 394 | return; |
| ... | @@ -380,7 +411,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -380,7 +411,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 380 | }; | 411 | }; |
| 381 | 412 | ||
| 382 | const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest); | 413 | const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest); |
| 383 | const decodeSessionUpdateNotification = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification); | 414 | const decodeSessionUpdateNotification = Schema.decodeUnknownSync( |
| 415 | CursorAcpSessionUpdateNotification, | ||
| 416 | ); | ||
| 384 | 417 | ||
| 385 | const emitRuntimeWarning = ( | 418 | const emitRuntimeWarning = ( |
| 386 | context: CursorSessionContext, | 419 | context: CursorSessionContext, |
| ... | @@ -393,10 +426,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -393,10 +426,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 393 | type: "runtime.warning", | 426 | type: "runtime.warning", |
| 394 | eventId: stamp.eventId, | 427 | eventId: stamp.eventId, |
| 395 | provider: PROVIDER, | 428 | provider: PROVIDER, |
| 396 | sessionId: context.session.sessionId, | 429 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 397 | createdAt: stamp.createdAt, | 430 | createdAt: stamp.createdAt, |
| 398 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 431 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 399 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 432 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 400 | payload: { | 433 | payload: { |
| 401 | message, | 434 | message, |
| 402 | ...(detail !== undefined ? { detail } : {}), | 435 | ...(detail !== undefined ? { detail } : {}), |
| ... | @@ -426,11 +459,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -426,11 +459,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 426 | type: "item.completed", | 459 | type: "item.completed", |
| 427 | eventId: itemStamp.eventId, | 460 | eventId: itemStamp.eventId, |
| 428 | provider: PROVIDER, | 461 | provider: PROVIDER, |
| 429 | sessionId: context.session.sessionId, | 462 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 430 | createdAt: itemStamp.createdAt, | 463 | createdAt: itemStamp.createdAt, |
| 431 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 464 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 432 | turnId: turnState.turnId, | 465 | turnId: asRuntimeTurnId(turnState.turnId), |
| 433 | itemId: turnState.assistantItemId, | 466 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 434 | payload: { | 467 | payload: { |
| 435 | itemType: "assistant_message", | 468 | itemType: "assistant_message", |
| 436 | status: "completed", | 469 | status: "completed", |
| ... | @@ -454,10 +487,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -454,10 +487,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 454 | type: "turn.completed", | 487 | type: "turn.completed", |
| 455 | eventId: stamp.eventId, | 488 | eventId: stamp.eventId, |
| 456 | provider: PROVIDER, | 489 | provider: PROVIDER, |
| 457 | sessionId: context.session.sessionId, | 490 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 458 | createdAt: stamp.createdAt, | 491 | createdAt: stamp.createdAt, |
| 459 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 492 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 460 | turnId: turnState.turnId, | 493 | turnId: asRuntimeTurnId(turnState.turnId), |
| 461 | payload: { | 494 | payload: { |
| 462 | state, | 495 | state, |
| 463 | ...(stopReason ? { stopReason } : {}), | 496 | ...(stopReason ? { stopReason } : {}), |
| ... | @@ -534,11 +567,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -534,11 +567,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 534 | type: "request.resolved", | 567 | type: "request.resolved", |
| 535 | eventId: stamp.eventId, | 568 | eventId: stamp.eventId, |
| 536 | provider: PROVIDER, | 569 | provider: PROVIDER, |
| 537 | sessionId: context.session.sessionId, | 570 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 538 | createdAt: stamp.createdAt, | 571 | createdAt: stamp.createdAt, |
| 539 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 572 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 540 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 573 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 541 | requestId, | 574 | requestId: asRuntimeRequestId(requestId), |
| 542 | payload: { | 575 | payload: { |
| 543 | requestType, | 576 | requestType, |
| 544 | decision: selection.decision, | 577 | decision: selection.decision, |
| ... | @@ -576,11 +609,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -576,11 +609,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 576 | type: "request.opened", | 609 | type: "request.opened", |
| 577 | eventId: stamp.eventId, | 610 | eventId: stamp.eventId, |
| 578 | provider: PROVIDER, | 611 | provider: PROVIDER, |
| 579 | sessionId: context.session.sessionId, | 612 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 580 | createdAt: stamp.createdAt, | 613 | createdAt: stamp.createdAt, |
| 581 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 614 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 582 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 615 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 583 | requestId, | 616 | requestId: asRuntimeRequestId(requestId), |
| 584 | payload: { | 617 | payload: { |
| 585 | requestType, | 618 | requestType, |
| 586 | ...(detail ? { detail } : {}), | 619 | ...(detail ? { detail } : {}), |
| ... | @@ -621,9 +654,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -621,9 +654,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 621 | 654 | ||
| 622 | const base = { | 655 | const base = { |
| 623 | provider: PROVIDER, | 656 | provider: PROVIDER, |
| 624 | sessionId: context.session.sessionId, | 657 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 625 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 658 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 626 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 659 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 627 | providerRefs: { | 660 | providerRefs: { |
| 628 | providerSessionId: context.session.sessionId, | 661 | providerSessionId: context.session.sessionId, |
| 629 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), | 662 | ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), |
| ... | @@ -663,8 +696,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -663,8 +696,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 663 | type: "content.delta", | 696 | type: "content.delta", |
| 664 | eventId: stamp.eventId, | 697 | eventId: stamp.eventId, |
| 665 | createdAt: stamp.createdAt, | 698 | createdAt: stamp.createdAt, |
| 666 | turnId: context.turnState.turnId, | 699 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 667 | itemId: context.turnState.assistantItemId, | 700 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 668 | payload: { | 701 | payload: { |
| 669 | streamKind: "reasoning_text", | 702 | streamKind: "reasoning_text", |
| 670 | delta: update.content.text, | 703 | delta: update.content.text, |
| ... | @@ -682,8 +715,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -682,8 +715,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 682 | type: "content.delta", | 715 | type: "content.delta", |
| 683 | eventId: stamp.eventId, | 716 | eventId: stamp.eventId, |
| 684 | createdAt: stamp.createdAt, | 717 | createdAt: stamp.createdAt, |
| 685 | turnId: context.turnState.turnId, | 718 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 686 | itemId: context.turnState.assistantItemId, | 719 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 687 | payload: { | 720 | payload: { |
| 688 | streamKind: "assistant_text", | 721 | streamKind: "assistant_text", |
| 689 | delta: update.content.text, | 722 | delta: update.content.text, |
| ... | @@ -707,8 +740,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -707,8 +740,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 707 | type: "item.started", | 740 | type: "item.started", |
| 708 | eventId: stamp.eventId, | 741 | eventId: stamp.eventId, |
| 709 | createdAt: stamp.createdAt, | 742 | createdAt: stamp.createdAt, |
| 710 | turnId: context.turnState.turnId, | 743 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 711 | itemId: update.toolCallId, | 744 | itemId: asRuntimeItemId(update.toolCallId), |
| 712 | payload: { | 745 | payload: { |
| 713 | itemType, | 746 | itemType, |
| 714 | status: "inProgress", | 747 | status: "inProgress", |
| ... | @@ -726,8 +759,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -726,8 +759,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 726 | type: "item.updated", | 759 | type: "item.updated", |
| 727 | eventId: stamp.eventId, | 760 | eventId: stamp.eventId, |
| 728 | createdAt: stamp.createdAt, | 761 | createdAt: stamp.createdAt, |
| 729 | turnId: context.turnState.turnId, | 762 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 730 | itemId: update.toolCallId, | 763 | itemId: asRuntimeItemId(update.toolCallId), |
| 731 | payload: { | 764 | payload: { |
| 732 | itemType, | 765 | itemType, |
| 733 | status: "inProgress", | 766 | status: "inProgress", |
| ... | @@ -749,8 +782,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -749,8 +782,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 749 | type: eventType, | 782 | type: eventType, |
| 750 | eventId: stamp.eventId, | 783 | eventId: stamp.eventId, |
| 751 | createdAt: stamp.createdAt, | 784 | createdAt: stamp.createdAt, |
| 752 | turnId: context.turnState.turnId, | 785 | turnId: asRuntimeTurnId(context.turnState.turnId), |
| 753 | itemId: update.toolCallId, | 786 | itemId: asRuntimeItemId(update.toolCallId), |
| 754 | payload: { | 787 | payload: { |
| 755 | itemType: "command_execution", | 788 | itemType: "command_execution", |
| 756 | status, | 789 | status, |
| ... | @@ -792,7 +825,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -792,7 +825,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 792 | const nativeMethod = | 825 | const nativeMethod = |
| 793 | typeof message.method === "string" | 826 | typeof message.method === "string" |
| 794 | ? message.method | 827 | ? message.method |
| 795 | : (typeof message.id === "string" || typeof message.id === "number") | 828 | : typeof message.id === "string" || typeof message.id === "number" |
| 796 | ? "cursor/acp/response" | 829 | ? "cursor/acp/response" |
| 797 | : "cursor/acp/message"; | 830 | : "cursor/acp/message"; |
| 798 | const nativeKind = | 831 | const nativeKind = |
| ... | @@ -808,11 +841,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -808,11 +841,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 808 | id: EventId.makeUnsafe(randomUUID()), | 841 | id: EventId.makeUnsafe(randomUUID()), |
| 809 | kind: nativeKind, | 842 | kind: nativeKind, |
| 810 | provider: PROVIDER, | 843 | provider: PROVIDER, |
| 811 | sessionId: context.session.sessionId, | 844 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 812 | createdAt: new Date().toISOString(), | 845 | createdAt: new Date().toISOString(), |
| 813 | method: nativeMethod, | 846 | method: nativeMethod, |
| 814 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 847 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 815 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 848 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 816 | payload: message, | 849 | payload: message, |
| 817 | }, | 850 | }, |
| 818 | }); | 851 | }); |
| ... | @@ -905,9 +938,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -905,9 +938,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 905 | type: "session.exited", | 938 | type: "session.exited", |
| 906 | eventId: stamp.eventId, | 939 | eventId: stamp.eventId, |
| 907 | provider: PROVIDER, | 940 | provider: PROVIDER, |
| 908 | sessionId: context.session.sessionId, | 941 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 909 | createdAt: stamp.createdAt, | 942 | createdAt: stamp.createdAt, |
| 910 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 943 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 911 | payload: { | 944 | payload: { |
| 912 | reason: "Session stopped", | 945 | reason: "Session stopped", |
| 913 | exitKind: "graceful", | 946 | exitKind: "graceful", |
| ... | @@ -956,11 +989,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -956,11 +989,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 956 | } | 989 | } |
| 957 | 990 | ||
| 958 | const startedAt = yield* nowIso; | 991 | const startedAt = yield* nowIso; |
| 959 | const sessionId = ProviderSessionId.makeUnsafe(`cursor-session-${yield* Random.nextUUIDv4}`); | 992 | const sessionId = ProviderSessionId.makeUnsafe(yield* Random.nextUUIDv4); |
| 960 | const cwd = input.cwd ?? process.cwd(); | 993 | const cwd = input.cwd ?? process.cwd(); |
| 961 | const cursorOptions = input.providerOptions?.cursor as | 994 | const cursorOptions = input.providerOptions?.cursor as { binaryPath?: string } | undefined; |
| 962 | | { binaryPath?: string } | ||
| 963 | | undefined; | ||
| 964 | const binaryPath = cursorOptions?.binaryPath ?? "agent"; | 995 | const binaryPath = cursorOptions?.binaryPath ?? "agent"; |
| 965 | const resumeState = readCursorResumeState(input.resumeCursor); | 996 | const resumeState = readCursorResumeState(input.resumeCursor); |
| 966 | 997 | ||
| ... | @@ -1031,10 +1062,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1031,10 +1062,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1031 | type: "runtime.error", | 1062 | type: "runtime.error", |
| 1032 | eventId: stamp.eventId, | 1063 | eventId: stamp.eventId, |
| 1033 | provider: PROVIDER, | 1064 | provider: PROVIDER, |
| 1034 | sessionId: context.session.sessionId, | 1065 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1035 | createdAt: stamp.createdAt, | 1066 | createdAt: stamp.createdAt, |
| 1036 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1067 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1037 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1068 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1038 | payload: { | 1069 | payload: { |
| 1039 | message: error.message || "Cursor ACP process error.", | 1070 | message: error.message || "Cursor ACP process error.", |
| 1040 | class: "transport_error", | 1071 | class: "transport_error", |
| ... | @@ -1064,9 +1095,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1064,9 +1095,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1064 | type: "session.exited", | 1095 | type: "session.exited", |
| 1065 | eventId: stamp.eventId, | 1096 | eventId: stamp.eventId, |
| 1066 | provider: PROVIDER, | 1097 | provider: PROVIDER, |
| 1067 | sessionId: context.session.sessionId, | 1098 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1068 | createdAt: stamp.createdAt, | 1099 | createdAt: stamp.createdAt, |
| 1069 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1100 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1070 | payload: { | 1101 | payload: { |
| 1071 | reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`, | 1102 | reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`, |
| 1072 | exitKind: code === 0 ? "graceful" : "error", | 1103 | exitKind: code === 0 ? "graceful" : "error", |
| ... | @@ -1088,14 +1119,23 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1088,14 +1119,23 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1088 | }), | 1119 | }), |
| 1089 | catch: (cause) => toRequestError(sessionId, "initialize", cause), | 1120 | catch: (cause) => toRequestError(sessionId, "initialize", cause), |
| 1090 | }); | 1121 | }); |
| 1091 | const decodedInitialize = Schema.decodeUnknownSync(CursorAcpInitializeResult)(initializeResult); | 1122 | const decodedInitialize = yield* Effect.try({ |
| 1123 | try: () => Schema.decodeUnknownSync(CursorAcpInitializeResult)(initializeResult), | ||
| 1124 | catch: (cause) => | ||
| 1125 | new ProviderAdapterValidationError({ | ||
| 1126 | provider: PROVIDER, | ||
| 1127 | operation: "startSession", | ||
| 1128 | issue: "Cursor initialize response did not match expected schema.", | ||
| 1129 | cause, | ||
| 1130 | }), | ||
| 1131 | }); | ||
| 1092 | 1132 | ||
| 1093 | const initStamp = yield* makeEventStamp(); | 1133 | const initStamp = yield* makeEventStamp(); |
| 1094 | yield* offerRuntimeEvent({ | 1134 | yield* offerRuntimeEvent({ |
| 1095 | type: "session.configured", | 1135 | type: "session.configured", |
| 1096 | eventId: initStamp.eventId, | 1136 | eventId: initStamp.eventId, |
| 1097 | provider: PROVIDER, | 1137 | provider: PROVIDER, |
| 1098 | sessionId, | 1138 | sessionId: asRuntimeSessionId(sessionId), |
| 1099 | createdAt: initStamp.createdAt, | 1139 | createdAt: initStamp.createdAt, |
| 1100 | payload: { | 1140 | payload: { |
| 1101 | config: decodedInitialize, | 1141 | config: decodedInitialize, |
| ... | @@ -1116,7 +1156,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1116,7 +1156,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1116 | type: "auth.status", | 1156 | type: "auth.status", |
| 1117 | eventId: authStartStamp.eventId, | 1157 | eventId: authStartStamp.eventId, |
| 1118 | provider: PROVIDER, | 1158 | provider: PROVIDER, |
| 1119 | sessionId, | 1159 | sessionId: asRuntimeSessionId(sessionId), |
| 1120 | createdAt: authStartStamp.createdAt, | 1160 | createdAt: authStartStamp.createdAt, |
| 1121 | payload: { | 1161 | payload: { |
| 1122 | isAuthenticating: true, | 1162 | isAuthenticating: true, |
| ... | @@ -1133,19 +1173,24 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1133,19 +1173,24 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1133 | 1173 | ||
| 1134 | const authenticateResult = yield* Effect.tryPromise({ | 1174 | const authenticateResult = yield* Effect.tryPromise({ |
| 1135 | try: async () => sendRequest(context, "authenticate", authenticateRequest), | 1175 | try: async () => sendRequest(context, "authenticate", authenticateRequest), |
| 1136 | catch: (cause) => cause, | 1176 | catch: (cause) => toRequestError(sessionId, "authenticate", cause), |
| 1137 | }); | 1177 | }).pipe( |
| 1178 | Effect.match({ | ||
| 1179 | onFailure: (error) => ({ ok: false as const, error }), | ||
| 1180 | onSuccess: (value) => ({ ok: true as const, value }), | ||
| 1181 | }), | ||
| 1182 | ); | ||
| 1138 | const authEndStamp = yield* makeEventStamp(); | 1183 | const authEndStamp = yield* makeEventStamp(); |
| 1139 | if (authenticateResult instanceof Error) { | 1184 | if (!authenticateResult.ok) { |
| 1140 | yield* offerRuntimeEvent({ | 1185 | yield* offerRuntimeEvent({ |
| 1141 | type: "auth.status", | 1186 | type: "auth.status", |
| 1142 | eventId: authEndStamp.eventId, | 1187 | eventId: authEndStamp.eventId, |
| 1143 | provider: PROVIDER, | 1188 | provider: PROVIDER, |
| 1144 | sessionId, | 1189 | sessionId: asRuntimeSessionId(sessionId), |
| 1145 | createdAt: authEndStamp.createdAt, | 1190 | createdAt: authEndStamp.createdAt, |
| 1146 | payload: { | 1191 | payload: { |
| 1147 | isAuthenticating: false, | 1192 | isAuthenticating: false, |
| 1148 | error: toMessage(authenticateResult, "Cursor authentication failed."), | 1193 | error: toMessage(authenticateResult.error, "Cursor authentication failed."), |
| 1149 | }, | 1194 | }, |
| 1150 | providerRefs: { | 1195 | providerRefs: { |
| 1151 | providerSessionId: sessionId, | 1196 | providerSessionId: sessionId, |
| ... | @@ -1154,7 +1199,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1154,7 +1199,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1154 | source: "cursor.acp.response", | 1199 | source: "cursor.acp.response", |
| 1155 | method: "authenticate", | 1200 | method: "authenticate", |
| 1156 | payload: { | 1201 | payload: { |
| 1157 | error: toMessage(authenticateResult, "Cursor authentication failed."), | 1202 | error: toMessage(authenticateResult.error, "Cursor authentication failed."), |
| 1158 | }, | 1203 | }, |
| 1159 | }, | 1204 | }, |
| 1160 | }); | 1205 | }); |
| ... | @@ -1163,7 +1208,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1163,7 +1208,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1163 | type: "auth.status", | 1208 | type: "auth.status", |
| 1164 | eventId: authEndStamp.eventId, | 1209 | eventId: authEndStamp.eventId, |
| 1165 | provider: PROVIDER, | 1210 | provider: PROVIDER, |
| 1166 | sessionId, | 1211 | sessionId: asRuntimeSessionId(sessionId), |
| 1167 | createdAt: authEndStamp.createdAt, | 1212 | createdAt: authEndStamp.createdAt, |
| 1168 | payload: { | 1213 | payload: { |
| 1169 | isAuthenticating: false, | 1214 | isAuthenticating: false, |
| ... | @@ -1174,7 +1219,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1174,7 +1219,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1174 | raw: { | 1219 | raw: { |
| 1175 | source: "cursor.acp.response", | 1220 | source: "cursor.acp.response", |
| 1176 | method: "authenticate", | 1221 | method: "authenticate", |
| 1177 | payload: authenticateResult, | 1222 | payload: authenticateResult.value, |
| 1178 | }, | 1223 | }, |
| 1179 | }); | 1224 | }); |
| 1180 | } | 1225 | } |
| ... | @@ -1225,9 +1270,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1225,9 +1270,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1225 | type: "session.started", | 1270 | type: "session.started", |
| 1226 | eventId: sessionStartedStamp.eventId, | 1271 | eventId: sessionStartedStamp.eventId, |
| 1227 | provider: PROVIDER, | 1272 | provider: PROVIDER, |
| 1228 | sessionId, | 1273 | sessionId: asRuntimeSessionId(sessionId), |
| 1229 | createdAt: sessionStartedStamp.createdAt, | 1274 | createdAt: sessionStartedStamp.createdAt, |
| 1230 | threadId, | 1275 | threadId: asRuntimeThreadId(threadId), |
| 1231 | payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {}, | 1276 | payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {}, |
| 1232 | providerRefs: { | 1277 | providerRefs: { |
| 1233 | providerSessionId: sessionId, | 1278 | providerSessionId: sessionId, |
| ... | @@ -1240,9 +1285,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1240,9 +1285,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1240 | type: "thread.started", | 1285 | type: "thread.started", |
| 1241 | eventId: threadStartedStamp.eventId, | 1286 | eventId: threadStartedStamp.eventId, |
| 1242 | provider: PROVIDER, | 1287 | provider: PROVIDER, |
| 1243 | sessionId, | 1288 | sessionId: asRuntimeSessionId(sessionId), |
| 1244 | createdAt: threadStartedStamp.createdAt, | 1289 | createdAt: threadStartedStamp.createdAt, |
| 1245 | threadId, | 1290 | threadId: asRuntimeThreadId(threadId), |
| 1246 | payload: { | 1291 | payload: { |
| 1247 | providerThreadId: threadId, | 1292 | providerThreadId: threadId, |
| 1248 | }, | 1293 | }, |
| ... | @@ -1257,9 +1302,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1257,9 +1302,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1257 | type: "session.state.changed", | 1302 | type: "session.state.changed", |
| 1258 | eventId: readyStamp.eventId, | 1303 | eventId: readyStamp.eventId, |
| 1259 | provider: PROVIDER, | 1304 | provider: PROVIDER, |
| 1260 | sessionId, | 1305 | sessionId: asRuntimeSessionId(sessionId), |
| 1261 | createdAt: readyStamp.createdAt, | 1306 | createdAt: readyStamp.createdAt, |
| 1262 | threadId, | 1307 | threadId: asRuntimeThreadId(threadId), |
| 1263 | payload: { | 1308 | payload: { |
| 1264 | state: "ready", | 1309 | state: "ready", |
| 1265 | }, | 1310 | }, |
| ... | @@ -1286,10 +1331,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1286,10 +1331,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1286 | }); | 1331 | }); |
| 1287 | } | 1332 | } |
| 1288 | 1333 | ||
| 1289 | const turnId = ProviderTurnId.makeUnsafe(`cursor-turn-${yield* Random.nextUUIDv4}`); | 1334 | const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1290 | const turnState: CursorTurnState = { | 1335 | const turnState: CursorTurnState = { |
| 1291 | turnId, | 1336 | turnId, |
| 1292 | assistantItemId: `cursor-assistant-${yield* Random.nextUUIDv4}`, | 1337 | assistantItemId: asProviderItemId(yield* Random.nextUUIDv4), |
| 1293 | startedToolCalls: new Set(), | 1338 | startedToolCalls: new Set(), |
| 1294 | items: [], | 1339 | items: [], |
| 1295 | }; | 1340 | }; |
| ... | @@ -1307,10 +1352,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1307,10 +1352,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1307 | type: "turn.started", | 1352 | type: "turn.started", |
| 1308 | eventId: startedStamp.eventId, | 1353 | eventId: startedStamp.eventId, |
| 1309 | provider: PROVIDER, | 1354 | provider: PROVIDER, |
| 1310 | sessionId: context.session.sessionId, | 1355 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1311 | createdAt: startedStamp.createdAt, | 1356 | createdAt: startedStamp.createdAt, |
| 1312 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1357 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1313 | turnId, | 1358 | turnId: asRuntimeTurnId(turnId), |
| 1314 | payload: { | 1359 | payload: { |
| 1315 | ...(input.model ? { model: input.model } : {}), | 1360 | ...(input.model ? { model: input.model } : {}), |
| 1316 | ...(input.effort ? { effort: input.effort } : {}), | 1361 | ...(input.effort ? { effort: input.effort } : {}), |
| ... | @@ -1340,7 +1385,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1340,7 +1385,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1340 | catch: (cause) => toRequestError(input.sessionId, "session/prompt", cause), | 1385 | catch: (cause) => toRequestError(input.sessionId, "session/prompt", cause), |
| 1341 | }); | 1386 | }); |
| 1342 | 1387 | ||
| 1343 | const promptResult = Schema.decodeUnknownSync(CursorAcpSessionPromptResult)(promptResultRaw); | 1388 | const promptResult = yield* Effect.try({ |
| 1389 | try: () => Schema.decodeUnknownSync(CursorAcpSessionPromptResult)(promptResultRaw), | ||
| 1390 | catch: (cause) => | ||
| 1391 | new ProviderAdapterValidationError({ | ||
| 1392 | provider: PROVIDER, | ||
| 1393 | operation: "sendTurn", | ||
| 1394 | issue: "Cursor session/prompt response did not match expected schema.", | ||
| 1395 | cause, | ||
| 1396 | }), | ||
| 1397 | }); | ||
| 1344 | const turnStateValue = mapStopReasonToTurnState(promptResult.stopReason); | 1398 | const turnStateValue = mapStopReasonToTurnState(promptResult.stopReason); |
| 1345 | yield* completeTurn( | 1399 | yield* completeTurn( |
| 1346 | context, | 1400 | context, |
| ... | @@ -1376,15 +1430,21 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1376,15 +1430,21 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1376 | } | 1430 | } |
| 1377 | 1431 | ||
| 1378 | const cancelResult = yield* Effect.tryPromise({ | 1432 | const cancelResult = yield* Effect.tryPromise({ |
| 1379 | try: async () => sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000), | 1433 | try: async () => |
| 1380 | catch: (cause) => cause, | 1434 | sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000), |
| 1381 | }); | 1435 | catch: (cause) => toRequestError(sessionId, "session/cancel", cause), |
| 1436 | }).pipe( | ||
| 1437 | Effect.match({ | ||
| 1438 | onFailure: (error) => ({ ok: false as const, error }), | ||
| 1439 | onSuccess: () => ({ ok: true as const }), | ||
| 1440 | }), | ||
| 1441 | ); | ||
| 1382 | 1442 | ||
| 1383 | if (cancelResult instanceof Error) { | 1443 | if (!cancelResult.ok) { |
| 1384 | yield* emitRuntimeWarning( | 1444 | yield* emitRuntimeWarning( |
| 1385 | context, | 1445 | context, |
| 1386 | "Cursor ACP session/cancel is unavailable; marking turn as interrupted.", | 1446 | "Cursor ACP session/cancel is unavailable; marking turn as interrupted.", |
| 1387 | cancelResult, | 1447 | cancelResult.error, |
| 1388 | ); | 1448 | ); |
| 1389 | } | 1449 | } |
| 1390 | 1450 | ||
| ... | @@ -1456,11 +1516,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { | ... | @@ -1456,11 +1516,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) { |
| 1456 | type: "request.resolved", | 1516 | type: "request.resolved", |
| 1457 | eventId: stamp.eventId, | 1517 | eventId: stamp.eventId, |
| 1458 | provider: PROVIDER, | 1518 | provider: PROVIDER, |
| 1459 | sessionId: context.session.sessionId, | 1519 | sessionId: asRuntimeSessionId(context.session.sessionId), |
| 1460 | createdAt: stamp.createdAt, | 1520 | createdAt: stamp.createdAt, |
| 1461 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | 1521 | ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})), |
| 1462 | ...(context.turnState ? { turnId: context.turnState.turnId } : {}), | 1522 | ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})), |
| 1463 | requestId, | 1523 | requestId: asRuntimeRequestId(requestId), |
| 1464 | payload: { | 1524 | payload: { |
| 1465 | requestType: pending.requestType, | 1525 | requestType: pending.requestType, |
| 1466 | decision, | 1526 | decision, |
apps/server/src/provider/Layers/ProviderService.test.ts+66-37| ... | @@ -52,6 +52,20 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make | ... | @@ -52,6 +52,20 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make |
| 52 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); | 52 | const asEventId = (value: string): EventId => EventId.makeUnsafe(value); |
| 53 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); | 53 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| 54 | 54 | ||
| 55 | type LegacyProviderRuntimeEvent = { | ||
| 56 | readonly type: string; | ||
| 57 | readonly eventId: EventId; | ||
| 58 | readonly provider: "codex" | "claudeCode" | "cursor"; | ||
| 59 | readonly sessionId: string; | ||
| 60 | readonly createdAt: string; | ||
| 61 | readonly threadId?: string | undefined; | ||
| 62 | readonly turnId?: string | undefined; | ||
| 63 | readonly itemId?: string | undefined; | ||
| 64 | readonly requestId?: string | undefined; | ||
| 65 | readonly payload?: unknown | undefined; | ||
| 66 | readonly [key: string]: unknown; | ||
| 67 | }; | ||
| 68 | |||
| 55 | function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | 69 | function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 56 | const sessions = new Map<ProviderSessionId, ProviderSession>(); | 70 | const sessions = new Map<ProviderSessionId, ProviderSession>(); |
| 57 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 71 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| ... | @@ -177,8 +191,8 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { | ... | @@ -177,8 +191,8 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { |
| 177 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), | 191 | streamEvents: Stream.fromPubSub(runtimeEventPubSub), |
| 178 | }; | 192 | }; |
| 179 | 193 | ||
| 180 | const emit = (event: ProviderRuntimeEvent): void => { | 194 | const emit = (event: LegacyProviderRuntimeEvent): void => { |
| 181 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event)); | 195 | Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent)); |
| 182 | }; | 196 | }; |
| 183 | 197 | ||
| 184 | return { | 198 | return { |
| ... | @@ -383,16 +397,21 @@ it.effect( | ... | @@ -383,16 +397,21 @@ it.effect( |
| 383 | }); | 397 | }); |
| 384 | }).pipe(Effect.provide(secondProviderLayer)); | 398 | }).pipe(Effect.provide(secondProviderLayer)); |
| 385 | 399 | ||
| 386 | assert.deepEqual(secondCodex.startSession.mock.calls, [ | 400 | assert.equal(secondCodex.startSession.mock.calls.length, 1); |
| 387 | [ | 401 | const resumedStartInput = secondCodex.startSession.mock.calls[0]?.[0]; |
| 388 | { | 402 | assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); |
| 389 | provider: "codex", | 403 | if (resumedStartInput && typeof resumedStartInput === "object") { |
| 390 | cwd: "/tmp/project", | 404 | const startPayload = resumedStartInput as { |
| 391 | resumeThreadId: startedSession.threadId, | 405 | provider?: string; |
| 392 | resumeCursor: startedSession.resumeCursor, | 406 | cwd?: string; |
| 393 | }, | 407 | resumeCursor?: unknown; |
| 394 | ], | 408 | resumeThreadId?: string; |
| 395 | ]); | 409 | }; |
| 410 | assert.equal(startPayload.provider, "codex"); | ||
| 411 | assert.equal(startPayload.cwd, "/tmp/project"); | ||
| 412 | assert.deepEqual(startPayload.resumeCursor, startedSession.resumeCursor); | ||
| 413 | assert.equal(startPayload.resumeThreadId, "thread-1"); | ||
| 414 | } | ||
| 396 | assert.equal(secondCodex.rollbackThread.mock.calls.length, 1); | 415 | assert.equal(secondCodex.rollbackThread.mock.calls.length, 1); |
| 397 | const rollbackCall = secondCodex.rollbackThread.mock.calls[0]; | 416 | const rollbackCall = secondCodex.rollbackThread.mock.calls[0]; |
| 398 | assert.equal(typeof rollbackCall?.[0], "string"); | 417 | assert.equal(typeof rollbackCall?.[0], "string"); |
| ... | @@ -493,16 +512,21 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -493,16 +512,21 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 493 | numTurns: 1, | 512 | numTurns: 1, |
| 494 | }); | 513 | }); |
| 495 | 514 | ||
| 496 | assert.deepEqual(routing.codex.startSession.mock.calls, [ | 515 | assert.equal(routing.codex.startSession.mock.calls.length, 1); |
| 497 | [ | 516 | const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; |
| 498 | { | 517 | assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); |
| 499 | provider: "codex", | 518 | if (resumedStartInput && typeof resumedStartInput === "object") { |
| 500 | cwd: "/tmp/project", | 519 | const startPayload = resumedStartInput as { |
| 501 | resumeThreadId: initial.threadId, | 520 | provider?: string; |
| 502 | resumeCursor: initial.resumeCursor, | 521 | cwd?: string; |
| 503 | }, | 522 | resumeCursor?: unknown; |
| 504 | ], | 523 | resumeThreadId?: string; |
| 505 | ]); | 524 | }; |
| 525 | assert.equal(startPayload.provider, "codex"); | ||
| 526 | assert.equal(startPayload.cwd, "/tmp/project"); | ||
| 527 | assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); | ||
| 528 | assert.equal(startPayload.resumeThreadId, "thread-2"); | ||
| 529 | } | ||
| 506 | assert.equal(routing.codex.rollbackThread.mock.calls.length, 1); | 530 | assert.equal(routing.codex.rollbackThread.mock.calls.length, 1); |
| 507 | const rollbackCall = routing.codex.rollbackThread.mock.calls[0]; | 531 | const rollbackCall = routing.codex.rollbackThread.mock.calls[0]; |
| 508 | assert.equal(rollbackCall?.[1], 1); | 532 | assert.equal(rollbackCall?.[1], 1); |
| ... | @@ -528,16 +552,21 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -528,16 +552,21 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 528 | attachments: [], | 552 | attachments: [], |
| 529 | }); | 553 | }); |
| 530 | 554 | ||
| 531 | assert.deepEqual(routing.codex.startSession.mock.calls, [ | 555 | assert.equal(routing.codex.startSession.mock.calls.length, 1); |
| 532 | [ | 556 | const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0]; |
| 533 | { | 557 | assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); |
| 534 | provider: "codex", | 558 | if (resumedStartInput && typeof resumedStartInput === "object") { |
| 535 | cwd: "/tmp/project-send-turn", | 559 | const startPayload = resumedStartInput as { |
| 536 | resumeThreadId: initial.threadId, | 560 | provider?: string; |
| 537 | resumeCursor: initial.resumeCursor, | 561 | cwd?: string; |
| 538 | }, | 562 | resumeCursor?: unknown; |
| 539 | ], | 563 | resumeThreadId?: string; |
| 540 | ]); | 564 | }; |
| 565 | assert.equal(startPayload.provider, "codex"); | ||
| 566 | assert.equal(startPayload.cwd, "/tmp/project-send-turn"); | ||
| 567 | assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); | ||
| 568 | assert.equal(startPayload.resumeThreadId, "thread-4"); | ||
| 569 | } | ||
| 541 | assert.equal(routing.codex.sendTurn.mock.calls.length, 1); | 570 | assert.equal(routing.codex.sendTurn.mock.calls.length, 1); |
| 542 | }), | 571 | }), |
| 543 | ); | 572 | ); |
| ... | @@ -626,7 +655,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -626,7 +655,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 626 | ).pipe(Effect.forkChild); | 655 | ).pipe(Effect.forkChild); |
| 627 | yield* sleep(20); | 656 | yield* sleep(20); |
| 628 | 657 | ||
| 629 | const completedEvent: ProviderRuntimeEvent = { | 658 | const completedEvent: LegacyProviderRuntimeEvent = { |
| 630 | type: "turn.completed", | 659 | type: "turn.completed", |
| 631 | eventId: asEventId("evt-1"), | 660 | eventId: asEventId("evt-1"), |
| 632 | provider: "codex", | 661 | provider: "codex", |
| ... | @@ -650,7 +679,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -650,7 +679,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 650 | }), | 679 | }), |
| 651 | ); | 680 | ); |
| 652 | 681 | ||
| 653 | it.effect("assigns monotonic per-session sequence numbers to canonical runtime events", () => | 682 | it.effect("fans out canonical runtime events in emission order", () => |
| 654 | Effect.gen(function* () { | 683 | Effect.gen(function* () { |
| 655 | const provider = yield* ProviderService; | 684 | const provider = yield* ProviderService; |
| 656 | const session = yield* provider.startSession(asThreadId("thread-seq"), { | 685 | const session = yield* provider.startSession(asThreadId("thread-seq"), { |
| ... | @@ -700,8 +729,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -700,8 +729,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 700 | yield* Fiber.join(consumer); | 729 | yield* Fiber.join(consumer); |
| 701 | const received = yield* Ref.get(receivedRef); | 730 | const received = yield* Ref.get(receivedRef); |
| 702 | assert.deepEqual( | 731 | assert.deepEqual( |
| 703 | received.map((event) => event.sessionSequence), | 732 | received.map((event) => event.eventId), |
| 704 | [1, 2, 3], | 733 | [asEventId("evt-seq-1"), asEventId("evt-seq-2"), asEventId("evt-seq-3")], |
| 705 | ); | 734 | ); |
| 706 | }), | 735 | }), |
| 707 | ); | 736 | ); |
| ... | @@ -729,7 +758,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -729,7 +758,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 729 | ); | 758 | ); |
| 730 | yield* sleep(20); | 759 | yield* sleep(20); |
| 731 | 760 | ||
| 732 | const events: ReadonlyArray<ProviderRuntimeEvent> = [ | 761 | const events: ReadonlyArray<LegacyProviderRuntimeEvent> = [ |
| 733 | { | 762 | { |
| 734 | type: "tool.completed", | 763 | type: "tool.completed", |
| 735 | eventId: asEventId("evt-ordered-1"), | 764 | eventId: asEventId("evt-ordered-1"), |
apps/server/src/provider/Layers/ProviderService.ts+5-31| ... | @@ -12,6 +12,7 @@ | ... | @@ -12,6 +12,7 @@ |
| 12 | import { | 12 | import { |
| 13 | NonNegativeInt, | 13 | NonNegativeInt, |
| 14 | ProviderSessionId, | 14 | ProviderSessionId, |
| 15 | RuntimeSessionId, | ||
| 15 | ThreadId, | 16 | ThreadId, |
| 16 | ProviderInterruptTurnInput, | 17 | ProviderInterruptTurnInput, |
| 17 | ProviderRespondToRequestInput, | 18 | ProviderRespondToRequestInput, |
| ... | @@ -123,7 +124,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -123,7 +124,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 123 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( | 124 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( |
| 124 | new Map(), | 125 | new Map(), |
| 125 | ); | 126 | ); |
| 126 | const sessionRuntimeSequenceRef = yield* Ref.make<Map<ProviderSessionId, number>>(new Map()); | 127 | const asRuntimeSessionId = (sessionId: ProviderSessionId): RuntimeSessionId => |
| 128 | RuntimeSessionId.makeUnsafe(sessionId); | ||
| 127 | 129 | ||
| 128 | const canonicalizeRuntimeEventSession = ( | 130 | const canonicalizeRuntimeEventSession = ( |
| 129 | event: ProviderRuntimeEvent, | 131 | event: ProviderRuntimeEvent, |
| ... | @@ -131,10 +133,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -131,10 +133,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 131 | Ref.get(routedSessionAliasesRef).pipe( | 133 | Ref.get(routedSessionAliasesRef).pipe( |
| 132 | Effect.map((aliases) => { | 134 | Effect.map((aliases) => { |
| 133 | for (const [staleSessionId, liveSessionId] of aliases) { | 135 | for (const [staleSessionId, liveSessionId] of aliases) { |
| 134 | if (liveSessionId === event.sessionId) { | 136 | if (asRuntimeSessionId(liveSessionId) === event.sessionId) { |
| 135 | return { | 137 | return { |
| 136 | ...event, | 138 | ...event, |
| 137 | sessionId: staleSessionId, | 139 | sessionId: asRuntimeSessionId(staleSessionId), |
| 138 | } satisfies ProviderRuntimeEvent; | 140 | } satisfies ProviderRuntimeEvent; |
| 139 | } | 141 | } |
| 140 | } | 142 | } |
| ... | @@ -144,21 +146,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -144,21 +146,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 144 | 146 | ||
| 145 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => | 147 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| 146 | canonicalizeRuntimeEventSession(event).pipe( | 148 | canonicalizeRuntimeEventSession(event).pipe( |
| 147 | Effect.flatMap((canonicalEvent) => | ||
| 148 | Ref.modify(sessionRuntimeSequenceRef, (current) => { | ||
| 149 | const next = new Map(current); | ||
| 150 | const currentSequence = next.get(canonicalEvent.sessionId) ?? 0; | ||
| 151 | const sessionSequence = currentSequence + 1; | ||
| 152 | next.set(canonicalEvent.sessionId, sessionSequence); | ||
| 153 | return [ | ||
| 154 | { | ||
| 155 | ...canonicalEvent, | ||
| 156 | sessionSequence, | ||
| 157 | } satisfies ProviderRuntimeEvent, | ||
| 158 | next, | ||
| 159 | ] as const; | ||
| 160 | }), | ||
| 161 | ), | ||
| 162 | Effect.tap((canonicalEvent) => | 149 | Effect.tap((canonicalEvent) => |
| 163 | canonicalEventLogger | 150 | canonicalEventLogger |
| 164 | ? Effect.flatMap( | 151 | ? Effect.flatMap( |
| ... | @@ -223,16 +210,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -223,16 +210,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 223 | return changed ? next : current; | 210 | return changed ? next : current; |
| 224 | }); | 211 | }); |
| 225 | 212 | ||
| 226 | const clearSessionSequence = (sessionId: ProviderSessionId) => | ||
| 227 | Ref.update(sessionRuntimeSequenceRef, (current) => { | ||
| 228 | if (!current.has(sessionId)) { | ||
| 229 | return current; | ||
| 230 | } | ||
| 231 | const next = new Map(current); | ||
| 232 | next.delete(sessionId); | ||
| 233 | return next; | ||
| 234 | }); | ||
| 235 | |||
| 236 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => | 213 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => |
| 237 | Ref.update(routedSessionAliasesRef, (current) => { | 214 | Ref.update(routedSessionAliasesRef, (current) => { |
| 238 | const existing = current.get(staleSessionId); | 215 | const existing = current.get(staleSessionId); |
| ... | @@ -554,11 +531,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -554,11 +531,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 554 | if (routed.sessionId !== input.sessionId) { | 531 | if (routed.sessionId !== input.sessionId) { |
| 555 | yield* directory.remove(routed.sessionId); | 532 | yield* directory.remove(routed.sessionId); |
| 556 | yield* clearAliasesReferencing(routed.sessionId); | 533 | yield* clearAliasesReferencing(routed.sessionId); |
| 557 | yield* clearSessionSequence(routed.sessionId); | ||
| 558 | } | 534 | } |
| 559 | yield* directory.remove(input.sessionId); | 535 | yield* directory.remove(input.sessionId); |
| 560 | yield* clearAliasesReferencing(input.sessionId); | 536 | yield* clearAliasesReferencing(input.sessionId); |
| 561 | yield* clearSessionSequence(input.sessionId); | ||
| 562 | }); | 537 | }); |
| 563 | 538 | ||
| 564 | const listSessions: ProviderServiceShape["listSessions"] = () => | 539 | const listSessions: ProviderServiceShape["listSessions"] = () => |
| ... | @@ -610,7 +585,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -610,7 +585,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 610 | // Keep persisted session bindings so stale sessions can be resumed after | 585 | // Keep persisted session bindings so stale sessions can be resumed after |
| 611 | // process restart via providerThreadId. | 586 | // process restart via providerThreadId. |
| 612 | yield* Ref.set(routedSessionAliasesRef, new Map()); | 587 | yield* Ref.set(routedSessionAliasesRef, new Map()); |
| 613 | yield* Ref.set(sessionRuntimeSequenceRef, new Map()); | ||
| 614 | }); | 588 | }); |
| 615 | 589 | ||
| 616 | return { | 590 | return { |
apps/server/src/wsServer.test.ts+12-10| ... | @@ -1234,16 +1234,18 @@ describe("WebSocket Server", () => { | ... | @@ -1234,16 +1234,18 @@ describe("WebSocket Server", () => { |
| 1234 | return event.type === "thread.session-set"; | 1234 | return event.type === "thread.session-set"; |
| 1235 | }); | 1235 | }); |
| 1236 | 1236 | ||
| 1237 | emitRuntimeEvent({ | 1237 | emitRuntimeEvent( |
| 1238 | type: "message.delta", | 1238 | { |
| 1239 | eventId: asEventId("evt-ws-runtime-message-delta"), | 1239 | type: "message.delta", |
| 1240 | provider: "codex", | 1240 | eventId: asEventId("evt-ws-runtime-message-delta"), |
| 1241 | sessionId, | 1241 | provider: "codex", |
| 1242 | createdAt: new Date().toISOString(), | 1242 | sessionId, |
| 1243 | turnId: asProviderTurnId("turn-1"), | 1243 | createdAt: new Date().toISOString(), |
| 1244 | itemId: asProviderItemId("item-1"), | 1244 | turnId: asProviderTurnId("turn-1"), |
| 1245 | delta: "hello from runtime", | 1245 | itemId: asProviderItemId("item-1"), |
| 1246 | }); | 1246 | delta: "hello from runtime", |
| 1247 | } as unknown as ProviderRuntimeEvent, | ||
| 1248 | ); | ||
| 1247 | 1249 | ||
| 1248 | const domainPush = await waitForPush(ws, ORCHESTRATION_WS_CHANNELS.domainEvent, (push) => { | 1250 | const domainPush = await waitForPush(ws, ORCHESTRATION_WS_CHANNELS.domainEvent, (push) => { |
| 1249 | const event = push.data as { type?: string; payload?: { messageId?: string; text?: string } }; | 1251 | const event = push.data as { type?: string; payload?: { messageId?: string; text?: string } }; |
apps/web/src/session-logic.test.ts+29| ... | @@ -100,6 +100,35 @@ describe("derivePendingApprovals", () => { | ... | @@ -100,6 +100,35 @@ describe("derivePendingApprovals", () => { |
| 100 | }, | 100 | }, |
| 101 | ]); | 101 | ]); |
| 102 | }); | 102 | }); |
| 103 | |||
| 104 | it("clears stale pending approvals when provider reports unknown pending request", () => { | ||
| 105 | const activities: OrchestrationThreadActivity[] = [ | ||
| 106 | makeActivity({ | ||
| 107 | id: "approval-open-stale", | ||
| 108 | createdAt: "2026-02-23T00:00:01.000Z", | ||
| 109 | kind: "approval.requested", | ||
| 110 | summary: "Command approval requested", | ||
| 111 | tone: "approval", | ||
| 112 | payload: { | ||
| 113 | requestId: "req-stale-1", | ||
| 114 | requestKind: "command", | ||
| 115 | }, | ||
| 116 | }), | ||
| 117 | makeActivity({ | ||
| 118 | id: "approval-failed-stale", | ||
| 119 | createdAt: "2026-02-23T00:00:02.000Z", | ||
| 120 | kind: "provider.approval.respond.failed", | ||
| 121 | summary: "Provider approval response failed", | ||
| 122 | tone: "error", | ||
| 123 | payload: { | ||
| 124 | requestId: "req-stale-1", | ||
| 125 | detail: "Unknown pending permission request: req-stale-1", | ||
| 126 | }, | ||
| 127 | }), | ||
| 128 | ]; | ||
| 129 | |||
| 130 | expect(derivePendingApprovals(activities)).toEqual([]); | ||
| 131 | }); | ||
| 103 | }); | 132 | }); |
| 104 | 133 | ||
| 105 | describe("deriveWorkLogEntries", () => { | 134 | describe("deriveWorkLogEntries", () => { |
apps/web/src/session-logic.ts+1| ... | @@ -138,6 +138,7 @@ export function derivePendingApprovals( | ... | @@ -138,6 +138,7 @@ export function derivePendingApprovals( |
| 138 | 138 | ||
| 139 | if (activity.kind === "approval.resolved" && requestId) { | 139 | if (activity.kind === "approval.resolved" && requestId) { |
| 140 | openByRequestId.delete(requestId); | 140 | openByRequestId.delete(requestId); |
| 141 | continue; | ||
| 141 | } | 142 | } |
| 142 | } | 143 | } |
| 143 | 144 |
packages/contracts/src/model.test.ts+3-3| ... | @@ -117,7 +117,7 @@ describe("cursor model selection", () => { | ... | @@ -117,7 +117,7 @@ describe("cursor model selection", () => { |
| 117 | thinking: false, | 117 | thinking: false, |
| 118 | }); | 118 | }); |
| 119 | expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual({ | 119 | expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual({ |
| 120 | family: "gpt-5.2-codex", | 120 | family: "gpt-5.3-codex", |
| 121 | reasoning: "normal", | 121 | reasoning: "normal", |
| 122 | fast: false, | 122 | fast: false, |
| 123 | thinking: false, | 123 | thinking: false, |
| ... | @@ -142,11 +142,11 @@ describe("cursor model selection", () => { | ... | @@ -142,11 +142,11 @@ describe("cursor model selection", () => { |
| 142 | it("resolves codex family selections into concrete model ids", () => { | 142 | it("resolves codex family selections into concrete model ids", () => { |
| 143 | expect( | 143 | expect( |
| 144 | resolveCursorModelFromSelection({ | 144 | resolveCursorModelFromSelection({ |
| 145 | family: "gpt-5.2-codex", | 145 | family: "gpt-5.3-codex", |
| 146 | reasoning: "xhigh", | 146 | reasoning: "xhigh", |
| 147 | fast: true, | 147 | fast: true, |
| 148 | }), | 148 | }), |
| 149 | ).toBe("gpt-5.2-codex-xhigh-fast"); | 149 | ).toBe("gpt-5.3-codex-xhigh-fast"); |
| 150 | }); | 150 | }); |
| 151 | }); | 151 | }); |
| 152 | 152 |