| 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,6 +598,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 598 | kind: event.payload.activity.kind, | 598 | kind: event.payload.activity.kind, |
| 599 | summary: event.payload.activity.summary, | 599 | summary: event.payload.activity.summary, |
| 600 | payload: event.payload.activity.payload, | 600 | payload: event.payload.activity.payload, |
| 601 | ...(event.payload.activity.sequence !== undefined | ||
| 602 | ? { sequence: event.payload.activity.sequence } | ||
| 603 | : {}), | ||
| 601 | createdAt: event.payload.activity.createdAt, | 604 | createdAt: event.payload.activity.createdAt, |
| 602 | }); | 605 | }); |
| 603 | return; | 606 | return; |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+10-1| ... | @@ -2,6 +2,7 @@ import { | ... | @@ -2,6 +2,7 @@ import { |
| 2 | ChatAttachment, | 2 | ChatAttachment, |
| 3 | IsoDateTime, | 3 | IsoDateTime, |
| 4 | MessageId, | 4 | MessageId, |
| 5 | NonNegativeInt, | ||
| 5 | OrchestrationCheckpointFile, | 6 | OrchestrationCheckpointFile, |
| 6 | OrchestrationReadModel, | 7 | OrchestrationReadModel, |
| 7 | ProjectScript, | 8 | ProjectScript, |
| ... | @@ -53,6 +54,7 @@ const ProjectionThreadDbRowSchema = ProjectionThread; | ... | @@ -53,6 +54,7 @@ const ProjectionThreadDbRowSchema = ProjectionThread; |
| 53 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( | 54 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( |
| 54 | Struct.assign({ | 55 | Struct.assign({ |
| 55 | payload: Schema.fromJsonString(Schema.Unknown), | 56 | payload: Schema.fromJsonString(Schema.Unknown), |
| 57 | sequence: Schema.NullOr(NonNegativeInt), | ||
| 56 | }), | 58 | }), |
| 57 | ); | 59 | ); |
| 58 | const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession; | 60 | const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession; |
| ... | @@ -195,9 +197,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -195,9 +197,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 195 | kind, | 197 | kind, |
| 196 | summary, | 198 | summary, |
| 197 | payload_json AS "payload", | 199 | payload_json AS "payload", |
| 200 | sequence, | ||
| 198 | created_at AS "createdAt" | 201 | created_at AS "createdAt" |
| 199 | FROM projection_thread_activities | 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,6 +406,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 398 | summary: row.summary, | 406 | summary: row.summary, |
| 399 | payload: row.payload, | 407 | payload: row.payload, |
| 400 | turnId: row.turnId, | 408 | turnId: row.turnId, |
| 409 | ...(row.sequence !== null ? { sequence: row.sequence } : {}), | ||
| 401 | createdAt: row.createdAt, | 410 | createdAt: row.createdAt, |
| 402 | }); | 411 | }); |
| 403 | activitiesByThread.set(row.threadId, threadActivities); | 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,6 +79,7 @@ function truncateDetail(value: string, limit = 180): string { |
| 79 | function runtimeEventToActivities( | 79 | function runtimeEventToActivities( |
| 80 | event: ProviderRuntimeEvent, | 80 | event: ProviderRuntimeEvent, |
| 81 | ): ReadonlyArray<OrchestrationThreadActivity> { | 81 | ): ReadonlyArray<OrchestrationThreadActivity> { |
| 82 | const maybeSequence = event.sessionSequence !== undefined ? { sequence: event.sessionSequence } : {}; | ||
| 82 | switch (event.type) { | 83 | switch (event.type) { |
| 83 | case "approval.requested": { | 84 | case "approval.requested": { |
| 84 | return [ | 85 | return [ |
| ... | @@ -97,6 +98,7 @@ function runtimeEventToActivities( | ... | @@ -97,6 +98,7 @@ function runtimeEventToActivities( |
| 97 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), | 98 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 98 | }, | 99 | }, |
| 99 | turnId: toTurnId(event.turnId) ?? null, | 100 | turnId: toTurnId(event.turnId) ?? null, |
| 101 | ...maybeSequence, | ||
| 100 | }, | 102 | }, |
| 101 | ]; | 103 | ]; |
| 102 | } | 104 | } |
| ... | @@ -115,6 +117,7 @@ function runtimeEventToActivities( | ... | @@ -115,6 +117,7 @@ function runtimeEventToActivities( |
| 115 | ...(event.decision ? { decision: event.decision } : {}), | 117 | ...(event.decision ? { decision: event.decision } : {}), |
| 116 | }, | 118 | }, |
| 117 | turnId: toTurnId(event.turnId) ?? null, | 119 | turnId: toTurnId(event.turnId) ?? null, |
| 120 | ...maybeSequence, | ||
| 118 | }, | 121 | }, |
| 119 | ]; | 122 | ]; |
| 120 | } | 123 | } |
| ... | @@ -131,6 +134,7 @@ function runtimeEventToActivities( | ... | @@ -131,6 +134,7 @@ function runtimeEventToActivities( |
| 131 | message: truncateDetail(event.message), | 134 | message: truncateDetail(event.message), |
| 132 | }, | 135 | }, |
| 133 | turnId: toTurnId(event.turnId) ?? null, | 136 | turnId: toTurnId(event.turnId) ?? null, |
| 137 | ...maybeSequence, | ||
| 134 | }, | 138 | }, |
| 135 | ]; | 139 | ]; |
| 136 | } | 140 | } |
| ... | @@ -148,6 +152,7 @@ function runtimeEventToActivities( | ... | @@ -148,6 +152,7 @@ function runtimeEventToActivities( |
| 148 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), | 152 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 149 | }, | 153 | }, |
| 150 | turnId: toTurnId(event.turnId) ?? null, | 154 | turnId: toTurnId(event.turnId) ?? null, |
| 155 | ...maybeSequence, | ||
| 151 | }, | 156 | }, |
| 152 | ]; | 157 | ]; |
| 153 | } | 158 | } |
| ... | @@ -165,6 +170,7 @@ function runtimeEventToActivities( | ... | @@ -165,6 +170,7 @@ function runtimeEventToActivities( |
| 165 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), | 170 | ...(event.detail ? { detail: truncateDetail(event.detail) } : {}), |
| 166 | }, | 171 | }, |
| 167 | turnId: toTurnId(event.turnId) ?? null, | 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,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 | export function createEmptyReadModel(nowIso: string): OrchestrationReadModel { | 144 | export function createEmptyReadModel(nowIso: string): OrchestrationReadModel { |
| 128 | return { | 145 | return { |
| 129 | snapshotSequence: 0, | 146 | snapshotSequence: 0, |
| ... | @@ -503,7 +520,7 @@ export function projectEvent( | ... | @@ -503,7 +520,7 @@ export function projectEvent( |
| 503 | ...thread.activities.filter((entry) => entry.id !== payload.activity.id), | 520 | ...thread.activities.filter((entry) => entry.id !== payload.activity.id), |
| 504 | payload.activity, | 521 | payload.activity, |
| 505 | ] | 522 | ] |
| 506 | .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)) | 523 | .toSorted(compareThreadActivities) |
| 507 | .slice(-500); | 524 | .slice(-500); |
| 508 | 525 | ||
| 509 | return { | 526 | return { |
apps/server/src/persistence/Layers/ProjectionThreadActivities.ts+25-5| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 1 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 2 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | 2 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; |
| 3 | import { NonNegativeInt } from "@t3tools/contracts"; | ||
| 3 | import { Effect, Layer, Schema, Struct } from "effect"; | 4 | import { Effect, Layer, Schema, Struct } from "effect"; |
| 4 | 5 | ||
| 5 | import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts"; | 6 | import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts"; |
| ... | @@ -15,6 +16,7 @@ import { | ... | @@ -15,6 +16,7 @@ import { |
| 15 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( | 16 | const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( |
| 16 | Struct.assign({ | 17 | Struct.assign({ |
| 17 | payload: Schema.fromJsonString(Schema.Unknown), | 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,7 +31,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 29 | const sql = yield* SqlClient.SqlClient; | 31 | const sql = yield* SqlClient.SqlClient; |
| 30 | 32 | ||
| 31 | const upsertProjectionThreadActivityRow = SqlSchema.void({ | 33 | const upsertProjectionThreadActivityRow = SqlSchema.void({ |
| 32 | Request: ProjectionThreadActivityDbRowSchema, | 34 | Request: ProjectionThreadActivity, |
| 33 | execute: (row) => | 35 | execute: (row) => |
| 34 | sql` | 36 | sql` |
| 35 | INSERT INTO projection_thread_activities ( | 37 | INSERT INTO projection_thread_activities ( |
| ... | @@ -40,6 +42,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { | ... | @@ -40,6 +42,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 40 | kind, | 42 | kind, |
| 41 | summary, | 43 | summary, |
| 42 | payload_json, | 44 | payload_json, |
| 45 | sequence, | ||
| 43 | created_at | 46 | created_at |
| 44 | ) | 47 | ) |
| 45 | VALUES ( | 48 | VALUES ( |
| ... | @@ -49,7 +52,8 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { | ... | @@ -49,7 +52,8 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 49 | ${row.tone}, | 52 | ${row.tone}, |
| 50 | ${row.kind}, | 53 | ${row.kind}, |
| 51 | ${row.summary}, | 54 | ${row.summary}, |
| 52 | ${row.payload}, | 55 | ${JSON.stringify(row.payload)}, |
| 56 | ${row.sequence ?? null}, | ||
| 53 | ${row.createdAt} | 57 | ${row.createdAt} |
| 54 | ) | 58 | ) |
| 55 | ON CONFLICT (activity_id) | 59 | ON CONFLICT (activity_id) |
| ... | @@ -60,6 +64,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { | ... | @@ -60,6 +64,7 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 60 | kind = excluded.kind, | 64 | kind = excluded.kind, |
| 61 | summary = excluded.summary, | 65 | summary = excluded.summary, |
| 62 | payload_json = excluded.payload_json, | 66 | payload_json = excluded.payload_json, |
| 67 | sequence = excluded.sequence, | ||
| 63 | created_at = excluded.created_at | 68 | created_at = excluded.created_at |
| 64 | `, | 69 | `, |
| 65 | }); | 70 | }); |
| ... | @@ -77,10 +82,15 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { | ... | @@ -77,10 +82,15 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 77 | kind, | 82 | kind, |
| 78 | summary, | 83 | summary, |
| 79 | payload_json AS "payload", | 84 | payload_json AS "payload", |
| 85 | sequence, | ||
| 80 | created_at AS "createdAt" | 86 | created_at AS "createdAt" |
| 81 | FROM projection_thread_activities | 87 | FROM projection_thread_activities |
| 82 | WHERE thread_id = ${threadId} | 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,8 +121,18 @@ const makeProjectionThreadActivityRepository = Effect.gen(function* () { |
| 111 | "ProjectionThreadActivityRepository.listByThreadId:decodeRows", | 121 | "ProjectionThreadActivityRepository.listByThreadId:decodeRows", |
| 112 | ), | 122 | ), |
| 113 | ), | 123 | ), |
| 114 | Effect.map( | 124 | Effect.map((rows) => |
| 115 | (rows) => rows as ReadonlyArray<Schema.Schema.Type<typeof ProjectionThreadActivity>>, | 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,6 +19,7 @@ import Migration0004 from "./Migrations/004_ProviderSessionRuntime.ts"; |
| 19 | import Migration0005 from "./Migrations/005_Projections.ts"; | 19 | import Migration0005 from "./Migrations/005_Projections.ts"; |
| 20 | import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts"; | 20 | import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts"; |
| 21 | import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts"; | 21 | import Migration0007 from "./Migrations/007_ProjectionThreadMessageAttachments.ts"; |
| 22 | import Migration0008 from "./Migrations/008_ProjectionThreadActivitySequence.ts"; | ||
| 22 | 23 | ||
| 23 | /** | 24 | /** |
| 24 | * Migration loader with all migrations defined inline. | 25 | * Migration loader with all migrations defined inline. |
| ... | @@ -38,6 +39,7 @@ const loader = Migrator.fromRecord({ | ... | @@ -38,6 +39,7 @@ const loader = Migrator.fromRecord({ |
| 38 | "5_Projections": Migration0005, | 39 | "5_Projections": Migration0005, |
| 39 | "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006, | 40 | "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006, |
| 40 | "7_ProjectionThreadMessageAttachments": Migration0007, | 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,6 +9,7 @@ |
| 9 | import { | 9 | import { |
| 10 | EventId, | 10 | EventId, |
| 11 | IsoDateTime, | 11 | IsoDateTime, |
| 12 | NonNegativeInt, | ||
| 12 | OrchestrationThreadActivityTone, | 13 | OrchestrationThreadActivityTone, |
| 13 | ThreadId, | 14 | ThreadId, |
| 14 | TurnId, | 15 | TurnId, |
| ... | @@ -26,6 +27,7 @@ export const ProjectionThreadActivity = Schema.Struct({ | ... | @@ -26,6 +27,7 @@ export const ProjectionThreadActivity = Schema.Struct({ |
| 26 | kind: Schema.String, | 27 | kind: Schema.String, |
| 27 | summary: Schema.String, | 28 | summary: Schema.String, |
| 28 | payload: Schema.Unknown, | 29 | payload: Schema.Unknown, |
| 30 | sequence: Schema.optional(NonNegativeInt), | ||
| 29 | createdAt: IsoDateTime, | 31 | createdAt: IsoDateTime, |
| 30 | }); | 32 | }); |
| 31 | export type ProjectionThreadActivity = typeof ProjectionThreadActivity.Type; | 33 | export type ProjectionThreadActivity = typeof ProjectionThreadActivity.Type; |
| ... | @@ -57,7 +59,8 @@ export interface ProjectionThreadActivityRepositoryShape { | ... | @@ -57,7 +59,8 @@ export interface ProjectionThreadActivityRepositoryShape { |
| 57 | /** | 59 | /** |
| 58 | * List projected thread activity rows for a thread. | 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 | readonly listByThreadId: ( | 65 | readonly listByThreadId: ( |
| 63 | input: ListProjectionThreadActivitiesInput, | 66 | input: ListProjectionThreadActivitiesInput, |
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+67| ... | @@ -505,6 +505,73 @@ describe("ClaudeCodeAdapterLive", () => { | ... | @@ -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 | it.effect("bridges approval request/response lifecycle through canUseTool", () => { | 575 | it.effect("bridges approval request/response lifecycle through canUseTool", () => { |
| 509 | const harness = makeHarness(); | 576 | const harness = makeHarness(); |
| 510 | return Effect.gen(function* () { | 577 | return Effect.gen(function* () { |
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+25-20| ... | @@ -126,6 +126,10 @@ function isUuid(value: string): boolean { | ... | @@ -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 | function toMessage(cause: unknown, fallback: string): string { | 133 | function toMessage(cause: unknown, fallback: string): string { |
| 130 | if (cause instanceof Error && cause.message.length > 0) { | 134 | if (cause instanceof Error && cause.message.length > 0) { |
| 131 | return cause.message; | 135 | return cause.message; |
| ... | @@ -158,7 +162,11 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef | ... | @@ -158,7 +162,11 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef |
| 158 | turnCount?: unknown; | 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 | const resumeCandidate = | 170 | const resumeCandidate = |
| 163 | typeof cursor.resume === "string" | 171 | typeof cursor.resume === "string" |
| 164 | ? cursor.resume | 172 | ? cursor.resume |
| ... | @@ -464,9 +472,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -464,9 +472,14 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 464 | }>; | 472 | }>; |
| 465 | }> => | 473 | }> => |
| 466 | Effect.gen(function* () { | 474 | Effect.gen(function* () { |
| 467 | const threadId = | 475 | const threadId = context.session.threadId; |
| 468 | context.session.threadId ?? | 476 | if (!threadId) { |
| 469 | ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`); | 477 | return yield* new ProviderAdapterValidationError({ |
| 478 | provider: PROVIDER, | ||
| 479 | operation: "readThread", | ||
| 480 | issue: "Session thread id is not initialized yet.", | ||
| 481 | }); | ||
| 482 | } | ||
| 470 | return { | 483 | return { |
| 471 | threadId, | 484 | threadId, |
| 472 | turns: context.turns.map((turn) => ({ | 485 | turns: context.turns.map((turn) => ({ |
| ... | @@ -500,6 +513,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -500,6 +513,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 500 | message: SDKMessage, | 513 | message: SDKMessage, |
| 501 | ): Effect.Effect<void> => | 514 | ): Effect.Effect<void> => |
| 502 | Effect.gen(function* () { | 515 | Effect.gen(function* () { |
| 516 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { | ||
| 517 | return; | ||
| 518 | } | ||
| 503 | const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id); | 519 | const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id); |
| 504 | context.resumeSessionId = message.session_id; | 520 | context.resumeSessionId = message.session_id; |
| 505 | const changed = context.session.threadId !== nextThreadId; | 521 | const changed = context.session.threadId !== nextThreadId; |
| ... | @@ -922,9 +938,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -922,9 +938,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 922 | `claude-session-${yield* Random.nextUUIDv4}`, | 938 | `claude-session-${yield* Random.nextUUIDv4}`, |
| 923 | ); | 939 | ); |
| 924 | const resumeState = readClaudeResumeState(input.resumeCursor); | 940 | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 925 | const threadId = | 941 | const threadId = resumeState?.threadId; |
| 926 | resumeState?.threadId ?? | ||
| 927 | ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`); | ||
| 928 | 942 | ||
| 929 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); | 943 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); |
| 930 | const prompt = Stream.fromQueue(promptQueue).pipe( | 944 | const prompt = Stream.fromQueue(promptQueue).pipe( |
| ... | @@ -1082,9 +1096,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1082,9 +1096,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1082 | status: "ready", | 1096 | status: "ready", |
| 1083 | ...(input.cwd ? { cwd: input.cwd } : {}), | 1097 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 1084 | ...(input.model ? { model: input.model } : {}), | 1098 | ...(input.model ? { model: input.model } : {}), |
| 1085 | threadId, | 1099 | ...(threadId ? { threadId } : {}), |
| 1086 | resumeCursor: { | 1100 | resumeCursor: { |
| 1087 | threadId, | 1101 | ...(threadId ? { threadId } : {}), |
| 1088 | ...(resumeState?.resume ? { resume: resumeState.resume } : {}), | 1102 | ...(resumeState?.resume ? { resume: resumeState.resume } : {}), |
| 1089 | ...(resumeState?.resumeSessionAt | 1103 | ...(resumeState?.resumeSessionAt |
| 1090 | ? { resumeSessionAt: resumeState.resumeSessionAt } | 1104 | ? { resumeSessionAt: resumeState.resumeSessionAt } |
| ... | @@ -1119,7 +1133,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1119,7 +1133,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1119 | provider: PROVIDER, | 1133 | provider: PROVIDER, |
| 1120 | sessionId, | 1134 | sessionId, |
| 1121 | createdAt: sessionStartedStamp.createdAt, | 1135 | createdAt: sessionStartedStamp.createdAt, |
| 1122 | threadId, | 1136 | ...(threadId ? { threadId } : {}), |
| 1123 | }); | 1137 | }); |
| 1124 | 1138 | ||
| 1125 | Effect.runFork(runSdkStream(context)); | 1139 | Effect.runFork(runSdkStream(context)); |
| ... | @@ -1186,17 +1200,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1186,17 +1200,8 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1186 | message, | 1200 | message, |
| 1187 | }).pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause))); | 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 | return { | 1203 | return { |
| 1199 | threadId, | 1204 | ...(context.session.threadId ? { threadId: context.session.threadId } : {}), |
| 1200 | turnId, | 1205 | turnId, |
| 1201 | ...(context.session.resumeCursor !== undefined | 1206 | ...(context.session.resumeCursor !== undefined |
| 1202 | ? { resumeCursor: context.session.resumeCursor } | 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,7 +28,6 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 28 | import { | 28 | import { |
| 29 | ProviderAdapterSessionNotFoundError, | 29 | ProviderAdapterSessionNotFoundError, |
| 30 | ProviderSessionNotFoundError, | 30 | ProviderSessionNotFoundError, |
| 31 | ProviderValidationError, | ||
| 32 | ProviderUnsupportedError, | 31 | ProviderUnsupportedError, |
| 33 | type ProviderAdapterError, | 32 | type ProviderAdapterError, |
| 34 | } from "../Errors.ts"; | 33 | } from "../Errors.ts"; |
| ... | @@ -648,6 +647,62 @@ fanout.layer("ProviderServiceLive fanout", (it) => { | ... | @@ -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 | it.effect("keeps subscriber delivery ordered and isolates failing subscribers", () => | 706 | it.effect("keeps subscriber delivery ordered and isolates failing subscribers", () => |
| 652 | Effect.gen(function* () { | 707 | Effect.gen(function* () { |
| 653 | const provider = yield* ProviderService; | 708 | const provider = yield* ProviderService; |
| ... | @@ -746,9 +801,10 @@ validation.layer("ProviderServiceLive validation", (it) => { | ... | @@ -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 | Effect.gen(function* () { | 805 | Effect.gen(function* () { |
| 751 | const provider = yield* ProviderService; | 806 | const provider = yield* ProviderService; |
| 807 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | ||
| 752 | 808 | ||
| 753 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => | 809 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => |
| 754 | Effect.sync(() => { | 810 | Effect.sync(() => { |
| ... | @@ -757,27 +813,28 @@ validation.layer("ProviderServiceLive validation", (it) => { | ... | @@ -757,27 +813,28 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 757 | sessionId: asSessionId("sess-missing-thread"), | 813 | sessionId: asSessionId("sess-missing-thread"), |
| 758 | provider: "codex", | 814 | provider: "codex", |
| 759 | status: "ready", | 815 | status: "ready", |
| 760 | cwd: input.cwd ?? process.cwd(), | 816 | cwd: input.cwd ?? process.cwd(), |
| 761 | createdAt: now, | 817 | createdAt: now, |
| 762 | updatedAt: now, | 818 | updatedAt: now, |
| 763 | } satisfies ProviderSession; | 819 | } satisfies ProviderSession; |
| 764 | }), | 820 | }), |
| 765 | ); | 821 | ); |
| 766 | 822 | ||
| 767 | const failure = yield* Effect.result( | 823 | const session = yield* provider.startSession(asThreadId("thread-missing"), { |
| 768 | provider.startSession(asThreadId("thread-missing"), { | 824 | provider: "codex", |
| 769 | provider: "codex", | 825 | cwd: "/tmp/project", |
| 770 | cwd: "/tmp/project", | 826 | }); |
| 771 | }), | ||
| 772 | ); | ||
| 773 | 827 | ||
| 774 | assertFailure( | 828 | assert.equal(session.sessionId, asSessionId("sess-missing-thread")); |
| 775 | failure, | 829 | assert.equal(session.threadId, undefined); |
| 776 | new ProviderValidationError({ | 830 | |
| 777 | operation: "ProviderService.startSession", | 831 | const runtime = yield* runtimeRepository.getBySessionId({ |
| 778 | issue: "Provider 'codex' returned a session without threadId.", | 832 | providerSessionId: session.sessionId, |
| 779 | }), | 833 | }); |
| 780 | ); | 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,6 +123,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 123 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( | 123 | const routedSessionAliasesRef = yield* Ref.make<Map<ProviderSessionId, ProviderSessionId>>( |
| 124 | new Map(), | 124 | new Map(), |
| 125 | ); | 125 | ); |
| 126 | const sessionRuntimeSequenceRef = yield* Ref.make<Map<ProviderSessionId, number>>(new Map()); | ||
| 126 | 127 | ||
| 127 | const canonicalizeRuntimeEventSession = ( | 128 | const canonicalizeRuntimeEventSession = ( |
| 128 | event: ProviderRuntimeEvent, | 129 | event: ProviderRuntimeEvent, |
| ... | @@ -143,6 +144,21 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -143,6 +144,21 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 143 | 144 | ||
| 144 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => | 145 | const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| 145 | canonicalizeRuntimeEventSession(event).pipe( | 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 | Effect.tap((canonicalEvent) => | 162 | Effect.tap((canonicalEvent) => |
| 147 | canonicalEventLogger | 163 | canonicalEventLogger |
| 148 | ? Effect.flatMap( | 164 | ? Effect.flatMap( |
| ... | @@ -165,23 +181,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -165,23 +181,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 165 | 181 | ||
| 166 | const upsertSessionBinding = ( | 182 | const upsertSessionBinding = ( |
| 167 | session: ProviderSession, | 183 | session: ProviderSession, |
| 168 | operation: string, | ||
| 169 | threadId: ThreadId, | 184 | threadId: ThreadId, |
| 170 | ) => | 185 | ) => |
| 171 | Effect.gen(function* () { | 186 | Effect.gen(function* () { |
| 172 | const providerThreadId = session.threadId; | 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 | yield* directory.upsert({ | 189 | yield* directory.upsert({ |
| 181 | sessionId: session.sessionId, | 190 | sessionId: session.sessionId, |
| 182 | provider: session.provider, | 191 | provider: session.provider, |
| 183 | threadId, | 192 | threadId, |
| 184 | providerThreadId, | 193 | ...(providerThreadId !== undefined ? { providerThreadId } : {}), |
| 185 | status: toRuntimeStatus(session), | 194 | status: toRuntimeStatus(session), |
| 186 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), | 195 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), |
| 187 | runtimePayload: toRuntimePayloadFromSession(session), | 196 | runtimePayload: toRuntimePayloadFromSession(session), |
| ... | @@ -214,6 +223,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -214,6 +223,16 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 214 | return changed ? next : current; | 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 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => | 236 | const setAlias = (staleSessionId: ProviderSessionId, liveSessionId: ProviderSessionId) => |
| 218 | Ref.update(routedSessionAliasesRef, (current) => { | 237 | Ref.update(routedSessionAliasesRef, (current) => { |
| 219 | const existing = current.get(staleSessionId); | 238 | const existing = current.get(staleSessionId); |
| ... | @@ -262,14 +281,15 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -262,14 +281,15 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 262 | if (existing) { | 281 | if (existing) { |
| 263 | const existingProviderThreadId = yield* upsertSessionBinding( | 282 | const existingProviderThreadId = yield* upsertSessionBinding( |
| 264 | existing, | 283 | existing, |
| 265 | `${input.operation}:upsertExistingSession`, | ||
| 266 | input.binding.threadId, | 284 | input.binding.threadId, |
| 267 | ); | 285 | ); |
| 268 | yield* directory.upsert({ | 286 | yield* directory.upsert({ |
| 269 | sessionId: input.staleSessionId, | 287 | sessionId: input.staleSessionId, |
| 270 | provider: existing.provider, | 288 | provider: existing.provider, |
| 271 | threadId: input.binding.threadId, | 289 | threadId: input.binding.threadId, |
| 272 | providerThreadId: existingProviderThreadId, | 290 | ...(existingProviderThreadId !== undefined |
| 291 | ? { providerThreadId: existingProviderThreadId } | ||
| 292 | : {}), | ||
| 273 | ...(existing.resumeCursor !== undefined ? { resumeCursor: existing.resumeCursor } : {}), | 293 | ...(existing.resumeCursor !== undefined ? { resumeCursor: existing.resumeCursor } : {}), |
| 274 | }); | 294 | }); |
| 275 | if (existing.sessionId !== input.staleSessionId) { | 295 | if (existing.sessionId !== input.staleSessionId) { |
| ... | @@ -307,7 +327,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -307,7 +327,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 307 | 327 | ||
| 308 | const resumedProviderThreadId = yield* upsertSessionBinding( | 328 | const resumedProviderThreadId = yield* upsertSessionBinding( |
| 309 | resumed, | 329 | resumed, |
| 310 | `${input.operation}:upsertRecoveredSession`, | ||
| 311 | input.binding.threadId, | 330 | input.binding.threadId, |
| 312 | ); | 331 | ); |
| 313 | 332 | ||
| ... | @@ -315,7 +334,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -315,7 +334,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 315 | sessionId: input.staleSessionId, | 334 | sessionId: input.staleSessionId, |
| 316 | provider: resumed.provider, | 335 | provider: resumed.provider, |
| 317 | threadId: input.binding.threadId, | 336 | threadId: input.binding.threadId, |
| 318 | providerThreadId: resumedProviderThreadId, | 337 | ...(resumedProviderThreadId !== undefined |
| 338 | ? { providerThreadId: resumedProviderThreadId } | ||
| 339 | : {}), | ||
| 319 | ...(resumed.resumeCursor !== undefined ? { resumeCursor: resumed.resumeCursor } : {}), | 340 | ...(resumed.resumeCursor !== undefined ? { resumeCursor: resumed.resumeCursor } : {}), |
| 320 | }); | 341 | }); |
| 321 | 342 | ||
| ... | @@ -428,7 +449,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -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 | return session; | 454 | return session; |
| 434 | }); | 455 | }); |
| ... | @@ -473,7 +494,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -473,7 +494,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 473 | sessionId: input.sessionId, | 494 | sessionId: input.sessionId, |
| 474 | provider: routed.adapter.provider, | 495 | provider: routed.adapter.provider, |
| 475 | threadId, | 496 | threadId, |
| 476 | providerThreadId: turn.threadId, | 497 | ...(turn.threadId !== undefined ? { providerThreadId: turn.threadId } : {}), |
| 477 | status: "running", | 498 | status: "running", |
| 478 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), | 499 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), |
| 479 | runtimePayload: { | 500 | runtimePayload: { |
| ... | @@ -533,9 +554,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -533,9 +554,11 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 533 | if (routed.sessionId !== input.sessionId) { | 554 | if (routed.sessionId !== input.sessionId) { |
| 534 | yield* directory.remove(routed.sessionId); | 555 | yield* directory.remove(routed.sessionId); |
| 535 | yield* clearAliasesReferencing(routed.sessionId); | 556 | yield* clearAliasesReferencing(routed.sessionId); |
| 557 | yield* clearSessionSequence(routed.sessionId); | ||
| 536 | } | 558 | } |
| 537 | yield* directory.remove(input.sessionId); | 559 | yield* directory.remove(input.sessionId); |
| 538 | yield* clearAliasesReferencing(input.sessionId); | 560 | yield* clearAliasesReferencing(input.sessionId); |
| 561 | yield* clearSessionSequence(input.sessionId); | ||
| 539 | }); | 562 | }); |
| 540 | 563 | ||
| 541 | const listSessions: ProviderServiceShape["listSessions"] = () => | 564 | const listSessions: ProviderServiceShape["listSessions"] = () => |
| ... | @@ -584,6 +607,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -584,6 +607,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 584 | // Keep persisted session bindings so stale sessions can be resumed after | 607 | // Keep persisted session bindings so stale sessions can be resumed after |
| 585 | // process restart via providerThreadId. | 608 | // process restart via providerThreadId. |
| 586 | yield* Ref.set(routedSessionAliasesRef, new Map()); | 609 | yield* Ref.set(routedSessionAliasesRef, new Map()); |
| 610 | yield* Ref.set(sessionRuntimeSequenceRef, new Map()); | ||
| 587 | }); | 611 | }); |
| 588 | 612 | ||
| 589 | return { | 613 | return { |
apps/web/src/session-logic.test.ts+24| ... | @@ -17,6 +17,7 @@ function makeActivity(overrides: { | ... | @@ -17,6 +17,7 @@ function makeActivity(overrides: { |
| 17 | tone?: OrchestrationThreadActivity["tone"]; | 17 | tone?: OrchestrationThreadActivity["tone"]; |
| 18 | payload?: Record<string, unknown>; | 18 | payload?: Record<string, unknown>; |
| 19 | turnId?: string; | 19 | turnId?: string; |
| 20 | sequence?: number; | ||
| 20 | }): OrchestrationThreadActivity { | 21 | }): OrchestrationThreadActivity { |
| 21 | const payload = overrides.payload ?? {}; | 22 | const payload = overrides.payload ?? {}; |
| 22 | return { | 23 | return { |
| ... | @@ -27,6 +28,7 @@ function makeActivity(overrides: { | ... | @@ -27,6 +28,7 @@ function makeActivity(overrides: { |
| 27 | tone: overrides.tone ?? "tool", | 28 | tone: overrides.tone ?? "tool", |
| 28 | payload, | 29 | payload, |
| 29 | turnId: overrides.turnId ? TurnId.makeUnsafe(overrides.turnId) : null, | 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,6 +133,28 @@ describe("deriveWorkLogEntries", () => { |
| 131 | const entries = deriveWorkLogEntries(activities, undefined); | 133 | const entries = deriveWorkLogEntries(activities, undefined); |
| 132 | expect(entries.map((entry) => entry.id)).toEqual(["tool-complete"]); | 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 | describe("hasToolActivityForTurn", () => { | 160 | describe("hasToolActivityForTurn", () => { |
apps/web/src/session-logic.ts+19-6| ... | @@ -91,9 +91,7 @@ export function derivePendingApprovals( | ... | @@ -91,9 +91,7 @@ export function derivePendingApprovals( |
| 91 | activities: ReadonlyArray<OrchestrationThreadActivity>, | 91 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 92 | ): PendingApproval[] { | 92 | ): PendingApproval[] { |
| 93 | const openByRequestId = new Map<ApprovalRequestId, PendingApproval>(); | 93 | const openByRequestId = new Map<ApprovalRequestId, PendingApproval>(); |
| 94 | const ordered = [...activities].toSorted((left, right) => | 94 | const ordered = [...activities].toSorted(compareActivitiesByOrder); |
| 95 | left.createdAt.localeCompare(right.createdAt), | ||
| 96 | ); | ||
| 97 | 95 | ||
| 98 | for (const activity of ordered) { | 96 | for (const activity of ordered) { |
| 99 | const payload = | 97 | const payload = |
| ... | @@ -134,9 +132,7 @@ export function deriveWorkLogEntries( | ... | @@ -134,9 +132,7 @@ export function deriveWorkLogEntries( |
| 134 | activities: ReadonlyArray<OrchestrationThreadActivity>, | 132 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 135 | latestTurnId: TurnId | undefined, | 133 | latestTurnId: TurnId | undefined, |
| 136 | ): WorkLogEntry[] { | 134 | ): WorkLogEntry[] { |
| 137 | const ordered = [...activities].toSorted((left, right) => | 135 | const ordered = [...activities].toSorted(compareActivitiesByOrder); |
| 138 | left.createdAt.localeCompare(right.createdAt), | ||
| 139 | ); | ||
| 140 | return ordered | 136 | return ordered |
| 141 | .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true)) | 137 | .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true)) |
| 142 | .filter((activity) => activity.kind !== "tool.started") | 138 | .filter((activity) => activity.kind !== "tool.started") |
| ... | @@ -159,6 +155,23 @@ export function deriveWorkLogEntries( | ... | @@ -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 | export function hasToolActivityForTurn( | 175 | export function hasToolActivityForTurn( |
| 163 | activities: ReadonlyArray<OrchestrationThreadActivity>, | 176 | activities: ReadonlyArray<OrchestrationThreadActivity>, |
| 164 | turnId: TurnId | null | undefined, | 177 | turnId: TurnId | null | undefined, |
packages/contracts/src/orchestration.ts+1| ... | @@ -212,6 +212,7 @@ export const OrchestrationThreadActivity = Schema.Struct({ | ... | @@ -212,6 +212,7 @@ export const OrchestrationThreadActivity = Schema.Struct({ |
| 212 | summary: TrimmedNonEmptyString, | 212 | summary: TrimmedNonEmptyString, |
| 213 | payload: Schema.Unknown, | 213 | payload: Schema.Unknown, |
| 214 | turnId: Schema.NullOr(TurnId), | 214 | turnId: Schema.NullOr(TurnId), |
| 215 | sequence: Schema.optional(NonNegativeInt), | ||
| 215 | createdAt: IsoDateTime, | 216 | createdAt: IsoDateTime, |
| 216 | }); | 217 | }); |
| 217 | export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Type; | 218 | export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Type; |
packages/contracts/src/provider.ts+1-1| ... | @@ -93,7 +93,7 @@ export const ProviderSendTurnInput = Schema.Struct({ | ... | @@ -93,7 +93,7 @@ export const ProviderSendTurnInput = Schema.Struct({ |
| 93 | export type ProviderSendTurnInput = typeof ProviderSendTurnInput.Type; | 93 | export type ProviderSendTurnInput = typeof ProviderSendTurnInput.Type; |
| 94 | 94 | ||
| 95 | export const ProviderTurnStartResult = Schema.Struct({ | 95 | export const ProviderTurnStartResult = Schema.Struct({ |
| 96 | threadId: ProviderThreadId, | 96 | threadId: Schema.optional(ProviderThreadId), |
| 97 | turnId: ProviderTurnId, | 97 | turnId: ProviderTurnId, |
| 98 | resumeCursor: Schema.optional(Schema.Unknown), | 98 | resumeCursor: Schema.optional(Schema.Unknown), |
| 99 | }); | 99 | }); |
packages/contracts/src/providerRuntime.ts+13| ... | @@ -31,6 +31,7 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ | ... | @@ -31,6 +31,7 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ |
| 31 | eventId: EventId, | 31 | eventId: EventId, |
| 32 | provider: ProviderKind, | 32 | provider: ProviderKind, |
| 33 | sessionId: ProviderSessionId, | 33 | sessionId: ProviderSessionId, |
| 34 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 34 | createdAt: IsoDateTime, | 35 | createdAt: IsoDateTime, |
| 35 | threadId: Schema.optional(ProviderThreadId), | 36 | threadId: Schema.optional(ProviderThreadId), |
| 36 | message: Schema.optional(TrimmedNonEmptyStringSchema), | 37 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | @@ -42,6 +43,7 @@ export const ProviderRuntimeSessionExitedEvent = Schema.Struct({ | ... | @@ -42,6 +43,7 @@ export const ProviderRuntimeSessionExitedEvent = Schema.Struct({ |
| 42 | eventId: EventId, | 43 | eventId: EventId, |
| 43 | provider: ProviderKind, | 44 | provider: ProviderKind, |
| 44 | sessionId: ProviderSessionId, | 45 | sessionId: ProviderSessionId, |
| 46 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 45 | createdAt: IsoDateTime, | 47 | createdAt: IsoDateTime, |
| 46 | threadId: Schema.optional(ProviderThreadId), | 48 | threadId: Schema.optional(ProviderThreadId), |
| 47 | message: Schema.optional(TrimmedNonEmptyStringSchema), | 49 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| ... | @@ -53,6 +55,7 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({ | ... | @@ -53,6 +55,7 @@ export const ProviderRuntimeThreadStartedEvent = Schema.Struct({ |
| 53 | eventId: EventId, | 55 | eventId: EventId, |
| 54 | provider: ProviderKind, | 56 | provider: ProviderKind, |
| 55 | sessionId: ProviderSessionId, | 57 | sessionId: ProviderSessionId, |
| 58 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 56 | createdAt: IsoDateTime, | 59 | createdAt: IsoDateTime, |
| 57 | threadId: ProviderThreadId, | 60 | threadId: ProviderThreadId, |
| 58 | }); | 61 | }); |
| ... | @@ -63,6 +66,7 @@ export const ProviderRuntimeTurnStartedEvent = Schema.Struct({ | ... | @@ -63,6 +66,7 @@ export const ProviderRuntimeTurnStartedEvent = Schema.Struct({ |
| 63 | eventId: EventId, | 66 | eventId: EventId, |
| 64 | provider: ProviderKind, | 67 | provider: ProviderKind, |
| 65 | sessionId: ProviderSessionId, | 68 | sessionId: ProviderSessionId, |
| 69 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 66 | createdAt: IsoDateTime, | 70 | createdAt: IsoDateTime, |
| 67 | threadId: Schema.optional(ProviderThreadId), | 71 | threadId: Schema.optional(ProviderThreadId), |
| 68 | turnId: ProviderTurnId, | 72 | turnId: ProviderTurnId, |
| ... | @@ -74,6 +78,7 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ | ... | @@ -74,6 +78,7 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ |
| 74 | eventId: EventId, | 78 | eventId: EventId, |
| 75 | provider: ProviderKind, | 79 | provider: ProviderKind, |
| 76 | sessionId: ProviderSessionId, | 80 | sessionId: ProviderSessionId, |
| 81 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 77 | createdAt: IsoDateTime, | 82 | createdAt: IsoDateTime, |
| 78 | threadId: Schema.optional(ProviderThreadId), | 83 | threadId: Schema.optional(ProviderThreadId), |
| 79 | turnId: Schema.optional(ProviderTurnId), | 84 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -87,6 +92,7 @@ export const ProviderRuntimeMessageDeltaEvent = Schema.Struct({ | ... | @@ -87,6 +92,7 @@ export const ProviderRuntimeMessageDeltaEvent = Schema.Struct({ |
| 87 | eventId: EventId, | 92 | eventId: EventId, |
| 88 | provider: ProviderKind, | 93 | provider: ProviderKind, |
| 89 | sessionId: ProviderSessionId, | 94 | sessionId: ProviderSessionId, |
| 95 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 90 | createdAt: IsoDateTime, | 96 | createdAt: IsoDateTime, |
| 91 | threadId: Schema.optional(ProviderThreadId), | 97 | threadId: Schema.optional(ProviderThreadId), |
| 92 | turnId: Schema.optional(ProviderTurnId), | 98 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -100,6 +106,7 @@ export const ProviderRuntimeMessageCompletedEvent = Schema.Struct({ | ... | @@ -100,6 +106,7 @@ export const ProviderRuntimeMessageCompletedEvent = Schema.Struct({ |
| 100 | eventId: EventId, | 106 | eventId: EventId, |
| 101 | provider: ProviderKind, | 107 | provider: ProviderKind, |
| 102 | sessionId: ProviderSessionId, | 108 | sessionId: ProviderSessionId, |
| 109 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 103 | createdAt: IsoDateTime, | 110 | createdAt: IsoDateTime, |
| 104 | itemId: ProviderItemId, | 111 | itemId: ProviderItemId, |
| 105 | threadId: Schema.optional(ProviderThreadId), | 112 | threadId: Schema.optional(ProviderThreadId), |
| ... | @@ -112,6 +119,7 @@ export const ProviderRuntimeToolStartedEvent = Schema.Struct({ | ... | @@ -112,6 +119,7 @@ export const ProviderRuntimeToolStartedEvent = Schema.Struct({ |
| 112 | eventId: EventId, | 119 | eventId: EventId, |
| 113 | provider: ProviderKind, | 120 | provider: ProviderKind, |
| 114 | sessionId: ProviderSessionId, | 121 | sessionId: ProviderSessionId, |
| 122 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 115 | createdAt: IsoDateTime, | 123 | createdAt: IsoDateTime, |
| 116 | threadId: Schema.optional(ProviderThreadId), | 124 | threadId: Schema.optional(ProviderThreadId), |
| 117 | turnId: Schema.optional(ProviderTurnId), | 125 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -127,6 +135,7 @@ export const ProviderRuntimeToolCompletedEvent = Schema.Struct({ | ... | @@ -127,6 +135,7 @@ export const ProviderRuntimeToolCompletedEvent = Schema.Struct({ |
| 127 | eventId: EventId, | 135 | eventId: EventId, |
| 128 | provider: ProviderKind, | 136 | provider: ProviderKind, |
| 129 | sessionId: ProviderSessionId, | 137 | sessionId: ProviderSessionId, |
| 138 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 130 | createdAt: IsoDateTime, | 139 | createdAt: IsoDateTime, |
| 131 | threadId: Schema.optional(ProviderThreadId), | 140 | threadId: Schema.optional(ProviderThreadId), |
| 132 | turnId: Schema.optional(ProviderTurnId), | 141 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -142,6 +151,7 @@ export const ProviderRuntimeApprovalRequestedEvent = Schema.Struct({ | ... | @@ -142,6 +151,7 @@ export const ProviderRuntimeApprovalRequestedEvent = Schema.Struct({ |
| 142 | eventId: EventId, | 151 | eventId: EventId, |
| 143 | provider: ProviderKind, | 152 | provider: ProviderKind, |
| 144 | sessionId: ProviderSessionId, | 153 | sessionId: ProviderSessionId, |
| 154 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 145 | createdAt: IsoDateTime, | 155 | createdAt: IsoDateTime, |
| 146 | threadId: Schema.optional(ProviderThreadId), | 156 | threadId: Schema.optional(ProviderThreadId), |
| 147 | turnId: Schema.optional(ProviderTurnId), | 157 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -158,6 +168,7 @@ export const ProviderRuntimeApprovalResolvedEvent = Schema.Struct({ | ... | @@ -158,6 +168,7 @@ export const ProviderRuntimeApprovalResolvedEvent = Schema.Struct({ |
| 158 | eventId: EventId, | 168 | eventId: EventId, |
| 159 | provider: ProviderKind, | 169 | provider: ProviderKind, |
| 160 | sessionId: ProviderSessionId, | 170 | sessionId: ProviderSessionId, |
| 171 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 161 | createdAt: IsoDateTime, | 172 | createdAt: IsoDateTime, |
| 162 | threadId: Schema.optional(ProviderThreadId), | 173 | threadId: Schema.optional(ProviderThreadId), |
| 163 | turnId: Schema.optional(ProviderTurnId), | 174 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -173,6 +184,7 @@ export const ProviderRuntimeCheckpointCapturedEvent = Schema.Struct({ | ... | @@ -173,6 +184,7 @@ export const ProviderRuntimeCheckpointCapturedEvent = Schema.Struct({ |
| 173 | eventId: EventId, | 184 | eventId: EventId, |
| 174 | provider: ProviderKind, | 185 | provider: ProviderKind, |
| 175 | sessionId: ProviderSessionId, | 186 | sessionId: ProviderSessionId, |
| 187 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 176 | createdAt: IsoDateTime, | 188 | createdAt: IsoDateTime, |
| 177 | threadId: ProviderThreadId, | 189 | threadId: ProviderThreadId, |
| 178 | turnId: Schema.optional(ProviderTurnId), | 190 | turnId: Schema.optional(ProviderTurnId), |
| ... | @@ -187,6 +199,7 @@ export const ProviderRuntimeErrorEvent = Schema.Struct({ | ... | @@ -187,6 +199,7 @@ export const ProviderRuntimeErrorEvent = Schema.Struct({ |
| 187 | eventId: EventId, | 199 | eventId: EventId, |
| 188 | provider: ProviderKind, | 200 | provider: ProviderKind, |
| 189 | sessionId: ProviderSessionId, | 201 | sessionId: ProviderSessionId, |
| 202 | sessionSequence: Schema.optional(NonNegativeInt), | ||
| 190 | createdAt: IsoDateTime, | 203 | createdAt: IsoDateTime, |
| 191 | threadId: Schema.optional(ProviderThreadId), | 204 | threadId: Schema.optional(ProviderThreadId), |
| 192 | turnId: Schema.optional(ProviderTurnId), | 205 | turnId: Schema.optional(ProviderTurnId), |