@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/dist/index.mjs +117 -85
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/do-conversation-store.ts +20 -15
- package/src/do-journal.ts +3 -4
- package/src/do-ledger.ts +60 -29
- package/src/do-storage-failpoint.ts +1 -1
- package/src/routing.ts +74 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/storage-cloudflare",
|
|
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-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.
|
|
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
|
|
556
|
+
if (isDoFenceRejected(error)) {
|
|
554
557
|
return mapFence(validated.conversationId, error);
|
|
555
558
|
}
|
|
556
|
-
if (error
|
|
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
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
2355
|
+
return NOT_WAITING;
|
|
2341
2356
|
}
|
|
2342
2357
|
if (
|
|
2343
2358
|
!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
|
|
2344
2359
|
) {
|
|
2345
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
2557
|
-
|
|
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
|
-
|
|
2695
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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,
|
|
3
|
+
import type { DoStorageFailpointError, DoStorageFailpointLocation } from "./errors.ts";
|
|
4
4
|
|
|
5
5
|
export type DoStorageFailpointHandler = (
|
|
6
6
|
location: DoStorageFailpointLocation,
|