@effect-agent/storage-cloudflare 0.1.0-beta.42 → 0.1.0-beta.44
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/dist/index.mjs.map +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -52
- package/src/do-journal.ts +42 -0
- package/src/do-ledger.ts +309 -0
- package/src/do-schedule-store.ts +49 -0
- package/src/do-storage-failpoint-testing.ts +1 -0
- package/src/do-subscription-store.ts +153 -0
- package/src/do-thread-store.ts +74 -0
- package/src/errors.ts +1 -0
- package/src/memory-protocol.ts +24 -0
- package/src/port-protocol.ts +3 -0
- package/src/routing.ts +41 -0
package/dist/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/do-storage-failpoint-testing.ts"],"sourcesContent":["import { Context, Effect, Layer, Ref } from \"effect\";\n\nimport { DoStorageFailpoint, type DoStorageFailpointHandler } from \"./do-storage-failpoint.ts\";\nimport type { DoStorageFailpointLocation } from \"./errors.ts\";\n\nconst noFailpoint: DoStorageFailpointHandler = () => Effect.void;\n\n/** Test-only control for replacing the active Durable Object failpoint handler. */\nexport class DoStorageFailpointTestControl extends Context.Service<\n DoStorageFailpointTestControl,\n {\n readonly clear: Effect.Effect<void>;\n readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;\n }\n>()(\"@effect-agent/storage-cloudflare/DoStorageFailpointTestControl\") {\n /** Reusable test Layer with a control service backed by the same handler Ref. */\n static readonly layer = Layer.effectContext(\n Effect.gen(function* () {\n const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);\n return Context.make(\n DoStorageFailpoint,\n DoStorageFailpoint.of({\n hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),\n }),\n ).pipe(\n Context.add(\n DoStorageFailpointTestControl,\n DoStorageFailpointTestControl.of({\n clear: Ref.set(handler, noFailpoint),\n setHandler: (next) => Ref.set(handler, next),\n }),\n ),\n );\n }),\n );\n}\n\n/**\n * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the\n * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk\n * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it\n * throws while destroying the in-memory instance and every in-flight implicit or explicit\n * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an\n * unannounced platform eviction; DO storage — the only correctness-critical state — survives\n * for the next incarnation, which the persisted alarm wakes without any incoming request.\n *\n * The handles stay injected: this package never imports `cloudflare:workers`, so the harness\n * that owns a `DurableObjectState` supplies the thunk.\n */\nexport const evictionFailpointHandler =\n (options: {\n readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;\n /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform\n * declares `abort` as returning, but it throws while destroying the instance. */\n readonly evict: (location: DoStorageFailpointLocation) => void;\n }): DoStorageFailpointHandler =>\n (location) =>\n options.isArmed(location).pipe(\n Effect.flatMap((armed) =>\n armed\n ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a\n // defect in the (already dying) incarnation. The defensive throw below keeps the\n // guarantee — nothing after an armed hit may observe in-memory state — even if a\n // harness supplies an evict thunk that returns.\n Effect.sync((): never => {\n options.evict(location);\n throw new Error(\n `Durable Object eviction did not interrupt execution at ${location}.`,\n );\n })\n : Effect.void,\n ),\n );\n"],"mappings":";;;AAKA,MAAM,oBAA+C,OAAO;;AAG5D,IAAa,gCAAb,MAAa,sCAAsC,QAAQ,QAMzD,CAAC,CAAC,gEAAgE,CAAC,CAAC;;CAEpE,OAAgB,QAAQ,MAAM,cAC5B,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,KAAgC,WAAW;
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/do-storage-failpoint-testing.ts"],"sourcesContent":["import { Context, Effect, Layer, Ref } from \"effect\";\n\nimport { DoStorageFailpoint, type DoStorageFailpointHandler } from \"./do-storage-failpoint.ts\";\nimport type { DoStorageFailpointLocation } from \"./errors.ts\";\n\nconst noFailpoint: DoStorageFailpointHandler = () => Effect.void;\n\n/** Test-only control for replacing the active Durable Object failpoint handler. */\nexport class DoStorageFailpointTestControl extends Context.Service<\n DoStorageFailpointTestControl,\n {\n readonly clear: Effect.Effect<void>;\n readonly setHandler: (handler: DoStorageFailpointHandler) => Effect.Effect<void>;\n }\n>()(\"@effect-agent/storage-cloudflare/DoStorageFailpointTestControl\") {\n /** Reusable test Layer with a control service backed by the same handler Ref. */\n static readonly layer = Layer.effectContext(\n Effect.gen(function* () {\n const handler = yield* Ref.make<DoStorageFailpointHandler>(noFailpoint);\n\n return Context.make(\n DoStorageFailpoint,\n DoStorageFailpoint.of({\n hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),\n }),\n ).pipe(\n Context.add(\n DoStorageFailpointTestControl,\n DoStorageFailpointTestControl.of({\n clear: Ref.set(handler, noFailpoint),\n setHandler: (next) => Ref.set(handler, next),\n }),\n ),\n );\n }),\n );\n}\n\n/**\n * The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the\n * Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk\n * is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it\n * throws while destroying the in-memory instance and every in-flight implicit or explicit\n * storage transaction rolls back), so an armed hit ends the current Attempt exactly like an\n * unannounced platform eviction; DO storage — the only correctness-critical state — survives\n * for the next incarnation, which the persisted alarm wakes without any incoming request.\n *\n * The handles stay injected: this package never imports `cloudflare:workers`, so the harness\n * that owns a `DurableObjectState` supplies the thunk.\n */\nexport const evictionFailpointHandler =\n (options: {\n readonly isArmed: (location: DoStorageFailpointLocation) => Effect.Effect<boolean>;\n /** Kills the incarnation — e.g. `() => ctx.abort()`. Typed `void` because the platform\n * declares `abort` as returning, but it throws while destroying the instance. */\n readonly evict: (location: DoStorageFailpointLocation) => void;\n }): DoStorageFailpointHandler =>\n (location) =>\n options.isArmed(location).pipe(\n Effect.flatMap((armed) =>\n armed\n ? // `ctx.abort()` throws while destroying the instance; that throw surfaces as a\n // defect in the (already dying) incarnation. The defensive throw below keeps the\n // guarantee — nothing after an armed hit may observe in-memory state — even if a\n // harness supplies an evict thunk that returns.\n Effect.sync((): never => {\n options.evict(location);\n throw new Error(\n `Durable Object eviction did not interrupt execution at ${location}.`,\n );\n })\n : Effect.void,\n ),\n );\n"],"mappings":";;;AAKA,MAAM,oBAA+C,OAAO;;AAG5D,IAAa,gCAAb,MAAa,sCAAsC,QAAQ,QAMzD,CAAC,CAAC,gEAAgE,CAAC,CAAC;;CAEpE,OAAgB,QAAQ,MAAM,cAC5B,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,KAAgC,WAAW;EAEtE,OAAO,QAAQ,KACb,oBACA,mBAAmB,GAAG,EACpB,MAAM,aAAa,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,YAAY,QAAQ,QAAQ,CAAC,CAAC,EACzF,CAAC,CACH,CAAC,CAAC,KACA,QAAQ,IACN,+BACA,8BAA8B,GAAG;GAC/B,OAAO,IAAI,IAAI,SAAS,WAAW;GACnC,aAAa,SAAS,IAAI,IAAI,SAAS,IAAI;EAC7C,CAAC,CACH,CACF;CACF,CAAC,CACH;AACF;;;;;;;;;;;;;AAcA,MAAa,4BACV,aAMA,aACC,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KACxB,OAAO,SAAS,UACd,QAKI,OAAO,WAAkB;CACvB,QAAQ,MAAM,QAAQ;CACtB,MAAM,IAAI,MACR,0DAA0D,SAAS,EACrE;AACF,CAAC,IACD,OAAO,IACb,CACF"}
|
package/package.json
CHANGED
|
@@ -1,52 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@effect-agent/storage-cloudflare",
|
|
3
|
-
"version": "0.1.0-beta.42",
|
|
4
|
-
"exports": {
|
|
5
|
-
".": {
|
|
6
|
-
"types": "./dist/index.d.mts",
|
|
7
|
-
"default": "./dist/index.mjs"
|
|
8
|
-
},
|
|
9
|
-
"./testing": {
|
|
10
|
-
"types": "./dist/testing.d.mts",
|
|
11
|
-
"default": "./dist/testing.mjs"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@effect-agent/core": "0.1.0-beta.42",
|
|
16
|
-
"@effect-agent/thread": "0.1.0-beta.42",
|
|
17
|
-
"@effect/platform-browser": "4.0.0-rc.111",
|
|
18
|
-
"@effect/sql-sqlite-do": "4.0.0-rc.111"
|
|
19
|
-
},
|
|
20
|
-
"peerDependencies": {
|
|
21
|
-
"effect": "^4.0.0-rc.111"
|
|
22
|
-
},
|
|
23
|
-
"description": "Durable Object SQLite storage adapters and the routed port protocol for Effect Agent on Cloudflare.",
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/danieljvdm/effect-agent.git",
|
|
28
|
-
"directory": "packages/storage-cloudflare"
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"src"
|
|
33
|
-
],
|
|
34
|
-
"type": "module",
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "vp pack",
|
|
40
|
-
"check": "tsc --noEmit -p tsconfig.json"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@cloudflare/vitest-pool-workers": "0.21.3",
|
|
44
|
-
"@cloudflare/workers-types": "5.20260825.1",
|
|
45
|
-
"@effect-agent/testing": "0.1.0-beta.40",
|
|
46
|
-
"@effect/vitest": "4.0.0-rc.111",
|
|
47
|
-
"effect": "4.0.0-rc.111",
|
|
48
|
-
"typescript": "7.0.2",
|
|
49
|
-
"vite-plus": "0.3.0",
|
|
50
|
-
"vitest": "4.1.11"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
{"name":"@effect-agent/storage-cloudflare","version":"0.1.0-beta.44","dependencies":{"@effect-agent/core":"0.1.0-beta.44","@effect-agent/thread":"0.1.0-beta.44","@effect/platform-browser":"4.0.0-rc.112","@effect/sql-sqlite-do":"4.0.0-rc.112"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.44","@effect/vitest":"4.0.0-rc.112","effect":"4.0.0-rc.112","typescript":"7.0.2","vite-plus":"0.3.0","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.112"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./testing":{"types":"./dist/testing.d.mts","default":"./dist/testing.mjs"}},"description":"Durable Object SQLite storage adapters and the routed port protocol for Effect Agent on Cloudflare.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/storage-cloudflare"},"files":["dist","src"],"type":"module","publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
|
package/src/do-journal.ts
CHANGED
|
@@ -35,9 +35,11 @@ const storedTextBytes = (value: string): number => new TextEncoder().encode(valu
|
|
|
35
35
|
|
|
36
36
|
const chunked = <A>(values: ReadonlyArray<A>, size: number): Array<ReadonlyArray<A>> => {
|
|
37
37
|
const chunks: Array<ReadonlyArray<A>> = [];
|
|
38
|
+
|
|
38
39
|
for (let index = 0; index < values.length; index += size) {
|
|
39
40
|
chunks.push(values.slice(index, index + size));
|
|
40
41
|
}
|
|
42
|
+
|
|
41
43
|
return chunks;
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -244,6 +246,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
244
246
|
WHERE type = 'table'
|
|
245
247
|
AND name = 'effect_agent_meta'
|
|
246
248
|
`.pipe(Effect.mapError(storageError("read storage version table")));
|
|
249
|
+
|
|
247
250
|
const metaTables = yield* decodeRows(
|
|
248
251
|
Schema.Array(DoNameRow),
|
|
249
252
|
"sqlite_master",
|
|
@@ -259,6 +262,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
259
262
|
AND name LIKE 'effect_agent_%'
|
|
260
263
|
ORDER BY name
|
|
261
264
|
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
265
|
+
|
|
262
266
|
const existing = yield* decodeRows(
|
|
263
267
|
Schema.Array(DoNameRow),
|
|
264
268
|
"sqlite_master",
|
|
@@ -293,6 +297,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
293
297
|
FROM effect_agent_meta
|
|
294
298
|
WHERE key = 'storage_version'
|
|
295
299
|
`.pipe(Effect.mapError(storageError("read storage version")));
|
|
300
|
+
|
|
296
301
|
const version = yield* decodeSingleRow(
|
|
297
302
|
Schema.Array(DoMetaRow),
|
|
298
303
|
"effect_agent_meta",
|
|
@@ -305,6 +310,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
305
310
|
// rather than being decoded incorrectly (DEPLOY-008).
|
|
306
311
|
if (version.value !== String(CurrentDoStorageVersion)) {
|
|
307
312
|
const actualVersion = Number.parseInt(version.value, 10);
|
|
313
|
+
|
|
308
314
|
return yield* DoStorageCompatibilityError.make({
|
|
309
315
|
actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
|
|
310
316
|
supportedVersion: CurrentDoStorageVersion,
|
|
@@ -323,12 +329,14 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
323
329
|
AND name IN ${sql.in([...REQUIRED_TABLES])}
|
|
324
330
|
ORDER BY name
|
|
325
331
|
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
332
|
+
|
|
326
333
|
const required = yield* decodeRows(
|
|
327
334
|
Schema.Array(DoNameRow),
|
|
328
335
|
"sqlite_master",
|
|
329
336
|
"required_tables",
|
|
330
337
|
requiredRows,
|
|
331
338
|
);
|
|
339
|
+
|
|
332
340
|
if (required.length !== REQUIRED_TABLES.length) {
|
|
333
341
|
return yield* DoStorageCompatibilityError.make({
|
|
334
342
|
actualVersion: CurrentDoStorageVersion,
|
|
@@ -352,6 +360,7 @@ const makeJournal = (
|
|
|
352
360
|
value: string,
|
|
353
361
|
): Effect.Effect<void, DoValueBoundExceeded> => {
|
|
354
362
|
const actualBytes = storedTextBytes(value);
|
|
363
|
+
|
|
355
364
|
return actualBytes > maxStoredValueBytes
|
|
356
365
|
? Effect.fail(
|
|
357
366
|
DoValueBoundExceeded.make({
|
|
@@ -411,12 +420,14 @@ const makeJournal = (
|
|
|
411
420
|
FROM effect_agent_threads
|
|
412
421
|
WHERE thread_id = ${threadId}
|
|
413
422
|
`.pipe(Effect.mapError(storageError("read materialized thread")));
|
|
423
|
+
|
|
414
424
|
const existing = yield* decodeRows(
|
|
415
425
|
Schema.Array(ThreadRow),
|
|
416
426
|
"effect_agent_threads",
|
|
417
427
|
threadId,
|
|
418
428
|
existingRows,
|
|
419
429
|
);
|
|
430
|
+
|
|
420
431
|
if (existing.length > 1) {
|
|
421
432
|
return yield* DoStorageCorruptionError.make({
|
|
422
433
|
table: "effect_agent_threads",
|
|
@@ -440,6 +451,7 @@ const makeJournal = (
|
|
|
440
451
|
${producerEpoch}
|
|
441
452
|
)
|
|
442
453
|
`.pipe(Effect.mapError(storageError("materialize thread")));
|
|
454
|
+
|
|
443
455
|
return;
|
|
444
456
|
}
|
|
445
457
|
if (producerEpoch < existing[0].producer_epoch) {
|
|
@@ -471,6 +483,7 @@ const makeJournal = (
|
|
|
471
483
|
FROM effect_agent_threads
|
|
472
484
|
WHERE thread_id = ${threadId}
|
|
473
485
|
`.pipe(Effect.mapError(storageError("read thread")));
|
|
486
|
+
|
|
474
487
|
return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
|
|
475
488
|
});
|
|
476
489
|
|
|
@@ -496,9 +509,11 @@ const makeJournal = (
|
|
|
496
509
|
(record) => checkValueBound("append canonical record", record.recordJson),
|
|
497
510
|
{ discard: true },
|
|
498
511
|
);
|
|
512
|
+
|
|
499
513
|
return yield* withWriteTransaction("append transaction")(
|
|
500
514
|
Effect.gen(function* () {
|
|
501
515
|
const recordIds = request.records.map((record) => record.recordId);
|
|
516
|
+
|
|
502
517
|
if (new Set(recordIds).size !== recordIds.length) {
|
|
503
518
|
return yield* DoAppendConflict.make({
|
|
504
519
|
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
@@ -516,6 +531,7 @@ const makeJournal = (
|
|
|
516
531
|
FROM effect_agent_threads
|
|
517
532
|
WHERE thread_id = ${request.threadId}
|
|
518
533
|
`.pipe(Effect.mapError(storageError("read append tail")));
|
|
534
|
+
|
|
519
535
|
const thread = yield* decodeSingleRow(
|
|
520
536
|
Schema.Array(ThreadRow),
|
|
521
537
|
"effect_agent_threads",
|
|
@@ -544,6 +560,7 @@ const makeJournal = (
|
|
|
544
560
|
WHERE thread_id = ${request.threadId}
|
|
545
561
|
AND batch_id = ${request.batchId}
|
|
546
562
|
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
563
|
+
|
|
547
564
|
const batches = yield* decodeRows(
|
|
548
565
|
Schema.Array(BatchRow),
|
|
549
566
|
"effect_agent_canonical_batches",
|
|
@@ -560,12 +577,14 @@ const makeJournal = (
|
|
|
560
577
|
}
|
|
561
578
|
if (batches.length === 1) {
|
|
562
579
|
const existing = batches[0];
|
|
580
|
+
|
|
563
581
|
if (existing.batch_digest !== request.batchDigest) {
|
|
564
582
|
return yield* DoAppendConflict.make({
|
|
565
583
|
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
566
584
|
reason: "batch-digest",
|
|
567
585
|
});
|
|
568
586
|
}
|
|
587
|
+
|
|
569
588
|
return RawAppendResult.make({
|
|
570
589
|
firstSequence: existing.first_sequence,
|
|
571
590
|
lastSequence: existing.last_sequence,
|
|
@@ -597,6 +616,7 @@ const makeJournal = (
|
|
|
597
616
|
// Chunked to respect the Durable Object platform's 100-bound-parameter statement
|
|
598
617
|
// limit: a batch may carry up to 256 records.
|
|
599
618
|
const existingRecords: Array<RecordRow> = [];
|
|
619
|
+
|
|
600
620
|
for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
|
|
601
621
|
const existingRecordRows = yield* sql<Record<string, unknown>>`
|
|
602
622
|
SELECT
|
|
@@ -610,6 +630,7 @@ const makeJournal = (
|
|
|
610
630
|
AND record_id IN ${sql.in([...chunk])}
|
|
611
631
|
ORDER BY sequence
|
|
612
632
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
633
|
+
|
|
613
634
|
existingRecords.push(
|
|
614
635
|
...(yield* decodeRows(
|
|
615
636
|
Schema.Array(RecordRow),
|
|
@@ -637,6 +658,7 @@ const makeJournal = (
|
|
|
637
658
|
}),
|
|
638
659
|
),
|
|
639
660
|
);
|
|
661
|
+
|
|
640
662
|
const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
|
|
641
663
|
firstSequence + request.records.length - 1,
|
|
642
664
|
).pipe(
|
|
@@ -728,6 +750,7 @@ const makeJournal = (
|
|
|
728
750
|
ORDER BY sequence
|
|
729
751
|
LIMIT ${request.limit}
|
|
730
752
|
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
753
|
+
|
|
731
754
|
return yield* decodeRows(
|
|
732
755
|
Schema.Array(RecordRow),
|
|
733
756
|
"effect_agent_canonical_records",
|
|
@@ -750,13 +773,16 @@ const makeJournal = (
|
|
|
750
773
|
FROM effect_agent_threads
|
|
751
774
|
WHERE thread_id = ${threadId}
|
|
752
775
|
`.pipe(Effect.mapError(storageError("export thread")));
|
|
776
|
+
|
|
753
777
|
const thread = yield* decodeSingleRow(
|
|
754
778
|
Schema.Array(ThreadRow),
|
|
755
779
|
"effect_agent_threads",
|
|
756
780
|
threadId,
|
|
757
781
|
threadRows,
|
|
758
782
|
);
|
|
783
|
+
|
|
759
784
|
yield* failpoint("export:after-thread-read");
|
|
785
|
+
|
|
760
786
|
const batchRows = yield* sql<Record<string, unknown>>`
|
|
761
787
|
SELECT
|
|
762
788
|
thread_id,
|
|
@@ -770,6 +796,7 @@ const makeJournal = (
|
|
|
770
796
|
WHERE thread_id = ${threadId}
|
|
771
797
|
ORDER BY first_sequence
|
|
772
798
|
`.pipe(Effect.mapError(storageError("export canonical batches")));
|
|
799
|
+
|
|
773
800
|
const recordRows = yield* sql<Record<string, unknown>>`
|
|
774
801
|
SELECT
|
|
775
802
|
thread_id,
|
|
@@ -781,6 +808,7 @@ const makeJournal = (
|
|
|
781
808
|
WHERE thread_id = ${threadId}
|
|
782
809
|
ORDER BY sequence
|
|
783
810
|
`.pipe(Effect.mapError(storageError("export canonical records")));
|
|
811
|
+
|
|
784
812
|
const checkpointRows = yield* sql<Record<string, unknown>>`
|
|
785
813
|
SELECT
|
|
786
814
|
thread_id,
|
|
@@ -844,12 +872,14 @@ const makeJournal = (
|
|
|
844
872
|
FROM effect_agent_threads
|
|
845
873
|
WHERE thread_id = ${checkpoint.threadId}
|
|
846
874
|
`.pipe(Effect.mapError(storageError("read checkpoint tail")));
|
|
875
|
+
|
|
847
876
|
const thread = yield* decodeSingleRow(
|
|
848
877
|
Schema.Array(ThreadRow),
|
|
849
878
|
"effect_agent_threads",
|
|
850
879
|
checkpoint.threadId,
|
|
851
880
|
threadRows,
|
|
852
881
|
);
|
|
882
|
+
|
|
853
883
|
if (checkpoint.throughSequence > thread.tail_sequence) {
|
|
854
884
|
return yield* DoCheckpointConflict.make({
|
|
855
885
|
message:
|
|
@@ -868,12 +898,14 @@ const makeJournal = (
|
|
|
868
898
|
WHERE thread_id = ${checkpoint.threadId}
|
|
869
899
|
AND through_sequence = ${checkpoint.throughSequence}
|
|
870
900
|
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
901
|
+
|
|
871
902
|
const existing = yield* decodeRows(
|
|
872
903
|
Schema.Array(CheckpointRow),
|
|
873
904
|
"effect_agent_checkpoints",
|
|
874
905
|
`${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
875
906
|
checkpointRows,
|
|
876
907
|
);
|
|
908
|
+
|
|
877
909
|
if (existing.length > 1) {
|
|
878
910
|
return yield* DoStorageCorruptionError.make({
|
|
879
911
|
table: "effect_agent_checkpoints",
|
|
@@ -890,6 +922,7 @@ const makeJournal = (
|
|
|
890
922
|
message: "A different checkpoint already exists at this canonical sequence.",
|
|
891
923
|
});
|
|
892
924
|
}
|
|
925
|
+
|
|
893
926
|
return;
|
|
894
927
|
}
|
|
895
928
|
|
|
@@ -926,6 +959,7 @@ const makeJournal = (
|
|
|
926
959
|
ORDER BY through_sequence DESC
|
|
927
960
|
LIMIT 1
|
|
928
961
|
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
962
|
+
|
|
929
963
|
return yield* decodeRows(
|
|
930
964
|
Schema.Array(CheckpointRow),
|
|
931
965
|
"effect_agent_checkpoints",
|
|
@@ -940,12 +974,14 @@ const makeJournal = (
|
|
|
940
974
|
) {
|
|
941
975
|
if (sequence === 0) {
|
|
942
976
|
const threads = yield* getThread(threadId);
|
|
977
|
+
|
|
943
978
|
return threads.length === 0
|
|
944
979
|
? []
|
|
945
980
|
: [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
|
|
946
981
|
(value): value is string => value !== undefined,
|
|
947
982
|
);
|
|
948
983
|
}
|
|
984
|
+
|
|
949
985
|
const rows = yield* sql<Record<string, unknown>>`
|
|
950
986
|
SELECT
|
|
951
987
|
thread_id,
|
|
@@ -959,12 +995,14 @@ const makeJournal = (
|
|
|
959
995
|
WHERE thread_id = ${threadId}
|
|
960
996
|
AND last_sequence = ${sequence}
|
|
961
997
|
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
998
|
+
|
|
962
999
|
const batches = yield* decodeRows(
|
|
963
1000
|
Schema.Array(BatchRow),
|
|
964
1001
|
"effect_agent_canonical_batches",
|
|
965
1002
|
`${threadId}/${sequence}`,
|
|
966
1003
|
rows,
|
|
967
1004
|
);
|
|
1005
|
+
|
|
968
1006
|
return batches.map((batch) => batch.tail_digest);
|
|
969
1007
|
});
|
|
970
1008
|
|
|
@@ -982,6 +1020,7 @@ const makeJournal = (
|
|
|
982
1020
|
FROM effect_agent_threads
|
|
983
1021
|
ORDER BY thread_id
|
|
984
1022
|
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
1023
|
+
|
|
985
1024
|
const batches = yield* sql<Record<string, unknown>>`
|
|
986
1025
|
SELECT
|
|
987
1026
|
thread_id,
|
|
@@ -994,6 +1033,7 @@ const makeJournal = (
|
|
|
994
1033
|
FROM effect_agent_canonical_batches
|
|
995
1034
|
ORDER BY thread_id, first_sequence
|
|
996
1035
|
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1036
|
+
|
|
997
1037
|
const records = yield* sql<Record<string, unknown>>`
|
|
998
1038
|
SELECT
|
|
999
1039
|
thread_id,
|
|
@@ -1004,6 +1044,7 @@ const makeJournal = (
|
|
|
1004
1044
|
FROM effect_agent_canonical_records
|
|
1005
1045
|
ORDER BY thread_id, sequence
|
|
1006
1046
|
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1047
|
+
|
|
1007
1048
|
const checkpoints = yield* sql<Record<string, unknown>>`
|
|
1008
1049
|
SELECT
|
|
1009
1050
|
thread_id,
|
|
@@ -1013,6 +1054,7 @@ const makeJournal = (
|
|
|
1013
1054
|
FROM effect_agent_checkpoints
|
|
1014
1055
|
ORDER BY thread_id, through_sequence
|
|
1015
1056
|
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1057
|
+
|
|
1016
1058
|
return {
|
|
1017
1059
|
threads: yield* decodeRows(
|
|
1018
1060
|
Schema.Array(ThreadRow),
|