@yielded/agent-storage-cloudflare 0.1.0-beta.166 → 0.1.0-beta.168
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
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import { DoAppendConflict, DoCheckpointConflict, DoStorageCorruptionError, DoStorageError, DoValueBoundExceeded } from "./DoStorageError.mjs";
|
|
2
|
+
import { n as ensureDoStorageLayout } from "./migrations-D_EP3KYf.mjs";
|
|
3
|
+
import { n as ownedRows, r as ownedState } from "./owned-state-BHzUi0eb.mjs";
|
|
4
|
+
import { SqlLifecycleRetainer, SqlLifecycleSource, makeSqlLifecyclePublication } from "@yielded/agent-storage-sql/sql-lifecycle-publication";
|
|
5
|
+
import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
|
|
6
|
+
import { Cause, Clock, Effect, Option, Predicate, Result, Schema, Stream } from "effect";
|
|
7
|
+
import * as SqlClient$1 from "effect/sql/SqlClient";
|
|
8
|
+
import { makeCanonicalStorage, recordBatchId } from "@yielded/agent-storage-sql/sql-canonical-storage";
|
|
9
|
+
import { makeSqlExecutions } from "@yielded/agent-storage-sql/sql-journal";
|
|
10
|
+
import { SqlStorageProgress } from "@yielded/agent-storage-sql/sql-storage-progress";
|
|
11
|
+
import { canonicalRecordJson, makeSqlThreadArchiveRange } from "@yielded/agent-storage-sql/sql-thread-archive-range";
|
|
12
|
+
import { makeProgressAppendValidation } from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
13
|
+
import { makeSqlThreadWork } from "@yielded/agent-storage-sql/sql-thread-work";
|
|
14
|
+
import { ThreadId } from "@yielded/agent/identifiers";
|
|
15
|
+
import { LifecyclePublicationError, LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
|
|
16
|
+
import { CanonicalRecord, CanonicalSequence } from "@yielded/agent/records";
|
|
17
|
+
import { ThreadStoreDiagnostic, ThreadStoreError } from "@yielded/agent/thread-store";
|
|
18
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
19
|
+
//#region src/internal/storage-span.ts
|
|
20
|
+
const isAppendContention = (error) => (error._tag === "DoAppendConflict" || error._tag === "AppendConflict") && error.reason === "tail";
|
|
21
|
+
/** Expected refusals remain typed at the port, but are outcomes of the storage operation. */
|
|
22
|
+
const storageResult = (effect, expected) => effect.pipe(Effect.map(Result.succeed), Effect.catchIf(expected, (error) => Effect.succeed(Result.fail(error))));
|
|
23
|
+
/** Only error discriminants enter telemetry; messages, SQL, identities and payloads do not. */
|
|
24
|
+
const annotateStorageError = (error) => Effect.annotateCurrentSpan({
|
|
25
|
+
"error.type": error._tag,
|
|
26
|
+
...Predicate.hasProperty(error, "cause") && Predicate.hasProperty(error.cause, "_tag") && typeof error.cause._tag === "string" ? { "error.cause.type": error.cause._tag } : {}
|
|
27
|
+
});
|
|
28
|
+
const withStorageSpan = (name, expected = () => false) => (effect) => storageResult(effect, expected).pipe(Effect.tap((result) => Result.isFailure(result) ? Effect.annotateCurrentSpan({ "storage.outcome": result.failure._tag }) : Effect.void), Effect.tapError(annotateStorageError), Effect.withSpan(name), Effect.flatMap(Effect.fromResult));
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/internal/do-journal.ts
|
|
31
|
+
/**
|
|
32
|
+
* Static schema ceiling for stored text columns. Writes are bounded in BYTES by the
|
|
33
|
+
* configured `maxStoredValueBytes` (always ≤ 2,000,000); UTF-8 byte length is never smaller
|
|
34
|
+
* than UTF-16 string length, so any value that passed the byte bound also passes this
|
|
35
|
+
* decode-side character ceiling.
|
|
36
|
+
*/
|
|
37
|
+
const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2e6));
|
|
38
|
+
const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
39
|
+
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
40
|
+
const MAX_READ_PAGE_JSON_BYTES = 4194304;
|
|
41
|
+
const MAX_RECORD_CACHE_JSON_BYTES = 8388608;
|
|
42
|
+
const MAX_RECORD_CACHE_ENTRIES = 4096;
|
|
43
|
+
/** Durable Object SQL storage allows at most 100 bound parameters per statement. */
|
|
44
|
+
const MAX_BOUND_PARAMETERS = 100;
|
|
45
|
+
const isSqlError$1 = Schema.is(SqlError);
|
|
46
|
+
const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
47
|
+
const chunked = (values, size) => {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
|
|
50
|
+
return chunks;
|
|
51
|
+
};
|
|
52
|
+
var ThreadRow = class extends Schema.Class("ThreadRow")({
|
|
53
|
+
thread_id: BoundedIdentifier,
|
|
54
|
+
created_at: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(128))),
|
|
55
|
+
tail_sequence: CanonicalSequence
|
|
56
|
+
}) {};
|
|
57
|
+
const threadRows = ownedRows(ThreadRow, "effect_agent_threads", (row) => row.thread_id, "thread_id");
|
|
58
|
+
var RecordRow = class extends Schema.Class("RecordRow")({
|
|
59
|
+
batch_id: Schema.fromJsonString(BoundedIdentifier),
|
|
60
|
+
thread_id: BoundedIdentifier,
|
|
61
|
+
record_id: Schema.fromJsonString(BoundedIdentifier),
|
|
62
|
+
record_json: BoundedStoredText,
|
|
63
|
+
sequence: CanonicalSequence
|
|
64
|
+
}) {};
|
|
65
|
+
const isolateRecords = /* @__PURE__ */ new Map();
|
|
66
|
+
let isolateRecordBytes = 0;
|
|
67
|
+
const releaseRecord = (entry) => {
|
|
68
|
+
if (!isolateRecords.delete(entry)) return;
|
|
69
|
+
isolateRecordBytes -= entry.bytes;
|
|
70
|
+
entry.evict();
|
|
71
|
+
};
|
|
72
|
+
const recordCaches = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
const makeRecordCache = () => {
|
|
74
|
+
const records = /* @__PURE__ */ new Map();
|
|
75
|
+
const key = (thread, sequence) => JSON.stringify([thread, sequence]);
|
|
76
|
+
return {
|
|
77
|
+
clear: () => {
|
|
78
|
+
for (const entry of records.values()) releaseRecord(entry);
|
|
79
|
+
},
|
|
80
|
+
get: (thread, sequence) => records.get(key(thread, sequence))?.row,
|
|
81
|
+
prefix: (thread, through) => {
|
|
82
|
+
const prefix = [];
|
|
83
|
+
for (let sequence = 1; sequence <= through; sequence++) {
|
|
84
|
+
const row = records.get(key(thread, sequence));
|
|
85
|
+
if (row === void 0) return void 0;
|
|
86
|
+
prefix.push(row.row);
|
|
87
|
+
}
|
|
88
|
+
return prefix;
|
|
89
|
+
},
|
|
90
|
+
put: (row, capturedBytes) => {
|
|
91
|
+
const id = key(row.thread_id, row.sequence);
|
|
92
|
+
const prior = records.get(id);
|
|
93
|
+
if (prior?.row === row) return;
|
|
94
|
+
const size = capturedBytes ?? storedTextBytes(row.record_json);
|
|
95
|
+
if (prior !== void 0) releaseRecord(prior);
|
|
96
|
+
const entry = {
|
|
97
|
+
row,
|
|
98
|
+
bytes: size,
|
|
99
|
+
evict: () => records.delete(id)
|
|
100
|
+
};
|
|
101
|
+
records.set(id, entry);
|
|
102
|
+
isolateRecords.set(entry, true);
|
|
103
|
+
isolateRecordBytes += size;
|
|
104
|
+
while (isolateRecordBytes > MAX_RECORD_CACHE_JSON_BYTES || isolateRecords.size > MAX_RECORD_CACHE_ENTRIES) {
|
|
105
|
+
const oldest = isolateRecords.keys().next().value;
|
|
106
|
+
if (oldest === void 0) break;
|
|
107
|
+
releaseRecord(oldest);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const ReadPlanRow = Schema.Struct({
|
|
113
|
+
sequence: CanonicalSequence,
|
|
114
|
+
record_json_bytes: Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_READ_PAGE_JSON_BYTES))
|
|
115
|
+
});
|
|
116
|
+
var CheckpointRow = class extends Schema.Class("CheckpointRow")({
|
|
117
|
+
checkpoint_json: BoundedStoredText,
|
|
118
|
+
thread_id: BoundedIdentifier,
|
|
119
|
+
through_sequence: CanonicalSequence
|
|
120
|
+
}) {};
|
|
121
|
+
const LifecycleCursorRow = Schema.Struct({
|
|
122
|
+
thread_id: BoundedIdentifier,
|
|
123
|
+
through_sequence: CanonicalSequence
|
|
124
|
+
});
|
|
125
|
+
const lifecycleCursorRows = ownedRows(LifecycleCursorRow, "effect_agent_lifecycle_cursors", (row) => row.thread_id, "thread_id");
|
|
126
|
+
const initializedLifecycleSources = /* @__PURE__ */ new WeakSet();
|
|
127
|
+
var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendResult")({
|
|
128
|
+
firstSequence: CanonicalSequence,
|
|
129
|
+
lastSequence: CanonicalSequence,
|
|
130
|
+
replayed: Schema.Boolean
|
|
131
|
+
}) {};
|
|
132
|
+
var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
|
|
133
|
+
threadId: BoundedIdentifier,
|
|
134
|
+
fromSequenceExclusive: CanonicalSequence,
|
|
135
|
+
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
136
|
+
}) {};
|
|
137
|
+
var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
|
|
138
|
+
checkpointJson: BoundedStoredText,
|
|
139
|
+
threadId: BoundedIdentifier,
|
|
140
|
+
throughSequence: CanonicalSequence
|
|
141
|
+
}) {};
|
|
142
|
+
const noFailpoint = () => Effect.void;
|
|
143
|
+
const storageError = (operation) => (error) => DoStorageError.make({
|
|
144
|
+
cause: error,
|
|
145
|
+
operation,
|
|
146
|
+
message: error.message,
|
|
147
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
148
|
+
causeTag: error._tag,
|
|
149
|
+
operation,
|
|
150
|
+
issueTag: error.reason._tag
|
|
151
|
+
})
|
|
152
|
+
});
|
|
153
|
+
/** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
154
|
+
const decodeRows = (schema, table, rowKey, rows, decoder = table) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
155
|
+
table,
|
|
156
|
+
rowKey,
|
|
157
|
+
message: "Stored rows do not satisfy the storage schema",
|
|
158
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
159
|
+
causeTag: error._tag,
|
|
160
|
+
operation: "decode storage rows",
|
|
161
|
+
decoder,
|
|
162
|
+
issueTag: error.issue._tag
|
|
163
|
+
})
|
|
164
|
+
})), Effect.tapError((error) => Effect.annotateCurrentSpan({
|
|
165
|
+
"storage.failure.operation": error.diagnostic?.operation,
|
|
166
|
+
"storage.failure.cause": error.diagnostic?.causeTag,
|
|
167
|
+
"storage.failure.decoder": decoder,
|
|
168
|
+
"storage.failure.issue": error.diagnostic?.issueTag
|
|
169
|
+
})));
|
|
170
|
+
/** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
|
|
171
|
+
const decodeSingleRow = (schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
|
|
172
|
+
table,
|
|
173
|
+
rowKey,
|
|
174
|
+
message: `Expected exactly one row but found ${decoded.length}.`
|
|
175
|
+
}))));
|
|
176
|
+
/** Open a validated layout and bind this Object's storage owner. */
|
|
177
|
+
const ensureCurrentStorage = Effect.fnUntraced(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
|
|
178
|
+
yield* ensureDoStorageLayout().pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
179
|
+
const state = yield* ownedState(sql);
|
|
180
|
+
const owner = (yield* SqlStorageOwner) ?? state;
|
|
181
|
+
let journal;
|
|
182
|
+
const lifecycle = yield* makeSqlLifecyclePublication(void 0, maxStoredValueBytes).pipe(Effect.provideService(SqlStorageOwner, owner), Effect.provideService(SqlLifecycleSource, {
|
|
183
|
+
beforeRetain: (threadId) => Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushCanonical(threadId)),
|
|
184
|
+
beforePending: Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushPublications())
|
|
185
|
+
}), Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
|
|
186
|
+
operation: "initialize lifecycle publication",
|
|
187
|
+
message: "Native publication storage unavailable",
|
|
188
|
+
cause
|
|
189
|
+
})));
|
|
190
|
+
journal = yield* makeJournal(sql, failpoint, maxStoredValueBytes, lifecycle, state).pipe(Effect.provideService(SqlStorageOwner, owner));
|
|
191
|
+
if (lifecycle !== void 0) yield* journal.initializeLifecycleSource();
|
|
192
|
+
return journal;
|
|
193
|
+
});
|
|
194
|
+
const makeJournal = (sql, failpoint, maxStoredValueBytes, lifecycle, state) => Effect.gen(function* () {
|
|
195
|
+
const owner = (yield* SqlStorageOwner) ?? state;
|
|
196
|
+
const progress = yield* SqlStorageProgress;
|
|
197
|
+
const work = yield* makeSqlThreadWork({
|
|
198
|
+
read: (body) => state.read(body),
|
|
199
|
+
write: (body) => owner.transaction(body.pipe(Effect.tap(() => state.invalidate))).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (cause) => ThreadStoreError.make({
|
|
200
|
+
operation: "rebuild Thread work",
|
|
201
|
+
message: "Thread work transaction failed",
|
|
202
|
+
cause
|
|
203
|
+
})))
|
|
204
|
+
}).pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
205
|
+
yield* Effect.acquireRelease(Effect.sync(() => state.invalidators.add(work.invalidate)), () => Effect.sync(() => state.invalidators.delete(work.invalidate)));
|
|
206
|
+
const workFailure = (cause) => DoStorageError.make({
|
|
207
|
+
operation: "publish Thread work",
|
|
208
|
+
message: "Canonical work metadata could not be published",
|
|
209
|
+
cause
|
|
210
|
+
});
|
|
211
|
+
const validateProgress = yield* makeProgressAppendValidation().pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
212
|
+
const executions = yield* makeSqlExecutions().pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
213
|
+
const executionConflict = (effect) => effect.pipe(Effect.catchTag("AppendConflict", (error) => Effect.fail(DoAppendConflict.make({
|
|
214
|
+
reason: error.reason,
|
|
215
|
+
message: "Active execution conflicts with the captured commit",
|
|
216
|
+
...error.actualTailSequence === void 0 ? {} : { actualTailSequence: error.actualTailSequence }
|
|
217
|
+
}))));
|
|
218
|
+
const threads = threadRows(state, sql);
|
|
219
|
+
const cursors = lifecycleCursorRows(state, sql);
|
|
220
|
+
let records = recordCaches.get(state);
|
|
221
|
+
if (records === void 0) {
|
|
222
|
+
records = makeRecordCache();
|
|
223
|
+
recordCaches.set(state, records);
|
|
224
|
+
state.invalidators.add(records.clear);
|
|
225
|
+
}
|
|
226
|
+
const recordCache = records;
|
|
227
|
+
const archives = yield* makeSqlThreadArchiveRange({
|
|
228
|
+
read: (body) => state.read(body),
|
|
229
|
+
write: (body) => owner.transaction(body.pipe(Effect.tap(() => state.invalidate))).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (cause) => ThreadStoreError.make({
|
|
230
|
+
operation: "archive write",
|
|
231
|
+
message: "Archive transaction failed.",
|
|
232
|
+
cause
|
|
233
|
+
})))
|
|
234
|
+
}).pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
235
|
+
const recordJson = canonicalRecordJson(sql);
|
|
236
|
+
const physical = yield* makeCanonicalStorage().pipe(Effect.provideService(SqlClient$1.SqlClient, sql));
|
|
237
|
+
/** Typed pre-write refusal for any single value over the configured byte bound. */
|
|
238
|
+
const checkValueBound = (operation, value, actualBytes = storedTextBytes(value)) => {
|
|
239
|
+
return actualBytes > maxStoredValueBytes ? Effect.fail(DoValueBoundExceeded.make({
|
|
240
|
+
actualBytes,
|
|
241
|
+
maxBytes: maxStoredValueBytes,
|
|
242
|
+
operation
|
|
243
|
+
})) : Effect.void;
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Runs one journal write transaction on the Durable Object storage-backed
|
|
247
|
+
* `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
|
|
248
|
+
* Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
|
|
249
|
+
* `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
|
|
250
|
+
* The shared local owner gate covers the transaction and acknowledgement.
|
|
251
|
+
*
|
|
252
|
+
* Journal write transactions are always top level: the Durable Object client rejects
|
|
253
|
+
* nested transactions, so new journal operations must not wrap this helper inside another
|
|
254
|
+
* transaction. Expected outcomes may be captured only when every such refusal precedes
|
|
255
|
+
* mutation; real faults still roll back. Successful preflight reads never authorize a write.
|
|
256
|
+
*/
|
|
257
|
+
const withWriteTransaction = (operation, expected = () => false) => (effect) => state.owner.withMutation(owner.transaction(storageResult(effect, expected))).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((error) => isSqlError$1(error) ? storageError(operation)(error) : error), Effect.tapError(annotateStorageError), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }), Effect.flatMap(Effect.fromResult));
|
|
258
|
+
const materialize = Effect.fnUntraced(function* (threadId, createdAt) {
|
|
259
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
|
|
260
|
+
operation: "materialize thread",
|
|
261
|
+
message: "Thread identity exceeds the Durable Object storage bounds."
|
|
262
|
+
});
|
|
263
|
+
if ((yield* getThread(threadId)).length === 1) return;
|
|
264
|
+
yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
|
|
265
|
+
const existingRows = yield* getThread(threadId);
|
|
266
|
+
const existing = yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
|
|
267
|
+
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
268
|
+
table: "effect_agent_threads",
|
|
269
|
+
rowKey: threadId,
|
|
270
|
+
message: "A thread primary key returned more than one row."
|
|
271
|
+
});
|
|
272
|
+
if (existing.length === 0) {
|
|
273
|
+
yield* sql`
|
|
274
|
+
INSERT INTO effect_agent_threads (
|
|
275
|
+
thread_id,
|
|
276
|
+
created_at,
|
|
277
|
+
tail_sequence
|
|
278
|
+
) VALUES (
|
|
279
|
+
${threadId},
|
|
280
|
+
${createdAt},
|
|
281
|
+
0
|
|
282
|
+
) ON CONFLICT(thread_id) DO UPDATE SET created_at=excluded.created_at
|
|
283
|
+
WHERE effect_agent_threads.created_at IS NULL
|
|
284
|
+
RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("materialize thread")(error) : error));
|
|
285
|
+
if (lifecycle !== void 0) yield* sql`INSERT INTO effect_agent_lifecycle_cursors (thread_id, through_sequence)
|
|
286
|
+
VALUES (${threadId}, 0) RETURNING *`.pipe(cursors.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("initialize lifecycle source")(error) : error));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}));
|
|
290
|
+
}, withStorageSpan("DoJournal.materialize"));
|
|
291
|
+
const getThread = Effect.fnUntraced(function* (threadId) {
|
|
292
|
+
return (yield* threads.by("thread_id", threadId).pipe(Effect.mapError((error) => error._tag === "SqlError" ? storageError("read thread")(error) : error))).filter((row) => row.thread_id === threadId && row.created_at !== null);
|
|
293
|
+
});
|
|
294
|
+
const hasRecord = Effect.fnUntraced(function* (threadId, recordId) {
|
|
295
|
+
const thread = (yield* getThread(threadId))[0];
|
|
296
|
+
const prefix = recordCache.prefix(threadId, thread?.tail_sequence ?? 0);
|
|
297
|
+
if (prefix !== void 0) return prefix.some((row) => row.record_id === recordId);
|
|
298
|
+
return (yield* sql`SELECT record_id FROM effect_agent_canonical_records
|
|
299
|
+
WHERE thread_id = ${threadId} AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(storageError("read canonical record identity")))).length > 0;
|
|
300
|
+
});
|
|
301
|
+
const prepareAppend = Effect.fnUntraced(function* (request) {
|
|
302
|
+
if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || request.executionWrites.some(({ update }) => update.checkpoint.progress.runId.length > MAX_IDENTIFIER_LENGTH) || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)) return yield* DoStorageError.make({
|
|
303
|
+
operation: "append canonical batch",
|
|
304
|
+
message: "Canonical identifiers exceed the Durable Object storage bounds."
|
|
305
|
+
});
|
|
306
|
+
yield* checkValueBound("append canonical batch", request.batchJson, request.batchBytes);
|
|
307
|
+
for (const record of request.records) if (record.recordBytes > maxStoredValueBytes) return yield* DoValueBoundExceeded.make({
|
|
308
|
+
actualBytes: record.recordBytes,
|
|
309
|
+
maxBytes: maxStoredValueBytes,
|
|
310
|
+
operation: "append canonical record"
|
|
311
|
+
});
|
|
312
|
+
for (const write of request.executionWrites) yield* checkValueBound("commit active execution", write.json, write.bytes);
|
|
313
|
+
const recordIds = request.records.map((record) => record.recordId);
|
|
314
|
+
if (new Set(recordIds).size !== recordIds.length) return yield* DoAppendConflict.make({
|
|
315
|
+
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
316
|
+
reason: "record-identity"
|
|
317
|
+
});
|
|
318
|
+
return request;
|
|
319
|
+
});
|
|
320
|
+
const appendPrepared = Effect.fnUntraced(function* (request) {
|
|
321
|
+
const recordIds = request.records.map((record) => record.recordId);
|
|
322
|
+
const threadRows = yield* getThread(request.threadId);
|
|
323
|
+
const thread = threadRows.length === 1 ? threadRows[0] : void 0;
|
|
324
|
+
if (thread === void 0) return yield* DoStorageCorruptionError.make({
|
|
325
|
+
table: "effect_agent_threads",
|
|
326
|
+
rowKey: request.threadId,
|
|
327
|
+
message: `Expected exactly one row but found ${threadRows.length}.`
|
|
328
|
+
});
|
|
329
|
+
const prefix = recordCache.prefix(request.threadId, thread.tail_sequence);
|
|
330
|
+
const replay = yield* executionConflict(executions.replay(request));
|
|
331
|
+
if (replay !== void 0) return RawAppendResult.make(replay);
|
|
332
|
+
const existing = prefix !== void 0 && !prefix.some((row) => row.batch_id === request.batchId) ? void 0 : yield* archives.replay(request.threadId, request.batchId);
|
|
333
|
+
if (existing !== void 0) {
|
|
334
|
+
if (existing.batch_json !== request.batchJson) return yield* DoAppendConflict.make({
|
|
335
|
+
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
336
|
+
reason: "batch-content"
|
|
337
|
+
});
|
|
338
|
+
return RawAppendResult.make({
|
|
339
|
+
firstSequence: existing.first_sequence,
|
|
340
|
+
lastSequence: existing.last_sequence,
|
|
341
|
+
replayed: true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
yield* executionConflict(executions.rejectSuperseded(request));
|
|
345
|
+
if (request.expectedTailSequence !== thread.tail_sequence) return yield* DoAppendConflict.make({
|
|
346
|
+
message: `Expected tail ${request.expectedTailSequence} but found ${thread.tail_sequence}.`,
|
|
347
|
+
reason: "tail",
|
|
348
|
+
actualTailSequence: thread.tail_sequence
|
|
349
|
+
});
|
|
350
|
+
const existingRecords = prefix === void 0 ? [] : prefix.filter((row) => recordIds.includes(row.record_id));
|
|
351
|
+
for (const chunk of prefix === void 0 ? chunked(recordIds, 90) : []) {
|
|
352
|
+
const existingRecordRows = yield* sql`
|
|
353
|
+
SELECT
|
|
354
|
+
thread_id,
|
|
355
|
+
sequence,
|
|
356
|
+
record_id,
|
|
357
|
+
${recordBatchId(sql)} AS batch_id,
|
|
358
|
+
${recordJson} AS record_json
|
|
359
|
+
FROM effect_agent_canonical_records
|
|
360
|
+
WHERE thread_id = ${request.threadId}
|
|
361
|
+
AND record_id IN ${sql.in(chunk.map((id) => JSON.stringify(id)))}
|
|
362
|
+
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
363
|
+
existingRecords.push(...yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
|
|
364
|
+
}
|
|
365
|
+
if (existingRecords.length > 0) return yield* DoAppendConflict.make({
|
|
366
|
+
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
367
|
+
reason: "record-identity"
|
|
368
|
+
});
|
|
369
|
+
const firstSequence = yield* Schema.decodeEffect(CanonicalSequence)(thread.tail_sequence + (request.records.length === 0 ? 0 : 1)).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
370
|
+
cause: error,
|
|
371
|
+
operation: "append canonical batch",
|
|
372
|
+
message: error.message
|
|
373
|
+
})));
|
|
374
|
+
const lastSequence = yield* Schema.decodeEffect(CanonicalSequence)(thread.tail_sequence + request.records.length).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
375
|
+
cause: error,
|
|
376
|
+
operation: "append canonical batch",
|
|
377
|
+
message: error.message
|
|
378
|
+
})));
|
|
379
|
+
yield* executionConflict(executions.validate(request));
|
|
380
|
+
yield* validateProgress(request);
|
|
381
|
+
const result = RawAppendResult.make({
|
|
382
|
+
firstSequence,
|
|
383
|
+
lastSequence,
|
|
384
|
+
replayed: false
|
|
385
|
+
});
|
|
386
|
+
if (request.records.length > 0) yield* sql`
|
|
387
|
+
INSERT INTO effect_agent_canonical_batches (
|
|
388
|
+
thread_id,
|
|
389
|
+
batch_id,
|
|
390
|
+
first_sequence,
|
|
391
|
+
last_sequence,
|
|
392
|
+
producer_id_json
|
|
393
|
+
) VALUES (
|
|
394
|
+
${request.threadId},
|
|
395
|
+
${JSON.stringify(request.batchId)},
|
|
396
|
+
${firstSequence},
|
|
397
|
+
${lastSequence},
|
|
398
|
+
${request.producerIdJson}
|
|
399
|
+
)
|
|
400
|
+
`.pipe(Effect.mapError(storageError("insert canonical batch")));
|
|
401
|
+
yield* failpoint("append:after-batch-insert");
|
|
402
|
+
const records = (yield* physical.records(request.threadId, firstSequence, request.records)).map(({ columns, memberships }, index) => ({
|
|
403
|
+
record: request.records[index],
|
|
404
|
+
canonical: request.records[index].canonical,
|
|
405
|
+
row: columns,
|
|
406
|
+
memberships
|
|
407
|
+
}));
|
|
408
|
+
for (const group of chunked(records, Math.floor(MAX_BOUND_PARAMETERS / 18))) {
|
|
409
|
+
yield* sql`INSERT INTO effect_agent_canonical_records ${sql.insert(group.map(({ row }) => row))}`.pipe(Effect.mapError(storageError("insert canonical records")));
|
|
410
|
+
const memberships = group.flatMap(({ memberships }) => memberships);
|
|
411
|
+
for (const members of chunked(memberships, Math.floor(MAX_BOUND_PARAMETERS / 3))) yield* sql`INSERT INTO effect_agent_record_runs ${sql.insert(members)}`.pipe(Effect.mapError(storageError("index canonical Run membership")));
|
|
412
|
+
for (const { row, record } of group) {
|
|
413
|
+
recordCache.put(RecordRow.make({
|
|
414
|
+
...row,
|
|
415
|
+
record_id: record.recordId,
|
|
416
|
+
batch_id: request.batchId,
|
|
417
|
+
record_json: record.recordJson,
|
|
418
|
+
sequence: Schema.decodeSync(CanonicalSequence)(row.sequence)
|
|
419
|
+
}), record.recordBytes);
|
|
420
|
+
yield* failpoint("append:after-record-insert");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (request.records.length > 0) {
|
|
424
|
+
const workFields = yield* work.apply(yield* Schema.decodeEffect(ThreadId)(request.threadId).pipe(Effect.mapError(workFailure)), firstSequence, request.records.map((record) => record.canonical)).pipe(Effect.mapError(workFailure));
|
|
425
|
+
yield* sql`UPDATE effect_agent_threads SET ${sql.update({
|
|
426
|
+
tail_sequence: lastSequence,
|
|
427
|
+
...workFields
|
|
428
|
+
})}
|
|
429
|
+
WHERE thread_id=${request.threadId} RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("advance thread tail")(error) : error));
|
|
430
|
+
}
|
|
431
|
+
yield* executionConflict(executions.commit(request, result));
|
|
432
|
+
if (request.executionWrites.length > 0) yield* failpoint("append:after-record-insert");
|
|
433
|
+
if (lifecycle !== void 0 && request.executionWrites.some(({ update }) => update.facts.some(({ payload }) => payload._tag === "RunStarted"))) yield* flushCanonical(request.threadId).pipe(Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }), Effect.mapError((cause) => DoStorageError.make({
|
|
434
|
+
operation: "retain lifecycle start prefix",
|
|
435
|
+
message: "Lifecycle start intent could not be retained",
|
|
436
|
+
cause
|
|
437
|
+
})));
|
|
438
|
+
for (const { update } of request.executionWrites) for (const fact of update.facts) if (lifecycle !== void 0 && Schema.is(Schema.toType(LifecyclePublicationFact))(fact.payload)) yield* lifecycle.retain({
|
|
439
|
+
id: JSON.stringify([
|
|
440
|
+
request.threadId,
|
|
441
|
+
"record",
|
|
442
|
+
fact.recordId
|
|
443
|
+
]),
|
|
444
|
+
ownerThreadId: request.threadId,
|
|
445
|
+
canonicalSequence: lastSequence,
|
|
446
|
+
createdAt: fact.createdAt,
|
|
447
|
+
fact: fact.payload
|
|
448
|
+
}).pipe(Effect.mapError((cause) => DoStorageError.make({
|
|
449
|
+
operation: "retain operational lifecycle",
|
|
450
|
+
message: cause.message,
|
|
451
|
+
cause
|
|
452
|
+
})));
|
|
453
|
+
yield* failpoint("append:after-tail-update");
|
|
454
|
+
if (lifecycle !== void 0 && records.some(({ canonical: { payload } }) => payload._tag === "RunStarted" || payload._tag === "SubagentStarted")) yield* flushCanonical(request.threadId).pipe(Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }), Effect.mapError((cause) => DoStorageError.make({
|
|
455
|
+
operation: "retain lifecycle start prefix",
|
|
456
|
+
message: "Lifecycle start intent could not be retained",
|
|
457
|
+
cause
|
|
458
|
+
})));
|
|
459
|
+
yield* progress.committed("canonical").pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, (error) => DoStorageError.make({
|
|
460
|
+
operation: "enroll canonical progress",
|
|
461
|
+
message: error.message,
|
|
462
|
+
cause: error
|
|
463
|
+
})))));
|
|
464
|
+
return result;
|
|
465
|
+
});
|
|
466
|
+
const append = Effect.fnUntraced(function* (request, observed) {
|
|
467
|
+
yield* prepareAppend(request);
|
|
468
|
+
if (request.records.length > 0 && request.expectedTailSequence !== observed.tail_sequence) {
|
|
469
|
+
if ((yield* sql`
|
|
470
|
+
SELECT batch_id FROM effect_agent_canonical_batches
|
|
471
|
+
WHERE thread_id = ${request.threadId} AND batch_id = ${JSON.stringify(request.batchId)}
|
|
472
|
+
UNION ALL SELECT last_commit_id FROM effect_agent_executions
|
|
473
|
+
WHERE thread_id = ${request.threadId} AND last_commit_id = ${request.batchId}
|
|
474
|
+
LIMIT 1
|
|
475
|
+
`.pipe(Effect.mapError(storageError("preflight append replay")))).length === 0) return yield* DoAppendConflict.make({
|
|
476
|
+
reason: "tail",
|
|
477
|
+
message: "The canonical tail has advanced.",
|
|
478
|
+
actualTailSequence: observed.tail_sequence
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return yield* withWriteTransaction("append transaction", isAppendContention)(appendPrepared(request));
|
|
482
|
+
}, withStorageSpan("DoJournal.append", isAppendContention));
|
|
483
|
+
const read = Effect.fnUntraced(function* (request) {
|
|
484
|
+
const thread = (yield* getThread(request.threadId))[0];
|
|
485
|
+
const through = Math.min(thread?.tail_sequence ?? 0, request.fromSequenceExclusive + request.limit);
|
|
486
|
+
const cached = [];
|
|
487
|
+
for (let sequence = request.fromSequenceExclusive + 1; sequence <= through; sequence++) {
|
|
488
|
+
const record = recordCache.get(request.threadId, sequence);
|
|
489
|
+
if (record === void 0) break;
|
|
490
|
+
cached.push(record);
|
|
491
|
+
}
|
|
492
|
+
if (cached.length === Math.max(0, through - request.fromSequenceExclusive)) return {
|
|
493
|
+
count: cached.length,
|
|
494
|
+
records: Stream.fromIterable(cached)
|
|
495
|
+
};
|
|
496
|
+
const planRows = yield* sql`
|
|
497
|
+
SELECT
|
|
498
|
+
sequence,
|
|
499
|
+
length(CAST(${recordJson} AS BLOB)) AS record_json_bytes
|
|
500
|
+
FROM effect_agent_canonical_records
|
|
501
|
+
WHERE thread_id = ${request.threadId}
|
|
502
|
+
AND sequence > ${request.fromSequenceExclusive}
|
|
503
|
+
ORDER BY sequence
|
|
504
|
+
LIMIT ${request.limit}
|
|
505
|
+
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
506
|
+
const plan = yield* decodeRows(Schema.Array(ReadPlanRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, planRows, "ReadPlanRow");
|
|
507
|
+
const mismatch = () => DoStorageCorruptionError.make({
|
|
508
|
+
table: "effect_agent_canonical_records",
|
|
509
|
+
rowKey: `${request.threadId}>${request.fromSequenceExclusive}`,
|
|
510
|
+
message: "Canonical read membership, sequence, or payload size changed from its read plan."
|
|
511
|
+
});
|
|
512
|
+
if (plan.length > request.limit) return yield* mismatch();
|
|
513
|
+
const pages = [];
|
|
514
|
+
let page;
|
|
515
|
+
let pageBytes = 0;
|
|
516
|
+
let previousSequence = request.fromSequenceExclusive;
|
|
517
|
+
for (const row of plan) {
|
|
518
|
+
if (row.sequence !== previousSequence + 1) return yield* mismatch();
|
|
519
|
+
previousSequence = row.sequence;
|
|
520
|
+
if (page === void 0 || pageBytes + row.record_json_bytes > MAX_READ_PAGE_JSON_BYTES) {
|
|
521
|
+
page = [row];
|
|
522
|
+
pages.push(page);
|
|
523
|
+
pageBytes = row.record_json_bytes;
|
|
524
|
+
} else {
|
|
525
|
+
page.push(row);
|
|
526
|
+
pageBytes += row.record_json_bytes;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const readPage = Effect.fnUntraced(function* (page) {
|
|
530
|
+
const rows = yield* sql`
|
|
531
|
+
SELECT thread_id, sequence, record_id, ${recordBatchId(sql)} AS batch_id, ${recordJson} AS record_json
|
|
532
|
+
FROM effect_agent_canonical_records
|
|
533
|
+
WHERE thread_id = ${request.threadId}
|
|
534
|
+
AND sequence >= ${page[0].sequence}
|
|
535
|
+
AND sequence <= ${page[page.length - 1].sequence}
|
|
536
|
+
ORDER BY sequence
|
|
537
|
+
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
538
|
+
const decoded = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/${page[0].sequence}`, rows, "RecordRow");
|
|
539
|
+
if (decoded.length !== page.length || decoded.some((row, index) => row.thread_id !== request.threadId || row.sequence !== page[index].sequence || storedTextBytes(row.record_json) !== page[index].record_json_bytes)) return yield* mismatch();
|
|
540
|
+
for (const row of decoded) recordCache.put(row);
|
|
541
|
+
return decoded;
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
count: plan.length,
|
|
545
|
+
records: Stream.fromIterable(pages).pipe(Stream.flatMap((page) => Stream.fromIterableEffect(state.read(readPage(page)))))
|
|
546
|
+
};
|
|
547
|
+
});
|
|
548
|
+
const saveCheckpoint = Effect.fnUntraced(function* (checkpoint) {
|
|
549
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
|
|
550
|
+
operation: "save checkpoint",
|
|
551
|
+
message: "Checkpoint identity exceeds the Durable Object storage bounds."
|
|
552
|
+
});
|
|
553
|
+
yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
|
|
554
|
+
yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
|
|
555
|
+
const threadRows = yield* getThread(checkpoint.threadId);
|
|
556
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
|
|
557
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({
|
|
558
|
+
reason: "ahead-of-tail",
|
|
559
|
+
message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.`
|
|
560
|
+
});
|
|
561
|
+
if (!(yield* isBatchBoundary(checkpoint.threadId, checkpoint.throughSequence))) return yield* DoCheckpointConflict.make({
|
|
562
|
+
reason: "not-batch-boundary",
|
|
563
|
+
message: "Checkpoint sequence is not the end of a complete canonical batch."
|
|
564
|
+
});
|
|
565
|
+
const checkpointRows = yield* sql`
|
|
566
|
+
SELECT
|
|
567
|
+
thread_id,
|
|
568
|
+
through_sequence,
|
|
569
|
+
checkpoint_json
|
|
570
|
+
FROM effect_agent_checkpoints
|
|
571
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
572
|
+
AND through_sequence = ${checkpoint.throughSequence}
|
|
573
|
+
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
574
|
+
const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
|
|
575
|
+
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
576
|
+
table: "effect_agent_checkpoints",
|
|
577
|
+
rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
578
|
+
message: "A checkpoint primary key returned more than one row."
|
|
579
|
+
});
|
|
580
|
+
if (existing.length === 1) {
|
|
581
|
+
if (existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* DoCheckpointConflict.make({
|
|
582
|
+
reason: "corrupt",
|
|
583
|
+
message: "A different checkpoint already exists at this canonical sequence."
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
yield* sql`
|
|
588
|
+
INSERT INTO effect_agent_checkpoints (
|
|
589
|
+
thread_id,
|
|
590
|
+
through_sequence,
|
|
591
|
+
checkpoint_json
|
|
592
|
+
) VALUES (
|
|
593
|
+
${checkpoint.threadId},
|
|
594
|
+
${checkpoint.throughSequence},
|
|
595
|
+
${checkpoint.checkpointJson}
|
|
596
|
+
)
|
|
597
|
+
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
598
|
+
}));
|
|
599
|
+
});
|
|
600
|
+
const loadCheckpoint = Effect.fnUntraced(function* (threadId, atOrBeforeSequence) {
|
|
601
|
+
const rows = yield* sql`
|
|
602
|
+
SELECT
|
|
603
|
+
thread_id,
|
|
604
|
+
through_sequence,
|
|
605
|
+
checkpoint_json
|
|
606
|
+
FROM effect_agent_checkpoints
|
|
607
|
+
WHERE thread_id = ${threadId}
|
|
608
|
+
AND through_sequence <= ${atOrBeforeSequence}
|
|
609
|
+
ORDER BY through_sequence DESC
|
|
610
|
+
LIMIT 1
|
|
611
|
+
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
612
|
+
return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
|
|
613
|
+
});
|
|
614
|
+
const isBatchBoundary = Effect.fnUntraced(function* (threadId, sequence) {
|
|
615
|
+
if (sequence === 0) return true;
|
|
616
|
+
const rows = yield* sql`
|
|
617
|
+
SELECT last_sequence FROM effect_agent_canonical_batches
|
|
618
|
+
WHERE thread_id = ${threadId}
|
|
619
|
+
AND batch_id = (
|
|
620
|
+
SELECT ${recordBatchId(sql)} FROM effect_agent_canonical_records
|
|
621
|
+
WHERE thread_id = ${threadId} AND sequence = ${sequence}
|
|
622
|
+
)
|
|
623
|
+
AND last_sequence = ${sequence}
|
|
624
|
+
`.pipe(Effect.mapError(storageError("read canonical batch boundary")));
|
|
625
|
+
return (yield* decodeRows(Schema.Array(Schema.Struct({ last_sequence: CanonicalSequence })), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).length === 1;
|
|
626
|
+
});
|
|
627
|
+
const sourceFailure = (cause) => LifecyclePublicationError.make({
|
|
628
|
+
reason: "unavailable",
|
|
629
|
+
cause
|
|
630
|
+
});
|
|
631
|
+
const sourceThreads = Effect.fnUntraced(function* () {
|
|
632
|
+
const all = yield* threads.matching("lifecycle-owners", (row) => row.created_at !== null, sql`SELECT * FROM effect_agent_threads WHERE created_at IS NOT NULL LIMIT 129`, 128);
|
|
633
|
+
if (all.length <= 128) return all;
|
|
634
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(ThreadRow))(yield* sql`SELECT t.* FROM effect_agent_threads t LEFT JOIN effect_agent_lifecycle_cursors c ON c.thread_id=t.thread_id
|
|
635
|
+
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`);
|
|
636
|
+
});
|
|
637
|
+
const initializeLifecycleSource = Effect.fnUntraced(function* () {
|
|
638
|
+
if (initializedLifecycleSources.has(state)) return;
|
|
639
|
+
yield* state.transaction(Effect.gen(function* () {
|
|
640
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_lifecycle_cursors'`).length === 0) yield* sql`CREATE TABLE effect_agent_lifecycle_cursors (
|
|
641
|
+
thread_id TEXT PRIMARY KEY, through_sequence INTEGER NOT NULL
|
|
642
|
+
)`;
|
|
643
|
+
yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence)
|
|
644
|
+
SELECT thread_id, tail_sequence FROM effect_agent_threads WHERE created_at IS NOT NULL
|
|
645
|
+
ON CONFLICT(thread_id) DO NOTHING`;
|
|
646
|
+
yield* sourceThreads();
|
|
647
|
+
})).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
|
|
648
|
+
operation: "initialize lifecycle source",
|
|
649
|
+
message: "Lifecycle source cursor unavailable",
|
|
650
|
+
cause
|
|
651
|
+
})));
|
|
652
|
+
initializedLifecycleSources.add(state);
|
|
653
|
+
});
|
|
654
|
+
/** The journal is durable intent; only the bounded publication wave materializes a write set. */
|
|
655
|
+
const flushCanonical = Effect.fnUntraced(function* (threadId, maxRecords = Infinity) {
|
|
656
|
+
const { retainMany } = yield* SqlLifecycleRetainer;
|
|
657
|
+
const thread = (yield* getThread(threadId))[0];
|
|
658
|
+
if (thread === void 0) return;
|
|
659
|
+
const cursor = (yield* cursors.by("thread_id", threadId))[0];
|
|
660
|
+
if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
661
|
+
let through = cursor.through_sequence;
|
|
662
|
+
const bound = Math.min(thread.tail_sequence, through + maxRecords);
|
|
663
|
+
while (through < bound) {
|
|
664
|
+
const page = yield* read(RawReadRequest.make({
|
|
665
|
+
threadId,
|
|
666
|
+
fromSequenceExclusive: through,
|
|
667
|
+
limit: Math.min(128, bound - through)
|
|
668
|
+
}));
|
|
669
|
+
const facts = [];
|
|
670
|
+
let bytes = 0;
|
|
671
|
+
yield* Stream.runForEach(page.records, (row) => Effect.gen(function* () {
|
|
672
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json);
|
|
673
|
+
if (record.recordId !== row.record_id || row.sequence !== through + 1) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
674
|
+
if (Schema.is(Schema.toType(LifecyclePublicationFact))(record.payload)) {
|
|
675
|
+
if (bytes + storedTextBytes(row.record_json) > MAX_READ_PAGE_JSON_BYTES && facts.length > 0) {
|
|
676
|
+
yield* retainMany(facts);
|
|
677
|
+
facts.length = 0;
|
|
678
|
+
bytes = 0;
|
|
679
|
+
}
|
|
680
|
+
facts.push({
|
|
681
|
+
id: JSON.stringify([
|
|
682
|
+
threadId,
|
|
683
|
+
"record",
|
|
684
|
+
record.recordId
|
|
685
|
+
]),
|
|
686
|
+
ownerThreadId: yield* Schema.decodeEffect(ThreadId)(threadId),
|
|
687
|
+
canonicalSequence: row.sequence,
|
|
688
|
+
createdAt: record.createdAt,
|
|
689
|
+
fact: record.payload
|
|
690
|
+
});
|
|
691
|
+
bytes += storedTextBytes(row.record_json);
|
|
692
|
+
}
|
|
693
|
+
through = row.sequence;
|
|
694
|
+
}));
|
|
695
|
+
if (page.count === 0) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
696
|
+
yield* retainMany(facts);
|
|
697
|
+
yield* sql`UPDATE effect_agent_lifecycle_cursors SET through_sequence=${through} WHERE thread_id=${threadId} RETURNING *`.pipe(cursors.write);
|
|
698
|
+
}
|
|
699
|
+
}, Effect.mapError(sourceFailure));
|
|
700
|
+
const flushPublications = Effect.fnUntraced(function* () {
|
|
701
|
+
let owners = 0;
|
|
702
|
+
for (const thread of yield* sourceThreads()) {
|
|
703
|
+
const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
|
|
704
|
+
if (cursor !== void 0 && cursor.through_sequence === thread.tail_sequence) continue;
|
|
705
|
+
yield* flushCanonical(thread.thread_id, 1024);
|
|
706
|
+
if (++owners === 4) break;
|
|
707
|
+
}
|
|
708
|
+
}, Effect.mapError(sourceFailure));
|
|
709
|
+
const sourcePending = Effect.gen(function* () {
|
|
710
|
+
for (const thread of yield* sourceThreads()) {
|
|
711
|
+
const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
|
|
712
|
+
if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
713
|
+
if (cursor.through_sequence < thread.tail_sequence) return true;
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}).pipe(Effect.mapError(sourceFailure));
|
|
717
|
+
const ownedLifecycle = lifecycle === void 0 ? void 0 : {
|
|
718
|
+
...lifecycle,
|
|
719
|
+
storage: {
|
|
720
|
+
...lifecycle.storage,
|
|
721
|
+
retainedPendingDeadline: lifecycle.storage.pendingDeadline,
|
|
722
|
+
pendingDeadline: state.read(Effect.gen(function* () {
|
|
723
|
+
const deadline = yield* lifecycle.storage.pendingDeadline;
|
|
724
|
+
if (!(yield* sourcePending)) return deadline;
|
|
725
|
+
const now = yield* Clock.currentTimeMillis;
|
|
726
|
+
return Option.some(Option.isSome(deadline) ? Math.min(now, deadline.value) : now);
|
|
727
|
+
}))
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
archives: archives.storage,
|
|
732
|
+
archiveRanges: archives,
|
|
733
|
+
work,
|
|
734
|
+
state,
|
|
735
|
+
owner,
|
|
736
|
+
threads,
|
|
737
|
+
lifecycle: ownedLifecycle,
|
|
738
|
+
initializeLifecycleSource,
|
|
739
|
+
flushCanonical,
|
|
740
|
+
flushPublications,
|
|
741
|
+
append,
|
|
742
|
+
prepareAppend,
|
|
743
|
+
appendPrepared,
|
|
744
|
+
checkCommitIntent: (request) => executionConflict(executions.checkCommitIntent(request)),
|
|
745
|
+
readExecution: executions.read,
|
|
746
|
+
checkValueBound,
|
|
747
|
+
getThread,
|
|
748
|
+
hasRecord,
|
|
749
|
+
isBatchBoundary,
|
|
750
|
+
loadCheckpoint,
|
|
751
|
+
materialize,
|
|
752
|
+
read: (request) => state.read(read(request)),
|
|
753
|
+
saveCheckpoint,
|
|
754
|
+
withWriteTransaction
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
const initializeDoJournal = ensureCurrentStorage;
|
|
758
|
+
//#endregion
|
|
759
|
+
export { isAppendContention as a, initializeDoJournal as i, RawReadRequest as n, withStorageSpan as o, decodeRows as r, RawCheckpoint as t };
|
|
760
|
+
|
|
761
|
+
//# sourceMappingURL=do-journal-Cawnsetj.mjs.map
|