@effect-agent/storage-cloudflare 0.1.0-beta.6 → 0.1.0-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-UrxrvbnF.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +162 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +34 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-Pt3jEHdW.d.mts +10 -0
  24. package/dist/DoStorageVersion.d.mts +2 -0
  25. package/dist/DoStorageVersion.mjs +8 -0
  26. package/dist/DoStorageVersion.mjs.map +1 -0
  27. package/dist/DoSubmissionLedger.d.mts +23 -0
  28. package/dist/DoSubmissionLedger.mjs +1826 -0
  29. package/dist/DoSubmissionLedger.mjs.map +1 -0
  30. package/dist/DoSubscriptionStore.d.mts +25 -0
  31. package/dist/DoSubscriptionStore.mjs +183 -0
  32. package/dist/DoSubscriptionStore.mjs.map +1 -0
  33. package/dist/DoThreadStore.d.mts +59 -0
  34. package/dist/DoThreadStore.mjs +432 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +19796 -0
  37. package/dist/MemoryProtocol.mjs +182 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +2951 -0
  40. package/dist/PortProtocol.mjs +176 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +82 -0
  43. package/dist/PortRouting.mjs +405 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-Brv7xSH5.mjs +869 -0
  46. package/dist/do-journal-Brv7xSH5.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1505
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-soC86CQA.mjs +267 -0
  50. package/dist/migrations-soC86CQA.mjs.map +1 -0
  51. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  52. package/package.json +1 -45
  53. package/src/DoMemoryStore.ts +59 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +8 -3
  57. package/src/DoStorageFailpoint.ts +20 -0
  58. package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
  59. package/src/DoStorageVersion.ts +2 -0
  60. package/src/{do-ledger.ts → DoSubmissionLedger.ts} +757 -123
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +924 -0
  63. package/src/MemoryProtocol.ts +291 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +41 -34
  65. package/src/{routing.ts → PortRouting.ts} +266 -259
  66. package/src/index.ts +13 -32
  67. package/src/{do-journal.ts → internal/do-journal.ts} +495 -214
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +40 -35
  70. package/dist/index.mjs.map +0 -1
  71. package/src/do-conversation-store.ts +0 -850
  72. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -1,20 +1,22 @@
1
- import { CanonicalSequence, ProducerEpoch } from "@effect-agent/session";
1
+ import { CanonicalSequence, ProducerEpoch } from "@effect-agent/thread/Records";
2
+ import { checkV2ThreadLayout } from "@effect-agent/thread/SqlStorageV2Upgrade";
2
3
  import { SqliteMigrator } from "@effect/sql-sqlite-do";
3
- import { Effect, Schema } from "effect";
4
+ import { Effect, Schema, Stream } from "effect";
4
5
  import * as SqlClient from "effect/unstable/sql/SqlClient";
5
6
  import { SqlError } from "effect/unstable/sql/SqlError";
6
7
 
7
8
  import {
9
+ type DoStorageFailpointError,
8
10
  DoAppendConflict,
9
11
  DoCheckpointConflict,
10
12
  DoFenceRejected,
11
13
  DoStorageCompatibilityError,
12
14
  DoStorageCorruptionError,
13
15
  DoStorageError,
14
- DoStorageFailpointError,
15
16
  DoValueBoundExceeded,
16
17
  type DoStorageFailpointLocation,
17
- } from "./errors.ts";
18
+ } from "../DoStorageError.ts";
19
+ import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
18
20
  import { CurrentDoStorageVersion, doMigrations } from "./migrations.ts";
19
21
 
20
22
  /**
@@ -25,18 +27,22 @@ import { CurrentDoStorageVersion, doMigrations } from "./migrations.ts";
25
27
  */
26
28
  const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2_000_000));
27
29
  const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
28
- const MAX_RECORDS_PER_CONVERSATION = 65_536;
30
+ const MAX_RECORDS_PER_THREAD = 65_536;
29
31
  const MAX_IDENTIFIER_LENGTH = 1_024;
32
+ const MAX_READ_PAGE_JSON_BYTES = 4 * 1024 * 1024;
30
33
  /** Durable Object SQL storage allows at most 100 bound parameters per statement. */
31
34
  const MAX_BOUND_PARAMETERS = 100;
35
+ const isSqlError = Schema.is(SqlError);
32
36
 
33
37
  const storedTextBytes = (value: string): number => new TextEncoder().encode(value).byteLength;
34
38
 
35
39
  const chunked = <A>(values: ReadonlyArray<A>, size: number): Array<ReadonlyArray<A>> => {
36
40
  const chunks: Array<ReadonlyArray<A>> = [];
41
+
37
42
  for (let index = 0; index < values.length; index += size) {
38
43
  chunks.push(values.slice(index, index + size));
39
44
  }
45
+
40
46
  return chunks;
41
47
  };
42
48
 
@@ -48,8 +54,8 @@ class DoNameRow extends Schema.Class<DoNameRow>("DoNameRow")({
48
54
  name: BoundedIdentifier,
49
55
  }) {}
50
56
 
51
- class ConversationRow extends Schema.Class<ConversationRow>("ConversationRow")({
52
- conversation_id: BoundedIdentifier,
57
+ class ThreadRow extends Schema.Class<ThreadRow>("ThreadRow")({
58
+ thread_id: BoundedIdentifier,
53
59
  created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
54
60
  producer_epoch: ProducerEpoch,
55
61
  tail_digest: BoundedStoredText,
@@ -60,7 +66,7 @@ class BatchRow extends Schema.Class<BatchRow>("BatchRow")({
60
66
  batch_digest: BoundedStoredText,
61
67
  batch_id: BoundedIdentifier,
62
68
  batch_json: BoundedStoredText,
63
- conversation_id: BoundedIdentifier,
69
+ thread_id: BoundedIdentifier,
64
70
  first_sequence: CanonicalSequence,
65
71
  last_sequence: CanonicalSequence,
66
72
  tail_digest: BoundedStoredText,
@@ -68,15 +74,22 @@ class BatchRow extends Schema.Class<BatchRow>("BatchRow")({
68
74
 
69
75
  class RecordRow extends Schema.Class<RecordRow>("RecordRow")({
70
76
  batch_id: BoundedIdentifier,
71
- conversation_id: BoundedIdentifier,
77
+ thread_id: BoundedIdentifier,
72
78
  record_id: BoundedIdentifier,
73
79
  record_json: BoundedStoredText,
74
80
  sequence: CanonicalSequence,
75
81
  }) {}
76
82
 
83
+ const ReadPlanRow = Schema.Struct({
84
+ sequence: CanonicalSequence,
85
+ record_json_bytes: Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_READ_PAGE_JSON_BYTES)),
86
+ });
87
+
88
+ type ReadPage = [typeof ReadPlanRow.Type, ...Array<typeof ReadPlanRow.Type>];
89
+
77
90
  class CheckpointRow extends Schema.Class<CheckpointRow>("CheckpointRow")({
78
91
  checkpoint_json: BoundedStoredText,
79
- conversation_id: BoundedIdentifier,
92
+ thread_id: BoundedIdentifier,
80
93
  tail_digest: BoundedStoredText,
81
94
  through_sequence: CanonicalSequence,
82
95
  }) {}
@@ -94,7 +107,7 @@ export class RawAppendRequest extends Schema.Class<RawAppendRequest>(
94
107
  batchDigest: BoundedStoredText,
95
108
  batchId: BoundedIdentifier,
96
109
  batchJson: BoundedStoredText,
97
- conversationId: BoundedIdentifier,
110
+ threadId: BoundedIdentifier,
98
111
  expectedTailDigest: BoundedStoredText,
99
112
  expectedTailSequence: CanonicalSequence,
100
113
  producerEpoch: ProducerEpoch,
@@ -114,7 +127,7 @@ export class RawAppendResult extends Schema.Class<RawAppendResult>(
114
127
  export class RawReadRequest extends Schema.Class<RawReadRequest>(
115
128
  "@effect-agent/storage-cloudflare/RawReadRequest",
116
129
  )({
117
- conversationId: BoundedIdentifier,
130
+ threadId: BoundedIdentifier,
118
131
  fromSequenceExclusive: CanonicalSequence,
119
132
  limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1_024)),
120
133
  }) {}
@@ -123,17 +136,15 @@ export class RawCheckpoint extends Schema.Class<RawCheckpoint>(
123
136
  "@effect-agent/storage-cloudflare/RawCheckpoint",
124
137
  )({
125
138
  checkpointJson: BoundedStoredText,
126
- conversationId: BoundedIdentifier,
139
+ threadId: BoundedIdentifier,
127
140
  tailDigest: BoundedStoredText,
128
141
  throughSequence: CanonicalSequence,
129
142
  }) {}
130
143
 
131
- export class RawConversationExport extends Schema.Class<RawConversationExport>(
132
- "@effect-agent/storage-cloudflare/RawConversationExport",
144
+ export class RawThreadExport extends Schema.Class<RawThreadExport>(
145
+ "@effect-agent/storage-cloudflare/RawThreadExport",
133
146
  )({
134
- batches: Schema.Array(BatchRow),
135
- checkpoints: Schema.Array(CheckpointRow),
136
- conversation: ConversationRow,
147
+ thread: ThreadRow,
137
148
  records: Schema.Array(RecordRow),
138
149
  }) {}
139
150
 
@@ -167,7 +178,7 @@ const storageError =
167
178
  });
168
179
 
169
180
  /** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
170
- export const decodeRows = Effect.fn("DoJournal.decodeRows")(
181
+ export const decodeRows = Effect.fn(
171
182
  <A, I>(
172
183
  schema: Schema.Codec<ReadonlyArray<A>, ReadonlyArray<I>>,
173
184
  table: string,
@@ -186,7 +197,7 @@ export const decodeRows = Effect.fn("DoJournal.decodeRows")(
186
197
  );
187
198
 
188
199
  /** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
189
- export const decodeSingleRow = Effect.fn("DoJournal.decodeSingleRow")(
200
+ export const decodeSingleRow = Effect.fn(
190
201
  <A, I>(
191
202
  schema: Schema.Codec<ReadonlyArray<A>, ReadonlyArray<I>>,
192
203
  table: string,
@@ -208,6 +219,147 @@ export const decodeSingleRow = Effect.fn("DoJournal.decodeSingleRow")(
208
219
  ),
209
220
  );
210
221
 
222
+ /** Column inventory of the supported v3 predecessor, independent of physical column order. */
223
+ const predecessorColumns = {
224
+ effect_agent_threads: [
225
+ "thread_id",
226
+ "created_at",
227
+ "tail_sequence",
228
+ "tail_digest",
229
+ "producer_epoch",
230
+ ],
231
+ effect_agent_canonical_batches: [
232
+ "thread_id",
233
+ "batch_id",
234
+ "first_sequence",
235
+ "last_sequence",
236
+ "batch_digest",
237
+ "tail_digest",
238
+ "batch_json",
239
+ ],
240
+ effect_agent_canonical_records: ["thread_id", "sequence", "record_id", "batch_id", "record_json"],
241
+ effect_agent_checkpoints: ["thread_id", "through_sequence", "tail_digest", "checkpoint_json"],
242
+ effect_agent_submissions: [
243
+ "submission_id",
244
+ "thread_id",
245
+ "queue_sequence",
246
+ "principal",
247
+ "idempotency_key",
248
+ "agent_id",
249
+ "agent_digests_json",
250
+ "deployment_id",
251
+ "input_json",
252
+ "input_digest",
253
+ "receipt_id",
254
+ "state",
255
+ "settled_outcome",
256
+ "created_at",
257
+ "ready_at",
258
+ "input_applied_record_id",
259
+ "input_applied_sequence",
260
+ "joined_host_submission_id",
261
+ "suspended_reason_json",
262
+ "suspended_at",
263
+ "unknown_reason",
264
+ "unknown_tool_call_ids_json",
265
+ "parent_submission_id",
266
+ "parent_tool_call_id",
267
+ "admission_group",
268
+ "admission_fence_json",
269
+ ],
270
+ effect_agent_submission_ownership: [
271
+ "submission_id",
272
+ "attempt_id",
273
+ "ownership_token",
274
+ "producer_epoch",
275
+ "owner_producer_id",
276
+ "lease_expires_at",
277
+ ],
278
+ effect_agent_attempts: [
279
+ "attempt_id",
280
+ "submission_id",
281
+ "thread_id",
282
+ "owner_producer_id",
283
+ "producer_epoch",
284
+ "claimed_at",
285
+ ],
286
+ effect_agent_settlement_reservations: [
287
+ "submission_id",
288
+ "settlement_id",
289
+ "outcome",
290
+ "record_id",
291
+ "record_json",
292
+ "record_digest",
293
+ "reserved_at",
294
+ "finalized_at",
295
+ ],
296
+ effect_agent_abort_intents: [
297
+ "submission_id",
298
+ "author",
299
+ "reason",
300
+ "requested_at",
301
+ "canonical_record_id",
302
+ ],
303
+ effect_agent_approval_decisions: [
304
+ "submission_id",
305
+ "tool_call_id",
306
+ "decision",
307
+ "resolver",
308
+ "reason",
309
+ "decided_at",
310
+ ],
311
+ effect_agent_unknown_resolutions: [
312
+ "submission_id",
313
+ "tool_call_id",
314
+ "author",
315
+ "reason",
316
+ "resolution_json",
317
+ "resolved_at",
318
+ ],
319
+ effect_agent_child_reservations: [
320
+ "reservation_id",
321
+ "parent_submission_id",
322
+ "parent_tool_call_id",
323
+ "child_submission_id",
324
+ "status",
325
+ "allocation_json",
326
+ "allocation_digest",
327
+ "accounting_json",
328
+ "reserved_at",
329
+ "release_began_at",
330
+ "released_at",
331
+ ],
332
+ effect_agent_meta: ["key", "value"],
333
+ effect_agent_child_settlements: [
334
+ "parent_submission_id",
335
+ "child_submission_id",
336
+ "child_outcome",
337
+ "recorded_at",
338
+ ],
339
+ } as const;
340
+
341
+ const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* () {
342
+ const sql = yield* SqlClient.SqlClient;
343
+
344
+ for (const [table, expected] of Object.entries(predecessorColumns)) {
345
+ const columns = yield* decodeRows(
346
+ Schema.Array(Schema.Struct({ name: BoundedIdentifier })),
347
+ table,
348
+ "schema",
349
+ yield* sql.unsafe(`PRAGMA table_info(${table})`),
350
+ );
351
+
352
+ const names = new Set<string>(expected);
353
+
354
+ if (columns.length !== names.size || columns.some((column) => !names.has(column.name)))
355
+ return yield* DoStorageCompatibilityError.make({
356
+ actualVersion: 3,
357
+ supportedVersion: CurrentDoStorageVersion,
358
+ message: `The v3 ${table} columns do not match the supported predecessor; no upgrade was committed.`,
359
+ });
360
+ }
361
+ });
362
+
211
363
  const REQUIRED_TABLES = [
212
364
  "effect_agent_abort_intents",
213
365
  "effect_agent_approval_decisions",
@@ -217,7 +369,7 @@ const REQUIRED_TABLES = [
217
369
  "effect_agent_checkpoints",
218
370
  "effect_agent_child_reservations",
219
371
  "effect_agent_child_settlements",
220
- "effect_agent_conversations",
372
+ "effect_agent_threads",
221
373
  "effect_agent_meta",
222
374
  "effect_agent_settlement_reservations",
223
375
  "effect_agent_submission_ownership",
@@ -226,7 +378,7 @@ const REQUIRED_TABLES = [
226
378
  ] as const;
227
379
 
228
380
  /**
229
- * Exact-or-fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
381
+ * Supported-predecessor or fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
230
382
  * `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
231
383
  * regardless). No WAL check (Durable Object storage owns durability and confirms writes
232
384
  * through output gates) and no busy timeout (a Durable Object has exactly one writer): the
@@ -243,6 +395,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
243
395
  WHERE type = 'table'
244
396
  AND name = 'effect_agent_meta'
245
397
  `.pipe(Effect.mapError(storageError("read storage version table")));
398
+
246
399
  const metaTables = yield* decodeRows(
247
400
  Schema.Array(DoNameRow),
248
401
  "sqlite_master",
@@ -258,6 +411,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
258
411
  AND name LIKE 'effect_agent_%'
259
412
  ORDER BY name
260
413
  `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
414
+
261
415
  const existing = yield* decodeRows(
262
416
  Schema.Array(DoNameRow),
263
417
  "sqlite_master",
@@ -270,7 +424,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
270
424
  actualVersion: 0,
271
425
  supportedVersion: CurrentDoStorageVersion,
272
426
  message:
273
- "The Durable Object contains unversioned Effect Agent tables. Reset the development namespace explicitly; refusing to mutate ambiguous stored data.",
427
+ "The Durable Object contains unversioned Effect Agent tables. Refusing to mutate ambiguous stored data; retain it for inspection with its original writer.",
274
428
  });
275
429
  }
276
430
 
@@ -292,6 +446,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
292
446
  FROM effect_agent_meta
293
447
  WHERE key = 'storage_version'
294
448
  `.pipe(Effect.mapError(storageError("read storage version")));
449
+
295
450
  const version = yield* decodeSingleRow(
296
451
  Schema.Array(DoMetaRow),
297
452
  "effect_agent_meta",
@@ -299,18 +454,91 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
299
454
  versionRows,
300
455
  );
301
456
 
302
- // The storage version must match EXACTLY. Older private-development versions fail
303
- // closed with reset guidance rather than being migrated, and newer versions fail closed
304
- // rather than being decoded incorrectly (DEPLOY-008).
305
- if (version.value !== String(CurrentDoStorageVersion)) {
457
+ if (version.value === "2" || version.value === "3") {
458
+ yield* sql
459
+ .withTransaction(
460
+ Effect.gen(function* () {
461
+ const current = yield* sql<{
462
+ value: string;
463
+ }>`SELECT value FROM effect_agent_meta WHERE key='storage_version'`;
464
+
465
+ if (current.length === 1 && current[0].value === String(CurrentDoStorageVersion))
466
+ return;
467
+ if (current.length !== 1 || (current[0].value !== "2" && current[0].value !== "3"))
468
+ return yield* DoStorageCompatibilityError.make({
469
+ actualVersion: -1,
470
+ supportedVersion: CurrentDoStorageVersion,
471
+ message: "Storage version changed while acquiring the upgrade transaction.",
472
+ });
473
+
474
+ const required = yield* decodeRows(
475
+ Schema.Array(DoNameRow),
476
+ "sqlite_master",
477
+ "required_tables",
478
+ yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name IN ${sql.in([...REQUIRED_TABLES])}`,
479
+ );
480
+
481
+ if (required.length !== REQUIRED_TABLES.length)
482
+ return yield* DoStorageCompatibilityError.make({
483
+ actualVersion: Number(current[0].value),
484
+ supportedVersion: CurrentDoStorageVersion,
485
+ message:
486
+ "The predecessor store is missing required tables. Retain the original store for inspection; no upgrade was committed.",
487
+ });
488
+ if (current[0].value === "2") {
489
+ yield* checkV2ThreadLayout();
490
+ for (const statement of [
491
+ sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_group TEXT`,
492
+ sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_fence_json TEXT`,
493
+ sql`CREATE INDEX effect_agent_submissions_group ON effect_agent_submissions (thread_id, admission_group, state)`,
494
+ ]) {
495
+ yield* failpoint("upgrade:before-mutation");
496
+ yield* statement;
497
+ yield* failpoint("upgrade:after-mutation");
498
+ }
499
+ }
500
+ if (current[0].value === "3") yield* checkPredecessorLayout();
501
+ yield* failpoint("upgrade:before-mutation");
502
+ yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
503
+ yield* failpoint("upgrade:after-mutation");
504
+ yield* failpoint("upgrade:before-mutation");
505
+ yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN message_admission_json TEXT`;
506
+ yield* failpoint("upgrade:after-mutation");
507
+ yield* failpoint("upgrade:before-mutation");
508
+ yield* createMessageDeliveryTables;
509
+ yield* failpoint("upgrade:after-mutation");
510
+ yield* failpoint("upgrade:before-version");
511
+ yield* sql`UPDATE effect_agent_meta SET value='4' WHERE key='storage_version'`;
512
+ yield* failpoint("upgrade:after-version");
513
+ }),
514
+ )
515
+ .pipe(
516
+ Effect.catchTag("DoStorageFailpointError", (error) =>
517
+ DoStorageError.make({
518
+ cause: error,
519
+ operation: "upgrade storage",
520
+ message: error.message,
521
+ }),
522
+ ),
523
+ Effect.catchTag("StorageUpgradeError", (error) =>
524
+ DoStorageCorruptionError.make({
525
+ table: error.table,
526
+ rowKey: error.rowKey,
527
+ message: error.message,
528
+ }),
529
+ ),
530
+ Effect.catchTag("SqlError", storageError("upgrade supported thread storage")),
531
+ );
532
+ } else if (version.value !== String(CurrentDoStorageVersion)) {
306
533
  const actualVersion = Number.parseInt(version.value, 10);
534
+
307
535
  return yield* DoStorageCompatibilityError.make({
308
536
  actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
309
537
  supportedVersion: CurrentDoStorageVersion,
310
538
  message:
311
- `The Durable Object uses private-development storage version ${version.value}; ` +
539
+ `The Durable Object uses unsupported storage version ${version.value}; ` +
312
540
  `this build supports exactly version ${CurrentDoStorageVersion}. ` +
313
- "Replace the development namespace explicitly; automatic stored-data migrations are not provided during private development.",
541
+ "Only supported v2 and v3 can be upgraded automatically. Keep the original store and use a compatible library version.",
314
542
  });
315
543
  }
316
544
  }
@@ -319,21 +547,23 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
319
547
  SELECT name
320
548
  FROM sqlite_master
321
549
  WHERE type = 'table'
322
- AND name IN ${sql.in([...REQUIRED_TABLES])}
550
+ AND name IN ${sql.in([...REQUIRED_TABLES, "effect_agent_message_deliveries"])}
323
551
  ORDER BY name
324
552
  `.pipe(Effect.mapError(storageError("verify storage tables")));
553
+
325
554
  const required = yield* decodeRows(
326
555
  Schema.Array(DoNameRow),
327
556
  "sqlite_master",
328
557
  "required_tables",
329
558
  requiredRows,
330
559
  );
331
- if (required.length !== REQUIRED_TABLES.length) {
560
+
561
+ if (required.length !== REQUIRED_TABLES.length + 1) {
332
562
  return yield* DoStorageCompatibilityError.make({
333
563
  actualVersion: CurrentDoStorageVersion,
334
564
  supportedVersion: CurrentDoStorageVersion,
335
565
  message:
336
- "The Durable Object claims the current format but is missing required tables. Reset the corrupt private-development data.",
566
+ "The Durable Object claims the current format but is missing required tables. Retain the original store for inspection.",
337
567
  });
338
568
  }
339
569
 
@@ -351,6 +581,7 @@ const makeJournal = (
351
581
  value: string,
352
582
  ): Effect.Effect<void, DoValueBoundExceeded> => {
353
583
  const actualBytes = storedTextBytes(value);
584
+
354
585
  return actualBytes > maxStoredValueBytes
355
586
  ? Effect.fail(
356
587
  DoValueBoundExceeded.make({
@@ -378,14 +609,12 @@ const makeJournal = (
378
609
  (operation: string) =>
379
610
  <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E | DoStorageError> =>
380
611
  sql.withTransaction(effect).pipe(
381
- Effect.mapError((error) =>
382
- error instanceof SqlError ? storageError(operation)(error) : error,
383
- ),
612
+ Effect.mapError((error) => (isSqlError(error) ? storageError(operation)(error) : error)),
384
613
  Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }),
385
614
  );
386
615
 
387
616
  const materialize = Effect.fn("DoJournal.materialize")(function* (
388
- conversationId: string,
617
+ threadId: string,
389
618
  createdAt: string,
390
619
  emptyTailDigest: string,
391
620
  producerEpoch: ProducerEpoch,
@@ -393,54 +622,57 @@ const makeJournal = (
393
622
  void,
394
623
  DoFenceRejected | DoStorageCorruptionError | DoStorageError | DoValueBoundExceeded
395
624
  > {
396
- if (conversationId.length > MAX_IDENTIFIER_LENGTH) {
625
+ if (threadId.length > MAX_IDENTIFIER_LENGTH) {
397
626
  return yield* DoStorageError.make({
398
- operation: "materialize conversation",
399
- message: "Conversation identity exceeds the Durable Object storage bounds.",
627
+ operation: "materialize thread",
628
+ message: "Thread identity exceeds the Durable Object storage bounds.",
400
629
  });
401
630
  }
402
- yield* checkValueBound("materialize conversation", emptyTailDigest);
631
+ yield* checkValueBound("materialize thread", emptyTailDigest);
403
632
  yield* withWriteTransaction("materialize transaction")(
404
633
  Effect.gen(function* () {
405
634
  const existingRows = yield* sql<Record<string, unknown>>`
406
635
  SELECT
407
- conversation_id,
636
+ thread_id,
408
637
  created_at,
409
638
  tail_sequence,
410
639
  tail_digest,
411
640
  producer_epoch
412
- FROM effect_agent_conversations
413
- WHERE conversation_id = ${conversationId}
414
- `.pipe(Effect.mapError(storageError("read materialized conversation")));
641
+ FROM effect_agent_threads
642
+ WHERE thread_id = ${threadId}
643
+ `.pipe(Effect.mapError(storageError("read materialized thread")));
644
+
415
645
  const existing = yield* decodeRows(
416
- Schema.Array(ConversationRow),
417
- "effect_agent_conversations",
418
- conversationId,
646
+ Schema.Array(ThreadRow),
647
+ "effect_agent_threads",
648
+ threadId,
419
649
  existingRows,
420
650
  );
651
+
421
652
  if (existing.length > 1) {
422
653
  return yield* DoStorageCorruptionError.make({
423
- table: "effect_agent_conversations",
424
- rowKey: conversationId,
425
- message: "A conversation primary key returned more than one row.",
654
+ table: "effect_agent_threads",
655
+ rowKey: threadId,
656
+ message: "A thread primary key returned more than one row.",
426
657
  });
427
658
  }
428
659
  if (existing.length === 0) {
429
660
  yield* sql`
430
- INSERT INTO effect_agent_conversations (
431
- conversation_id,
661
+ INSERT INTO effect_agent_threads (
662
+ thread_id,
432
663
  created_at,
433
664
  tail_sequence,
434
665
  tail_digest,
435
666
  producer_epoch
436
667
  ) VALUES (
437
- ${conversationId},
668
+ ${threadId},
438
669
  ${createdAt},
439
670
  0,
440
671
  ${emptyTailDigest},
441
672
  ${producerEpoch}
442
673
  )
443
- `.pipe(Effect.mapError(storageError("materialize conversation")));
674
+ `.pipe(Effect.mapError(storageError("materialize thread")));
675
+
444
676
  return;
445
677
  }
446
678
  if (producerEpoch < existing[0].producer_epoch) {
@@ -452,41 +684,35 @@ const makeJournal = (
452
684
  }
453
685
  if (producerEpoch > existing[0].producer_epoch) {
454
686
  yield* sql`
455
- UPDATE effect_agent_conversations
687
+ UPDATE effect_agent_threads
456
688
  SET producer_epoch = ${producerEpoch}
457
- WHERE conversation_id = ${conversationId}
689
+ WHERE thread_id = ${threadId}
458
690
  `.pipe(Effect.mapError(storageError("advance materialization epoch")));
459
691
  }
460
692
  }),
461
693
  );
462
694
  });
463
695
 
464
- const getConversation = Effect.fn("DoJournal.getConversation")(function* (
465
- conversationId: string,
466
- ) {
696
+ const getThread = Effect.fn("DoJournal.getThread")(function* (threadId: string) {
467
697
  const rows = yield* sql<Record<string, unknown>>`
468
698
  SELECT
469
- conversation_id,
699
+ thread_id,
470
700
  created_at,
471
701
  tail_sequence,
472
702
  tail_digest,
473
703
  producer_epoch
474
- FROM effect_agent_conversations
475
- WHERE conversation_id = ${conversationId}
476
- `.pipe(Effect.mapError(storageError("read conversation")));
477
- return yield* decodeRows(
478
- Schema.Array(ConversationRow),
479
- "effect_agent_conversations",
480
- conversationId,
481
- rows,
482
- );
704
+ FROM effect_agent_threads
705
+ WHERE thread_id = ${threadId}
706
+ `.pipe(Effect.mapError(storageError("read thread")));
707
+
708
+ return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
483
709
  });
484
710
 
485
711
  const append = Effect.fn("DoJournal.append")(function* (
486
712
  request: RawAppendRequest,
487
713
  ): Effect.fn.Return<RawAppendResult, AppendError> {
488
714
  if (
489
- request.conversationId.length > MAX_IDENTIFIER_LENGTH ||
715
+ request.threadId.length > MAX_IDENTIFIER_LENGTH ||
490
716
  request.batchId.length > MAX_IDENTIFIER_LENGTH ||
491
717
  request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)
492
718
  ) {
@@ -504,9 +730,11 @@ const makeJournal = (
504
730
  (record) => checkValueBound("append canonical record", record.recordJson),
505
731
  { discard: true },
506
732
  );
733
+
507
734
  return yield* withWriteTransaction("append transaction")(
508
735
  Effect.gen(function* () {
509
736
  const recordIds = request.records.map((record) => record.recordId);
737
+
510
738
  if (new Set(recordIds).size !== recordIds.length) {
511
739
  return yield* DoAppendConflict.make({
512
740
  message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
@@ -514,34 +742,35 @@ const makeJournal = (
514
742
  });
515
743
  }
516
744
 
517
- const conversationRows = yield* sql<Record<string, unknown>>`
745
+ const threadRows = yield* sql<Record<string, unknown>>`
518
746
  SELECT
519
- conversation_id,
747
+ thread_id,
520
748
  created_at,
521
749
  tail_sequence,
522
750
  tail_digest,
523
751
  producer_epoch
524
- FROM effect_agent_conversations
525
- WHERE conversation_id = ${request.conversationId}
752
+ FROM effect_agent_threads
753
+ WHERE thread_id = ${request.threadId}
526
754
  `.pipe(Effect.mapError(storageError("read append tail")));
527
- const conversation = yield* decodeSingleRow(
528
- Schema.Array(ConversationRow),
529
- "effect_agent_conversations",
530
- request.conversationId,
531
- conversationRows,
755
+
756
+ const thread = yield* decodeSingleRow(
757
+ Schema.Array(ThreadRow),
758
+ "effect_agent_threads",
759
+ request.threadId,
760
+ threadRows,
532
761
  );
533
762
 
534
- if (request.producerEpoch !== conversation.producer_epoch) {
763
+ if (request.producerEpoch !== thread.producer_epoch) {
535
764
  return yield* DoFenceRejected.make({
536
765
  producerEpoch: request.producerEpoch,
537
- actualEpoch: conversation.producer_epoch,
538
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${conversation.producer_epoch}.`,
766
+ actualEpoch: thread.producer_epoch,
767
+ message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`,
539
768
  });
540
769
  }
541
770
 
542
771
  const batchRows = yield* sql<Record<string, unknown>>`
543
772
  SELECT
544
- conversation_id,
773
+ thread_id,
545
774
  batch_id,
546
775
  first_sequence,
547
776
  last_sequence,
@@ -549,31 +778,34 @@ const makeJournal = (
549
778
  tail_digest,
550
779
  batch_json
551
780
  FROM effect_agent_canonical_batches
552
- WHERE conversation_id = ${request.conversationId}
781
+ WHERE thread_id = ${request.threadId}
553
782
  AND batch_id = ${request.batchId}
554
783
  `.pipe(Effect.mapError(storageError("read idempotent batch")));
784
+
555
785
  const batches = yield* decodeRows(
556
786
  Schema.Array(BatchRow),
557
787
  "effect_agent_canonical_batches",
558
- `${request.conversationId}/${request.batchId}`,
788
+ `${request.threadId}/${request.batchId}`,
559
789
  batchRows,
560
790
  );
561
791
 
562
792
  if (batches.length > 1) {
563
793
  return yield* DoStorageCorruptionError.make({
564
794
  table: "effect_agent_canonical_batches",
565
- rowKey: `${request.conversationId}/${request.batchId}`,
795
+ rowKey: `${request.threadId}/${request.batchId}`,
566
796
  message: "A canonical batch primary key returned more than one row.",
567
797
  });
568
798
  }
569
799
  if (batches.length === 1) {
570
800
  const existing = batches[0];
801
+
571
802
  if (existing.batch_digest !== request.batchDigest) {
572
803
  return yield* DoAppendConflict.make({
573
804
  message: `Batch ${request.batchId} already exists with different canonical content.`,
574
805
  reason: "batch-digest",
575
806
  });
576
807
  }
808
+
577
809
  return RawAppendResult.make({
578
810
  firstSequence: existing.first_sequence,
579
811
  lastSequence: existing.last_sequence,
@@ -583,46 +815,48 @@ const makeJournal = (
583
815
  }
584
816
 
585
817
  if (
586
- request.expectedTailSequence !== conversation.tail_sequence ||
587
- request.expectedTailDigest !== conversation.tail_digest
818
+ request.expectedTailSequence !== thread.tail_sequence ||
819
+ request.expectedTailDigest !== thread.tail_digest
588
820
  ) {
589
821
  return yield* DoAppendConflict.make({
590
822
  message:
591
823
  `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} ` +
592
- `but found ${conversation.tail_sequence}/${conversation.tail_digest}.`,
824
+ `but found ${thread.tail_sequence}/${thread.tail_digest}.`,
593
825
  reason: "tail",
594
- actualTailSequence: conversation.tail_sequence,
595
- actualTailDigest: conversation.tail_digest,
826
+ actualTailSequence: thread.tail_sequence,
827
+ actualTailDigest: thread.tail_digest,
596
828
  });
597
829
  }
598
- if (conversation.tail_sequence + request.records.length > MAX_RECORDS_PER_CONVERSATION) {
830
+ if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) {
599
831
  return yield* DoStorageError.make({
600
832
  operation: "append canonical batch",
601
- message: `Conversation record limit ${MAX_RECORDS_PER_CONVERSATION} would be exceeded.`,
833
+ message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`,
602
834
  });
603
835
  }
604
836
 
605
837
  // Chunked to respect the Durable Object platform's 100-bound-parameter statement
606
838
  // limit: a batch may carry up to 256 records.
607
839
  const existingRecords: Array<RecordRow> = [];
840
+
608
841
  for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
609
842
  const existingRecordRows = yield* sql<Record<string, unknown>>`
610
843
  SELECT
611
- conversation_id,
844
+ thread_id,
612
845
  sequence,
613
846
  record_id,
614
847
  batch_id,
615
848
  record_json
616
849
  FROM effect_agent_canonical_records
617
- WHERE conversation_id = ${request.conversationId}
850
+ WHERE thread_id = ${request.threadId}
618
851
  AND record_id IN ${sql.in([...chunk])}
619
852
  ORDER BY sequence
620
853
  `.pipe(Effect.mapError(storageError("check canonical record identities")));
854
+
621
855
  existingRecords.push(
622
856
  ...(yield* decodeRows(
623
857
  Schema.Array(RecordRow),
624
858
  "effect_agent_canonical_records",
625
- `${request.conversationId}/record_ids`,
859
+ `${request.threadId}/record_ids`,
626
860
  existingRecordRows,
627
861
  )),
628
862
  );
@@ -635,7 +869,7 @@ const makeJournal = (
635
869
  }
636
870
 
637
871
  const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
638
- conversation.tail_sequence + 1,
872
+ thread.tail_sequence + 1,
639
873
  ).pipe(
640
874
  Effect.mapError((error) =>
641
875
  DoStorageError.make({
@@ -645,6 +879,7 @@ const makeJournal = (
645
879
  }),
646
880
  ),
647
881
  );
882
+
648
883
  const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
649
884
  firstSequence + request.records.length - 1,
650
885
  ).pipe(
@@ -659,7 +894,7 @@ const makeJournal = (
659
894
 
660
895
  yield* sql`
661
896
  INSERT INTO effect_agent_canonical_batches (
662
- conversation_id,
897
+ thread_id,
663
898
  batch_id,
664
899
  first_sequence,
665
900
  last_sequence,
@@ -667,7 +902,7 @@ const makeJournal = (
667
902
  tail_digest,
668
903
  batch_json
669
904
  ) VALUES (
670
- ${request.conversationId},
905
+ ${request.threadId},
671
906
  ${request.batchId},
672
907
  ${firstSequence},
673
908
  ${lastSequence},
@@ -684,13 +919,13 @@ const makeJournal = (
684
919
  Effect.gen(function* () {
685
920
  yield* sql`
686
921
  INSERT INTO effect_agent_canonical_records (
687
- conversation_id,
922
+ thread_id,
688
923
  sequence,
689
924
  record_id,
690
925
  batch_id,
691
926
  record_json
692
927
  ) VALUES (
693
- ${request.conversationId},
928
+ ${request.threadId},
694
929
  ${firstSequence + index},
695
930
  ${record.recordId},
696
931
  ${request.batchId},
@@ -703,13 +938,13 @@ const makeJournal = (
703
938
  );
704
939
 
705
940
  yield* sql`
706
- UPDATE effect_agent_conversations
941
+ UPDATE effect_agent_threads
707
942
  SET
708
943
  tail_sequence = ${lastSequence},
709
944
  tail_digest = ${request.tailDigest},
710
945
  producer_epoch = ${request.producerEpoch}
711
- WHERE conversation_id = ${request.conversationId}
712
- `.pipe(Effect.mapError(storageError("advance conversation tail")));
946
+ WHERE thread_id = ${request.threadId}
947
+ `.pipe(Effect.mapError(storageError("advance thread tail")));
713
948
  yield* failpoint("append:after-tail-update");
714
949
 
715
950
  return RawAppendResult.make({
@@ -723,105 +958,137 @@ const makeJournal = (
723
958
  });
724
959
 
725
960
  const read = Effect.fn("DoJournal.read")(function* (request: RawReadRequest) {
726
- const rows = yield* sql<Record<string, unknown>>`
961
+ // Capture membership without retaining payloads. Append-only sequences keep each later
962
+ // payload query inside this snapshot, even when new records arrive during consumption.
963
+ const planRows = yield* sql<Record<string, unknown>>`
727
964
  SELECT
728
- conversation_id,
729
965
  sequence,
730
- record_id,
731
- batch_id,
732
- record_json
966
+ length(CAST(record_json AS BLOB)) AS record_json_bytes
733
967
  FROM effect_agent_canonical_records
734
- WHERE conversation_id = ${request.conversationId}
968
+ WHERE thread_id = ${request.threadId}
735
969
  AND sequence > ${request.fromSequenceExclusive}
736
970
  ORDER BY sequence
737
971
  LIMIT ${request.limit}
738
972
  `.pipe(Effect.mapError(storageError("read canonical records")));
739
- return yield* decodeRows(
740
- Schema.Array(RecordRow),
973
+
974
+ const plan = yield* decodeRows(
975
+ Schema.Array(ReadPlanRow),
741
976
  "effect_agent_canonical_records",
742
- `${request.conversationId}>${request.fromSequenceExclusive}`,
743
- rows,
977
+ `${request.threadId}>${request.fromSequenceExclusive}`,
978
+ planRows,
744
979
  );
980
+
981
+ const mismatch = () =>
982
+ DoStorageCorruptionError.make({
983
+ table: "effect_agent_canonical_records",
984
+ rowKey: `${request.threadId}>${request.fromSequenceExclusive}`,
985
+ message: "Canonical read membership, sequence, or payload size changed from its read plan.",
986
+ });
987
+
988
+ if (plan.length > request.limit) return yield* mismatch();
989
+
990
+ const pages: Array<ReadPage> = [];
991
+ let page: ReadPage | undefined;
992
+ let pageBytes = 0;
993
+ let previousSequence = request.fromSequenceExclusive;
994
+
995
+ for (const row of plan) {
996
+ if (row.sequence !== previousSequence + 1) return yield* mismatch();
997
+ previousSequence = row.sequence;
998
+ if (page === undefined || pageBytes + row.record_json_bytes > MAX_READ_PAGE_JSON_BYTES) {
999
+ page = [row];
1000
+ pages.push(page);
1001
+ pageBytes = row.record_json_bytes;
1002
+ } else {
1003
+ page.push(row);
1004
+ pageBytes += row.record_json_bytes;
1005
+ }
1006
+ }
1007
+
1008
+ const readPage = Effect.fn("DoJournal.readPage")(function* (page: ReadPage) {
1009
+ const rows = yield* sql<Record<string, unknown>>`
1010
+ SELECT thread_id, sequence, record_id, batch_id, record_json
1011
+ FROM effect_agent_canonical_records
1012
+ WHERE thread_id = ${request.threadId}
1013
+ AND sequence >= ${page[0].sequence}
1014
+ AND sequence <= ${page[page.length - 1].sequence}
1015
+ ORDER BY sequence
1016
+ `.pipe(Effect.mapError(storageError("read canonical records")));
1017
+
1018
+ const decoded = yield* decodeRows(
1019
+ Schema.Array(RecordRow),
1020
+ "effect_agent_canonical_records",
1021
+ `${request.threadId}/${page[0].sequence}`,
1022
+ rows,
1023
+ );
1024
+
1025
+ if (
1026
+ decoded.length !== page.length ||
1027
+ decoded.some(
1028
+ (row, index) =>
1029
+ row.thread_id !== request.threadId ||
1030
+ row.sequence !== page[index].sequence ||
1031
+ storedTextBytes(row.record_json) !== page[index].record_json_bytes,
1032
+ )
1033
+ ) {
1034
+ return yield* mismatch();
1035
+ }
1036
+
1037
+ return decoded;
1038
+ });
1039
+
1040
+ return {
1041
+ count: plan.length,
1042
+ records: Stream.fromIterable(pages).pipe(
1043
+ Stream.flatMap((page) => Stream.fromIterableEffect(readPage(page))),
1044
+ ),
1045
+ };
745
1046
  });
746
1047
 
747
- const exportConversation = Effect.fn("DoJournal.exportConversation")(function* (
748
- conversationId: string,
749
- ) {
1048
+ const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId: string) {
750
1049
  return yield* sql
751
1050
  .withTransaction(
752
1051
  Effect.gen(function* () {
753
- const conversationRows = yield* sql<Record<string, unknown>>`
1052
+ const threadRows = yield* sql<Record<string, unknown>>`
754
1053
  SELECT
755
- conversation_id,
1054
+ thread_id,
756
1055
  created_at,
757
1056
  tail_sequence,
758
1057
  tail_digest,
759
1058
  producer_epoch
760
- FROM effect_agent_conversations
761
- WHERE conversation_id = ${conversationId}
762
- `.pipe(Effect.mapError(storageError("export conversation")));
763
- const conversation = yield* decodeSingleRow(
764
- Schema.Array(ConversationRow),
765
- "effect_agent_conversations",
766
- conversationId,
767
- conversationRows,
1059
+ FROM effect_agent_threads
1060
+ WHERE thread_id = ${threadId}
1061
+ `.pipe(Effect.mapError(storageError("export thread")));
1062
+
1063
+ const thread = yield* decodeSingleRow(
1064
+ Schema.Array(ThreadRow),
1065
+ "effect_agent_threads",
1066
+ threadId,
1067
+ threadRows,
768
1068
  );
769
- yield* failpoint("export:after-conversation-read");
770
- const batchRows = yield* sql<Record<string, unknown>>`
771
- SELECT
772
- conversation_id,
773
- batch_id,
774
- first_sequence,
775
- last_sequence,
776
- batch_digest,
777
- tail_digest,
778
- batch_json
779
- FROM effect_agent_canonical_batches
780
- WHERE conversation_id = ${conversationId}
781
- ORDER BY first_sequence
782
- `.pipe(Effect.mapError(storageError("export canonical batches")));
1069
+
1070
+ yield* failpoint("export:after-thread-read");
1071
+
783
1072
  const recordRows = yield* sql<Record<string, unknown>>`
784
1073
  SELECT
785
- conversation_id,
1074
+ thread_id,
786
1075
  sequence,
787
1076
  record_id,
788
1077
  batch_id,
789
1078
  record_json
790
1079
  FROM effect_agent_canonical_records
791
- WHERE conversation_id = ${conversationId}
1080
+ WHERE thread_id = ${threadId}
792
1081
  ORDER BY sequence
793
1082
  `.pipe(Effect.mapError(storageError("export canonical records")));
794
- const checkpointRows = yield* sql<Record<string, unknown>>`
795
- SELECT
796
- conversation_id,
797
- through_sequence,
798
- tail_digest,
799
- checkpoint_json
800
- FROM effect_agent_checkpoints
801
- WHERE conversation_id = ${conversationId}
802
- ORDER BY through_sequence
803
- `.pipe(Effect.mapError(storageError("export checkpoints")));
804
1083
 
805
- return RawConversationExport.make({
806
- conversation,
807
- batches: yield* decodeRows(
808
- Schema.Array(BatchRow),
809
- "effect_agent_canonical_batches",
810
- conversationId,
811
- batchRows,
812
- ),
1084
+ return RawThreadExport.make({
1085
+ thread,
813
1086
  records: yield* decodeRows(
814
1087
  Schema.Array(RecordRow),
815
1088
  "effect_agent_canonical_records",
816
- conversationId,
1089
+ threadId,
817
1090
  recordRows,
818
1091
  ),
819
- checkpoints: yield* decodeRows(
820
- Schema.Array(CheckpointRow),
821
- "effect_agent_checkpoints",
822
- conversationId,
823
- checkpointRows,
824
- ),
825
1092
  });
826
1093
  }),
827
1094
  )
@@ -835,7 +1102,7 @@ const makeJournal = (
835
1102
  const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (
836
1103
  checkpoint: RawCheckpoint,
837
1104
  ): Effect.fn.Return<void, CheckpointError> {
838
- if (checkpoint.conversationId.length > MAX_IDENTIFIER_LENGTH) {
1105
+ if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) {
839
1106
  return yield* DoStorageError.make({
840
1107
  operation: "save checkpoint",
841
1108
  message: "Checkpoint identity exceeds the Durable Object storage bounds.",
@@ -844,50 +1111,54 @@ const makeJournal = (
844
1111
  yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
845
1112
  yield* withWriteTransaction("checkpoint transaction")(
846
1113
  Effect.gen(function* () {
847
- const conversationRows = yield* sql<Record<string, unknown>>`
1114
+ const threadRows = yield* sql<Record<string, unknown>>`
848
1115
  SELECT
849
- conversation_id,
1116
+ thread_id,
850
1117
  created_at,
851
1118
  tail_sequence,
852
1119
  tail_digest,
853
1120
  producer_epoch
854
- FROM effect_agent_conversations
855
- WHERE conversation_id = ${checkpoint.conversationId}
1121
+ FROM effect_agent_threads
1122
+ WHERE thread_id = ${checkpoint.threadId}
856
1123
  `.pipe(Effect.mapError(storageError("read checkpoint tail")));
857
- const conversation = yield* decodeSingleRow(
858
- Schema.Array(ConversationRow),
859
- "effect_agent_conversations",
860
- checkpoint.conversationId,
861
- conversationRows,
1124
+
1125
+ const thread = yield* decodeSingleRow(
1126
+ Schema.Array(ThreadRow),
1127
+ "effect_agent_threads",
1128
+ checkpoint.threadId,
1129
+ threadRows,
862
1130
  );
863
- if (checkpoint.throughSequence > conversation.tail_sequence) {
1131
+
1132
+ if (checkpoint.throughSequence > thread.tail_sequence) {
864
1133
  return yield* DoCheckpointConflict.make({
865
1134
  message:
866
1135
  `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ` +
867
- `${conversation.tail_sequence}.`,
1136
+ `${thread.tail_sequence}.`,
868
1137
  });
869
1138
  }
870
1139
 
871
1140
  const checkpointRows = yield* sql<Record<string, unknown>>`
872
1141
  SELECT
873
- conversation_id,
1142
+ thread_id,
874
1143
  through_sequence,
875
1144
  tail_digest,
876
1145
  checkpoint_json
877
1146
  FROM effect_agent_checkpoints
878
- WHERE conversation_id = ${checkpoint.conversationId}
1147
+ WHERE thread_id = ${checkpoint.threadId}
879
1148
  AND through_sequence = ${checkpoint.throughSequence}
880
1149
  `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
1150
+
881
1151
  const existing = yield* decodeRows(
882
1152
  Schema.Array(CheckpointRow),
883
1153
  "effect_agent_checkpoints",
884
- `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
1154
+ `${checkpoint.threadId}/${checkpoint.throughSequence}`,
885
1155
  checkpointRows,
886
1156
  );
1157
+
887
1158
  if (existing.length > 1) {
888
1159
  return yield* DoStorageCorruptionError.make({
889
1160
  table: "effect_agent_checkpoints",
890
- rowKey: `${checkpoint.conversationId}/${checkpoint.throughSequence}`,
1161
+ rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
891
1162
  message: "A checkpoint primary key returned more than one row.",
892
1163
  });
893
1164
  }
@@ -900,17 +1171,18 @@ const makeJournal = (
900
1171
  message: "A different checkpoint already exists at this canonical sequence.",
901
1172
  });
902
1173
  }
1174
+
903
1175
  return;
904
1176
  }
905
1177
 
906
1178
  yield* sql`
907
1179
  INSERT INTO effect_agent_checkpoints (
908
- conversation_id,
1180
+ thread_id,
909
1181
  through_sequence,
910
1182
  tail_digest,
911
1183
  checkpoint_json
912
1184
  ) VALUES (
913
- ${checkpoint.conversationId},
1185
+ ${checkpoint.threadId},
914
1186
  ${checkpoint.throughSequence},
915
1187
  ${checkpoint.tailDigest},
916
1188
  ${checkpoint.checkpointJson}
@@ -921,44 +1193,47 @@ const makeJournal = (
921
1193
  });
922
1194
 
923
1195
  const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (
924
- conversationId: string,
1196
+ threadId: string,
925
1197
  atOrBeforeSequence: CanonicalSequence,
926
1198
  ) {
927
1199
  const rows = yield* sql<Record<string, unknown>>`
928
1200
  SELECT
929
- conversation_id,
1201
+ thread_id,
930
1202
  through_sequence,
931
1203
  tail_digest,
932
1204
  checkpoint_json
933
1205
  FROM effect_agent_checkpoints
934
- WHERE conversation_id = ${conversationId}
1206
+ WHERE thread_id = ${threadId}
935
1207
  AND through_sequence <= ${atOrBeforeSequence}
936
1208
  ORDER BY through_sequence DESC
937
1209
  LIMIT 1
938
1210
  `.pipe(Effect.mapError(storageError("load checkpoint")));
1211
+
939
1212
  return yield* decodeRows(
940
1213
  Schema.Array(CheckpointRow),
941
1214
  "effect_agent_checkpoints",
942
- `${conversationId}<=${atOrBeforeSequence}`,
1215
+ `${threadId}<=${atOrBeforeSequence}`,
943
1216
  rows,
944
1217
  );
945
1218
  });
946
1219
 
947
1220
  const getTailDigestAt = Effect.fn("DoJournal.getTailDigestAt")(function* (
948
- conversationId: string,
1221
+ threadId: string,
949
1222
  sequence: CanonicalSequence,
950
1223
  ) {
951
1224
  if (sequence === 0) {
952
- const conversations = yield* getConversation(conversationId);
953
- return conversations.length === 0
1225
+ const threads = yield* getThread(threadId);
1226
+
1227
+ return threads.length === 0
954
1228
  ? []
955
- : [conversations[0].tail_sequence === 0 ? conversations[0].tail_digest : undefined].filter(
1229
+ : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
956
1230
  (value): value is string => value !== undefined,
957
1231
  );
958
1232
  }
1233
+
959
1234
  const rows = yield* sql<Record<string, unknown>>`
960
1235
  SELECT
961
- conversation_id,
1236
+ thread_id,
962
1237
  batch_id,
963
1238
  first_sequence,
964
1239
  last_sequence,
@@ -966,15 +1241,17 @@ const makeJournal = (
966
1241
  tail_digest,
967
1242
  batch_json
968
1243
  FROM effect_agent_canonical_batches
969
- WHERE conversation_id = ${conversationId}
1244
+ WHERE thread_id = ${threadId}
970
1245
  AND last_sequence = ${sequence}
971
1246
  `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
1247
+
972
1248
  const batches = yield* decodeRows(
973
1249
  Schema.Array(BatchRow),
974
1250
  "effect_agent_canonical_batches",
975
- `${conversationId}/${sequence}`,
1251
+ `${threadId}/${sequence}`,
976
1252
  rows,
977
1253
  );
1254
+
978
1255
  return batches.map((batch) => batch.tail_digest);
979
1256
  });
980
1257
 
@@ -982,19 +1259,20 @@ const makeJournal = (
982
1259
  return yield* sql
983
1260
  .withTransaction(
984
1261
  Effect.gen(function* () {
985
- const conversations = yield* sql<Record<string, unknown>>`
1262
+ const threads = yield* sql<Record<string, unknown>>`
986
1263
  SELECT
987
- conversation_id,
1264
+ thread_id,
988
1265
  created_at,
989
1266
  tail_sequence,
990
1267
  tail_digest,
991
1268
  producer_epoch
992
- FROM effect_agent_conversations
993
- ORDER BY conversation_id
994
- `.pipe(Effect.mapError(storageError("scan conversations")));
1269
+ FROM effect_agent_threads
1270
+ ORDER BY thread_id
1271
+ `.pipe(Effect.mapError(storageError("scan threads")));
1272
+
995
1273
  const batches = yield* sql<Record<string, unknown>>`
996
1274
  SELECT
997
- conversation_id,
1275
+ thread_id,
998
1276
  batch_id,
999
1277
  first_sequence,
1000
1278
  last_sequence,
@@ -1002,33 +1280,36 @@ const makeJournal = (
1002
1280
  tail_digest,
1003
1281
  batch_json
1004
1282
  FROM effect_agent_canonical_batches
1005
- ORDER BY conversation_id, first_sequence
1283
+ ORDER BY thread_id, first_sequence
1006
1284
  `.pipe(Effect.mapError(storageError("scan canonical batches")));
1285
+
1007
1286
  const records = yield* sql<Record<string, unknown>>`
1008
1287
  SELECT
1009
- conversation_id,
1288
+ thread_id,
1010
1289
  sequence,
1011
1290
  record_id,
1012
1291
  batch_id,
1013
1292
  record_json
1014
1293
  FROM effect_agent_canonical_records
1015
- ORDER BY conversation_id, sequence
1294
+ ORDER BY thread_id, sequence
1016
1295
  `.pipe(Effect.mapError(storageError("scan canonical records")));
1296
+
1017
1297
  const checkpoints = yield* sql<Record<string, unknown>>`
1018
1298
  SELECT
1019
- conversation_id,
1299
+ thread_id,
1020
1300
  through_sequence,
1021
1301
  tail_digest,
1022
1302
  checkpoint_json
1023
1303
  FROM effect_agent_checkpoints
1024
- ORDER BY conversation_id, through_sequence
1304
+ ORDER BY thread_id, through_sequence
1025
1305
  `.pipe(Effect.mapError(storageError("scan checkpoints")));
1306
+
1026
1307
  return {
1027
- conversations: yield* decodeRows(
1028
- Schema.Array(ConversationRow),
1029
- "effect_agent_conversations",
1308
+ threads: yield* decodeRows(
1309
+ Schema.Array(ThreadRow),
1310
+ "effect_agent_threads",
1030
1311
  "startup_scan",
1031
- conversations,
1312
+ threads,
1032
1313
  ),
1033
1314
  batches: yield* decodeRows(
1034
1315
  Schema.Array(BatchRow),
@@ -1061,8 +1342,8 @@ const makeJournal = (
1061
1342
  return {
1062
1343
  append,
1063
1344
  checkValueBound,
1064
- exportConversation,
1065
- getConversation,
1345
+ exportThread,
1346
+ getThread,
1066
1347
  getTailDigestAt,
1067
1348
  loadCheckpoint,
1068
1349
  materialize,