@effect-agent/storage-sqlite 0.1.0-beta.24 → 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-sqlite",
3
- "version": "0.1.0-beta.24",
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.24",
11
+ "@effect-agent/session": "0.1.0-beta.25",
12
12
  "@effect/platform-node": "4.0.0-rc.110",
13
13
  "@effect/sql-sqlite-node": "4.0.0-rc.110",
14
14
  "effect": "4.0.0-rc.110"
@@ -43,12 +43,12 @@ import {
43
43
  } from "effect";
44
44
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
45
45
 
46
+ import type { SqliteStorageCompatibilityError } from "./errors.ts";
46
47
  import {
47
48
  SqliteAppendConflict,
48
49
  SqliteCheckpointConflict,
49
50
  SqliteFenceRejected,
50
51
  type SqliteStorageFailpointLocation,
51
- SqliteStorageCompatibilityError,
52
52
  SqliteStorageCorruptionError,
53
53
  SqliteStorageError,
54
54
  } from "./errors.ts";
@@ -93,6 +93,9 @@ const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
93
93
  const SQLITE_OFFSET_PREFIX = "effect-agent-sqlite@1:";
94
94
  const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
95
95
  const isDigest = Schema.is(Digest);
96
+ const isSqliteFenceRejected = Schema.is(SqliteFenceRejected);
97
+ const isSqliteAppendConflict = Schema.is(SqliteAppendConflict);
98
+ const isSqliteCheckpointConflict = Schema.is(SqliteCheckpointConflict);
96
99
 
97
100
  const storeError = (operation: string, error: { readonly message: string }) =>
98
101
  ConversationStoreError.make({
@@ -542,10 +545,10 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
542
545
  yield* hitFailpoint("append:before");
543
546
  const result = yield* journal.append(rawRequest).pipe(
544
547
  Effect.mapError((error) => {
545
- if (error instanceof SqliteFenceRejected) {
548
+ if (isSqliteFenceRejected(error)) {
546
549
  return mapFence(validated.conversationId, error);
547
550
  }
548
- if (error instanceof SqliteAppendConflict) {
551
+ if (isSqliteAppendConflict(error)) {
549
552
  return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
550
553
  ? AppendConflict.make({
551
554
  conversationId: validated.conversationId,
@@ -713,7 +716,7 @@ const makeServices = Effect.fn("SqliteConversationStore.makeServices")(function*
713
716
  yield* hitFailpoint("save-checkpoint:before");
714
717
  yield* journal.saveCheckpoint(raw).pipe(
715
718
  Effect.mapError((error) =>
716
- error instanceof SqliteCheckpointConflict
719
+ isSqliteCheckpointConflict(error)
717
720
  ? CheckpointRejected.make({
718
721
  conversationId: validated.checkpoint.conversationId,
719
722
  reason: "digest-mismatch",
@@ -823,19 +826,21 @@ export const storageFailpointLayer = (
823
826
  */
824
827
  export const layer = (
825
828
  options: SqliteStorageOptions,
826
- ): Layer.Layer<ConversationStore, SqliteStorageInitializationError> => {
827
- const sqlLayer = SqliteClient.layer({ filename: options.filename });
828
- return conversationStoreLayer.pipe(
829
- Layer.provide(
830
- Layer.mergeAll(
831
- storageConfigLayer(options),
832
- storageFailpointLayer(options),
833
- sqlLayer,
834
- NodeCrypto.layer,
829
+ ): Layer.Layer<ConversationStore, SqliteStorageInitializationError> =>
830
+ Layer.unwrap(
831
+ Effect.map(SqliteStorageConfig, (config) =>
832
+ conversationStoreLayer.pipe(
833
+ Layer.provide(
834
+ Layer.mergeAll(
835
+ Layer.succeed(SqliteStorageConfig)(config),
836
+ storageFailpointLayer(options),
837
+ SqliteClient.layer({ filename: options.filename }),
838
+ NodeCrypto.layer,
839
+ ),
840
+ ),
835
841
  ),
836
842
  ),
837
- );
838
- };
843
+ ).pipe(Layer.provide(storageConfigLayer(options)));
839
844
 
840
845
  /** Create an adapter-owned resumable observation offset for a known canonical sequence. */
841
846
  export const observationOffsetAt = makeOffset;
@@ -105,6 +105,11 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
105
105
 
106
106
  const SCAN_PAGE_SIZE = 256;
107
107
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
108
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
109
+ const SUSPENDED: SuspensionOutcome = "suspended";
110
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
111
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
112
+ const WOKEN: ChildSettledOutcome = "woken";
108
113
 
109
114
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
110
115
  submission_id: BoundedIdentifier,
@@ -276,6 +281,11 @@ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
276
281
  ChildBudgetReservationSnapshot,
277
282
  );
278
283
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
284
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
285
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
286
+ const isSqliteTransactionFailure = Schema.is(
287
+ Schema.Union([SqliteStorageError, SqliteWriteContention]),
288
+ );
279
289
 
280
290
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
281
291
  const internalFailure =
@@ -343,9 +353,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
343
353
  .withWriteTransaction(operation)(effect)
344
354
  .pipe(
345
355
  Effect.mapError((error) =>
346
- error instanceof SqliteStorageError || error instanceof SqliteWriteContention
347
- ? internalFailure(operation)(error)
348
- : error,
356
+ isSqliteTransactionFailure(error) ? internalFailure(operation)(error) : error,
349
357
  ),
350
358
  );
351
359
 
@@ -1749,7 +1757,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1749
1757
  }).pipe(Effect.mapError(internalFailure(operation))),
1750
1758
  );
1751
1759
  }
1752
- return claimed as ReadonlyArray<JoiningClaim>;
1760
+ return claimed;
1753
1761
  }),
1754
1762
  );
1755
1763
  yield* hitFailpoint("ledger:claim-joining:after", operation);
@@ -1882,7 +1890,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1882
1890
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1883
1891
  const decided = new Set(decisions.map((row) => row.tool_call_id));
1884
1892
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1885
- return "resume-immediately" as SuspensionOutcome;
1893
+ return RESUME_IMMEDIATELY;
1886
1894
  }
1887
1895
  } else {
1888
1896
  let allSettled = true;
@@ -1894,7 +1902,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1894
1902
  }
1895
1903
  }
1896
1904
  if (allSettled) {
1897
- return "resume-immediately" as SuspensionOutcome;
1905
+ return RESUME_IMMEDIATELY;
1898
1906
  }
1899
1907
  }
1900
1908
  const now = yield* currentInstant;
@@ -1912,7 +1920,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1912
1920
  DELETE FROM effect_agent_submission_ownership
1913
1921
  WHERE submission_id = ${validated.submissionId}
1914
1922
  `.pipe(Effect.mapError(sqlFailure(operation)));
1915
- return "suspended" as SuspensionOutcome;
1923
+ return SUSPENDED;
1916
1924
  }),
1917
1925
  );
1918
1926
  yield* hitFailpoint("ledger:suspend:after", operation);
@@ -2118,17 +2126,24 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2118
2126
  }
2119
2127
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2120
2128
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2121
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2129
+ const existingIntent =
2130
+ existing === undefined
2131
+ ? undefined
2132
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2133
+ if (
2134
+ existingIntent !== undefined &&
2135
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2136
+ ) {
2122
2137
  return yield* UnknownResolutionConflict.make({
2123
2138
  submissionId: validated.submissionId,
2124
2139
  toolCallId: validated.toolCallId,
2125
2140
  });
2126
2141
  }
2127
2142
  let resolved: UnknownResolutionIntent;
2128
- if (existing !== undefined) {
2143
+ if (existingIntent !== undefined) {
2129
2144
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2130
2145
  // audit fields win, exactly like requestAbort).
2131
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2146
+ resolved = existingIntent;
2132
2147
  } else {
2133
2148
  const now = yield* currentInstant;
2134
2149
  yield* sql`
@@ -2213,7 +2228,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2213
2228
  });
2214
2229
  }
2215
2230
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2216
- return "not-waiting" as ChildSettledOutcome;
2231
+ return NOT_WAITING;
2217
2232
  }
2218
2233
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2219
2234
  parent.suspended_reason_json,
@@ -2228,12 +2243,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2228
2243
  ),
2229
2244
  );
2230
2245
  if (reason._tag !== "WaitingForChild") {
2231
- return "not-waiting" as ChildSettledOutcome;
2246
+ return NOT_WAITING;
2232
2247
  }
2233
2248
  if (
2234
2249
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2235
2250
  ) {
2236
- return "not-waiting" as ChildSettledOutcome;
2251
+ return NOT_WAITING;
2237
2252
  }
2238
2253
  // Every listed child must be either finalized or canonically announced from the exact
2239
2254
  // terminalizing reservation. Replays re-run this coverage check idempotently.
@@ -2245,7 +2260,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2245
2260
  (listed.value.state === "settled" ||
2246
2261
  (listed.value.state === "terminalizing" && Option.isSome(reservation)));
2247
2262
  if (!covered) {
2248
- return "still-waiting" as ChildSettledOutcome;
2263
+ return STILL_WAITING;
2249
2264
  }
2250
2265
  }
2251
2266
  yield* sql`
@@ -2256,7 +2271,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2256
2271
  suspended_at = NULL
2257
2272
  WHERE submission_id = ${validated.parentSubmissionId}
2258
2273
  `.pipe(Effect.mapError(sqlFailure(operation)));
2259
- return "woken" as ChildSettledOutcome;
2274
+ return WOKEN;
2260
2275
  }),
2261
2276
  );
2262
2277
  yield* hitFailpoint("ledger:child-settled:after", operation);
@@ -2279,13 +2294,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2279
2294
  Effect.gen(function* () {
2280
2295
  const existing = yield* readChildReservation(operation, validated.reservationId);
2281
2296
  if (Option.isSome(existing)) {
2297
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2298
+ operation,
2299
+ existing.value,
2300
+ );
2282
2301
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2283
2302
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2284
2303
  const identical =
2285
2304
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2286
2305
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2287
2306
  existing.value.allocation_digest === validated.allocationDigest &&
2288
- existing.value.allocation_json === allocationJson;
2307
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2289
2308
  if (!identical) {
2290
2309
  return yield* ChildReservationConflict.make({
2291
2310
  reservationId: validated.reservationId,
@@ -2295,7 +2314,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2295
2314
  });
2296
2315
  }
2297
2316
  return ReservedChildBudget.make({
2298
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2317
+ reservation: existingSnapshot,
2299
2318
  replayed: true,
2300
2319
  });
2301
2320
  }
@@ -2445,10 +2464,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2445
2464
  });
2446
2465
  }
2447
2466
  if (existing.value.status !== "reserved") {
2467
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2468
+ operation,
2469
+ existing.value,
2470
+ );
2448
2471
  // The accounting decision was already frozen exactly once; an identical replay is a
2449
2472
  // no-op and a divergent decision conflicts (spec §12 join step 6).
2450
- if (existing.value.accounting_json === accountingJson) {
2451
- return yield* childReservationSnapshotFromRow(operation, existing.value);
2473
+ if (
2474
+ existingSnapshot.accounting !== undefined &&
2475
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
2476
+ ) {
2477
+ return existingSnapshot;
2452
2478
  }
2453
2479
  return yield* ChildReservationConflict.make({
2454
2480
  reservationId: validated.reservationId,
@@ -2584,10 +2610,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2584
2610
  return [snapshots, next] as const;
2585
2611
  });
2586
2612
 
2587
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2588
- undefined as ScanCursor | undefined,
2589
- scanPage,
2590
- );
2613
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
2614
+ ScanCursor | undefined,
2615
+ SubmissionSnapshot,
2616
+ LedgerError
2617
+ >(undefined, scanPage);
2591
2618
 
2592
2619
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2593
2620
  "SqliteSubmissionLedger.loadRecoverySnapshot",
@@ -2841,13 +2868,17 @@ export const submissionLedgerLayer: Layer.Layer<
2841
2868
  export const ledgerLayer = (
2842
2869
  options: SqliteStorageOptions,
2843
2870
  ): Layer.Layer<SubmissionLedger, SqliteStorageInitializationError> =>
2844
- submissionLedgerLayer.pipe(
2845
- Layer.provide(
2846
- Layer.mergeAll(
2847
- storageConfigLayer(options),
2848
- storageFailpointLayer(options),
2849
- SqliteClient.layer({ filename: options.filename }),
2850
- NodeCrypto.layer,
2871
+ Layer.unwrap(
2872
+ Effect.map(SqliteStorageConfig, (config) =>
2873
+ submissionLedgerLayer.pipe(
2874
+ Layer.provide(
2875
+ Layer.mergeAll(
2876
+ Layer.succeed(SqliteStorageConfig)(config),
2877
+ storageFailpointLayer(options),
2878
+ SqliteClient.layer({ filename: options.filename }),
2879
+ NodeCrypto.layer,
2880
+ ),
2881
+ ),
2851
2882
  ),
2852
2883
  ),
2853
- );
2884
+ ).pipe(Layer.provide(storageConfigLayer(options)));
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Layer, Ref } from "effect";
2
2
 
3
- import { SqliteStorageFailpointError, type SqliteStorageFailpointLocation } from "./errors.ts";
3
+ import type { SqliteStorageFailpointError, SqliteStorageFailpointLocation } from "./errors.ts";
4
4
 
5
5
  export type SqliteStorageFailpointHandler = (
6
6
  location: SqliteStorageFailpointLocation,