@effect-agent/storage-cloudflare 0.1.0-beta.23 → 0.1.0-beta.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/storage-cloudflare",
3
- "version": "0.1.0-beta.23",
3
+ "version": "0.1.0-beta.25",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -8,7 +8,7 @@
8
8
  }
9
9
  },
10
10
  "dependencies": {
11
- "@effect-agent/session": "0.1.0-beta.23",
11
+ "@effect-agent/session": "0.1.0-beta.25",
12
12
  "@effect/platform-browser": "4.0.0-rc.110",
13
13
  "@effect/sql-sqlite-do": "4.0.0-rc.110",
14
14
  "effect": "4.0.0-rc.110"
@@ -36,7 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@cloudflare/vitest-pool-workers": "0.21.3",
38
38
  "@cloudflare/workers-types": "5.20260813.1",
39
- "@effect-agent/testing": "0.1.0-beta.22",
39
+ "@effect-agent/testing": "0.1.0-beta.23",
40
40
  "@effect/vitest": "4.0.0-rc.110",
41
41
  "typescript": "7.0.2",
42
42
  "vite-plus": "0.2.6",
@@ -56,12 +56,12 @@ import {
56
56
  DoStorageConfigValue,
57
57
  } from "./do-storage-config.ts";
58
58
  import { DoStorageFailpoint, type DoStorageFailpointHandler } from "./do-storage-failpoint.ts";
59
+ import type { DoStorageCompatibilityError } from "./errors.ts";
59
60
  import {
60
61
  DoAppendConflict,
61
62
  DoCheckpointConflict,
62
63
  DoFenceRejected,
63
64
  type DoStorageFailpointLocation,
64
- DoStorageCompatibilityError,
65
65
  DoStorageCorruptionError,
66
66
  DoStorageError,
67
67
  } from "./errors.ts";
@@ -102,6 +102,9 @@ const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
102
102
  const DO_OFFSET_PREFIX = "effect-agent-do@1:";
103
103
  const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
104
104
  const isDigest = Schema.is(Digest);
105
+ const isDoFenceRejected = Schema.is(DoFenceRejected);
106
+ const isDoAppendConflict = Schema.is(DoAppendConflict);
107
+ const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
105
108
 
106
109
  const storeError = (operation: string, error: { readonly message: string }) =>
107
110
  ConversationStoreError.make({
@@ -550,10 +553,10 @@ const makeServices = Effect.fn("DoConversationStore.makeServices")(function* ()
550
553
  yield* hitFailpoint("append:before");
551
554
  const result = yield* journal.append(rawRequest).pipe(
552
555
  Effect.mapError((error) => {
553
- if (error instanceof DoFenceRejected) {
556
+ if (isDoFenceRejected(error)) {
554
557
  return mapFence(validated.conversationId, error);
555
558
  }
556
- if (error instanceof DoAppendConflict) {
559
+ if (isDoAppendConflict(error)) {
557
560
  return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
558
561
  ? AppendConflict.make({
559
562
  conversationId: validated.conversationId,
@@ -720,7 +723,7 @@ const makeServices = Effect.fn("DoConversationStore.makeServices")(function* ()
720
723
  yield* hitFailpoint("save-checkpoint:before");
721
724
  yield* journal.saveCheckpoint(raw).pipe(
722
725
  Effect.mapError((error) =>
723
- error instanceof DoCheckpointConflict
726
+ isDoCheckpointConflict(error)
724
727
  ? CheckpointRejected.make({
725
728
  conversationId: validated.checkpoint.conversationId,
726
729
  reason: "digest-mismatch",
@@ -832,19 +835,21 @@ export const storageFailpointLayer = (
832
835
  */
833
836
  export const layer = (
834
837
  options: DoStorageOptions,
835
- ): Layer.Layer<ConversationStore, DoStorageInitializationError> => {
836
- const sqlLayer = SqliteClient.layer({ storage: options.storage });
837
- return conversationStoreLayer.pipe(
838
- Layer.provide(
839
- Layer.mergeAll(
840
- storageConfigLayer(options),
841
- storageFailpointLayer(options),
842
- sqlLayer,
843
- BrowserCrypto.layer,
838
+ ): Layer.Layer<ConversationStore, DoStorageInitializationError> =>
839
+ Layer.unwrap(
840
+ Effect.map(DoStorageConfig, (config) =>
841
+ conversationStoreLayer.pipe(
842
+ Layer.provide(
843
+ Layer.mergeAll(
844
+ Layer.succeed(DoStorageConfig)(config),
845
+ storageFailpointLayer(options),
846
+ SqliteClient.layer({ storage: options.storage }),
847
+ BrowserCrypto.layer,
848
+ ),
849
+ ),
844
850
  ),
845
851
  ),
846
- );
847
- };
852
+ ).pipe(Layer.provide(storageConfigLayer(options)));
848
853
 
849
854
  /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
850
855
  export const observationOffsetAt = makeOffset;
package/src/do-journal.ts CHANGED
@@ -4,6 +4,7 @@ import { Effect, Schema } from "effect";
4
4
  import * as SqlClient from "effect/unstable/sql/SqlClient";
5
5
  import { SqlError } from "effect/unstable/sql/SqlError";
6
6
 
7
+ import type { DoStorageFailpointError } from "./errors.ts";
7
8
  import {
8
9
  DoAppendConflict,
9
10
  DoCheckpointConflict,
@@ -11,7 +12,6 @@ import {
11
12
  DoStorageCompatibilityError,
12
13
  DoStorageCorruptionError,
13
14
  DoStorageError,
14
- DoStorageFailpointError,
15
15
  DoValueBoundExceeded,
16
16
  type DoStorageFailpointLocation,
17
17
  } from "./errors.ts";
@@ -29,6 +29,7 @@ const MAX_RECORDS_PER_CONVERSATION = 65_536;
29
29
  const MAX_IDENTIFIER_LENGTH = 1_024;
30
30
  /** Durable Object SQL storage allows at most 100 bound parameters per statement. */
31
31
  const MAX_BOUND_PARAMETERS = 100;
32
+ const isSqlError = Schema.is(SqlError);
32
33
 
33
34
  const storedTextBytes = (value: string): number => new TextEncoder().encode(value).byteLength;
34
35
 
@@ -378,9 +379,7 @@ const makeJournal = (
378
379
  (operation: string) =>
379
380
  <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E | DoStorageError> =>
380
381
  sql.withTransaction(effect).pipe(
381
- Effect.mapError((error) =>
382
- error instanceof SqlError ? storageError(operation)(error) : error,
383
- ),
382
+ Effect.mapError((error) => (isSqlError(error) ? storageError(operation)(error) : error)),
384
383
  Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }),
385
384
  );
386
385
 
package/src/do-ledger.ts CHANGED
@@ -108,6 +108,11 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
108
108
 
109
109
  const SCAN_PAGE_SIZE = 256;
110
110
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
111
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
112
+ const SUSPENDED: SuspensionOutcome = "suspended";
113
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
114
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
115
+ const WOKEN: ChildSettledOutcome = "woken";
111
116
  const MAX_IDENTIFIER_LENGTH = 1_024;
112
117
 
113
118
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
@@ -289,6 +294,9 @@ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
289
294
  ChildBudgetReservationSnapshot,
290
295
  );
291
296
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
297
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
298
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
299
+ const isDoStorageError = Schema.is(DoStorageError);
292
300
 
293
301
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
294
302
  const internalFailure =
@@ -352,7 +360,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
352
360
  .withWriteTransaction(operation)(effect)
353
361
  .pipe(
354
362
  Effect.mapError((error) =>
355
- error instanceof DoStorageError ? internalFailure(operation)(error) : error,
363
+ isDoStorageError(error) ? internalFailure(operation)(error) : error,
356
364
  ),
357
365
  );
358
366
 
@@ -1819,7 +1827,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1819
1827
  }).pipe(Effect.mapError(internalFailure(operation))),
1820
1828
  );
1821
1829
  }
1822
- return claimed as ReadonlyArray<JoiningClaim>;
1830
+ return claimed;
1823
1831
  }),
1824
1832
  );
1825
1833
  yield* hitFailpoint("ledger:claim-joining:after", operation);
@@ -1954,7 +1962,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1954
1962
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1955
1963
  const decided = new Set(decisions.map((row) => row.tool_call_id));
1956
1964
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1957
- return "resume-immediately" as SuspensionOutcome;
1965
+ return RESUME_IMMEDIATELY;
1958
1966
  }
1959
1967
  } else {
1960
1968
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
@@ -1972,7 +1980,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1972
1980
  }
1973
1981
  }
1974
1982
  if (allSettled) {
1975
- return "resume-immediately" as SuspensionOutcome;
1983
+ return RESUME_IMMEDIATELY;
1976
1984
  }
1977
1985
  }
1978
1986
  const now = yield* currentInstant;
@@ -1990,7 +1998,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1990
1998
  DELETE FROM effect_agent_submission_ownership
1991
1999
  WHERE submission_id = ${validated.submissionId}
1992
2000
  `.pipe(Effect.mapError(sqlFailure(operation)));
1993
- return "suspended" as SuspensionOutcome;
2001
+ return SUSPENDED;
1994
2002
  }),
1995
2003
  );
1996
2004
  yield* hitFailpoint("ledger:suspend:after", operation);
@@ -2198,17 +2206,24 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2198
2206
  }
2199
2207
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2200
2208
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2201
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2209
+ const existingIntent =
2210
+ existing === undefined
2211
+ ? undefined
2212
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2213
+ if (
2214
+ existingIntent !== undefined &&
2215
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2216
+ ) {
2202
2217
  return yield* UnknownResolutionConflict.make({
2203
2218
  submissionId: validated.submissionId,
2204
2219
  toolCallId: validated.toolCallId,
2205
2220
  });
2206
2221
  }
2207
2222
  let resolved: UnknownResolutionIntent;
2208
- if (existing !== undefined) {
2223
+ if (existingIntent !== undefined) {
2209
2224
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2210
2225
  // audit fields win, exactly like requestAbort).
2211
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2226
+ resolved = existingIntent;
2212
2227
  } else {
2213
2228
  const now = yield* currentInstant;
2214
2229
  yield* sql`
@@ -2322,7 +2337,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2322
2337
  `.pipe(Effect.mapError(sqlFailure(operation)));
2323
2338
 
2324
2339
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2325
- return "not-waiting" as ChildSettledOutcome;
2340
+ return NOT_WAITING;
2326
2341
  }
2327
2342
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2328
2343
  parent.suspended_reason_json,
@@ -2337,12 +2352,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2337
2352
  ),
2338
2353
  );
2339
2354
  if (reason._tag !== "WaitingForChild") {
2340
- return "not-waiting" as ChildSettledOutcome;
2355
+ return NOT_WAITING;
2341
2356
  }
2342
2357
  if (
2343
2358
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2344
2359
  ) {
2345
- return "not-waiting" as ChildSettledOutcome;
2360
+ return NOT_WAITING;
2346
2361
  }
2347
2362
  // The parent wakes exactly when EVERY listed child is provably settled — from its
2348
2363
  // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
@@ -2356,7 +2371,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2356
2371
  entry.childSubmissionId,
2357
2372
  );
2358
2373
  if (!settled) {
2359
- return "still-waiting" as ChildSettledOutcome;
2374
+ return STILL_WAITING;
2360
2375
  }
2361
2376
  }
2362
2377
  yield* sql`
@@ -2367,7 +2382,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2367
2382
  suspended_at = NULL
2368
2383
  WHERE submission_id = ${validated.parentSubmissionId}
2369
2384
  `.pipe(Effect.mapError(sqlFailure(operation)));
2370
- return "woken" as ChildSettledOutcome;
2385
+ return WOKEN;
2371
2386
  }),
2372
2387
  );
2373
2388
  yield* hitFailpoint("ledger:child-settled:after", operation);
@@ -2390,13 +2405,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2390
2405
  Effect.gen(function* () {
2391
2406
  const existing = yield* readChildReservation(operation, validated.reservationId);
2392
2407
  if (Option.isSome(existing)) {
2408
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2409
+ operation,
2410
+ existing.value,
2411
+ );
2393
2412
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2394
2413
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2395
2414
  const identical =
2396
2415
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2397
2416
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2398
2417
  existing.value.allocation_digest === validated.allocationDigest &&
2399
- existing.value.allocation_json === allocationJson;
2418
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2400
2419
  if (!identical) {
2401
2420
  return yield* ChildReservationConflict.make({
2402
2421
  reservationId: validated.reservationId,
@@ -2406,7 +2425,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2406
2425
  });
2407
2426
  }
2408
2427
  return ReservedChildBudget.make({
2409
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2428
+ reservation: existingSnapshot,
2410
2429
  replayed: true,
2411
2430
  });
2412
2431
  }
@@ -2551,10 +2570,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2551
2570
  });
2552
2571
  }
2553
2572
  if (existing.value.status !== "reserved") {
2573
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2574
+ operation,
2575
+ existing.value,
2576
+ );
2554
2577
  // The accounting decision was already frozen exactly once; an identical replay is a
2555
2578
  // no-op and a divergent decision conflicts (spec §12 join step 6).
2556
- if (existing.value.accounting_json === accountingJson) {
2557
- return yield* childReservationSnapshotFromRow(operation, existing.value);
2579
+ if (
2580
+ existingSnapshot.accounting !== undefined &&
2581
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
2582
+ ) {
2583
+ return existingSnapshot;
2558
2584
  }
2559
2585
  return yield* ChildReservationConflict.make({
2560
2586
  reservationId: validated.reservationId,
@@ -2690,10 +2716,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2690
2716
  return [snapshots, next] as const;
2691
2717
  });
2692
2718
 
2693
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2694
- undefined as ScanCursor | undefined,
2695
- scanPage,
2696
- );
2719
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
2720
+ ScanCursor | undefined,
2721
+ SubmissionSnapshot,
2722
+ LedgerError
2723
+ >(undefined, scanPage);
2697
2724
 
2698
2725
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2699
2726
  "DoSubmissionLedger.loadRecoverySnapshot",
@@ -2964,13 +2991,17 @@ export const submissionLedgerLayer: Layer.Layer<
2964
2991
  export const ledgerLayer = (
2965
2992
  options: DoStorageOptions,
2966
2993
  ): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
2967
- submissionLedgerLayer.pipe(
2968
- Layer.provide(
2969
- Layer.mergeAll(
2970
- storageConfigLayer(options),
2971
- storageFailpointLayer(options),
2972
- SqliteClient.layer({ storage: options.storage }),
2973
- BrowserCrypto.layer,
2994
+ Layer.unwrap(
2995
+ Effect.map(DoStorageConfig, (config) =>
2996
+ submissionLedgerLayer.pipe(
2997
+ Layer.provide(
2998
+ Layer.mergeAll(
2999
+ Layer.succeed(DoStorageConfig)(config),
3000
+ storageFailpointLayer(options),
3001
+ SqliteClient.layer({ storage: options.storage }),
3002
+ BrowserCrypto.layer,
3003
+ ),
3004
+ ),
2974
3005
  ),
2975
3006
  ),
2976
- );
3007
+ ).pipe(Layer.provide(storageConfigLayer(options)));
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Layer, Ref } from "effect";
2
2
 
3
- import { DoStorageFailpointError, type DoStorageFailpointLocation } from "./errors.ts";
3
+ import type { DoStorageFailpointError, DoStorageFailpointLocation } from "./errors.ts";
4
4
 
5
5
  export type DoStorageFailpointHandler = (
6
6
  location: DoStorageFailpointLocation,