authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-03 18:37:32-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log7b71e234e0c7d9e937cad940a6a2474d060ca3c6
treeff0d12284723a82a1b830151b25114b48a1b1aac
parent77f67d1766e6b2afa7e3b87a6da52cc652ceb8bf

Normalize runtime events and tighten orchestration approval flow

- 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 requests

20 files changed, 765 insertions(+), 497 deletions(-)

AGENTS.md+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1# CLAUDE.md1# AGENTS.md
22
3## Task Completion Requirements3## Task Completion Requirements
44
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";
14import { Effect, Queue, Stream } from "effect";17import { Effect, Queue, Stream } from "effect";
1518
...@@ -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";
2629
27export interface TestTurnResponse {30export 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}
3437
38export 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.
53export type LegacyProviderRuntimeEvent = FixtureProviderRuntimeEvent;
54
35interface SessionState {55interface 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}
4262
63function isRecord(value: unknown): value is Record<string, unknown> {
64 return typeof value === "object" && value !== null;
65}
66
67function 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
79function 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
92function 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
102function 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
43export interface TestProviderAdapterHarness {183export 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 }
184324
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
325476
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 @@
1import {1import { EventId, RuntimeRequestId } from "@t3tools/contracts";
2 ApprovalRequestId,2import type { LegacyProviderRuntimeEvent } from "../TestProviderAdapter.integration.ts";
3 EventId,
4 ProviderSessionId,
5 ProviderThreadId,
6 ProviderTurnId,
7 type ProviderRuntimeEvent,
8} from "@t3tools/contracts";
93
10const PROVIDER = "codex" as const;4const PROVIDER = "codex" as const;
11const SESSION_ID = ProviderSessionId.makeUnsafe("fixture-session");5const SESSION_ID = "fixture-session";
12const THREAD_ID = ProviderThreadId.makeUnsafe("fixture-thread");6const THREAD_ID = "fixture-thread";
13const TURN_ID = ProviderTurnId.makeUnsafe("fixture-turn");7const TURN_ID = "fixture-turn";
14const REQUEST_ID = ApprovalRequestId.makeUnsafe("req-1");8const REQUEST_ID = RuntimeRequestId.makeUnsafe("req-1");
159
16function baseEvent(10function 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>;
5760
58export const codexTurnToolFixture = [61export 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>;
98112
99export const codexTurnApprovalFixture = [113export 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
45const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);45const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
46const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);46const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
4747
48type 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
48function createProviderServiceHarness(62function 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 };
88102
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 };
92106
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 });
368382
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 });
448462
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 });
466480
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 });
527541
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 });
575589
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 });
667681
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 });
680694
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();
684698
...@@ -715,11 +729,11 @@ describe("CheckpointReactor", () => {...@@ -715,11 +729,11 @@ describe("CheckpointReactor", () => {
715 status: "completed",729 status: "completed",
716 });730 });
717731
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 });
725739
...@@ -757,15 +771,14 @@ describe("CheckpointReactor", () => {...@@ -757,15 +771,14 @@ describe("CheckpointReactor", () => {
757 );771 );
758772
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 });
770783
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";
12import { Cause, Effect, Layer, Option, Queue, Stream } from "effect";13import { Cause, Effect, Layer, Option, Queue, Stream } from "effect";
1314
...@@ -34,11 +35,15 @@ type ReactorInput =...@@ -34,11 +35,15 @@ type ReactorInput =
34 };35 };
3536
36function toTurnId(value: string | undefined): TurnId | null {37function 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}
3940
40function toProviderThreadId(value: string | undefined): ProviderThreadId | null {41function 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
45function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId {
46 return ProviderSessionId.makeUnsafe(String(value));
42}47}
4348
44function sameId(left: string | null | undefined, right: string | null | undefined): boolean {49function 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* () {
179185
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* () {
333340
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 });
677601
678 const processInput = (602 const processInput = (
...@@ -717,11 +641,7 @@ const make = Effect.gen(function* () {...@@ -717,11 +641,7 @@ const make = Effect.gen(function* () {
717641
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 });
779779
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 });
874874
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";
15import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect";15import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Schema, Stream } from "effect";
1616
17import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";17import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
18import { GitCore } from "../../git/Services/GitCore.ts";18import { 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}
8585
86function isUnknownPendingApprovalRequestError(error: unknown): boolean {86function 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 });
177177
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 });
571544
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
40const asEventId = (value: string): EventId => EventId.makeUnsafe(value);40const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
41const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value);41const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value);
4242
43type 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
43function createProviderServiceHarness() {57function createProviderServiceHarness() {
44 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());58 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
4559
...@@ -57,8 +71,8 @@ function createProviderServiceHarness() {...@@ -57,8 +71,8 @@ function createProviderServiceHarness() {
57 streamEvents: Stream.fromPubSub(runtimeEventPubSub),71 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
58 };72 };
5973
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 };
6377
64 return {78 return {
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+39-23
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import {1import {
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";
15import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect";17import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect";
1618
...@@ -50,12 +52,20 @@ type RuntimeIngestionInput =...@@ -50,12 +52,20 @@ type RuntimeIngestionInput =
50 event: TurnStartRequestedDomainEvent;52 event: TurnStartRequestedDomainEvent;
51 };53 };
5254
53function toTurnId(value: string | undefined): TurnId | undefined {55function 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}
5658
57function toProviderThreadId(value: string | undefined): ProviderThreadId | null {59function 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
63function toProviderSessionId(value: RuntimeSessionId): ProviderSessionId {
64 return ProviderSessionId.makeUnsafe(String(value));
65}
66
67function toApprovalRequestId(value: string | undefined): ApprovalRequestId | undefined {
68 return value === undefined ? undefined : ApprovalRequestId.makeUnsafe(value);
59}69}
6070
61function sameId(left: string | null | undefined, right: string | null | undefined): boolean {71function 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 {
163function runtimeEventToActivities(173function 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* () {
528543
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;
536552
...@@ -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 !== undefined634 : 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 }
682696
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 }
729743
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 });
740756
741 if (turnId) {757 if (turnId) {
742 yield* forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId);758 yield* forgetAssistantMessageId(providerSessionId, turnId, assistantMessageId);
743 }759 }
744 }760 }
745761
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 }
767783
768 if (event.type === "session.exited") {784 if (event.type === "session.exited") {
769 yield* clearTurnStateForSession(event.sessionId);785 yield* clearTurnStateForSession(providerSessionId);
770 }786 }
771787
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 }
336336
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 }
343343
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 }
634639
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 );
640645
...@@ -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 }
749754
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 }
775780
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", () => {
875880
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";
35import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";40import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";
3641
...@@ -123,9 +128,7 @@ export interface ClaudeCodeAdapterLiveOptions {...@@ -123,9 +128,7 @@ export interface ClaudeCodeAdapterLiveOptions {
123}128}
124129
125function isUuid(value: string): boolean {130function 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}
130133
131function isSyntheticClaudeThreadId(value: string): boolean {134function 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}
141144
145function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId {
146 return RuntimeSessionId.makeUnsafe(value);
147}
148
149function asRuntimeThreadId(value: ProviderThreadId): ThreadId {
150 return ThreadId.makeUnsafe(value);
151}
152
153function asRuntimeTurnId(value: ProviderTurnId): TurnId {
154 return TurnId.makeUnsafe(value);
155}
156
157function asRuntimeItemId(value: string): RuntimeItemId {
158 return RuntimeItemId.makeUnsafe(value);
159}
160
161function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {
162 return RuntimeRequestId.makeUnsafe(value);
163}
164
142function toPermissionMode(value: unknown): PermissionMode | undefined {165function 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 }
17071740
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";
19import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect";25import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect";
2026
...@@ -278,7 +284,27 @@ function contentStreamKindFromMethod(...@@ -278,7 +284,27 @@ function contentStreamKindFromMethod(
278 }284 }
279}285}
280286
281function eventRawSource(event: ProviderEvent): ProviderRuntimeEvent["raw"]["source"] {287function asRuntimeSessionId(sessionId: ProviderSessionId): RuntimeSessionId {
288 return RuntimeSessionId.makeUnsafe(sessionId);
289}
290
291function asRuntimeThreadId(threadId: ProviderThreadId): ThreadId {
292 return ThreadId.makeUnsafe(threadId);
293}
294
295function asRuntimeTurnId(turnId: ProviderTurnId): TurnId {
296 return TurnId.makeUnsafe(turnId);
297}
298
299function asRuntimeItemId(itemId: ProviderItemId): RuntimeItemId {
300 return RuntimeItemId.makeUnsafe(itemId);
301}
302
303function asRuntimeRequestId(requestId: string): RuntimeRequestId {
304 return RuntimeRequestId.makeUnsafe(requestId);
305}
306
307function 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}
284310
...@@ -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 }
325325
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 }
451456
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 );
457462
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";
26import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect";32import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect";
2733
...@@ -67,7 +73,7 @@ interface PendingPermission {...@@ -67,7 +73,7 @@ interface PendingPermission {
6773
68interface CursorTurnState {74interface 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}
108114
115function asRuntimeSessionId(value: ProviderSessionId): RuntimeSessionId {
116 return RuntimeSessionId.makeUnsafe(value);
117}
118
119function asRuntimeThreadId(value: ProviderThreadId): ThreadId {
120 return ThreadId.makeUnsafe(value);
121}
122
123function asRuntimeTurnId(value: ProviderTurnId): TurnId {
124 return TurnId.makeUnsafe(value);
125}
126
127function asRuntimeItemId(value: string): RuntimeItemId {
128 return RuntimeItemId.makeUnsafe(value);
129}
130
131function asProviderItemId(value: string): ProviderItemId {
132 return ProviderItemId.makeUnsafe(value);
133}
134
135function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {
136 return RuntimeRequestId.makeUnsafe(value);
137}
138
109function toSessionError(139function 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}
254284
255function mapStopReasonToTurnState(stopReason: string | undefined):285function 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 };
359387
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 };
381412
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 );
384417
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) {
621654
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.method827 ? 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 }
957990
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 as994 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);
966997
...@@ -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 });
10921132
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) {
11331173
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 }
12881333
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 });
13421387
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 }
13771431
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 );
13821442
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 }
13901450
...@@ -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
52const asEventId = (value: string): EventId => EventId.makeUnsafe(value);52const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
53const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);53const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);
5454
55type 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
55function makeFakeCodexAdapter(provider: ProviderKind = "codex") {69function 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 };
179193
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 };
183197
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));
385399
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 });
495514
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 });
530554
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);
628657
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 );
652681
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);
731760
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 @@
12import {12import {
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);
127129
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) =>
144146
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 canonicalEventLogger150 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 });
225212
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 });
563538
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 after585 // 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 });
615589
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 });
12361236
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 );
12471249
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});
104133
105describe("deriveWorkLogEntries", () => {134describe("deriveWorkLogEntries", () => {
apps/web/src/session-logic.ts+1
...@@ -138,6 +138,7 @@ export function derivePendingApprovals(...@@ -138,6 +138,7 @@ export function derivePendingApprovals(
138138
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 }
143144
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});
152152