@effect-agent/storage-sqlite 0.1.0-beta.38 → 0.1.0-beta.39
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.d.mts +17 -114
- package/dist/index.mjs +1036 -393
- package/dist/index.mjs.map +1 -1
- package/dist/sqlite-storage-failpoint-Cr0SXflP.d.mts +95 -0
- package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs +12 -0
- package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs.map +1 -0
- package/dist/testing.d.mts +15 -0
- package/dist/testing.mjs +19 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +12 -5
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -2
- package/src/migrations.ts +105 -24
- package/src/sqlite-journal.ts +147 -156
- package/src/sqlite-ledger.ts +60 -60
- package/src/sqlite-schedule-store.ts +2 -2
- package/src/sqlite-storage-config.ts +1 -1
- package/src/sqlite-storage-failpoint-testing.ts +38 -0
- package/src/sqlite-storage-failpoint.ts +1 -31
- package/src/sqlite-subscription-store.ts +935 -0
- package/src/{sqlite-conversation-store.ts → sqlite-thread-store.ts} +248 -267
- package/src/testing.ts +2 -0
package/src/sqlite-ledger.ts
CHANGED
|
@@ -73,7 +73,7 @@ import {
|
|
|
73
73
|
submissionAbortRecordId,
|
|
74
74
|
type ChildSettledOutcome,
|
|
75
75
|
type SuspensionOutcome,
|
|
76
|
-
} from "@effect-agent/
|
|
76
|
+
} from "@effect-agent/thread";
|
|
77
77
|
import { NodeCrypto } from "@effect/platform-node";
|
|
78
78
|
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
79
79
|
import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
@@ -87,15 +87,15 @@ import {
|
|
|
87
87
|
SqliteWriteContention,
|
|
88
88
|
type SqliteStorageFailpointLocation,
|
|
89
89
|
} from "./errors.ts";
|
|
90
|
+
import { decodeRows, initializeSqliteJournal } from "./sqlite-journal.ts";
|
|
91
|
+
import { SqliteStorageConfig } from "./sqlite-storage-config.ts";
|
|
92
|
+
import { SqliteStorageFailpoint } from "./sqlite-storage-failpoint.ts";
|
|
90
93
|
import {
|
|
91
94
|
storageConfigLayer,
|
|
92
95
|
storageFailpointLayer,
|
|
93
96
|
type SqliteStorageInitializationError,
|
|
94
97
|
type SqliteStorageOptions,
|
|
95
|
-
} from "./sqlite-
|
|
96
|
-
import { decodeRows, initializeSqliteJournal } from "./sqlite-journal.ts";
|
|
97
|
-
import { SqliteStorageConfig } from "./sqlite-storage-config.ts";
|
|
98
|
-
import { SqliteStorageFailpoint } from "./sqlite-storage-failpoint.ts";
|
|
98
|
+
} from "./sqlite-thread-store.ts";
|
|
99
99
|
|
|
100
100
|
type SubmissionId = SubmissionSnapshot["submissionId"];
|
|
101
101
|
|
|
@@ -113,7 +113,7 @@ const WOKEN: ChildSettledOutcome = "woken";
|
|
|
113
113
|
|
|
114
114
|
class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
115
115
|
submission_id: BoundedIdentifier,
|
|
116
|
-
|
|
116
|
+
thread_id: BoundedIdentifier,
|
|
117
117
|
queue_sequence: QueueSequence,
|
|
118
118
|
principal: BoundedIdentifier,
|
|
119
119
|
idempotency_key: BoundedIdentifier,
|
|
@@ -208,7 +208,7 @@ class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("Canonical
|
|
|
208
208
|
|
|
209
209
|
const SUBMISSION_COLUMNS = `
|
|
210
210
|
submission_id,
|
|
211
|
-
|
|
211
|
+
thread_id,
|
|
212
212
|
queue_sequence,
|
|
213
213
|
principal,
|
|
214
214
|
idempotency_key,
|
|
@@ -247,7 +247,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
247
247
|
released_at
|
|
248
248
|
`;
|
|
249
249
|
|
|
250
|
-
/** The branded ToolCallId schema, reached through the
|
|
250
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
251
251
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
252
252
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
253
253
|
|
|
@@ -448,20 +448,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
448
448
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
-
const
|
|
451
|
+
const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (
|
|
452
452
|
operation: string,
|
|
453
|
-
|
|
453
|
+
threadId: string,
|
|
454
454
|
): Effect.fn.Return<ProducerEpoch, LedgerError> {
|
|
455
|
-
const
|
|
456
|
-
.
|
|
455
|
+
const threads = yield* journal
|
|
456
|
+
.getThread(threadId)
|
|
457
457
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
458
|
-
return
|
|
458
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
459
459
|
});
|
|
460
460
|
|
|
461
461
|
/**
|
|
462
462
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
463
463
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
464
|
-
*
|
|
464
|
+
* Thread's current producer epoch (DUR-006).
|
|
465
465
|
*/
|
|
466
466
|
const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (
|
|
467
467
|
operation: string,
|
|
@@ -470,7 +470,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
470
470
|
): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
|
|
471
471
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
472
472
|
if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
|
|
473
|
-
const actualEpoch = yield*
|
|
473
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
474
474
|
const submissionId = yield* Schema.decodeUnknownEffect(
|
|
475
475
|
SubmissionSnapshot.fields.submissionId,
|
|
476
476
|
)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
@@ -514,7 +514,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
514
514
|
}
|
|
515
515
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
516
516
|
submissionId: row.submission_id,
|
|
517
|
-
|
|
517
|
+
threadId: row.thread_id,
|
|
518
518
|
queueSequence: row.queue_sequence,
|
|
519
519
|
principal: row.principal,
|
|
520
520
|
idempotencyKey: row.idempotency_key,
|
|
@@ -847,20 +847,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
847
847
|
const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(
|
|
848
848
|
function* (
|
|
849
849
|
operation: string,
|
|
850
|
-
|
|
850
|
+
threadId: string,
|
|
851
851
|
submissionId: SubmissionId,
|
|
852
852
|
): Effect.fn.Return<string | undefined, LedgerError> {
|
|
853
853
|
const recordId = submissionAbortRecordId(submissionId);
|
|
854
854
|
const rows = yield* sql<Record<string, unknown>>`
|
|
855
855
|
SELECT record_id
|
|
856
856
|
FROM effect_agent_canonical_records
|
|
857
|
-
WHERE
|
|
857
|
+
WHERE thread_id = ${threadId}
|
|
858
858
|
AND record_id = ${recordId}
|
|
859
859
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
860
860
|
const decoded = yield* decodeRows(
|
|
861
861
|
Schema.Array(CanonicalRecordIdRow),
|
|
862
862
|
"effect_agent_canonical_records",
|
|
863
|
-
`${
|
|
863
|
+
`${threadId}/${recordId}`,
|
|
864
864
|
rows,
|
|
865
865
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
866
866
|
return decoded.length === 0 ? undefined : recordId;
|
|
@@ -875,7 +875,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
875
875
|
): Effect.fn.Return<AbortIntent, LedgerError> {
|
|
876
876
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
877
877
|
operation,
|
|
878
|
-
submission.
|
|
878
|
+
submission.thread_id,
|
|
879
879
|
submissionId,
|
|
880
880
|
);
|
|
881
881
|
return yield* decodeAbortIntent({
|
|
@@ -916,11 +916,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
916
916
|
const result = yield* inWriteTransaction(
|
|
917
917
|
operation,
|
|
918
918
|
Effect.gen(function* () {
|
|
919
|
-
const keyRowKey = `${validated.
|
|
919
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
920
920
|
const existingRows = yield* sql<Record<string, unknown>>`
|
|
921
921
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
922
922
|
FROM effect_agent_submissions
|
|
923
|
-
WHERE
|
|
923
|
+
WHERE thread_id = ${validated.threadId}
|
|
924
924
|
AND principal = ${validated.principal}
|
|
925
925
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
926
926
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
@@ -944,7 +944,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
944
944
|
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
945
945
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
946
946
|
return yield* AdmissionConflict.make({
|
|
947
|
-
|
|
947
|
+
threadId: validated.threadId,
|
|
948
948
|
principal: validated.principal,
|
|
949
949
|
idempotencyKey: validated.idempotencyKey,
|
|
950
950
|
existingInputDigest: existing[0].input_digest,
|
|
@@ -963,12 +963,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
963
963
|
const maxRows = yield* sql<Record<string, unknown>>`
|
|
964
964
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
965
965
|
FROM effect_agent_submissions
|
|
966
|
-
WHERE
|
|
966
|
+
WHERE thread_id = ${validated.threadId}
|
|
967
967
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
968
968
|
const decodedMax = yield* decodeRows(
|
|
969
969
|
Schema.Array(MaxQueueSequenceRow),
|
|
970
970
|
"effect_agent_submissions",
|
|
971
|
-
validated.
|
|
971
|
+
validated.threadId,
|
|
972
972
|
maxRows,
|
|
973
973
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
974
974
|
const queueSequence = yield* decodeQueueSequence(
|
|
@@ -979,7 +979,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
979
979
|
yield* sql`
|
|
980
980
|
INSERT INTO effect_agent_submissions (
|
|
981
981
|
submission_id,
|
|
982
|
-
|
|
982
|
+
thread_id,
|
|
983
983
|
queue_sequence,
|
|
984
984
|
principal,
|
|
985
985
|
idempotency_key,
|
|
@@ -995,7 +995,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
995
995
|
parent_tool_call_id
|
|
996
996
|
) VALUES (
|
|
997
997
|
${mintedSubmissionId},
|
|
998
|
-
${validated.
|
|
998
|
+
${validated.threadId},
|
|
999
999
|
${queueSequence},
|
|
1000
1000
|
${validated.principal},
|
|
1001
1001
|
${validated.idempotencyKey},
|
|
@@ -1064,20 +1064,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1064
1064
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1065
1065
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1066
1066
|
FROM effect_agent_submissions
|
|
1067
|
-
WHERE
|
|
1067
|
+
WHERE thread_id = ${validated.threadId}
|
|
1068
1068
|
AND principal = ${validated.principal}
|
|
1069
1069
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1070
1070
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1071
1071
|
const decoded = yield* decodeSubmissionRows(
|
|
1072
1072
|
operation,
|
|
1073
|
-
`${validated.
|
|
1073
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1074
1074
|
rows,
|
|
1075
1075
|
);
|
|
1076
1076
|
if (decoded.length > 1) {
|
|
1077
1077
|
return yield* corruptionFailure(
|
|
1078
1078
|
operation,
|
|
1079
1079
|
"effect_agent_submissions",
|
|
1080
|
-
`${validated.
|
|
1080
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1081
1081
|
"An admission idempotency key returned more than one row.",
|
|
1082
1082
|
);
|
|
1083
1083
|
}
|
|
@@ -1099,20 +1099,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1099
1099
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1100
1100
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1101
1101
|
FROM effect_agent_submissions
|
|
1102
|
-
WHERE
|
|
1102
|
+
WHERE thread_id = ${validated.threadId}
|
|
1103
1103
|
AND principal = ${validated.principal}
|
|
1104
1104
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1105
1105
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1106
1106
|
const decoded = yield* decodeSubmissionRows(
|
|
1107
1107
|
operation,
|
|
1108
|
-
`${validated.
|
|
1108
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1109
1109
|
rows,
|
|
1110
1110
|
);
|
|
1111
1111
|
if (decoded.length > 1) {
|
|
1112
1112
|
return yield* corruptionFailure(
|
|
1113
1113
|
operation,
|
|
1114
1114
|
"effect_agent_submissions",
|
|
1115
|
-
`${validated.
|
|
1115
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1116
1116
|
"An admission idempotency key returned more than one row.",
|
|
1117
1117
|
);
|
|
1118
1118
|
}
|
|
@@ -1137,12 +1137,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1137
1137
|
const headRows = yield* sql<Record<string, unknown>>`
|
|
1138
1138
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1139
1139
|
FROM effect_agent_submissions
|
|
1140
|
-
WHERE
|
|
1140
|
+
WHERE thread_id = ${validated.threadId}
|
|
1141
1141
|
AND state <> 'settled'
|
|
1142
1142
|
ORDER BY queue_sequence ASC
|
|
1143
1143
|
LIMIT 1
|
|
1144
1144
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1145
|
-
const heads = yield* decodeSubmissionRows(operation, validated.
|
|
1145
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
1146
1146
|
if (heads.length === 0) return Option.none<Claim>();
|
|
1147
1147
|
const head = heads[0];
|
|
1148
1148
|
|
|
@@ -1171,25 +1171,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1171
1171
|
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
-
// Bump the
|
|
1175
|
-
// Attempt is fenced out of canonical appends (DUR-006). A
|
|
1174
|
+
// Bump the Thread's producer epoch atomically with the claim so every stale
|
|
1175
|
+
// Attempt is fenced out of canonical appends (DUR-006). A Thread that was
|
|
1176
1176
|
// never materialized (crash between admission and materialization) is created here
|
|
1177
1177
|
// so recovery can claim first and re-materialize idempotently at this epoch.
|
|
1178
|
-
const
|
|
1179
|
-
.
|
|
1178
|
+
const threads = yield* journal
|
|
1179
|
+
.getThread(head.thread_id)
|
|
1180
1180
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1181
1181
|
let producerEpoch: number;
|
|
1182
|
-
if (
|
|
1182
|
+
if (threads.length === 0) {
|
|
1183
1183
|
producerEpoch = 1;
|
|
1184
1184
|
yield* sql`
|
|
1185
|
-
INSERT INTO
|
|
1186
|
-
|
|
1185
|
+
INSERT INTO effect_agent_threads (
|
|
1186
|
+
thread_id,
|
|
1187
1187
|
created_at,
|
|
1188
1188
|
tail_sequence,
|
|
1189
1189
|
tail_digest,
|
|
1190
1190
|
producer_epoch
|
|
1191
1191
|
) VALUES (
|
|
1192
|
-
${head.
|
|
1192
|
+
${head.thread_id},
|
|
1193
1193
|
${now.iso},
|
|
1194
1194
|
0,
|
|
1195
1195
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -1197,11 +1197,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1197
1197
|
)
|
|
1198
1198
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1199
1199
|
} else {
|
|
1200
|
-
producerEpoch =
|
|
1200
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
1201
1201
|
yield* sql`
|
|
1202
|
-
UPDATE
|
|
1202
|
+
UPDATE effect_agent_threads
|
|
1203
1203
|
SET producer_epoch = ${producerEpoch}
|
|
1204
|
-
WHERE
|
|
1204
|
+
WHERE thread_id = ${head.thread_id}
|
|
1205
1205
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1206
1206
|
}
|
|
1207
1207
|
|
|
@@ -1234,14 +1234,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1234
1234
|
INSERT INTO effect_agent_attempts (
|
|
1235
1235
|
attempt_id,
|
|
1236
1236
|
submission_id,
|
|
1237
|
-
|
|
1237
|
+
thread_id,
|
|
1238
1238
|
owner_producer_id,
|
|
1239
1239
|
producer_epoch,
|
|
1240
1240
|
claimed_at
|
|
1241
1241
|
) VALUES (
|
|
1242
1242
|
${attemptId},
|
|
1243
1243
|
${head.submission_id},
|
|
1244
|
-
${head.
|
|
1244
|
+
${head.thread_id},
|
|
1245
1245
|
${validated.producerId},
|
|
1246
1246
|
${producerEpoch},
|
|
1247
1247
|
${now.iso}
|
|
@@ -1673,7 +1673,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1673
1673
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1674
1674
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
1675
1675
|
operation,
|
|
1676
|
-
submission.
|
|
1676
|
+
submission.thread_id,
|
|
1677
1677
|
validated.submissionId,
|
|
1678
1678
|
);
|
|
1679
1679
|
return yield* decodeAbortIntent({
|
|
@@ -1701,10 +1701,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1701
1701
|
operation,
|
|
1702
1702
|
Effect.gen(function* () {
|
|
1703
1703
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
1704
|
-
if (host.
|
|
1704
|
+
if (host.thread_id !== validated.threadId) {
|
|
1705
1705
|
return yield* LedgerError.make({
|
|
1706
1706
|
operation,
|
|
1707
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
1707
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
|
|
1708
1708
|
});
|
|
1709
1709
|
}
|
|
1710
1710
|
// The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
|
|
@@ -1712,11 +1712,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1712
1712
|
const laterRows = yield* sql<Record<string, unknown>>`
|
|
1713
1713
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1714
1714
|
FROM effect_agent_submissions
|
|
1715
|
-
WHERE
|
|
1715
|
+
WHERE thread_id = ${validated.threadId}
|
|
1716
1716
|
AND queue_sequence > ${host.queue_sequence}
|
|
1717
1717
|
ORDER BY queue_sequence ASC
|
|
1718
1718
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1719
|
-
const later = yield* decodeSubmissionRows(operation, validated.
|
|
1719
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
1720
1720
|
const claimed: Array<JoiningClaim> = [];
|
|
1721
1721
|
for (const row of later) {
|
|
1722
1722
|
if (claimed.length >= validated.maxCount) break;
|
|
@@ -2561,7 +2561,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2561
2561
|
});
|
|
2562
2562
|
|
|
2563
2563
|
interface ScanCursor {
|
|
2564
|
-
readonly
|
|
2564
|
+
readonly threadId: string;
|
|
2565
2565
|
readonly queueSequence: number;
|
|
2566
2566
|
}
|
|
2567
2567
|
|
|
@@ -2578,7 +2578,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2578
2578
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2579
2579
|
FROM effect_agent_submissions
|
|
2580
2580
|
WHERE state <> 'settled'
|
|
2581
|
-
ORDER BY
|
|
2581
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2582
2582
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2583
2583
|
`
|
|
2584
2584
|
: sql<Record<string, unknown>>`
|
|
@@ -2586,13 +2586,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2586
2586
|
FROM effect_agent_submissions
|
|
2587
2587
|
WHERE state <> 'settled'
|
|
2588
2588
|
AND (
|
|
2589
|
-
|
|
2589
|
+
thread_id > ${cursor.threadId}
|
|
2590
2590
|
OR (
|
|
2591
|
-
|
|
2591
|
+
thread_id = ${cursor.threadId}
|
|
2592
2592
|
AND queue_sequence > ${cursor.queueSequence}
|
|
2593
2593
|
)
|
|
2594
2594
|
)
|
|
2595
|
-
ORDER BY
|
|
2595
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2596
2596
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2597
2597
|
`
|
|
2598
2598
|
).pipe(Effect.mapError(sqlFailure(operation)));
|
|
@@ -2605,7 +2605,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2605
2605
|
last === undefined || decoded.length < SCAN_PAGE_SIZE
|
|
2606
2606
|
? Option.none()
|
|
2607
2607
|
: Option.some({
|
|
2608
|
-
|
|
2608
|
+
threadId: last.thread_id,
|
|
2609
2609
|
queueSequence: last.queue_sequence,
|
|
2610
2610
|
});
|
|
2611
2611
|
return [snapshots, next] as const;
|
|
@@ -2864,7 +2864,7 @@ export const submissionLedgerLayer: Layer.Layer<
|
|
|
2864
2864
|
|
|
2865
2865
|
/**
|
|
2866
2866
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
2867
|
-
* same database file as the
|
|
2867
|
+
* same database file as the ThreadStore so claims fence the same producer epochs.
|
|
2868
2868
|
*/
|
|
2869
2869
|
export const ledgerLayer = (
|
|
2870
2870
|
options: SqliteStorageOptions,
|
|
@@ -18,14 +18,14 @@ import {
|
|
|
18
18
|
ScheduleStorageError,
|
|
19
19
|
ScheduleStore,
|
|
20
20
|
scheduleDeadline,
|
|
21
|
-
} from "@effect-agent/
|
|
21
|
+
} from "@effect-agent/thread";
|
|
22
22
|
import { Effect, Layer, Result, Schema } from "effect";
|
|
23
23
|
import * as SqlClientService from "effect/unstable/sql/SqlClient";
|
|
24
24
|
|
|
25
|
-
import type { SqliteStorageInitializationError } from "./sqlite-conversation-store.ts";
|
|
26
25
|
import { initializeSqliteJournal } from "./sqlite-journal.ts";
|
|
27
26
|
import type { SqliteStorageConfig } from "./sqlite-storage-config.ts";
|
|
28
27
|
import type { SqliteStorageFailpoint } from "./sqlite-storage-failpoint.ts";
|
|
28
|
+
import type { SqliteStorageInitializationError } from "./sqlite-thread-store.ts";
|
|
29
29
|
|
|
30
30
|
// Configuration and the immutable pending envelope may each carry the canonical input. Leave
|
|
31
31
|
// room for JSON escaping and bounded status while rejecting an unreadable oversized row.
|
|
@@ -19,7 +19,7 @@ export class SqliteStorageConfigValue extends Schema.Class<SqliteStorageConfigVa
|
|
|
19
19
|
* Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
|
|
20
20
|
* that makes an abandoned claim reclaimable; correctness never depends on it because every
|
|
21
21
|
* canonical append is fenced by producer epoch. Convenience layers default this to
|
|
22
|
-
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/
|
|
22
|
+
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
|
|
23
23
|
*/
|
|
24
24
|
ownershipLeaseDuration: OwnershipLeaseMillis,
|
|
25
25
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Ref } from "effect";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SqliteStorageFailpoint,
|
|
5
|
+
type SqliteStorageFailpointHandler,
|
|
6
|
+
} from "./sqlite-storage-failpoint.ts";
|
|
7
|
+
|
|
8
|
+
const noFailpoint: SqliteStorageFailpointHandler = () => Effect.void;
|
|
9
|
+
|
|
10
|
+
/** Test-only control for replacing the active SQLite failpoint handler. */
|
|
11
|
+
export class SqliteStorageFailpointTestControl extends Context.Service<
|
|
12
|
+
SqliteStorageFailpointTestControl,
|
|
13
|
+
{
|
|
14
|
+
readonly clear: Effect.Effect<void>;
|
|
15
|
+
readonly setHandler: (handler: SqliteStorageFailpointHandler) => Effect.Effect<void>;
|
|
16
|
+
}
|
|
17
|
+
>()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {
|
|
18
|
+
/** Reusable test Layer with a control service backed by the same handler Ref. */
|
|
19
|
+
static readonly layer = Layer.effectContext(
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const handler = yield* Ref.make<SqliteStorageFailpointHandler>(noFailpoint);
|
|
22
|
+
return Context.make(
|
|
23
|
+
SqliteStorageFailpoint,
|
|
24
|
+
SqliteStorageFailpoint.of({
|
|
25
|
+
hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),
|
|
26
|
+
}),
|
|
27
|
+
).pipe(
|
|
28
|
+
Context.add(
|
|
29
|
+
SqliteStorageFailpointTestControl,
|
|
30
|
+
SqliteStorageFailpointTestControl.of({
|
|
31
|
+
clear: Ref.set(handler, noFailpoint),
|
|
32
|
+
setHandler: (next) => Ref.set(handler, next),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
|
|
3
3
|
import type { SqliteStorageFailpointError, SqliteStorageFailpointLocation } from "./errors.ts";
|
|
4
4
|
|
|
@@ -8,15 +8,6 @@ export type SqliteStorageFailpointHandler = (
|
|
|
8
8
|
|
|
9
9
|
const noFailpoint: SqliteStorageFailpointHandler = () => Effect.void;
|
|
10
10
|
|
|
11
|
-
/** Test-only control for replacing the active SQLite failpoint handler. */
|
|
12
|
-
export class SqliteStorageFailpointTestControl extends Context.Service<
|
|
13
|
-
SqliteStorageFailpointTestControl,
|
|
14
|
-
{
|
|
15
|
-
readonly clear: Effect.Effect<void>;
|
|
16
|
-
readonly setHandler: (handler: SqliteStorageFailpointHandler) => Effect.Effect<void>;
|
|
17
|
-
}
|
|
18
|
-
>()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {}
|
|
19
|
-
|
|
20
11
|
/** Explicit fault-injection authority used at SQLite operation boundaries. */
|
|
21
12
|
export class SqliteStorageFailpoint extends Context.Service<
|
|
22
13
|
SqliteStorageFailpoint,
|
|
@@ -26,25 +17,4 @@ export class SqliteStorageFailpoint extends Context.Service<
|
|
|
26
17
|
>()("@effect-agent/storage-sqlite/SqliteStorageFailpoint") {
|
|
27
18
|
/** Production default: no fault injection. */
|
|
28
19
|
static readonly layer = Layer.succeed(this)({ hit: noFailpoint });
|
|
29
|
-
|
|
30
|
-
/** Reusable test Layer with a control service backed by the same handler Ref. */
|
|
31
|
-
static readonly layerTest = Layer.effectContext(
|
|
32
|
-
Effect.gen(function* () {
|
|
33
|
-
const handler = yield* Ref.make<SqliteStorageFailpointHandler>(noFailpoint);
|
|
34
|
-
return Context.make(
|
|
35
|
-
SqliteStorageFailpoint,
|
|
36
|
-
SqliteStorageFailpoint.of({
|
|
37
|
-
hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))),
|
|
38
|
-
}),
|
|
39
|
-
).pipe(
|
|
40
|
-
Context.add(
|
|
41
|
-
SqliteStorageFailpointTestControl,
|
|
42
|
-
SqliteStorageFailpointTestControl.of({
|
|
43
|
-
clear: Ref.set(handler, noFailpoint),
|
|
44
|
-
setHandler: (next) => Ref.set(handler, next),
|
|
45
|
-
}),
|
|
46
|
-
),
|
|
47
|
-
);
|
|
48
|
-
}),
|
|
49
|
-
);
|
|
50
20
|
}
|