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# CLAUDE.md
1# AGENTS.md
22
33## Task Completion Requirements
44
apps/server/integration/TestProviderAdapter.integration.ts+165-11
......@@ -5,11 +5,14 @@ import {
55 EventId,
66 ProviderApprovalDecision,
77 ProviderRuntimeEvent,
8 RuntimeSessionId,
89 ProviderSession,
910 ProviderSessionId,
1011 ProviderThreadId,
1112 ProviderTurnId,
1213 ProviderTurnStartResult,
14 ThreadId,
15 TurnId,
1316} from "@t3tools/contracts";
1417import { Effect, Queue, Stream } from "effect";
1518
......@@ -25,13 +28,30 @@ import type {
2528} from "../src/provider/Services/ProviderAdapter.ts";
2629
2730export interface TestTurnResponse {
28 readonly events: ReadonlyArray<ProviderRuntimeEvent>;
31 readonly events: ReadonlyArray<FixtureProviderRuntimeEvent>;
2932 readonly mutateWorkspace?: (input: {
3033 readonly cwd: string;
3134 readonly turnCount: number;
3235 }) => Effect.Effect<void, never>;
3336}
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
3555interface SessionState {
3656 readonly session: ProviderSession;
3757 snapshot: ProviderThreadSnapshot;
......@@ -40,6 +60,126 @@ interface SessionState {
4060 readonly rollbackCalls: Array<number>;
4161}
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
43183export interface TestProviderAdapterHarness {
44184 readonly adapter: ProviderAdapterShape<ProviderAdapterError>;
45185 readonly provider: "codex" | "claudeCode";
......@@ -172,19 +312,28 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
172312 ...(fixtureEvent as Record<string, unknown>),
173313 eventId: randomUUID(),
174314 provider,
175 sessionId: input.sessionId,
315 sessionId: RuntimeSessionId.makeUnsafe(input.sessionId),
176316 createdAt: nowIso(),
177317 };
178318 if (Object.hasOwn(rawEvent, "threadId")) {
179 rawEvent.threadId = state.snapshot.threadId;
319 rawEvent.threadId = ThreadId.makeUnsafe(state.snapshot.threadId);
180320 }
181321 if (Object.hasOwn(rawEvent, "turnId")) {
182 rawEvent.turnId = turnId;
322 rawEvent.turnId = TurnId.makeUnsafe(turnId);
183323 }
184324
185 const runtimeEvent = rawEvent as ProviderRuntimeEvent;
186 if (runtimeEvent.type === "message.delta") {
187 assistantDeltas.push(runtimeEvent.delta);
325 const runtimeEvent = normalizeFixtureEvent(rawEvent);
326 const runtimeType = (runtimeEvent as { type: string }).type;
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 }
188337 }
189338 if (runtimeEvent.type === "turn.completed") {
190339 deferredTurnCompletedEvents.push(runtimeEvent);
......@@ -223,11 +372,13 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
223372 type: "turn.completed",
224373 eventId: EventId.makeUnsafe(randomUUID()),
225374 provider,
226 sessionId: input.sessionId,
375 sessionId: RuntimeSessionId.makeUnsafe(input.sessionId),
227376 createdAt: nowIso(),
228 threadId: state.snapshot.threadId,
229 turnId,
230 status: "completed",
377 threadId: ThreadId.makeUnsafe(state.snapshot.threadId),
378 turnId: TurnId.makeUnsafe(turnId),
379 payload: {
380 state: "completed",
381 },
231382 });
232383 } else {
233384 for (const completedEvent of deferredTurnCompletedEvents) {
......@@ -325,6 +476,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
325476
326477 const adapter: ProviderAdapterShape<ProviderAdapterError> = {
327478 provider,
479 capabilities: {
480 sessionModelSwitch: "in-session",
481 },
328482 startSession,
329483 sendTurn,
330484 interruptTurn,
apps/server/integration/fixtures/providerRuntime.ts+65-41
......@@ -1,22 +1,16 @@
1import {
2 ApprovalRequestId,
3 EventId,
4 ProviderSessionId,
5 ProviderThreadId,
6 ProviderTurnId,
7 type ProviderRuntimeEvent,
8} from "@t3tools/contracts";
1import { EventId, RuntimeRequestId } from "@t3tools/contracts";
2import type { LegacyProviderRuntimeEvent } from "../TestProviderAdapter.integration.ts";
93
104const PROVIDER = "codex" as const;
11const SESSION_ID = ProviderSessionId.makeUnsafe("fixture-session");
12const THREAD_ID = ProviderThreadId.makeUnsafe("fixture-thread");
13const TURN_ID = ProviderTurnId.makeUnsafe("fixture-turn");
14const REQUEST_ID = ApprovalRequestId.makeUnsafe("req-1");
5const SESSION_ID = "fixture-session";
6const THREAD_ID = "fixture-thread";
7const TURN_ID = "fixture-turn";
8const REQUEST_ID = RuntimeRequestId.makeUnsafe("req-1");
159
1610function baseEvent(
1711 eventId: string,
1812 createdAt: string,
19): Pick<ProviderRuntimeEvent, "eventId" | "provider" | "sessionId" | "createdAt"> {
13): Pick<LegacyProviderRuntimeEvent, "eventId" | "provider" | "sessionId" | "createdAt"> {
2014 return {
2115 eventId: EventId.makeUnsafe(eventId),
2216 provider: PROVIDER,
......@@ -31,29 +25,38 @@ export const codexTurnTextFixture = [
3125 ...baseEvent("evt-1", "2026-02-23T00:00:00.000Z"),
3226 threadId: THREAD_ID,
3327 turnId: TURN_ID,
28 payload: {},
3429 },
3530 {
36 type: "message.delta",
31 type: "content.delta",
3732 ...baseEvent("evt-2", "2026-02-23T00:00:00.100Z"),
3833 threadId: THREAD_ID,
3934 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 },
4139 },
4240 {
43 type: "message.delta",
41 type: "content.delta",
4442 ...baseEvent("evt-3", "2026-02-23T00:00:00.200Z"),
4543 threadId: THREAD_ID,
4644 turnId: TURN_ID,
47 delta: "Done.\n",
45 payload: {
46 streamKind: "assistant_text",
47 delta: "Done.\n",
48 },
4849 },
4950 {
5051 type: "turn.completed",
5152 ...baseEvent("evt-4", "2026-02-23T00:00:00.300Z"),
5253 threadId: THREAD_ID,
5354 turnId: TURN_ID,
54 status: "completed",
55 payload: {
56 state: "completed",
57 },
5558 },
56] satisfies ReadonlyArray<ProviderRuntimeEvent>;
59] satisfies ReadonlyArray<LegacyProviderRuntimeEvent>;
5760
5861export const codexTurnToolFixture = [
5962 {
......@@ -61,40 +64,51 @@ export const codexTurnToolFixture = [
6164 ...baseEvent("evt-11", "2026-02-23T00:01:00.000Z"),
6265 threadId: THREAD_ID,
6366 turnId: TURN_ID,
67 payload: {},
6468 },
6569 {
66 type: "tool.started",
70 type: "item.started",
6771 ...baseEvent("evt-12", "2026-02-23T00:01:00.100Z"),
6872 threadId: THREAD_ID,
6973 turnId: TURN_ID,
70 toolKind: "command",
71 title: "Command run",
72 detail: "echo integration",
74 payload: {
75 itemType: "command_execution",
76 title: "Command run",
77 detail: "echo integration",
78 },
7379 },
7480 {
75 type: "tool.completed",
81 type: "item.completed",
7682 ...baseEvent("evt-13", "2026-02-23T00:01:00.200Z"),
7783 threadId: THREAD_ID,
7884 turnId: TURN_ID,
79 toolKind: "command",
80 title: "Command run",
81 detail: "echo integration",
85 payload: {
86 itemType: "command_execution",
87 status: "completed",
88 title: "Command run",
89 detail: "echo integration",
90 },
8291 },
8392 {
84 type: "message.delta",
93 type: "content.delta",
8594 ...baseEvent("evt-14", "2026-02-23T00:01:00.300Z"),
8695 threadId: THREAD_ID,
8796 turnId: TURN_ID,
88 delta: "Applied the requested edit.\n",
97 payload: {
98 streamKind: "assistant_text",
99 delta: "Applied the requested edit.\n",
100 },
89101 },
90102 {
91103 type: "turn.completed",
92104 ...baseEvent("evt-15", "2026-02-23T00:01:00.400Z"),
93105 threadId: THREAD_ID,
94106 turnId: TURN_ID,
95 status: "completed",
107 payload: {
108 state: "completed",
109 },
96110 },
97] satisfies ReadonlyArray<ProviderRuntimeEvent>;
111] satisfies ReadonlyArray<LegacyProviderRuntimeEvent>;
98112
99113export const codexTurnApprovalFixture = [
100114 {
......@@ -102,37 +116,47 @@ export const codexTurnApprovalFixture = [
102116 ...baseEvent("evt-21", "2026-02-23T00:02:00.000Z"),
103117 threadId: THREAD_ID,
104118 turnId: TURN_ID,
119 payload: {},
105120 },
106121 {
107 type: "approval.requested",
122 type: "request.opened",
108123 ...baseEvent("evt-22", "2026-02-23T00:02:00.100Z"),
109124 threadId: THREAD_ID,
110125 turnId: TURN_ID,
111126 requestId: REQUEST_ID,
112 requestKind: "command",
113 detail: "Please approve command",
127 payload: {
128 requestType: "command_execution_approval",
129 detail: "Please approve command",
130 },
114131 },
115132 {
116 type: "approval.resolved",
133 type: "request.resolved",
117134 ...baseEvent("evt-23", "2026-02-23T00:02:00.200Z"),
118135 threadId: THREAD_ID,
119136 turnId: TURN_ID,
120137 requestId: REQUEST_ID,
121 requestKind: "command",
122 decision: "accept",
138 payload: {
139 requestType: "command_execution_approval",
140 decision: "accept",
141 },
123142 },
124143 {
125 type: "message.delta",
144 type: "content.delta",
126145 ...baseEvent("evt-24", "2026-02-23T00:02:00.300Z"),
127146 threadId: THREAD_ID,
128147 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 },
130152 },
131153 {
132154 type: "turn.completed",
133155 ...baseEvent("evt-25", "2026-02-23T00:02:00.400Z"),
134156 threadId: THREAD_ID,
135157 turnId: TURN_ID,
136 status: "completed",
158 payload: {
159 state: "completed",
160 },
137161 },
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
4545const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
4646const 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
4862function createProviderServiceHarness(
4963 cwd: string,
5064 hasSession = true,
......@@ -86,8 +100,8 @@ function createProviderServiceHarness(
86100 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
87101 };
88102
89 const emit = (event: ProviderRuntimeEvent): void => {
90 Effect.runSync(PubSub.publish(runtimeEventPubSub, event));
103 const emit = (event: LegacyProviderRuntimeEvent): void => {
104 Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent));
91105 };
92106
93107 return {
......@@ -363,7 +377,7 @@ describe("CheckpointReactor", () => {
363377 createdAt: new Date().toISOString(),
364378 threadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
365379 turnId: asProviderTurnId("turn-1"),
366 status: "completed",
380 payload: { state: "completed" },
367381 });
368382
369383 await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
......@@ -443,7 +457,7 @@ describe("CheckpointReactor", () => {
443457 createdAt: new Date().toISOString(),
444458 threadId: ProviderThreadId.makeUnsafe("provider-thread-aux"),
445459 turnId: asProviderTurnId("turn-aux"),
446 status: "completed",
460 payload: { state: "completed" },
447461 });
448462
449463 await Effect.runPromise(Effect.sleep("40 millis"));
......@@ -461,7 +475,7 @@ describe("CheckpointReactor", () => {
461475 createdAt: new Date().toISOString(),
462476 threadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
463477 turnId: asProviderTurnId("turn-main"),
464 status: "completed",
478 payload: { state: "completed" },
465479 });
466480
467481 const thread = await waitForThread(
......@@ -522,7 +536,7 @@ describe("CheckpointReactor", () => {
522536 createdAt: new Date().toISOString(),
523537 threadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
524538 turnId: asProviderTurnId("turn-claude-1"),
525 status: "completed",
539 payload: { state: "completed" },
526540 });
527541
528542 await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
......@@ -570,7 +584,7 @@ describe("CheckpointReactor", () => {
570584 createdAt: new Date().toISOString(),
571585 threadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
572586 turnId: asProviderTurnId("turn-missing-baseline"),
573 status: "completed",
587 payload: { state: "completed" },
574588 });
575589
576590 await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
......@@ -662,7 +676,7 @@ describe("CheckpointReactor", () => {
662676 createdAt: new Date().toISOString(),
663677 threadId: ProviderThreadId.makeUnsafe("provider-thread-missing"),
664678 turnId: asProviderTurnId("turn-missing-cwd"),
665 status: "completed",
679 payload: { state: "completed" },
666680 });
667681
668682 await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
......@@ -678,7 +692,7 @@ describe("CheckpointReactor", () => {
678692 ).toBe("v2\n");
679693 });
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 () => {
682696 const harness = await createHarness();
683697 const createdAt = new Date().toISOString();
684698
......@@ -715,11 +729,11 @@ describe("CheckpointReactor", () => {
715729 status: "completed",
716730 });
717731
718 const thread = await waitForThread(harness.engine, (entry) =>
719 entry.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3),
720 );
721 expect(thread.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3)).toBe(
722 true,
732 await Effect.runPromise(Effect.sleep("40 millis"));
733 const readModel = await Effect.runPromise(harness.engine.getReadModel());
734 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
735 expect(thread?.checkpoints.some((checkpoint) => checkpoint.checkpointTurnCount === 3)).toBe(
736 false,
723737 );
724738 });
725739
......@@ -757,15 +771,14 @@ describe("CheckpointReactor", () => {
757771 );
758772
759773 harness.provider.emit({
760 type: "checkpoint.captured",
774 type: "turn.completed",
761775 eventId: EventId.makeUnsafe("evt-runtime-capture-failure"),
762776 provider: "codex",
763777 sessionId: asSessionId("sess-1"),
764778 createdAt: new Date().toISOString(),
765779 threadId: ProviderThreadId.makeUnsafe("provider-thread-1"),
766780 turnId: asProviderTurnId("turn-runtime-failure"),
767 turnCount: 1,
768 status: "completed",
781 payload: { state: "completed" },
769782 });
770783
771784 harness.provider.emit({
apps/server/src/orchestration/Layers/CheckpointReactor.ts+16-96
......@@ -8,6 +8,7 @@ import {
88 TurnId,
99 type OrchestrationEvent,
1010 type ProviderRuntimeEvent,
11 RuntimeSessionId,
1112} from "@t3tools/contracts";
1213import { Cause, Effect, Layer, Option, Queue, Stream } from "effect";
1314
......@@ -34,11 +35,15 @@ type ReactorInput =
3435 };
3536
3637function toTurnId(value: string | undefined): TurnId | null {
37 return value === undefined ? null : TurnId.makeUnsafe(value);
38 return value === undefined ? null : TurnId.makeUnsafe(String(value));
3839}
3940
4041function 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));
4247}
4348
4449function sameId(left: string | null | undefined, right: string | null | undefined): boolean {
......@@ -172,6 +177,7 @@ const make = Effect.gen(function* () {
172177 const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* (
173178 event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>,
174179 ) {
180 const providerSessionId = toProviderSessionId(event.sessionId);
175181 const turnId = toTurnId(event.turnId);
176182 if (!turnId) {
177183 return;
......@@ -179,7 +185,7 @@ const make = Effect.gen(function* () {
179185
180186 const readModel = yield* orchestrationEngine.getReadModel();
181187 const thread = readModel.threads.find(
182 (entry) => entry.session?.providerSessionId === event.sessionId,
188 (entry) => entry.session?.providerSessionId === providerSessionId,
183189 );
184190 if (!thread) {
185191 return;
......@@ -266,7 +272,7 @@ const make = Effect.gen(function* () {
266272 ),
267273 Effect.tapError((error) =>
268274 appendCaptureFailureActivity({
269 sessionId: event.sessionId,
275 sessionId: providerSessionId,
270276 turnId,
271277 detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`,
272278 createdAt: event.createdAt,
......@@ -296,7 +302,7 @@ const make = Effect.gen(function* () {
296302 turnId,
297303 completedAt: now,
298304 checkpointRef: targetCheckpointRef,
299 status: checkpointStatusFromRuntime(event.status),
305 status: checkpointStatusFromRuntime(event.payload.state),
300306 files,
301307 assistantMessageId,
302308 checkpointTurnCount: nextTurnCount,
......@@ -314,7 +320,7 @@ const make = Effect.gen(function* () {
314320 summary: "Checkpoint captured",
315321 payload: {
316322 turnCount: nextTurnCount,
317 ...(event.status !== undefined ? { status: event.status } : {}),
323 status: event.payload.state,
318324 },
319325 turnId,
320326 createdAt: now,
......@@ -326,6 +332,7 @@ const make = Effect.gen(function* () {
326332 const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* (
327333 event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>,
328334 ) {
335 const providerSessionId = toProviderSessionId(event.sessionId);
329336 const turnId = toTurnId(event.turnId);
330337 if (!turnId) {
331338 return;
......@@ -333,7 +340,7 @@ const make = Effect.gen(function* () {
333340
334341 const readModel = yield* orchestrationEngine.getReadModel();
335342 const thread = readModel.threads.find(
336 (entry) => entry.session?.providerSessionId === event.sessionId,
343 (entry) => entry.session?.providerSessionId === providerSessionId,
337344 );
338345 if (!thread) {
339346 return;
......@@ -581,7 +588,7 @@ const make = Effect.gen(function* () {
581588 yield* captureCheckpointFromTurnCompletion(event).pipe(
582589 Effect.catch((error) =>
583590 appendCaptureFailureActivity({
584 sessionId: event.sessionId,
591 sessionId: toProviderSessionId(event.sessionId),
585592 turnId,
586593 detail: error.message,
587594 createdAt: new Date().toISOString(),
......@@ -590,89 +597,6 @@ const make = Effect.gen(function* () {
590597 );
591598 return;
592599 }
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 }
676600 });
677601
678602 const processInput = (
......@@ -717,11 +641,7 @@ const make = Effect.gen(function* () {
717641
718642 yield* Effect.forkScoped(
719643 Stream.runForEach(providerService.streamEvents, (event) => {
720 if (
721 event.type !== "turn.started" &&
722 event.type !== "turn.completed" &&
723 event.type !== "checkpoint.captured"
724 ) {
644 if (event.type !== "turn.started" && event.type !== "turn.completed") {
725645 return Effect.void;
726646 }
727647 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", () => {
130130 }),
131131 );
132132 const interruptTurn = vi.fn((_: unknown) => Effect.void);
133 const respondToRequest = vi.fn((_: unknown) => Effect.void);
133 const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void);
134134 const stopSession = vi.fn((input: unknown) =>
135135 Effect.sync(() => {
136136 const sessionId =
......@@ -777,7 +777,7 @@ describe("ProviderCommandReactor", () => {
777777 });
778778 });
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 () => {
781781 const harness = await createHarness();
782782 const now = new Date().toISOString();
783783 harness.respondToRequest.mockImplementation(() =>
......@@ -869,7 +869,7 @@ describe("ProviderCommandReactor", () => {
869869 activity.payload !== null &&
870870 (activity.payload as Record<string, unknown>).requestId === "approval-request-1",
871871 );
872 expect(resolvedActivity).toBeDefined();
872 expect(resolvedActivity).toBeUndefined();
873873 });
874874
875875 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 {
1212 type ProviderSession,
1313 type TurnId,
1414} 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
1717import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
1818import { GitCore } from "../../git/Services/GitCore.ts";
......@@ -84,7 +84,7 @@ function toErrorMessage(error: unknown): string {
8484}
8585
8686function isUnknownPendingApprovalRequestError(error: unknown): boolean {
87 if (error instanceof ProviderAdapterRequestError) {
87 if (Schema.is(ProviderAdapterRequestError)(error)) {
8888 const detail = error.detail.toLowerCase();
8989 return (
9090 detail.includes("unknown pending approval request") ||
......@@ -175,33 +175,6 @@ const make = Effect.gen(function* () {
175175 createdAt: input.createdAt,
176176 });
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
205178 const setThreadSession = (input: {
206179 readonly threadId: ThreadId;
207180 readonly session: OrchestrationSession;
......@@ -569,17 +542,7 @@ const make = Effect.gen(function* () {
569542 requestId: event.payload.requestId,
570543 });
571544
572 if (!isUnknownPendingApprovalRequestError(error)) {
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 });
545 if (!isUnknownPendingApprovalRequestError(error)) return;
583546 }),
584547 ),
585548 );
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+16-2
......@@ -40,6 +40,20 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(va
4040const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
4141const 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
4357function createProviderServiceHarness() {
4458 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
4559
......@@ -57,8 +71,8 @@ function createProviderServiceHarness() {
5771 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
5872 };
5973
60 const emit = (event: ProviderRuntimeEvent): void => {
61 Effect.runSync(PubSub.publish(runtimeEventPubSub, event));
74 const emit = (event: LegacyProviderRuntimeEvent): void => {
75 Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent));
6276 };
6377
6478 return {
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+39-23
......@@ -1,4 +1,5 @@
11import {
2 ApprovalRequestId,
23 type AssistantDeliveryMode,
34 CommandId,
45 MessageId,
......@@ -10,7 +11,8 @@ import {
1011 TurnId,
1112 type OrchestrationThreadActivity,
1213 type ProviderRuntimeEvent,
13 type ProviderSessionId,
14 ProviderSessionId,
15 RuntimeSessionId,
1416} from "@t3tools/contracts";
1517import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Ref, Stream } from "effect";
1618
......@@ -50,12 +52,20 @@ type RuntimeIngestionInput =
5052 event: TurnStartRequestedDomainEvent;
5153 };
5254
53function toTurnId(value: string | undefined): TurnId | undefined {
54 return value === undefined ? undefined : TurnId.makeUnsafe(value);
55function toTurnId(value: TurnId | string | undefined): TurnId | undefined {
56 return value === undefined ? undefined : TurnId.makeUnsafe(String(value));
5557}
5658
5759function 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);
5969}
6070
6171function sameId(left: string | null | undefined, right: string | null | undefined): boolean {
......@@ -163,7 +173,12 @@ function isToolLifecycleItemType(itemType: string): boolean {
163173function runtimeEventToActivities(
164174 event: ProviderRuntimeEvent,
165175): 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 })();
167182 switch (event.type) {
168183 case "request.opened": {
169184 const requestKind = requestKindFromCanonicalRequestType(event.payload.requestType);
......@@ -180,7 +195,7 @@ function runtimeEventToActivities(
180195 ? "File-change approval requested"
181196 : "Approval requested",
182197 payload: {
183 requestId: event.requestId,
198 requestId: toApprovalRequestId(event.requestId),
184199 ...(requestKind ? { requestKind } : {}),
185200 requestType: event.payload.requestType,
186201 ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
......@@ -201,7 +216,7 @@ function runtimeEventToActivities(
201216 kind: "approval.resolved",
202217 summary: "Approval resolved",
203218 payload: {
204 requestId: event.requestId,
219 requestId: toApprovalRequestId(event.requestId),
205220 ...(requestKind ? { requestKind } : {}),
206221 requestType: event.payload.requestType,
207222 ...(event.payload.decision ? { decision: event.payload.decision } : {}),
......@@ -296,7 +311,7 @@ function runtimeEventToActivities(
296311 ? "Command approval requested"
297312 : "File-change approval requested",
298313 payload: {
299 requestId: event.requestId,
314 requestId: toApprovalRequestId(event.requestId),
300315 ...(requestKind ? { requestKind } : {}),
301316 ...(detail ? { detail: truncateDetail(detail) } : {}),
302317 },
......@@ -317,7 +332,7 @@ function runtimeEventToActivities(
317332 kind: "approval.resolved",
318333 summary: "Approval resolved",
319334 payload: {
320 requestId: event.requestId,
335 requestId: toApprovalRequestId(event.requestId),
321336 ...(requestKind ? { requestKind } : {}),
322337 ...(decision ? { decision } : {}),
323338 },
......@@ -528,9 +543,10 @@ const make = Effect.gen(function* () {
528543
529544 const processRuntimeEvent = (event: ProviderRuntimeEvent) =>
530545 Effect.gen(function* () {
546 const providerSessionId = toProviderSessionId(event.sessionId);
531547 const readModel = yield* orchestrationEngine.getReadModel();
532548 const thread = readModel.threads.find(
533 (entry) => entry.session?.providerSessionId === event.sessionId,
549 (entry) => entry.session?.providerSessionId === providerSessionId,
534550 );
535551 if (!thread) return;
536552
......@@ -614,10 +630,8 @@ const make = Effect.gen(function* () {
614630 : activeTurnId;
615631 const providerThreadIdFromEvent =
616632 event.type === "thread.started"
617 ? ProviderThreadId.makeUnsafe(event.threadId)
618 : event.threadId !== undefined
619 ? ProviderThreadId.makeUnsafe(event.threadId)
620 : null;
633 ? toProviderThreadId(event.threadId)
634 : toProviderThreadId(event.threadId);
621635 const providerThreadId =
622636 providerThreadIdFromEvent ?? scopedSessionProviderThreadId ?? null;
623637 const status = (() => {
......@@ -651,7 +665,7 @@ const make = Effect.gen(function* () {
651665 threadId: thread.id,
652666 status,
653667 providerName: event.provider,
654 providerSessionId: event.sessionId,
668 providerSessionId,
655669 providerThreadId,
656670 approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
657671 sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
......@@ -677,7 +691,7 @@ const make = Effect.gen(function* () {
677691 );
678692 const turnId = toTurnId(event.turnId);
679693 if (turnId) {
680 yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId);
694 yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId);
681695 }
682696
683697 const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef);
......@@ -724,7 +738,7 @@ const make = Effect.gen(function* () {
724738 const assistantMessageId = assistantCompletion.messageId;
725739 const turnId = toTurnId(event.turnId);
726740 if (turnId) {
727 yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId);
741 yield* rememberAssistantMessageId(providerSessionId, turnId, assistantMessageId);
728742 }
729743
730744 yield* finalizeAssistantMessage({
......@@ -735,18 +749,20 @@ const make = Effect.gen(function* () {
735749 createdAt: now,
736750 commandTag: "assistant-complete",
737751 finalDeltaCommandTag: "assistant-delta-finalize",
738 fallbackText: assistantCompletion.fallbackText,
752 ...(assistantCompletion.fallbackText !== undefined
753 ? { fallbackText: assistantCompletion.fallbackText }
754 : {}),
739755 });
740756
741757 if (turnId) {
742 yield* forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId);
758 yield* forgetAssistantMessageId(providerSessionId, turnId, assistantMessageId);
743759 }
744760 }
745761
746762 if (event.type === "turn.completed") {
747763 const turnId = toTurnId(event.turnId);
748764 if (turnId) {
749 const assistantMessageIds = yield* getAssistantMessageIdsForTurn(event.sessionId, turnId);
765 const assistantMessageIds = yield* getAssistantMessageIdsForTurn(providerSessionId, turnId);
750766 yield* Effect.forEach(
751767 assistantMessageIds,
752768 (assistantMessageId) =>
......@@ -761,12 +777,12 @@ const make = Effect.gen(function* () {
761777 }),
762778 { concurrency: 1 },
763779 ).pipe(Effect.asVoid);
764 yield* clearAssistantMessageIdsForTurn(event.sessionId, turnId);
780 yield* clearAssistantMessageIdsForTurn(providerSessionId, turnId);
765781 }
766782 }
767783
768784 if (event.type === "session.exited") {
769 yield* clearTurnStateForSession(event.sessionId);
785 yield* clearTurnStateForSession(providerSessionId);
770786 }
771787
772788 if (event.type === "runtime.error") {
......@@ -793,7 +809,7 @@ const make = Effect.gen(function* () {
793809 threadId: thread.id,
794810 status: "error",
795811 providerName: event.provider,
796 providerSessionId: event.sessionId,
812 providerSessionId,
797813 providerThreadId,
798814 approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY,
799815 sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE,
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+14-9
......@@ -331,14 +331,14 @@ describe("ClaudeCodeAdapterLive", () => {
331331 const turnStarted = runtimeEvents[3];
332332 assert.equal(turnStarted?.type, "turn.started");
333333 if (turnStarted?.type === "turn.started") {
334 assert.equal(turnStarted.turnId, turn.turnId);
334 assert.equal(String(turnStarted.turnId), String(turn.turnId));
335335 }
336336
337337 const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta");
338338 assert.equal(deltaEvent?.type, "content.delta");
339339 if (deltaEvent?.type === "content.delta") {
340340 assert.equal(deltaEvent.payload.delta, "Hi");
341 assert.equal(deltaEvent.turnId, turn.turnId);
341 assert.equal(String(deltaEvent.turnId), String(turn.turnId));
342342 }
343343
344344 const toolStarted = runtimeEvents.find((event) => event.type === "item.started");
......@@ -350,7 +350,7 @@ describe("ClaudeCodeAdapterLive", () => {
350350 const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
351351 assert.equal(turnCompleted?.type, "turn.completed");
352352 if (turnCompleted?.type === "turn.completed") {
353 assert.equal(turnCompleted.turnId, turn.turnId);
353 assert.equal(String(turnCompleted.turnId), String(turn.turnId));
354354 assert.equal(turnCompleted.payload.state, "completed");
355355 }
356356 }).pipe(
......@@ -438,7 +438,7 @@ describe("ClaudeCodeAdapterLive", () => {
438438 assert.equal(deltaEvent?.type, "content.delta");
439439 if (deltaEvent?.type === "content.delta") {
440440 assert.equal(deltaEvent.payload.delta, "Late text");
441 assert.equal(deltaEvent.turnId, turn.turnId);
441 assert.equal(String(deltaEvent.turnId), String(turn.turnId));
442442 }
443443 }).pipe(
444444 Effect.provideService(Random.Random, makeDeterministicRandomService()),
......@@ -506,7 +506,7 @@ describe("ClaudeCodeAdapterLive", () => {
506506 assert.equal(deltaEvent?.type, "content.delta");
507507 if (deltaEvent?.type === "content.delta") {
508508 assert.equal(deltaEvent.payload.delta, "Fallback hello");
509 assert.equal(deltaEvent.turnId, turn.turnId);
509 assert.equal(String(deltaEvent.turnId), String(turn.turnId));
510510 }
511511 }).pipe(
512512 Effect.provideService(Random.Random, makeDeterministicRandomService()),
......@@ -631,10 +631,15 @@ describe("ClaudeCodeAdapterLive", () => {
631631 if (requested.value.type !== "request.opened") {
632632 return;
633633 }
634 const runtimeRequestId = requested.value.requestId;
635 assert.equal(typeof runtimeRequestId, "string");
636 if (runtimeRequestId === undefined) {
637 return;
638 }
634639
635640 yield* adapter.respondToRequest(
636641 session.sessionId,
637 ApprovalRequestId.makeUnsafe(requested.value.requestId),
642 ApprovalRequestId.makeUnsafe(runtimeRequestId),
638643 "accept",
639644 );
640645
......@@ -744,7 +749,7 @@ describe("ClaudeCodeAdapterLive", () => {
744749 const firstCompleted = yield* Fiber.join(firstCompletedFiber);
745750 assert.equal(firstCompleted._tag, "Some");
746751 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));
748753 }
749754
750755 const secondTurn = yield* adapter.sendTurn({
......@@ -770,7 +775,7 @@ describe("ClaudeCodeAdapterLive", () => {
770775 const secondCompleted = yield* Fiber.join(secondCompletedFiber);
771776 assert.equal(secondCompleted._tag, "Some");
772777 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));
774779 }
775780
776781 const threadBeforeRollback = yield* adapter.readThread(session.sessionId);
......@@ -875,7 +880,7 @@ describe("ClaudeCodeAdapterLive", () => {
875880
876881 assert.equal(records.some((record) => record.event?.provider === "claudeCode"), true);
877882 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);
879884 assert.equal(
880885 records.some(
881886 (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 {
3131 type ProviderSession,
3232 ProviderThreadId,
3333 ProviderTurnId,
34 RuntimeItemId,
35 RuntimeRequestId,
36 RuntimeSessionId,
37 ThreadId,
38 TurnId,
3439} from "@t3tools/contracts";
3540import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";
3641
......@@ -123,9 +128,7 @@ export interface ClaudeCodeAdapterLiveOptions {
123128}
124129
125130function 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(
127 value,
128 );
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);
129132}
130133
131134function isSyntheticClaudeThreadId(value: string): boolean {
......@@ -139,6 +142,26 @@ function toMessage(cause: unknown, fallback: string): string {
139142 return fallback;
140143}
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
142165function toPermissionMode(value: unknown): PermissionMode | undefined {
143166 switch (value) {
144167 case "default":
......@@ -315,7 +338,11 @@ function extractAssistantText(message: SDKMessage): string {
315338 continue;
316339 }
317340 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 ) {
319346 fragments.push(candidate.text);
320347 }
321348 }
......@@ -388,7 +415,9 @@ function sdkNativeMethod(message: SDKMessage): string {
388415 const streamType = sdkMessageType(message.event);
389416 if (streamType) {
390417 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;
392421 if (deltaType) {
393422 return `claude/${message.type}/${streamType}/${deltaType}`;
394423 }
......@@ -466,11 +495,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
466495 : crypto.randomUUID(),
467496 kind: "notification",
468497 provider: PROVIDER,
469 sessionId: context.session.sessionId,
498 sessionId: asRuntimeSessionId(context.session.sessionId),
470499 createdAt: observedAt,
471500 method: sdkNativeMethod(message),
472501 ...(typeof message.session_id === "string" ? { threadId: message.session_id } : {}),
473 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
502 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
474503 ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}),
475504 payload: message,
476505 },
......@@ -485,7 +514,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
485514 id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
486515 items: ReadonlyArray<unknown>;
487516 }>;
488 }> =>
517 }, ProviderAdapterValidationError> =>
489518 Effect.gen(function* () {
490519 const threadId = context.session.threadId;
491520 if (!threadId) {
......@@ -552,9 +581,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
552581 type: "thread.started",
553582 eventId: stamp.eventId,
554583 provider: PROVIDER,
555 sessionId: context.session.sessionId,
584 sessionId: asRuntimeSessionId(context.session.sessionId),
556585 createdAt: stamp.createdAt,
557 threadId: nextThreadId,
586 threadId: asRuntimeThreadId(nextThreadId),
558587 payload: {
559588 providerThreadId: nextThreadId,
560589 },
......@@ -588,10 +617,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
588617 type: "runtime.error",
589618 eventId: stamp.eventId,
590619 provider: PROVIDER,
591 sessionId: context.session.sessionId,
620 sessionId: asRuntimeSessionId(context.session.sessionId),
592621 createdAt: stamp.createdAt,
593 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
594 ...(turnState ? { turnId: turnState.turnId } : {}),
622 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
623 ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}),
595624 payload: {
596625 message,
597626 class: "provider_error",
......@@ -617,10 +646,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
617646 type: "runtime.warning",
618647 eventId: stamp.eventId,
619648 provider: PROVIDER,
620 sessionId: context.session.sessionId,
649 sessionId: asRuntimeSessionId(context.session.sessionId),
621650 createdAt: stamp.createdAt,
622 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
623 ...(turnState ? { turnId: turnState.turnId } : {}),
651 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
652 ...(turnState ? { turnId: asRuntimeTurnId(turnState.turnId) } : {}),
624653 payload: {
625654 message,
626655 ...(detail !== undefined ? { detail } : {}),
......@@ -647,9 +676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
647676 type: "turn.completed",
648677 eventId: stamp.eventId,
649678 provider: PROVIDER,
650 sessionId: context.session.sessionId,
679 sessionId: asRuntimeSessionId(context.session.sessionId),
651680 createdAt: stamp.createdAt,
652 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
681 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
653682 payload: {
654683 state: status,
655684 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
......@@ -675,11 +704,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
675704 type: "content.delta",
676705 eventId: deltaStamp.eventId,
677706 provider: PROVIDER,
678 sessionId: context.session.sessionId,
707 sessionId: asRuntimeSessionId(context.session.sessionId),
679708 createdAt: deltaStamp.createdAt,
680 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
681 turnId: turnState.turnId,
682 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
709 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
710 turnId: asRuntimeTurnId(turnState.turnId),
711 itemId: asRuntimeItemId(turnState.assistantItemId),
683712 payload: {
684713 streamKind: "assistant_text",
685714 delta: turnState.fallbackAssistantText,
......@@ -698,11 +727,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
698727 type: "item.completed",
699728 eventId: stamp.eventId,
700729 provider: PROVIDER,
701 sessionId: context.session.sessionId,
730 sessionId: asRuntimeSessionId(context.session.sessionId),
702731 createdAt: stamp.createdAt,
703 itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
704 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
705 turnId: turnState.turnId,
732 itemId: asRuntimeItemId(turnState.assistantItemId),
733 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
734 turnId: asRuntimeTurnId(turnState.turnId),
706735 payload: {
707736 itemType: "assistant_message",
708737 status: "completed",
......@@ -727,10 +756,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
727756 type: "turn.completed",
728757 eventId: stamp.eventId,
729758 provider: PROVIDER,
730 sessionId: context.session.sessionId,
759 sessionId: asRuntimeSessionId(context.session.sessionId),
731760 createdAt: stamp.createdAt,
732 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
733 turnId: turnState.turnId,
761 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
762 turnId: asRuntimeTurnId(turnState.turnId),
734763 payload: {
735764 state: status,
736765 ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}),
......@@ -788,11 +817,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
788817 type: "content.delta",
789818 eventId: stamp.eventId,
790819 provider: PROVIDER,
791 sessionId: context.session.sessionId,
820 sessionId: asRuntimeSessionId(context.session.sessionId),
792821 createdAt: stamp.createdAt,
793 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
794 turnId: context.turnState.turnId,
795 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
822 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
823 turnId: asRuntimeTurnId(context.turnState.turnId),
824 itemId: asRuntimeItemId(context.turnState.assistantItemId),
796825 payload: {
797826 streamKind: streamKindFromDeltaType(event.delta.type),
798827 delta: event.delta.text,
......@@ -846,11 +875,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
846875 type: "item.started",
847876 eventId: stamp.eventId,
848877 provider: PROVIDER,
849 sessionId: context.session.sessionId,
878 sessionId: asRuntimeSessionId(context.session.sessionId),
850879 createdAt: stamp.createdAt,
851 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
852 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
853 itemId: ProviderItemId.makeUnsafe(tool.itemId),
880 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
881 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
882 itemId: asRuntimeItemId(tool.itemId),
854883 payload: {
855884 itemType: tool.itemType,
856885 status: "inProgress",
......@@ -889,11 +918,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
889918 type: "item.completed",
890919 eventId: stamp.eventId,
891920 provider: PROVIDER,
892 sessionId: context.session.sessionId,
921 sessionId: asRuntimeSessionId(context.session.sessionId),
893922 createdAt: stamp.createdAt,
894 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
895 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
896 itemId: ProviderItemId.makeUnsafe(tool.itemId),
923 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
924 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
925 itemId: asRuntimeItemId(tool.itemId),
897926 payload: {
898927 itemType: tool.itemType,
899928 status: "completed",
......@@ -942,11 +971,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
942971 type: "item.updated",
943972 eventId: stamp.eventId,
944973 provider: PROVIDER,
945 sessionId: context.session.sessionId,
974 sessionId: asRuntimeSessionId(context.session.sessionId),
946975 createdAt: stamp.createdAt,
947 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
948 turnId: context.turnState.turnId,
949 itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
976 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
977 turnId: asRuntimeTurnId(context.turnState.turnId),
978 itemId: asRuntimeItemId(context.turnState.assistantItemId),
950979 payload: {
951980 itemType: "assistant_message",
952981 status: "inProgress",
......@@ -1003,10 +1032,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
10031032 const base = {
10041033 eventId: stamp.eventId,
10051034 provider: PROVIDER,
1006 sessionId: context.session.sessionId,
1035 sessionId: asRuntimeSessionId(context.session.sessionId),
10071036 createdAt: stamp.createdAt,
1008 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1009 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1037 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1038 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
10101039 providerRefs: {
10111040 providerSessionId: context.session.sessionId,
10121041 ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
......@@ -1164,10 +1193,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
11641193 const base = {
11651194 eventId: stamp.eventId,
11661195 provider: PROVIDER,
1167 sessionId: context.session.sessionId,
1196 sessionId: asRuntimeSessionId(context.session.sessionId),
11681197 createdAt: stamp.createdAt,
1169 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1170 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1198 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1199 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
11711200 providerRefs: {
11721201 providerSessionId: context.session.sessionId,
11731202 ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
......@@ -1303,11 +1332,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
13031332 type: "request.resolved",
13041333 eventId: stamp.eventId,
13051334 provider: PROVIDER,
1306 sessionId: context.session.sessionId,
1335 sessionId: asRuntimeSessionId(context.session.sessionId),
13071336 createdAt: stamp.createdAt,
1308 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1309 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1310 requestId,
1337 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1338 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
1339 requestId: asRuntimeRequestId(requestId),
13111340 payload: {
13121341 requestType: pending.requestType,
13131342 decision: "cancel",
......@@ -1344,9 +1373,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
13441373 type: "session.exited",
13451374 eventId: stamp.eventId,
13461375 provider: PROVIDER,
1347 sessionId: context.session.sessionId,
1376 sessionId: asRuntimeSessionId(context.session.sessionId),
13481377 createdAt: stamp.createdAt,
1349 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1378 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
13501379 payload: {
13511380 reason: "Session stopped",
13521381 exitKind: "graceful",
......@@ -1450,11 +1479,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
14501479 type: "request.opened",
14511480 eventId: requestedStamp.eventId,
14521481 provider: PROVIDER,
1453 sessionId: context.session.sessionId,
1482 sessionId: asRuntimeSessionId(context.session.sessionId),
14541483 createdAt: requestedStamp.createdAt,
1455 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1456 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1457 requestId,
1484 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1485 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
1486 requestId: asRuntimeRequestId(requestId),
14581487 payload: {
14591488 requestType,
14601489 detail,
......@@ -1466,7 +1495,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
14661495 },
14671496 providerRefs: {
14681497 providerSessionId: context.session.sessionId,
1469 ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1498 ...(context.session.threadId
1499 ? { providerThreadId: context.session.threadId }
1500 : {}),
14701501 ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
14711502 providerRequestId: requestId,
14721503 },
......@@ -1502,18 +1533,20 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
15021533 type: "request.resolved",
15031534 eventId: resolvedStamp.eventId,
15041535 provider: PROVIDER,
1505 sessionId: context.session.sessionId,
1536 sessionId: asRuntimeSessionId(context.session.sessionId),
15061537 createdAt: resolvedStamp.createdAt,
1507 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1508 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1509 requestId,
1538 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1539 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
1540 requestId: asRuntimeRequestId(requestId),
15101541 payload: {
15111542 requestType,
15121543 decision,
15131544 },
15141545 providerRefs: {
15151546 providerSessionId: context.session.sessionId,
1516 ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
1547 ...(context.session.threadId
1548 ? { providerThreadId: context.session.threadId }
1549 : {}),
15171550 ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
15181551 providerRequestId: requestId,
15191552 },
......@@ -1628,9 +1661,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
16281661 type: "session.started",
16291662 eventId: sessionStartedStamp.eventId,
16301663 provider: PROVIDER,
1631 sessionId,
1664 sessionId: asRuntimeSessionId(sessionId),
16321665 createdAt: sessionStartedStamp.createdAt,
1633 ...(threadId ? { threadId } : {}),
1666 ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}),
16341667 payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {},
16351668 providerRefs: {
16361669 providerSessionId: sessionId,
......@@ -1643,9 +1676,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
16431676 type: "session.configured",
16441677 eventId: configuredStamp.eventId,
16451678 provider: PROVIDER,
1646 sessionId,
1679 sessionId: asRuntimeSessionId(sessionId),
16471680 createdAt: configuredStamp.createdAt,
1648 ...(threadId ? { threadId } : {}),
1681 ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}),
16491682 payload: {
16501683 config: {
16511684 ...(input.model ? { model: input.model } : {}),
......@@ -1667,9 +1700,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
16671700 type: "session.state.changed",
16681701 eventId: readyStamp.eventId,
16691702 provider: PROVIDER,
1670 sessionId,
1703 sessionId: asRuntimeSessionId(sessionId),
16711704 createdAt: readyStamp.createdAt,
1672 ...(threadId ? { threadId } : {}),
1705 ...(threadId ? { threadId: asRuntimeThreadId(threadId) } : {}),
16731706 payload: {
16741707 state: "ready",
16751708 },
......@@ -1705,10 +1738,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
17051738 });
17061739 }
17071740
1708 const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* Random.nextUUIDv4}`);
1741 const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4);
17091742 const turnState: ClaudeTurnState = {
17101743 turnId,
1711 assistantItemId: `claude-message-${yield* Random.nextUUIDv4}`,
1744 assistantItemId: yield* Random.nextUUIDv4,
17121745 startedAt: yield* nowIso,
17131746 items: [],
17141747 messageCompleted: false,
......@@ -1730,10 +1763,10 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
17301763 type: "turn.started",
17311764 eventId: turnStartedStamp.eventId,
17321765 provider: PROVIDER,
1733 sessionId: context.session.sessionId,
1766 sessionId: asRuntimeSessionId(context.session.sessionId),
17341767 createdAt: turnStartedStamp.createdAt,
1735 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1736 turnId,
1768 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1769 turnId: asRuntimeTurnId(turnId),
17371770 payload: {
17381771 ...(input.model ? { model: input.model } : {}),
17391772 ...(input.effort ? { effort: input.effort } : {}),
apps/server/src/provider/Layers/CodexAdapter.ts+33-7
......@@ -11,10 +11,16 @@ import {
1111 type CanonicalRequestType,
1212 type ProviderEvent,
1313 type ProviderRuntimeEvent,
14 RuntimeItemId,
15 RuntimeRequestId,
16 RuntimeSessionId,
1417 ProviderApprovalDecision,
18 ProviderItemId,
1519 ProviderSessionId,
1620 ProviderThreadId,
1721 ProviderTurnId,
22 ThreadId,
23 TurnId,
1824} from "@t3tools/contracts";
1925import { Effect, FileSystem, Layer, Option, Queue, Schema, Stream } from "effect";
2026
......@@ -278,7 +284,27 @@ function contentStreamKindFromMethod(
278284 }
279285}
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"] {
282308 return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification";
283309}
284310
......@@ -301,12 +327,12 @@ function runtimeEventBase(event: ProviderEvent): Omit<ProviderRuntimeEvent, "typ
301327 return {
302328 eventId: event.id,
303329 provider: event.provider,
304 sessionId: event.sessionId,
330 sessionId: asRuntimeSessionId(event.sessionId),
305331 createdAt: event.createdAt,
306 ...(event.threadId ? { threadId: event.threadId } : {}),
307 ...(event.turnId ? { turnId: event.turnId } : {}),
308 ...(event.itemId ? { itemId: event.itemId } : {}),
309 ...(event.requestId ? { requestId: event.requestId } : {}),
332 ...(event.threadId ? { threadId: asRuntimeThreadId(event.threadId) } : {}),
333 ...(event.turnId ? { turnId: asRuntimeTurnId(event.turnId) } : {}),
334 ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}),
335 ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}),
310336 ...(refs ? { providerRefs: refs } : {}),
311337 raw: {
312338 source: eventRawSource(event),
......@@ -541,7 +567,7 @@ function mapToRuntimeEvents(event: ProviderEvent): ReadonlyArray<ProviderRuntime
541567 return [
542568 {
543569 ...runtimeEventBase(event),
544 turnId,
570 turnId: asRuntimeTurnId(turnId),
545571 type: "turn.started",
546572 payload: {
547573 ...(asString(turn?.model) ? { model: asString(turn?.model) } : {}),
apps/server/src/provider/Layers/CursorAdapter.test.ts+7-2
......@@ -320,7 +320,7 @@ describe("CursorAdapterLive", () => {
320320 const turnStarted = events[6];
321321 assert.equal(turnStarted?.type, "turn.started");
322322 if (turnStarted?.type === "turn.started") {
323 assert.equal(turnStarted.turnId, turn.turnId);
323 assert.equal(String(turnStarted.turnId), String(turn.turnId));
324324 }
325325
326326 const completion = events[12];
......@@ -448,10 +448,15 @@ describe("CursorAdapterLive", () => {
448448 if (opened.value.type !== "request.opened") {
449449 return;
450450 }
451 const runtimeRequestId = opened.value.requestId;
452 assert.equal(typeof runtimeRequestId, "string");
453 if (runtimeRequestId === undefined) {
454 return;
455 }
451456
452457 yield* adapter.respondToRequest(
453458 session.sessionId,
454 ApprovalRequestId.makeUnsafe(opened.value.requestId),
459 ApprovalRequestId.makeUnsafe(runtimeRequestId),
455460 "acceptForSession",
456461 );
457462
apps/server/src/provider/Layers/CursorAdapter.ts+146-86
......@@ -16,12 +16,18 @@ import {
1616 type CanonicalItemType,
1717 type CanonicalRequestType,
1818 type ProviderApprovalPolicy,
19 ProviderItemId,
1920 ProviderSessionId,
2021 type ProviderRuntimeEvent,
2122 type ProviderSession,
2223 ProviderThreadId,
2324 ProviderTurnId,
2425 type ProviderTurnStartResult,
26 RuntimeItemId,
27 RuntimeRequestId,
28 RuntimeSessionId,
29 ThreadId,
30 TurnId,
2531} from "@t3tools/contracts";
2632import { DateTime, Effect, Layer, Queue, Random, Schema, Stream } from "effect";
2733
......@@ -67,7 +73,7 @@ interface PendingPermission {
6773
6874interface CursorTurnState {
6975 readonly turnId: ReturnType<typeof ProviderTurnId.makeUnsafe>;
70 readonly assistantItemId: string;
76 readonly assistantItemId: ReturnType<typeof ProviderItemId.makeUnsafe>;
7177 readonly startedToolCalls: Set<string>;
7278 readonly items: Array<unknown>;
7379}
......@@ -106,6 +112,30 @@ function toMessage(cause: unknown, fallback: string): string {
106112 return fallback;
107113}
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
109139function toSessionError(
110140 sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
111141 cause: unknown,
......@@ -252,11 +282,9 @@ function summarizeToolOutput(rawOutput: unknown): string | undefined {
252282 return summary.length > 400 ? `${summary.slice(0, 397)}...` : summary;
253283}
254284
255function mapStopReasonToTurnState(stopReason: string | undefined):
256 | "completed"
257 | "failed"
258 | "interrupted"
259 | "cancelled" {
285function mapStopReasonToTurnState(
286 stopReason: string | undefined,
287): "completed" | "failed" | "interrupted" | "cancelled" {
260288 if (stopReason === "cancelled") return "cancelled";
261289 if (stopReason === "interrupted") return "interrupted";
262290 return "completed";
......@@ -357,7 +385,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
357385 });
358386 };
359387
360 const resolvePendingRequest = (context: CursorSessionContext, message: Record<string, unknown>) => {
388 const resolvePendingRequest = (
389 context: CursorSessionContext,
390 message: Record<string, unknown>,
391 ) => {
361392 const id = message.id;
362393 if (typeof id !== "string" && typeof id !== "number") {
363394 return;
......@@ -380,7 +411,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
380411 };
381412
382413 const decodePermissionRequest = Schema.decodeUnknownSync(CursorAcpPermissionRequest);
383 const decodeSessionUpdateNotification = Schema.decodeUnknownSync(CursorAcpSessionUpdateNotification);
414 const decodeSessionUpdateNotification = Schema.decodeUnknownSync(
415 CursorAcpSessionUpdateNotification,
416 );
384417
385418 const emitRuntimeWarning = (
386419 context: CursorSessionContext,
......@@ -393,10 +426,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
393426 type: "runtime.warning",
394427 eventId: stamp.eventId,
395428 provider: PROVIDER,
396 sessionId: context.session.sessionId,
429 sessionId: asRuntimeSessionId(context.session.sessionId),
397430 createdAt: stamp.createdAt,
398 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
399 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
431 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
432 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
400433 payload: {
401434 message,
402435 ...(detail !== undefined ? { detail } : {}),
......@@ -426,11 +459,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
426459 type: "item.completed",
427460 eventId: itemStamp.eventId,
428461 provider: PROVIDER,
429 sessionId: context.session.sessionId,
462 sessionId: asRuntimeSessionId(context.session.sessionId),
430463 createdAt: itemStamp.createdAt,
431 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
432 turnId: turnState.turnId,
433 itemId: turnState.assistantItemId,
464 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
465 turnId: asRuntimeTurnId(turnState.turnId),
466 itemId: asRuntimeItemId(turnState.assistantItemId),
434467 payload: {
435468 itemType: "assistant_message",
436469 status: "completed",
......@@ -454,10 +487,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
454487 type: "turn.completed",
455488 eventId: stamp.eventId,
456489 provider: PROVIDER,
457 sessionId: context.session.sessionId,
490 sessionId: asRuntimeSessionId(context.session.sessionId),
458491 createdAt: stamp.createdAt,
459 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
460 turnId: turnState.turnId,
492 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
493 turnId: asRuntimeTurnId(turnState.turnId),
461494 payload: {
462495 state,
463496 ...(stopReason ? { stopReason } : {}),
......@@ -534,11 +567,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
534567 type: "request.resolved",
535568 eventId: stamp.eventId,
536569 provider: PROVIDER,
537 sessionId: context.session.sessionId,
570 sessionId: asRuntimeSessionId(context.session.sessionId),
538571 createdAt: stamp.createdAt,
539 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
540 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
541 requestId,
572 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
573 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
574 requestId: asRuntimeRequestId(requestId),
542575 payload: {
543576 requestType,
544577 decision: selection.decision,
......@@ -576,11 +609,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
576609 type: "request.opened",
577610 eventId: stamp.eventId,
578611 provider: PROVIDER,
579 sessionId: context.session.sessionId,
612 sessionId: asRuntimeSessionId(context.session.sessionId),
580613 createdAt: stamp.createdAt,
581 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
582 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
583 requestId,
614 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
615 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
616 requestId: asRuntimeRequestId(requestId),
584617 payload: {
585618 requestType,
586619 ...(detail ? { detail } : {}),
......@@ -621,9 +654,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
621654
622655 const base = {
623656 provider: PROVIDER,
624 sessionId: context.session.sessionId,
625 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
626 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
657 sessionId: asRuntimeSessionId(context.session.sessionId),
658 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
659 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
627660 providerRefs: {
628661 providerSessionId: context.session.sessionId,
629662 ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
......@@ -663,8 +696,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
663696 type: "content.delta",
664697 eventId: stamp.eventId,
665698 createdAt: stamp.createdAt,
666 turnId: context.turnState.turnId,
667 itemId: context.turnState.assistantItemId,
699 turnId: asRuntimeTurnId(context.turnState.turnId),
700 itemId: asRuntimeItemId(context.turnState.assistantItemId),
668701 payload: {
669702 streamKind: "reasoning_text",
670703 delta: update.content.text,
......@@ -682,8 +715,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
682715 type: "content.delta",
683716 eventId: stamp.eventId,
684717 createdAt: stamp.createdAt,
685 turnId: context.turnState.turnId,
686 itemId: context.turnState.assistantItemId,
718 turnId: asRuntimeTurnId(context.turnState.turnId),
719 itemId: asRuntimeItemId(context.turnState.assistantItemId),
687720 payload: {
688721 streamKind: "assistant_text",
689722 delta: update.content.text,
......@@ -707,8 +740,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
707740 type: "item.started",
708741 eventId: stamp.eventId,
709742 createdAt: stamp.createdAt,
710 turnId: context.turnState.turnId,
711 itemId: update.toolCallId,
743 turnId: asRuntimeTurnId(context.turnState.turnId),
744 itemId: asRuntimeItemId(update.toolCallId),
712745 payload: {
713746 itemType,
714747 status: "inProgress",
......@@ -726,8 +759,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
726759 type: "item.updated",
727760 eventId: stamp.eventId,
728761 createdAt: stamp.createdAt,
729 turnId: context.turnState.turnId,
730 itemId: update.toolCallId,
762 turnId: asRuntimeTurnId(context.turnState.turnId),
763 itemId: asRuntimeItemId(update.toolCallId),
731764 payload: {
732765 itemType,
733766 status: "inProgress",
......@@ -749,8 +782,8 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
749782 type: eventType,
750783 eventId: stamp.eventId,
751784 createdAt: stamp.createdAt,
752 turnId: context.turnState.turnId,
753 itemId: update.toolCallId,
785 turnId: asRuntimeTurnId(context.turnState.turnId),
786 itemId: asRuntimeItemId(update.toolCallId),
754787 payload: {
755788 itemType: "command_execution",
756789 status,
......@@ -792,7 +825,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
792825 const nativeMethod =
793826 typeof message.method === "string"
794827 ? message.method
795 : (typeof message.id === "string" || typeof message.id === "number")
828 : typeof message.id === "string" || typeof message.id === "number"
796829 ? "cursor/acp/response"
797830 : "cursor/acp/message";
798831 const nativeKind =
......@@ -808,11 +841,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
808841 id: EventId.makeUnsafe(randomUUID()),
809842 kind: nativeKind,
810843 provider: PROVIDER,
811 sessionId: context.session.sessionId,
844 sessionId: asRuntimeSessionId(context.session.sessionId),
812845 createdAt: new Date().toISOString(),
813846 method: nativeMethod,
814 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
815 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
847 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
848 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
816849 payload: message,
817850 },
818851 });
......@@ -905,9 +938,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
905938 type: "session.exited",
906939 eventId: stamp.eventId,
907940 provider: PROVIDER,
908 sessionId: context.session.sessionId,
941 sessionId: asRuntimeSessionId(context.session.sessionId),
909942 createdAt: stamp.createdAt,
910 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
943 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
911944 payload: {
912945 reason: "Session stopped",
913946 exitKind: "graceful",
......@@ -956,11 +989,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
956989 }
957990
958991 const startedAt = yield* nowIso;
959 const sessionId = ProviderSessionId.makeUnsafe(`cursor-session-${yield* Random.nextUUIDv4}`);
992 const sessionId = ProviderSessionId.makeUnsafe(yield* Random.nextUUIDv4);
960993 const cwd = input.cwd ?? process.cwd();
961 const cursorOptions = input.providerOptions?.cursor as
962 | { binaryPath?: string }
963 | undefined;
994 const cursorOptions = input.providerOptions?.cursor as { binaryPath?: string } | undefined;
964995 const binaryPath = cursorOptions?.binaryPath ?? "agent";
965996 const resumeState = readCursorResumeState(input.resumeCursor);
966997
......@@ -1031,10 +1062,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
10311062 type: "runtime.error",
10321063 eventId: stamp.eventId,
10331064 provider: PROVIDER,
1034 sessionId: context.session.sessionId,
1065 sessionId: asRuntimeSessionId(context.session.sessionId),
10351066 createdAt: stamp.createdAt,
1036 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1037 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1067 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1068 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
10381069 payload: {
10391070 message: error.message || "Cursor ACP process error.",
10401071 class: "transport_error",
......@@ -1064,9 +1095,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
10641095 type: "session.exited",
10651096 eventId: stamp.eventId,
10661097 provider: PROVIDER,
1067 sessionId: context.session.sessionId,
1098 sessionId: asRuntimeSessionId(context.session.sessionId),
10681099 createdAt: stamp.createdAt,
1069 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1100 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
10701101 payload: {
10711102 reason: `Cursor ACP exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`,
10721103 exitKind: code === 0 ? "graceful" : "error",
......@@ -1088,14 +1119,23 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
10881119 }),
10891120 catch: (cause) => toRequestError(sessionId, "initialize", cause),
10901121 });
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
10931133 const initStamp = yield* makeEventStamp();
10941134 yield* offerRuntimeEvent({
10951135 type: "session.configured",
10961136 eventId: initStamp.eventId,
10971137 provider: PROVIDER,
1098 sessionId,
1138 sessionId: asRuntimeSessionId(sessionId),
10991139 createdAt: initStamp.createdAt,
11001140 payload: {
11011141 config: decodedInitialize,
......@@ -1116,7 +1156,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
11161156 type: "auth.status",
11171157 eventId: authStartStamp.eventId,
11181158 provider: PROVIDER,
1119 sessionId,
1159 sessionId: asRuntimeSessionId(sessionId),
11201160 createdAt: authStartStamp.createdAt,
11211161 payload: {
11221162 isAuthenticating: true,
......@@ -1133,19 +1173,24 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
11331173
11341174 const authenticateResult = yield* Effect.tryPromise({
11351175 try: async () => sendRequest(context, "authenticate", authenticateRequest),
1136 catch: (cause) => cause,
1137 });
1176 catch: (cause) => toRequestError(sessionId, "authenticate", cause),
1177 }).pipe(
1178 Effect.match({
1179 onFailure: (error) => ({ ok: false as const, error }),
1180 onSuccess: (value) => ({ ok: true as const, value }),
1181 }),
1182 );
11381183 const authEndStamp = yield* makeEventStamp();
1139 if (authenticateResult instanceof Error) {
1184 if (!authenticateResult.ok) {
11401185 yield* offerRuntimeEvent({
11411186 type: "auth.status",
11421187 eventId: authEndStamp.eventId,
11431188 provider: PROVIDER,
1144 sessionId,
1189 sessionId: asRuntimeSessionId(sessionId),
11451190 createdAt: authEndStamp.createdAt,
11461191 payload: {
11471192 isAuthenticating: false,
1148 error: toMessage(authenticateResult, "Cursor authentication failed."),
1193 error: toMessage(authenticateResult.error, "Cursor authentication failed."),
11491194 },
11501195 providerRefs: {
11511196 providerSessionId: sessionId,
......@@ -1154,7 +1199,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
11541199 source: "cursor.acp.response",
11551200 method: "authenticate",
11561201 payload: {
1157 error: toMessage(authenticateResult, "Cursor authentication failed."),
1202 error: toMessage(authenticateResult.error, "Cursor authentication failed."),
11581203 },
11591204 },
11601205 });
......@@ -1163,7 +1208,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
11631208 type: "auth.status",
11641209 eventId: authEndStamp.eventId,
11651210 provider: PROVIDER,
1166 sessionId,
1211 sessionId: asRuntimeSessionId(sessionId),
11671212 createdAt: authEndStamp.createdAt,
11681213 payload: {
11691214 isAuthenticating: false,
......@@ -1174,7 +1219,7 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
11741219 raw: {
11751220 source: "cursor.acp.response",
11761221 method: "authenticate",
1177 payload: authenticateResult,
1222 payload: authenticateResult.value,
11781223 },
11791224 });
11801225 }
......@@ -1225,9 +1270,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
12251270 type: "session.started",
12261271 eventId: sessionStartedStamp.eventId,
12271272 provider: PROVIDER,
1228 sessionId,
1273 sessionId: asRuntimeSessionId(sessionId),
12291274 createdAt: sessionStartedStamp.createdAt,
1230 threadId,
1275 threadId: asRuntimeThreadId(threadId),
12311276 payload: resumeState?.acpSessionId ? { resume: input.resumeCursor } : {},
12321277 providerRefs: {
12331278 providerSessionId: sessionId,
......@@ -1240,9 +1285,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
12401285 type: "thread.started",
12411286 eventId: threadStartedStamp.eventId,
12421287 provider: PROVIDER,
1243 sessionId,
1288 sessionId: asRuntimeSessionId(sessionId),
12441289 createdAt: threadStartedStamp.createdAt,
1245 threadId,
1290 threadId: asRuntimeThreadId(threadId),
12461291 payload: {
12471292 providerThreadId: threadId,
12481293 },
......@@ -1257,9 +1302,9 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
12571302 type: "session.state.changed",
12581303 eventId: readyStamp.eventId,
12591304 provider: PROVIDER,
1260 sessionId,
1305 sessionId: asRuntimeSessionId(sessionId),
12611306 createdAt: readyStamp.createdAt,
1262 threadId,
1307 threadId: asRuntimeThreadId(threadId),
12631308 payload: {
12641309 state: "ready",
12651310 },
......@@ -1286,10 +1331,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
12861331 });
12871332 }
12881333
1289 const turnId = ProviderTurnId.makeUnsafe(`cursor-turn-${yield* Random.nextUUIDv4}`);
1334 const turnId = ProviderTurnId.makeUnsafe(yield* Random.nextUUIDv4);
12901335 const turnState: CursorTurnState = {
12911336 turnId,
1292 assistantItemId: `cursor-assistant-${yield* Random.nextUUIDv4}`,
1337 assistantItemId: asProviderItemId(yield* Random.nextUUIDv4),
12931338 startedToolCalls: new Set(),
12941339 items: [],
12951340 };
......@@ -1307,10 +1352,10 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
13071352 type: "turn.started",
13081353 eventId: startedStamp.eventId,
13091354 provider: PROVIDER,
1310 sessionId: context.session.sessionId,
1355 sessionId: asRuntimeSessionId(context.session.sessionId),
13111356 createdAt: startedStamp.createdAt,
1312 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1313 turnId,
1357 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1358 turnId: asRuntimeTurnId(turnId),
13141359 payload: {
13151360 ...(input.model ? { model: input.model } : {}),
13161361 ...(input.effort ? { effort: input.effort } : {}),
......@@ -1340,7 +1385,16 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
13401385 catch: (cause) => toRequestError(input.sessionId, "session/prompt", cause),
13411386 });
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 });
13441398 const turnStateValue = mapStopReasonToTurnState(promptResult.stopReason);
13451399 yield* completeTurn(
13461400 context,
......@@ -1376,15 +1430,21 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
13761430 }
13771431
13781432 const cancelResult = yield* Effect.tryPromise({
1379 try: async () => sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000),
1380 catch: (cause) => cause,
1381 });
1433 try: async () =>
1434 sendRequest(context, "session/cancel", { sessionId: context.acpSessionId }, 15_000),
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) {
13841444 yield* emitRuntimeWarning(
13851445 context,
13861446 "Cursor ACP session/cancel is unavailable; marking turn as interrupted.",
1387 cancelResult,
1447 cancelResult.error,
13881448 );
13891449 }
13901450
......@@ -1456,11 +1516,11 @@ function makeCursorAdapter(options?: CursorAdapterLiveOptions) {
14561516 type: "request.resolved",
14571517 eventId: stamp.eventId,
14581518 provider: PROVIDER,
1459 sessionId: context.session.sessionId,
1519 sessionId: asRuntimeSessionId(context.session.sessionId),
14601520 createdAt: stamp.createdAt,
1461 ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1462 ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
1463 requestId,
1521 ...((context.session.threadId ? { threadId: asRuntimeThreadId(context.session.threadId) } : {})),
1522 ...((context.turnState ? { turnId: asRuntimeTurnId(context.turnState.turnId) } : {})),
1523 requestId: asRuntimeRequestId(requestId),
14641524 payload: {
14651525 requestType: pending.requestType,
14661526 decision,
apps/server/src/provider/Layers/ProviderService.test.ts+66-37
......@@ -52,6 +52,20 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make
5252const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
5353const 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
5569function makeFakeCodexAdapter(provider: ProviderKind = "codex") {
5670 const sessions = new Map<ProviderSessionId, ProviderSession>();
5771 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
......@@ -177,8 +191,8 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") {
177191 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
178192 };
179193
180 const emit = (event: ProviderRuntimeEvent): void => {
181 Effect.runSync(PubSub.publish(runtimeEventPubSub, event));
194 const emit = (event: LegacyProviderRuntimeEvent): void => {
195 Effect.runSync(PubSub.publish(runtimeEventPubSub, event as unknown as ProviderRuntimeEvent));
182196 };
183197
184198 return {
......@@ -383,16 +397,21 @@ it.effect(
383397 });
384398 }).pipe(Effect.provide(secondProviderLayer));
385399
386 assert.deepEqual(secondCodex.startSession.mock.calls, [
387 [
388 {
389 provider: "codex",
390 cwd: "/tmp/project",
391 resumeThreadId: startedSession.threadId,
392 resumeCursor: startedSession.resumeCursor,
393 },
394 ],
395 ]);
400 assert.equal(secondCodex.startSession.mock.calls.length, 1);
401 const resumedStartInput = secondCodex.startSession.mock.calls[0]?.[0];
402 assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true);
403 if (resumedStartInput && typeof resumedStartInput === "object") {
404 const startPayload = resumedStartInput as {
405 provider?: string;
406 cwd?: string;
407 resumeCursor?: unknown;
408 resumeThreadId?: string;
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 }
396415 assert.equal(secondCodex.rollbackThread.mock.calls.length, 1);
397416 const rollbackCall = secondCodex.rollbackThread.mock.calls[0];
398417 assert.equal(typeof rollbackCall?.[0], "string");
......@@ -493,16 +512,21 @@ routing.layer("ProviderServiceLive routing", (it) => {
493512 numTurns: 1,
494513 });
495514
496 assert.deepEqual(routing.codex.startSession.mock.calls, [
497 [
498 {
499 provider: "codex",
500 cwd: "/tmp/project",
501 resumeThreadId: initial.threadId,
502 resumeCursor: initial.resumeCursor,
503 },
504 ],
505 ]);
515 assert.equal(routing.codex.startSession.mock.calls.length, 1);
516 const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0];
517 assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true);
518 if (resumedStartInput && typeof resumedStartInput === "object") {
519 const startPayload = resumedStartInput as {
520 provider?: string;
521 cwd?: string;
522 resumeCursor?: unknown;
523 resumeThreadId?: string;
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 }
506530 assert.equal(routing.codex.rollbackThread.mock.calls.length, 1);
507531 const rollbackCall = routing.codex.rollbackThread.mock.calls[0];
508532 assert.equal(rollbackCall?.[1], 1);
......@@ -528,16 +552,21 @@ routing.layer("ProviderServiceLive routing", (it) => {
528552 attachments: [],
529553 });
530554
531 assert.deepEqual(routing.codex.startSession.mock.calls, [
532 [
533 {
534 provider: "codex",
535 cwd: "/tmp/project-send-turn",
536 resumeThreadId: initial.threadId,
537 resumeCursor: initial.resumeCursor,
538 },
539 ],
540 ]);
555 assert.equal(routing.codex.startSession.mock.calls.length, 1);
556 const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0];
557 assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true);
558 if (resumedStartInput && typeof resumedStartInput === "object") {
559 const startPayload = resumedStartInput as {
560 provider?: string;
561 cwd?: string;
562 resumeCursor?: unknown;
563 resumeThreadId?: string;
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 }
541570 assert.equal(routing.codex.sendTurn.mock.calls.length, 1);
542571 }),
543572 );
......@@ -626,7 +655,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
626655 ).pipe(Effect.forkChild);
627656 yield* sleep(20);
628657
629 const completedEvent: ProviderRuntimeEvent = {
658 const completedEvent: LegacyProviderRuntimeEvent = {
630659 type: "turn.completed",
631660 eventId: asEventId("evt-1"),
632661 provider: "codex",
......@@ -650,7 +679,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
650679 }),
651680 );
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", () =>
654683 Effect.gen(function* () {
655684 const provider = yield* ProviderService;
656685 const session = yield* provider.startSession(asThreadId("thread-seq"), {
......@@ -700,8 +729,8 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
700729 yield* Fiber.join(consumer);
701730 const received = yield* Ref.get(receivedRef);
702731 assert.deepEqual(
703 received.map((event) => event.sessionSequence),
704 [1, 2, 3],
732 received.map((event) => event.eventId),
733 [asEventId("evt-seq-1"), asEventId("evt-seq-2"), asEventId("evt-seq-3")],
705734 );
706735 }),
707736 );
......@@ -729,7 +758,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
729758 );
730759 yield* sleep(20);
731760
732 const events: ReadonlyArray<ProviderRuntimeEvent> = [
761 const events: ReadonlyArray<LegacyProviderRuntimeEvent> = [
733762 {
734763 type: "tool.completed",
735764 eventId: asEventId("evt-ordered-1"),
apps/server/src/provider/Layers/ProviderService.ts+5-31
......@@ -12,6 +12,7 @@
1212import {
1313 NonNegativeInt,
1414 ProviderSessionId,
15 RuntimeSessionId,
1516 ThreadId,
1617 ProviderInterruptTurnInput,
1718 ProviderRespondToRequestInput,
......@@ -123,7 +124,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
123124 const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>(
124125 new Map(),
125126 );
126 const sessionRuntimeSequenceRef = yield* Ref.make<Map<ProviderSessionId, number>>(new Map());
127 const asRuntimeSessionId = (sessionId: ProviderSessionId): RuntimeSessionId =>
128 RuntimeSessionId.makeUnsafe(sessionId);
127129
128130 const canonicalizeRuntimeEventSession = (
129131 event: ProviderRuntimeEvent,
......@@ -131,10 +133,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
131133 Ref.get(routedSessionAliasesRef).pipe(
132134 Effect.map((aliases) => {
133135 for (const [staleSessionId, liveSessionId] of aliases) {
134 if (liveSessionId === event.sessionId) {
136 if (asRuntimeSessionId(liveSessionId) === event.sessionId) {
135137 return {
136138 ...event,
137 sessionId: staleSessionId,
139 sessionId: asRuntimeSessionId(staleSessionId),
138140 } satisfies ProviderRuntimeEvent;
139141 }
140142 }
......@@ -144,21 +146,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
144146
145147 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
146148 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 ),
162149 Effect.tap((canonicalEvent) =>
163150 canonicalEventLogger
164151 ? Effect.flatMap(
......@@ -223,16 +210,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
223210 return changed ? next : current;
224211 });
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
236213 const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) =>
237214 Ref.update(routedSessionAliasesRef, (current) => {
238215 const existing = current.get(staleSessionId);
......@@ -554,11 +531,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
554531 if (routed.sessionId !== input.sessionId) {
555532 yield* directory.remove(routed.sessionId);
556533 yield* clearAliasesReferencing(routed.sessionId);
557 yield* clearSessionSequence(routed.sessionId);
558534 }
559535 yield* directory.remove(input.sessionId);
560536 yield* clearAliasesReferencing(input.sessionId);
561 yield* clearSessionSequence(input.sessionId);
562537 });
563538
564539 const listSessions: ProviderServiceShape["listSessions"] = () =>
......@@ -610,7 +585,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
610585 // Keep persisted session bindings so stale sessions can be resumed after
611586 // process restart via providerThreadId.
612587 yield* Ref.set(routedSessionAliasesRef, new Map());
613 yield* Ref.set(sessionRuntimeSequenceRef, new Map());
614588 });
615589
616590 return {
apps/server/src/wsServer.test.ts+12-10
......@@ -1234,16 +1234,18 @@ describe("WebSocket Server", () => {
12341234 return event.type === "thread.session-set";
12351235 });
12361236
1237 emitRuntimeEvent({
1238 type: "message.delta",
1239 eventId: asEventId("evt-ws-runtime-message-delta"),
1240 provider: "codex",
1241 sessionId,
1242 createdAt: new Date().toISOString(),
1243 turnId: asProviderTurnId("turn-1"),
1244 itemId: asProviderItemId("item-1"),
1245 delta: "hello from runtime",
1246 });
1237 emitRuntimeEvent(
1238 {
1239 type: "message.delta",
1240 eventId: asEventId("evt-ws-runtime-message-delta"),
1241 provider: "codex",
1242 sessionId,
1243 createdAt: new Date().toISOString(),
1244 turnId: asProviderTurnId("turn-1"),
1245 itemId: asProviderItemId("item-1"),
1246 delta: "hello from runtime",
1247 } as unknown as ProviderRuntimeEvent,
1248 );
12471249
12481250 const domainPush = await waitForPush(ws, ORCHESTRATION_WS_CHANNELS.domainEvent, (push) => {
12491251 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", () => {
100100 },
101101 ]);
102102 });
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 });
103132});
104133
105134describe("deriveWorkLogEntries", () => {
apps/web/src/session-logic.ts+1
......@@ -138,6 +138,7 @@ export function derivePendingApprovals(
138138
139139 if (activity.kind === "approval.resolved" && requestId) {
140140 openByRequestId.delete(requestId);
141 continue;
141142 }
142143 }
143144
packages/contracts/src/model.test.ts+3-3
......@@ -117,7 +117,7 @@ describe("cursor model selection", () => {
117117 thinking: false,
118118 });
119119 expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual({
120 family: "gpt-5.2-codex",
120 family: "gpt-5.3-codex",
121121 reasoning: "normal",
122122 fast: false,
123123 thinking: false,
......@@ -142,11 +142,11 @@ describe("cursor model selection", () => {
142142 it("resolves codex family selections into concrete model ids", () => {
143143 expect(
144144 resolveCursorModelFromSelection({
145 family: "gpt-5.2-codex",
145 family: "gpt-5.3-codex",
146146 reasoning: "xhigh",
147147 fast: true,
148148 }),
149 ).toBe("gpt-5.2-codex-xhigh-fast");
149 ).toBe("gpt-5.3-codex-xhigh-fast");
150150 });
151151});
152152