@yielded/agent-storage-cloudflare 0.1.0-beta.166 → 0.1.0-beta.168

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 (71) hide show
  1. package/dist/DoMemoryStore.mjs +1 -1
  2. package/dist/DoMessageDeliveryStore.d.mts +1 -1
  3. package/dist/DoMessageDeliveryStore.mjs +3 -2
  4. package/dist/DoMessageDeliveryStore.mjs.map +1 -1
  5. package/dist/DoScheduleStore.mjs +16 -37
  6. package/dist/DoScheduleStore.mjs.map +1 -1
  7. package/dist/DoStorageConfig.d.mts +2 -10
  8. package/dist/DoStorageConfig.mjs +2 -11
  9. package/dist/DoStorageConfig.mjs.map +1 -1
  10. package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
  11. package/dist/DoStorageError.d.mts +2 -2
  12. package/dist/DoStorageError.mjs +14 -30
  13. package/dist/DoStorageError.mjs.map +1 -1
  14. package/dist/DoStorageFailpoint.d.mts +1 -1
  15. package/dist/DoStorageFailpointTesting.d.mts +1 -1
  16. package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
  17. package/dist/DoStorageVersion.d.mts +1 -1
  18. package/dist/DoStorageVersion.mjs +2 -2
  19. package/dist/DoStorageVersion.mjs.map +1 -1
  20. package/dist/DoSubmissionLedger.d.mts +7 -5
  21. package/dist/DoSubmissionLedger.mjs +378 -528
  22. package/dist/DoSubmissionLedger.mjs.map +1 -1
  23. package/dist/DoSubscriptionStore.mjs +5 -24
  24. package/dist/DoSubscriptionStore.mjs.map +1 -1
  25. package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
  26. package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
  27. package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
  28. package/dist/DoThreadStore.d.mts +2 -2
  29. package/dist/DoThreadStore.mjs +4 -529
  30. package/dist/MemoryProtocol.mjs +3 -3
  31. package/dist/MemoryProtocol.mjs.map +1 -1
  32. package/dist/PortProtocol.d.mts +11727 -3410
  33. package/dist/PortProtocol.mjs +103 -15
  34. package/dist/PortProtocol.mjs.map +1 -1
  35. package/dist/PortRouting.d.mts +14 -9
  36. package/dist/PortRouting.mjs +155 -29
  37. package/dist/PortRouting.mjs.map +1 -1
  38. package/dist/do-journal-Cawnsetj.mjs +761 -0
  39. package/dist/do-journal-Cawnsetj.mjs.map +1 -0
  40. package/dist/index.d.mts +3 -3
  41. package/dist/index.mjs +1 -1
  42. package/dist/migrations-D_EP3KYf.mjs +148 -0
  43. package/dist/migrations-D_EP3KYf.mjs.map +1 -0
  44. package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
  45. package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
  46. package/package.json +1 -1
  47. package/src/DoMessageDeliveryStore.ts +2 -1
  48. package/src/DoScheduleStore.ts +19 -59
  49. package/src/DoStorageConfig.ts +2 -11
  50. package/src/DoStorageError.ts +3 -26
  51. package/src/DoStorageVersion.ts +1 -1
  52. package/src/DoSubmissionLedger.ts +1806 -2030
  53. package/src/DoSubscriptionStore.ts +5 -47
  54. package/src/DoThreadStore.ts +276 -622
  55. package/src/MemoryProtocol.ts +3 -3
  56. package/src/PortProtocol.ts +198 -12
  57. package/src/PortRouting.ts +527 -108
  58. package/src/internal/canonical-append.ts +15 -0
  59. package/src/internal/do-journal.ts +548 -1440
  60. package/src/internal/migrations.ts +184 -287
  61. package/src/internal/owned-state.ts +81 -30
  62. package/src/internal/prompt-checkpoint.ts +158 -0
  63. package/src/internal/storage-span.ts +2 -5
  64. package/dist/DoThreadStore.mjs.map +0 -1
  65. package/dist/do-journal-DUhjZkOf.mjs +0 -1352
  66. package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
  67. package/dist/migrations-DtKz_Gxr.mjs +0 -306
  68. package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
  69. package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
  70. package/src/internal/message-delivery-schema.ts +0 -24
  71. package/src/internal/recovery-checkpoint-schema.ts +0 -17
@@ -1,1352 +0,0 @@
1
- import { DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoStorageCompatibilityError, DoStorageCorruptionError, DoStorageError, DoValueBoundExceeded } from "./DoStorageError.mjs";
2
- import { a as createRecoveryCheckpointTable, i as doMigrations, n as createNonterminalIndex, o as createMessageDeliveryTables, r as createWorkerStops } from "./migrations-DtKz_Gxr.mjs";
3
- import { n as ownedRows, r as ownedState } from "./owned-state-DHm2aRXx.mjs";
4
- import { SqlLifecycleRetainer, SqlLifecycleSource, makeSqlLifecyclePublication } from "@yielded/agent-storage-sql/sql-lifecycle-publication";
5
- import { createMessageDeliveryPendingIndex } from "@yielded/agent-storage-sql/sql-message-delivery-store";
6
- import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
7
- import { Cause, Clock, Effect, Option, Predicate, Result, Schema, Stream } from "effect";
8
- import * as SqlClient$1 from "effect/sql/SqlClient";
9
- import { SqliteMigrator } from "@effect/sql-sqlite-do";
10
- import { SqlStorageProgress } from "@yielded/agent-storage-sql/sql-storage-progress";
11
- import { checkV2ThreadLayout } from "@yielded/agent-storage-sql/sql-storage-v2-upgrade";
12
- import { canonicalRecordOutstanding, createNativeReadIndexes, indexCanonicalRecord, seedNativeReadIndexes } from "@yielded/agent-storage-sql/sql-thread-native-reads";
13
- import { EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
14
- import { ThreadId } from "@yielded/agent/identifiers";
15
- import { LifecyclePublicationError, LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
16
- import { CanonicalRecord, CanonicalSequence, ProducerEpoch } from "@yielded/agent/records";
17
- import { CheckpointRejected, FenceRejected, MAX_THREAD_EXPORT_RECORDS, ThreadNotMaterialized, ThreadStoreDiagnostic } from "@yielded/agent/thread-store";
18
- import { SqlError } from "effect/sql/SqlError";
19
- //#region src/internal/storage-span.ts
20
- const isAppendContention = (error) => error._tag === "DoFenceRejected" || error._tag === "FenceRejected" || (error._tag === "DoAppendConflict" || error._tag === "AppendConflict") && error.reason === "tail";
21
- /** Expected refusals remain typed at the port, but are outcomes of the storage operation. */
22
- const storageResult = (effect, expected) => effect.pipe(Effect.map(Result.succeed), Effect.catchIf(expected, (error) => Effect.succeed(Result.fail(error))));
23
- /** Only error discriminants enter telemetry; messages, SQL, identities and payloads do not. */
24
- const annotateStorageError = (error) => Effect.annotateCurrentSpan({
25
- "error.type": error._tag,
26
- ...Predicate.hasProperty(error, "cause") && Predicate.hasProperty(error.cause, "_tag") && typeof error.cause._tag === "string" ? { "error.cause.type": error.cause._tag } : {}
27
- });
28
- const withStorageSpan = (name, expected) => (effect) => storageResult(effect, expected).pipe(Effect.tap((result) => Result.isFailure(result) ? Effect.annotateCurrentSpan({ "storage.outcome": result.failure._tag }) : Effect.void), Effect.tapError(annotateStorageError), Effect.withSpan(name), Effect.flatMap(Effect.fromResult));
29
- //#endregion
30
- //#region src/internal/do-journal.ts
31
- /**
32
- * Static schema ceiling for stored text columns. Writes are bounded in BYTES by the
33
- * configured `maxStoredValueBytes` (always ≤ 2,000,000); UTF-8 byte length is never smaller
34
- * than UTF-16 string length, so any value that passed the byte bound also passes this
35
- * decode-side character ceiling.
36
- */
37
- const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2e6));
38
- const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
39
- const MAX_RECORDS_PER_THREAD = MAX_THREAD_EXPORT_RECORDS;
40
- const ZERO_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
41
- const MAX_IDENTIFIER_LENGTH = 1024;
42
- const MAX_READ_PAGE_JSON_BYTES = 4194304;
43
- const MAX_RECORD_CACHE_JSON_BYTES = 8388608;
44
- const MAX_RECORD_CACHE_ENTRIES = 4096;
45
- /** Durable Object SQL storage allows at most 100 bound parameters per statement. */
46
- const MAX_BOUND_PARAMETERS = 100;
47
- const isSqlError = Schema.is(SqlError);
48
- const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
49
- const chunked = (values, size) => {
50
- const chunks = [];
51
- for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
52
- return chunks;
53
- };
54
- var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
55
- var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier }) {};
56
- var ThreadRow = class extends Schema.Class("ThreadRow")({
57
- thread_id: BoundedIdentifier,
58
- created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
59
- producer_epoch: ProducerEpoch,
60
- tail_digest: BoundedStoredText,
61
- tail_sequence: CanonicalSequence
62
- }) {};
63
- const threadRows = ownedRows(ThreadRow, "effect_agent_threads", (row) => row.thread_id, "thread_id");
64
- var BatchRow = class extends Schema.Class("BatchRow")({
65
- batch_digest: BoundedStoredText,
66
- batch_id: BoundedIdentifier,
67
- batch_json: BoundedStoredText,
68
- thread_id: BoundedIdentifier,
69
- first_sequence: CanonicalSequence,
70
- last_sequence: CanonicalSequence,
71
- tail_digest: BoundedStoredText
72
- }) {};
73
- var RecordRow = class extends Schema.Class("RecordRow")({
74
- batch_id: BoundedIdentifier,
75
- thread_id: BoundedIdentifier,
76
- record_id: BoundedIdentifier,
77
- record_json: BoundedStoredText,
78
- sequence: CanonicalSequence
79
- }) {};
80
- const isolateRecords = /* @__PURE__ */ new Map();
81
- let isolateRecordBytes = 0;
82
- const releaseRecord = (entry) => {
83
- if (!isolateRecords.delete(entry)) return;
84
- isolateRecordBytes -= entry.bytes;
85
- entry.evict();
86
- };
87
- const recordCaches = /* @__PURE__ */ new WeakMap();
88
- const makeRecordCache = () => {
89
- const records = /* @__PURE__ */ new Map();
90
- const key = (thread, sequence) => JSON.stringify([thread, sequence]);
91
- return {
92
- clear: () => {
93
- for (const entry of records.values()) releaseRecord(entry);
94
- },
95
- get: (thread, sequence) => records.get(key(thread, sequence))?.row,
96
- prefix: (thread, through) => {
97
- const prefix = [];
98
- for (let sequence = 1; sequence <= through; sequence++) {
99
- const row = records.get(key(thread, sequence));
100
- if (row === void 0) return void 0;
101
- prefix.push(row.row);
102
- }
103
- return prefix;
104
- },
105
- put: (row) => {
106
- const id = key(row.thread_id, row.sequence);
107
- const prior = records.get(id);
108
- if (prior?.row === row) return;
109
- const size = storedTextBytes(row.record_json);
110
- if (prior !== void 0) releaseRecord(prior);
111
- const entry = {
112
- row,
113
- bytes: size,
114
- evict: () => records.delete(id)
115
- };
116
- records.set(id, entry);
117
- isolateRecords.set(entry, true);
118
- isolateRecordBytes += size;
119
- while (isolateRecordBytes > MAX_RECORD_CACHE_JSON_BYTES || isolateRecords.size > MAX_RECORD_CACHE_ENTRIES) {
120
- const oldest = isolateRecords.keys().next().value;
121
- if (oldest === void 0) break;
122
- releaseRecord(oldest);
123
- }
124
- }
125
- };
126
- };
127
- const ReadPlanRow = Schema.Struct({
128
- sequence: CanonicalSequence,
129
- record_json_bytes: Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_READ_PAGE_JSON_BYTES))
130
- });
131
- var CheckpointRow = class extends Schema.Class("CheckpointRow")({
132
- checkpoint_json: BoundedStoredText,
133
- thread_id: BoundedIdentifier,
134
- tail_digest: BoundedStoredText,
135
- through_sequence: CanonicalSequence
136
- }) {};
137
- const recoveryRows = ownedRows(CheckpointRow, "effect_agent_recovery_checkpoints", (row) => row.thread_id, "thread_id");
138
- const LifecycleCursorRow = Schema.Struct({
139
- thread_id: BoundedIdentifier,
140
- through_sequence: CanonicalSequence
141
- });
142
- const lifecycleCursorRows = ownedRows(LifecycleCursorRow, "effect_agent_lifecycle_cursors", (row) => row.thread_id, "thread_id");
143
- const initializedLifecycleSources = /* @__PURE__ */ new WeakSet();
144
- var RawRecord = class extends Schema.Class("@effect-agent/storage-cloudflare/RawRecord")({
145
- recordId: BoundedIdentifier,
146
- recordJson: BoundedStoredText
147
- }) {};
148
- var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendRequest")({
149
- batchDigest: BoundedStoredText,
150
- batchId: BoundedIdentifier,
151
- batchJson: BoundedStoredText,
152
- threadId: BoundedIdentifier,
153
- expectedTailDigest: BoundedStoredText,
154
- expectedTailSequence: CanonicalSequence,
155
- producerEpoch: ProducerEpoch,
156
- records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
157
- tailDigest: BoundedStoredText
158
- }) {};
159
- var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendResult")({
160
- firstSequence: CanonicalSequence,
161
- lastSequence: CanonicalSequence,
162
- replayed: Schema.Boolean,
163
- tailDigest: BoundedStoredText
164
- }) {};
165
- var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
166
- threadId: BoundedIdentifier,
167
- fromSequenceExclusive: CanonicalSequence,
168
- limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
169
- }) {};
170
- var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
171
- checkpointJson: BoundedStoredText,
172
- threadId: BoundedIdentifier,
173
- tailDigest: BoundedStoredText,
174
- throughSequence: CanonicalSequence
175
- }) {};
176
- var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
177
- thread: ThreadRow,
178
- records: Schema.Array(RecordRow)
179
- }) {};
180
- const noFailpoint = () => Effect.void;
181
- const storageError = (operation) => (error) => DoStorageError.make({
182
- cause: error,
183
- operation,
184
- message: error.message,
185
- diagnostic: ThreadStoreDiagnostic.make({
186
- causeTag: error._tag,
187
- operation,
188
- issueTag: error.reason._tag
189
- })
190
- });
191
- /** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
192
- const decodeRows = Effect.fn((schema, table, rowKey, rows, decoder = table) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
193
- table,
194
- rowKey,
195
- message: "Stored rows do not satisfy the storage schema",
196
- diagnostic: ThreadStoreDiagnostic.make({
197
- causeTag: error._tag,
198
- operation: "decode storage rows",
199
- decoder,
200
- issueTag: error.issue._tag
201
- })
202
- })), Effect.tapError((error) => Effect.annotateCurrentSpan({
203
- "storage.failure.operation": error.diagnostic?.operation,
204
- "storage.failure.cause": error.diagnostic?.causeTag,
205
- "storage.failure.decoder": decoder,
206
- "storage.failure.issue": error.diagnostic?.issueTag
207
- }))));
208
- /** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
209
- const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
210
- table,
211
- rowKey,
212
- message: `Expected exactly one row but found ${decoded.length}.`
213
- })))));
214
- /** Column inventory of the supported v3 predecessor, independent of physical column order. */
215
- const predecessorColumns = {
216
- effect_agent_threads: [
217
- "thread_id",
218
- "created_at",
219
- "tail_sequence",
220
- "tail_digest",
221
- "producer_epoch"
222
- ],
223
- effect_agent_canonical_batches: [
224
- "thread_id",
225
- "batch_id",
226
- "first_sequence",
227
- "last_sequence",
228
- "batch_digest",
229
- "tail_digest",
230
- "batch_json"
231
- ],
232
- effect_agent_canonical_records: [
233
- "thread_id",
234
- "sequence",
235
- "record_id",
236
- "batch_id",
237
- "record_json"
238
- ],
239
- effect_agent_checkpoints: [
240
- "thread_id",
241
- "through_sequence",
242
- "tail_digest",
243
- "checkpoint_json"
244
- ],
245
- effect_agent_submissions: [
246
- "submission_id",
247
- "thread_id",
248
- "queue_sequence",
249
- "principal",
250
- "idempotency_key",
251
- "agent_id",
252
- "agent_digests_json",
253
- "deployment_id",
254
- "input_json",
255
- "input_digest",
256
- "receipt_id",
257
- "state",
258
- "settled_outcome",
259
- "created_at",
260
- "ready_at",
261
- "input_applied_record_id",
262
- "input_applied_sequence",
263
- "joined_host_submission_id",
264
- "suspended_reason_json",
265
- "suspended_at",
266
- "unknown_reason",
267
- "unknown_tool_call_ids_json",
268
- "parent_submission_id",
269
- "parent_tool_call_id",
270
- "admission_group",
271
- "admission_fence_json"
272
- ],
273
- effect_agent_submission_ownership: [
274
- "submission_id",
275
- "attempt_id",
276
- "ownership_token",
277
- "producer_epoch",
278
- "owner_producer_id",
279
- "lease_expires_at"
280
- ],
281
- effect_agent_attempts: [
282
- "attempt_id",
283
- "submission_id",
284
- "thread_id",
285
- "owner_producer_id",
286
- "producer_epoch",
287
- "claimed_at"
288
- ],
289
- effect_agent_settlement_reservations: [
290
- "submission_id",
291
- "settlement_id",
292
- "outcome",
293
- "record_id",
294
- "record_json",
295
- "record_digest",
296
- "reserved_at",
297
- "finalized_at"
298
- ],
299
- effect_agent_abort_intents: [
300
- "submission_id",
301
- "author",
302
- "reason",
303
- "requested_at",
304
- "canonical_record_id"
305
- ],
306
- effect_agent_approval_decisions: [
307
- "submission_id",
308
- "tool_call_id",
309
- "decision",
310
- "resolver",
311
- "reason",
312
- "decided_at"
313
- ],
314
- effect_agent_unknown_resolutions: [
315
- "submission_id",
316
- "tool_call_id",
317
- "author",
318
- "reason",
319
- "resolution_json",
320
- "resolved_at"
321
- ],
322
- effect_agent_child_reservations: [
323
- "reservation_id",
324
- "parent_submission_id",
325
- "parent_tool_call_id",
326
- "child_submission_id",
327
- "status",
328
- "allocation_json",
329
- "allocation_digest",
330
- "accounting_json",
331
- "reserved_at",
332
- "release_began_at",
333
- "released_at"
334
- ],
335
- effect_agent_meta: ["key", "value"],
336
- effect_agent_child_settlements: [
337
- "parent_submission_id",
338
- "child_submission_id",
339
- "child_outcome",
340
- "recorded_at"
341
- ]
342
- };
343
- const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* (version) {
344
- const sql = (yield* SqlClient$1.SqlClient).withoutTransforms();
345
- const expectedColumns = {
346
- ...version === 3 ? predecessorColumns : {
347
- ...predecessorColumns,
348
- effect_agent_submissions: [
349
- ...predecessorColumns.effect_agent_submissions,
350
- "worker_admission_json",
351
- "message_admission_json"
352
- ],
353
- effect_agent_message_deliveries: [
354
- "owner_thread_id",
355
- "message_id",
356
- "version",
357
- "state",
358
- "deadline_at_millis",
359
- "record_json"
360
- ]
361
- },
362
- ...version === 7 ? { effect_agent_canonical_records: [...predecessorColumns.effect_agent_canonical_records, "outstanding"] } : {},
363
- ...version >= 5 ? { effect_agent_recovery_checkpoints: [
364
- "thread_id",
365
- "through_sequence",
366
- "tail_digest",
367
- "checkpoint_json"
368
- ] } : {}
369
- };
370
- for (const [table, expected] of Object.entries(expectedColumns)) {
371
- const columns = yield* decodeRows(Schema.Array(Schema.Struct({ name: BoundedIdentifier })), table, "schema", yield* sql.unsafe(`PRAGMA table_info(${table})`));
372
- const names = new Set(expected);
373
- if (columns.length !== names.size || columns.some((column) => !names.has(column.name))) return yield* DoStorageCompatibilityError.make({
374
- actualVersion: version,
375
- supportedVersion: 9,
376
- message: `The v${version} ${table} columns do not match the supported predecessor; no upgrade was committed.`
377
- });
378
- }
379
- });
380
- const REQUIRED_TABLES = [
381
- "effect_agent_abort_intents",
382
- "effect_agent_approval_decisions",
383
- "effect_agent_attempts",
384
- "effect_agent_canonical_batches",
385
- "effect_agent_canonical_records",
386
- "effect_agent_checkpoints",
387
- "effect_agent_child_reservations",
388
- "effect_agent_child_settlements",
389
- "effect_agent_threads",
390
- "effect_agent_meta",
391
- "effect_agent_settlement_reservations",
392
- "effect_agent_submission_ownership",
393
- "effect_agent_submissions",
394
- "effect_agent_unknown_resolutions"
395
- ];
396
- /**
397
- * Supported-predecessor or fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
398
- * `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
399
- * regardless). No WAL check (Durable Object storage owns durability and confirms writes
400
- * through output gates) and no busy timeout (a Durable Object has exactly one writer): the
401
- * Node machinery those served has no DC analogue and is deliberately absent.
402
- */
403
- const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
404
- const verifyWorkerPredecessor = Effect.fnUntraced(function* (workerContract) {
405
- const requiredRows = yield* sql`
406
- SELECT name
407
- FROM sqlite_master
408
- WHERE (type = 'table'
409
- AND name IN ${sql.in([
410
- ...REQUIRED_TABLES,
411
- "effect_agent_message_deliveries",
412
- "effect_agent_recovery_checkpoints"
413
- ])}
414
- ) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
415
- OR (${workerContract ? 1 : 0} = 1 AND name IN ('effect_agent_worker_stops', 'effect_agent_worker_starts', 'effect_agent_worker_pending', 'effect_agent_worker_execution'))
416
- ORDER BY name
417
- `.pipe(Effect.mapError(storageError("verify storage tables")));
418
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length + 9 + (workerContract ? 4 : 0)) return yield* DoStorageCompatibilityError.make({
419
- actualVersion: 9,
420
- supportedVersion: 9,
421
- message: "The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection."
422
- });
423
- });
424
- const metaTableRows = yield* sql`
425
- SELECT name
426
- FROM sqlite_master
427
- WHERE type = 'table'
428
- AND name = 'effect_agent_meta'
429
- `.pipe(Effect.mapError(storageError("read storage version table")));
430
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
431
- const existingRows = yield* sql`
432
- SELECT name
433
- FROM sqlite_master
434
- WHERE type = 'table'
435
- AND name LIKE 'effect_agent_%'
436
- ORDER BY name
437
- `.pipe(Effect.mapError(storageError("inspect unversioned storage")));
438
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
439
- actualVersion: 0,
440
- supportedVersion: 9,
441
- message: "The Durable Object contains unversioned Effect Agent tables. Refusing to mutate ambiguous stored data; retain it for inspection with its original writer."
442
- });
443
- yield* SqliteMigrator.run({
444
- loader: doMigrations,
445
- table: "effect_sql_migrations_agent_threads"
446
- }).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
447
- cause: error,
448
- operation: "initialize current storage",
449
- message: error.message
450
- })));
451
- } else {
452
- const versionRows = yield* sql`
453
- SELECT value
454
- FROM effect_agent_meta
455
- WHERE key = 'storage_version'
456
- `.pipe(Effect.mapError(storageError("read storage version")));
457
- const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
458
- if (version.value === "2" || version.value === "3" || version.value === "4" || version.value === "5") yield* sql.withTransaction(Effect.gen(function* () {
459
- const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key='storage_version'`;
460
- if (current.length === 1 && current[0].value === String(9)) return;
461
- if (current.length !== 1 || current[0].value !== "2" && current[0].value !== "3" && current[0].value !== "4" && current[0].value !== "5") return yield* DoStorageCompatibilityError.make({
462
- actualVersion: -1,
463
- supportedVersion: 9,
464
- message: "Storage version changed while acquiring the upgrade transaction."
465
- });
466
- if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name IN ${sql.in([...REQUIRED_TABLES])}`)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
467
- actualVersion: Number(current[0].value),
468
- supportedVersion: 9,
469
- message: "The predecessor store is missing required tables. Retain the original store for inspection; no upgrade was committed."
470
- });
471
- if ((yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name='effect_agent_recovery_checkpoints'`).length !== (current[0].value === "5" ? 1 : 0)) return yield* DoStorageCompatibilityError.make({
472
- actualVersion: Number(current[0].value),
473
- supportedVersion: 9,
474
- message: "The predecessor recovery checkpoint storage does not match its version; refusing ambiguous data without mutation."
475
- });
476
- if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_submissions_nonterminal'`).length !== 0) return yield* DoStorageCompatibilityError.make({
477
- actualVersion: Number(current[0].value),
478
- supportedVersion: 9,
479
- message: "The predecessor already contains the nonterminal index; refusing ambiguous storage without mutation."
480
- });
481
- if (current[0].value === "2") {
482
- yield* checkV2ThreadLayout();
483
- for (const statement of [
484
- sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_group TEXT`,
485
- sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_fence_json TEXT`,
486
- sql`CREATE INDEX effect_agent_submissions_group ON effect_agent_submissions (thread_id, admission_group, state)`
487
- ]) {
488
- yield* failpoint("upgrade:before-mutation");
489
- yield* statement;
490
- yield* failpoint("upgrade:after-mutation");
491
- }
492
- }
493
- if (current[0].value === "3") yield* checkPredecessorLayout(3);
494
- if (current[0].value === "4") yield* checkPredecessorLayout(4);
495
- if (current[0].value === "5") yield* checkPredecessorLayout(5);
496
- if (current[0].value === "2" || current[0].value === "3") {
497
- yield* failpoint("upgrade:before-mutation");
498
- yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
499
- yield* failpoint("upgrade:after-mutation");
500
- yield* failpoint("upgrade:before-mutation");
501
- yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN message_admission_json TEXT`;
502
- yield* failpoint("upgrade:after-mutation");
503
- yield* failpoint("upgrade:before-mutation");
504
- yield* createMessageDeliveryTables;
505
- yield* failpoint("upgrade:after-mutation");
506
- }
507
- if (current[0].value !== "5") {
508
- yield* failpoint("upgrade:before-mutation");
509
- yield* createRecoveryCheckpointTable;
510
- yield* failpoint("upgrade:after-mutation");
511
- }
512
- yield* failpoint("upgrade:before-mutation");
513
- yield* createNonterminalIndex;
514
- yield* failpoint("upgrade:after-mutation");
515
- yield* failpoint("upgrade:before-version");
516
- yield* createNativeReadIndexes();
517
- yield* createMessageDeliveryPendingIndex();
518
- yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
519
- table: "effect_agent_canonical_records",
520
- rowKey: "upgrade",
521
- message: error.message
522
- })));
523
- yield* createWorkerStops;
524
- yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
525
- yield* failpoint("upgrade:after-version");
526
- })).pipe(Effect.catchTag("DoStorageFailpointError", (error) => DoStorageError.make({
527
- cause: error,
528
- operation: "upgrade storage",
529
- message: error.message
530
- })), Effect.catchTag("StorageUpgradeError", (error) => DoStorageCorruptionError.make({
531
- table: error.table,
532
- rowKey: error.rowKey,
533
- message: error.message
534
- })), Effect.catchTag("SchemaError", (error) => DoStorageCorruptionError.make({
535
- table: "effect_agent_canonical_records",
536
- rowKey: "upgrade",
537
- message: error.message
538
- })), Effect.catchTag("SqlError", storageError("upgrade supported thread storage")));
539
- else if (version.value === "6") yield* sql.withTransaction(Effect.gen(function* () {
540
- const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
541
- if (current[0]?.value === "9") return;
542
- if (current[0]?.value !== "6") return yield* DoStorageCompatibilityError.make({
543
- actualVersion: -1,
544
- supportedVersion: 9,
545
- message: "Storage version changed during native index upgrade"
546
- });
547
- yield* checkPredecessorLayout(5);
548
- if ((yield* sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'effect_agent_submissions_nonterminal'`).length !== 1) return yield* DoStorageCompatibilityError.make({
549
- actualVersion: 6,
550
- supportedVersion: 9,
551
- message: "Predecessor storage is missing its required nonterminal index"
552
- });
553
- yield* failpoint("upgrade:before-mutation");
554
- yield* createNativeReadIndexes();
555
- yield* createMessageDeliveryPendingIndex();
556
- yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
557
- table: "effect_agent_canonical_records",
558
- rowKey: "upgrade",
559
- message: error.message
560
- })));
561
- yield* failpoint("upgrade:after-mutation");
562
- yield* failpoint("upgrade:before-version");
563
- yield* createWorkerStops;
564
- yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
565
- yield* failpoint("upgrade:after-version");
566
- })).pipe(Effect.mapError((error) => DoStorageError.make({
567
- operation: "upgrade native indexes",
568
- message: error.message,
569
- cause: error
570
- })));
571
- else if (version.value === "7") yield* sql.withTransaction(Effect.gen(function* () {
572
- const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
573
- if (current[0]?.value === "9") return;
574
- if (current[0]?.value !== "7") return yield* DoStorageCompatibilityError.make({
575
- actualVersion: -1,
576
- supportedVersion: 9,
577
- message: "Storage version changed during worker stop upgrade"
578
- });
579
- yield* checkPredecessorLayout(7);
580
- yield* verifyWorkerPredecessor(false);
581
- yield* failpoint("upgrade:before-mutation");
582
- yield* createWorkerStops;
583
- yield* failpoint("upgrade:after-mutation");
584
- yield* failpoint("upgrade:before-version");
585
- yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
586
- yield* failpoint("upgrade:after-version");
587
- })).pipe(Effect.mapError((cause) => DoStorageError.make({
588
- operation: "upgrade worker stop",
589
- message: "Worker stop upgrade failed",
590
- cause
591
- })));
592
- else if (version.value === "8") yield* sql.withTransaction(Effect.gen(function* () {
593
- const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
594
- if (current[0]?.value === "9") return;
595
- if (current[0]?.value !== "8") return yield* DoStorageCompatibilityError.make({
596
- actualVersion: -1,
597
- supportedVersion: 9,
598
- message: "Storage version changed during assignment seal upgrade"
599
- });
600
- yield* checkPredecessorLayout(7);
601
- yield* verifyWorkerPredecessor(true);
602
- const columns = yield* sql`PRAGMA table_info(effect_agent_worker_stops)`;
603
- yield* Schema.decodeUnknownEffect(Schema.Tuple([Schema.Struct({
604
- cid: Schema.Literal(0),
605
- name: Schema.Literal("thread_id"),
606
- type: Schema.Literal("TEXT"),
607
- notnull: Schema.Literal(1),
608
- dflt_value: Schema.Null,
609
- pk: Schema.Literal(1)
610
- })]))(columns).pipe(Effect.mapError(() => DoStorageCompatibilityError.make({
611
- actualVersion: 8,
612
- supportedVersion: 9,
613
- message: "Unsupported worker seal layout; no upgrade was committed"
614
- })));
615
- yield* failpoint("upgrade:before-mutation");
616
- yield* sql`ALTER TABLE effect_agent_worker_stops ADD COLUMN terminal TEXT`;
617
- yield* failpoint("upgrade:after-mutation");
618
- yield* failpoint("upgrade:before-version");
619
- yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
620
- yield* failpoint("upgrade:after-version");
621
- })).pipe(Effect.mapError((cause) => DoStorageError.make({
622
- operation: "upgrade assignment seals",
623
- message: "Assignment seal upgrade failed",
624
- cause
625
- })));
626
- else if (version.value !== String(9)) {
627
- const actualVersion = Number.parseInt(version.value, 10);
628
- return yield* DoStorageCompatibilityError.make({
629
- actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
630
- supportedVersion: 9,
631
- message: `The Durable Object uses unsupported storage version ${version.value}; this build supports exactly version 9. Only supported v2, v3, v4, v5, v6, v7 and v8 can be upgraded automatically. Keep the original store and use a compatible library version.`
632
- });
633
- }
634
- }
635
- yield* verifyWorkerPredecessor(true);
636
- const state = yield* ownedState(sql);
637
- const owner = (yield* SqlStorageOwner) ?? state;
638
- let journal;
639
- const lifecycle = yield* makeSqlLifecyclePublication(void 0, maxStoredValueBytes).pipe(Effect.provideService(SqlStorageOwner, owner), Effect.provideService(SqlLifecycleSource, {
640
- beforeRetain: (threadId) => Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushCanonical(threadId)),
641
- beforePending: Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushPublications())
642
- }), Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
643
- operation: "initialize lifecycle publication",
644
- message: "Native publication storage unavailable",
645
- cause
646
- })));
647
- journal = yield* makeJournal(sql, failpoint, maxStoredValueBytes, lifecycle, state).pipe(Effect.provideService(SqlStorageOwner, owner));
648
- if (lifecycle !== void 0) yield* journal.initializeLifecycleSource();
649
- return journal;
650
- });
651
- const makeJournal = (sql, failpoint, maxStoredValueBytes, lifecycle, state) => Effect.gen(function* () {
652
- const owner = (yield* SqlStorageOwner) ?? state;
653
- const progress = yield* SqlStorageProgress;
654
- const threads = threadRows(state, sql);
655
- const recovery = recoveryRows(state, sql);
656
- const cursors = lifecycleCursorRows(state, sql);
657
- let records = recordCaches.get(state);
658
- if (records === void 0) {
659
- records = makeRecordCache();
660
- recordCaches.set(state, records);
661
- state.invalidators.add(records.clear);
662
- }
663
- const recordCache = records;
664
- /** Typed pre-write refusal for any single value over the configured byte bound. */
665
- const checkValueBound = (operation, value) => {
666
- const actualBytes = storedTextBytes(value);
667
- return actualBytes > maxStoredValueBytes ? Effect.fail(DoValueBoundExceeded.make({
668
- actualBytes,
669
- maxBytes: maxStoredValueBytes,
670
- operation
671
- })) : Effect.void;
672
- };
673
- /**
674
- * Runs one journal write transaction on the Durable Object storage-backed
675
- * `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
676
- * Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
677
- * `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
678
- * Ownership-token and epoch checks still run INSIDE the transaction, so fencing atomicity
679
- * (DUR-006) is preserved identically.
680
- *
681
- * Journal write transactions are always top level: the Durable Object client rejects
682
- * nested transactions, so new journal operations must not wrap this helper inside another
683
- * transaction. Expected outcomes may be captured only when every such refusal precedes
684
- * mutation; real faults still roll back. Successful preflight reads never authorize a write.
685
- */
686
- const withWriteTransaction = (operation, expected = () => false) => (effect) => owner.transaction(storageResult(effect, expected)).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.tapError(annotateStorageError), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }), Effect.flatMap(Effect.fromResult));
687
- const materialize = Effect.fnUntraced(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
688
- if (threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
689
- operation: "materialize thread",
690
- message: "Thread identity exceeds the Durable Object storage bounds."
691
- });
692
- yield* checkValueBound("materialize thread", emptyTailDigest);
693
- const current = yield* getThread(threadId);
694
- if (current.length === 1 && producerEpoch <= current[0].producer_epoch) {
695
- if (producerEpoch === current[0].producer_epoch) return;
696
- return yield* DoFenceRejected.make({
697
- producerEpoch,
698
- actualEpoch: current[0].producer_epoch,
699
- message: "The materialization producer epoch is stale."
700
- });
701
- }
702
- yield* withWriteTransaction("materialize transaction", (error) => error._tag === "DoFenceRejected")(Effect.gen(function* () {
703
- const existingRows = yield* getThread(threadId);
704
- const existing = yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
705
- if (existing.length > 1) return yield* DoStorageCorruptionError.make({
706
- table: "effect_agent_threads",
707
- rowKey: threadId,
708
- message: "A thread primary key returned more than one row."
709
- });
710
- if (existing.length === 0) {
711
- yield* sql`
712
- INSERT INTO effect_agent_threads (
713
- thread_id,
714
- created_at,
715
- tail_sequence,
716
- tail_digest,
717
- producer_epoch
718
- ) VALUES (
719
- ${threadId},
720
- ${createdAt},
721
- 0,
722
- ${emptyTailDigest},
723
- ${producerEpoch}
724
- )
725
- RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("materialize thread")(error) : error));
726
- if (lifecycle !== void 0) yield* sql`INSERT INTO effect_agent_lifecycle_cursors (thread_id, through_sequence)
727
- VALUES (${threadId}, 0) RETURNING *`.pipe(cursors.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("initialize lifecycle source")(error) : error));
728
- return;
729
- }
730
- if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
731
- producerEpoch,
732
- actualEpoch: existing[0].producer_epoch,
733
- message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
734
- });
735
- if (producerEpoch > existing[0].producer_epoch) yield* sql`
736
- UPDATE effect_agent_threads
737
- SET producer_epoch = ${producerEpoch}
738
- WHERE thread_id = ${threadId}
739
- RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("advance materialization epoch")(error) : error));
740
- }));
741
- }, withStorageSpan("DoJournal.materialize", (error) => error._tag === "DoFenceRejected"));
742
- const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
743
- return (yield* threads.by("thread_id", threadId).pipe(Effect.mapError((error) => error._tag === "SqlError" ? storageError("read thread")(error) : error))).filter((row) => row.thread_id === threadId);
744
- });
745
- const hasRecord = Effect.fnUntraced(function* (threadId, recordId) {
746
- const thread = (yield* getThread(threadId))[0];
747
- const prefix = recordCache.prefix(threadId, thread?.tail_sequence ?? 0);
748
- if (prefix !== void 0) return prefix.some((row) => row.record_id === recordId);
749
- return (yield* sql`SELECT record_id FROM effect_agent_canonical_records
750
- WHERE thread_id = ${threadId} AND record_id = ${recordId}`.pipe(Effect.mapError(storageError("read canonical record identity")))).length > 0;
751
- });
752
- const append = Effect.fnUntraced(function* (request, observed) {
753
- if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)) return yield* DoStorageError.make({
754
- operation: "append canonical batch",
755
- message: "Canonical identifiers exceed the Durable Object storage bounds."
756
- });
757
- yield* checkValueBound("append canonical batch", request.batchJson);
758
- yield* checkValueBound("append canonical batch", request.batchDigest);
759
- yield* checkValueBound("append canonical batch", request.tailDigest);
760
- yield* Effect.forEach(request.records, (record) => checkValueBound("append canonical record", record.recordJson), { discard: true });
761
- const recordIds = request.records.map((record) => record.recordId);
762
- if (new Set(recordIds).size !== recordIds.length) return yield* DoAppendConflict.make({
763
- message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
764
- reason: "record-identity"
765
- });
766
- if (request.producerEpoch !== observed.producer_epoch) return yield* DoFenceRejected.make({
767
- producerEpoch: request.producerEpoch,
768
- actualEpoch: observed.producer_epoch,
769
- message: "The append producer epoch is not current."
770
- });
771
- if (request.expectedTailSequence !== observed.tail_sequence || request.expectedTailDigest !== observed.tail_digest) {
772
- if ((yield* sql`
773
- SELECT batch_id FROM effect_agent_canonical_batches
774
- WHERE thread_id = ${request.threadId} AND batch_id = ${request.batchId}
775
- `.pipe(Effect.mapError(storageError("preflight append replay")))).length === 0) return yield* DoAppendConflict.make({
776
- reason: "tail",
777
- message: "The canonical tail has advanced.",
778
- actualTailSequence: observed.tail_sequence,
779
- actualTailDigest: observed.tail_digest
780
- });
781
- }
782
- return yield* withWriteTransaction("append transaction", isAppendContention)(Effect.gen(function* () {
783
- const threadRows = yield* getThread(request.threadId);
784
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
785
- if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
786
- producerEpoch: request.producerEpoch,
787
- actualEpoch: thread.producer_epoch,
788
- message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
789
- });
790
- const prefix = recordCache.prefix(request.threadId, thread.tail_sequence);
791
- const batchRows = prefix !== void 0 && !prefix.some((row) => row.batch_id === request.batchId) ? [] : yield* sql`
792
- SELECT
793
- thread_id,
794
- batch_id,
795
- first_sequence,
796
- last_sequence,
797
- batch_digest,
798
- tail_digest,
799
- batch_json
800
- FROM effect_agent_canonical_batches
801
- WHERE thread_id = ${request.threadId}
802
- AND batch_id = ${request.batchId}
803
- `.pipe(Effect.mapError(storageError("read idempotent batch")));
804
- const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
805
- if (batches.length > 1) return yield* DoStorageCorruptionError.make({
806
- table: "effect_agent_canonical_batches",
807
- rowKey: `${request.threadId}/${request.batchId}`,
808
- message: "A canonical batch primary key returned more than one row."
809
- });
810
- if (batches.length === 1) {
811
- const existing = batches[0];
812
- if (existing.batch_digest !== request.batchDigest) return yield* DoAppendConflict.make({
813
- message: `Batch ${request.batchId} already exists with different canonical content.`,
814
- reason: "batch-digest"
815
- });
816
- return RawAppendResult.make({
817
- firstSequence: existing.first_sequence,
818
- lastSequence: existing.last_sequence,
819
- replayed: true,
820
- tailDigest: existing.tail_digest
821
- });
822
- }
823
- if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
824
- message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
825
- reason: "tail",
826
- actualTailSequence: thread.tail_sequence,
827
- actualTailDigest: thread.tail_digest
828
- });
829
- if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
830
- operation: "append canonical batch",
831
- message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
832
- });
833
- const existingRecords = prefix === void 0 ? [] : prefix.filter((row) => recordIds.includes(row.record_id));
834
- for (const chunk of prefix === void 0 ? chunked(recordIds, 90) : []) {
835
- const existingRecordRows = yield* sql`
836
- SELECT
837
- thread_id,
838
- sequence,
839
- record_id,
840
- batch_id,
841
- record_json
842
- FROM effect_agent_canonical_records
843
- WHERE thread_id = ${request.threadId}
844
- AND record_id IN ${sql.in([...chunk])}
845
- ORDER BY sequence
846
- `.pipe(Effect.mapError(storageError("check canonical record identities")));
847
- existingRecords.push(...yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
848
- }
849
- if (existingRecords.length > 0) return yield* DoAppendConflict.make({
850
- message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
851
- reason: "record-identity"
852
- });
853
- const firstSequence = yield* Schema.decodeEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
854
- cause: error,
855
- operation: "append canonical batch",
856
- message: error.message
857
- })));
858
- const lastSequence = yield* Schema.decodeEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => DoStorageError.make({
859
- cause: error,
860
- operation: "append canonical batch",
861
- message: error.message
862
- })));
863
- yield* sql`
864
- INSERT INTO effect_agent_canonical_batches (
865
- thread_id,
866
- batch_id,
867
- first_sequence,
868
- last_sequence,
869
- batch_digest,
870
- tail_digest,
871
- batch_json
872
- ) VALUES (
873
- ${request.threadId},
874
- ${request.batchId},
875
- ${firstSequence},
876
- ${lastSequence},
877
- ${request.batchDigest},
878
- ${request.tailDigest},
879
- ${request.batchJson}
880
- )
881
- `.pipe(Effect.mapError(storageError("insert canonical batch")));
882
- yield* failpoint("append:after-batch-insert");
883
- const records = yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
884
- const canonical = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.recordJson).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
885
- table: "effect_agent_canonical_records",
886
- rowKey: record.recordId,
887
- message: error.message
888
- })));
889
- return {
890
- record,
891
- canonical,
892
- row: {
893
- thread_id: request.threadId,
894
- sequence: firstSequence + index,
895
- record_id: record.recordId,
896
- batch_id: request.batchId,
897
- record_json: record.recordJson,
898
- outstanding: canonicalRecordOutstanding(canonical)
899
- }
900
- };
901
- }));
902
- for (const group of chunked(records, Math.floor(MAX_BOUND_PARAMETERS / 6))) {
903
- yield* sql`INSERT INTO effect_agent_canonical_records ${sql.insert(group.map(({ row }) => row))}`.pipe(Effect.mapError(storageError("insert canonical records")));
904
- for (const { canonical, row } of group) {
905
- yield* indexCanonicalRecord(request.threadId, canonical, void 0, { sequence: row.sequence }).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError(storageError("index canonical record")));
906
- recordCache.put(RecordRow.make({
907
- ...row,
908
- sequence: Schema.decodeSync(CanonicalSequence)(row.sequence)
909
- }));
910
- yield* failpoint("append:after-record-insert");
911
- }
912
- }
913
- yield* sql`
914
- UPDATE effect_agent_threads
915
- SET
916
- tail_sequence = ${lastSequence},
917
- tail_digest = ${request.tailDigest},
918
- producer_epoch = ${request.producerEpoch}
919
- WHERE thread_id = ${request.threadId}
920
- RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("advance thread tail")(error) : error));
921
- yield* failpoint("append:after-tail-update");
922
- if (lifecycle !== void 0 && records.some(({ canonical: { payload } }) => payload._tag === "RunStarted" || payload._tag === "SubagentStarted")) yield* flushCanonical(request.threadId).pipe(Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }), Effect.mapError((cause) => DoStorageError.make({
923
- operation: "retain lifecycle start prefix",
924
- message: "Lifecycle start intent could not be retained",
925
- cause
926
- })));
927
- yield* progress.committed("canonical").pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, (error) => DoStorageError.make({
928
- operation: "enroll canonical progress",
929
- message: error.message,
930
- cause: error
931
- })))));
932
- return RawAppendResult.make({
933
- firstSequence,
934
- lastSequence,
935
- replayed: false,
936
- tailDigest: request.tailDigest
937
- });
938
- }));
939
- }, withStorageSpan("DoJournal.append", isAppendContention));
940
- const read = Effect.fnUntraced(function* (request) {
941
- const thread = (yield* getThread(request.threadId))[0];
942
- const through = Math.min(thread?.tail_sequence ?? 0, request.fromSequenceExclusive + request.limit);
943
- const cached = [];
944
- for (let sequence = request.fromSequenceExclusive + 1; sequence <= through; sequence++) {
945
- const record = recordCache.get(request.threadId, sequence);
946
- if (record === void 0) break;
947
- cached.push(record);
948
- }
949
- if (cached.length === Math.max(0, through - request.fromSequenceExclusive)) return {
950
- count: cached.length,
951
- records: Stream.fromIterable(cached)
952
- };
953
- const planRows = yield* sql`
954
- SELECT
955
- sequence,
956
- length(CAST(record_json AS BLOB)) AS record_json_bytes
957
- FROM effect_agent_canonical_records
958
- WHERE thread_id = ${request.threadId}
959
- AND sequence > ${request.fromSequenceExclusive}
960
- ORDER BY sequence
961
- LIMIT ${request.limit}
962
- `.pipe(Effect.mapError(storageError("read canonical records")));
963
- const plan = yield* decodeRows(Schema.Array(ReadPlanRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, planRows, "ReadPlanRow");
964
- const mismatch = () => DoStorageCorruptionError.make({
965
- table: "effect_agent_canonical_records",
966
- rowKey: `${request.threadId}>${request.fromSequenceExclusive}`,
967
- message: "Canonical read membership, sequence, or payload size changed from its read plan."
968
- });
969
- if (plan.length > request.limit) return yield* mismatch();
970
- const pages = [];
971
- let page;
972
- let pageBytes = 0;
973
- let previousSequence = request.fromSequenceExclusive;
974
- for (const row of plan) {
975
- if (row.sequence !== previousSequence + 1) return yield* mismatch();
976
- previousSequence = row.sequence;
977
- if (page === void 0 || pageBytes + row.record_json_bytes > MAX_READ_PAGE_JSON_BYTES) {
978
- page = [row];
979
- pages.push(page);
980
- pageBytes = row.record_json_bytes;
981
- } else {
982
- page.push(row);
983
- pageBytes += row.record_json_bytes;
984
- }
985
- }
986
- const readPage = Effect.fn("DoJournal.readPage")(function* (page) {
987
- const rows = yield* sql`
988
- SELECT thread_id, sequence, record_id, batch_id, record_json
989
- FROM effect_agent_canonical_records
990
- WHERE thread_id = ${request.threadId}
991
- AND sequence >= ${page[0].sequence}
992
- AND sequence <= ${page[page.length - 1].sequence}
993
- ORDER BY sequence
994
- `.pipe(Effect.mapError(storageError("read canonical records")));
995
- const decoded = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/${page[0].sequence}`, rows, "RecordRow");
996
- if (decoded.length !== page.length || decoded.some((row, index) => row.thread_id !== request.threadId || row.sequence !== page[index].sequence || storedTextBytes(row.record_json) !== page[index].record_json_bytes)) return yield* mismatch();
997
- for (const row of decoded) recordCache.put(row);
998
- return decoded;
999
- });
1000
- return {
1001
- count: plan.length,
1002
- records: Stream.fromIterable(pages).pipe(Stream.flatMap((page) => Stream.fromIterableEffect(state.read(readPage(page)))))
1003
- };
1004
- });
1005
- const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
1006
- return yield* state.transaction(Effect.gen(function* () {
1007
- const threadRows = yield* getThread(threadId);
1008
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
1009
- yield* failpoint("export:after-thread-read");
1010
- if (thread.tail_sequence > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
1011
- operation: "export thread",
1012
- message: "The thread exceeds the current export record limit."
1013
- });
1014
- const records = [];
1015
- let afterSequence = ZERO_SEQUENCE;
1016
- while (afterSequence < thread.tail_sequence) {
1017
- const limit = Math.min(1024, thread.tail_sequence - afterSequence);
1018
- const request = RawReadRequest.make({
1019
- threadId,
1020
- fromSequenceExclusive: afterSequence,
1021
- limit
1022
- });
1023
- const plan = yield* read(request);
1024
- const page = yield* Stream.runCollect(plan.records);
1025
- if (page.length !== limit || page.some((record, index) => record.sequence !== afterSequence + index + 1)) return yield* DoStorageCorruptionError.make({
1026
- table: "effect_agent_canonical_records",
1027
- rowKey: threadId,
1028
- message: "The exported canonical prefix is not contiguous through its captured tail."
1029
- });
1030
- records.push(...page);
1031
- afterSequence = page[page.length - 1].sequence;
1032
- }
1033
- if ((yield* sql`SELECT sequence FROM effect_agent_canonical_records WHERE thread_id=${threadId} AND sequence > ${thread.tail_sequence} LIMIT 1`.pipe(Effect.mapError(storageError("verify export tail")))).length !== 0) return yield* DoStorageCorruptionError.make({
1034
- table: "effect_agent_canonical_records",
1035
- rowKey: threadId,
1036
- message: "Canonical records exist beyond the captured thread tail."
1037
- });
1038
- return RawThreadExport.make({
1039
- thread,
1040
- records
1041
- });
1042
- })).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
1043
- });
1044
- const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
1045
- if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
1046
- operation: "save checkpoint",
1047
- message: "Checkpoint identity exceeds the Durable Object storage bounds."
1048
- });
1049
- yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
1050
- yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
1051
- const threadRows = yield* getThread(checkpoint.threadId);
1052
- const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
1053
- if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
1054
- const checkpointRows = yield* sql`
1055
- SELECT
1056
- thread_id,
1057
- through_sequence,
1058
- tail_digest,
1059
- checkpoint_json
1060
- FROM effect_agent_checkpoints
1061
- WHERE thread_id = ${checkpoint.threadId}
1062
- AND through_sequence = ${checkpoint.throughSequence}
1063
- `.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
1064
- const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
1065
- if (existing.length > 1) return yield* DoStorageCorruptionError.make({
1066
- table: "effect_agent_checkpoints",
1067
- rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
1068
- message: "A checkpoint primary key returned more than one row."
1069
- });
1070
- if (existing.length === 1) {
1071
- if (existing[0].tail_digest !== checkpoint.tailDigest || existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* DoCheckpointConflict.make({ message: "A different checkpoint already exists at this canonical sequence." });
1072
- return;
1073
- }
1074
- yield* sql`
1075
- INSERT INTO effect_agent_checkpoints (
1076
- thread_id,
1077
- through_sequence,
1078
- tail_digest,
1079
- checkpoint_json
1080
- ) VALUES (
1081
- ${checkpoint.threadId},
1082
- ${checkpoint.throughSequence},
1083
- ${checkpoint.tailDigest},
1084
- ${checkpoint.checkpointJson}
1085
- )
1086
- `.pipe(Effect.mapError(storageError("insert checkpoint")));
1087
- }));
1088
- });
1089
- const saveRecoveryCheckpoint = Effect.fn("DoJournal.saveRecoveryCheckpoint")(function* (request, checkpointJson) {
1090
- const { checkpoint } = request;
1091
- if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
1092
- operation: "save recovery checkpoint",
1093
- message: "Checkpoint identity exceeds the Durable Object storage bounds."
1094
- });
1095
- yield* checkValueBound("save recovery checkpoint", checkpointJson);
1096
- yield* failpoint("save-recovery-checkpoint:before");
1097
- yield* withWriteTransaction("recovery checkpoint transaction")(Effect.gen(function* () {
1098
- const thread = (yield* getThread(checkpoint.threadId))[0];
1099
- if (thread === void 0) return yield* ThreadNotMaterialized.make({ threadId: checkpoint.threadId });
1100
- if (request.producerEpoch !== thread.producer_epoch) return yield* FenceRejected.make({
1101
- threadId: checkpoint.threadId,
1102
- actualEpoch: thread.producer_epoch,
1103
- attemptedEpoch: request.producerEpoch
1104
- });
1105
- if (checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
1106
- threadId: checkpoint.threadId,
1107
- reason: "ahead-of-tail"
1108
- });
1109
- const digests = checkpoint.throughSequence === 0 ? [EMPTY_TAIL_DIGEST] : yield* getTailDigestAt(checkpoint.threadId, checkpoint.throughSequence);
1110
- if (digests.length !== 1 || digests[0] !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
1111
- threadId: checkpoint.threadId,
1112
- reason: "digest-mismatch"
1113
- });
1114
- yield* sql`
1115
- INSERT INTO effect_agent_recovery_checkpoints (thread_id, through_sequence, tail_digest, checkpoint_json)
1116
- VALUES (${checkpoint.threadId}, ${checkpoint.throughSequence}, ${checkpoint.tailDigest}, ${checkpointJson})
1117
- ON CONFLICT (thread_id) DO UPDATE SET
1118
- through_sequence = excluded.through_sequence,
1119
- tail_digest = excluded.tail_digest,
1120
- checkpoint_json = excluded.checkpoint_json
1121
- WHERE excluded.through_sequence >= effect_agent_recovery_checkpoints.through_sequence
1122
- RETURNING *
1123
- `.pipe(recovery.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("save recovery checkpoint")(error) : error));
1124
- }));
1125
- yield* failpoint("save-recovery-checkpoint:after");
1126
- });
1127
- const loadRecoveryCheckpoint = Effect.fn("DoJournal.loadRecoveryCheckpoint")(function* (threadId) {
1128
- return yield* recovery.by("thread_id", threadId).pipe(Effect.mapError((error) => error._tag === "SqlError" ? storageError("load recovery checkpoint")(error) : error));
1129
- });
1130
- const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
1131
- const rows = yield* sql`
1132
- SELECT
1133
- thread_id,
1134
- through_sequence,
1135
- tail_digest,
1136
- checkpoint_json
1137
- FROM effect_agent_checkpoints
1138
- WHERE thread_id = ${threadId}
1139
- AND through_sequence <= ${atOrBeforeSequence}
1140
- ORDER BY through_sequence DESC
1141
- LIMIT 1
1142
- `.pipe(Effect.mapError(storageError("load checkpoint")));
1143
- return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
1144
- });
1145
- const getTailDigestAt = Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
1146
- const thread = (yield* getThread(threadId))[0];
1147
- if (thread !== void 0 && sequence === thread.tail_sequence) return [thread.tail_digest];
1148
- if (sequence === 0) {
1149
- const threads = yield* getThread(threadId);
1150
- return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
1151
- }
1152
- const rows = yield* sql`
1153
- SELECT
1154
- thread_id,
1155
- batch_id,
1156
- first_sequence,
1157
- last_sequence,
1158
- batch_digest,
1159
- tail_digest,
1160
- batch_json
1161
- FROM effect_agent_canonical_batches
1162
- WHERE thread_id = ${threadId}
1163
- AND batch_id = (
1164
- SELECT batch_id FROM effect_agent_canonical_records
1165
- WHERE thread_id = ${threadId} AND sequence = ${sequence}
1166
- )
1167
- AND last_sequence = ${sequence}
1168
- `.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
1169
- return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
1170
- });
1171
- const scanStoredPayloads = Effect.fn("DoJournal.scanStoredPayloads")(function* () {
1172
- return yield* state.transaction(Effect.gen(function* () {
1173
- const threads = yield* sql`
1174
- SELECT
1175
- thread_id,
1176
- created_at,
1177
- tail_sequence,
1178
- tail_digest,
1179
- producer_epoch
1180
- FROM effect_agent_threads
1181
- ORDER BY thread_id
1182
- `.pipe(Effect.mapError(storageError("scan threads")));
1183
- const batches = yield* sql`
1184
- SELECT
1185
- thread_id,
1186
- batch_id,
1187
- first_sequence,
1188
- last_sequence,
1189
- batch_digest,
1190
- tail_digest,
1191
- batch_json
1192
- FROM effect_agent_canonical_batches
1193
- ORDER BY thread_id, first_sequence
1194
- `.pipe(Effect.mapError(storageError("scan canonical batches")));
1195
- const records = yield* sql`
1196
- SELECT
1197
- thread_id,
1198
- sequence,
1199
- record_id,
1200
- batch_id,
1201
- record_json
1202
- FROM effect_agent_canonical_records
1203
- ORDER BY thread_id, sequence
1204
- `.pipe(Effect.mapError(storageError("scan canonical records")));
1205
- const checkpoints = yield* sql`
1206
- SELECT
1207
- thread_id,
1208
- through_sequence,
1209
- tail_digest,
1210
- checkpoint_json
1211
- FROM effect_agent_checkpoints
1212
- ORDER BY thread_id, through_sequence
1213
- `.pipe(Effect.mapError(storageError("scan checkpoints")));
1214
- return {
1215
- threads: yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
1216
- batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
1217
- records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
1218
- checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
1219
- };
1220
- })).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
1221
- });
1222
- const sourceFailure = (cause) => LifecyclePublicationError.make({
1223
- reason: "unavailable",
1224
- cause
1225
- });
1226
- const sourceThreads = Effect.fnUntraced(function* () {
1227
- const all = yield* threads.matching("lifecycle-owners", () => true, sql`SELECT * FROM effect_agent_threads LIMIT 129`, 128);
1228
- if (all.length <= 128) return all;
1229
- return yield* Schema.decodeUnknownEffect(Schema.Array(ThreadRow))(yield* sql`SELECT t.* FROM effect_agent_threads t LEFT JOIN effect_agent_lifecycle_cursors c ON c.thread_id=t.thread_id
1230
- WHERE c.thread_id IS NULL OR t.tail_sequence > c.through_sequence ORDER BY t.thread_id LIMIT 128`);
1231
- });
1232
- const initializeLifecycleSource = Effect.fnUntraced(function* () {
1233
- if (initializedLifecycleSources.has(state)) return;
1234
- yield* state.transaction(Effect.gen(function* () {
1235
- if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_lifecycle_cursors'`).length === 0) yield* sql`CREATE TABLE effect_agent_lifecycle_cursors (
1236
- thread_id TEXT PRIMARY KEY, through_sequence INTEGER NOT NULL
1237
- )`;
1238
- yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence)
1239
- SELECT thread_id, tail_sequence FROM effect_agent_threads WHERE true
1240
- ON CONFLICT(thread_id) DO NOTHING`;
1241
- yield* sourceThreads();
1242
- })).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
1243
- operation: "initialize lifecycle source",
1244
- message: "Lifecycle source cursor unavailable",
1245
- cause
1246
- })));
1247
- initializedLifecycleSources.add(state);
1248
- });
1249
- /** The journal is durable intent; only the bounded publication wave materializes a write set. */
1250
- const flushCanonical = Effect.fnUntraced(function* (threadId, maxRecords = Infinity) {
1251
- const { retainMany } = yield* SqlLifecycleRetainer;
1252
- const thread = (yield* getThread(threadId))[0];
1253
- if (thread === void 0) return;
1254
- const cursor = (yield* cursors.by("thread_id", threadId))[0];
1255
- if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
1256
- let through = cursor.through_sequence;
1257
- const bound = Math.min(thread.tail_sequence, through + maxRecords);
1258
- while (through < bound) {
1259
- const page = yield* read(RawReadRequest.make({
1260
- threadId,
1261
- fromSequenceExclusive: through,
1262
- limit: Math.min(128, bound - through)
1263
- }));
1264
- const facts = [];
1265
- let bytes = 0;
1266
- yield* Stream.runForEach(page.records, (row) => Effect.gen(function* () {
1267
- const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json);
1268
- if (record.recordId !== row.record_id || row.sequence !== through + 1) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
1269
- if (Schema.is(Schema.toType(LifecyclePublicationFact))(record.payload)) {
1270
- if (bytes + storedTextBytes(row.record_json) > MAX_READ_PAGE_JSON_BYTES && facts.length > 0) {
1271
- yield* retainMany(facts);
1272
- facts.length = 0;
1273
- bytes = 0;
1274
- }
1275
- facts.push({
1276
- id: JSON.stringify([
1277
- threadId,
1278
- "record",
1279
- record.recordId
1280
- ]),
1281
- ownerThreadId: yield* Schema.decodeEffect(ThreadId)(threadId),
1282
- canonicalSequence: row.sequence,
1283
- createdAt: record.createdAt,
1284
- fact: record.payload
1285
- });
1286
- bytes += storedTextBytes(row.record_json);
1287
- }
1288
- through = row.sequence;
1289
- }));
1290
- if (page.count === 0) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
1291
- yield* retainMany(facts);
1292
- yield* sql`UPDATE effect_agent_lifecycle_cursors SET through_sequence=${through} WHERE thread_id=${threadId} RETURNING *`.pipe(cursors.write);
1293
- }
1294
- }, Effect.mapError(sourceFailure));
1295
- const flushPublications = Effect.fnUntraced(function* () {
1296
- let owners = 0;
1297
- for (const thread of yield* sourceThreads()) {
1298
- const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
1299
- if (cursor !== void 0 && cursor.through_sequence === thread.tail_sequence) continue;
1300
- yield* flushCanonical(thread.thread_id, 1024);
1301
- if (++owners === 4) break;
1302
- }
1303
- }, Effect.mapError(sourceFailure));
1304
- const sourcePending = Effect.gen(function* () {
1305
- for (const thread of yield* sourceThreads()) {
1306
- const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
1307
- if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
1308
- if (cursor.through_sequence < thread.tail_sequence) return true;
1309
- }
1310
- return false;
1311
- }).pipe(Effect.mapError(sourceFailure));
1312
- return {
1313
- state,
1314
- owner,
1315
- threads,
1316
- lifecycle: lifecycle === void 0 ? void 0 : {
1317
- ...lifecycle,
1318
- storage: {
1319
- ...lifecycle.storage,
1320
- retainedPendingDeadline: lifecycle.storage.pendingDeadline,
1321
- pendingDeadline: state.read(Effect.gen(function* () {
1322
- const deadline = yield* lifecycle.storage.pendingDeadline;
1323
- if (!(yield* sourcePending)) return deadline;
1324
- const now = yield* Clock.currentTimeMillis;
1325
- return Option.some(Option.isSome(deadline) ? Math.min(now, deadline.value) : now);
1326
- }))
1327
- }
1328
- },
1329
- initializeLifecycleSource,
1330
- flushCanonical,
1331
- flushPublications,
1332
- append,
1333
- checkValueBound,
1334
- exportThread,
1335
- getThread,
1336
- hasRecord,
1337
- getTailDigestAt,
1338
- loadCheckpoint,
1339
- loadRecoveryCheckpoint,
1340
- saveRecoveryCheckpoint,
1341
- materialize,
1342
- read: (request) => state.read(read(request)),
1343
- saveCheckpoint,
1344
- scanStoredPayloads,
1345
- withWriteTransaction
1346
- };
1347
- });
1348
- const initializeDoJournal = ensureCurrentStorage;
1349
- //#endregion
1350
- export { initializeDoJournal as a, decodeRows as i, RawCheckpoint as n, isAppendContention as o, RawReadRequest as r, withStorageSpan as s, RawAppendRequest as t };
1351
-
1352
- //# sourceMappingURL=do-journal-DUhjZkOf.mjs.map