@effect-agent/storage-sqlite 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/dist/index.mjs +33 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/sqlite-conversation-store.ts +20 -15
- package/src/sqlite-ledger.ts +62 -31
- package/src/sqlite-storage-failpoint.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/storage-sqlite",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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.
|
|
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
|
|
548
|
+
if (isSqliteFenceRejected(error)) {
|
|
546
549
|
return mapFence(validated.conversationId, error);
|
|
547
550
|
}
|
|
548
|
-
if (error
|
|
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
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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;
|
package/src/sqlite-ledger.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
2246
|
+
return NOT_WAITING;
|
|
2232
2247
|
}
|
|
2233
2248
|
if (
|
|
2234
2249
|
!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
|
|
2235
2250
|
) {
|
|
2236
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
2451
|
-
|
|
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
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
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,
|
|
3
|
+
import type { SqliteStorageFailpointError, SqliteStorageFailpointLocation } from "./errors.ts";
|
|
4
4
|
|
|
5
5
|
export type SqliteStorageFailpointHandler = (
|
|
6
6
|
location: SqliteStorageFailpointLocation,
|