@effect-agent/storage-cloudflare 0.1.0-beta.37 → 0.1.0-beta.39

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.
package/src/do-ledger.ts CHANGED
@@ -73,22 +73,22 @@ import {
73
73
  submissionAbortRecordId,
74
74
  type ChildSettledOutcome,
75
75
  type SuspensionOutcome,
76
- } from "@effect-agent/session";
76
+ } from "@effect-agent/thread";
77
77
  import { BrowserCrypto } from "@effect/platform-browser";
78
78
  import { SqliteClient } from "@effect/sql-sqlite-do";
79
79
  import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
80
80
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
81
81
  import type { SqlError } from "effect/unstable/sql/SqlError";
82
82
 
83
+ import { decodeRows, initializeDoJournal } from "./do-journal.ts";
84
+ import { DoStorageConfig } from "./do-storage-config.ts";
85
+ import { DoStorageFailpoint } from "./do-storage-failpoint.ts";
83
86
  import {
84
87
  storageConfigLayer,
85
88
  storageFailpointLayer,
86
89
  type DoStorageInitializationError,
87
90
  type DoStorageOptions,
88
- } from "./do-conversation-store.ts";
89
- import { decodeRows, initializeDoJournal } from "./do-journal.ts";
90
- import { DoStorageConfig } from "./do-storage-config.ts";
91
- import { DoStorageFailpoint } from "./do-storage-failpoint.ts";
91
+ } from "./do-thread-store.ts";
92
92
  import {
93
93
  DoLedgerError,
94
94
  DoStorageCorruptionError,
@@ -117,7 +117,7 @@ const MAX_IDENTIFIER_LENGTH = 1_024;
117
117
 
118
118
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
119
119
  submission_id: BoundedIdentifier,
120
- conversation_id: BoundedIdentifier,
120
+ thread_id: BoundedIdentifier,
121
121
  queue_sequence: QueueSequence,
122
122
  principal: BoundedIdentifier,
123
123
  idempotency_key: BoundedIdentifier,
@@ -221,7 +221,7 @@ class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("Canonical
221
221
 
222
222
  const SUBMISSION_COLUMNS = `
223
223
  submission_id,
224
- conversation_id,
224
+ thread_id,
225
225
  queue_sequence,
226
226
  principal,
227
227
  idempotency_key,
@@ -260,7 +260,7 @@ const CHILD_RESERVATION_COLUMNS = `
260
260
  released_at
261
261
  `;
262
262
 
263
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
263
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
264
264
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
265
265
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
266
266
 
@@ -452,20 +452,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
452
452
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
453
453
  });
454
454
 
455
- const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (
455
+ const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (
456
456
  operation: string,
457
- conversationId: string,
457
+ threadId: string,
458
458
  ): Effect.fn.Return<ProducerEpoch, LedgerError> {
459
- const conversations = yield* journal
460
- .getConversation(conversationId)
459
+ const threads = yield* journal
460
+ .getThread(threadId)
461
461
  .pipe(Effect.mapError(internalFailure(operation)));
462
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
462
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
463
463
  });
464
464
 
465
465
  /**
466
466
  * Verify inside the surrounding write transaction that the presented token still owns the
467
467
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
468
- * Conversation's current producer epoch (DUR-006).
468
+ * Thread's current producer epoch (DUR-006).
469
469
  */
470
470
  const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (
471
471
  operation: string,
@@ -474,7 +474,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
474
474
  ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
475
475
  const ownership = yield* readOwnership(operation, submission.submission_id);
476
476
  if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
477
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
477
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
478
478
  const submissionId = yield* Schema.decodeUnknownEffect(
479
479
  SubmissionSnapshot.fields.submissionId,
480
480
  )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
@@ -518,7 +518,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
518
518
  }
519
519
  return yield* decodeSubmissionSnapshotUnknown({
520
520
  submissionId: row.submission_id,
521
- conversationId: row.conversation_id,
521
+ threadId: row.thread_id,
522
522
  queueSequence: row.queue_sequence,
523
523
  principal: row.principal,
524
524
  idempotencyKey: row.idempotency_key,
@@ -889,20 +889,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
889
889
  */
890
890
  const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
891
891
  operation: string,
892
- conversationId: string,
892
+ threadId: string,
893
893
  submissionId: SubmissionId,
894
894
  ): Effect.fn.Return<string | undefined, LedgerError> {
895
895
  const recordId = submissionAbortRecordId(submissionId);
896
896
  const rows = yield* sql<Record<string, unknown>>`
897
897
  SELECT record_id
898
898
  FROM effect_agent_canonical_records
899
- WHERE conversation_id = ${conversationId}
899
+ WHERE thread_id = ${threadId}
900
900
  AND record_id = ${recordId}
901
901
  `.pipe(Effect.mapError(sqlFailure(operation)));
902
902
  const decoded = yield* decodeRows(
903
903
  Schema.Array(CanonicalRecordIdRow),
904
904
  "effect_agent_canonical_records",
905
- `${conversationId}/${recordId}`,
905
+ `${threadId}/${recordId}`,
906
906
  rows,
907
907
  ).pipe(Effect.mapError(internalFailure(operation)));
908
908
  return decoded.length === 0 ? undefined : recordId;
@@ -916,7 +916,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
916
916
  ): Effect.fn.Return<AbortIntent, LedgerError> {
917
917
  const canonicalRecordId = yield* canonicalAbortRecordId(
918
918
  operation,
919
- submission.conversation_id,
919
+ submission.thread_id,
920
920
  submissionId,
921
921
  );
922
922
  return yield* decodeAbortIntent({
@@ -961,17 +961,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
961
961
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
962
962
  Effect.mapError(internalFailure(operation)),
963
963
  );
964
- // Routable Submission identity (D-P6-5): `{uuidv7}:{conversationId}`. The cross-DO
964
+ // Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
965
965
  // routing layer parses ITS OWN minted format (split at the first ":") to address
966
- // submissionId-only operations to the owning Conversation Object; the id stays opaque
966
+ // submissionId-only operations to the owning Thread Object; the id stays opaque
967
967
  // to every other component, exactly like DN's `submission-{uuid}` prefix.
968
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
968
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
969
969
  if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
970
970
  return yield* LedgerError.make({
971
971
  operation,
972
972
  message:
973
973
  `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
974
- `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`,
974
+ `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
975
975
  });
976
976
  }
977
977
  const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
@@ -979,11 +979,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
979
979
  const result = yield* inWriteTransaction(
980
980
  operation,
981
981
  Effect.gen(function* () {
982
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
982
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
983
983
  const existingRows = yield* sql<Record<string, unknown>>`
984
984
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
985
985
  FROM effect_agent_submissions
986
- WHERE conversation_id = ${validated.conversationId}
986
+ WHERE thread_id = ${validated.threadId}
987
987
  AND principal = ${validated.principal}
988
988
  AND idempotency_key = ${validated.idempotencyKey}
989
989
  `.pipe(Effect.mapError(sqlFailure(operation)));
@@ -1007,7 +1007,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1007
1007
  existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1008
1008
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1009
1009
  return yield* AdmissionConflict.make({
1010
- conversationId: validated.conversationId,
1010
+ threadId: validated.threadId,
1011
1011
  principal: validated.principal,
1012
1012
  idempotencyKey: validated.idempotencyKey,
1013
1013
  existingInputDigest: existing[0].input_digest,
@@ -1026,12 +1026,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1026
1026
  const maxRows = yield* sql<Record<string, unknown>>`
1027
1027
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1028
1028
  FROM effect_agent_submissions
1029
- WHERE conversation_id = ${validated.conversationId}
1029
+ WHERE thread_id = ${validated.threadId}
1030
1030
  `.pipe(Effect.mapError(sqlFailure(operation)));
1031
1031
  const decodedMax = yield* decodeRows(
1032
1032
  Schema.Array(MaxQueueSequenceRow),
1033
1033
  "effect_agent_submissions",
1034
- validated.conversationId,
1034
+ validated.threadId,
1035
1035
  maxRows,
1036
1036
  ).pipe(Effect.mapError(internalFailure(operation)));
1037
1037
  const queueSequence = yield* decodeQueueSequence(
@@ -1042,7 +1042,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1042
1042
  yield* sql`
1043
1043
  INSERT INTO effect_agent_submissions (
1044
1044
  submission_id,
1045
- conversation_id,
1045
+ thread_id,
1046
1046
  queue_sequence,
1047
1047
  principal,
1048
1048
  idempotency_key,
@@ -1058,7 +1058,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1058
1058
  parent_tool_call_id
1059
1059
  ) VALUES (
1060
1060
  ${mintedSubmissionId},
1061
- ${validated.conversationId},
1061
+ ${validated.threadId},
1062
1062
  ${queueSequence},
1063
1063
  ${validated.principal},
1064
1064
  ${validated.idempotencyKey},
@@ -1127,20 +1127,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1127
1127
  const rows = yield* sql<Record<string, unknown>>`
1128
1128
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1129
1129
  FROM effect_agent_submissions
1130
- WHERE conversation_id = ${validated.conversationId}
1130
+ WHERE thread_id = ${validated.threadId}
1131
1131
  AND principal = ${validated.principal}
1132
1132
  AND idempotency_key = ${validated.idempotencyKey}
1133
1133
  `.pipe(Effect.mapError(sqlFailure(operation)));
1134
1134
  const decoded = yield* decodeSubmissionRows(
1135
1135
  operation,
1136
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1136
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1137
1137
  rows,
1138
1138
  );
1139
1139
  if (decoded.length > 1) {
1140
1140
  return yield* corruptionFailure(
1141
1141
  operation,
1142
1142
  "effect_agent_submissions",
1143
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1143
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1144
1144
  "An admission idempotency key returned more than one row.",
1145
1145
  );
1146
1146
  }
@@ -1149,7 +1149,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1149
1149
  },
1150
1150
  );
1151
1151
 
1152
- // This LOCAL facet is the authoritative owner of every Conversation stored in this Durable
1152
+ // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1153
1153
  // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1154
1154
  // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1155
1155
  // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
@@ -1163,20 +1163,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1163
1163
  const rows = yield* sql<Record<string, unknown>>`
1164
1164
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1165
1165
  FROM effect_agent_submissions
1166
- WHERE conversation_id = ${validated.conversationId}
1166
+ WHERE thread_id = ${validated.threadId}
1167
1167
  AND principal = ${validated.principal}
1168
1168
  AND idempotency_key = ${validated.idempotencyKey}
1169
1169
  `.pipe(Effect.mapError(sqlFailure(operation)));
1170
1170
  const decoded = yield* decodeSubmissionRows(
1171
1171
  operation,
1172
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1172
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1173
1173
  rows,
1174
1174
  );
1175
1175
  if (decoded.length > 1) {
1176
1176
  return yield* corruptionFailure(
1177
1177
  operation,
1178
1178
  "effect_agent_submissions",
1179
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1179
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1180
1180
  "An admission idempotency key returned more than one row.",
1181
1181
  );
1182
1182
  }
@@ -1201,12 +1201,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1201
1201
  const headRows = yield* sql<Record<string, unknown>>`
1202
1202
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1203
1203
  FROM effect_agent_submissions
1204
- WHERE conversation_id = ${validated.conversationId}
1204
+ WHERE thread_id = ${validated.threadId}
1205
1205
  AND state <> 'settled'
1206
1206
  ORDER BY queue_sequence ASC
1207
1207
  LIMIT 1
1208
1208
  `.pipe(Effect.mapError(sqlFailure(operation)));
1209
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1209
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1210
1210
  if (heads.length === 0) return Option.none<Claim>();
1211
1211
  const head = heads[0];
1212
1212
 
@@ -1236,25 +1236,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1236
1236
  if (expiresAt > now.millis) return Option.none<Claim>();
1237
1237
  }
1238
1238
 
1239
- // Bump the Conversation's producer epoch atomically with the claim so every stale
1240
- // Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
1239
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1240
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1241
1241
  // never materialized (eviction between admission and materialization) is created
1242
1242
  // here so recovery can claim first and re-materialize idempotently at this epoch.
1243
- const conversations = yield* journal
1244
- .getConversation(head.conversation_id)
1243
+ const threads = yield* journal
1244
+ .getThread(head.thread_id)
1245
1245
  .pipe(Effect.mapError(internalFailure(operation)));
1246
1246
  let producerEpoch: number;
1247
- if (conversations.length === 0) {
1247
+ if (threads.length === 0) {
1248
1248
  producerEpoch = 1;
1249
1249
  yield* sql`
1250
- INSERT INTO effect_agent_conversations (
1251
- conversation_id,
1250
+ INSERT INTO effect_agent_threads (
1251
+ thread_id,
1252
1252
  created_at,
1253
1253
  tail_sequence,
1254
1254
  tail_digest,
1255
1255
  producer_epoch
1256
1256
  ) VALUES (
1257
- ${head.conversation_id},
1257
+ ${head.thread_id},
1258
1258
  ${now.iso},
1259
1259
  0,
1260
1260
  ${EMPTY_TAIL_DIGEST},
@@ -1262,11 +1262,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1262
1262
  )
1263
1263
  `.pipe(Effect.mapError(sqlFailure(operation)));
1264
1264
  } else {
1265
- producerEpoch = conversations[0].producer_epoch + 1;
1265
+ producerEpoch = threads[0].producer_epoch + 1;
1266
1266
  yield* sql`
1267
- UPDATE effect_agent_conversations
1267
+ UPDATE effect_agent_threads
1268
1268
  SET producer_epoch = ${producerEpoch}
1269
- WHERE conversation_id = ${head.conversation_id}
1269
+ WHERE thread_id = ${head.thread_id}
1270
1270
  `.pipe(Effect.mapError(sqlFailure(operation)));
1271
1271
  }
1272
1272
 
@@ -1299,14 +1299,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1299
1299
  INSERT INTO effect_agent_attempts (
1300
1300
  attempt_id,
1301
1301
  submission_id,
1302
- conversation_id,
1302
+ thread_id,
1303
1303
  owner_producer_id,
1304
1304
  producer_epoch,
1305
1305
  claimed_at
1306
1306
  ) VALUES (
1307
1307
  ${attemptId},
1308
1308
  ${head.submission_id},
1309
- ${head.conversation_id},
1309
+ ${head.thread_id},
1310
1310
  ${validated.producerId},
1311
1311
  ${producerEpoch},
1312
1312
  ${now.iso}
@@ -1743,7 +1743,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1743
1743
  `.pipe(Effect.mapError(sqlFailure(operation)));
1744
1744
  const canonicalRecordId = yield* canonicalAbortRecordId(
1745
1745
  operation,
1746
- submission.conversation_id,
1746
+ submission.thread_id,
1747
1747
  validated.submissionId,
1748
1748
  );
1749
1749
  return yield* decodeAbortIntent({
@@ -1771,10 +1771,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1771
1771
  operation,
1772
1772
  Effect.gen(function* () {
1773
1773
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1774
- if (host.conversation_id !== validated.conversationId) {
1774
+ if (host.thread_id !== validated.threadId) {
1775
1775
  return yield* LedgerError.make({
1776
1776
  operation,
1777
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
1777
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1778
1778
  });
1779
1779
  }
1780
1780
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
@@ -1782,11 +1782,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1782
1782
  const laterRows = yield* sql<Record<string, unknown>>`
1783
1783
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1784
1784
  FROM effect_agent_submissions
1785
- WHERE conversation_id = ${validated.conversationId}
1785
+ WHERE thread_id = ${validated.threadId}
1786
1786
  AND queue_sequence > ${host.queue_sequence}
1787
1787
  ORDER BY queue_sequence ASC
1788
1788
  `.pipe(Effect.mapError(sqlFailure(operation)));
1789
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
1789
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1790
1790
  const claimed: Array<JoiningClaim> = [];
1791
1791
  for (const row of later) {
1792
1792
  if (claimed.length >= validated.maxCount) break;
@@ -1956,7 +1956,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1956
1956
  // A covering event that raced ahead of the suspend transaction resumes the caller
1957
1957
  // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
1958
1958
  // covering evidence is EITHER a locally settled child row OR a durable cross-store
1959
- // notification marker: parent and child Conversations live in different Durable
1959
+ // notification marker: parent and child Threads live in different Durable
1960
1960
  // Objects, and the port contract requires that a child settlement reported (via
1961
1961
  // `recordChildSettled` → marker) before this suspend commits is observed here.
1962
1962
  if (validated.reason._tag === "ApprovalPending") {
@@ -2667,7 +2667,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2667
2667
  });
2668
2668
 
2669
2669
  interface ScanCursor {
2670
- readonly conversationId: string;
2670
+ readonly threadId: string;
2671
2671
  readonly queueSequence: number;
2672
2672
  }
2673
2673
 
@@ -2684,7 +2684,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2684
2684
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2685
2685
  FROM effect_agent_submissions
2686
2686
  WHERE state <> 'settled'
2687
- ORDER BY conversation_id ASC, queue_sequence ASC
2687
+ ORDER BY thread_id ASC, queue_sequence ASC
2688
2688
  LIMIT ${SCAN_PAGE_SIZE}
2689
2689
  `
2690
2690
  : sql<Record<string, unknown>>`
@@ -2692,13 +2692,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2692
2692
  FROM effect_agent_submissions
2693
2693
  WHERE state <> 'settled'
2694
2694
  AND (
2695
- conversation_id > ${cursor.conversationId}
2695
+ thread_id > ${cursor.threadId}
2696
2696
  OR (
2697
- conversation_id = ${cursor.conversationId}
2697
+ thread_id = ${cursor.threadId}
2698
2698
  AND queue_sequence > ${cursor.queueSequence}
2699
2699
  )
2700
2700
  )
2701
- ORDER BY conversation_id ASC, queue_sequence ASC
2701
+ ORDER BY thread_id ASC, queue_sequence ASC
2702
2702
  LIMIT ${SCAN_PAGE_SIZE}
2703
2703
  `
2704
2704
  ).pipe(Effect.mapError(sqlFailure(operation)));
@@ -2711,7 +2711,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2711
2711
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2712
2712
  ? Option.none()
2713
2713
  : Option.some({
2714
- conversationId: last.conversation_id,
2714
+ threadId: last.thread_id,
2715
2715
  queueSequence: last.queue_sequence,
2716
2716
  });
2717
2717
  return [snapshots, next] as const;
@@ -2987,7 +2987,7 @@ export const submissionLedgerLayer: Layer.Layer<
2987
2987
 
2988
2988
  /**
2989
2989
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
2990
- * same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
2990
+ * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
2991
2991
  */
2992
2992
  export const ledgerLayer = (
2993
2993
  options: DoStorageOptions,
@@ -19,11 +19,11 @@ import {
19
19
  ScheduleRecord,
20
20
  ScheduleStorageError,
21
21
  ScheduleStore,
22
- } from "@effect-agent/session";
22
+ } from "@effect-agent/thread";
23
23
  import { Context, Effect, Layer, Result, Schema } from "effect";
24
24
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
25
25
 
26
- const CURRENT_SCHEDULE_STORE_VERSION = 1;
26
+ const CURRENT_SCHEDULE_STORE_VERSION = 2;
27
27
  const MAX_STORED_SCHEDULE_BYTES = 1_900_000;
28
28
 
29
29
  const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(MAX_STORED_SCHEDULE_BYTES));
@@ -226,7 +226,11 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
226
226
  `.pipe(Effect.mapError(() => unavailable(operation)));
227
227
  const state = yield* decodeRows(Schema.Array(ScheduleStoreStateRow), rawState, operation);
228
228
  if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) {
229
- return yield* corrupt(operation);
229
+ return yield* corrupt(
230
+ state.length === 1
231
+ ? `${operation}: incompatible storage version ${state[0].storage_version}; expected ${CURRENT_SCHEDULE_STORE_VERSION}`
232
+ : `${operation}: invalid storage version row`,
233
+ );
230
234
  }
231
235
  });
232
236
 
@@ -0,0 +1,73 @@
1
+ import { Context, Effect, Layer, Ref } from "effect";
2
+
3
+ import { DoStorageFailpoint, type DoStorageFailpointHandler } from "./do-storage-failpoint.ts";
4
+ import type { DoStorageFailpointLocation } from "./errors.ts";
5
+
6
+ const noFailpoint: DoStorageFailpointHandler = () => Effect.void;
7
+
8
+ /** Test-only control for replacing the active Durable Object failpoint handler. */
9
+ export class DoStorageFailpointTestControl extends Context.Service<
10
+ DoStorageFailpointTestControl,
11
+ {
12
+ readonly clear: Effect.Effect<void>;
13
+ readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;
14
+ }
15
+ >()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {
16
+ /** Reusable test Layer with a control service backed by the same handler Ref. */
17
+ static readonly layer = Layer.effectContext(
18
+ Effect.gen(function* () {
19
+ const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);
20
+ return Context.make(
21
+ DoStorageFailpoint,
22
+ DoStorageFailpoint.of({
23
+ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),
24
+ }),
25
+ ).pipe(
26
+ Context.add(
27
+ DoStorageFailpointTestControl,
28
+ DoStorageFailpointTestControl.of({
29
+ clear: Ref.set(handler, noFailpoint),
30
+ setHandler: (next) => Ref.set(handler, next),
31
+ }),
32
+ ),
33
+ );
34
+ }),
35
+ );
36
+ }
37
+
38
+ /**
39
+ * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
40
+ * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
41
+ * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
42
+ * throws while destroying the in-memory instance and every in-flight implicit or explicit
43
+ * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
44
+ * unannounced platform eviction; DO storage — the only correctness-critical state — survives
45
+ * for the next incarnation, which the persisted alarm wakes without any incoming request.
46
+ *
47
+ * The handles stay injected: this package never imports `cloudflare:workers`, so the harness
48
+ * that owns a `DurableObjectState` supplies the thunk.
49
+ */
50
+ export const evictionFailpointHandler =
51
+ (options: {
52
+ readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;
53
+ /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform
54
+ * declares `abort` as returning, but it throws while destroying the instance. */
55
+ readonly evict: (location: DoStorageFailpointLocation) => void;
56
+ }): DoStorageFailpointHandler =>
57
+ (location) =>
58
+ options.isArmed(location).pipe(
59
+ Effect.flatMap((armed) =>
60
+ armed
61
+ ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a
62
+ // defect in the (already dying) incarnation. The defensive throw below keeps the
63
+ // guarantee — nothing after an armed hit may observe in-memory state — even if a
64
+ // harness supplies an evict thunk that returns.
65
+ Effect.sync((): never => {
66
+ options.evict(location);
67
+ throw new Error(
68
+ `Durable Object eviction did not interrupt execution at ${location}.`,
69
+ );
70
+ })
71
+ : Effect.void,
72
+ ),
73
+ );
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Layer, Ref } from "effect";
1
+ import { Context, Effect, Layer } from "effect";
2
2
 
3
3
  import type { DoStorageFailpointError, DoStorageFailpointLocation } from "./errors.ts";
4
4
 
@@ -8,15 +8,6 @@ export type DoStorageFailpointHandler = (
8
8
 
9
9
  const noFailpoint: DoStorageFailpointHandler = () => Effect.void;
10
10
 
11
- /** Test-only control for replacing the active Durable Object failpoint handler. */
12
- export class DoStorageFailpointTestControl extends Context.Service<
13
- DoStorageFailpointTestControl,
14
- {
15
- readonly clear: Effect.Effect<void>;
16
- readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;
17
- }
18
- >()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {}
19
-
20
11
  /** Explicit fault-injection authority used at Durable Object storage operation boundaries. */
21
12
  export class DoStorageFailpoint extends Context.Service<
22
13
  DoStorageFailpoint,
@@ -26,62 +17,4 @@ export class DoStorageFailpoint extends Context.Service<
26
17
  >()("@effect-agent/storage-cloudflare/DoStorageFailpoint") {
27
18
  /** Production default: no fault injection. */
28
19
  static readonly layer = Layer.succeed(this)({ hit: noFailpoint });
29
-
30
- /** Reusable test Layer with a control service backed by the same handler Ref. */
31
- static readonly layerTest = Layer.effectContext(
32
- Effect.gen(function* () {
33
- const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);
34
- return Context.make(
35
- DoStorageFailpoint,
36
- DoStorageFailpoint.of({
37
- hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),
38
- }),
39
- ).pipe(
40
- Context.add(
41
- DoStorageFailpointTestControl,
42
- DoStorageFailpointTestControl.of({
43
- clear: Ref.set(handler, noFailpoint),
44
- setHandler: (next) => Ref.set(handler, next),
45
- }),
46
- ),
47
- );
48
- }),
49
- );
50
20
  }
51
-
52
- /**
53
- * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
54
- * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
55
- * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
56
- * throws while destroying the in-memory instance and every in-flight implicit or explicit
57
- * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
58
- * unannounced platform eviction; DO storage — the only correctness-critical state — survives
59
- * for the next incarnation, which the persisted alarm wakes without any incoming request.
60
- *
61
- * The handles stay injected: this package never imports `cloudflare:workers`, so the harness
62
- * that owns a `DurableObjectState` supplies the thunk.
63
- */
64
- export const evictionFailpointHandler =
65
- (options: {
66
- readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;
67
- /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform
68
- * declares `abort` as returning, but it throws while destroying the instance. */
69
- readonly evict: (location: DoStorageFailpointLocation) => void;
70
- }): DoStorageFailpointHandler =>
71
- (location) =>
72
- options.isArmed(location).pipe(
73
- Effect.flatMap((armed) =>
74
- armed
75
- ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a
76
- // defect in the (already dying) incarnation. The defensive throw below keeps the
77
- // guarantee — nothing after an armed hit may observe in-memory state — even if a
78
- // harness supplies an evict thunk that returns.
79
- Effect.sync((): never => {
80
- options.evict(location);
81
- throw new Error(
82
- `Durable Object eviction did not interrupt execution at ${location}.`,
83
- );
84
- })
85
- : Effect.void,
86
- ),
87
- );