| author | |
| committer | |
| log | 75354bbd962d9fc38a1ba40ee78520822b349016 |
| tree | 5f47577128b4605a7d7db4f7fbfd301d41e7cf31 |
| parent | 6a10e97a97f768137f7978375e4cfa13211b063e |
- add per-session `sessionSequence` on provider runtime events and persist activity `sequence`
- migrate `projection_thread_activities` with nullable `sequence` column + index
- sort server/web activity projections by sequence fallback to timestamp/id
- allow provider sessions/turns to start before a real threadId is emitted17 files changed, 349 insertions(+), 69 deletions(-)
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+3| ... | ... | @@ -598,6 +598,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 598 | 598 | kind: event.payload.activity.kind, |
| 599 | 599 | summary: event.payload.activity.summary, |
| 600 | 600 | payload: event.payload.activity.payload, |
| 601 | ...(event.payload.activity.sequence !== undefined | |
| 602 | ? { sequence: event.payload.activity.sequence } | |
| 603 | : {}), | |
| 601 | 604 | createdAt: event.payload.activity.createdAt, |
| 602 | 605 | }); |
| 603 | 606 | return; |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+10-1| ... | ... | @@ -2,6 +2,7 @@ import { |
| 2 | 2 | ChatAttachment, |
| 3 | 3 | IsoDateTime, |
| 4 | 4 | MessageId, |
| 5 | NonNegativeInt, | |
| 5 | 6 | OrchestrationCheckpointFile, |
| 6 | 7 | OrchestrationReadModel, |
| 7 | 8 | ProjectScript, |
| ... | ... | @@ -53,6 +54,7 @@ const ProjectionThreadDbRowSchema = ProjectionThread; |
| 53 | 54 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( |
| 54 | 55 | Struct.assign({ |
| 55 | 56 | payload: Schema.fromJsonString(Schema.Unknown), |
| 57 | sequence: Schema.NullOr(NonNegativeInt), | |
| 56 | 58 | }), |
| 57 | 59 | ); |
| 58 | 60 | const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession; |
| ... | ... | @@ -195,9 +197,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 195 | 197 | kind, |
| 196 | 198 | summary, |
| 197 | 199 | payload_json AS "payload", |
| 200 | sequence, | |
| 198 | 201 | created_at AS "createdAt" |
| 199 | 202 | FROM projection_thread_activities |
| 200 | ORDER BY thread_id ASC, created_at ASC, activity_id ASC | |
| 203 | ORDER BY | |
| 204 | thread_id ASC, | |
| 205 | CASE WHEN sequence IS NULL THEN 0 ELSE 1 END ASC, | |
| 206 | sequence ASC, | |
| 207 | created_at ASC, | |
| 208 | activity_id ASC | |
| 201 | 209 | `, |
| 202 | 210 | }); |
| 203 | 211 | |
| ... | ... | @@ -398,6 +406,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 398 | 406 | summary: row.summary, |
| 399 | 407 | payload: row.payload, |
| 400 | 408 | turnId: row.turnId, |
| 409 | ...(row.sequence !== null ? { sequence: row.sequence } : {}), | |
| 401 | 410 | createdAt: row.createdAt, |
| 402 | 411 | }); |
| 403 | 412 | activitiesByThread.set(row.threadId, threadActivities); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+6| ... | ... | @@ -79,6 +79,7 @@ function truncateDetail(value: string, limit = 180): string { |
| 79 | 79 | function runtimeEventToActivities( |
| 80 | 80 | event: ProviderRuntimeEvent, |
| 81 | 81 | ): ReadonlyArray<OrchestrationThreadActivity> { |
| 82 | const maybeSequence = event.sessionSequence !== undefined ? { sequence: event.sessionSequence } : {}; | |
| 82 | 83 | switch (event.type) { |
| 83 | 84 | case "approval.requested": { |
| 84 | 85 | return [ |
| ... | ... | @@ -97,6 +98,7 @@ function runtimeEventToActivities( |
| 97 | 98 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 98 | 99 | }, |
| 99 | 100 | turnId: toTurnId(event.turnId) ?? null, |
| 101 | ...maybeSequence, | |
| 100 | 102 | }, |
| 101 | 103 | ]; |
| 102 | 104 | } |
| ... | ... | @@ -115,6 +117,7 @@ function runtimeEventToActivities( |
| 115 | 117 | ...(event.decision ? { decision: event.decision } : {}), |
| 116 | 118 | }, |
| 117 | 119 | turnId: toTurnId(event.turnId) ?? null, |
| 120 | ...maybeSequence, | |
| 118 | 121 | }, |
| 119 | 122 | ]; |
| 120 | 123 | } |
| ... | ... | @@ -131,6 +134,7 @@ function runtimeEventToActivities( |
| 131 | 134 | message: truncateDetail(event.message), |
| 132 | 135 | }, |
| 133 | 136 | turnId: toTurnId(event.turnId) ?? null, |
| 137 | ...maybeSequence, | |
| 134 | 138 | }, |
| 135 | 139 | ]; |
| 136 | 140 | } |
| ... | ... | @@ -148,6 +152,7 @@ function runtimeEventToActivities( |
| 148 | 152 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 149 | 153 | }, |
| 150 | 154 | turnId: toTurnId(event.turnId) ?? null, |
| 155 | ...maybeSequence, | |
| 151 | 156 | }, |
| 152 | 157 | ]; |
| 153 | 158 | } |
| ... | ... | @@ -165,6 +170,7 @@ function runtimeEventToActivities( |
| 165 | 170 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 166 | 171 | }, |
| 167 | 172 | turnId: toTurnId(event.turnId) ?? null, |
| 173 | ...maybeSequence, | |
| 168 | 174 | }, |
| 169 | 175 | ]; |
| 170 | 176 | } |
apps/server/src/orchestration/projector.ts+18-1| ... | ... | @@ -124,6 +124,23 @@ function retainThreadActivitiesAfterRevert( |
| 124 | 124 | ); |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | function compareThreadActivities( | |
| 128 | left: OrchestrationThread["activities"][number], | |
| 129 | right: OrchestrationThread["activities"][number], | |
| 130 | ): number { | |
| 131 | if (left.sequence !== undefined && right.sequence !== undefined) { | |
| 132 | if (left.sequence !== right.sequence) { | |
| 133 | return left.sequence - right.sequence; | |
| 134 | } | |
| 135 | } else if (left.sequence !== undefined) { | |
| 136 | return 1; | |
| 137 | } else if (right.sequence !== undefined) { | |
| 138 | return -1; | |
| 139 | } | |
| 140 | ||
| 141 | return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id); | |
| 142 | } | |
| 143 | ||
| 127 | 144 | export function createEmptyReadModel(nowIso: string): OrchestrationReadModel { |
| 128 | 145 | return { |
| 129 | 146 | snapshotSequence: 0, |
| ... | ... | @@ -503,7 +520,7 @@ export function projectEvent( |
| 503 | 520 | ...thread.activities.filter((entry) => entry.id !== payload.activity.id), |
| 504 | 521 | payload.activity, |
| 505 | 522 | ] |
| 506 | .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)) | |
| 523 | .toSorted(compareThreadActivities) | |
| 507 | 524 | .slice(-500); |
| 508 | 525 | |
| 509 | 526 | return { |
apps/server/src/persistence/Layers/ProjectionThreadActivities.ts+25-5| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 2 | 2 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; |
| 3 | import { NonNegativeInt } from "@t3tools/contracts"; | |
| 3 | 4 | import { Effect, Layer, Schema, Struct } from "effect"; |
| 4 | 5 | |
| 5 | 6 | import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts"; |
| ... | ... | @@ -15,6 +16,7 @@ import { |
| 15 | 16 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( |
| 16 | 17 | Struct.assign({ |
| 17 | 18 | payload: Schema.fromJsonString(Schema.Unknown), |
| 19 | sequence: Schema.NullOr(NonNegativeInt), | |
| 18 | 20 | }), |
| 19 | 21 | ); |
| 20 | 22 | |
| ... | ... | @@ -29,7 +31,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 29 | 31 | const sql = yield* SqlClient.SqlClient; |
| 30 | 32 | |
| 31 | 33 | const upsertProjectionThreadActivityRow = SqlSchema.void({ |
| 32 | Request: ProjectionThreadActivityDbRowSchema, | |
| 34 | Request: ProjectionThreadActivity, | |
| 33 | 35 | execute: (row) => |
| 34 | 36 | sql` |
| 35 | 37 | INSERT INTO projection_thread_activities ( |
| ... | ... | @@ -40,6 +42,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 40 | 42 | kind, |
| 41 | 43 | summary, |
| 42 | 44 | payload_json, |
| 45 | sequence, | |
| 43 | 46 | created_at |
| 44 | 47 | ) |
| 45 | 48 | VALUES ( |
| ... | ... | @@ -49,7 +52,8 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 49 | 52 | ${row.tone}, |
| 50 | 53 | ${row.kind}, |
| 51 | 54 | ${row.summary}, |
| 52 | ${row.payload}, | |
| 55 | ${JSON.stringify(row.payload)}, | |
| 56 | ${row.sequence ?? null}, | |
| 53 | 57 | ${row.createdAt} |
| 54 | 58 | ) |
| 55 | 59 | ON CONFLICT (activity_id) |
| ... | ... | @@ -60,6 +64,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 60 | 64 | kind = excluded.kind, |
| 61 | 65 | summary = excluded.summary, |
| 62 | 66 | payload_json = excluded.payload_json, |
| 67 | sequence = excluded.sequence, | |
| 63 | 68 | created_at = excluded.created_at |
| 64 | 69 | `, |
| 65 | 70 | }); |
| ... | ... | @@ -77,10 +82,15 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 77 | 82 | kind, |
| 78 | 83 | summary, |
| 79 | 84 | payload_json AS "payload", |
| 85 | sequence, | |
| 80 | 86 | created_at AS "createdAt" |
| 81 | 87 | FROM projection_thread_activities |
| 82 | 88 | WHERE thread_id = ${threadId} |
| 83 | ORDER BY created_at ASC, activity_id ASC | |
| 89 | ORDER BY | |
| 90 | CASE WHEN sequence IS NULL THEN 0 ELSE 1 END ASC, | |
| 91 | sequence ASC, | |
| 92 | created_at ASC, | |
| 93 | activity_id ASC | |
| 84 | 94 | `, |
| 85 | 95 | }); |
| 86 | 96 | |
| ... | ... | @@ -111,8 +121,18 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 111 | 121 | "ProjectionThreadActivityRepository.listByThreadId:decodeRows", |
| 112 | 122 | ), |
| 113 | 123 | ), |
| 114 | Effect.map( | |
| 115 | (rows) => rows as ReadonlyArray<Schema.Schema.Type<typeof ProjectionThreadActivity>>, | |
| 124 | Effect.map((rows) => | |
| 125 | rows.map((row) => ({ | |
| 126 | activityId: row.activityId, | |
| 127 | threadId: row.threadId, | |
| 128 | turnId: row.turnId, | |
| 129 | tone: row.tone, | |
| 130 | kind: row.kind, | |
| 131 | summary: row.summary, | |
| 132 | payload: row.payload, | |
| 133 | ...(row.sequence !== null ? { sequence: row.sequence } : {}), | |
| 134 | createdAt: row.createdAt, | |
| 135 | })), | |
| 116 | 136 | ), |
| 117 | 137 | ); |
| 118 | 138 |
apps/server/src/persistence/Migrations.ts+2| ... | ... | @@ -19,6 +19,7 @@ import Migration0004 from "./Migrations/004_ProviderSessionRuntime.ts"; |
| 19 | 19 | import Migration0005 from "./Migrations/005_Projections.ts"; |
| 20 | 20 | import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts"; |
| 21 | 21 | import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts"; |
| 22 | import Migration0008 from "./Migrations/008_ProjectionThreadActivitySequence.ts"; | |
| 22 | 23 | |
| 23 | 24 | /** |
| 24 | 25 | * Migration loader with all migrations defined inline. |
| ... | ... | @@ -38,6 +39,7 @@ const loader = Migrator.fromRecord({ |
| 38 | 39 | "5_Projections": Migration0005, |
| 39 | 40 | "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006, |
| 40 | 41 | "7_ProjectionThreadMessageAttachments": Migration0007, |
| 42 | "8_ProjectionThreadActivitySequence": Migration0008, | |
| 41 | 43 | }); |
| 42 | 44 | |
| 43 | 45 | /** |
apps/server/src/persistence/Migrations/008_ProjectionThreadActivitySequence.ts created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | import * as Effect from "effect/Effect"; | |
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 3 | ||
| 4 | export default Effect.gen(function* () { | |
| 5 | const sql = yield* SqlClient.SqlClient; | |
| 6 | ||
| 7 | yield* sql` | |
| 8 | ALTER TABLE projection_thread_activities | |
| 9 | ADD COLUMN sequence INTEGER | |
| 10 | `; | |
| 11 | ||
| 12 | yield* sql` | |
| 13 | CREATE INDEX IF NOT EXISTS idx_projection_thread_activities_thread_sequence | |
| 14 | ON projection_thread_activities(thread_id, sequence) | |
| 15 | `; | |
| 16 | }); |
apps/server/src/persistence/Services/ProjectionThreadActivities.ts+4-1| ... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 | import { |
| 10 | 10 | EventId, |
| 11 | 11 | IsoDateTime, |
| 12 | NonNegativeInt, | |
| 12 | 13 | OrchestrationThreadActivityTone, |
| 13 | 14 | ThreadId, |
| 14 | 15 | TurnId, |
| ... | ... | @@ -26,6 +27,7 @@ export const ProjectionThreadActivity = Schema.Struct({ |
| 26 | 27 | kind: Schema.String, |
| 27 | 28 | summary: Schema.String, |
| 28 | 29 | payload: Schema.Unknown, |
| 30 | sequence: Schema.optional(NonNegativeInt), | |
| 29 | 31 | createdAt: IsoDateTime, |
| 30 | 32 | }); |
| 31 | 33 | export type ProjectionThreadActivity = typeof ProjectionThreadActivity.Type; |
| ... | ... | @@ -57,7 +59,8 @@ export interface ProjectionThreadActivityRepositoryShape { |
| 57 | 59 | /** |
| 58 | 60 | * List projected thread activity rows for a thread. |
| 59 | 61 | * |
| 60 | * Returned in ascending creation order. | |
| 62 | * Returned in ascending runtime sequence order (or creation order when | |
| 63 | * sequence is unavailable). | |
| 61 | 64 | */ |
| 62 | 65 | readonly listByThreadId: ( |
| 63 | 66 | input: ListProjectionThreadActivitiesInput, |
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+67| ... | ... | @@ -505,6 +505,73 @@ describe("ClaudeCodeAdapterLive", () => { |
| 505 | 505 | ); |
| 506 | 506 | }); |
| 507 | 507 | |
| 508 | it.effect("does not fabricate provider thread ids before first SDK session_id", () => { | |
| 509 | const harness = makeHarness(); | |
| 510 | return Effect.gen(function* () { | |
| 511 | const adapter = yield* ClaudeCodeAdapter; | |
| 512 | ||
| 513 | const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( | |
| 514 | Stream.runCollect, | |
| 515 | Effect.forkChild, | |
| 516 | ); | |
| 517 | ||
| 518 | const session = yield* adapter.startSession({ | |
| 519 | provider: "claudeCode", | |
| 520 | }); | |
| 521 | assert.equal(session.threadId, undefined); | |
| 522 | ||
| 523 | const turn = yield* adapter.sendTurn({ | |
| 524 | sessionId: session.sessionId, | |
| 525 | input: "hello", | |
| 526 | attachments: [], | |
| 527 | }); | |
| 528 | assert.equal(turn.threadId, undefined); | |
| 529 | ||
| 530 | harness.query.emit({ | |
| 531 | type: "stream_event", | |
| 532 | session_id: "sdk-thread-real", | |
| 533 | uuid: "stream-thread-real", | |
| 534 | parent_tool_use_id: null, | |
| 535 | event: { | |
| 536 | type: "message_start", | |
| 537 | message: { | |
| 538 | id: "msg-thread-real", | |
| 539 | }, | |
| 540 | }, | |
| 541 | } as unknown as SDKMessage); | |
| 542 | ||
| 543 | harness.query.emit({ | |
| 544 | type: "result", | |
| 545 | subtype: "success", | |
| 546 | is_error: false, | |
| 547 | errors: [], | |
| 548 | session_id: "sdk-thread-real", | |
| 549 | uuid: "result-thread-real", | |
| 550 | } as unknown as SDKMessage); | |
| 551 | ||
| 552 | const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); | |
| 553 | assert.deepEqual( | |
| 554 | runtimeEvents.map((event) => event.type), | |
| 555 | ["session.started", "turn.started", "thread.started", "message.completed", "turn.completed"], | |
| 556 | ); | |
| 557 | ||
| 558 | const sessionStarted = runtimeEvents[0]; | |
| 559 | assert.equal(sessionStarted?.type, "session.started"); | |
| 560 | if (sessionStarted?.type === "session.started") { | |
| 561 | assert.equal("threadId" in sessionStarted, false); | |
| 562 | } | |
| 563 | ||
| 564 | const threadStarted = runtimeEvents[2]; | |
| 565 | assert.equal(threadStarted?.type, "thread.started"); | |
| 566 | if (threadStarted?.type === "thread.started") { | |
| 567 | assert.equal(threadStarted.threadId, "sdk-thread-real"); | |
| 568 | } | |
| 569 | }).pipe( | |
| 570 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | |
| 571 | Effect.provide(harness.layer), | |
| 572 | ); | |
| 573 | }); | |
| 574 | ||
| 508 | 575 | it.effect("bridges approval request/response lifecycle through canUseTool", () => { |
| 509 | 576 | const harness = makeHarness(); |
| 510 | 577 | return Effect.gen(function* () { |
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+25-20| ... | ... | @@ -126,6 +126,10 @@ function isUuid(value: string): boolean { |
| 126 | 126 | ); |
| 127 | 127 | } |
| 128 | 128 | |
| 129 | function isSyntheticClaudeThreadId(value: string): boolean { | |
| 130 | return value.startsWith("claude-thread-"); | |
| 131 | } | |
| 132 | ||
| 129 | 133 | function toMessage(cause: unknown, fallback: string): string { |
| 130 | 134 | if (cause instanceof Error && cause.message.length > 0) { |
| 131 | 135 | return cause.message; |
| ... | ... | @@ -158,7 +162,11 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef |
| 158 | 162 | turnCount?: unknown; |
| 159 | 163 | }; |
| 160 | 164 | |
| 161 | const threadId = typeof cursor.threadId === "string" ? cursor.threadId : undefined; | |
| 165 | const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; | |
| 166 | const threadId = | |
| 167 | threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) | |
| 168 | ? threadIdCandidate | |
| 169 | : undefined; | |
| 162 | 170 | const resumeCandidate = |
| 163 | 171 | typeof cursor.resume === "string" |
| 164 | 172 | ? cursor.resume |
| ... | ... | @@ -464,9 +472,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 464 | 472 | }>; |
| 465 | 473 | }> => |
| 466 | 474 | Effect.gen(function* () { |
| 467 | const threadId = | |
| 468 | context.session.threadId ?? | |
| 469 | ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`); | |
| 475 | const threadId = context.session.threadId; | |
| 476 | if (!threadId) { | |
| 477 | return yield* new ProviderAdapterValidationError({ | |
| 478 | provider: PROVIDER, | |
| 479 | operation: "readThread", | |
| 480 | issue: "Session thread id is not initialized yet.", | |
| 481 | }); | |
| 482 | } | |
| 470 | 483 | return { |
| 471 | 484 | threadId, |
| 472 | 485 | turns: context.turns.map((turn) => ({ |
| ... | ... | @@ -500,6 +513,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 500 | 513 | message: SDKMessage, |
| 501 | 514 | ): Effect.Effect<void> => |
| 502 | 515 | Effect.gen(function* () { |
| 516 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { | |
| 517 | return; | |
| 518 | } | |
| 503 | 519 | const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id); |
| 504 | 520 | context.resumeSessionId = message.session_id; |
| 505 | 521 | const changed = context.session.threadId !== nextThreadId; |
| ... | ... | @@ -922,9 +938,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 922 | 938 | `claude-session-${yield* Random.nextUUIDv4}`, |
| 923 | 939 | ); |
| 924 | 940 | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 925 | const threadId = | |
| 926 | resumeState?.threadId ?? | |
| 927 | ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`); | |
| 941 | const threadId = resumeState?.threadId; | |
| 928 | 942 | |
| 929 | 943 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); |
| 930 | 944 | const prompt = Stream.fromQueue(promptQueue).pipe( |
| ... | ... | @@ -1082,9 +1096,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1082 | 1096 | status: "ready", |
| 1083 | 1097 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 1084 | 1098 | ...(input.model ? { model: input.model } : {}), |
| 1085 | threadId, | |
| 1099 | ...(threadId ? { threadId } : {}), | |
| 1086 | 1100 | resumeCursor: { |
| 1087 | threadId, | |
| 1101 | ...(threadId ? { threadId } : {}), | |
| 1088 | 1102 | ...(resumeState?.resume ? { resume: resumeState.resume } : {}), |
| 1089 | 1103 | ...(resumeState?.resumeSessionAt |
| 1090 | 1104 | ? { resumeSessionAt: resumeState.resumeSessionAt } |
| ... | ... | @@ -1119,7 +1133,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1119 | 1133 | provider: PROVIDER, |
| 1120 | 1134 | sessionId, |
| 1121 | 1135 | createdAt: sessionStartedStamp.createdAt, |
| 1122 | threadId, | |
| 1136 | ...(threadId ? { threadId } : {}), | |
| 1123 | 1137 | }); |
| 1124 | 1138 | |
| 1125 | 1139 | Effect.runFork(runSdkStream(context)); |
| ... | ... | @@ -1186,17 +1200,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1186 | 1200 | message, |
| 1187 | 1201 | }).pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause))); |
| 1188 | 1202 | |
| 1189 | const threadId = context.session.threadId; | |
| 1190 | if (!threadId) { | |
| 1191 | return yield* new ProviderAdapterValidationError({ | |
| 1192 | provider: PROVIDER, | |
| 1193 | operation: "sendTurn", | |
| 1194 | issue: "Session thread id is not initialized.", | |
| 1195 | }); | |
| 1196 | } | |
| 1197 | ||
| 1198 | 1203 | return { |
| 1199 | threadId, | |
| 1204 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), | |
| 1200 | 1205 | turnId, |
| 1201 | 1206 | ...(context.session.resumeCursor !== undefined |
| 1202 | 1207 | ? { resumeCursor: context.session.resumeCursor } |
apps/server/src/provider/Layers/ProviderService.test.ts+77-20| ... | ... | @@ -28,7 +28,6 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 28 | 28 | import { |
| 29 | 29 | ProviderAdapterSessionNotFoundError, |
| 30 | 30 | ProviderSessionNotFoundError, |
| 31 | ProviderValidationError, | |
| 32 | 31 | ProviderUnsupportedError, |
| 33 | 32 | type ProviderAdapterError, |
| 34 | 33 | } from "../Errors.ts"; |
| ... | ... | @@ -648,6 +647,62 @@ fanout.layer("ProviderServiceLive fanout", (it) => { |
| 648 | 647 | }), |
| 649 | 648 | ); |
| 650 | 649 | |
| 650 | it.effect("assigns monotonic per-session sequence numbers to canonical runtime events", () => | |
| 651 | Effect.gen(function* () { | |
| 652 | const provider = yield* ProviderService; | |
| 653 | const session = yield* provider.startSession(asThreadId("thread-seq"), { | |
| 654 | provider: "codex", | |
| 655 | }); | |
| 656 | ||
| 657 | const receivedRef = yield* Ref.make<Array<ProviderRuntimeEvent>>([]); | |
| 658 | const consumer = yield* Stream.take(provider.streamEvents, 3).pipe( | |
| 659 | Stream.runForEach((event) => Ref.update(receivedRef, (current) => [...current, event])), | |
| 660 | Effect.forkChild, | |
| 661 | ); | |
| 662 | yield* sleep(20); | |
| 663 | ||
| 664 | fanout.codex.emit({ | |
| 665 | type: "tool.started", | |
| 666 | eventId: asEventId("evt-seq-1"), | |
| 667 | provider: "codex", | |
| 668 | sessionId: session.sessionId, | |
| 669 | createdAt: new Date().toISOString(), | |
| 670 | threadId: asProviderThreadId("thread-1"), | |
| 671 | turnId: asTurnId("turn-1"), | |
| 672 | toolKind: "command", | |
| 673 | title: "Command run", | |
| 674 | }); | |
| 675 | fanout.codex.emit({ | |
| 676 | type: "tool.completed", | |
| 677 | eventId: asEventId("evt-seq-2"), | |
| 678 | provider: "codex", | |
| 679 | sessionId: session.sessionId, | |
| 680 | createdAt: new Date().toISOString(), | |
| 681 | threadId: asProviderThreadId("thread-1"), | |
| 682 | turnId: asTurnId("turn-1"), | |
| 683 | toolKind: "command", | |
| 684 | title: "Command run", | |
| 685 | }); | |
| 686 | fanout.codex.emit({ | |
| 687 | type: "turn.completed", | |
| 688 | eventId: asEventId("evt-seq-3"), | |
| 689 | provider: "codex", | |
| 690 | sessionId: session.sessionId, | |
| 691 | createdAt: new Date().toISOString(), | |
| 692 | threadId: asProviderThreadId("thread-1"), | |
| 693 | turnId: asTurnId("turn-1"), | |
| 694 | status: "completed", | |
| 695 | }); | |
| 696 | ||
| 697 | yield* Fiber.join(consumer); | |
| 698 | const received = yield* Ref.get(receivedRef); | |
| 699 | assert.deepEqual( | |
| 700 | received.map((event) => event.sessionSequence), | |
| 701 | [1, 2, 3], | |
| 702 | ); | |
| 703 | }), | |
| 704 | ); | |
| 705 | ||
| 651 | 706 | it.effect("keeps subscriber delivery ordered and isolates failing subscribers", () => |
| 652 | 707 | Effect.gen(function* () { |
| 653 | 708 | const provider = yield* ProviderService; |
| ... | ... | @@ -746,9 +801,10 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 746 | 801 | }), |
| 747 | 802 | ); |
| 748 | 803 | |
| 749 | it.effect("fails startSession when adapter returns no threadId", () => | |
| 804 | it.effect("accepts startSession when adapter has not emitted provider thread id yet", () => | |
| 750 | 805 | Effect.gen(function* () { |
| 751 | 806 | const provider = yield* ProviderService; |
| 807 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 752 | 808 | |
| 753 | 809 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => |
| 754 | 810 | Effect.sync(() => { |
| ... | ... | @@ -757,27 +813,28 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 757 | 813 | sessionId: asSessionId("sess-missing-thread"), |
| 758 | 814 | provider: "codex", |
| 759 | 815 | status: "ready", |
| 760 | cwd: input.cwd ?? process.cwd(), | |
| 761 | createdAt: now, | |
| 762 | updatedAt: now, | |
| 763 | } satisfies ProviderSession; | |
| 764 | }), | |
| 816 | cwd: input.cwd ?? process.cwd(), | |
| 817 | createdAt: now, | |
| 818 | updatedAt: now, | |
| 819 | } satisfies ProviderSession; | |
| 820 | }), | |
| 765 | 821 | ); |
| 766 | 822 | |
| 767 | const failure = yield* Effect.result( | |
| 768 | provider.startSession(asThreadId("thread-missing"), { | |
| 769 | provider: "codex", | |
| 770 | cwd: "/tmp/project", | |
| 771 | }), | |
| 772 | ); | |
| 823 | const session = yield* provider.startSession(asThreadId("thread-missing"), { | |
| 824 | provider: "codex", | |
| 825 | cwd: "/tmp/project", | |
| 826 | }); | |
| 773 | 827 | |
| 774 | assertFailure( | |
| 775 | failure, | |
| 776 | new ProviderValidationError({ | |
| 777 | operation: "ProviderService.startSession", | |
| 778 | issue: "Provider 'codex' returned a session without threadId.", | |
| 779 | }), | |
| 780 | ); | |
| 828 | assert.equal(session.sessionId, asSessionId("sess-missing-thread")); | |
| 829 | assert.equal(session.threadId, undefined); | |
| 830 | ||
| 831 | const runtime = yield* runtimeRepository.getBySessionId({ | |
| 832 | providerSessionId: session.sessionId, | |
| 833 | }); | |
| 834 | assert.equal(Option.isSome(runtime), true); | |
| 835 | if (Option.isSome(runtime)) { | |
| 836 | assert.equal(runtime.value.providerThreadId, null); | |
| 837 | } | |
| 781 | 838 | }), |
| 782 | 839 | ); |
| 783 | 840 | }); |
apps/server/src/provider/Layers/ProviderService.ts+38-14| ... | ... | @@ -123,6 +123,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 123 | 123 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( |
| 124 | 124 | new Map(), |
| 125 | 125 | ); |
| 126 | const sessionRuntimeSequenceRef = yield* Ref.make<Map<ProviderSessionId, number>>(new Map()); | |
| 126 | 127 | |
| 127 | 128 | const canonicalizeRuntimeEventSession = ( |
| 128 | 129 | event: ProviderRuntimeEvent, |
| ... | ... | @@ -143,6 +144,21 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 143 | 144 | |
| 144 | 145 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| 145 | 146 | 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 | ), | |
| 146 | 162 | Effect.tap((canonicalEvent) => |
| 147 | 163 | canonicalEventLogger |
| 148 | 164 | ? Effect.flatMap( |
| ... | ... | @@ -165,23 +181,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 165 | 181 | |
| 166 | 182 | const upsertSessionBinding = ( |
| 167 | 183 | session: ProviderSession, |
| 168 | operation: string, | |
| 169 | 184 | threadId: ThreadId, |
| 170 | 185 | ) => |
| 171 | 186 | Effect.gen(function* () { |
| 172 | 187 | const providerThreadId = session.threadId; |
| 173 | if (!providerThreadId) { | |
| 174 | return yield* toValidationError( | |
| 175 | operation, | |
| 176 | `Provider '${session.provider}' returned a session without threadId.`, | |
| 177 | ); | |
| 178 | } | |
| 179 | 188 | |
| 180 | 189 | yield* directory.upsert({ |
| 181 | 190 | sessionId: session.sessionId, |
| 182 | 191 | provider: session.provider, |
| 183 | 192 | threadId, |
| 184 | providerThreadId, | |
| 193 | ...(providerThreadId !== undefined ? { providerThreadId } : {}), | |
| 185 | 194 | status: toRuntimeStatus(session), |
| 186 | 195 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), |
| 187 | 196 | runtimePayload: toRuntimePayloadFromSession(session), |
| ... | ... | @@ -214,6 +223,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 214 | 223 | return changed ? next : current; |
| 215 | 224 | }); |
| 216 | 225 | |
| 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 | ||
| 217 | 236 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => |
| 218 | 237 | Ref.update(routedSessionAliasesRef, (current) => { |
| 219 | 238 | const existing = current.get(staleSessionId); |
| ... | ... | @@ -262,14 +281,15 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 262 | 281 | if (existing) { |
| 263 | 282 | const existingProviderThreadId = yield* upsertSessionBinding( |
| 264 | 283 | existing, |
| 265 | `${input.operation}:upsertExistingSession`, | |
| 266 | 284 | input.binding.threadId, |
| 267 | 285 | ); |
| 268 | 286 | yield* directory.upsert({ |
| 269 | 287 | sessionId: input.staleSessionId, |
| 270 | 288 | provider: existing.provider, |
| 271 | 289 | threadId: input.binding.threadId, |
| 272 | providerThreadId: existingProviderThreadId, | |
| 290 | ...(existingProviderThreadId !== undefined | |
| 291 | ? { providerThreadId: existingProviderThreadId } | |
| 292 | : {}), | |
| 273 | 293 | ...(existing.resumeCursor !== undefined ? { resumeCursor: existing.resumeCursor } : {}), |
| 274 | 294 | }); |
| 275 | 295 | if (existing.sessionId !== input.staleSessionId) { |
| ... | ... | @@ -307,7 +327,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 307 | 327 | |
| 308 | 328 | const resumedProviderThreadId = yield* upsertSessionBinding( |
| 309 | 329 | resumed, |
| 310 | `${input.operation}:upsertRecoveredSession`, | |
| 311 | 330 | input.binding.threadId, |
| 312 | 331 | ); |
| 313 | 332 | |
| ... | ... | @@ -315,7 +334,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 315 | 334 | sessionId: input.staleSessionId, |
| 316 | 335 | provider: resumed.provider, |
| 317 | 336 | threadId: input.binding.threadId, |
| 318 | providerThreadId: resumedProviderThreadId, | |
| 337 | ...(resumedProviderThreadId !== undefined | |
| 338 | ? { providerThreadId: resumedProviderThreadId } | |
| 339 | : {}), | |
| 319 | 340 | ...(resumed.resumeCursor !== undefined ? { resumeCursor: resumed.resumeCursor } : {}), |
| 320 | 341 | }); |
| 321 | 342 | |
| ... | ... | @@ -428,7 +449,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 428 | 449 | ); |
| 429 | 450 | } |
| 430 | 451 | |
| 431 | yield* upsertSessionBinding(session, "ProviderService.startSession", threadId); | |
| 452 | yield* upsertSessionBinding(session, threadId); | |
| 432 | 453 | |
| 433 | 454 | return session; |
| 434 | 455 | }); |
| ... | ... | @@ -473,7 +494,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 473 | 494 | sessionId: input.sessionId, |
| 474 | 495 | provider: routed.adapter.provider, |
| 475 | 496 | threadId, |
| 476 | providerThreadId: turn.threadId, | |
| 497 | ...(turn.threadId !== undefined ? { providerThreadId: turn.threadId } : {}), | |
| 477 | 498 | status: "running", |
| 478 | 499 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), |
| 479 | 500 | runtimePayload: { |
| ... | ... | @@ -533,9 +554,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 533 | 554 | if (routed.sessionId !== input.sessionId) { |
| 534 | 555 | yield* directory.remove(routed.sessionId); |
| 535 | 556 | yield* clearAliasesReferencing(routed.sessionId); |
| 557 | yield* clearSessionSequence(routed.sessionId); | |
| 536 | 558 | } |
| 537 | 559 | yield* directory.remove(input.sessionId); |
| 538 | 560 | yield* clearAliasesReferencing(input.sessionId); |
| 561 | yield* clearSessionSequence(input.sessionId); | |
| 539 | 562 | }); |
| 540 | 563 | |
| 541 | 564 | const listSessions: ProviderServiceShape["listSessions"] = () => |
| ... | ... | @@ -584,6 +607,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 584 | 607 | // Keep persisted session bindings so stale sessions can be resumed after |
| 585 | 608 | // process restart via providerThreadId. |
| 586 | 609 | yield* Ref.set(routedSessionAliasesRef, new Map()); |
| 610 | yield* Ref.set(sessionRuntimeSequenceRef, new Map()); | |
| 587 | 611 | }); |
| 588 | 612 | |
| 589 | 613 | return { |
apps/web/src/session-logic.test.ts+24| ... | ... | @@ -17,6 +17,7 @@ function makeActivity(overrides: { |
| 17 | 17 | tone?: OrchestrationThreadActivity["tone"]; |
| 18 | 18 | payload?: Record<string, unknown>; |
| 19 | 19 | turnId?: string; |
| 20 | sequence?: number; | |
| 20 | 21 | }): OrchestrationThreadActivity { |
| 21 | 22 | const payload = overrides.payload ?? {}; |
| 22 | 23 | return { |
| ... | ... | @@ -27,6 +28,7 @@ function makeActivity(overrides: { |
| 27 | 28 | tone: overrides.tone ?? "tool", |
| 28 | 29 | payload, |
| 29 | 30 | turnId: overrides.turnId ? TurnId.makeUnsafe(overrides.turnId) : null, |
| 31 | ...(overrides.sequence !== undefined ? { sequence: overrides.sequence } : {}), | |
| 30 | 32 | }; |
| 31 | 33 | } |
| 32 | 34 | |
| ... | ... | @@ -131,6 +133,28 @@ describe("deriveWorkLogEntries", () => { |
| 131 | 133 | const entries = deriveWorkLogEntries(activities, undefined); |
| 132 | 134 | expect(entries.map((entry) => entry.id)).toEqual(["tool-complete"]); |
| 133 | 135 | }); |
| 136 | ||
| 137 | it("orders work log by activity sequence when present", () => { | |
| 138 | const activities: OrchestrationThreadActivity[] = [ | |
| 139 | makeActivity({ | |
| 140 | id: "second", | |
| 141 | createdAt: "2026-02-23T00:00:03.000Z", | |
| 142 | sequence: 2, | |
| 143 | summary: "Tool call complete", | |
| 144 | kind: "tool.completed", | |
| 145 | }), | |
| 146 | makeActivity({ | |
| 147 | id: "first", | |
| 148 | createdAt: "2026-02-23T00:00:04.000Z", | |
| 149 | sequence: 1, | |
| 150 | summary: "Tool call complete", | |
| 151 | kind: "tool.completed", | |
| 152 | }), | |
| 153 | ]; | |
| 154 | ||
| 155 | const entries = deriveWorkLogEntries(activities, undefined); | |
| 156 | expect(entries.map((entry) => entry.id)).toEqual(["first", "second"]); | |
| 157 | }); | |
| 134 | 158 | }); |
| 135 | 159 | |
| 136 | 160 | describe("hasToolActivityForTurn", () => { |
apps/web/src/session-logic.ts+19-6| ... | ... | @@ -91,9 +91,7 @@ export function derivePendingApprovals( |
| 91 | 91 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 92 | 92 | ): PendingApproval[] { |
| 93 | 93 | const openByRequestId = new Map<ApprovalRequestId, PendingApproval>(); |
| 94 | const ordered = [...activities].toSorted((left, right) => | |
| 95 | left.createdAt.localeCompare(right.createdAt), | |
| 96 | ); | |
| 94 | const ordered = [...activities].toSorted(compareActivitiesByOrder); | |
| 97 | 95 | |
| 98 | 96 | for (const activity of ordered) { |
| 99 | 97 | const payload = |
| ... | ... | @@ -134,9 +132,7 @@ export function deriveWorkLogEntries( |
| 134 | 132 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 135 | 133 | latestTurnId: TurnId | undefined, |
| 136 | 134 | ): WorkLogEntry[] { |
| 137 | const ordered = [...activities].toSorted((left, right) => | |
| 138 | left.createdAt.localeCompare(right.createdAt), | |
| 139 | ); | |
| 135 | const ordered = [...activities].toSorted(compareActivitiesByOrder); | |
| 140 | 136 | return ordered |
| 141 | 137 | .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true)) |
| 142 | 138 | .filter((activity) => activity.kind !== "tool.started") |
| ... | ... | @@ -159,6 +155,23 @@ export function deriveWorkLogEntries( |
| 159 | 155 | }); |
| 160 | 156 | } |
| 161 | 157 | |
| 158 | function compareActivitiesByOrder( | |
| 159 | left: OrchestrationThreadActivity, | |
| 160 | right: OrchestrationThreadActivity, | |
| 161 | ): number { | |
| 162 | if (left.sequence !== undefined && right.sequence !== undefined) { | |
| 163 | if (left.sequence !== right.sequence) { | |
| 164 | return left.sequence - right.sequence; | |
| 165 | } | |
| 166 | } else if (left.sequence !== undefined) { | |
| 167 | return 1; | |
| 168 | } else if (right.sequence !== undefined) { | |
| 169 | return -1; | |
| 170 | } | |
| 171 | ||
| 172 | return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id); | |
| 173 | } | |
| 174 | ||
| 162 | 175 | export function hasToolActivityForTurn( |
| 163 | 176 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 164 | 177 | turnId: TurnId | null | undefined, |
packages/contracts/src/orchestration.ts+1| ... | ... | @@ -212,6 +212,7 @@ export const OrchestrationThreadActivity = Schema.Struct({ |
| 212 | 212 | summary: TrimmedNonEmptyString, |
| 213 | 213 | payload: Schema.Unknown, |
| 214 | 214 | turnId: Schema.NullOr(TurnId), |
| 215 | sequence: Schema.optional(NonNegativeInt), | |
| 215 | 216 | createdAt: IsoDateTime, |
| 216 | 217 | }); |
| 217 | 218 | export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Type; |
packages/contracts/src/provider.ts+1-1| ... | ... | @@ -93,7 +93,7 @@ export const ProviderSendTurnInput = Schema.Struct({ |
| 93 | 93 | export type ProviderSendTurnInput = typeof ProviderSendTurnInput.Type; |
| 94 | 94 | |
| 95 | 95 | export const ProviderTurnStartResult = Schema.Struct({ |
| 96 | threadId: ProviderThreadId, | |
| 96 | threadId: Schema.optional(ProviderThreadId), | |
| 97 | 97 | turnId: ProviderTurnId, |
| 98 | 98 | resumeCursor: Schema.optional(Schema.Unknown), |
| 99 | 99 | }); |
packages/contracts/src/providerRuntime.ts+13| ... | ... | @@ -31,6 +31,7 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ |
| 31 | 31 | eventId: EventId, |
| 32 | 32 | provider: ProviderKind, |
| 33 | 33 | sessionId: ProviderSessionId, |
| 34 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 34 | 35 | createdAt: IsoDateTime, |
| 35 | 36 | threadId: Schema.optional(ProviderThreadId), |
| 36 | 37 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | ... | @@ -42,6 +43,7 @@ export const ProviderRuntimeSessionExitedEvent = Schema.Struct({ |
| 42 | 43 | eventId: EventId, |
| 43 | 44 | provider: ProviderKind, |
| 44 | 45 | sessionId: ProviderSessionId, |
| 46 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 45 | 47 | createdAt: IsoDateTime, |
| 46 | 48 | threadId: Schema.optional(ProviderThreadId), |
| 47 | 49 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | ... | @@ -53,6 +55,7 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({ |
| 53 | 55 | eventId: EventId, |
| 54 | 56 | provider: ProviderKind, |
| 55 | 57 | sessionId: ProviderSessionId, |
| 58 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 56 | 59 | createdAt: IsoDateTime, |
| 57 | 60 | threadId: ProviderThreadId, |
| 58 | 61 | }); |
| ... | ... | @@ -63,6 +66,7 @@ export const ProviderRuntimeTurnStartedEvent = Schema.Struct({ |
| 63 | 66 | eventId: EventId, |
| 64 | 67 | provider: ProviderKind, |
| 65 | 68 | sessionId: ProviderSessionId, |
| 69 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 66 | 70 | createdAt: IsoDateTime, |
| 67 | 71 | threadId: Schema.optional(ProviderThreadId), |
| 68 | 72 | turnId: ProviderTurnId, |
| ... | ... | @@ -74,6 +78,7 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ |
| 74 | 78 | eventId: EventId, |
| 75 | 79 | provider: ProviderKind, |
| 76 | 80 | sessionId: ProviderSessionId, |
| 81 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 77 | 82 | createdAt: IsoDateTime, |
| 78 | 83 | threadId: Schema.optional(ProviderThreadId), |
| 79 | 84 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -87,6 +92,7 @@ export const ProviderRuntimeMessageDeltaEvent = Schema.Struct({ |
| 87 | 92 | eventId: EventId, |
| 88 | 93 | provider: ProviderKind, |
| 89 | 94 | sessionId: ProviderSessionId, |
| 95 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 90 | 96 | createdAt: IsoDateTime, |
| 91 | 97 | threadId: Schema.optional(ProviderThreadId), |
| 92 | 98 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -100,6 +106,7 @@ export const ProviderRuntimeMessageCompletedEvent = Schema.Struct({ |
| 100 | 106 | eventId: EventId, |
| 101 | 107 | provider: ProviderKind, |
| 102 | 108 | sessionId: ProviderSessionId, |
| 109 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 103 | 110 | createdAt: IsoDateTime, |
| 104 | 111 | itemId: ProviderItemId, |
| 105 | 112 | threadId: Schema.optional(ProviderThreadId), |
| ... | ... | @@ -112,6 +119,7 @@ export const ProviderRuntimeToolStartedEvent = Schema.Struct({ |
| 112 | 119 | eventId: EventId, |
| 113 | 120 | provider: ProviderKind, |
| 114 | 121 | sessionId: ProviderSessionId, |
| 122 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 115 | 123 | createdAt: IsoDateTime, |
| 116 | 124 | threadId: Schema.optional(ProviderThreadId), |
| 117 | 125 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -127,6 +135,7 @@ export const ProviderRuntimeToolCompletedEvent = Schema.Struct({ |
| 127 | 135 | eventId: EventId, |
| 128 | 136 | provider: ProviderKind, |
| 129 | 137 | sessionId: ProviderSessionId, |
| 138 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 130 | 139 | createdAt: IsoDateTime, |
| 131 | 140 | threadId: Schema.optional(ProviderThreadId), |
| 132 | 141 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -142,6 +151,7 @@ export const ProviderRuntimeApprovalRequestedEvent = Schema.Struct({ |
| 142 | 151 | eventId: EventId, |
| 143 | 152 | provider: ProviderKind, |
| 144 | 153 | sessionId: ProviderSessionId, |
| 154 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 145 | 155 | createdAt: IsoDateTime, |
| 146 | 156 | threadId: Schema.optional(ProviderThreadId), |
| 147 | 157 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -158,6 +168,7 @@ export const ProviderRuntimeApprovalResolvedEvent = Schema.Struct({ |
| 158 | 168 | eventId: EventId, |
| 159 | 169 | provider: ProviderKind, |
| 160 | 170 | sessionId: ProviderSessionId, |
| 171 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 161 | 172 | createdAt: IsoDateTime, |
| 162 | 173 | threadId: Schema.optional(ProviderThreadId), |
| 163 | 174 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -173,6 +184,7 @@ export const ProviderRuntimeCheckpointCapturedEvent = Schema.Struct({ |
| 173 | 184 | eventId: EventId, |
| 174 | 185 | provider: ProviderKind, |
| 175 | 186 | sessionId: ProviderSessionId, |
| 187 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 176 | 188 | createdAt: IsoDateTime, |
| 177 | 189 | threadId: ProviderThreadId, |
| 178 | 190 | turnId: Schema.optional(ProviderTurnId), |
| ... | ... | @@ -187,6 +199,7 @@ export const ProviderRuntimeErrorEvent = Schema.Struct({ |
| 187 | 199 | eventId: EventId, |
| 188 | 200 | provider: ProviderKind, |
| 189 | 201 | sessionId: ProviderSessionId, |
| 202 | sessionSequence: Schema.optional(NonNegativeInt), | |
| 190 | 203 | createdAt: IsoDateTime, |
| 191 | 204 | threadId: Schema.optional(ProviderThreadId), |
| 192 | 205 | turnId: Schema.optional(ProviderTurnId), |