@effect-agent/storage-sqlite 0.1.0-beta.38 → 0.1.0-beta.40

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.
@@ -6,17 +6,18 @@ import {
6
6
  CanonicalRecordEnvelope,
7
7
  CanonicalSequence,
8
8
  CheckpointRejected,
9
- ConversationCheckpoint,
10
- ConversationExport,
11
- ConversationExportRequest,
12
- ConversationMaterialization,
13
- ConversationNotMaterialized,
14
- ConversationObservation,
15
- ConversationRead,
16
- ConversationStore,
17
- ConversationStoreError,
18
- ConversationTail,
19
- ConversationTailRequest,
9
+ ThreadCheckpoint,
10
+ ThreadExport,
11
+ ThreadExportRequest,
12
+ ThreadMaterialization,
13
+ ThreadNotMaterialized,
14
+ ThreadObservation,
15
+ ThreadRead,
16
+ ThreadStore,
17
+ type ThreadCheckpoints,
18
+ ThreadStoreError,
19
+ ThreadTail,
20
+ ThreadTailRequest,
20
21
  DEFAULT_OWNERSHIP_LEASE_DURATION,
21
22
  digestCanonicalBatch,
22
23
  Digest,
@@ -26,7 +27,7 @@ import {
26
27
  LoadCheckpointRequest,
27
28
  ObservationOffset,
28
29
  SaveCheckpointRequest,
29
- } from "@effect-agent/session";
30
+ } from "@effect-agent/thread";
30
31
  import { NodeCrypto } from "@effect/platform-node";
31
32
  import { SqliteClient } from "@effect/sql-sqlite-node";
32
33
  import {
@@ -72,7 +73,7 @@ export interface SqliteStorageOptions {
72
73
  readonly busyTimeout?: number | undefined;
73
74
  /**
74
75
  * Submission ownership lease duration in milliseconds (D5). Defaults to
75
- * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/session`.
76
+ * `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
76
77
  */
77
78
  readonly ownershipLeaseDuration?: number | undefined;
78
79
  /**
@@ -98,52 +99,51 @@ const isSqliteAppendConflict = Schema.is(SqliteAppendConflict);
98
99
  const isSqliteCheckpointConflict = Schema.is(SqliteCheckpointConflict);
99
100
 
100
101
  const storeError = (operation: string, error: { readonly message: string }) =>
101
- ConversationStoreError.make({
102
+ ThreadStoreError.make({
102
103
  cause: error,
103
104
  operation,
104
105
  message: error.message,
105
106
  });
106
107
 
107
108
  const schemaStoreError = (operation: string, error: { readonly message: string }) =>
108
- ConversationStoreError.make({
109
+ ThreadStoreError.make({
109
110
  cause: error,
110
111
  operation,
111
112
  message: error.message,
112
113
  });
113
114
 
114
- const makeOffset = Effect.fn("SqliteConversationStore.makeOffset")(function* (
115
- conversationId: ConversationMaterialization["conversationId"],
115
+ const makeOffset = Effect.fn("SqliteThreadStore.makeOffset")(function* (
116
+ threadId: ThreadMaterialization["threadId"],
116
117
  sequence: number,
117
- ): Effect.fn.Return<ObservationOffset, ConversationStoreError> {
118
+ ): Effect.fn.Return<ObservationOffset, ThreadStoreError> {
118
119
  return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(
119
120
  Effect.flatMap((validatedSequence) =>
120
121
  Schema.decodeUnknownEffect(ObservationOffset)(
121
- `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:${validatedSequence}`,
122
+ `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`,
122
123
  ),
123
124
  ),
124
125
  Effect.mapError((error) => schemaStoreError("encode observation offset", error)),
125
126
  );
126
127
  });
127
128
 
128
- const parseOffset = Effect.fn("SqliteConversationStore.parseOffset")(function* (
129
- conversationId: ConversationMaterialization["conversationId"],
129
+ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (
130
+ threadId: ThreadMaterialization["threadId"],
130
131
  offset: ObservationOffset | undefined,
131
- ): Effect.fn.Return<CanonicalSequence, ConversationStoreError> {
132
+ ): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
132
133
  if (offset === undefined) return ZERO_CANONICAL_SEQUENCE;
133
134
  const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(
134
135
  Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
135
136
  );
136
- const conversationPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(conversationId)}:`;
137
- if (!text.startsWith(conversationPrefix)) {
138
- return yield* ConversationStoreError.make({
137
+ const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
138
+ if (!text.startsWith(threadPrefix)) {
139
+ return yield* ThreadStoreError.make({
139
140
  operation: "decode observation offset",
140
- message:
141
- "The observation offset belongs to a different adapter, storage version, or Conversation.",
141
+ message: "The observation offset belongs to a different adapter, storage version, or Thread.",
142
142
  });
143
143
  }
144
- const sequenceText = text.slice(conversationPrefix.length);
144
+ const sequenceText = text.slice(threadPrefix.length);
145
145
  if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) {
146
- return yield* ConversationStoreError.make({
146
+ return yield* ThreadStoreError.make({
147
147
  operation: "decode observation offset",
148
148
  message: "The observation offset is malformed.",
149
149
  });
@@ -153,43 +153,40 @@ const parseOffset = Effect.fn("SqliteConversationStore.parseOffset")(function* (
153
153
  );
154
154
  });
155
155
 
156
- const mapFence = (
157
- conversationId: ConversationMaterialization["conversationId"],
158
- error: SqliteFenceRejected,
159
- ) =>
156
+ const mapFence = (threadId: ThreadMaterialization["threadId"], error: SqliteFenceRejected) =>
160
157
  FenceRejected.make({
161
- conversationId,
158
+ threadId,
162
159
  actualEpoch: error.actualEpoch,
163
160
  attemptedEpoch: error.producerEpoch,
164
161
  });
165
162
 
166
- const encodeCanonicalRecord = Effect.fn("SqliteConversationStore.encodeCanonicalRecord")(function* (
163
+ const encodeCanonicalRecord = Effect.fn("SqliteThreadStore.encodeCanonicalRecord")(function* (
167
164
  record: CanonicalRecord,
168
- ): Effect.fn.Return<string, ConversationStoreError> {
165
+ ): Effect.fn.Return<string, ThreadStoreError> {
169
166
  return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(
170
167
  Effect.mapError((error) => schemaStoreError("encode canonical record", error)),
171
168
  );
172
169
  });
173
170
 
174
- const encodeCanonicalBatch = Effect.fn("SqliteConversationStore.encodeCanonicalBatch")(function* (
171
+ const encodeCanonicalBatch = Effect.fn("SqliteThreadStore.encodeCanonicalBatch")(function* (
175
172
  batch: CanonicalBatch,
176
- ): Effect.fn.Return<string, ConversationStoreError> {
173
+ ): Effect.fn.Return<string, ThreadStoreError> {
177
174
  return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(
178
175
  Effect.mapError((error) => schemaStoreError("encode canonical batch", error)),
179
176
  );
180
177
  });
181
178
 
182
- const encodeCheckpoint = Effect.fn("SqliteConversationStore.encodeCheckpoint")(function* (
183
- checkpoint: ConversationCheckpoint,
184
- ): Effect.fn.Return<string, ConversationStoreError> {
185
- return yield* Schema.encodeEffect(Schema.fromJsonString(ConversationCheckpoint))(checkpoint).pipe(
179
+ const encodeCheckpoint = Effect.fn("SqliteThreadStore.encodeCheckpoint")(function* (
180
+ checkpoint: ThreadCheckpoint,
181
+ ): Effect.fn.Return<string, ThreadStoreError> {
182
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(
186
183
  Effect.mapError((error) => schemaStoreError("encode checkpoint", error)),
187
184
  );
188
185
  });
189
186
 
190
- const decodeEnvelope = Effect.fn("SqliteConversationStore.decodeEnvelope")(function* (row: {
187
+ const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (row: {
191
188
  readonly batch_id: string;
192
- readonly conversation_id: string;
189
+ readonly thread_id: string;
193
190
  readonly record_json: string;
194
191
  readonly sequence: CanonicalSequence;
195
192
  }) {
@@ -197,23 +194,21 @@ const decodeEnvelope = Effect.fn("SqliteConversationStore.decodeEnvelope")(funct
197
194
  row.record_json,
198
195
  ).pipe(
199
196
  Effect.mapError((error) =>
200
- ConversationStoreError.make({
197
+ ThreadStoreError.make({
201
198
  operation: "decode canonical record",
202
199
  message: error.message,
203
200
  }),
204
201
  ),
205
202
  );
206
- const conversationId = yield* Schema.decodeUnknownEffect(
207
- CanonicalRecordEnvelope.fields.conversationId,
208
- )(row.conversation_id).pipe(
209
- Effect.mapError((error) => schemaStoreError("decode conversation identity", error)),
210
- );
211
- const offset = yield* makeOffset(conversationId, row.sequence);
203
+ const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(
204
+ row.thread_id,
205
+ ).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
206
+ const offset = yield* makeOffset(threadId, row.sequence);
212
207
  const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(
213
208
  row.batch_id,
214
209
  ).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
215
210
  return CanonicalRecordEnvelope.make({
216
- conversationId,
211
+ threadId,
217
212
  batchId,
218
213
  sequence: row.sequence,
219
214
  offset,
@@ -221,39 +216,39 @@ const decodeEnvelope = Effect.fn("SqliteConversationStore.decodeEnvelope")(funct
221
216
  });
222
217
  });
223
218
 
224
- const decodeCheckpoint = Effect.fn("SqliteConversationStore.decodeCheckpoint")(function* (
219
+ const decodeCheckpoint = Effect.fn("SqliteThreadStore.decodeCheckpoint")(function* (
225
220
  checkpointJson: string,
226
- ): Effect.fn.Return<ConversationCheckpoint, ConversationStoreError> {
227
- return yield* Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(
228
- checkpointJson,
229
- ).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
221
+ ): Effect.fn.Return<ThreadCheckpoint, ThreadStoreError> {
222
+ return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(
223
+ Effect.mapError((error) => schemaStoreError("decode checkpoint", error)),
224
+ );
230
225
  });
231
226
 
232
- const requireConversation = Effect.fn("SqliteConversationStore.requireConversation")(function* (
227
+ const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (
233
228
  journal: SqliteJournal,
234
- conversationId: ConversationMaterialization["conversationId"],
229
+ threadId: ThreadMaterialization["threadId"],
235
230
  ) {
236
231
  const rows = yield* journal
237
- .getConversation(conversationId)
238
- .pipe(Effect.mapError((error) => storeError("read conversation", error)));
232
+ .getThread(threadId)
233
+ .pipe(Effect.mapError((error) => storeError("read thread", error)));
239
234
  if (rows.length === 0) {
240
- return yield* ConversationNotMaterialized.make({ conversationId });
235
+ return yield* ThreadNotMaterialized.make({ threadId });
241
236
  }
242
237
  return rows[0];
243
238
  });
244
239
 
245
- const tailDigestAt = Effect.fn("SqliteConversationStore.tailDigestAt")(function* (
240
+ const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (
246
241
  journal: SqliteJournal,
247
- conversationId: ConversationMaterialization["conversationId"],
242
+ threadId: ThreadMaterialization["threadId"],
248
243
  sequence: CanonicalSequence,
249
244
  ) {
250
245
  if (sequence === 0) return EMPTY_TAIL_DIGEST;
251
246
  const digests = yield* journal
252
- .getTailDigestAt(conversationId, sequence)
247
+ .getTailDigestAt(threadId, sequence)
253
248
  .pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
254
249
  if (digests.length !== 1) {
255
250
  return yield* CheckpointRejected.make({
256
- conversationId,
251
+ threadId,
257
252
  reason: "digest-mismatch",
258
253
  });
259
254
  }
@@ -283,7 +278,7 @@ const groupByKey = <A>(
283
278
  * and re-digested against the canonical chain. Routine opens skip this scan: per-operation
284
279
  * Schema decoding plus the digest chain already fail clearly on corrupt rows.
285
280
  */
286
- const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPayloads")(function* (
281
+ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads")(function* (
287
282
  journal: SqliteJournal,
288
283
  crypto: Crypto.Crypto,
289
284
  ) {
@@ -294,7 +289,7 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
294
289
  Effect.mapError((error) =>
295
290
  SqliteStorageCorruptionError.make({
296
291
  table: "effect_agent_canonical_batches",
297
- rowKey: `${batch.conversation_id}/${batch.batch_id}`,
292
+ rowKey: `${batch.thread_id}/${batch.batch_id}`,
298
293
  message: error.message,
299
294
  }),
300
295
  ),
@@ -306,46 +301,41 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
306
301
  Effect.mapError((error) =>
307
302
  SqliteStorageCorruptionError.make({
308
303
  table: "effect_agent_canonical_records",
309
- rowKey: `${record.conversation_id}/${record.sequence}`,
304
+ rowKey: `${record.thread_id}/${record.sequence}`,
310
305
  message: error.message,
311
306
  }),
312
307
  ),
313
308
  ),
314
309
  );
315
310
  const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
316
- Schema.decodeEffect(Schema.fromJsonString(ConversationCheckpoint))(
317
- checkpoint.checkpoint_json,
318
- ).pipe(
311
+ Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
319
312
  Effect.map((decoded) => ({ decoded, row: checkpoint })),
320
313
  Effect.mapError((error) =>
321
314
  SqliteStorageCorruptionError.make({
322
315
  table: "effect_agent_checkpoints",
323
- rowKey: `${checkpoint.conversation_id}/${checkpoint.through_sequence}`,
316
+ rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
324
317
  message: error.message,
325
318
  }),
326
319
  ),
327
320
  ),
328
321
  );
329
322
 
330
- const batchesByConversation = groupByKey(batches, ({ row }) => row.conversation_id);
331
- const recordsByConversation = groupByKey(records, ({ row }) => row.conversation_id);
332
- const checkpointsByConversation = groupByKey(checkpoints, ({ row }) => row.conversation_id);
333
- const materializedIds = new Set(
334
- stored.conversations.map((conversation) => conversation.conversation_id),
335
- );
323
+ const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
324
+ const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
325
+ const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
326
+ const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
336
327
 
337
- for (const conversation of stored.conversations) {
338
- const conversationBatches = batchesByConversation.get(conversation.conversation_id) ?? [];
339
- const conversationRecords = recordsByConversation.get(conversation.conversation_id) ?? [];
340
- const conversationCheckpoints =
341
- checkpointsByConversation.get(conversation.conversation_id) ?? [];
342
- const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
328
+ for (const thread of stored.threads) {
329
+ const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
330
+ const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
331
+ const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
332
+ const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
343
333
  let previousDigest = EMPTY_TAIL_DIGEST;
344
334
  let expectedSequence = 1;
345
335
  const tailDigests = new Map<number, string>([[0, EMPTY_TAIL_DIGEST]]);
346
336
 
347
- for (const { decoded: canonicalBatch, row: batchRow } of conversationBatches) {
348
- const key = `${batchRow.conversation_id}/${batchRow.batch_id}`;
337
+ for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
338
+ const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
349
339
  if (
350
340
  canonicalBatch.batchId !== batchRow.batch_id ||
351
341
  batchRow.first_sequence !== expectedSequence ||
@@ -428,27 +418,27 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
428
418
  }
429
419
 
430
420
  if (
431
- conversationRecords.length !== conversation.tail_sequence ||
432
- conversation.tail_sequence !== expectedSequence - 1 ||
433
- conversation.tail_digest !== previousDigest
421
+ threadRecords.length !== thread.tail_sequence ||
422
+ thread.tail_sequence !== expectedSequence - 1 ||
423
+ thread.tail_digest !== previousDigest
434
424
  ) {
435
425
  return yield* SqliteStorageCorruptionError.make({
436
- table: "effect_agent_conversations",
437
- rowKey: conversation.conversation_id,
438
- message: "Conversation tail does not match its canonical batch chain.",
426
+ table: "effect_agent_threads",
427
+ rowKey: thread.thread_id,
428
+ message: "Thread tail does not match its canonical batch chain.",
439
429
  });
440
430
  }
441
431
 
442
- for (const checkpoint of conversationCheckpoints) {
432
+ for (const checkpoint of threadCheckpoints) {
443
433
  if (
444
- checkpoint.decoded.conversationId !== conversation.conversation_id ||
434
+ checkpoint.decoded.threadId !== thread.thread_id ||
445
435
  checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence ||
446
436
  checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest ||
447
437
  tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest
448
438
  ) {
449
439
  return yield* SqliteStorageCorruptionError.make({
450
440
  table: "effect_agent_checkpoints",
451
- rowKey: `${conversation.conversation_id}/${checkpoint.row.through_sequence}`,
441
+ rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
452
442
  message: "Checkpoint identity or digest is not bound to a canonical batch tail.",
453
443
  });
454
444
  }
@@ -456,19 +446,19 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
456
446
  }
457
447
 
458
448
  if (
459
- batches.some(({ row }) => !materializedIds.has(row.conversation_id)) ||
460
- records.some(({ row }) => !materializedIds.has(row.conversation_id)) ||
461
- checkpoints.some(({ row }) => !materializedIds.has(row.conversation_id))
449
+ batches.some(({ row }) => !materializedIds.has(row.thread_id)) ||
450
+ records.some(({ row }) => !materializedIds.has(row.thread_id)) ||
451
+ checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))
462
452
  ) {
463
453
  return yield* SqliteStorageCorruptionError.make({
464
- table: "effect_agent_conversations",
454
+ table: "effect_agent_threads",
465
455
  rowKey: "startup_scan",
466
- message: "Canonical rows exist without a materialized Conversation.",
456
+ message: "Canonical rows exist without a materialized Thread.",
467
457
  });
468
458
  }
469
459
  });
470
460
 
471
- const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function* () {
461
+ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
472
462
  const config = yield* SqliteStorageConfig;
473
463
  const failpoint = yield* SqliteStorageFailpoint;
474
464
  const crypto = yield* Crypto.Crypto;
@@ -479,24 +469,24 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
479
469
 
480
470
  const provideCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
481
471
  Effect.provideService(effect, Crypto.Crypto, crypto);
482
- const hitFailpoint = Effect.fn("SqliteConversationStore.hitFailpoint")(
483
- (location: SqliteStorageFailpointLocation): Effect.Effect<void, ConversationStoreError> =>
472
+ const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")(
473
+ (location: SqliteStorageFailpointLocation): Effect.Effect<void, ThreadStoreError> =>
484
474
  failpoint
485
475
  .hit(location)
486
476
  .pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))),
487
477
  );
488
478
 
489
- const materialize: ConversationStore["Service"]["materialize"] = Effect.fn(
490
- "SqliteConversationStore.materialize",
491
- )(function* (request: ConversationMaterialization) {
492
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(
479
+ const materialize: ThreadStore["Service"]["materialize"] = Effect.fn(
480
+ "SqliteThreadStore.materialize",
481
+ )(function* (request: ThreadMaterialization) {
482
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(
493
483
  request,
494
484
  ).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
495
485
  const now = yield* Clock.currentTimeMillis;
496
486
  yield* hitFailpoint("materialize:before");
497
487
  yield* journal
498
488
  .materialize(
499
- validated.conversationId,
489
+ validated.threadId,
500
490
  new Date(now).toISOString(),
501
491
  EMPTY_TAIL_DIGEST,
502
492
  validated.producerEpoch,
@@ -504,20 +494,20 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
504
494
  .pipe(
505
495
  Effect.mapError((error) =>
506
496
  error._tag === "SqliteFenceRejected"
507
- ? mapFence(validated.conversationId, error)
508
- : storeError("materialize conversation", error),
497
+ ? mapFence(validated.threadId, error)
498
+ : storeError("materialize thread", error),
509
499
  ),
510
500
  );
511
501
  yield* hitFailpoint("materialize:after");
512
502
  });
513
503
 
514
- const append: ConversationStore["Service"]["append"] = Effect.fn(
515
- "SqliteConversationStore.append",
516
- )(function* (request: FencedAppendRequest) {
504
+ const append: ThreadStore["Service"]["append"] = Effect.fn("SqliteThreadStore.append")(function* (
505
+ request: FencedAppendRequest,
506
+ ) {
517
507
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(
518
508
  request,
519
509
  ).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
520
- yield* requireConversation(journal, validated.conversationId);
510
+ yield* requireThread(journal, validated.threadId);
521
511
  const tailDigest = yield* provideCrypto(
522
512
  digestCanonicalBatch(validated.expectedTailDigest, validated.batch),
523
513
  ).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
@@ -531,7 +521,7 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
531
521
  ),
532
522
  );
533
523
  const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
534
- conversationId: validated.conversationId,
524
+ threadId: validated.threadId,
535
525
  batchId: validated.batch.batchId,
536
526
  batchDigest: tailDigest,
537
527
  batchJson,
@@ -545,19 +535,19 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
545
535
  const result = yield* journal.append(rawRequest).pipe(
546
536
  Effect.mapError((error) => {
547
537
  if (isSqliteFenceRejected(error)) {
548
- return mapFence(validated.conversationId, error);
538
+ return mapFence(validated.threadId, error);
549
539
  }
550
540
  if (isSqliteAppendConflict(error)) {
551
541
  return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
552
542
  ? AppendConflict.make({
553
- conversationId: validated.conversationId,
543
+ threadId: validated.threadId,
554
544
  batchId: validated.batch.batchId,
555
545
  reason: error.reason,
556
546
  actualTailSequence: error.actualTailSequence,
557
547
  actualTailDigest: error.actualTailDigest,
558
548
  })
559
549
  : AppendConflict.make({
560
- conversationId: validated.conversationId,
550
+ threadId: validated.threadId,
561
551
  batchId: validated.batch.batchId,
562
552
  reason: error.reason,
563
553
  });
@@ -574,7 +564,7 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
574
564
  return result;
575
565
  });
576
566
 
577
- const loadRecords = Effect.fn("SqliteConversationStore.loadRecords")(function* (
567
+ const loadRecords = Effect.fn("SqliteThreadStore.loadRecords")(function* (
578
568
  request: RawReadRequest,
579
569
  ) {
580
570
  const rows = yield* journal
@@ -583,41 +573,36 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
583
573
  return yield* Effect.forEach(rows, decodeEnvelope);
584
574
  });
585
575
 
586
- const readEffect = Effect.fn("SqliteConversationStore.read")(function* (
587
- request: ConversationRead,
588
- ) {
589
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationRead))(
590
- request,
591
- ).pipe(Effect.mapError((error) => schemaStoreError("validate conversation read", error)));
592
- yield* requireConversation(journal, validated.conversationId);
576
+ const readEffect = Effect.fn("SqliteThreadStore.read")(function* (request: ThreadRead) {
577
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(
578
+ Effect.mapError((error) => schemaStoreError("validate thread read", error)),
579
+ );
580
+ yield* requireThread(journal, validated.threadId);
593
581
  const records = yield* loadRecords(
594
582
  RawReadRequest.make({
595
- conversationId: validated.conversationId,
583
+ threadId: validated.threadId,
596
584
  fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
597
585
  limit: validated.limit,
598
586
  }),
599
587
  );
600
588
  return Stream.fromIterable(records);
601
589
  });
602
- const read: ConversationStore["Service"]["read"] = (request) =>
603
- Stream.unwrap(readEffect(request));
590
+ const read: ThreadStore["Service"]["read"] = (request) => Stream.unwrap(readEffect(request));
604
591
 
605
- const observeEffect = Effect.fn("SqliteConversationStore.observe")(function* (
606
- request: ConversationObservation,
592
+ const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (
593
+ request: ThreadObservation,
607
594
  ) {
608
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(
595
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(
609
596
  request,
610
- ).pipe(
611
- Effect.mapError((error) => schemaStoreError("validate conversation observation", error)),
612
- );
613
- yield* requireConversation(journal, validated.conversationId);
614
- const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
597
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
598
+ yield* requireThread(journal, validated.threadId);
599
+ const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
615
600
  const cursor = yield* Ref.make(initialSequence);
616
- const poll = Effect.fn("SqliteConversationStore.observePoll")(function* () {
601
+ const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
617
602
  const fromSequenceExclusive = yield* Ref.get(cursor);
618
603
  const records = yield* loadRecords(
619
604
  RawReadRequest.make({
620
- conversationId: validated.conversationId,
605
+ threadId: validated.threadId,
621
606
  fromSequenceExclusive,
622
607
  limit: 1_024,
623
608
  }),
@@ -631,163 +616,159 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
631
616
  });
632
617
  return Stream.fromIterableEffectRepeat(poll());
633
618
  });
634
- const observe: ConversationStore["Service"]["observe"] = (request) =>
619
+ const observe: ThreadStore["Service"]["observe"] = (request) =>
635
620
  Stream.unwrap(observeEffect(request));
636
621
 
637
- const exportConversation: ConversationStore["Service"]["export"] = Effect.fn(
638
- "SqliteConversationStore.export",
639
- )(function* (request: ConversationExportRequest) {
640
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationExportRequest))(
641
- request,
642
- ).pipe(Effect.mapError((error) => schemaStoreError("validate conversation export", error)));
643
- yield* requireConversation(journal, validated.conversationId);
644
- const exported = yield* journal
645
- .exportConversation(validated.conversationId)
646
- .pipe(Effect.mapError((error) => storeError("export conversation", error)));
647
- const records = yield* Effect.forEach(exported.records, decodeEnvelope);
648
- if (records.length > 65_536) {
649
- return yield* ConversationStoreError.make({
650
- operation: "decode conversation export",
651
- message: "The conversation exceeds the current export record limit.",
622
+ const exportThread: ThreadStore["Service"]["export"] = Effect.fn("SqliteThreadStore.export")(
623
+ function* (request: ThreadExportRequest) {
624
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(
625
+ request,
626
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
627
+ yield* requireThread(journal, validated.threadId);
628
+ const exported = yield* journal
629
+ .exportThread(validated.threadId)
630
+ .pipe(Effect.mapError((error) => storeError("export thread", error)));
631
+ const records = yield* Effect.forEach(exported.records, decodeEnvelope);
632
+ if (records.length > 65_536) {
633
+ return yield* ThreadStoreError.make({
634
+ operation: "decode thread export",
635
+ message: "The thread exceeds the current export record limit.",
636
+ });
637
+ }
638
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
639
+ exported.thread.tail_digest,
640
+ ).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
641
+ return ThreadExport.make({
642
+ format: "effect-agent/thread@1",
643
+ threadId: validated.threadId,
644
+ tailSequence: exported.thread.tail_sequence,
645
+ tailDigest,
646
+ records,
652
647
  });
653
- }
654
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
655
- exported.conversation.tail_digest,
656
- ).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
657
- return ConversationExport.make({
658
- format: "effect-agent/conversation@1",
659
- conversationId: validated.conversationId,
660
- tailSequence: exported.conversation.tail_sequence,
661
- tailDigest,
662
- records,
663
- });
664
- });
648
+ },
649
+ );
665
650
 
666
- const inspectTail: ConversationStore["Service"]["inspectTail"] = Effect.fn(
667
- "SqliteConversationStore.inspectTail",
668
- )(function* (request: ConversationTailRequest) {
669
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(
651
+ const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fn(
652
+ "SqliteThreadStore.inspectTail",
653
+ )(function* (request: ThreadTailRequest) {
654
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(
670
655
  request,
671
656
  ).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
672
- const conversation = yield* requireConversation(journal, validated.conversationId);
673
- const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(
657
+ const thread = yield* requireThread(journal, validated.threadId);
658
+ const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(
674
659
  Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
675
660
  );
676
- return ConversationTail.make({
677
- conversationId: validated.conversationId,
678
- tailSequence: conversation.tail_sequence,
661
+ return ThreadTail.make({
662
+ threadId: validated.threadId,
663
+ tailSequence: thread.tail_sequence,
679
664
  tailDigest,
680
- producerEpoch: conversation.producer_epoch,
665
+ producerEpoch: thread.producer_epoch,
681
666
  });
682
667
  });
683
668
 
684
- const saveCheckpoint: ConversationStore["Service"]["saveCheckpoint"] = Effect.fn(
685
- "SqliteConversationStore.saveCheckpoint",
686
- )(function* (request: SaveCheckpointRequest) {
687
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(
688
- request,
689
- ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
690
- const conversation = yield* requireConversation(journal, validated.checkpoint.conversationId);
691
- if (validated.checkpoint.throughSequence > conversation.tail_sequence) {
692
- return yield* CheckpointRejected.make({
693
- conversationId: validated.checkpoint.conversationId,
694
- reason: "ahead-of-tail",
695
- });
696
- }
697
- const canonicalDigest = yield* tailDigestAt(
698
- journal,
699
- validated.checkpoint.conversationId,
700
- validated.checkpoint.throughSequence,
701
- );
702
- if (canonicalDigest !== validated.checkpoint.tailDigest) {
703
- return yield* CheckpointRejected.make({
704
- conversationId: validated.checkpoint.conversationId,
705
- reason: "digest-mismatch",
669
+ const saveCheckpoint: ThreadCheckpoints["save"] = Effect.fn("SqliteThreadStore.saveCheckpoint")(
670
+ function* (request: SaveCheckpointRequest) {
671
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(
672
+ request,
673
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
674
+ const thread = yield* requireThread(journal, validated.checkpoint.threadId);
675
+ if (validated.checkpoint.throughSequence > thread.tail_sequence) {
676
+ return yield* CheckpointRejected.make({
677
+ threadId: validated.checkpoint.threadId,
678
+ reason: "ahead-of-tail",
679
+ });
680
+ }
681
+ const canonicalDigest = yield* tailDigestAt(
682
+ journal,
683
+ validated.checkpoint.threadId,
684
+ validated.checkpoint.throughSequence,
685
+ );
686
+ if (canonicalDigest !== validated.checkpoint.tailDigest) {
687
+ return yield* CheckpointRejected.make({
688
+ threadId: validated.checkpoint.threadId,
689
+ reason: "digest-mismatch",
690
+ });
691
+ }
692
+ const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
693
+ const raw = RawCheckpoint.make({
694
+ threadId: validated.checkpoint.threadId,
695
+ throughSequence: validated.checkpoint.throughSequence,
696
+ tailDigest: validated.checkpoint.tailDigest,
697
+ checkpointJson,
706
698
  });
707
- }
708
- const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
709
- const raw = RawCheckpoint.make({
710
- conversationId: validated.checkpoint.conversationId,
711
- throughSequence: validated.checkpoint.throughSequence,
712
- tailDigest: validated.checkpoint.tailDigest,
713
- checkpointJson,
714
- });
715
- yield* hitFailpoint("save-checkpoint:before");
716
- yield* journal.saveCheckpoint(raw).pipe(
717
- Effect.mapError((error) =>
718
- isSqliteCheckpointConflict(error)
719
- ? CheckpointRejected.make({
720
- conversationId: validated.checkpoint.conversationId,
721
- reason: "digest-mismatch",
722
- })
723
- : storeError("save checkpoint", error),
724
- ),
725
- );
726
- yield* hitFailpoint("save-checkpoint:after");
727
- });
699
+ yield* hitFailpoint("save-checkpoint:before");
700
+ yield* journal.saveCheckpoint(raw).pipe(
701
+ Effect.mapError((error) =>
702
+ isSqliteCheckpointConflict(error)
703
+ ? CheckpointRejected.make({
704
+ threadId: validated.checkpoint.threadId,
705
+ reason: "digest-mismatch",
706
+ })
707
+ : storeError("save checkpoint", error),
708
+ ),
709
+ );
710
+ yield* hitFailpoint("save-checkpoint:after");
711
+ },
712
+ );
728
713
 
729
- const loadCheckpoint: ConversationStore["Service"]["loadCheckpoint"] = Effect.fn(
730
- "SqliteConversationStore.loadCheckpoint",
731
- )(function* (request: LoadCheckpointRequest) {
732
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(
733
- request,
734
- ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
735
- const conversation = yield* requireConversation(journal, validated.conversationId);
736
- const rows = yield* journal
737
- .loadCheckpoint(
738
- validated.conversationId,
739
- validated.atOrBeforeSequence ?? conversation.tail_sequence,
740
- )
741
- .pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
742
- if (rows.length === 0) return Option.none();
743
- if (rows.length !== 1) {
744
- return yield* ConversationStoreError.make({
745
- operation: "load checkpoint",
746
- message: `Expected at most one checkpoint row but found ${rows.length}.`,
747
- });
748
- }
749
- const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
750
- const canonicalDigest = yield* tailDigestAt(
751
- journal,
752
- checkpoint.conversationId,
753
- checkpoint.throughSequence,
754
- );
755
- if (canonicalDigest !== checkpoint.tailDigest) {
756
- return yield* CheckpointRejected.make({
757
- conversationId: checkpoint.conversationId,
758
- reason: "digest-mismatch",
759
- });
760
- }
761
- return Option.some(checkpoint);
762
- });
714
+ const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fn("SqliteThreadStore.loadCheckpoint")(
715
+ function* (request: LoadCheckpointRequest) {
716
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(
717
+ request,
718
+ ).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
719
+ const thread = yield* requireThread(journal, validated.threadId);
720
+ const rows = yield* journal
721
+ .loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
722
+ .pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
723
+ if (rows.length === 0) return Option.none();
724
+ if (rows.length !== 1) {
725
+ return yield* ThreadStoreError.make({
726
+ operation: "load checkpoint",
727
+ message: `Expected at most one checkpoint row but found ${rows.length}.`,
728
+ });
729
+ }
730
+ const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
731
+ const canonicalDigest = yield* tailDigestAt(
732
+ journal,
733
+ checkpoint.threadId,
734
+ checkpoint.throughSequence,
735
+ );
736
+ if (canonicalDigest !== checkpoint.tailDigest) {
737
+ return yield* CheckpointRejected.make({
738
+ threadId: checkpoint.threadId,
739
+ reason: "digest-mismatch",
740
+ });
741
+ }
742
+ return Option.some(checkpoint);
743
+ },
744
+ );
763
745
 
764
- const conversationStore = ConversationStore.of({
746
+ const threadStore = ThreadStore.of({
765
747
  append,
766
- export: exportConversation,
748
+ export: exportThread,
767
749
  inspectTail,
768
- loadCheckpoint,
769
750
  materialize,
770
751
  observe,
771
752
  read,
772
- saveCheckpoint,
753
+ checkpoints: { save: saveCheckpoint, load: loadCheckpoint },
773
754
  });
774
755
 
775
- return Context.make(ConversationStore, conversationStore);
756
+ return Context.make(ThreadStore, threadStore);
776
757
  });
777
758
 
778
759
  /**
779
- * SQLite Conversation Store implementation with configuration, failpoint, SQL, and Crypto
760
+ * SQLite Thread Store implementation with configuration, failpoint, SQL, and Crypto
780
761
  * authority kept visible in its input channel.
781
762
  */
782
- export const conversationStoreLayer: Layer.Layer<
783
- ConversationStore,
763
+ export const threadStoreLayer: Layer.Layer<
764
+ ThreadStore,
784
765
  SqliteStorageInitializationError,
785
766
  SqliteStorageConfig | SqliteStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
786
767
  > = Layer.effectContext(makeServices());
787
768
 
788
769
  /**
789
770
  * Validated SQLite storage configuration Layer with the documented defaults applied. Shared
790
- * by the ConversationStore and SubmissionLedger convenience layers so their defaults cannot
771
+ * by the ThreadStore and SubmissionLedger convenience layers so their defaults cannot
791
772
  * drift.
792
773
  */
793
774
  export const storageConfigLayer = (
@@ -820,15 +801,15 @@ export const storageFailpointLayer = (
820
801
  : Layer.succeed(SqliteStorageFailpoint)({ hit: options.failpoint });
821
802
 
822
803
  /**
823
- * A composition-root convenience Layer for canonical Conversations. Durable accepted work is
804
+ * A composition-root convenience Layer for canonical Threads. Durable accepted work is
824
805
  * served by the separate SubmissionLedger port.
825
806
  */
826
807
  export const layer = (
827
808
  options: SqliteStorageOptions,
828
- ): Layer.Layer<ConversationStore, SqliteStorageInitializationError> =>
809
+ ): Layer.Layer<ThreadStore, SqliteStorageInitializationError> =>
829
810
  Layer.unwrap(
830
811
  Effect.map(SqliteStorageConfig, (config) =>
831
- conversationStoreLayer.pipe(
812
+ threadStoreLayer.pipe(
832
813
  Layer.provide(
833
814
  Layer.mergeAll(
834
815
  Layer.succeed(SqliteStorageConfig)(config),