@yielded/agent-storage-cloudflare 0.1.0-beta.175 → 0.1.0-beta.177

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.
@@ -110,6 +110,8 @@ import {
110
110
  UnknownResolutionKind,
111
111
  unknownResolutionKind,
112
112
  unknownResolutionMatchesResult,
113
+ submissionInputRecordId,
114
+ submissionInputBatchId,
113
115
  submissionAbortRecordId,
114
116
  submissionSettlementRecordId,
115
117
  submissionSettlementBatchId,
@@ -1146,9 +1148,7 @@ const makeServices = Effect.fnUntraced(function* () {
1146
1148
  Effect.gen(function* () {
1147
1149
  const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1148
1150
 
1149
- const existingRows = yield* byKey(validated, operation);
1150
-
1151
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1151
+ const existing = yield* byKey(validated, operation);
1152
1152
 
1153
1153
  if (existing.length > 1) {
1154
1154
  return yield* corruptionFailure(
@@ -1530,13 +1530,7 @@ const makeServices = Effect.fnUntraced(function* () {
1530
1530
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1531
1531
  }
1532
1532
 
1533
- const foundRows = yield* byKey(validated, operation);
1534
-
1535
- const decoded = yield* decodeSubmissionRows(
1536
- operation,
1537
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1538
- foundRows,
1539
- );
1533
+ const decoded = yield* byKey(validated, operation);
1540
1534
 
1541
1535
  if (decoded.length > 1) {
1542
1536
  return yield* corruptionFailure(
@@ -1563,13 +1557,7 @@ const makeServices = Effect.fnUntraced(function* () {
1563
1557
  request,
1564
1558
  ).pipe(Effect.mapError(internalFailure(operation)));
1565
1559
 
1566
- const foundRows = yield* byKey(validated, operation);
1567
-
1568
- const decoded = yield* decodeSubmissionRows(
1569
- operation,
1570
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1571
- foundRows,
1572
- );
1560
+ const decoded = yield* byKey(validated, operation);
1573
1561
 
1574
1562
  if (decoded.length > 1) {
1575
1563
  return yield* corruptionFailure(
@@ -1706,6 +1694,45 @@ const makeServices = Effect.fnUntraced(function* () {
1706
1694
  return claimed;
1707
1695
  });
1708
1696
 
1697
+ const markInputAppliedInTransaction = Effect.fnUntraced(function* (
1698
+ validated: MarkInputAppliedRequest,
1699
+ ) {
1700
+ const operation = "ledger mark input applied";
1701
+
1702
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1703
+
1704
+ yield* requireOwner(operation, submission);
1705
+ if (submission.input_applied_record_id !== null) {
1706
+ if (
1707
+ submission.input_applied_record_id === validated.recordId &&
1708
+ submission.input_applied_sequence === validated.sequence
1709
+ ) {
1710
+ return;
1711
+ }
1712
+
1713
+ return yield* corruptionFailure(
1714
+ operation,
1715
+ "effect_agent_submissions",
1716
+ validated.submissionId,
1717
+ "A different canonical input marker is already recorded for this Submission.",
1718
+ );
1719
+ }
1720
+ yield* sql`
1721
+ UPDATE effect_agent_submissions
1722
+ SET
1723
+ input_applied_record_id = ${validated.recordId},
1724
+ input_applied_sequence = ${validated.sequence},
1725
+ state = CASE
1726
+ WHEN state IN ('admitted', 'ready') THEN 'input-applied'
1727
+ ELSE state
1728
+ END
1729
+ WHERE submission_id = ${validated.submissionId}
1730
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1731
+ rows.submissions.write,
1732
+ Effect.mapError(internalFailure(operation)),
1733
+ );
1734
+ });
1735
+
1709
1736
  const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fnUntraced(
1710
1737
  function* (request: MarkInputAppliedRequest) {
1711
1738
  const operation = "ledger mark input applied";
@@ -1715,44 +1742,111 @@ const makeServices = Effect.fnUntraced(function* () {
1715
1742
  );
1716
1743
 
1717
1744
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1718
- yield* inWriteTransaction(
1719
- operation,
1720
- Effect.gen(function* () {
1721
- const submission = yield* requireSubmission(operation, validated.submissionId);
1745
+ yield* inWriteTransaction(operation, markInputAppliedInTransaction(validated));
1746
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1747
+ },
1748
+ );
1722
1749
 
1723
- yield* requireOwner(operation, submission);
1724
- if (submission.input_applied_record_id !== null) {
1725
- if (
1726
- submission.input_applied_record_id === validated.recordId &&
1727
- submission.input_applied_sequence === validated.sequence
1728
- ) {
1729
- return;
1730
- }
1750
+ const appendInput: NonNullable<SubmissionLedger["Service"]["appendInput"]> = Effect.fnUntraced(
1751
+ function* (submissionId, request) {
1752
+ const operation = "append original input and context";
1731
1753
 
1732
- return yield* corruptionFailure(
1733
- operation,
1734
- "effect_agent_submissions",
1735
- validated.submissionId,
1736
- "A different canonical input marker is already recorded for this Submission.",
1737
- );
1738
- }
1739
- yield* sql`
1740
- UPDATE effect_agent_submissions
1741
- SET
1742
- input_applied_record_id = ${validated.recordId},
1743
- input_applied_sequence = ${validated.sequence},
1744
- state = CASE
1745
- WHEN state IN ('admitted', 'ready') THEN 'input-applied'
1746
- ELSE state
1747
- END
1748
- WHERE submission_id = ${validated.submissionId}
1749
- RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1750
- rows.submissions.write,
1751
- Effect.mapError(internalFailure(operation)),
1752
- );
1753
- }),
1754
+ const prepared = yield* prepareCanonicalAppend(request).pipe(
1755
+ Effect.provideService(Crypto.Crypto, crypto),
1756
+ );
1757
+
1758
+ const record = prepared.request.batch.records[0];
1759
+
1760
+ const context = prepared.request.executions
1761
+ ?.flatMap((update) => update.facts ?? [])
1762
+ .find(({ payload }) => payload._tag === "RunContextRecorded");
1763
+
1764
+ if (
1765
+ prepared.request.executions?.length !== 1 ||
1766
+ prepared.request.executions[0]?.previousRevision !== 0 ||
1767
+ prepared.request.batch.records.length !== 1 ||
1768
+ record?.payload._tag !== "UserInputRecorded" ||
1769
+ record.payload.kind !== "user" ||
1770
+ record.payload.submissionId !== submissionId ||
1771
+ record.payload.runId !== runIdForSubmission(submissionId) ||
1772
+ record.recordId !== submissionInputRecordId(submissionId) ||
1773
+ prepared.request.batch.batchId !== submissionInputBatchId(submissionId) ||
1774
+ (context?.payload._tag === "RunContextRecorded" &&
1775
+ context.payload.historyThrough !== prepared.request.expectedTailSequence)
1776
+ )
1777
+ return yield* ThreadStoreError.make({
1778
+ operation,
1779
+ message: "Original input publication has a different context boundary",
1780
+ });
1781
+
1782
+ const mapError = (cause: { readonly message: string }) =>
1783
+ ThreadStoreError.make({ operation, message: cause.message, cause });
1784
+
1785
+ yield* journal.prepareAppend(prepared.raw).pipe(Effect.mapError(mapError));
1786
+
1787
+ return yield* owner.withProgress(
1788
+ prepared.request.threadId,
1789
+ owner.withMutation(
1790
+ Effect.gen(function* () {
1791
+ yield* hitFailpoint("append:before", operation);
1792
+ yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1793
+
1794
+ const result = yield* journal
1795
+ .withWriteTransaction(operation)(
1796
+ Effect.gen(function* () {
1797
+ const submission = yield* requireSubmission(operation, submissionId);
1798
+
1799
+ yield* requireOwner(operation, submission);
1800
+ if (submission.thread_id !== prepared.request.threadId)
1801
+ return yield* LedgerError.make({
1802
+ operation,
1803
+ message: "Original input belongs to another Thread",
1804
+ });
1805
+ const result = yield* journal.appendPrepared(prepared.raw);
1806
+
1807
+ yield* markInputAppliedInTransaction(
1808
+ MarkInputAppliedRequest.make({
1809
+ submissionId,
1810
+ recordId: record.recordId,
1811
+ sequence: result.firstSequence,
1812
+ }),
1813
+ );
1814
+
1815
+ return result;
1816
+ }),
1817
+ )
1818
+ .pipe(
1819
+ Effect.mapError((error) =>
1820
+ error._tag === "DoAppendConflict"
1821
+ ? AppendConflict.make({
1822
+ threadId: prepared.request.threadId,
1823
+ batchId: prepared.request.batch.batchId,
1824
+ reason: error.reason,
1825
+ ...(error.actualTailSequence === undefined
1826
+ ? {}
1827
+ : { actualTailSequence: error.actualTailSequence }),
1828
+ })
1829
+ : error._tag === "OwnershipLost"
1830
+ ? error
1831
+ : mapError(error),
1832
+ ),
1833
+ );
1834
+
1835
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1836
+ yield* hitFailpoint("append:after", operation);
1837
+
1838
+ return result;
1839
+ }).pipe(
1840
+ Effect.mapError((error) =>
1841
+ error._tag === "OwnershipLost" ||
1842
+ error._tag === "ThreadStoreError" ||
1843
+ error._tag === "AppendConflict"
1844
+ ? error
1845
+ : mapError(error),
1846
+ ),
1847
+ ),
1848
+ ),
1754
1849
  );
1755
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1756
1850
  },
1757
1851
  );
1758
1852
 
@@ -3702,19 +3796,16 @@ const makeServices = Effect.fnUntraced(function* () {
3702
3796
  .filter((row) => row.joined_host_submission_id === validated.submissionId)
3703
3797
  .sort((a, b) => a.queue_sequence - b.queue_sequence);
3704
3798
 
3705
- const joinSubmissions = yield* decodeSubmissionRows(
3706
- operation,
3707
- validated.submissionId,
3708
- joinRows,
3709
- );
3799
+ const joins: Array<JoinSnapshot> = [];
3710
3800
 
3711
- const joins = yield* Effect.forEach(joinSubmissions, (row) =>
3712
- decodeJoinSnapshot({
3713
- submissionId: row.submission_id,
3714
- state: row.state,
3715
- hostSubmissionId: validated.submissionId,
3716
- }).pipe(Effect.mapError(internalFailure(operation))),
3717
- );
3801
+ for (const row of joinRows)
3802
+ joins.push(
3803
+ yield* decodeJoinSnapshot({
3804
+ submissionId: row.submission_id,
3805
+ state: row.state,
3806
+ hostSubmissionId: validated.submissionId,
3807
+ }).pipe(Effect.mapError(internalFailure(operation))),
3808
+ );
3718
3809
 
3719
3810
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3720
3811
 
@@ -3755,15 +3846,17 @@ const makeServices = Effect.fnUntraced(function* () {
3755
3846
 
3756
3847
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3757
3848
 
3758
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
3759
- approvalIntentFromRow(operation, row),
3760
- );
3849
+ const approvalDecisions: Array<ApprovalDecisionIntent> = [];
3850
+
3851
+ for (const row of decisionRows)
3852
+ approvalDecisions.push(yield* approvalIntentFromRow(operation, row));
3761
3853
 
3762
3854
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3763
3855
 
3764
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
3765
- unknownResolutionIntentFromRow(operation, row),
3766
- );
3856
+ const unknownResolutions: Array<UnknownResolutionIntent> = [];
3857
+
3858
+ for (const row of resolutionRows)
3859
+ unknownResolutions.push(yield* unknownResolutionIntentFromRow(operation, row));
3767
3860
 
3768
3861
  // Parent-side subagent view: this Submission's child budget reservations in parent
3769
3862
  // Tool Call order, plus each attached child's current lane state (a disposable
@@ -3778,21 +3871,16 @@ const makeServices = Effect.fnUntraced(function* () {
3778
3871
  .filter((row) => row.parent_submission_id === validated.submissionId)
3779
3872
  .sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
3780
3873
 
3781
- const decodedChildReservations = yield* decodeChildReservationRows(
3782
- operation,
3783
- validated.submissionId,
3784
- childReservationRows,
3785
- );
3874
+ const childReservations: Array<ChildBudgetReservationSnapshot> = [];
3786
3875
 
3787
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
3788
- childReservationSnapshotFromRow(operation, row),
3789
- );
3876
+ for (const row of childReservationRows)
3877
+ childReservations.push(yield* childReservationSnapshotFromRow(operation, row));
3790
3878
 
3791
3879
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
3792
3880
  const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
3793
3881
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3794
3882
 
3795
- for (const row of decodedChildReservations) {
3883
+ for (const row of childReservationRows) {
3796
3884
  if (row.child_submission_id === null) continue;
3797
3885
  const child = yield* readSubmission(operation, row.child_submission_id);
3798
3886
 
@@ -3863,6 +3951,7 @@ const makeServices = Effect.fnUntraced(function* () {
3863
3951
  resolveAdmission,
3864
3952
  selectHead,
3865
3953
  markInputApplied,
3954
+ appendInput,
3866
3955
  finalizeSettlement: (request) => owner.withMutation(finalizeSettlement(request)),
3867
3956
  requestAbort,
3868
3957
  stopWorker,
@@ -633,6 +633,13 @@ export const threadStoreLayer: Layer.Layer<
633
633
  DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
634
634
  > = ThreadReader.layer().pipe(Layer.provideMerge(Layer.effectContext(makeServices())));
635
635
 
636
+ /**
637
+ * Share one initialized journal between local storage ports in one Object build. Provide
638
+ * this beneath both ports and above their common SQL, owner, configuration, failpoint,
639
+ * progress and lifecycle providers. Standalone ports initialize their own journal.
640
+ */
641
+ export { initializedDoJournalLayer as journalLayer } from "./internal/do-journal.ts";
642
+
636
643
  /**
637
644
  * Validated Durable Object storage configuration Layer with the documented defaults applied.
638
645
  * Shared by the ThreadStore and SubmissionLedger convenience layers so their defaults
@@ -236,35 +236,36 @@ const LOCAL: RouteTarget = { _tag: "local" };
236
236
  * Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
237
237
  * refused them at admission, so they cannot name any stored row anywhere.
238
238
  */
239
- const routableSubmissionTarget = (
240
- ownsThread: RoutedPortOptions["ownsThread"],
241
- ): ((operation: string, submissionId: string) => Effect.Effect<RouteTarget, LedgerError>) =>
242
- Effect.fnUntraced(function* (
243
- operation: string,
244
- submissionId: string,
245
- ): Effect.fn.Return<RouteTarget, LedgerError> {
246
- if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) {
247
- return yield* LedgerError.make({
248
- operation,
249
- message:
250
- `A Submission identity of ${submissionId.length} characters exceeds the ` +
251
- `${MAX_ROUTABLE_SUBMISSION_ID_LENGTH}-character routable identity bound; admission ` +
252
- "refuses such identities, so it cannot name any stored row.",
253
- });
254
- }
255
- const separator = submissionId.indexOf(":");
256
-
257
- if (separator === -1) return LOCAL;
258
- if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
259
- const tail = submissionId.slice(separator + 1);
260
-
261
- return yield* decodeThreadId(tail).pipe(
262
- Effect.map((threadId): RouteTarget =>
263
- ownsThread(threadId) ? LOCAL : { _tag: "foreign", threadId },
264
- ),
265
- Effect.orElseSucceed(() => LOCAL),
266
- );
267
- });
239
+ const routableSubmissionTarget =
240
+ (
241
+ ownsThread: RoutedPortOptions["ownsThread"],
242
+ ): ((operation: string, submissionId: string) => Effect.Effect<RouteTarget, LedgerError>) =>
243
+ (operation, submissionId) =>
244
+ Effect.suspend(() => {
245
+ if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) {
246
+ return Effect.fail(
247
+ LedgerError.make({
248
+ operation,
249
+ message:
250
+ `A Submission identity of ${submissionId.length} characters exceeds the ` +
251
+ `${MAX_ROUTABLE_SUBMISSION_ID_LENGTH}-character routable identity bound; admission ` +
252
+ "refuses such identities, so it cannot name any stored row.",
253
+ }),
254
+ );
255
+ }
256
+ const separator = submissionId.indexOf(":");
257
+
258
+ if (separator === -1 || !UUID_HEAD_PATTERN.test(submissionId.slice(0, separator)))
259
+ return Effect.succeed(LOCAL);
260
+
261
+ return decodeThreadId(submissionId.slice(separator + 1)).pipe(
262
+ Effect.match({
263
+ onFailure: () => LOCAL,
264
+ onSuccess: (threadId): RouteTarget =>
265
+ ownsThread(threadId) ? LOCAL : { _tag: "foreign", threadId },
266
+ }),
267
+ );
268
+ });
268
269
 
269
270
  /**
270
271
  * Read a Submission only from this physical owner. Encoded foreign identities fail before
@@ -730,6 +731,23 @@ const makeRoutedLedgerServices = Effect.fnUntraced(function* (options: RoutedPor
730
731
  ? local.claimJoining(request)
731
732
  : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
732
733
 
734
+ ...(local.appendInput === undefined
735
+ ? {}
736
+ : {
737
+ appendInput: (
738
+ submissionId: SubmissionSnapshot["submissionId"],
739
+ request: Parameters<NonNullable<SubmissionLedger["Service"]["appendInput"]>>[1],
740
+ ) =>
741
+ options.ownsThread(request.threadId)
742
+ ? local.appendInput!(submissionId, request)
743
+ : Effect.fail(
744
+ ThreadStoreError.make({
745
+ operation: "append original input",
746
+ message: "Original input publication is lane-local",
747
+ }),
748
+ ),
749
+ }),
750
+
733
751
  markInputApplied: (request) =>
734
752
  submissionTarget("ledger mark input applied", request.submissionId).pipe(
735
753
  Effect.flatMap((target) =>
@@ -33,7 +33,9 @@ import {
33
33
  import {
34
34
  Cause,
35
35
  Clock,
36
+ Context,
36
37
  Effect,
38
+ Layer,
37
39
  Option,
38
40
  Predicate,
39
41
  References,
@@ -46,6 +48,7 @@ import * as SqlClient from "effect/sql/SqlClient";
46
48
  import { classifySqliteError, SqlError } from "effect/sql/SqlError";
47
49
  import { CurrentTransformer, type Statement } from "effect/sql/Statement";
48
50
 
51
+ import { DoStorageConfig } from "../DoStorageConfig.ts";
49
52
  import {
50
53
  type DoStorageFailpointError,
51
54
  DoAppendConflict,
@@ -55,7 +58,7 @@ import {
55
58
  DoValueBoundExceeded,
56
59
  type DoStorageFailpointLocation,
57
60
  } from "../DoStorageError.ts";
58
- import { isDefaultDoStorageFailpoint } from "../DoStorageFailpoint.ts";
61
+ import { DoStorageFailpoint, isDefaultDoStorageFailpoint } from "../DoStorageFailpoint.ts";
59
62
  import { ensureDoStorageLayout } from "./migrations.ts";
60
63
  import { ownedState, ownedRows, type OwnedState } from "./owned-state.ts";
61
64
  import {
@@ -498,35 +501,37 @@ const makeJournal = Effect.fnUntraced(function* (
498
501
  * transaction. Expected outcomes may be captured only when every such refusal precedes
499
502
  * mutation; real faults still roll back. Successful preflight reads never authorize a write.
500
503
  */
501
- const withWriteTransaction =
502
- (operation: string, expected: (error: { readonly _tag: string }) => boolean = () => false) =>
503
- <A, E extends { readonly _tag: string }>(
504
+ const withWriteTransaction = (
505
+ operation: string,
506
+ expected?: (error: { readonly _tag: string }) => boolean,
507
+ ) => {
508
+ const transaction = <A, E extends { readonly _tag: string }>(
504
509
  effect: Effect.Effect<A, E>,
505
510
  ): Effect.Effect<A, E | DoStorageError> =>
506
511
  Effect.withFiber((fiber) => {
507
- const body = state.owner
508
- .withMutation(owner.transaction(storageResult(effect, expected)))
509
- .pipe(
510
- Effect.provideService(SqlClient.SqlClient, sql),
511
- Effect.mapError((error) =>
512
- isSqlError(error) ? storageError(operation)(error) : error,
513
- ),
514
- );
512
+ const body = state.owner.withMutation(owner.transaction(effect)).pipe(
513
+ Effect.provideService(SqlClient.SqlClient, sql),
514
+ Effect.mapError((error) => (isSqlError(error) ? storageError(operation)(error) : error)),
515
+ );
515
516
 
516
517
  const traced =
517
518
  fiber.getRef(References.TracerEnabled) &&
518
519
  fiber.getRef(Tracer.Tracer) !== Tracer.nativeTracer;
519
520
 
520
- return (
521
- traced
522
- ? body.pipe(
523
- Effect.tapError(annotateStorageError),
524
- Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }),
525
- )
526
- : body
527
- ).pipe(Effect.flatMap(Effect.fromResult));
521
+ return traced
522
+ ? body.pipe(
523
+ Effect.tapError(annotateStorageError),
524
+ Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }),
525
+ )
526
+ : body;
528
527
  });
529
528
 
529
+ return <A, E extends { readonly _tag: string }>(effect: Effect.Effect<A, E>) =>
530
+ expected === undefined
531
+ ? transaction(effect)
532
+ : transaction(storageResult(effect, expected)).pipe(Effect.flatMap(Effect.fromResult));
533
+ };
534
+
530
535
  const materialize = Effect.fnUntraced(function* (
531
536
  threadId: string,
532
537
  createdAt: string,
@@ -690,7 +695,7 @@ const makeJournal = Effect.fnUntraced(function* (
690
695
  )
691
696
  return false;
692
697
 
693
- const workPlan = work.prepareClosedPair(
698
+ const workPlan = yield* work.prepareClosedPair(
694
699
  request.threadId,
695
700
  first,
696
701
  last,
@@ -729,7 +734,6 @@ const makeJournal = Effect.fnUntraced(function* (
729
734
  // A zero-row predicate miss has made no change. A successful update
730
735
  // must never fall back: every later refusal rolls back this transaction.
731
736
  if (rows.length === 0) return undefined;
732
- const closed = yield* workPlan.fold();
733
737
  const executionPlans = executions.prepareCommit(request);
734
738
 
735
739
  if (!batchInserted)
@@ -760,7 +764,8 @@ const makeJournal = Effect.fnUntraced(function* (
760
764
  sql`INSERT INTO effect_agent_record_runs ${sql.insert(group)}`,
761
765
  "index canonical Run membership",
762
766
  );
763
- for (const pointer of closed.pointers) yield* query(pointer, "index canonical pointers");
767
+ for (const pointer of workPlan.pointers)
768
+ yield* query(pointer, "index canonical pointers");
764
769
 
765
770
  for (const plan of executionPlans) {
766
771
  const changed = yield* query(plan.statement, "commit active execution");
@@ -1568,4 +1573,27 @@ const makeJournal = Effect.fnUntraced(function* (
1568
1573
 
1569
1574
  export type DoJournal = Effect.Success<ReturnType<typeof makeJournal>>;
1570
1575
 
1571
- export const initializeDoJournal = ensureCurrentStorage;
1576
+ const InitializedDoJournal = Context.Reference<DoJournal | undefined>(
1577
+ "@effect-agent/storage-cloudflare/InitializedDoJournal",
1578
+ { defaultValue: () => undefined },
1579
+ );
1580
+
1581
+ /** Build once in the common owner, SQL, configuration, failpoint and lifecycle context. */
1582
+ export const initializedDoJournalLayer = Layer.effect(InitializedDoJournal)(
1583
+ Effect.gen(function* () {
1584
+ const sql = yield* SqlClient.SqlClient;
1585
+ const config = yield* DoStorageConfig;
1586
+ const failpoint = yield* DoStorageFailpoint;
1587
+
1588
+ return yield* ensureCurrentStorage(sql, failpoint.hit, config.maxStoredValueBytes);
1589
+ }),
1590
+ );
1591
+
1592
+ export const initializeDoJournal = (
1593
+ sql: SqlClient.SqlClient,
1594
+ failpoint: DoJournalFailpoint,
1595
+ maxStoredValueBytes: number,
1596
+ ) =>
1597
+ Effect.filterOrElse(InitializedDoJournal, Predicate.isNotUndefined, () =>
1598
+ ensureCurrentStorage(sql, failpoint, maxStoredValueBytes),
1599
+ );
@@ -156,7 +156,7 @@ export const ownedRows = <A, I>(
156
156
 
157
157
  const decode = (rows: unknown) =>
158
158
  decodeRows(rows).pipe(
159
- Effect.mapError(() =>
159
+ Effect.mapErrorEager(() =>
160
160
  DoStorageCorruptionError.make({
161
161
  table,
162
162
  rowKey: "owned-state",
@@ -200,9 +200,8 @@ export const ownedRows = <A, I>(
200
200
 
201
201
  if (view !== undefined) return Effect.succeed(view.rows);
202
202
 
203
- return query.pipe(
204
- Effect.flatMap(decode),
205
- Effect.map((rows) => {
203
+ return Effect.flatMap(query, (rows) =>
204
+ Effect.mapEager(decode(rows), (rows) => {
206
205
  retain(id, matches, rows, maxRows);
207
206
 
208
207
  return rows;