@yielded/agent-storage-cloudflare 0.1.0-beta.167 → 0.1.0-beta.169
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/DoMemoryStore.mjs +1 -1
- package/dist/DoMessageDeliveryStore.d.mts +1 -1
- package/dist/DoMessageDeliveryStore.mjs +3 -2
- package/dist/DoMessageDeliveryStore.mjs.map +1 -1
- package/dist/DoScheduleStore.mjs +16 -37
- package/dist/DoScheduleStore.mjs.map +1 -1
- package/dist/DoStorageConfig.d.mts +2 -10
- package/dist/DoStorageConfig.mjs +2 -11
- package/dist/DoStorageConfig.mjs.map +1 -1
- package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
- package/dist/DoStorageError.d.mts +2 -2
- package/dist/DoStorageError.mjs +14 -30
- package/dist/DoStorageError.mjs.map +1 -1
- package/dist/DoStorageFailpoint.d.mts +1 -1
- package/dist/DoStorageFailpointTesting.d.mts +1 -1
- package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoStorageVersion.mjs.map +1 -1
- package/dist/DoSubmissionLedger.d.mts +7 -5
- package/dist/DoSubmissionLedger.mjs +378 -528
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/DoSubscriptionStore.mjs +5 -24
- package/dist/DoSubscriptionStore.mjs.map +1 -1
- package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
- package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
- package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
- package/dist/DoThreadStore.d.mts +2 -2
- package/dist/DoThreadStore.mjs +4 -529
- package/dist/MemoryProtocol.mjs +3 -3
- package/dist/MemoryProtocol.mjs.map +1 -1
- package/dist/PortProtocol.d.mts +11727 -3410
- package/dist/PortProtocol.mjs +103 -15
- package/dist/PortProtocol.mjs.map +1 -1
- package/dist/PortRouting.d.mts +14 -9
- package/dist/PortRouting.mjs +155 -29
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/do-journal-Cawnsetj.mjs +761 -0
- package/dist/do-journal-Cawnsetj.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/migrations-D_EP3KYf.mjs +148 -0
- package/dist/migrations-D_EP3KYf.mjs.map +1 -0
- package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
- package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoMessageDeliveryStore.ts +2 -1
- package/src/DoScheduleStore.ts +19 -59
- package/src/DoStorageConfig.ts +2 -11
- package/src/DoStorageError.ts +3 -26
- package/src/DoStorageVersion.ts +1 -1
- package/src/DoSubmissionLedger.ts +1806 -2030
- package/src/DoSubscriptionStore.ts +5 -47
- package/src/DoThreadStore.ts +276 -622
- package/src/MemoryProtocol.ts +3 -3
- package/src/PortProtocol.ts +198 -12
- package/src/PortRouting.ts +527 -108
- package/src/internal/canonical-append.ts +15 -0
- package/src/internal/do-journal.ts +548 -1440
- package/src/internal/migrations.ts +184 -287
- package/src/internal/owned-state.ts +81 -30
- package/src/internal/prompt-checkpoint.ts +158 -0
- package/src/internal/storage-span.ts +2 -5
- package/dist/DoThreadStore.mjs.map +0 -1
- package/dist/do-journal-DUhjZkOf.mjs +0 -1352
- package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
- package/dist/migrations-DtKz_Gxr.mjs +0 -306
- package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
- package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
- package/src/internal/message-delivery-schema.ts +0 -24
- package/src/internal/recovery-checkpoint-schema.ts +0 -17
|
@@ -1,34 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
makeCanonicalStorage,
|
|
3
|
+
recordBatchId,
|
|
4
|
+
} from "@yielded/agent-storage-sql/sql-canonical-storage";
|
|
5
|
+
import type { RawAppendRequest } from "@yielded/agent-storage-sql/sql-journal";
|
|
6
|
+
import { makeSqlExecutions } from "@yielded/agent-storage-sql/sql-journal";
|
|
2
7
|
import {
|
|
3
8
|
makeSqlLifecyclePublication,
|
|
4
9
|
type SqlLifecycleRetainMany,
|
|
5
10
|
SqlLifecycleSource,
|
|
6
11
|
SqlLifecycleRetainer,
|
|
7
12
|
} from "@yielded/agent-storage-sql/sql-lifecycle-publication";
|
|
8
|
-
import { createMessageDeliveryPendingIndex } from "@yielded/agent-storage-sql/sql-message-delivery-store";
|
|
9
13
|
import { SqlStorageProgress } from "@yielded/agent-storage-sql/sql-storage-progress";
|
|
10
|
-
import { checkV2ThreadLayout } from "@yielded/agent-storage-sql/sql-storage-v2-upgrade";
|
|
11
14
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "@yielded/agent-storage-sql/sql-thread-
|
|
17
|
-
import { EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
|
|
15
|
+
canonicalRecordJson,
|
|
16
|
+
makeSqlThreadArchiveRange,
|
|
17
|
+
} from "@yielded/agent-storage-sql/sql-thread-archive-range";
|
|
18
|
+
import { makeProgressAppendValidation } from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
19
|
+
import { makeSqlThreadWork } from "@yielded/agent-storage-sql/sql-thread-work";
|
|
18
20
|
import { ThreadId } from "@yielded/agent/identifiers";
|
|
19
21
|
import {
|
|
20
22
|
LifecyclePublicationFact,
|
|
21
23
|
LifecyclePublicationError,
|
|
22
24
|
} from "@yielded/agent/lifecycle-publication";
|
|
23
|
-
import { CanonicalRecord, CanonicalSequence
|
|
25
|
+
import { CanonicalRecord, CanonicalSequence } from "@yielded/agent/records";
|
|
24
26
|
import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
|
|
25
27
|
import {
|
|
26
|
-
|
|
27
|
-
CheckpointRejected,
|
|
28
|
-
FenceRejected,
|
|
29
|
-
ThreadNotMaterialized,
|
|
28
|
+
type AppendConflict,
|
|
30
29
|
ThreadStoreDiagnostic,
|
|
31
|
-
|
|
30
|
+
ThreadStoreError,
|
|
32
31
|
} from "@yielded/agent/thread-store";
|
|
33
32
|
import { Cause, Clock, Effect, Option, Schema, Stream } from "effect";
|
|
34
33
|
import * as SqlClient from "effect/sql/SqlClient";
|
|
@@ -38,22 +37,13 @@ import {
|
|
|
38
37
|
type DoStorageFailpointError,
|
|
39
38
|
DoAppendConflict,
|
|
40
39
|
DoCheckpointConflict,
|
|
41
|
-
DoFenceRejected,
|
|
42
|
-
DoStorageCompatibilityError,
|
|
43
40
|
DoStorageCorruptionError,
|
|
44
41
|
DoStorageError,
|
|
45
42
|
DoValueBoundExceeded,
|
|
46
43
|
type DoStorageFailpointLocation,
|
|
47
44
|
} from "../DoStorageError.ts";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
CurrentDoStorageVersion,
|
|
51
|
-
createNonterminalIndex,
|
|
52
|
-
createWorkerStops,
|
|
53
|
-
doMigrations,
|
|
54
|
-
} from "./migrations.ts";
|
|
45
|
+
import { ensureDoStorageLayout } from "./migrations.ts";
|
|
55
46
|
import { ownedState, ownedRows, type OwnedState } from "./owned-state.ts";
|
|
56
|
-
import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
|
|
57
47
|
import {
|
|
58
48
|
isAppendContention,
|
|
59
49
|
annotateStorageError,
|
|
@@ -69,8 +59,6 @@ import {
|
|
|
69
59
|
*/
|
|
70
60
|
const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2_000_000));
|
|
71
61
|
const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
72
|
-
const MAX_RECORDS_PER_THREAD = MAX_THREAD_EXPORT_RECORDS;
|
|
73
|
-
const ZERO_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
74
62
|
const MAX_IDENTIFIER_LENGTH = 1_024;
|
|
75
63
|
const MAX_READ_PAGE_JSON_BYTES = 4 * 1024 * 1024;
|
|
76
64
|
// A shared isolate budget permits multi-page hydration without multiplying its
|
|
@@ -93,19 +81,9 @@ const chunked = <A>(values: ReadonlyArray<A>, size: number): Array<ReadonlyArray
|
|
|
93
81
|
return chunks;
|
|
94
82
|
};
|
|
95
83
|
|
|
96
|
-
class DoMetaRow extends Schema.Class<DoMetaRow>("DoMetaRow")({
|
|
97
|
-
value: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
98
|
-
}) {}
|
|
99
|
-
|
|
100
|
-
class DoNameRow extends Schema.Class<DoNameRow>("DoNameRow")({
|
|
101
|
-
name: BoundedIdentifier,
|
|
102
|
-
}) {}
|
|
103
|
-
|
|
104
84
|
class ThreadRow extends Schema.Class<ThreadRow>("ThreadRow")({
|
|
105
85
|
thread_id: BoundedIdentifier,
|
|
106
|
-
created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
107
|
-
producer_epoch: ProducerEpoch,
|
|
108
|
-
tail_digest: BoundedStoredText,
|
|
86
|
+
created_at: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(128))),
|
|
109
87
|
tail_sequence: CanonicalSequence,
|
|
110
88
|
}) {}
|
|
111
89
|
|
|
@@ -116,20 +94,10 @@ const threadRows = ownedRows(
|
|
|
116
94
|
"thread_id",
|
|
117
95
|
);
|
|
118
96
|
|
|
119
|
-
class BatchRow extends Schema.Class<BatchRow>("BatchRow")({
|
|
120
|
-
batch_digest: BoundedStoredText,
|
|
121
|
-
batch_id: BoundedIdentifier,
|
|
122
|
-
batch_json: BoundedStoredText,
|
|
123
|
-
thread_id: BoundedIdentifier,
|
|
124
|
-
first_sequence: CanonicalSequence,
|
|
125
|
-
last_sequence: CanonicalSequence,
|
|
126
|
-
tail_digest: BoundedStoredText,
|
|
127
|
-
}) {}
|
|
128
|
-
|
|
129
97
|
class RecordRow extends Schema.Class<RecordRow>("RecordRow")({
|
|
130
|
-
batch_id: BoundedIdentifier,
|
|
98
|
+
batch_id: Schema.fromJsonString(BoundedIdentifier),
|
|
131
99
|
thread_id: BoundedIdentifier,
|
|
132
|
-
record_id: BoundedIdentifier,
|
|
100
|
+
record_id: Schema.fromJsonString(BoundedIdentifier),
|
|
133
101
|
record_json: BoundedStoredText,
|
|
134
102
|
sequence: CanonicalSequence,
|
|
135
103
|
}) {}
|
|
@@ -173,12 +141,12 @@ const makeRecordCache = () => {
|
|
|
173
141
|
|
|
174
142
|
return prefix;
|
|
175
143
|
},
|
|
176
|
-
put: (row: RecordRow) => {
|
|
144
|
+
put: (row: RecordRow, capturedBytes?: number) => {
|
|
177
145
|
const id = key(row.thread_id, row.sequence);
|
|
178
146
|
const prior = records.get(id);
|
|
179
147
|
|
|
180
148
|
if (prior?.row === row) return;
|
|
181
|
-
const size = storedTextBytes(row.record_json);
|
|
149
|
+
const size = capturedBytes ?? storedTextBytes(row.record_json);
|
|
182
150
|
|
|
183
151
|
if (prior !== undefined) releaseRecord(prior);
|
|
184
152
|
|
|
@@ -214,17 +182,9 @@ type ReadPage = [typeof ReadPlanRow.Type, ...Array<typeof ReadPlanRow.Type>];
|
|
|
214
182
|
class CheckpointRow extends Schema.Class<CheckpointRow>("CheckpointRow")({
|
|
215
183
|
checkpoint_json: BoundedStoredText,
|
|
216
184
|
thread_id: BoundedIdentifier,
|
|
217
|
-
tail_digest: BoundedStoredText,
|
|
218
185
|
through_sequence: CanonicalSequence,
|
|
219
186
|
}) {}
|
|
220
187
|
|
|
221
|
-
const recoveryRows = ownedRows(
|
|
222
|
-
CheckpointRow,
|
|
223
|
-
"effect_agent_recovery_checkpoints",
|
|
224
|
-
(row) => row.thread_id,
|
|
225
|
-
"thread_id",
|
|
226
|
-
);
|
|
227
|
-
|
|
228
188
|
const LifecycleCursorRow = Schema.Struct({
|
|
229
189
|
thread_id: BoundedIdentifier,
|
|
230
190
|
through_sequence: CanonicalSequence,
|
|
@@ -239,26 +199,7 @@ const lifecycleCursorRows = ownedRows(
|
|
|
239
199
|
|
|
240
200
|
const initializedLifecycleSources = new WeakSet<OwnedState>();
|
|
241
201
|
|
|
242
|
-
export
|
|
243
|
-
"@effect-agent/storage-cloudflare/RawRecord",
|
|
244
|
-
)({
|
|
245
|
-
recordId: BoundedIdentifier,
|
|
246
|
-
recordJson: BoundedStoredText,
|
|
247
|
-
}) {}
|
|
248
|
-
|
|
249
|
-
export class RawAppendRequest extends Schema.Class<RawAppendRequest>(
|
|
250
|
-
"@effect-agent/storage-cloudflare/RawAppendRequest",
|
|
251
|
-
)({
|
|
252
|
-
batchDigest: BoundedStoredText,
|
|
253
|
-
batchId: BoundedIdentifier,
|
|
254
|
-
batchJson: BoundedStoredText,
|
|
255
|
-
threadId: BoundedIdentifier,
|
|
256
|
-
expectedTailDigest: BoundedStoredText,
|
|
257
|
-
expectedTailSequence: CanonicalSequence,
|
|
258
|
-
producerEpoch: ProducerEpoch,
|
|
259
|
-
records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
|
|
260
|
-
tailDigest: BoundedStoredText,
|
|
261
|
-
}) {}
|
|
202
|
+
export type { RawAppendRequest } from "@yielded/agent-storage-sql/sql-journal";
|
|
262
203
|
|
|
263
204
|
export class RawAppendResult extends Schema.Class<RawAppendResult>(
|
|
264
205
|
"@effect-agent/storage-cloudflare/RawAppendResult",
|
|
@@ -266,7 +207,6 @@ export class RawAppendResult extends Schema.Class<RawAppendResult>(
|
|
|
266
207
|
firstSequence: CanonicalSequence,
|
|
267
208
|
lastSequence: CanonicalSequence,
|
|
268
209
|
replayed: Schema.Boolean,
|
|
269
|
-
tailDigest: BoundedStoredText,
|
|
270
210
|
}) {}
|
|
271
211
|
|
|
272
212
|
export class RawReadRequest extends Schema.Class<RawReadRequest>(
|
|
@@ -282,20 +222,12 @@ export class RawCheckpoint extends Schema.Class<RawCheckpoint>(
|
|
|
282
222
|
)({
|
|
283
223
|
checkpointJson: BoundedStoredText,
|
|
284
224
|
threadId: BoundedIdentifier,
|
|
285
|
-
tailDigest: BoundedStoredText,
|
|
286
225
|
throughSequence: CanonicalSequence,
|
|
287
226
|
}) {}
|
|
288
227
|
|
|
289
|
-
export class RawThreadExport extends Schema.Class<RawThreadExport>(
|
|
290
|
-
"@effect-agent/storage-cloudflare/RawThreadExport",
|
|
291
|
-
)({
|
|
292
|
-
thread: ThreadRow,
|
|
293
|
-
records: Schema.Array(RecordRow),
|
|
294
|
-
}) {}
|
|
295
|
-
|
|
296
228
|
type AppendError =
|
|
229
|
+
| ThreadStoreError
|
|
297
230
|
| DoAppendConflict
|
|
298
|
-
| DoFenceRejected
|
|
299
231
|
| DoStorageCorruptionError
|
|
300
232
|
| DoStorageError
|
|
301
233
|
| DoStorageFailpointError
|
|
@@ -328,673 +260,65 @@ const storageError =
|
|
|
328
260
|
});
|
|
329
261
|
|
|
330
262
|
/** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
331
|
-
export const decodeRows =
|
|
332
|
-
<A
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
issueTag: error.issue._tag,
|
|
350
|
-
}),
|
|
263
|
+
export const decodeRows = <A, I>(
|
|
264
|
+
schema: Schema.Codec<ReadonlyArray<A>, ReadonlyArray<I>>,
|
|
265
|
+
table: string,
|
|
266
|
+
rowKey: string,
|
|
267
|
+
rows: unknown,
|
|
268
|
+
decoder: string = table,
|
|
269
|
+
): Effect.Effect<ReadonlyArray<A>, DoStorageCorruptionError> =>
|
|
270
|
+
Schema.decodeUnknownEffect(schema)(rows).pipe(
|
|
271
|
+
Effect.mapError((error) =>
|
|
272
|
+
DoStorageCorruptionError.make({
|
|
273
|
+
table,
|
|
274
|
+
rowKey,
|
|
275
|
+
message: "Stored rows do not satisfy the storage schema",
|
|
276
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
277
|
+
causeTag: error._tag,
|
|
278
|
+
operation: "decode storage rows",
|
|
279
|
+
decoder,
|
|
280
|
+
issueTag: error.issue._tag,
|
|
351
281
|
}),
|
|
352
|
-
),
|
|
353
|
-
Effect.tapError((error) =>
|
|
354
|
-
Effect.annotateCurrentSpan({
|
|
355
|
-
"storage.failure.operation": error.diagnostic?.operation,
|
|
356
|
-
"storage.failure.cause": error.diagnostic?.causeTag,
|
|
357
|
-
"storage.failure.decoder": decoder,
|
|
358
|
-
"storage.failure.issue": error.diagnostic?.issueTag,
|
|
359
|
-
}),
|
|
360
|
-
),
|
|
282
|
+
}),
|
|
361
283
|
),
|
|
362
|
-
)
|
|
284
|
+
Effect.tapError((error) =>
|
|
285
|
+
Effect.annotateCurrentSpan({
|
|
286
|
+
"storage.failure.operation": error.diagnostic?.operation,
|
|
287
|
+
"storage.failure.cause": error.diagnostic?.causeTag,
|
|
288
|
+
"storage.failure.decoder": decoder,
|
|
289
|
+
"storage.failure.issue": error.diagnostic?.issueTag,
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
363
293
|
|
|
364
294
|
/** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
|
|
365
|
-
export const decodeSingleRow =
|
|
366
|
-
<A
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
decoded
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
),
|
|
383
|
-
),
|
|
295
|
+
export const decodeSingleRow = <A, I>(
|
|
296
|
+
schema: Schema.Codec<ReadonlyArray<A>, ReadonlyArray<I>>,
|
|
297
|
+
table: string,
|
|
298
|
+
rowKey: string,
|
|
299
|
+
rows: unknown,
|
|
300
|
+
): Effect.Effect<A, DoStorageCorruptionError> =>
|
|
301
|
+
decodeRows(schema, table, rowKey, rows).pipe(
|
|
302
|
+
Effect.flatMap((decoded) =>
|
|
303
|
+
decoded.length === 1
|
|
304
|
+
? Effect.succeed(decoded[0])
|
|
305
|
+
: Effect.fail(
|
|
306
|
+
DoStorageCorruptionError.make({
|
|
307
|
+
table,
|
|
308
|
+
rowKey,
|
|
309
|
+
message: `Expected exactly one row but found ${decoded.length}.`,
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
384
312
|
),
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
/** Column inventory of the supported v3 predecessor, independent of physical column order. */
|
|
388
|
-
const predecessorColumns = {
|
|
389
|
-
effect_agent_threads: [
|
|
390
|
-
"thread_id",
|
|
391
|
-
"created_at",
|
|
392
|
-
"tail_sequence",
|
|
393
|
-
"tail_digest",
|
|
394
|
-
"producer_epoch",
|
|
395
|
-
],
|
|
396
|
-
effect_agent_canonical_batches: [
|
|
397
|
-
"thread_id",
|
|
398
|
-
"batch_id",
|
|
399
|
-
"first_sequence",
|
|
400
|
-
"last_sequence",
|
|
401
|
-
"batch_digest",
|
|
402
|
-
"tail_digest",
|
|
403
|
-
"batch_json",
|
|
404
|
-
],
|
|
405
|
-
effect_agent_canonical_records: ["thread_id", "sequence", "record_id", "batch_id", "record_json"],
|
|
406
|
-
effect_agent_checkpoints: ["thread_id", "through_sequence", "tail_digest", "checkpoint_json"],
|
|
407
|
-
effect_agent_submissions: [
|
|
408
|
-
"submission_id",
|
|
409
|
-
"thread_id",
|
|
410
|
-
"queue_sequence",
|
|
411
|
-
"principal",
|
|
412
|
-
"idempotency_key",
|
|
413
|
-
"agent_id",
|
|
414
|
-
"agent_digests_json",
|
|
415
|
-
"deployment_id",
|
|
416
|
-
"input_json",
|
|
417
|
-
"input_digest",
|
|
418
|
-
"receipt_id",
|
|
419
|
-
"state",
|
|
420
|
-
"settled_outcome",
|
|
421
|
-
"created_at",
|
|
422
|
-
"ready_at",
|
|
423
|
-
"input_applied_record_id",
|
|
424
|
-
"input_applied_sequence",
|
|
425
|
-
"joined_host_submission_id",
|
|
426
|
-
"suspended_reason_json",
|
|
427
|
-
"suspended_at",
|
|
428
|
-
"unknown_reason",
|
|
429
|
-
"unknown_tool_call_ids_json",
|
|
430
|
-
"parent_submission_id",
|
|
431
|
-
"parent_tool_call_id",
|
|
432
|
-
"admission_group",
|
|
433
|
-
"admission_fence_json",
|
|
434
|
-
],
|
|
435
|
-
effect_agent_submission_ownership: [
|
|
436
|
-
"submission_id",
|
|
437
|
-
"attempt_id",
|
|
438
|
-
"ownership_token",
|
|
439
|
-
"producer_epoch",
|
|
440
|
-
"owner_producer_id",
|
|
441
|
-
"lease_expires_at",
|
|
442
|
-
],
|
|
443
|
-
effect_agent_attempts: [
|
|
444
|
-
"attempt_id",
|
|
445
|
-
"submission_id",
|
|
446
|
-
"thread_id",
|
|
447
|
-
"owner_producer_id",
|
|
448
|
-
"producer_epoch",
|
|
449
|
-
"claimed_at",
|
|
450
|
-
],
|
|
451
|
-
effect_agent_settlement_reservations: [
|
|
452
|
-
"submission_id",
|
|
453
|
-
"settlement_id",
|
|
454
|
-
"outcome",
|
|
455
|
-
"record_id",
|
|
456
|
-
"record_json",
|
|
457
|
-
"record_digest",
|
|
458
|
-
"reserved_at",
|
|
459
|
-
"finalized_at",
|
|
460
|
-
],
|
|
461
|
-
effect_agent_abort_intents: [
|
|
462
|
-
"submission_id",
|
|
463
|
-
"author",
|
|
464
|
-
"reason",
|
|
465
|
-
"requested_at",
|
|
466
|
-
"canonical_record_id",
|
|
467
|
-
],
|
|
468
|
-
effect_agent_approval_decisions: [
|
|
469
|
-
"submission_id",
|
|
470
|
-
"tool_call_id",
|
|
471
|
-
"decision",
|
|
472
|
-
"resolver",
|
|
473
|
-
"reason",
|
|
474
|
-
"decided_at",
|
|
475
|
-
],
|
|
476
|
-
effect_agent_unknown_resolutions: [
|
|
477
|
-
"submission_id",
|
|
478
|
-
"tool_call_id",
|
|
479
|
-
"author",
|
|
480
|
-
"reason",
|
|
481
|
-
"resolution_json",
|
|
482
|
-
"resolved_at",
|
|
483
|
-
],
|
|
484
|
-
effect_agent_child_reservations: [
|
|
485
|
-
"reservation_id",
|
|
486
|
-
"parent_submission_id",
|
|
487
|
-
"parent_tool_call_id",
|
|
488
|
-
"child_submission_id",
|
|
489
|
-
"status",
|
|
490
|
-
"allocation_json",
|
|
491
|
-
"allocation_digest",
|
|
492
|
-
"accounting_json",
|
|
493
|
-
"reserved_at",
|
|
494
|
-
"release_began_at",
|
|
495
|
-
"released_at",
|
|
496
|
-
],
|
|
497
|
-
effect_agent_meta: ["key", "value"],
|
|
498
|
-
effect_agent_child_settlements: [
|
|
499
|
-
"parent_submission_id",
|
|
500
|
-
"child_submission_id",
|
|
501
|
-
"child_outcome",
|
|
502
|
-
"recorded_at",
|
|
503
|
-
],
|
|
504
|
-
} as const;
|
|
505
|
-
|
|
506
|
-
const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* (
|
|
507
|
-
version: 3 | 4 | 5 | 7,
|
|
508
|
-
) {
|
|
509
|
-
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
|
|
510
|
-
|
|
511
|
-
const messageColumns =
|
|
512
|
-
version === 3
|
|
513
|
-
? predecessorColumns
|
|
514
|
-
: {
|
|
515
|
-
...predecessorColumns,
|
|
516
|
-
effect_agent_submissions: [
|
|
517
|
-
...predecessorColumns.effect_agent_submissions,
|
|
518
|
-
"worker_admission_json",
|
|
519
|
-
"message_admission_json",
|
|
520
|
-
],
|
|
521
|
-
effect_agent_message_deliveries: [
|
|
522
|
-
"owner_thread_id",
|
|
523
|
-
"message_id",
|
|
524
|
-
"version",
|
|
525
|
-
"state",
|
|
526
|
-
"deadline_at_millis",
|
|
527
|
-
"record_json",
|
|
528
|
-
],
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const expectedColumns = {
|
|
532
|
-
...messageColumns,
|
|
533
|
-
...(version === 7
|
|
534
|
-
? {
|
|
535
|
-
effect_agent_canonical_records: [
|
|
536
|
-
...predecessorColumns.effect_agent_canonical_records,
|
|
537
|
-
"outstanding",
|
|
538
|
-
],
|
|
539
|
-
}
|
|
540
|
-
: {}),
|
|
541
|
-
...(version >= 5
|
|
542
|
-
? {
|
|
543
|
-
effect_agent_recovery_checkpoints: [
|
|
544
|
-
"thread_id",
|
|
545
|
-
"through_sequence",
|
|
546
|
-
"tail_digest",
|
|
547
|
-
"checkpoint_json",
|
|
548
|
-
],
|
|
549
|
-
}
|
|
550
|
-
: {}),
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
for (const [table, expected] of Object.entries(expectedColumns)) {
|
|
554
|
-
const columns = yield* decodeRows(
|
|
555
|
-
Schema.Array(Schema.Struct({ name: BoundedIdentifier })),
|
|
556
|
-
table,
|
|
557
|
-
"schema",
|
|
558
|
-
yield* sql.unsafe(`PRAGMA table_info(${table})`),
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
const names = new Set<string>(expected);
|
|
562
|
-
|
|
563
|
-
if (columns.length !== names.size || columns.some((column) => !names.has(column.name)))
|
|
564
|
-
return yield* DoStorageCompatibilityError.make({
|
|
565
|
-
actualVersion: version,
|
|
566
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
567
|
-
message: `The v${version} ${table} columns do not match the supported predecessor; no upgrade was committed.`,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const REQUIRED_TABLES = [
|
|
573
|
-
"effect_agent_abort_intents",
|
|
574
|
-
"effect_agent_approval_decisions",
|
|
575
|
-
"effect_agent_attempts",
|
|
576
|
-
"effect_agent_canonical_batches",
|
|
577
|
-
"effect_agent_canonical_records",
|
|
578
|
-
"effect_agent_checkpoints",
|
|
579
|
-
"effect_agent_child_reservations",
|
|
580
|
-
"effect_agent_child_settlements",
|
|
581
|
-
"effect_agent_threads",
|
|
582
|
-
"effect_agent_meta",
|
|
583
|
-
"effect_agent_settlement_reservations",
|
|
584
|
-
"effect_agent_submission_ownership",
|
|
585
|
-
"effect_agent_submissions",
|
|
586
|
-
"effect_agent_unknown_resolutions",
|
|
587
|
-
] as const;
|
|
313
|
+
);
|
|
588
314
|
|
|
589
|
-
/**
|
|
590
|
-
|
|
591
|
-
* `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
|
|
592
|
-
* regardless). No WAL check (Durable Object storage owns durability and confirms writes
|
|
593
|
-
* through output gates) and no busy timeout (a Durable Object has exactly one writer): the
|
|
594
|
-
* Node machinery those served has no DC analogue and is deliberately absent.
|
|
595
|
-
*/
|
|
596
|
-
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (
|
|
315
|
+
/** Open a validated layout and bind this Object's storage owner. */
|
|
316
|
+
const ensureCurrentStorage = Effect.fnUntraced(function* (
|
|
597
317
|
sql: SqlClient.SqlClient,
|
|
598
318
|
failpoint: DoJournalFailpoint = noFailpoint,
|
|
599
319
|
maxStoredValueBytes: number,
|
|
600
320
|
) {
|
|
601
|
-
|
|
602
|
-
const requiredRows = yield* sql<Record<string, unknown>>`
|
|
603
|
-
SELECT name
|
|
604
|
-
FROM sqlite_master
|
|
605
|
-
WHERE (type = 'table'
|
|
606
|
-
AND name IN ${sql.in([...REQUIRED_TABLES, "effect_agent_message_deliveries", "effect_agent_recovery_checkpoints"])}
|
|
607
|
-
) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
|
|
608
|
-
OR (${workerContract ? 1 : 0} = 1 AND name IN ('effect_agent_worker_stops', 'effect_agent_worker_starts', 'effect_agent_worker_pending', 'effect_agent_worker_execution'))
|
|
609
|
-
ORDER BY name
|
|
610
|
-
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
611
|
-
|
|
612
|
-
const required = yield* decodeRows(
|
|
613
|
-
Schema.Array(DoNameRow),
|
|
614
|
-
"sqlite_master",
|
|
615
|
-
"required_tables",
|
|
616
|
-
requiredRows,
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
if (required.length !== REQUIRED_TABLES.length + 9 + (workerContract ? 4 : 0)) {
|
|
620
|
-
return yield* DoStorageCompatibilityError.make({
|
|
621
|
-
actualVersion: CurrentDoStorageVersion,
|
|
622
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
623
|
-
message:
|
|
624
|
-
"The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
const metaTableRows = yield* sql<Record<string, unknown>>`
|
|
630
|
-
SELECT name
|
|
631
|
-
FROM sqlite_master
|
|
632
|
-
WHERE type = 'table'
|
|
633
|
-
AND name = 'effect_agent_meta'
|
|
634
|
-
`.pipe(Effect.mapError(storageError("read storage version table")));
|
|
635
|
-
|
|
636
|
-
const metaTables = yield* decodeRows(
|
|
637
|
-
Schema.Array(DoNameRow),
|
|
638
|
-
"sqlite_master",
|
|
639
|
-
"effect_agent_meta",
|
|
640
|
-
metaTableRows,
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
if (metaTables.length === 0) {
|
|
644
|
-
const existingRows = yield* sql<Record<string, unknown>>`
|
|
645
|
-
SELECT name
|
|
646
|
-
FROM sqlite_master
|
|
647
|
-
WHERE type = 'table'
|
|
648
|
-
AND name LIKE 'effect_agent_%'
|
|
649
|
-
ORDER BY name
|
|
650
|
-
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
651
|
-
|
|
652
|
-
const existing = yield* decodeRows(
|
|
653
|
-
Schema.Array(DoNameRow),
|
|
654
|
-
"sqlite_master",
|
|
655
|
-
"effect_agent_%",
|
|
656
|
-
existingRows,
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
if (existing.length > 0) {
|
|
660
|
-
return yield* DoStorageCompatibilityError.make({
|
|
661
|
-
actualVersion: 0,
|
|
662
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
663
|
-
message:
|
|
664
|
-
"The Durable Object contains unversioned Effect Agent tables. Refusing to mutate ambiguous stored data; retain it for inspection with its original writer.",
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
yield* SqliteMigrator.run({
|
|
669
|
-
loader: doMigrations,
|
|
670
|
-
// An application can share this SQL client and own its own migration history.
|
|
671
|
-
// Keep bookkeeping outside effect_agent_% so interrupted unversioned schemas
|
|
672
|
-
// still fail the ambiguity check above.
|
|
673
|
-
table: "effect_sql_migrations_agent_threads",
|
|
674
|
-
}).pipe(
|
|
675
|
-
// SqliteMigrator depends on the generic client supplied by this adapter. The concrete
|
|
676
|
-
// Durable Object client is kept at the outer Layer boundary.
|
|
677
|
-
Effect.provideService(SqlClient.SqlClient, sql),
|
|
678
|
-
Effect.mapError((error) =>
|
|
679
|
-
DoStorageError.make({
|
|
680
|
-
cause: error,
|
|
681
|
-
operation: "initialize current storage",
|
|
682
|
-
message: error.message,
|
|
683
|
-
}),
|
|
684
|
-
),
|
|
685
|
-
);
|
|
686
|
-
} else {
|
|
687
|
-
const versionRows = yield* sql<Record<string, unknown>>`
|
|
688
|
-
SELECT value
|
|
689
|
-
FROM effect_agent_meta
|
|
690
|
-
WHERE key = 'storage_version'
|
|
691
|
-
`.pipe(Effect.mapError(storageError("read storage version")));
|
|
692
|
-
|
|
693
|
-
const version = yield* decodeSingleRow(
|
|
694
|
-
Schema.Array(DoMetaRow),
|
|
695
|
-
"effect_agent_meta",
|
|
696
|
-
"storage_version",
|
|
697
|
-
versionRows,
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
if (
|
|
701
|
-
version.value === "2" ||
|
|
702
|
-
version.value === "3" ||
|
|
703
|
-
version.value === "4" ||
|
|
704
|
-
version.value === "5"
|
|
705
|
-
) {
|
|
706
|
-
yield* sql
|
|
707
|
-
.withTransaction(
|
|
708
|
-
Effect.gen(function* () {
|
|
709
|
-
const current = yield* sql<{
|
|
710
|
-
value: string;
|
|
711
|
-
}>`SELECT value FROM effect_agent_meta WHERE key='storage_version'`;
|
|
712
|
-
|
|
713
|
-
if (current.length === 1 && current[0].value === String(CurrentDoStorageVersion))
|
|
714
|
-
return;
|
|
715
|
-
if (
|
|
716
|
-
current.length !== 1 ||
|
|
717
|
-
(current[0].value !== "2" &&
|
|
718
|
-
current[0].value !== "3" &&
|
|
719
|
-
current[0].value !== "4" &&
|
|
720
|
-
current[0].value !== "5")
|
|
721
|
-
)
|
|
722
|
-
return yield* DoStorageCompatibilityError.make({
|
|
723
|
-
actualVersion: -1,
|
|
724
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
725
|
-
message: "Storage version changed while acquiring the upgrade transaction.",
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
const required = yield* decodeRows(
|
|
729
|
-
Schema.Array(DoNameRow),
|
|
730
|
-
"sqlite_master",
|
|
731
|
-
"required_tables",
|
|
732
|
-
yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name IN ${sql.in([...REQUIRED_TABLES])}`,
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
if (required.length !== REQUIRED_TABLES.length)
|
|
736
|
-
return yield* DoStorageCompatibilityError.make({
|
|
737
|
-
actualVersion: Number(current[0].value),
|
|
738
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
739
|
-
message:
|
|
740
|
-
"The predecessor store is missing required tables. Retain the original store for inspection; no upgrade was committed.",
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
const recoveryTables =
|
|
744
|
-
yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name='effect_agent_recovery_checkpoints'`;
|
|
745
|
-
|
|
746
|
-
if (recoveryTables.length !== (current[0].value === "5" ? 1 : 0))
|
|
747
|
-
return yield* DoStorageCompatibilityError.make({
|
|
748
|
-
actualVersion: Number(current[0].value),
|
|
749
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
750
|
-
message:
|
|
751
|
-
"The predecessor recovery checkpoint storage does not match its version; refusing ambiguous data without mutation.",
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
const indexes =
|
|
755
|
-
yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_submissions_nonterminal'`;
|
|
756
|
-
|
|
757
|
-
if (indexes.length !== 0)
|
|
758
|
-
return yield* DoStorageCompatibilityError.make({
|
|
759
|
-
actualVersion: Number(current[0].value),
|
|
760
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
761
|
-
message:
|
|
762
|
-
"The predecessor already contains the nonterminal index; refusing ambiguous storage without mutation.",
|
|
763
|
-
});
|
|
764
|
-
if (current[0].value === "2") {
|
|
765
|
-
yield* checkV2ThreadLayout();
|
|
766
|
-
for (const statement of [
|
|
767
|
-
sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_group TEXT`,
|
|
768
|
-
sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_fence_json TEXT`,
|
|
769
|
-
sql`CREATE INDEX effect_agent_submissions_group ON effect_agent_submissions (thread_id, admission_group, state)`,
|
|
770
|
-
]) {
|
|
771
|
-
yield* failpoint("upgrade:before-mutation");
|
|
772
|
-
yield* statement;
|
|
773
|
-
yield* failpoint("upgrade:after-mutation");
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
if (current[0].value === "3") yield* checkPredecessorLayout(3);
|
|
777
|
-
if (current[0].value === "4") yield* checkPredecessorLayout(4);
|
|
778
|
-
if (current[0].value === "5") yield* checkPredecessorLayout(5);
|
|
779
|
-
if (current[0].value === "2" || current[0].value === "3") {
|
|
780
|
-
yield* failpoint("upgrade:before-mutation");
|
|
781
|
-
yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
|
|
782
|
-
yield* failpoint("upgrade:after-mutation");
|
|
783
|
-
yield* failpoint("upgrade:before-mutation");
|
|
784
|
-
yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN message_admission_json TEXT`;
|
|
785
|
-
yield* failpoint("upgrade:after-mutation");
|
|
786
|
-
yield* failpoint("upgrade:before-mutation");
|
|
787
|
-
yield* createMessageDeliveryTables;
|
|
788
|
-
yield* failpoint("upgrade:after-mutation");
|
|
789
|
-
}
|
|
790
|
-
if (current[0].value !== "5") {
|
|
791
|
-
yield* failpoint("upgrade:before-mutation");
|
|
792
|
-
yield* createRecoveryCheckpointTable;
|
|
793
|
-
yield* failpoint("upgrade:after-mutation");
|
|
794
|
-
}
|
|
795
|
-
yield* failpoint("upgrade:before-mutation");
|
|
796
|
-
yield* createNonterminalIndex;
|
|
797
|
-
yield* failpoint("upgrade:after-mutation");
|
|
798
|
-
yield* failpoint("upgrade:before-version");
|
|
799
|
-
yield* createNativeReadIndexes();
|
|
800
|
-
yield* createMessageDeliveryPendingIndex();
|
|
801
|
-
yield* seedNativeReadIndexes.pipe(
|
|
802
|
-
Effect.catchTag("ThreadStoreError", (error) =>
|
|
803
|
-
DoStorageCorruptionError.make({
|
|
804
|
-
table: "effect_agent_canonical_records",
|
|
805
|
-
rowKey: "upgrade",
|
|
806
|
-
message: error.message,
|
|
807
|
-
}),
|
|
808
|
-
),
|
|
809
|
-
);
|
|
810
|
-
yield* createWorkerStops;
|
|
811
|
-
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
812
|
-
yield* failpoint("upgrade:after-version");
|
|
813
|
-
}),
|
|
814
|
-
)
|
|
815
|
-
.pipe(
|
|
816
|
-
Effect.catchTag("DoStorageFailpointError", (error) =>
|
|
817
|
-
DoStorageError.make({
|
|
818
|
-
cause: error,
|
|
819
|
-
operation: "upgrade storage",
|
|
820
|
-
message: error.message,
|
|
821
|
-
}),
|
|
822
|
-
),
|
|
823
|
-
Effect.catchTag("StorageUpgradeError", (error) =>
|
|
824
|
-
DoStorageCorruptionError.make({
|
|
825
|
-
table: error.table,
|
|
826
|
-
rowKey: error.rowKey,
|
|
827
|
-
message: error.message,
|
|
828
|
-
}),
|
|
829
|
-
),
|
|
830
|
-
Effect.catchTag("SchemaError", (error) =>
|
|
831
|
-
DoStorageCorruptionError.make({
|
|
832
|
-
table: "effect_agent_canonical_records",
|
|
833
|
-
rowKey: "upgrade",
|
|
834
|
-
message: error.message,
|
|
835
|
-
}),
|
|
836
|
-
),
|
|
837
|
-
Effect.catchTag("SqlError", storageError("upgrade supported thread storage")),
|
|
838
|
-
);
|
|
839
|
-
} else if (version.value === "6") {
|
|
840
|
-
yield* sql
|
|
841
|
-
.withTransaction(
|
|
842
|
-
Effect.gen(function* () {
|
|
843
|
-
const current = yield* sql<{
|
|
844
|
-
value: string;
|
|
845
|
-
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
846
|
-
|
|
847
|
-
if (current[0]?.value === "9") return;
|
|
848
|
-
if (current[0]?.value !== "6")
|
|
849
|
-
return yield* DoStorageCompatibilityError.make({
|
|
850
|
-
actualVersion: -1,
|
|
851
|
-
supportedVersion: 9,
|
|
852
|
-
message: "Storage version changed during native index upgrade",
|
|
853
|
-
});
|
|
854
|
-
yield* checkPredecessorLayout(5);
|
|
855
|
-
|
|
856
|
-
const requiredIndex =
|
|
857
|
-
yield* sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'effect_agent_submissions_nonterminal'`;
|
|
858
|
-
|
|
859
|
-
if (requiredIndex.length !== 1)
|
|
860
|
-
return yield* DoStorageCompatibilityError.make({
|
|
861
|
-
actualVersion: 6,
|
|
862
|
-
supportedVersion: 9,
|
|
863
|
-
message: "Predecessor storage is missing its required nonterminal index",
|
|
864
|
-
});
|
|
865
|
-
yield* failpoint("upgrade:before-mutation");
|
|
866
|
-
yield* createNativeReadIndexes();
|
|
867
|
-
yield* createMessageDeliveryPendingIndex();
|
|
868
|
-
yield* seedNativeReadIndexes.pipe(
|
|
869
|
-
Effect.catchTag("ThreadStoreError", (error) =>
|
|
870
|
-
DoStorageCorruptionError.make({
|
|
871
|
-
table: "effect_agent_canonical_records",
|
|
872
|
-
rowKey: "upgrade",
|
|
873
|
-
message: error.message,
|
|
874
|
-
}),
|
|
875
|
-
),
|
|
876
|
-
);
|
|
877
|
-
yield* failpoint("upgrade:after-mutation");
|
|
878
|
-
yield* failpoint("upgrade:before-version");
|
|
879
|
-
yield* createWorkerStops;
|
|
880
|
-
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
881
|
-
yield* failpoint("upgrade:after-version");
|
|
882
|
-
}),
|
|
883
|
-
)
|
|
884
|
-
.pipe(
|
|
885
|
-
Effect.mapError((error) =>
|
|
886
|
-
DoStorageError.make({
|
|
887
|
-
operation: "upgrade native indexes",
|
|
888
|
-
message: error.message,
|
|
889
|
-
cause: error,
|
|
890
|
-
}),
|
|
891
|
-
),
|
|
892
|
-
);
|
|
893
|
-
} else if (version.value === "7") {
|
|
894
|
-
yield* sql
|
|
895
|
-
.withTransaction(
|
|
896
|
-
Effect.gen(function* () {
|
|
897
|
-
const current = yield* sql<{
|
|
898
|
-
value: string;
|
|
899
|
-
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
900
|
-
|
|
901
|
-
if (current[0]?.value === "9") return;
|
|
902
|
-
if (current[0]?.value !== "7")
|
|
903
|
-
return yield* DoStorageCompatibilityError.make({
|
|
904
|
-
actualVersion: -1,
|
|
905
|
-
supportedVersion: 9,
|
|
906
|
-
message: "Storage version changed during worker stop upgrade",
|
|
907
|
-
});
|
|
908
|
-
yield* checkPredecessorLayout(7);
|
|
909
|
-
yield* verifyWorkerPredecessor(false);
|
|
910
|
-
yield* failpoint("upgrade:before-mutation");
|
|
911
|
-
yield* createWorkerStops;
|
|
912
|
-
yield* failpoint("upgrade:after-mutation");
|
|
913
|
-
yield* failpoint("upgrade:before-version");
|
|
914
|
-
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
915
|
-
yield* failpoint("upgrade:after-version");
|
|
916
|
-
}),
|
|
917
|
-
)
|
|
918
|
-
.pipe(
|
|
919
|
-
Effect.mapError((cause) =>
|
|
920
|
-
DoStorageError.make({
|
|
921
|
-
operation: "upgrade worker stop",
|
|
922
|
-
message: "Worker stop upgrade failed",
|
|
923
|
-
cause,
|
|
924
|
-
}),
|
|
925
|
-
),
|
|
926
|
-
);
|
|
927
|
-
} else if (version.value === "8") {
|
|
928
|
-
yield* sql
|
|
929
|
-
.withTransaction(
|
|
930
|
-
Effect.gen(function* () {
|
|
931
|
-
const current = yield* sql<{
|
|
932
|
-
value: string;
|
|
933
|
-
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
934
|
-
|
|
935
|
-
if (current[0]?.value === "9") return;
|
|
936
|
-
if (current[0]?.value !== "8")
|
|
937
|
-
return yield* DoStorageCompatibilityError.make({
|
|
938
|
-
actualVersion: -1,
|
|
939
|
-
supportedVersion: 9,
|
|
940
|
-
message: "Storage version changed during assignment seal upgrade",
|
|
941
|
-
});
|
|
942
|
-
yield* checkPredecessorLayout(7);
|
|
943
|
-
yield* verifyWorkerPredecessor(true);
|
|
944
|
-
const columns = yield* sql`PRAGMA table_info(effect_agent_worker_stops)`;
|
|
945
|
-
|
|
946
|
-
yield* Schema.decodeUnknownEffect(
|
|
947
|
-
Schema.Tuple([
|
|
948
|
-
Schema.Struct({
|
|
949
|
-
cid: Schema.Literal(0),
|
|
950
|
-
name: Schema.Literal("thread_id"),
|
|
951
|
-
type: Schema.Literal("TEXT"),
|
|
952
|
-
notnull: Schema.Literal(1),
|
|
953
|
-
dflt_value: Schema.Null,
|
|
954
|
-
pk: Schema.Literal(1),
|
|
955
|
-
}),
|
|
956
|
-
]),
|
|
957
|
-
)(columns).pipe(
|
|
958
|
-
Effect.mapError(() =>
|
|
959
|
-
DoStorageCompatibilityError.make({
|
|
960
|
-
actualVersion: 8,
|
|
961
|
-
supportedVersion: 9,
|
|
962
|
-
message: "Unsupported worker seal layout; no upgrade was committed",
|
|
963
|
-
}),
|
|
964
|
-
),
|
|
965
|
-
);
|
|
966
|
-
yield* failpoint("upgrade:before-mutation");
|
|
967
|
-
yield* sql`ALTER TABLE effect_agent_worker_stops ADD COLUMN terminal TEXT`;
|
|
968
|
-
yield* failpoint("upgrade:after-mutation");
|
|
969
|
-
yield* failpoint("upgrade:before-version");
|
|
970
|
-
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
971
|
-
yield* failpoint("upgrade:after-version");
|
|
972
|
-
}),
|
|
973
|
-
)
|
|
974
|
-
.pipe(
|
|
975
|
-
Effect.mapError((cause) =>
|
|
976
|
-
DoStorageError.make({
|
|
977
|
-
operation: "upgrade assignment seals",
|
|
978
|
-
message: "Assignment seal upgrade failed",
|
|
979
|
-
cause,
|
|
980
|
-
}),
|
|
981
|
-
),
|
|
982
|
-
);
|
|
983
|
-
} else if (version.value !== String(CurrentDoStorageVersion)) {
|
|
984
|
-
const actualVersion = Number.parseInt(version.value, 10);
|
|
985
|
-
|
|
986
|
-
return yield* DoStorageCompatibilityError.make({
|
|
987
|
-
actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
|
|
988
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
989
|
-
message:
|
|
990
|
-
`The Durable Object uses unsupported storage version ${version.value}; ` +
|
|
991
|
-
`this build supports exactly version ${CurrentDoStorageVersion}. ` +
|
|
992
|
-
"Only supported v2, v3, v4, v5, v6, v7 and v8 can be upgraded automatically. Keep the original store and use a compatible library version.",
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
yield* verifyWorkerPredecessor(true);
|
|
321
|
+
yield* ensureDoStorageLayout().pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
998
322
|
|
|
999
323
|
const state = yield* ownedState(sql);
|
|
1000
324
|
const owner = (yield* SqlStorageOwner) ?? state;
|
|
@@ -1043,8 +367,58 @@ const makeJournal = (
|
|
|
1043
367
|
Effect.gen(function* () {
|
|
1044
368
|
const owner = (yield* SqlStorageOwner) ?? state;
|
|
1045
369
|
const progress = yield* SqlStorageProgress;
|
|
370
|
+
|
|
371
|
+
const work = yield* makeSqlThreadWork({
|
|
372
|
+
read: (body) => state.read(body),
|
|
373
|
+
write: (body) =>
|
|
374
|
+
owner.transaction(body.pipe(Effect.tap(() => state.invalidate))).pipe(
|
|
375
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
376
|
+
Effect.catchTag("SqlError", (cause) =>
|
|
377
|
+
ThreadStoreError.make({
|
|
378
|
+
operation: "rebuild Thread work",
|
|
379
|
+
message: "Thread work transaction failed",
|
|
380
|
+
cause,
|
|
381
|
+
}),
|
|
382
|
+
),
|
|
383
|
+
),
|
|
384
|
+
}).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
385
|
+
|
|
386
|
+
yield* Effect.acquireRelease(
|
|
387
|
+
Effect.sync(() => state.invalidators.add(work.invalidate)),
|
|
388
|
+
() => Effect.sync(() => state.invalidators.delete(work.invalidate)),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const workFailure = (cause: unknown) =>
|
|
392
|
+
DoStorageError.make({
|
|
393
|
+
operation: "publish Thread work",
|
|
394
|
+
message: "Canonical work metadata could not be published",
|
|
395
|
+
cause,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const validateProgress = yield* makeProgressAppendValidation().pipe(
|
|
399
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const executions = yield* makeSqlExecutions().pipe(
|
|
403
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const executionConflict = <A>(effect: Effect.Effect<A, AppendConflict | ThreadStoreError>) =>
|
|
407
|
+
effect.pipe(
|
|
408
|
+
Effect.catchTag("AppendConflict", (error) =>
|
|
409
|
+
Effect.fail(
|
|
410
|
+
DoAppendConflict.make({
|
|
411
|
+
reason: error.reason,
|
|
412
|
+
message: "Active execution conflicts with the captured commit",
|
|
413
|
+
...(error.actualTailSequence === undefined
|
|
414
|
+
? {}
|
|
415
|
+
: { actualTailSequence: error.actualTailSequence }),
|
|
416
|
+
}),
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
|
|
1046
421
|
const threads = threadRows(state, sql);
|
|
1047
|
-
const recovery = recoveryRows(state, sql);
|
|
1048
422
|
const cursors = lifecycleCursorRows(state, sql);
|
|
1049
423
|
let records = recordCaches.get(state);
|
|
1050
424
|
|
|
@@ -1055,13 +429,33 @@ const makeJournal = (
|
|
|
1055
429
|
}
|
|
1056
430
|
const recordCache = records;
|
|
1057
431
|
|
|
432
|
+
const archives = yield* makeSqlThreadArchiveRange({
|
|
433
|
+
read: (body) => state.read(body),
|
|
434
|
+
write: (body) =>
|
|
435
|
+
owner.transaction(body.pipe(Effect.tap(() => state.invalidate))).pipe(
|
|
436
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
437
|
+
Effect.catchTag("SqlError", (cause) =>
|
|
438
|
+
ThreadStoreError.make({
|
|
439
|
+
operation: "archive write",
|
|
440
|
+
message: "Archive transaction failed.",
|
|
441
|
+
cause,
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
),
|
|
445
|
+
}).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
446
|
+
|
|
447
|
+
const recordJson = canonicalRecordJson(sql);
|
|
448
|
+
|
|
449
|
+
const physical = yield* makeCanonicalStorage().pipe(
|
|
450
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
451
|
+
);
|
|
452
|
+
|
|
1058
453
|
/** Typed pre-write refusal for any single value over the configured byte bound. */
|
|
1059
454
|
const checkValueBound = (
|
|
1060
455
|
operation: string,
|
|
1061
456
|
value: string,
|
|
457
|
+
actualBytes = storedTextBytes(value),
|
|
1062
458
|
): Effect.Effect<void, DoValueBoundExceeded> => {
|
|
1063
|
-
const actualBytes = storedTextBytes(value);
|
|
1064
|
-
|
|
1065
459
|
return actualBytes > maxStoredValueBytes
|
|
1066
460
|
? Effect.fail(
|
|
1067
461
|
DoValueBoundExceeded.make({
|
|
@@ -1078,8 +472,7 @@ const makeJournal = (
|
|
|
1078
472
|
* `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
|
|
1079
473
|
* Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
|
|
1080
474
|
* `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
|
|
1081
|
-
*
|
|
1082
|
-
* (DUR-006) is preserved identically.
|
|
475
|
+
* The shared local owner gate covers the transaction and acknowledgement.
|
|
1083
476
|
*
|
|
1084
477
|
* Journal write transactions are always top level: the Durable Object client rejects
|
|
1085
478
|
* nested transactions, so new journal operations must not wrap this helper inside another
|
|
@@ -1091,7 +484,7 @@ const makeJournal = (
|
|
|
1091
484
|
<A, E extends { readonly _tag: string }>(
|
|
1092
485
|
effect: Effect.Effect<A, E>,
|
|
1093
486
|
): Effect.Effect<A, E | DoStorageError> =>
|
|
1094
|
-
owner.transaction(storageResult(effect, expected)).pipe(
|
|
487
|
+
state.owner.withMutation(owner.transaction(storageResult(effect, expected))).pipe(
|
|
1095
488
|
Effect.provideService(SqlClient.SqlClient, sql),
|
|
1096
489
|
Effect.mapError((error) => (isSqlError(error) ? storageError(operation)(error) : error)),
|
|
1097
490
|
Effect.tapError(annotateStorageError),
|
|
@@ -1099,126 +492,84 @@ const makeJournal = (
|
|
|
1099
492
|
Effect.flatMap(Effect.fromResult),
|
|
1100
493
|
);
|
|
1101
494
|
|
|
1102
|
-
const materialize = Effect.fnUntraced(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
operation: "materialize thread",
|
|
1115
|
-
message: "Thread identity exceeds the Durable Object storage bounds.",
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
yield* checkValueBound("materialize thread", emptyTailDigest);
|
|
1119
|
-
// Refusals and idempotent materialization need no transaction. Recheck before writes.
|
|
1120
|
-
const current = yield* getThread(threadId);
|
|
1121
|
-
|
|
1122
|
-
if (current.length === 1 && producerEpoch <= current[0].producer_epoch) {
|
|
1123
|
-
if (producerEpoch === current[0].producer_epoch) return;
|
|
495
|
+
const materialize = Effect.fnUntraced(function* (
|
|
496
|
+
threadId: string,
|
|
497
|
+
createdAt: string,
|
|
498
|
+
): Effect.fn.Return<void, DoStorageCorruptionError | DoStorageError | DoValueBoundExceeded> {
|
|
499
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH) {
|
|
500
|
+
return yield* DoStorageError.make({
|
|
501
|
+
operation: "materialize thread",
|
|
502
|
+
message: "Thread identity exceeds the Durable Object storage bounds.",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// Refusals and idempotent materialization need no transaction. Recheck before writes.
|
|
506
|
+
const current = yield* getThread(threadId);
|
|
1124
507
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
yield* withWriteTransaction(
|
|
1132
|
-
"materialize transaction",
|
|
1133
|
-
(error) => error._tag === "DoFenceRejected",
|
|
1134
|
-
)(
|
|
1135
|
-
Effect.gen(function* () {
|
|
1136
|
-
const existingRows = yield* getThread(threadId);
|
|
508
|
+
if (current.length === 1) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
yield* withWriteTransaction("materialize transaction")(
|
|
512
|
+
Effect.gen(function* () {
|
|
513
|
+
const existingRows = yield* getThread(threadId);
|
|
1137
514
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
515
|
+
const existing = yield* decodeRows(
|
|
516
|
+
Schema.Array(ThreadRow),
|
|
517
|
+
"effect_agent_threads",
|
|
518
|
+
threadId,
|
|
519
|
+
existingRows,
|
|
520
|
+
);
|
|
1144
521
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
522
|
+
if (existing.length > 1) {
|
|
523
|
+
return yield* DoStorageCorruptionError.make({
|
|
524
|
+
table: "effect_agent_threads",
|
|
525
|
+
rowKey: threadId,
|
|
526
|
+
message: "A thread primary key returned more than one row.",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (existing.length === 0) {
|
|
530
|
+
yield* sql`
|
|
1154
531
|
INSERT INTO effect_agent_threads (
|
|
1155
532
|
thread_id,
|
|
1156
533
|
created_at,
|
|
1157
|
-
tail_sequence
|
|
1158
|
-
tail_digest,
|
|
1159
|
-
producer_epoch
|
|
534
|
+
tail_sequence
|
|
1160
535
|
) VALUES (
|
|
1161
536
|
${threadId},
|
|
1162
537
|
${createdAt},
|
|
1163
|
-
0
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
)
|
|
538
|
+
0
|
|
539
|
+
) ON CONFLICT(thread_id) DO UPDATE SET created_at=excluded.created_at
|
|
540
|
+
WHERE effect_agent_threads.created_at IS NULL
|
|
1167
541
|
RETURNING *`.pipe(
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
542
|
+
threads.write,
|
|
543
|
+
Effect.mapError((error) =>
|
|
544
|
+
error._tag === "SqlError" ? storageError("materialize thread")(error) : error,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
1173
547
|
|
|
1174
|
-
|
|
1175
|
-
|
|
548
|
+
if (lifecycle !== undefined)
|
|
549
|
+
yield* sql`INSERT INTO effect_agent_lifecycle_cursors (thread_id, through_sequence)
|
|
1176
550
|
VALUES (${threadId}, 0) RETURNING *`.pipe(
|
|
1177
|
-
|
|
1178
|
-
Effect.mapError((error) =>
|
|
1179
|
-
error._tag === "SqlError"
|
|
1180
|
-
? storageError("initialize lifecycle source")(error)
|
|
1181
|
-
: error,
|
|
1182
|
-
),
|
|
1183
|
-
);
|
|
1184
|
-
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
if (producerEpoch < existing[0].producer_epoch) {
|
|
1188
|
-
return yield* DoFenceRejected.make({
|
|
1189
|
-
producerEpoch,
|
|
1190
|
-
actualEpoch: existing[0].producer_epoch,
|
|
1191
|
-
message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`,
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
if (producerEpoch > existing[0].producer_epoch) {
|
|
1195
|
-
yield* sql`
|
|
1196
|
-
UPDATE effect_agent_threads
|
|
1197
|
-
SET producer_epoch = ${producerEpoch}
|
|
1198
|
-
WHERE thread_id = ${threadId}
|
|
1199
|
-
RETURNING *`.pipe(
|
|
1200
|
-
threads.write,
|
|
551
|
+
cursors.write,
|
|
1201
552
|
Effect.mapError((error) =>
|
|
1202
553
|
error._tag === "SqlError"
|
|
1203
|
-
? storageError("
|
|
554
|
+
? storageError("initialize lifecycle source")(error)
|
|
1204
555
|
: error,
|
|
1205
556
|
),
|
|
1206
557
|
);
|
|
1207
|
-
}
|
|
1208
|
-
}),
|
|
1209
|
-
);
|
|
1210
|
-
},
|
|
1211
|
-
withStorageSpan("DoJournal.materialize", (error) => error._tag === "DoFenceRejected"),
|
|
1212
|
-
);
|
|
1213
558
|
|
|
1214
|
-
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
}, withStorageSpan("DoJournal.materialize"));
|
|
564
|
+
|
|
565
|
+
const getThread = Effect.fnUntraced(function* (threadId: string) {
|
|
1215
566
|
return (yield* threads
|
|
1216
567
|
.by("thread_id", threadId)
|
|
1217
568
|
.pipe(
|
|
1218
569
|
Effect.mapError((error) =>
|
|
1219
570
|
error._tag === "SqlError" ? storageError("read thread")(error) : error,
|
|
1220
571
|
),
|
|
1221
|
-
)).filter((row) => row.thread_id === threadId);
|
|
572
|
+
)).filter((row) => row.thread_id === threadId && row.created_at !== null);
|
|
1222
573
|
});
|
|
1223
574
|
|
|
1224
575
|
const hasRecord = Effect.fnUntraced(function* (threadId: string, recordId: string) {
|
|
@@ -1228,360 +579,367 @@ const makeJournal = (
|
|
|
1228
579
|
if (prefix !== undefined) return prefix.some((row) => row.record_id === recordId);
|
|
1229
580
|
|
|
1230
581
|
const rows = yield* sql`SELECT record_id FROM effect_agent_canonical_records
|
|
1231
|
-
WHERE thread_id = ${threadId} AND record_id = ${recordId}`.pipe(
|
|
582
|
+
WHERE thread_id = ${threadId} AND record_id = ${JSON.stringify(recordId)}`.pipe(
|
|
1232
583
|
Effect.mapError(storageError("read canonical record identity")),
|
|
1233
584
|
);
|
|
1234
585
|
|
|
1235
586
|
return rows.length > 0;
|
|
1236
587
|
});
|
|
1237
588
|
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
request
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
589
|
+
const prepareAppend = Effect.fnUntraced(function* (request: RawAppendRequest) {
|
|
590
|
+
if (
|
|
591
|
+
request.threadId.length > MAX_IDENTIFIER_LENGTH ||
|
|
592
|
+
request.batchId.length > MAX_IDENTIFIER_LENGTH ||
|
|
593
|
+
request.executionWrites.some(
|
|
594
|
+
({ update }) => update.checkpoint.progress.runId.length > MAX_IDENTIFIER_LENGTH,
|
|
595
|
+
) ||
|
|
596
|
+
request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)
|
|
597
|
+
) {
|
|
598
|
+
return yield* DoStorageError.make({
|
|
599
|
+
operation: "append canonical batch",
|
|
600
|
+
message: "Canonical identifiers exceed the Durable Object storage bounds.",
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
// Archives store the full batch as one value, so their ceiling also bounds hot appends.
|
|
604
|
+
// The trusted append SPI pairs captured JSON with its exact UTF-8 width.
|
|
605
|
+
yield* checkValueBound("append canonical batch", request.batchJson, request.batchBytes);
|
|
606
|
+
for (const record of request.records) {
|
|
607
|
+
if (record.recordBytes > maxStoredValueBytes)
|
|
608
|
+
return yield* DoValueBoundExceeded.make({
|
|
609
|
+
actualBytes: record.recordBytes,
|
|
610
|
+
maxBytes: maxStoredValueBytes,
|
|
611
|
+
operation: "append canonical record",
|
|
1251
612
|
});
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
yield* checkValueBound("
|
|
1255
|
-
yield* checkValueBound("append canonical batch", request.batchDigest);
|
|
1256
|
-
yield* checkValueBound("append canonical batch", request.tailDigest);
|
|
1257
|
-
yield* Effect.forEach(
|
|
1258
|
-
request.records,
|
|
1259
|
-
(record) => checkValueBound("append canonical record", record.recordJson),
|
|
1260
|
-
{ discard: true },
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
const recordIds = request.records.map((record) => record.recordId);
|
|
613
|
+
}
|
|
614
|
+
for (const write of request.executionWrites)
|
|
615
|
+
yield* checkValueBound("commit active execution", write.json, write.bytes);
|
|
1264
616
|
|
|
1265
|
-
|
|
1266
|
-
return yield* DoAppendConflict.make({
|
|
1267
|
-
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
1268
|
-
reason: "record-identity",
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
617
|
+
const recordIds: Array<string> = request.records.map((record) => record.recordId);
|
|
1271
618
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
actualEpoch: observed.producer_epoch,
|
|
1279
|
-
message: "The append producer epoch is not current.",
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
if (
|
|
1283
|
-
request.expectedTailSequence !== observed.tail_sequence ||
|
|
1284
|
-
request.expectedTailDigest !== observed.tail_digest
|
|
1285
|
-
) {
|
|
1286
|
-
const batches = yield* sql`
|
|
1287
|
-
SELECT batch_id FROM effect_agent_canonical_batches
|
|
1288
|
-
WHERE thread_id = ${request.threadId} AND batch_id = ${request.batchId}
|
|
1289
|
-
`.pipe(Effect.mapError(storageError("preflight append replay")));
|
|
619
|
+
if (new Set(recordIds).size !== recordIds.length) {
|
|
620
|
+
return yield* DoAppendConflict.make({
|
|
621
|
+
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
622
|
+
reason: "record-identity",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
1290
625
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return yield* DoAppendConflict.make({
|
|
1294
|
-
reason: "tail",
|
|
1295
|
-
message: "The canonical tail has advanced.",
|
|
1296
|
-
actualTailSequence: observed.tail_sequence,
|
|
1297
|
-
actualTailDigest: observed.tail_digest,
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
626
|
+
return request;
|
|
627
|
+
});
|
|
1301
628
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
const threadRows = yield* getThread(request.threadId);
|
|
629
|
+
const appendPrepared = Effect.fnUntraced(function* (
|
|
630
|
+
request: RawAppendRequest,
|
|
631
|
+
): Effect.fn.Return<RawAppendResult, AppendError> {
|
|
632
|
+
const recordIds: Array<string> = request.records.map((record) => record.recordId);
|
|
633
|
+
const threadRows = yield* getThread(request.threadId);
|
|
1308
634
|
|
|
1309
|
-
|
|
1310
|
-
Schema.Array(ThreadRow),
|
|
1311
|
-
"effect_agent_threads",
|
|
1312
|
-
request.threadId,
|
|
1313
|
-
threadRows,
|
|
1314
|
-
);
|
|
635
|
+
const thread = threadRows.length === 1 ? threadRows[0] : undefined;
|
|
1315
636
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
637
|
+
if (thread === undefined)
|
|
638
|
+
return yield* DoStorageCorruptionError.make({
|
|
639
|
+
table: "effect_agent_threads",
|
|
640
|
+
rowKey: request.threadId,
|
|
641
|
+
message: `Expected exactly one row but found ${threadRows.length}.`,
|
|
642
|
+
});
|
|
1323
643
|
|
|
1324
|
-
|
|
644
|
+
const prefix = recordCache.prefix(request.threadId, thread.tail_sequence);
|
|
1325
645
|
|
|
1326
|
-
|
|
1327
|
-
prefix !== undefined && !prefix.some((row) => row.batch_id === request.batchId)
|
|
1328
|
-
? []
|
|
1329
|
-
: yield* sql<Record<string, unknown>>`
|
|
1330
|
-
SELECT
|
|
1331
|
-
thread_id,
|
|
1332
|
-
batch_id,
|
|
1333
|
-
first_sequence,
|
|
1334
|
-
last_sequence,
|
|
1335
|
-
batch_digest,
|
|
1336
|
-
tail_digest,
|
|
1337
|
-
batch_json
|
|
1338
|
-
FROM effect_agent_canonical_batches
|
|
1339
|
-
WHERE thread_id = ${request.threadId}
|
|
1340
|
-
AND batch_id = ${request.batchId}
|
|
1341
|
-
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
1342
|
-
|
|
1343
|
-
const batches = yield* decodeRows(
|
|
1344
|
-
Schema.Array(BatchRow),
|
|
1345
|
-
"effect_agent_canonical_batches",
|
|
1346
|
-
`${request.threadId}/${request.batchId}`,
|
|
1347
|
-
batchRows,
|
|
1348
|
-
);
|
|
646
|
+
const replay = yield* executionConflict(executions.replay(request));
|
|
1349
647
|
|
|
1350
|
-
|
|
1351
|
-
return yield* DoStorageCorruptionError.make({
|
|
1352
|
-
table: "effect_agent_canonical_batches",
|
|
1353
|
-
rowKey: `${request.threadId}/${request.batchId}`,
|
|
1354
|
-
message: "A canonical batch primary key returned more than one row.",
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
if (batches.length === 1) {
|
|
1358
|
-
const existing = batches[0];
|
|
1359
|
-
|
|
1360
|
-
if (existing.batch_digest !== request.batchDigest) {
|
|
1361
|
-
return yield* DoAppendConflict.make({
|
|
1362
|
-
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
1363
|
-
reason: "batch-digest",
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
648
|
+
if (replay !== undefined) return RawAppendResult.make(replay);
|
|
1366
649
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
tailDigest: existing.tail_digest,
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
650
|
+
const existing =
|
|
651
|
+
prefix !== undefined && !prefix.some((row) => row.batch_id === request.batchId)
|
|
652
|
+
? undefined
|
|
653
|
+
: yield* archives.replay(request.threadId, request.batchId);
|
|
1374
654
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
`but found ${thread.tail_sequence}/${thread.tail_digest}.`,
|
|
1383
|
-
reason: "tail",
|
|
1384
|
-
actualTailSequence: thread.tail_sequence,
|
|
1385
|
-
actualTailDigest: thread.tail_digest,
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) {
|
|
1389
|
-
return yield* DoStorageError.make({
|
|
1390
|
-
operation: "append canonical batch",
|
|
1391
|
-
message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`,
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
655
|
+
if (existing !== undefined) {
|
|
656
|
+
if (existing.batch_json !== request.batchJson) {
|
|
657
|
+
return yield* DoAppendConflict.make({
|
|
658
|
+
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
659
|
+
reason: "batch-content",
|
|
660
|
+
});
|
|
661
|
+
}
|
|
1394
662
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
663
|
+
return RawAppendResult.make({
|
|
664
|
+
firstSequence: existing.first_sequence,
|
|
665
|
+
lastSequence: existing.last_sequence,
|
|
666
|
+
replayed: true,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
1399
669
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
670
|
+
yield* executionConflict(executions.rejectSuperseded(request));
|
|
671
|
+
|
|
672
|
+
if (request.expectedTailSequence !== thread.tail_sequence) {
|
|
673
|
+
return yield* DoAppendConflict.make({
|
|
674
|
+
message:
|
|
675
|
+
`Expected tail ${request.expectedTailSequence} ` + `but found ${thread.tail_sequence}.`,
|
|
676
|
+
reason: "tail",
|
|
677
|
+
actualTailSequence: thread.tail_sequence,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Chunked to respect the Durable Object platform's 100-bound-parameter statement
|
|
682
|
+
// limit: a batch may carry up to 256 records.
|
|
683
|
+
const existingRecords: Array<RecordRow> =
|
|
684
|
+
prefix === undefined ? [] : prefix.filter((row) => recordIds.includes(row.record_id));
|
|
685
|
+
|
|
686
|
+
for (const chunk of prefix === undefined
|
|
687
|
+
? chunked(recordIds, MAX_BOUND_PARAMETERS - 10)
|
|
688
|
+
: []) {
|
|
689
|
+
const existingRecordRows = yield* sql<Record<string, unknown>>`
|
|
1404
690
|
SELECT
|
|
1405
691
|
thread_id,
|
|
1406
692
|
sequence,
|
|
1407
693
|
record_id,
|
|
1408
|
-
batch_id,
|
|
1409
|
-
record_json
|
|
694
|
+
${recordBatchId(sql)} AS batch_id,
|
|
695
|
+
${recordJson} AS record_json
|
|
1410
696
|
FROM effect_agent_canonical_records
|
|
1411
697
|
WHERE thread_id = ${request.threadId}
|
|
1412
|
-
AND record_id IN ${sql.in(
|
|
1413
|
-
ORDER BY sequence
|
|
698
|
+
AND record_id IN ${sql.in(chunk.map((id) => JSON.stringify(id)))}
|
|
1414
699
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
1415
700
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
701
|
+
existingRecords.push(
|
|
702
|
+
...(yield* decodeRows(
|
|
703
|
+
Schema.Array(RecordRow),
|
|
704
|
+
"effect_agent_canonical_records",
|
|
705
|
+
`${request.threadId}/record_ids`,
|
|
706
|
+
existingRecordRows,
|
|
707
|
+
)),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (existingRecords.length > 0) {
|
|
711
|
+
return yield* DoAppendConflict.make({
|
|
712
|
+
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
713
|
+
reason: "record-identity",
|
|
714
|
+
});
|
|
715
|
+
}
|
|
1431
716
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
717
|
+
const firstSequence = yield* Schema.decodeEffect(CanonicalSequence)(
|
|
718
|
+
thread.tail_sequence + (request.records.length === 0 ? 0 : 1),
|
|
719
|
+
).pipe(
|
|
720
|
+
Effect.mapError((error) =>
|
|
721
|
+
DoStorageError.make({
|
|
722
|
+
cause: error,
|
|
723
|
+
operation: "append canonical batch",
|
|
724
|
+
message: error.message,
|
|
725
|
+
}),
|
|
726
|
+
),
|
|
727
|
+
);
|
|
1443
728
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
729
|
+
const lastSequence = yield* Schema.decodeEffect(CanonicalSequence)(
|
|
730
|
+
thread.tail_sequence + request.records.length,
|
|
731
|
+
).pipe(
|
|
732
|
+
Effect.mapError((error) =>
|
|
733
|
+
DoStorageError.make({
|
|
734
|
+
cause: error,
|
|
735
|
+
operation: "append canonical batch",
|
|
736
|
+
message: error.message,
|
|
737
|
+
}),
|
|
738
|
+
),
|
|
739
|
+
);
|
|
1455
740
|
|
|
1456
|
-
|
|
741
|
+
yield* executionConflict(executions.validate(request));
|
|
742
|
+
yield* validateProgress(request);
|
|
743
|
+
|
|
744
|
+
const result = RawAppendResult.make({
|
|
745
|
+
firstSequence,
|
|
746
|
+
lastSequence,
|
|
747
|
+
replayed: false,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (request.records.length > 0) {
|
|
751
|
+
yield* sql`
|
|
1457
752
|
INSERT INTO effect_agent_canonical_batches (
|
|
1458
753
|
thread_id,
|
|
1459
754
|
batch_id,
|
|
1460
755
|
first_sequence,
|
|
1461
756
|
last_sequence,
|
|
1462
|
-
|
|
1463
|
-
tail_digest,
|
|
1464
|
-
batch_json
|
|
757
|
+
producer_id_json
|
|
1465
758
|
) VALUES (
|
|
1466
759
|
${request.threadId},
|
|
1467
|
-
${request.batchId},
|
|
760
|
+
${JSON.stringify(request.batchId)},
|
|
1468
761
|
${firstSequence},
|
|
1469
762
|
${lastSequence},
|
|
1470
|
-
${request.
|
|
1471
|
-
${request.tailDigest},
|
|
1472
|
-
${request.batchJson}
|
|
763
|
+
${request.producerIdJson}
|
|
1473
764
|
)
|
|
1474
765
|
`.pipe(Effect.mapError(storageError("insert canonical batch")));
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const records = yield* Effect.forEach(request.records, (record, index) =>
|
|
1478
|
-
Effect.gen(function* () {
|
|
1479
|
-
const canonical = yield* Schema.decodeEffect(
|
|
1480
|
-
Schema.fromJsonString(CanonicalRecord),
|
|
1481
|
-
)(record.recordJson).pipe(
|
|
1482
|
-
Effect.mapError((error) =>
|
|
1483
|
-
DoStorageCorruptionError.make({
|
|
1484
|
-
table: "effect_agent_canonical_records",
|
|
1485
|
-
rowKey: record.recordId,
|
|
1486
|
-
message: error.message,
|
|
1487
|
-
}),
|
|
1488
|
-
),
|
|
1489
|
-
);
|
|
1490
|
-
|
|
1491
|
-
return {
|
|
1492
|
-
record,
|
|
1493
|
-
canonical,
|
|
1494
|
-
row: {
|
|
1495
|
-
thread_id: request.threadId,
|
|
1496
|
-
sequence: firstSequence + index,
|
|
1497
|
-
record_id: record.recordId,
|
|
1498
|
-
batch_id: request.batchId,
|
|
1499
|
-
record_json: record.recordJson,
|
|
1500
|
-
outstanding: canonicalRecordOutstanding(canonical),
|
|
1501
|
-
},
|
|
1502
|
-
};
|
|
1503
|
-
}),
|
|
1504
|
-
);
|
|
766
|
+
}
|
|
767
|
+
yield* failpoint("append:after-batch-insert");
|
|
1505
768
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
for (const { canonical, row } of group) {
|
|
1512
|
-
yield* indexCanonicalRecord(request.threadId, canonical, undefined, {
|
|
1513
|
-
sequence: row.sequence,
|
|
1514
|
-
}).pipe(
|
|
1515
|
-
Effect.provideService(SqlClient.SqlClient, sql),
|
|
1516
|
-
Effect.mapError(storageError("index canonical record")),
|
|
1517
|
-
);
|
|
1518
|
-
recordCache.put(
|
|
1519
|
-
RecordRow.make({
|
|
1520
|
-
...row,
|
|
1521
|
-
sequence: Schema.decodeSync(CanonicalSequence)(row.sequence),
|
|
1522
|
-
}),
|
|
1523
|
-
);
|
|
1524
|
-
yield* failpoint("append:after-record-insert");
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
769
|
+
const physicalRecords = yield* physical.records(
|
|
770
|
+
request.threadId,
|
|
771
|
+
firstSequence,
|
|
772
|
+
request.records,
|
|
773
|
+
);
|
|
1527
774
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
775
|
+
const records = physicalRecords.map(({ columns, memberships }, index) => ({
|
|
776
|
+
record: request.records[index]!,
|
|
777
|
+
canonical: request.records[index]!.canonical,
|
|
778
|
+
row: columns,
|
|
779
|
+
memberships,
|
|
780
|
+
}));
|
|
781
|
+
|
|
782
|
+
// Captured scalar locators avoid parsing canonical JSON under the writer.
|
|
783
|
+
// Eighteen bound columns per row stay within workerd's statement limit.
|
|
784
|
+
for (const group of chunked(records, Math.floor(MAX_BOUND_PARAMETERS / 18))) {
|
|
785
|
+
yield* sql`INSERT INTO effect_agent_canonical_records ${sql.insert(group.map(({ row }) => row))}`.pipe(
|
|
786
|
+
Effect.mapError(storageError("insert canonical records")),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const memberships = group.flatMap(({ memberships }) => memberships);
|
|
790
|
+
|
|
791
|
+
for (const members of chunked(memberships, Math.floor(MAX_BOUND_PARAMETERS / 3)))
|
|
792
|
+
yield* sql`INSERT INTO effect_agent_record_runs ${sql.insert(members)}`.pipe(
|
|
793
|
+
Effect.mapError(storageError("index canonical Run membership")),
|
|
794
|
+
);
|
|
795
|
+
for (const { row, record } of group) {
|
|
796
|
+
recordCache.put(
|
|
797
|
+
RecordRow.make({
|
|
798
|
+
...row,
|
|
799
|
+
record_id: record.recordId,
|
|
800
|
+
batch_id: request.batchId,
|
|
801
|
+
record_json: record.recordJson,
|
|
802
|
+
sequence: Schema.decodeSync(CanonicalSequence)(row.sequence),
|
|
803
|
+
}),
|
|
804
|
+
record.recordBytes,
|
|
805
|
+
);
|
|
806
|
+
yield* failpoint("append:after-record-insert");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (request.records.length > 0) {
|
|
811
|
+
const workFields = yield* work
|
|
812
|
+
.apply(
|
|
813
|
+
yield* Schema.decodeEffect(ThreadId)(request.threadId).pipe(
|
|
814
|
+
Effect.mapError(workFailure),
|
|
815
|
+
),
|
|
816
|
+
firstSequence,
|
|
817
|
+
request.records.map((record) => record.canonical),
|
|
818
|
+
)
|
|
819
|
+
.pipe(Effect.mapError(workFailure));
|
|
820
|
+
|
|
821
|
+
yield* sql`UPDATE effect_agent_threads SET ${sql.update({ tail_sequence: lastSequence, ...workFields })}
|
|
822
|
+
WHERE thread_id=${request.threadId} RETURNING *`.pipe(
|
|
823
|
+
threads.write,
|
|
824
|
+
Effect.mapError((error) =>
|
|
825
|
+
error._tag === "SqlError" ? storageError("advance thread tail")(error) : error,
|
|
826
|
+
),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
yield* executionConflict(executions.commit(request, result));
|
|
830
|
+
if (request.executionWrites.length > 0) yield* failpoint("append:after-record-insert");
|
|
831
|
+
if (
|
|
832
|
+
lifecycle !== undefined &&
|
|
833
|
+
request.executionWrites.some(({ update }) =>
|
|
834
|
+
update.facts.some(({ payload }) => payload._tag === "RunStarted"),
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
yield* flushCanonical(request.threadId).pipe(
|
|
838
|
+
Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }),
|
|
839
|
+
Effect.mapError((cause) =>
|
|
840
|
+
DoStorageError.make({
|
|
841
|
+
operation: "retain lifecycle start prefix",
|
|
842
|
+
message: "Lifecycle start intent could not be retained",
|
|
843
|
+
cause,
|
|
844
|
+
}),
|
|
845
|
+
),
|
|
846
|
+
);
|
|
847
|
+
for (const { update } of request.executionWrites) {
|
|
848
|
+
for (const fact of update.facts) {
|
|
849
|
+
if (
|
|
850
|
+
lifecycle !== undefined &&
|
|
851
|
+
Schema.is(Schema.toType(LifecyclePublicationFact))(fact.payload)
|
|
852
|
+
)
|
|
853
|
+
yield* lifecycle
|
|
854
|
+
.retain({
|
|
855
|
+
id: JSON.stringify([request.threadId, "record", fact.recordId]),
|
|
856
|
+
ownerThreadId: request.threadId,
|
|
857
|
+
canonicalSequence: lastSequence,
|
|
858
|
+
createdAt: fact.createdAt,
|
|
859
|
+
fact: fact.payload,
|
|
860
|
+
})
|
|
861
|
+
.pipe(
|
|
1554
862
|
Effect.mapError((cause) =>
|
|
1555
863
|
DoStorageError.make({
|
|
1556
|
-
operation: "retain lifecycle
|
|
1557
|
-
message:
|
|
864
|
+
operation: "retain operational lifecycle",
|
|
865
|
+
message: cause.message,
|
|
1558
866
|
cause,
|
|
1559
867
|
}),
|
|
1560
868
|
),
|
|
1561
869
|
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
yield* failpoint("append:after-tail-update");
|
|
873
|
+
|
|
874
|
+
// Retain one start prefix atomically with its canonical proof, before model/tool
|
|
875
|
+
// execution. Later facts stay journal-backed until the settlement publication wave.
|
|
876
|
+
if (
|
|
877
|
+
lifecycle !== undefined &&
|
|
878
|
+
records.some(
|
|
879
|
+
({ canonical: { payload } }) =>
|
|
880
|
+
payload._tag === "RunStarted" || payload._tag === "SubagentStarted",
|
|
881
|
+
)
|
|
882
|
+
)
|
|
883
|
+
yield* flushCanonical(request.threadId).pipe(
|
|
884
|
+
Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }),
|
|
885
|
+
Effect.mapError((cause) =>
|
|
886
|
+
DoStorageError.make({
|
|
887
|
+
operation: "retain lifecycle start prefix",
|
|
888
|
+
message: "Lifecycle start intent could not be retained",
|
|
889
|
+
cause,
|
|
890
|
+
}),
|
|
891
|
+
),
|
|
892
|
+
);
|
|
1562
893
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
894
|
+
yield* progress.committed("canonical").pipe(
|
|
895
|
+
Effect.catchCause((cause) =>
|
|
896
|
+
Effect.failCause(
|
|
897
|
+
Cause.map(cause, (error) =>
|
|
898
|
+
DoStorageError.make({
|
|
899
|
+
operation: "enroll canonical progress",
|
|
900
|
+
message: error.message,
|
|
901
|
+
cause: error,
|
|
902
|
+
}),
|
|
903
|
+
),
|
|
904
|
+
),
|
|
905
|
+
),
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
return result;
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const append = Effect.fnUntraced(
|
|
912
|
+
function* (
|
|
913
|
+
request: RawAppendRequest,
|
|
914
|
+
observed: ThreadRow,
|
|
915
|
+
): Effect.fn.Return<RawAppendResult, AppendError> {
|
|
916
|
+
yield* prepareAppend(request);
|
|
917
|
+
// The facade already read this tail. Reject a known stale writer without opening a
|
|
918
|
+
// transaction; a matching tail is still checked atomically below. Replays precede tail
|
|
919
|
+
// conflicts, preserving retry identity even after the log has advanced.
|
|
920
|
+
if (request.records.length > 0 && request.expectedTailSequence !== observed.tail_sequence) {
|
|
921
|
+
const batches = yield* sql`
|
|
922
|
+
SELECT batch_id FROM effect_agent_canonical_batches
|
|
923
|
+
WHERE thread_id = ${request.threadId} AND batch_id = ${JSON.stringify(request.batchId)}
|
|
924
|
+
UNION ALL SELECT last_commit_id FROM effect_agent_executions
|
|
925
|
+
WHERE thread_id = ${request.threadId} AND last_commit_id = ${request.batchId}
|
|
926
|
+
LIMIT 1
|
|
927
|
+
`.pipe(Effect.mapError(storageError("preflight append replay")));
|
|
1576
928
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
929
|
+
// A possible replay still checks exact content in the transaction.
|
|
930
|
+
if (batches.length === 0) {
|
|
931
|
+
return yield* DoAppendConflict.make({
|
|
932
|
+
reason: "tail",
|
|
933
|
+
message: "The canonical tail has advanced.",
|
|
934
|
+
actualTailSequence: observed.tail_sequence,
|
|
1582
935
|
});
|
|
1583
|
-
}
|
|
1584
|
-
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return yield* withWriteTransaction(
|
|
940
|
+
"append transaction",
|
|
941
|
+
isAppendContention,
|
|
942
|
+
)(appendPrepared(request));
|
|
1585
943
|
},
|
|
1586
944
|
withStorageSpan("DoJournal.append", isAppendContention),
|
|
1587
945
|
);
|
|
@@ -1611,7 +969,7 @@ const makeJournal = (
|
|
|
1611
969
|
const planRows = yield* sql<Record<string, unknown>>`
|
|
1612
970
|
SELECT
|
|
1613
971
|
sequence,
|
|
1614
|
-
length(CAST(
|
|
972
|
+
length(CAST(${recordJson} AS BLOB)) AS record_json_bytes
|
|
1615
973
|
FROM effect_agent_canonical_records
|
|
1616
974
|
WHERE thread_id = ${request.threadId}
|
|
1617
975
|
AND sequence > ${request.fromSequenceExclusive}
|
|
@@ -1655,9 +1013,9 @@ const makeJournal = (
|
|
|
1655
1013
|
}
|
|
1656
1014
|
}
|
|
1657
1015
|
|
|
1658
|
-
const readPage = Effect.
|
|
1016
|
+
const readPage = Effect.fnUntraced(function* (page: ReadPage) {
|
|
1659
1017
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1660
|
-
SELECT thread_id, sequence, record_id, batch_id, record_json
|
|
1018
|
+
SELECT thread_id, sequence, record_id, ${recordBatchId(sql)} AS batch_id, ${recordJson} AS record_json
|
|
1661
1019
|
FROM effect_agent_canonical_records
|
|
1662
1020
|
WHERE thread_id = ${request.threadId}
|
|
1663
1021
|
AND sequence >= ${page[0].sequence}
|
|
@@ -1698,80 +1056,7 @@ const makeJournal = (
|
|
|
1698
1056
|
};
|
|
1699
1057
|
});
|
|
1700
1058
|
|
|
1701
|
-
const
|
|
1702
|
-
return yield* state
|
|
1703
|
-
.transaction(
|
|
1704
|
-
Effect.gen(function* () {
|
|
1705
|
-
const threadRows = yield* getThread(threadId);
|
|
1706
|
-
|
|
1707
|
-
const thread = yield* decodeSingleRow(
|
|
1708
|
-
Schema.Array(ThreadRow),
|
|
1709
|
-
"effect_agent_threads",
|
|
1710
|
-
threadId,
|
|
1711
|
-
threadRows,
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
yield* failpoint("export:after-thread-read");
|
|
1715
|
-
|
|
1716
|
-
if (thread.tail_sequence > MAX_RECORDS_PER_THREAD)
|
|
1717
|
-
return yield* DoStorageError.make({
|
|
1718
|
-
operation: "export thread",
|
|
1719
|
-
message: "The thread exceeds the current export record limit.",
|
|
1720
|
-
});
|
|
1721
|
-
const records: Array<RecordRow> = [];
|
|
1722
|
-
let afterSequence = ZERO_SEQUENCE;
|
|
1723
|
-
|
|
1724
|
-
while (afterSequence < thread.tail_sequence) {
|
|
1725
|
-
const limit = Math.min(1_024, thread.tail_sequence - afterSequence);
|
|
1726
|
-
|
|
1727
|
-
const request = RawReadRequest.make({
|
|
1728
|
-
threadId,
|
|
1729
|
-
fromSequenceExclusive: afterSequence,
|
|
1730
|
-
limit,
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
const plan = yield* read(request);
|
|
1734
|
-
const page = yield* Stream.runCollect(plan.records);
|
|
1735
|
-
|
|
1736
|
-
if (
|
|
1737
|
-
page.length !== limit ||
|
|
1738
|
-
page.some((record, index) => record.sequence !== afterSequence + index + 1)
|
|
1739
|
-
) {
|
|
1740
|
-
return yield* DoStorageCorruptionError.make({
|
|
1741
|
-
table: "effect_agent_canonical_records",
|
|
1742
|
-
rowKey: threadId,
|
|
1743
|
-
message:
|
|
1744
|
-
"The exported canonical prefix is not contiguous through its captured tail.",
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
records.push(...page);
|
|
1748
|
-
afterSequence = page[page.length - 1].sequence;
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
const beyondTail =
|
|
1752
|
-
yield* sql`SELECT sequence FROM effect_agent_canonical_records WHERE thread_id=${threadId} AND sequence > ${thread.tail_sequence} LIMIT 1`.pipe(
|
|
1753
|
-
Effect.mapError(storageError("verify export tail")),
|
|
1754
|
-
);
|
|
1755
|
-
|
|
1756
|
-
if (beyondTail.length !== 0)
|
|
1757
|
-
return yield* DoStorageCorruptionError.make({
|
|
1758
|
-
table: "effect_agent_canonical_records",
|
|
1759
|
-
rowKey: threadId,
|
|
1760
|
-
message: "Canonical records exist beyond the captured thread tail.",
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
return RawThreadExport.make({ thread, records });
|
|
1764
|
-
}),
|
|
1765
|
-
)
|
|
1766
|
-
.pipe(
|
|
1767
|
-
Effect.provideService(SqlClient.SqlClient, sql),
|
|
1768
|
-
Effect.catchTag("SqlError", (error) =>
|
|
1769
|
-
Effect.fail(storageError("export transaction")(error)),
|
|
1770
|
-
),
|
|
1771
|
-
);
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (
|
|
1059
|
+
const saveCheckpoint = Effect.fnUntraced(function* (
|
|
1775
1060
|
checkpoint: RawCheckpoint,
|
|
1776
1061
|
): Effect.fn.Return<void, CheckpointError> {
|
|
1777
1062
|
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) {
|
|
@@ -1794,17 +1079,24 @@ const makeJournal = (
|
|
|
1794
1079
|
|
|
1795
1080
|
if (checkpoint.throughSequence > thread.tail_sequence) {
|
|
1796
1081
|
return yield* DoCheckpointConflict.make({
|
|
1082
|
+
reason: "ahead-of-tail",
|
|
1797
1083
|
message:
|
|
1798
1084
|
`Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ` +
|
|
1799
1085
|
`${thread.tail_sequence}.`,
|
|
1800
1086
|
});
|
|
1801
1087
|
}
|
|
1802
1088
|
|
|
1089
|
+
if (!(yield* isBatchBoundary(checkpoint.threadId, checkpoint.throughSequence))) {
|
|
1090
|
+
return yield* DoCheckpointConflict.make({
|
|
1091
|
+
reason: "not-batch-boundary",
|
|
1092
|
+
message: "Checkpoint sequence is not the end of a complete canonical batch.",
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1803
1096
|
const checkpointRows = yield* sql<Record<string, unknown>>`
|
|
1804
1097
|
SELECT
|
|
1805
1098
|
thread_id,
|
|
1806
1099
|
through_sequence,
|
|
1807
|
-
tail_digest,
|
|
1808
1100
|
checkpoint_json
|
|
1809
1101
|
FROM effect_agent_checkpoints
|
|
1810
1102
|
WHERE thread_id = ${checkpoint.threadId}
|
|
@@ -1826,11 +1118,9 @@ const makeJournal = (
|
|
|
1826
1118
|
});
|
|
1827
1119
|
}
|
|
1828
1120
|
if (existing.length === 1) {
|
|
1829
|
-
if (
|
|
1830
|
-
existing[0].tail_digest !== checkpoint.tailDigest ||
|
|
1831
|
-
existing[0].checkpoint_json !== checkpoint.checkpointJson
|
|
1832
|
-
) {
|
|
1121
|
+
if (existing[0].checkpoint_json !== checkpoint.checkpointJson) {
|
|
1833
1122
|
return yield* DoCheckpointConflict.make({
|
|
1123
|
+
reason: "corrupt",
|
|
1834
1124
|
message: "A different checkpoint already exists at this canonical sequence.",
|
|
1835
1125
|
});
|
|
1836
1126
|
}
|
|
@@ -1842,12 +1132,10 @@ const makeJournal = (
|
|
|
1842
1132
|
INSERT INTO effect_agent_checkpoints (
|
|
1843
1133
|
thread_id,
|
|
1844
1134
|
through_sequence,
|
|
1845
|
-
tail_digest,
|
|
1846
1135
|
checkpoint_json
|
|
1847
1136
|
) VALUES (
|
|
1848
1137
|
${checkpoint.threadId},
|
|
1849
1138
|
${checkpoint.throughSequence},
|
|
1850
|
-
${checkpoint.tailDigest},
|
|
1851
1139
|
${checkpoint.checkpointJson}
|
|
1852
1140
|
)
|
|
1853
1141
|
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
@@ -1855,84 +1143,7 @@ const makeJournal = (
|
|
|
1855
1143
|
);
|
|
1856
1144
|
});
|
|
1857
1145
|
|
|
1858
|
-
const
|
|
1859
|
-
request: SaveRecoveryCheckpointRequest,
|
|
1860
|
-
checkpointJson: string,
|
|
1861
|
-
) {
|
|
1862
|
-
const { checkpoint } = request;
|
|
1863
|
-
|
|
1864
|
-
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) {
|
|
1865
|
-
return yield* DoStorageError.make({
|
|
1866
|
-
operation: "save recovery checkpoint",
|
|
1867
|
-
message: "Checkpoint identity exceeds the Durable Object storage bounds.",
|
|
1868
|
-
});
|
|
1869
|
-
}
|
|
1870
|
-
yield* checkValueBound("save recovery checkpoint", checkpointJson);
|
|
1871
|
-
// Keep injected waits outside the storage-backed transaction callback.
|
|
1872
|
-
yield* failpoint("save-recovery-checkpoint:before");
|
|
1873
|
-
yield* withWriteTransaction("recovery checkpoint transaction")(
|
|
1874
|
-
Effect.gen(function* () {
|
|
1875
|
-
const threads = yield* getThread(checkpoint.threadId);
|
|
1876
|
-
const thread = threads[0];
|
|
1877
|
-
|
|
1878
|
-
if (thread === undefined)
|
|
1879
|
-
return yield* ThreadNotMaterialized.make({ threadId: checkpoint.threadId });
|
|
1880
|
-
if (request.producerEpoch !== thread.producer_epoch)
|
|
1881
|
-
return yield* FenceRejected.make({
|
|
1882
|
-
threadId: checkpoint.threadId,
|
|
1883
|
-
actualEpoch: thread.producer_epoch,
|
|
1884
|
-
attemptedEpoch: request.producerEpoch,
|
|
1885
|
-
});
|
|
1886
|
-
if (checkpoint.throughSequence > thread.tail_sequence)
|
|
1887
|
-
return yield* CheckpointRejected.make({
|
|
1888
|
-
threadId: checkpoint.threadId,
|
|
1889
|
-
reason: "ahead-of-tail",
|
|
1890
|
-
});
|
|
1891
|
-
|
|
1892
|
-
const digests =
|
|
1893
|
-
checkpoint.throughSequence === 0
|
|
1894
|
-
? [EMPTY_TAIL_DIGEST]
|
|
1895
|
-
: yield* getTailDigestAt(checkpoint.threadId, checkpoint.throughSequence);
|
|
1896
|
-
|
|
1897
|
-
if (digests.length !== 1 || digests[0] !== checkpoint.tailDigest)
|
|
1898
|
-
return yield* CheckpointRejected.make({
|
|
1899
|
-
threadId: checkpoint.threadId,
|
|
1900
|
-
reason: "digest-mismatch",
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
yield* sql`
|
|
1904
|
-
INSERT INTO effect_agent_recovery_checkpoints (thread_id, through_sequence, tail_digest, checkpoint_json)
|
|
1905
|
-
VALUES (${checkpoint.threadId}, ${checkpoint.throughSequence}, ${checkpoint.tailDigest}, ${checkpointJson})
|
|
1906
|
-
ON CONFLICT (thread_id) DO UPDATE SET
|
|
1907
|
-
through_sequence = excluded.through_sequence,
|
|
1908
|
-
tail_digest = excluded.tail_digest,
|
|
1909
|
-
checkpoint_json = excluded.checkpoint_json
|
|
1910
|
-
WHERE excluded.through_sequence >= effect_agent_recovery_checkpoints.through_sequence
|
|
1911
|
-
RETURNING *
|
|
1912
|
-
`.pipe(
|
|
1913
|
-
recovery.write,
|
|
1914
|
-
Effect.mapError((error) =>
|
|
1915
|
-
error._tag === "SqlError" ? storageError("save recovery checkpoint")(error) : error,
|
|
1916
|
-
),
|
|
1917
|
-
);
|
|
1918
|
-
}),
|
|
1919
|
-
);
|
|
1920
|
-
yield* failpoint("save-recovery-checkpoint:after");
|
|
1921
|
-
});
|
|
1922
|
-
|
|
1923
|
-
const loadRecoveryCheckpoint = Effect.fn("DoJournal.loadRecoveryCheckpoint")(function* (
|
|
1924
|
-
threadId: string,
|
|
1925
|
-
) {
|
|
1926
|
-
return yield* recovery
|
|
1927
|
-
.by("thread_id", threadId)
|
|
1928
|
-
.pipe(
|
|
1929
|
-
Effect.mapError((error) =>
|
|
1930
|
-
error._tag === "SqlError" ? storageError("load recovery checkpoint")(error) : error,
|
|
1931
|
-
),
|
|
1932
|
-
);
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (
|
|
1146
|
+
const loadCheckpoint = Effect.fnUntraced(function* (
|
|
1936
1147
|
threadId: string,
|
|
1937
1148
|
atOrBeforeSequence: CanonicalSequence,
|
|
1938
1149
|
) {
|
|
@@ -1940,7 +1151,6 @@ const makeJournal = (
|
|
|
1940
1151
|
SELECT
|
|
1941
1152
|
thread_id,
|
|
1942
1153
|
through_sequence,
|
|
1943
|
-
tail_digest,
|
|
1944
1154
|
checkpoint_json
|
|
1945
1155
|
FROM effect_agent_checkpoints
|
|
1946
1156
|
WHERE thread_id = ${threadId}
|
|
@@ -1957,136 +1167,30 @@ const makeJournal = (
|
|
|
1957
1167
|
);
|
|
1958
1168
|
});
|
|
1959
1169
|
|
|
1960
|
-
const
|
|
1170
|
+
const isBatchBoundary = Effect.fnUntraced(function* (
|
|
1961
1171
|
threadId: string,
|
|
1962
1172
|
sequence: CanonicalSequence,
|
|
1963
1173
|
) {
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
if (thread !== undefined && sequence === thread.tail_sequence) return [thread.tail_digest];
|
|
1967
|
-
if (sequence === 0) {
|
|
1968
|
-
const threads = yield* getThread(threadId);
|
|
1969
|
-
|
|
1970
|
-
return threads.length === 0
|
|
1971
|
-
? []
|
|
1972
|
-
: [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
|
|
1973
|
-
(value): value is string => value !== undefined,
|
|
1974
|
-
);
|
|
1975
|
-
}
|
|
1174
|
+
if (sequence === 0) return true;
|
|
1976
1175
|
|
|
1977
|
-
// Resolve both primary keys; last_sequence alone scans the Thread's archived batches.
|
|
1978
|
-
// Retain the batch-end predicate so an interior record cannot certify a checkpoint.
|
|
1979
1176
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1980
|
-
|
|
1981
|
-
thread_id
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
FROM effect_agent_canonical_batches
|
|
1989
|
-
WHERE thread_id = ${threadId}
|
|
1990
|
-
AND batch_id = (
|
|
1991
|
-
SELECT batch_id FROM effect_agent_canonical_records
|
|
1992
|
-
WHERE thread_id = ${threadId} AND sequence = ${sequence}
|
|
1993
|
-
)
|
|
1994
|
-
AND last_sequence = ${sequence}
|
|
1995
|
-
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
1177
|
+
SELECT last_sequence FROM effect_agent_canonical_batches
|
|
1178
|
+
WHERE thread_id = ${threadId}
|
|
1179
|
+
AND batch_id = (
|
|
1180
|
+
SELECT ${recordBatchId(sql)} FROM effect_agent_canonical_records
|
|
1181
|
+
WHERE thread_id = ${threadId} AND sequence = ${sequence}
|
|
1182
|
+
)
|
|
1183
|
+
AND last_sequence = ${sequence}
|
|
1184
|
+
`.pipe(Effect.mapError(storageError("read canonical batch boundary")));
|
|
1996
1185
|
|
|
1997
1186
|
const batches = yield* decodeRows(
|
|
1998
|
-
Schema.Array(
|
|
1187
|
+
Schema.Array(Schema.Struct({ last_sequence: CanonicalSequence })),
|
|
1999
1188
|
"effect_agent_canonical_batches",
|
|
2000
1189
|
`${threadId}/${sequence}`,
|
|
2001
1190
|
rows,
|
|
2002
1191
|
);
|
|
2003
1192
|
|
|
2004
|
-
return batches.
|
|
2005
|
-
});
|
|
2006
|
-
|
|
2007
|
-
const scanStoredPayloads = Effect.fn("DoJournal.scanStoredPayloads")(function* () {
|
|
2008
|
-
return yield* state
|
|
2009
|
-
.transaction(
|
|
2010
|
-
Effect.gen(function* () {
|
|
2011
|
-
const threads = yield* sql<Record<string, unknown>>`
|
|
2012
|
-
SELECT
|
|
2013
|
-
thread_id,
|
|
2014
|
-
created_at,
|
|
2015
|
-
tail_sequence,
|
|
2016
|
-
tail_digest,
|
|
2017
|
-
producer_epoch
|
|
2018
|
-
FROM effect_agent_threads
|
|
2019
|
-
ORDER BY thread_id
|
|
2020
|
-
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
2021
|
-
|
|
2022
|
-
const batches = yield* sql<Record<string, unknown>>`
|
|
2023
|
-
SELECT
|
|
2024
|
-
thread_id,
|
|
2025
|
-
batch_id,
|
|
2026
|
-
first_sequence,
|
|
2027
|
-
last_sequence,
|
|
2028
|
-
batch_digest,
|
|
2029
|
-
tail_digest,
|
|
2030
|
-
batch_json
|
|
2031
|
-
FROM effect_agent_canonical_batches
|
|
2032
|
-
ORDER BY thread_id, first_sequence
|
|
2033
|
-
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
2034
|
-
|
|
2035
|
-
const records = yield* sql<Record<string, unknown>>`
|
|
2036
|
-
SELECT
|
|
2037
|
-
thread_id,
|
|
2038
|
-
sequence,
|
|
2039
|
-
record_id,
|
|
2040
|
-
batch_id,
|
|
2041
|
-
record_json
|
|
2042
|
-
FROM effect_agent_canonical_records
|
|
2043
|
-
ORDER BY thread_id, sequence
|
|
2044
|
-
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
2045
|
-
|
|
2046
|
-
const checkpoints = yield* sql<Record<string, unknown>>`
|
|
2047
|
-
SELECT
|
|
2048
|
-
thread_id,
|
|
2049
|
-
through_sequence,
|
|
2050
|
-
tail_digest,
|
|
2051
|
-
checkpoint_json
|
|
2052
|
-
FROM effect_agent_checkpoints
|
|
2053
|
-
ORDER BY thread_id, through_sequence
|
|
2054
|
-
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
2055
|
-
|
|
2056
|
-
return {
|
|
2057
|
-
threads: yield* decodeRows(
|
|
2058
|
-
Schema.Array(ThreadRow),
|
|
2059
|
-
"effect_agent_threads",
|
|
2060
|
-
"startup_scan",
|
|
2061
|
-
threads,
|
|
2062
|
-
),
|
|
2063
|
-
batches: yield* decodeRows(
|
|
2064
|
-
Schema.Array(BatchRow),
|
|
2065
|
-
"effect_agent_canonical_batches",
|
|
2066
|
-
"startup_scan",
|
|
2067
|
-
batches,
|
|
2068
|
-
),
|
|
2069
|
-
records: yield* decodeRows(
|
|
2070
|
-
Schema.Array(RecordRow),
|
|
2071
|
-
"effect_agent_canonical_records",
|
|
2072
|
-
"startup_scan",
|
|
2073
|
-
records,
|
|
2074
|
-
),
|
|
2075
|
-
checkpoints: yield* decodeRows(
|
|
2076
|
-
Schema.Array(CheckpointRow),
|
|
2077
|
-
"effect_agent_checkpoints",
|
|
2078
|
-
"startup_scan",
|
|
2079
|
-
checkpoints,
|
|
2080
|
-
),
|
|
2081
|
-
};
|
|
2082
|
-
}),
|
|
2083
|
-
)
|
|
2084
|
-
.pipe(
|
|
2085
|
-
Effect.provideService(SqlClient.SqlClient, sql),
|
|
2086
|
-
Effect.catchTag("SqlError", (error) =>
|
|
2087
|
-
Effect.fail(storageError("startup scan transaction")(error)),
|
|
2088
|
-
),
|
|
2089
|
-
);
|
|
1193
|
+
return batches.length === 1;
|
|
2090
1194
|
});
|
|
2091
1195
|
|
|
2092
1196
|
const sourceFailure = (cause: unknown) =>
|
|
@@ -2095,8 +1199,8 @@ const makeJournal = (
|
|
|
2095
1199
|
const sourceThreads = Effect.fnUntraced(function* () {
|
|
2096
1200
|
const all = yield* threads.matching(
|
|
2097
1201
|
"lifecycle-owners",
|
|
2098
|
-
() =>
|
|
2099
|
-
sql`SELECT * FROM effect_agent_threads LIMIT 129`,
|
|
1202
|
+
(row) => row.created_at !== null,
|
|
1203
|
+
sql`SELECT * FROM effect_agent_threads WHERE created_at IS NOT NULL LIMIT 129`,
|
|
2100
1204
|
128,
|
|
2101
1205
|
);
|
|
2102
1206
|
|
|
@@ -2105,7 +1209,7 @@ const makeJournal = (
|
|
|
2105
1209
|
// Large custom hosts hydrate only owners whose journal has an unmaterialized suffix.
|
|
2106
1210
|
return yield* Schema.decodeUnknownEffect(Schema.Array(ThreadRow))(
|
|
2107
1211
|
yield* sql`SELECT t.* FROM effect_agent_threads t LEFT JOIN effect_agent_lifecycle_cursors c ON c.thread_id=t.thread_id
|
|
2108
|
-
WHERE c.thread_id IS NULL OR t.tail_sequence > c.through_sequence ORDER BY t.thread_id LIMIT 128`,
|
|
1212
|
+
WHERE t.created_at IS NOT NULL AND (c.thread_id IS NULL OR t.tail_sequence > c.through_sequence) ORDER BY t.thread_id LIMIT 128`,
|
|
2109
1213
|
);
|
|
2110
1214
|
});
|
|
2111
1215
|
|
|
@@ -2127,7 +1231,7 @@ const makeJournal = (
|
|
|
2127
1231
|
// New Threads created with publication disabled have no cursor. Never advance
|
|
2128
1232
|
// existing cursors: they can owe intent from an interrupted Attempt.
|
|
2129
1233
|
yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence)
|
|
2130
|
-
SELECT thread_id, tail_sequence FROM effect_agent_threads WHERE
|
|
1234
|
+
SELECT thread_id, tail_sequence FROM effect_agent_threads WHERE created_at IS NOT NULL
|
|
2131
1235
|
ON CONFLICT(thread_id) DO NOTHING`;
|
|
2132
1236
|
yield* sourceThreads();
|
|
2133
1237
|
}),
|
|
@@ -2253,6 +1357,9 @@ const makeJournal = (
|
|
|
2253
1357
|
};
|
|
2254
1358
|
|
|
2255
1359
|
return {
|
|
1360
|
+
archives: archives.storage,
|
|
1361
|
+
archiveRanges: archives,
|
|
1362
|
+
work,
|
|
2256
1363
|
state,
|
|
2257
1364
|
owner,
|
|
2258
1365
|
threads,
|
|
@@ -2261,18 +1368,19 @@ const makeJournal = (
|
|
|
2261
1368
|
flushCanonical,
|
|
2262
1369
|
flushPublications,
|
|
2263
1370
|
append,
|
|
1371
|
+
prepareAppend,
|
|
1372
|
+
appendPrepared,
|
|
1373
|
+
checkCommitIntent: (request: RawAppendRequest) =>
|
|
1374
|
+
executionConflict(executions.checkCommitIntent(request)),
|
|
1375
|
+
readExecution: executions.read,
|
|
2264
1376
|
checkValueBound,
|
|
2265
|
-
exportThread,
|
|
2266
1377
|
getThread,
|
|
2267
1378
|
hasRecord,
|
|
2268
|
-
|
|
1379
|
+
isBatchBoundary,
|
|
2269
1380
|
loadCheckpoint,
|
|
2270
|
-
loadRecoveryCheckpoint,
|
|
2271
|
-
saveRecoveryCheckpoint,
|
|
2272
1381
|
materialize,
|
|
2273
1382
|
read: (request: RawReadRequest) => state.read(read(request)),
|
|
2274
1383
|
saveCheckpoint,
|
|
2275
|
-
scanStoredPayloads,
|
|
2276
1384
|
withWriteTransaction,
|
|
2277
1385
|
} as const;
|
|
2278
1386
|
});
|