@evomap/evolver-core 2.0.0-beta.9 → 2.0.1
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/algo/antiDistill.js +1 -167
- package/dist/algo/bans.js +1 -33
- package/dist/algo/candidateAssembly.d.ts +7 -2
- package/dist/algo/candidateAssembly.js +1 -205
- package/dist/algo/capabilityCandidates.js +1 -136
- package/dist/algo/confidence.js +1 -196
- package/dist/algo/conversationSniffer.js +1 -149
- package/dist/algo/cycleEngine.d.ts +23 -2
- package/dist/algo/cycleEngine.js +1 -425
- package/dist/algo/cycleFailureClassifier.js +1 -112
- package/dist/algo/epigenetics.js +1 -41
- package/dist/algo/evolutionEvent.js +1 -24
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +1 -55
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +1 -16
- package/dist/algo/geneIntake.js +1 -128
- package/dist/algo/genePromotion.js +1 -53
- package/dist/algo/geneSelection.d.ts +63 -5
- package/dist/algo/geneSelection.js +1 -246
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +1 -20
- package/dist/algo/memoryGraph.js +1 -86
- package/dist/algo/mutation.js +1 -22
- package/dist/algo/orchestrator.d.ts +8 -1
- package/dist/algo/orchestrator.js +1 -75
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +1 -0
- package/dist/algo/solidify.js +1 -63
- package/dist/algo/strategyPresets.js +1 -61
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +1 -0
- package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
- package/dist/assetstore/assetSidecarRecords.js +238 -0
- package/dist/assetstore/assetSidecarRecovery.js +2 -2
- package/dist/assetstore/assetStoreHealth.d.ts +6 -0
- package/dist/assetstore/assetStoreHealth.js +56 -14
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +27 -9
- package/dist/assetstore/assetSyncLedger.d.ts +86 -0
- package/dist/assetstore/assetSyncLedger.js +683 -4
- package/dist/assetstore/index.d.ts +1 -0
- package/dist/assetstore/index.js +1 -0
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +4 -2
- package/dist/assetstore/localJsonl.js +89 -9
- package/dist/assetstore/provenance.d.ts +80 -4
- package/dist/assetstore/provenance.js +313 -5
- package/dist/assetstore/provider.d.ts +58 -1
- package/dist/assetstore/provider.js +97 -6
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +1 -251
- package/dist/benchmark/antiGeneImpact.js +1 -34
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +1 -268
- package/dist/benchmark/benchmark.js +1 -26
- package/dist/benchmark/evolutionThesisSolver.js +1 -41
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +1 -6
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +1 -0
- package/dist/benchmark/thesis.js +1 -150
- package/dist/benchmark/triggerShift.js +1 -106
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
- package/dist/bootstrap/v1EnvCompat.js +280 -0
- package/dist/cycle/cycleTimeline.js +1 -74
- package/dist/cycle/index.js +1 -2
- package/dist/cycle/stateMachine.js +1 -25
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +1 -0
- package/dist/events/ingest.js +1 -0
- package/dist/events/paths.d.ts +3 -1
- package/dist/events/paths.js +4 -0
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +43 -2
- package/dist/exec/autoExec.js +76 -9
- package/dist/exec/autonomousCycle.d.ts +22 -4
- package/dist/exec/autonomousCycle.js +64 -13
- package/dist/exec/claudeBridge.d.ts +32 -7
- package/dist/exec/claudeBridge.js +281 -29
- package/dist/exec/prompt.js +5 -1
- package/dist/exec/runnerRegistry.d.ts +68 -26
- package/dist/exec/runnerRegistry.js +307 -72
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/agentDirectory.js +1 -104
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +1 -72
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +1 -110
- package/dist/hub/capability.d.ts +90 -3
- package/dist/hub/capability.js +1 -1
- package/dist/hub/conversationDistiller.js +1 -264
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -70
- package/dist/hub/hubReview.js +1 -106
- package/dist/hub/index.js +1 -11
- package/dist/hub/ingest.js +1 -15
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +1 -406
- package/dist/hub/reuseDecision.js +1 -127
- package/dist/hub/sanitize.js +1 -322
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +10 -2
- package/dist/mailbox/ipcServer.js +163 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- package/dist/material/boundary.js +1 -14
- package/dist/material/consumer.js +1 -55
- package/dist/material/emit.js +1 -52
- package/dist/material/factory.js +1 -25
- package/dist/material/index.js +1 -9
- package/dist/material/materialArchive.js +1 -466
- package/dist/material/materialStore.js +1 -69
- package/dist/material/sampling.js +1 -39
- package/dist/material/sources.js +1 -33
- package/dist/material/watermark.js +1 -76
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/personality/drift.js +1 -106
- package/dist/personality/events.js +1 -37
- package/dist/personality/evolveOps.js +1 -93
- package/dist/personality/index.js +1 -11
- package/dist/personality/mutate.js +1 -39
- package/dist/personality/pivot.js +1 -22
- package/dist/personality/prompt.js +1 -62
- package/dist/personality/riskGate.js +1 -80
- package/dist/personality/schema.d.ts +16 -16
- package/dist/personality/schema.js +1 -119
- package/dist/personality/select.js +1 -71
- package/dist/personality/stats.js +1 -82
- package/dist/personality/store.js +1 -92
- package/dist/schema/common.js +1 -17
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +1 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -5
- package/dist/schema/material.js +1 -50
- package/dist/schema/problem.js +1 -45
- package/dist/schema/proofOfWork.js +1 -9
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -19
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +1 -0
- package/dist/signals/cycleHistoryFromEvents.js +1 -96
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +1 -125
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +1 -57
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +1 -6
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +1 -158
- package/dist/signals/signalGate.js +1 -40
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +1 -0
- package/dist/signals/traceSignals.js +1 -123
- package/dist/strategy/constraintAblation.js +1 -3074
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +1 -0
- package/dist/strategy/experiment.js +1 -63
- package/dist/strategy/index.js +1 -3
- package/dist/strategy/strategyPoint.js +1 -23
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +209 -0
- package/dist/trace/learningTrace.js +276 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/sandboxRunner.d.ts +28 -0
- package/dist/verify/sandboxRunner.js +218 -19
- package/dist/verify/sandboxedValidation.d.ts +9 -0
- package/dist/verify/sandboxedValidation.js +113 -13
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- package/package.json +5 -2
package/dist/mailbox/store.js
CHANGED
|
@@ -1,72 +1,516 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readSync, rmSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
5
|
import { createRequire } from 'node:module';
|
|
4
6
|
import { ulid as makeUlid } from 'ulid';
|
|
5
|
-
import { createEnvelope } from './envelope.js';
|
|
7
|
+
import { createEnvelope, ENVELOPE_SCHEMA_VERSION, normalizePriority, sanitizePayload, specForType, } from './envelope.js';
|
|
6
8
|
const nodeRequire = createRequire(import.meta.url);
|
|
7
9
|
function isBunRuntime() {
|
|
8
10
|
return typeof process.versions === 'object' && typeof process.versions.bun === 'string';
|
|
9
11
|
}
|
|
10
|
-
function openSqliteDatabase(path) {
|
|
12
|
+
function openSqliteDatabase(path, options = {}) {
|
|
11
13
|
if (isBunRuntime()) {
|
|
12
14
|
const { Database } = nodeRequire('bun:sqlite');
|
|
13
|
-
return new Database(path);
|
|
15
|
+
return options.readOnly ? new Database(path, { readonly: true }) : new Database(path);
|
|
14
16
|
}
|
|
15
17
|
// node:sqlite is exposed only as `node:sqlite` (no bare `sqlite` alias). Load it through createRequire so
|
|
16
18
|
// bundlers do not statically strip the prefix and break Vitest/Vite or Bun standalone builds.
|
|
17
19
|
const { DatabaseSync } = nodeRequire('node:sqlite');
|
|
18
|
-
return new DatabaseSync(path);
|
|
20
|
+
return options.readOnly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
|
|
21
|
+
}
|
|
22
|
+
const MAILBOX_PREVIEW_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
23
|
+
const MAILBOX_PREVIEW_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
24
|
+
const MAILBOX_PREVIEW_COPY_BUFFER_BYTES = 64 * 1024;
|
|
25
|
+
function mailboxPreviewError(code) {
|
|
26
|
+
return new Error(`mailbox_preview_${code}`);
|
|
27
|
+
}
|
|
28
|
+
function mailboxPreviewNoFollowFlag() {
|
|
29
|
+
return constants['O_NOFOLLOW'] ?? 0;
|
|
30
|
+
}
|
|
31
|
+
function mailboxPreviewStat(path) {
|
|
32
|
+
let stat;
|
|
33
|
+
try {
|
|
34
|
+
stat = lstatSync(path, { bigint: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return null;
|
|
39
|
+
throw mailboxPreviewError('target_unsafe');
|
|
40
|
+
}
|
|
41
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1n) {
|
|
42
|
+
throw mailboxPreviewError('target_unsafe');
|
|
43
|
+
}
|
|
44
|
+
return stat;
|
|
45
|
+
}
|
|
46
|
+
function sameMailboxPreviewSnapshot(left, right) {
|
|
47
|
+
const hasStableFileId = left.dev !== 0n || left.ino !== 0n || right.dev !== 0n || right.ino !== 0n;
|
|
48
|
+
const sameIdentity = hasStableFileId
|
|
49
|
+
? left.dev === right.dev && left.ino === right.ino
|
|
50
|
+
: left.birthtimeNs === right.birthtimeNs && left.mode === right.mode;
|
|
51
|
+
return sameIdentity
|
|
52
|
+
&& left.mode === right.mode
|
|
53
|
+
&& left.size === right.size
|
|
54
|
+
&& left.mtimeNs === right.mtimeNs
|
|
55
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
56
|
+
}
|
|
57
|
+
function assertMailboxPreviewSize(stat) {
|
|
58
|
+
if (stat.size > BigInt(MAILBOX_PREVIEW_MAX_FILE_BYTES)
|
|
59
|
+
|| stat.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
60
|
+
throw mailboxPreviewError('target_too_large');
|
|
61
|
+
}
|
|
62
|
+
return Number(stat.size);
|
|
63
|
+
}
|
|
64
|
+
function writeMailboxPreviewChunk(fd, chunk, position) {
|
|
65
|
+
let offset = 0;
|
|
66
|
+
while (offset < chunk.length) {
|
|
67
|
+
const written = writeSync(fd, chunk, offset, chunk.length - offset, position + offset);
|
|
68
|
+
if (written <= 0)
|
|
69
|
+
throw mailboxPreviewError('snapshot_write_failed');
|
|
70
|
+
offset += written;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function copyStableMailboxPreviewFile(sourcePath, destinationPath, expected) {
|
|
74
|
+
const expectedBytes = assertMailboxPreviewSize(expected);
|
|
75
|
+
let sourceFd;
|
|
76
|
+
let destinationFd;
|
|
77
|
+
try {
|
|
78
|
+
sourceFd = openSync(sourcePath, constants.O_RDONLY | mailboxPreviewNoFollowFlag());
|
|
79
|
+
const opened = fstatSync(sourceFd, { bigint: true });
|
|
80
|
+
if (!opened.isFile() || !sameMailboxPreviewSnapshot(expected, opened)) {
|
|
81
|
+
throw mailboxPreviewError('target_changed');
|
|
82
|
+
}
|
|
83
|
+
destinationFd = openSync(destinationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | mailboxPreviewNoFollowFlag(), 0o600);
|
|
84
|
+
const digest = createHash('sha256');
|
|
85
|
+
const buffer = Buffer.allocUnsafe(MAILBOX_PREVIEW_COPY_BUFFER_BYTES);
|
|
86
|
+
let total = 0;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const bytesRead = readSync(sourceFd, buffer, 0, buffer.length, null);
|
|
89
|
+
if (bytesRead === 0)
|
|
90
|
+
break;
|
|
91
|
+
if (total > MAILBOX_PREVIEW_MAX_FILE_BYTES - bytesRead) {
|
|
92
|
+
throw mailboxPreviewError('target_too_large');
|
|
93
|
+
}
|
|
94
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
95
|
+
digest.update(chunk);
|
|
96
|
+
writeMailboxPreviewChunk(destinationFd, chunk, total);
|
|
97
|
+
total += bytesRead;
|
|
98
|
+
}
|
|
99
|
+
fsyncSync(destinationFd);
|
|
100
|
+
const afterDescriptor = fstatSync(sourceFd, { bigint: true });
|
|
101
|
+
const afterPath = mailboxPreviewStat(sourcePath);
|
|
102
|
+
if (total !== expectedBytes
|
|
103
|
+
|| !sameMailboxPreviewSnapshot(opened, afterDescriptor)
|
|
104
|
+
|| !afterPath
|
|
105
|
+
|| !sameMailboxPreviewSnapshot(afterDescriptor, afterPath)) {
|
|
106
|
+
throw mailboxPreviewError('target_changed');
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({ stat: afterDescriptor, digest: digest.digest('hex'), bytes: total });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error instanceof Error && error.message.startsWith('mailbox_preview_'))
|
|
112
|
+
throw error;
|
|
113
|
+
throw mailboxPreviewError('snapshot_failed');
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (sourceFd !== undefined)
|
|
117
|
+
closeSync(sourceFd);
|
|
118
|
+
if (destinationFd !== undefined)
|
|
119
|
+
closeSync(destinationFd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function hashStableMailboxPreviewFile(path, expected) {
|
|
123
|
+
const before = mailboxPreviewStat(path);
|
|
124
|
+
if (!before || !sameMailboxPreviewSnapshot(expected.stat, before)) {
|
|
125
|
+
throw mailboxPreviewError('target_changed');
|
|
126
|
+
}
|
|
127
|
+
assertMailboxPreviewSize(before);
|
|
128
|
+
let fd;
|
|
129
|
+
try {
|
|
130
|
+
fd = openSync(path, constants.O_RDONLY | mailboxPreviewNoFollowFlag());
|
|
131
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
132
|
+
if (!opened.isFile() || !sameMailboxPreviewSnapshot(before, opened)) {
|
|
133
|
+
throw mailboxPreviewError('target_changed');
|
|
134
|
+
}
|
|
135
|
+
const digest = createHash('sha256');
|
|
136
|
+
const buffer = Buffer.allocUnsafe(MAILBOX_PREVIEW_COPY_BUFFER_BYTES);
|
|
137
|
+
let total = 0;
|
|
138
|
+
for (;;) {
|
|
139
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
|
140
|
+
if (bytesRead === 0)
|
|
141
|
+
break;
|
|
142
|
+
if (total > MAILBOX_PREVIEW_MAX_FILE_BYTES - bytesRead) {
|
|
143
|
+
throw mailboxPreviewError('target_too_large');
|
|
144
|
+
}
|
|
145
|
+
digest.update(buffer.subarray(0, bytesRead));
|
|
146
|
+
total += bytesRead;
|
|
147
|
+
}
|
|
148
|
+
const afterDescriptor = fstatSync(fd, { bigint: true });
|
|
149
|
+
const afterPath = mailboxPreviewStat(path);
|
|
150
|
+
if (total !== expected.bytes
|
|
151
|
+
|| digest.digest('hex') !== expected.digest
|
|
152
|
+
|| !sameMailboxPreviewSnapshot(opened, afterDescriptor)
|
|
153
|
+
|| !afterPath
|
|
154
|
+
|| !sameMailboxPreviewSnapshot(afterDescriptor, afterPath)) {
|
|
155
|
+
throw mailboxPreviewError('target_changed');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof Error && error.message.startsWith('mailbox_preview_'))
|
|
160
|
+
throw error;
|
|
161
|
+
throw mailboxPreviewError('target_changed');
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
if (fd !== undefined)
|
|
165
|
+
closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function assertMailboxRollbackJournalAbsent(mailboxPath) {
|
|
169
|
+
try {
|
|
170
|
+
lstatSync(`${mailboxPath}-journal`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === 'ENOENT')
|
|
174
|
+
return;
|
|
175
|
+
throw mailboxPreviewError('target_unsafe');
|
|
176
|
+
}
|
|
177
|
+
throw mailboxPreviewError('hot_rollback_journal');
|
|
178
|
+
}
|
|
179
|
+
function assertMailboxPreviewTargetAbsent(mailboxPath) {
|
|
180
|
+
for (const suffix of ['-wal', '-shm', '-journal']) {
|
|
181
|
+
try {
|
|
182
|
+
lstatSync(`${mailboxPath}${suffix}`);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error.code === 'ENOENT')
|
|
186
|
+
continue;
|
|
187
|
+
throw mailboxPreviewError('target_unsafe');
|
|
188
|
+
}
|
|
189
|
+
throw mailboxPreviewError('target_unsafe');
|
|
190
|
+
}
|
|
191
|
+
if (mailboxPreviewStat(mailboxPath))
|
|
192
|
+
throw mailboxPreviewError('target_changed');
|
|
193
|
+
}
|
|
194
|
+
function stableMailboxPreviewCopy(mailboxPath) {
|
|
195
|
+
const databaseStat = mailboxPreviewStat(mailboxPath);
|
|
196
|
+
if (!databaseStat)
|
|
197
|
+
throw mailboxPreviewError('target_changed');
|
|
198
|
+
assertMailboxRollbackJournalAbsent(mailboxPath);
|
|
199
|
+
const walPath = `${mailboxPath}-wal`;
|
|
200
|
+
const walStat = mailboxPreviewStat(walPath);
|
|
201
|
+
const totalBytes = databaseStat.size + (walStat?.size ?? 0n);
|
|
202
|
+
if (totalBytes > BigInt(MAILBOX_PREVIEW_MAX_TOTAL_BYTES)) {
|
|
203
|
+
throw mailboxPreviewError('target_too_large');
|
|
204
|
+
}
|
|
205
|
+
const snapshotDir = mkdtempSync(join(tmpdir(), 'evolver-mailbox-preview-'));
|
|
206
|
+
const snapshotPath = join(snapshotDir, basename(mailboxPath));
|
|
207
|
+
try {
|
|
208
|
+
chmodSync(snapshotDir, 0o700);
|
|
209
|
+
const databaseSnapshot = copyStableMailboxPreviewFile(mailboxPath, snapshotPath, databaseStat);
|
|
210
|
+
const walSnapshot = walStat
|
|
211
|
+
? copyStableMailboxPreviewFile(walPath, `${snapshotPath}-wal`, walStat)
|
|
212
|
+
: null;
|
|
213
|
+
assertMailboxRollbackJournalAbsent(mailboxPath);
|
|
214
|
+
const currentWal = mailboxPreviewStat(walPath);
|
|
215
|
+
if ((walSnapshot === null) !== (currentWal === null)) {
|
|
216
|
+
throw mailboxPreviewError('target_changed');
|
|
217
|
+
}
|
|
218
|
+
hashStableMailboxPreviewFile(mailboxPath, databaseSnapshot);
|
|
219
|
+
if (walSnapshot)
|
|
220
|
+
hashStableMailboxPreviewFile(walPath, walSnapshot);
|
|
221
|
+
return { dir: snapshotDir, path: snapshotPath };
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
try {
|
|
225
|
+
rmSync(snapshotDir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
catch { /* preserve primary error */ }
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
19
230
|
}
|
|
20
231
|
export const MAX_ATTEMPTS = 5;
|
|
21
232
|
export const DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
233
|
+
export const DEFAULT_IMPORT_JSONL_MAX_BYTES = 128 * 1024 * 1024;
|
|
234
|
+
export const DEFAULT_IMPORT_JSONL_MAX_RECORDS = 100_000;
|
|
22
235
|
const JSONL_READ_CHUNK_BYTES = 64 * 1024;
|
|
236
|
+
export const MAILBOX_CLAIM_OWNER = Symbol('mailboxClaimOwner');
|
|
237
|
+
export class MailboxJsonlImportError extends Error {
|
|
238
|
+
code;
|
|
239
|
+
line;
|
|
240
|
+
constructor(code, line, message) {
|
|
241
|
+
super(`Mailbox JSONL line ${line}: ${message}`);
|
|
242
|
+
this.name = 'MailboxJsonlImportError';
|
|
243
|
+
this.code = code;
|
|
244
|
+
this.line = line;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function mailboxClaimOwner(envelope) {
|
|
248
|
+
return envelope[MAILBOX_CLAIM_OWNER];
|
|
249
|
+
}
|
|
23
250
|
export function expBackoffMs(attempt) {
|
|
24
251
|
return Math.min(4000 * 2 ** (attempt - 1), 5 * 60 * 1000); // 4s→8s→…→5min
|
|
25
252
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
253
|
+
function asRow(value) {
|
|
254
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
/** Read one mailbox state value without creating, migrating, or taking write ownership of the database. */
|
|
257
|
+
export function readMailboxState(path, key) {
|
|
258
|
+
const database = openSqliteDatabase(path, { readOnly: true });
|
|
259
|
+
try {
|
|
260
|
+
const row = database.prepare('SELECT v FROM kv WHERE k = ?').get(key);
|
|
261
|
+
return typeof row?.['v'] === 'string' ? row['v'] : undefined;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
database.close();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function nonEmptyString(value) {
|
|
268
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
269
|
+
}
|
|
270
|
+
function finiteNumber(value) {
|
|
271
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
272
|
+
}
|
|
273
|
+
function nullableTimestamp(value) {
|
|
274
|
+
if (value === null)
|
|
275
|
+
return null;
|
|
276
|
+
return finiteNumber(value);
|
|
277
|
+
}
|
|
278
|
+
function timestampField(record, ...keys) {
|
|
279
|
+
for (const key of keys) {
|
|
280
|
+
if (Object.prototype.hasOwnProperty.call(record, key))
|
|
281
|
+
return nullableTimestamp(record[key]);
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
function nullableStringField(record, ...keys) {
|
|
286
|
+
for (const key of keys) {
|
|
287
|
+
if (!Object.prototype.hasOwnProperty.call(record, key))
|
|
288
|
+
continue;
|
|
289
|
+
const value = record[key];
|
|
290
|
+
if (value === null)
|
|
291
|
+
return null;
|
|
292
|
+
if (typeof value === 'string')
|
|
293
|
+
return value;
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
function importedStatus(value) {
|
|
299
|
+
switch (value) {
|
|
300
|
+
case 'pending':
|
|
301
|
+
case 'in_flight':
|
|
302
|
+
case 'done':
|
|
303
|
+
case 'failed':
|
|
304
|
+
case 'expired':
|
|
305
|
+
return value;
|
|
306
|
+
case 'synced':
|
|
307
|
+
case 'delivered':
|
|
308
|
+
return 'done';
|
|
309
|
+
case 'rejected':
|
|
310
|
+
return 'failed';
|
|
311
|
+
default:
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function recoverImportedStatus(type, requested) {
|
|
316
|
+
if (!specForType(type)) {
|
|
317
|
+
return requested === 'pending' || requested === 'in_flight' ? 'failed' : requested;
|
|
318
|
+
}
|
|
319
|
+
// A V1 worker and its lease cannot survive migration. Requeue in-flight work instead of creating a V2 row
|
|
320
|
+
// with status=in_flight and leasedUntil=NULL, which SQLite will never consider expired or claimable.
|
|
321
|
+
return requested === 'in_flight' ? 'pending' : requested;
|
|
322
|
+
}
|
|
323
|
+
function normalizeImportedEnvelope(record, now = Date.now()) {
|
|
324
|
+
const id = nonEmptyString(record['id']);
|
|
325
|
+
const type = nonEmptyString(record['type']);
|
|
326
|
+
if (!id || !type)
|
|
327
|
+
return undefined;
|
|
328
|
+
const createdAt = finiteNumber(record['createdAt']) ?? finiteNumber(record['created_at']) ?? now;
|
|
329
|
+
const spec = specForType(type);
|
|
330
|
+
const directionValue = record['direction'];
|
|
331
|
+
const direction = directionValue === 'outbound' || directionValue === 'inbound' || directionValue === 'local'
|
|
332
|
+
? directionValue
|
|
333
|
+
: spec?.direction;
|
|
334
|
+
if (!direction)
|
|
335
|
+
return undefined;
|
|
336
|
+
const handlerValue = record['handler'];
|
|
337
|
+
const handler = handlerValue === 'core' || handlerValue === 'proxy' || handlerValue === 'agent'
|
|
338
|
+
? handlerValue
|
|
339
|
+
: spec?.handler ?? (direction === 'outbound' ? 'proxy' : 'agent');
|
|
340
|
+
const originalStatus = importedStatus(record['status']) ?? 'pending';
|
|
341
|
+
// Preserve unknown V1 types for audit without dispatching a contract that V2 cannot handle.
|
|
342
|
+
const status = recoverImportedStatus(type, originalStatus);
|
|
343
|
+
// V1 `channel` names a transport (normally `evomap-hub`); V2 runtimeNamespace is an isolation boundary.
|
|
344
|
+
const runtimeNamespace = nonEmptyString(record['runtimeNamespace']) ?? 'default';
|
|
345
|
+
const correlationId = nonEmptyString(record['correlationId'])
|
|
346
|
+
?? nonEmptyString(record['correlation_id'])
|
|
347
|
+
?? nonEmptyString(record['ref_id'])
|
|
348
|
+
?? id;
|
|
349
|
+
const attempts = Math.max(0, Math.floor(finiteNumber(record['attempts']) ?? finiteNumber(record['retry_count']) ?? 0));
|
|
350
|
+
const importedTtlAt = timestampField(record, 'ttlAt', 'expires_at');
|
|
351
|
+
const ttlAt = importedTtlAt === undefined
|
|
352
|
+
? (spec?.ttlClass === 'control' ? createdAt + 60 * 60 * 1000 : createdAt + 7 * 24 * 60 * 60 * 1000)
|
|
353
|
+
: importedTtlAt;
|
|
354
|
+
const updatedAt = finiteNumber(record['updatedAt'])
|
|
355
|
+
?? finiteNumber(record['updated_at'])
|
|
356
|
+
?? finiteNumber(record['synced_at'])
|
|
357
|
+
?? createdAt;
|
|
358
|
+
return {
|
|
359
|
+
id,
|
|
360
|
+
type,
|
|
361
|
+
direction,
|
|
362
|
+
status,
|
|
363
|
+
handler,
|
|
364
|
+
payload: sanitizePayload(record['payload'] ?? {}),
|
|
365
|
+
correlationId,
|
|
366
|
+
replyTo: nonEmptyString(record['replyTo']) ?? nonEmptyString(record['reply_to']) ?? null,
|
|
367
|
+
receiptId: nonEmptyString(record['receiptId']) ?? nonEmptyString(record['receipt_id']) ?? id,
|
|
368
|
+
idempotencyKey: nonEmptyString(record['idempotencyKey']) ?? nonEmptyString(record['idempotency_key']) ?? id,
|
|
369
|
+
sourceAgent: nonEmptyString(record['sourceAgent']) ?? nonEmptyString(record['source_agent']) ?? '',
|
|
370
|
+
targetAgent: nonEmptyString(record['targetAgent']) ?? nonEmptyString(record['target_agent']) ?? '',
|
|
371
|
+
runtimeNamespace,
|
|
372
|
+
priority: normalizePriority(record['priority']),
|
|
373
|
+
attempts,
|
|
374
|
+
nextRetryAt: timestampField(record, 'nextRetryAt', 'next_retry_at') ?? null,
|
|
375
|
+
ttlAt,
|
|
376
|
+
createdAt,
|
|
377
|
+
updatedAt,
|
|
378
|
+
schemaVersion: nonEmptyString(record['schemaVersion']) ?? ENVELOPE_SCHEMA_VERSION,
|
|
379
|
+
feedsMaterial: typeof record['feedsMaterial'] === 'boolean' ? record['feedsMaterial'] : spec?.feedsMaterial ?? false,
|
|
380
|
+
lastError: nullableStringField(record, 'lastError', 'last_error', 'error') ?? null,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function positiveInteger(value, fallback) {
|
|
384
|
+
const parsed = Number(value);
|
|
385
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
386
|
+
}
|
|
387
|
+
function deepFreeze(value) {
|
|
388
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
389
|
+
for (const nested of Object.values(value))
|
|
390
|
+
deepFreeze(nested);
|
|
391
|
+
Object.freeze(value);
|
|
392
|
+
}
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
const mailboxFatalUtf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
396
|
+
function decodeMailboxImportLine(bytes, line) {
|
|
397
|
+
try {
|
|
398
|
+
return mailboxFatalUtf8Decoder.decode(bytes);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
throw new MailboxJsonlImportError('invalid_utf8', line, 'invalid UTF-8');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Parse and normalize a V1 mailbox journal without opening or mutating a V2 mailbox.
|
|
406
|
+
* The returned batch is sealed so dry-run and apply can consume the exact same records.
|
|
407
|
+
*/
|
|
408
|
+
export function prepareMailboxJsonlImport(source, options = {}) {
|
|
409
|
+
if (typeof source === 'string' && !existsSync(source)) {
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
records: Object.freeze([]),
|
|
412
|
+
sourceRecords: 0,
|
|
413
|
+
messageCandidates: 0,
|
|
414
|
+
uniqueMessageCandidates: 0,
|
|
415
|
+
updateCandidates: 0,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const maxLineBytes = positiveInteger(options.maxLineBytes ?? process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES'], DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES);
|
|
419
|
+
const maxRecords = positiveInteger(options.maxRecords ?? process.env['EVOLVER_IMPORT_JSONL_MAX_RECORDS'], DEFAULT_IMPORT_JSONL_MAX_RECORDS);
|
|
420
|
+
const maxBytes = positiveInteger(options.maxBytes ?? process.env['EVOLVER_IMPORT_JSONL_MAX_BYTES'], DEFAULT_IMPORT_JSONL_MAX_BYTES);
|
|
421
|
+
const now = Number.isFinite(options.now) ? Number(options.now) : Date.now();
|
|
33
422
|
const buf = Buffer.allocUnsafe(JSONL_READ_CHUNK_BYTES);
|
|
423
|
+
const records = [];
|
|
424
|
+
const messageIds = new Set();
|
|
425
|
+
let sourceRecords = 0;
|
|
426
|
+
let messageCandidates = 0;
|
|
427
|
+
let updateCandidates = 0;
|
|
34
428
|
let fd;
|
|
429
|
+
let ownsFd = false;
|
|
430
|
+
let position = 0;
|
|
35
431
|
let parts = [];
|
|
36
432
|
let lineBytes = 0;
|
|
37
|
-
let
|
|
38
|
-
|
|
433
|
+
let lineNumber = 1;
|
|
434
|
+
let overlong = false;
|
|
435
|
+
const reset = () => { parts = []; lineBytes = 0; overlong = false; };
|
|
39
436
|
const append = (segment) => {
|
|
40
|
-
if (
|
|
437
|
+
if (overlong || segment.length === 0)
|
|
41
438
|
return;
|
|
42
439
|
if (lineBytes + segment.length > maxLineBytes) {
|
|
43
440
|
reset();
|
|
44
|
-
|
|
441
|
+
overlong = true;
|
|
45
442
|
return;
|
|
46
443
|
}
|
|
47
444
|
parts.push(Buffer.from(segment));
|
|
48
445
|
lineBytes += segment.length;
|
|
49
446
|
};
|
|
50
447
|
const finish = () => {
|
|
51
|
-
|
|
448
|
+
const currentLine = lineNumber;
|
|
449
|
+
lineNumber += 1;
|
|
450
|
+
if (overlong) {
|
|
451
|
+
reset();
|
|
452
|
+
throw new MailboxJsonlImportError('line_too_large', currentLine, `record exceeds ${maxLineBytes} bytes`);
|
|
453
|
+
}
|
|
454
|
+
if (lineBytes === 0) {
|
|
52
455
|
reset();
|
|
53
456
|
return;
|
|
54
457
|
}
|
|
55
|
-
const text = Buffer.concat(parts, lineBytes)
|
|
458
|
+
const text = decodeMailboxImportLine(Buffer.concat(parts, lineBytes), currentLine).trim();
|
|
56
459
|
reset();
|
|
57
460
|
if (!text)
|
|
58
461
|
return;
|
|
462
|
+
sourceRecords += 1;
|
|
463
|
+
if (sourceRecords > maxRecords) {
|
|
464
|
+
throw new MailboxJsonlImportError('too_many_records', currentLine, `journal exceeds ${maxRecords} records`);
|
|
465
|
+
}
|
|
466
|
+
let record;
|
|
59
467
|
try {
|
|
60
|
-
|
|
468
|
+
record = JSON.parse(text);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
throw new MailboxJsonlImportError('invalid_json', currentLine, 'invalid JSON');
|
|
61
472
|
}
|
|
62
|
-
|
|
473
|
+
const row = asRow(record);
|
|
474
|
+
if (!row) {
|
|
475
|
+
throw new MailboxJsonlImportError('invalid_record', currentLine, 'record must be a JSON object');
|
|
476
|
+
}
|
|
477
|
+
if (Object.prototype.hasOwnProperty.call(row, '_op')) {
|
|
478
|
+
if (row['_op'] !== 'update') {
|
|
479
|
+
throw new MailboxJsonlImportError('unsupported_operation', currentLine, 'unsupported _op value');
|
|
480
|
+
}
|
|
481
|
+
const id = nonEmptyString(row['id']);
|
|
482
|
+
const fields = asRow(row['fields']);
|
|
483
|
+
if (!id || !fields) {
|
|
484
|
+
throw new MailboxJsonlImportError('invalid_update', currentLine, 'update requires a non-empty id and object fields');
|
|
485
|
+
}
|
|
486
|
+
records.push(deepFreeze({ kind: 'update', id, fields }));
|
|
487
|
+
updateCandidates += 1;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const envelope = normalizeImportedEnvelope(row, now);
|
|
491
|
+
if (!envelope) {
|
|
492
|
+
throw new MailboxJsonlImportError('invalid_message', currentLine, 'message requires a non-empty id, type, and valid direction');
|
|
493
|
+
}
|
|
494
|
+
records.push(deepFreeze({ kind: 'message', envelope }));
|
|
495
|
+
messageCandidates += 1;
|
|
496
|
+
messageIds.add(envelope.id);
|
|
63
497
|
};
|
|
64
498
|
try {
|
|
65
|
-
|
|
499
|
+
if (typeof source === 'number') {
|
|
500
|
+
fd = source;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
fd = openSync(source, 'r');
|
|
504
|
+
ownsFd = true;
|
|
505
|
+
}
|
|
66
506
|
for (;;) {
|
|
67
|
-
const bytes = readSync(fd, buf, 0, buf.length,
|
|
507
|
+
const bytes = readSync(fd, buf, 0, buf.length, position);
|
|
68
508
|
if (bytes <= 0)
|
|
69
509
|
break;
|
|
510
|
+
position += bytes;
|
|
511
|
+
if (position > maxBytes) {
|
|
512
|
+
throw new MailboxJsonlImportError('journal_too_large', lineNumber, `journal exceeds ${maxBytes} bytes`);
|
|
513
|
+
}
|
|
70
514
|
let start = 0;
|
|
71
515
|
for (let i = 0; i < bytes; i += 1) {
|
|
72
516
|
if (buf[i] !== 0x0a)
|
|
@@ -81,9 +525,16 @@ function forEachJsonlRecord(path, onRecord) {
|
|
|
81
525
|
finish();
|
|
82
526
|
}
|
|
83
527
|
finally {
|
|
84
|
-
if (fd !== undefined)
|
|
528
|
+
if (ownsFd && fd !== undefined)
|
|
85
529
|
closeSync(fd);
|
|
86
530
|
}
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
records: Object.freeze(records),
|
|
533
|
+
sourceRecords,
|
|
534
|
+
messageCandidates,
|
|
535
|
+
uniqueMessageCandidates: messageIds.size,
|
|
536
|
+
updateCandidates,
|
|
537
|
+
});
|
|
87
538
|
}
|
|
88
539
|
function rowToEnvelope(r) {
|
|
89
540
|
return {
|
|
@@ -94,12 +545,186 @@ function rowToEnvelope(r) {
|
|
|
94
545
|
receiptId: r['receiptId'], idempotencyKey: r['idempotencyKey'],
|
|
95
546
|
sourceAgent: r['sourceAgent'], targetAgent: r['targetAgent'],
|
|
96
547
|
runtimeNamespace: r['runtimeNamespace'],
|
|
548
|
+
priority: normalizePriority(r['priority']),
|
|
97
549
|
attempts: r['attempts'], nextRetryAt: r['nextRetryAt'] ?? null,
|
|
98
550
|
ttlAt: r['ttlAt'] ?? null,
|
|
99
551
|
createdAt: r['createdAt'], updatedAt: r['updatedAt'],
|
|
100
552
|
schemaVersion: r['schemaVersion'], feedsMaterial: Boolean(r['feedsMaterial']),
|
|
553
|
+
lastError: r['lastError'] ?? null,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function applyImportedV1UpdateToEnvelope(current, fields) {
|
|
557
|
+
const requestedStatus = importedStatus(fields['status']) ?? current.status;
|
|
558
|
+
const status = recoverImportedStatus(current.type, requestedStatus);
|
|
559
|
+
const attempts = Math.max(0, Math.floor(finiteNumber(fields['attempts']) ?? finiteNumber(fields['retry_count']) ?? current.attempts));
|
|
560
|
+
const importedNextRetryAt = timestampField(fields, 'nextRetryAt', 'next_retry_at');
|
|
561
|
+
const nextRetryAt = importedNextRetryAt === undefined ? current.nextRetryAt : importedNextRetryAt;
|
|
562
|
+
const importedTtlAt = timestampField(fields, 'ttlAt', 'expires_at');
|
|
563
|
+
const ttlAt = importedTtlAt === undefined ? current.ttlAt : importedTtlAt;
|
|
564
|
+
const updatedAt = finiteNumber(fields['updatedAt'])
|
|
565
|
+
?? finiteNumber(fields['updated_at'])
|
|
566
|
+
?? finiteNumber(fields['synced_at'])
|
|
567
|
+
?? current.updatedAt;
|
|
568
|
+
const payload = Object.prototype.hasOwnProperty.call(fields, 'payload')
|
|
569
|
+
? sanitizePayload(fields['payload'])
|
|
570
|
+
: current.payload;
|
|
571
|
+
const priority = Object.prototype.hasOwnProperty.call(fields, 'priority')
|
|
572
|
+
? normalizePriority(fields['priority'])
|
|
573
|
+
: current.priority;
|
|
574
|
+
const importedLastError = nullableStringField(fields, 'lastError', 'last_error', 'error');
|
|
575
|
+
const lastError = importedLastError === undefined ? current.lastError : importedLastError;
|
|
576
|
+
return {
|
|
577
|
+
...current,
|
|
578
|
+
status,
|
|
579
|
+
payload,
|
|
580
|
+
priority,
|
|
581
|
+
attempts,
|
|
582
|
+
nextRetryAt,
|
|
583
|
+
ttlAt,
|
|
584
|
+
updatedAt,
|
|
585
|
+
lastError,
|
|
101
586
|
};
|
|
102
587
|
}
|
|
588
|
+
function buildMailboxPreparedImportPlan(batch, getById, getBaseline) {
|
|
589
|
+
const eligibleIds = new Set();
|
|
590
|
+
const insertedIds = new Set();
|
|
591
|
+
const managedIds = new Set();
|
|
592
|
+
const protectedIds = new Set();
|
|
593
|
+
const updatedIds = new Set();
|
|
594
|
+
const finalById = new Map();
|
|
595
|
+
for (const record of batch.records) {
|
|
596
|
+
if (record.kind === 'update') {
|
|
597
|
+
if (!eligibleIds.has(record.id))
|
|
598
|
+
continue;
|
|
599
|
+
const current = finalById.get(record.id);
|
|
600
|
+
if (!current)
|
|
601
|
+
continue;
|
|
602
|
+
const updated = applyImportedV1UpdateToEnvelope(current, record.fields);
|
|
603
|
+
if (JSON.stringify(updated) !== JSON.stringify(current))
|
|
604
|
+
updatedIds.add(record.id);
|
|
605
|
+
finalById.set(record.id, updated);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const envelope = { ...record.envelope };
|
|
609
|
+
if (eligibleIds.has(envelope.id))
|
|
610
|
+
continue;
|
|
611
|
+
let current = finalById.get(envelope.id);
|
|
612
|
+
if (!current) {
|
|
613
|
+
current = getById(envelope.id);
|
|
614
|
+
if (current)
|
|
615
|
+
finalById.set(envelope.id, current);
|
|
616
|
+
}
|
|
617
|
+
if (!current) {
|
|
618
|
+
finalById.set(envelope.id, envelope);
|
|
619
|
+
insertedIds.add(envelope.id);
|
|
620
|
+
eligibleIds.add(envelope.id);
|
|
621
|
+
protectedIds.delete(envelope.id);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const baseline = getBaseline(envelope.id);
|
|
625
|
+
if (baseline !== undefined && JSON.stringify(current) === baseline) {
|
|
626
|
+
eligibleIds.add(envelope.id);
|
|
627
|
+
managedIds.add(envelope.id);
|
|
628
|
+
protectedIds.delete(envelope.id);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
protectedIds.add(envelope.id);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
preview: Object.freeze({
|
|
636
|
+
sourceRecords: batch.sourceRecords,
|
|
637
|
+
messageCandidates: batch.messageCandidates,
|
|
638
|
+
uniqueMessageCandidates: batch.uniqueMessageCandidates,
|
|
639
|
+
updateCandidates: batch.updateCandidates,
|
|
640
|
+
insertedMessages: insertedIds.size,
|
|
641
|
+
updatedMessages: updatedIds.size,
|
|
642
|
+
managedMessages: managedIds.size,
|
|
643
|
+
protectedMessages: protectedIds.size,
|
|
644
|
+
}),
|
|
645
|
+
finalById,
|
|
646
|
+
insertedIds,
|
|
647
|
+
managedIds,
|
|
648
|
+
eligibleIds,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function previewPreparedMailboxDatabase(batch, database) {
|
|
652
|
+
try {
|
|
653
|
+
database.exec('BEGIN');
|
|
654
|
+
const hasMessages = database.prepare("SELECT 1 present FROM sqlite_master WHERE type='table' AND name='messages'").get() !== undefined;
|
|
655
|
+
const hasImportState = database.prepare("SELECT 1 present FROM sqlite_master WHERE type='table' AND name='v1_import_state'").get() !== undefined;
|
|
656
|
+
const readMessage = hasMessages
|
|
657
|
+
? database.prepare('SELECT * FROM messages WHERE id=?')
|
|
658
|
+
: undefined;
|
|
659
|
+
const readBaseline = hasImportState
|
|
660
|
+
? database.prepare('SELECT snapshot FROM v1_import_state WHERE id=?')
|
|
661
|
+
: undefined;
|
|
662
|
+
const plan = buildMailboxPreparedImportPlan(batch, (id) => {
|
|
663
|
+
const row = readMessage?.get(id);
|
|
664
|
+
return row ? rowToEnvelope(row) : undefined;
|
|
665
|
+
}, (id) => {
|
|
666
|
+
const snapshot = readBaseline?.get(id)?.['snapshot'];
|
|
667
|
+
return typeof snapshot === 'string' ? snapshot : undefined;
|
|
668
|
+
});
|
|
669
|
+
database.exec('COMMIT');
|
|
670
|
+
return plan.preview;
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
try {
|
|
674
|
+
database.exec('ROLLBACK');
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// Preserve the primary preview error.
|
|
678
|
+
}
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/** Preview a prepared import against an optional existing mailbox without creating or migrating it. */
|
|
683
|
+
export function previewPreparedMailboxImport(batch, mailboxPath) {
|
|
684
|
+
if (!mailboxPath) {
|
|
685
|
+
return buildMailboxPreparedImportPlan(batch, () => undefined, () => undefined).preview;
|
|
686
|
+
}
|
|
687
|
+
const databaseStat = mailboxPreviewStat(mailboxPath);
|
|
688
|
+
if (!databaseStat) {
|
|
689
|
+
assertMailboxPreviewTargetAbsent(mailboxPath);
|
|
690
|
+
return buildMailboxPreparedImportPlan(batch, () => undefined, () => undefined).preview;
|
|
691
|
+
}
|
|
692
|
+
const snapshot = stableMailboxPreviewCopy(mailboxPath);
|
|
693
|
+
let database;
|
|
694
|
+
let preview;
|
|
695
|
+
let operationError;
|
|
696
|
+
try {
|
|
697
|
+
database = openSqliteDatabase(snapshot.path, { readOnly: true });
|
|
698
|
+
preview = previewPreparedMailboxDatabase(batch, database);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
operationError = error;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
database?.close();
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
operationError ??= error;
|
|
708
|
+
}
|
|
709
|
+
let cleanupError;
|
|
710
|
+
try {
|
|
711
|
+
rmSync(snapshot.dir, { recursive: true, force: true });
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
cleanupError = error;
|
|
715
|
+
}
|
|
716
|
+
if (operationError !== undefined)
|
|
717
|
+
throw operationError;
|
|
718
|
+
if (cleanupError !== undefined)
|
|
719
|
+
throw mailboxPreviewError('cleanup_failed');
|
|
720
|
+
return preview;
|
|
721
|
+
}
|
|
722
|
+
function ensureMessageColumn(db, name, definition) {
|
|
723
|
+
const columns = db.prepare('PRAGMA table_info(messages)').all();
|
|
724
|
+
if (!columns.some((column) => column['name'] === name)) {
|
|
725
|
+
db.exec(`ALTER TABLE messages ADD COLUMN ${definition}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
103
728
|
/** mailbox sqlite 引擎 (WAL + busy_timeout). 状态机 pending→in_flight→done/failed/expired + 租约 + 重试/DLQ. */
|
|
104
729
|
export class MailboxStore {
|
|
105
730
|
db;
|
|
@@ -115,14 +740,19 @@ export class MailboxStore {
|
|
|
115
740
|
this.db.exec(`CREATE TABLE IF NOT EXISTS messages (
|
|
116
741
|
id TEXT PRIMARY KEY, type TEXT, direction TEXT, status TEXT, handler TEXT,
|
|
117
742
|
payload TEXT, correlationId TEXT, replyTo TEXT, receiptId TEXT, idempotencyKey TEXT,
|
|
118
|
-
sourceAgent TEXT, targetAgent TEXT, runtimeNamespace TEXT,
|
|
743
|
+
sourceAgent TEXT, targetAgent TEXT, runtimeNamespace TEXT, priority TEXT NOT NULL DEFAULT 'normal',
|
|
119
744
|
attempts INTEGER, nextRetryAt INTEGER, ttlAt INTEGER, createdAt INTEGER, updatedAt INTEGER,
|
|
120
|
-
schemaVersion TEXT, feedsMaterial INTEGER, dlq INTEGER DEFAULT 0, leasedUntil INTEGER, workerId TEXT,
|
|
745
|
+
schemaVersion TEXT, feedsMaterial INTEGER, dlq INTEGER DEFAULT 0, leasedUntil INTEGER, workerId TEXT,
|
|
746
|
+
lastError TEXT, claimCheckpoint TEXT)`);
|
|
747
|
+
ensureMessageColumn(this.db, 'priority', "priority TEXT NOT NULL DEFAULT 'normal'");
|
|
748
|
+
ensureMessageColumn(this.db, 'lastError', 'lastError TEXT');
|
|
749
|
+
ensureMessageColumn(this.db, 'claimCheckpoint', 'claimCheckpoint TEXT');
|
|
121
750
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_status ON messages(status, handler)');
|
|
122
751
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_corr ON messages(correlationId)');
|
|
123
752
|
this.db.exec('CREATE INDEX IF NOT EXISTS idx_message_view ON messages(runtimeNamespace,type,direction,status,createdAt)');
|
|
124
753
|
this.db.exec('CREATE TABLE IF NOT EXISTS idempotency (key TEXT PRIMARY KEY, result TEXT, at INTEGER)');
|
|
125
754
|
this.db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)');
|
|
755
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS v1_import_state (id TEXT PRIMARY KEY, snapshot TEXT NOT NULL)');
|
|
126
756
|
}
|
|
127
757
|
/** 通用 KV 状态(M6: sync 游标 inbound_cursor / lifecycle reauth 退避 / node_id 等). */
|
|
128
758
|
getState(key) {
|
|
@@ -135,8 +765,8 @@ export class MailboxStore {
|
|
|
135
765
|
/** 投递; 同 id 幂等(OR IGNORE). 返回 receiptId. */
|
|
136
766
|
send(e) {
|
|
137
767
|
const r = this.db.prepare(`INSERT OR IGNORE INTO messages
|
|
138
|
-
(id,type,direction,status,handler,payload,correlationId,replyTo,receiptId,idempotencyKey,sourceAgent,targetAgent,runtimeNamespace,attempts,nextRetryAt,ttlAt,createdAt,updatedAt,schemaVersion,feedsMaterial,dlq)
|
|
139
|
-
VALUES (
|
|
768
|
+
(id,type,direction,status,handler,payload,correlationId,replyTo,receiptId,idempotencyKey,sourceAgent,targetAgent,runtimeNamespace,priority,attempts,nextRetryAt,ttlAt,createdAt,updatedAt,schemaVersion,feedsMaterial,lastError,dlq)
|
|
769
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)`).run(e.id, e.type, e.direction, e.status, e.handler, JSON.stringify(e.payload), e.correlationId, e.replyTo, e.receiptId, e.idempotencyKey, e.sourceAgent, e.targetAgent, e.runtimeNamespace, e.priority, e.attempts, e.nextRetryAt, e.ttlAt, e.createdAt, e.updatedAt, e.schemaVersion, e.feedsMaterial ? 1 : 0, e.lastError);
|
|
140
770
|
return { receiptId: e.id, stored: Number(r.changes) > 0 };
|
|
141
771
|
}
|
|
142
772
|
getById(id) {
|
|
@@ -253,13 +883,17 @@ export class MailboxStore {
|
|
|
253
883
|
const rows = this.db.prepare(`SELECT * FROM messages WHERE handler=? AND dlq=0
|
|
254
884
|
AND (status='pending' OR (status='in_flight' AND leasedUntil < ?))
|
|
255
885
|
AND (nextRetryAt IS NULL OR nextRetryAt <= ?)${nsClause}
|
|
256
|
-
ORDER BY createdAt LIMIT ?`).all(handler, now, now, ...nsArgs, limit);
|
|
886
|
+
ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'low' THEN 2 ELSE 1 END, createdAt LIMIT ?`).all(handler, now, now, ...nsArgs, limit);
|
|
257
887
|
const workerId = makeUlid();
|
|
258
888
|
const upd = this.db.prepare(`UPDATE messages SET status='in_flight', leasedUntil=?, workerId=?, updatedAt=? WHERE id=?`);
|
|
259
889
|
for (const r of rows)
|
|
260
890
|
upd.run(now + leaseMs, workerId, now, r['id']);
|
|
261
891
|
this.db.exec('COMMIT');
|
|
262
|
-
return rows.map((r) => ({
|
|
892
|
+
return rows.map((r) => ({
|
|
893
|
+
...rowToEnvelope(r),
|
|
894
|
+
status: 'in_flight',
|
|
895
|
+
[MAILBOX_CLAIM_OWNER]: workerId,
|
|
896
|
+
}));
|
|
263
897
|
}
|
|
264
898
|
catch (e) {
|
|
265
899
|
this.db.exec('ROLLBACK');
|
|
@@ -267,7 +901,7 @@ export class MailboxStore {
|
|
|
267
901
|
}
|
|
268
902
|
}
|
|
269
903
|
complete(id, now) {
|
|
270
|
-
this.db.prepare(`UPDATE messages SET status='done', leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`).run(now, id);
|
|
904
|
+
this.db.prepare(`UPDATE messages SET status='done', dlq=0, lastError=NULL, leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`).run(now, id);
|
|
271
905
|
}
|
|
272
906
|
/** 失败: attempts<N→退避回 pending; ≥N→DLQ(不自动丢). */
|
|
273
907
|
fail(id, err, now, maxAttempts = MAX_ATTEMPTS) {
|
|
@@ -289,9 +923,101 @@ export class MailboxStore {
|
|
|
289
923
|
this.db.prepare(`UPDATE messages SET status='pending', nextRetryAt=?, lastError=?, leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`)
|
|
290
924
|
.run(now + delay, err, now, id);
|
|
291
925
|
}
|
|
926
|
+
deferUnlessProcessed(id, processedKey, err, now, retryAfterMs) {
|
|
927
|
+
return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'pending', () => this.defer(id, err, now, retryAfterMs));
|
|
928
|
+
}
|
|
929
|
+
failUnlessProcessed(id, processedKey, err, now, maxAttempts = 5) {
|
|
930
|
+
return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'pending', () => this.fail(id, err, now, maxAttempts));
|
|
931
|
+
}
|
|
932
|
+
completeClaimed(id, now, claimOwner) {
|
|
933
|
+
return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.complete(id, now), claimOwner);
|
|
934
|
+
}
|
|
935
|
+
/** Atomically complete the current worker's lease and persist its idempotency result. */
|
|
936
|
+
completeClaimedAndMarkProcessed(id, processedKey, result, now, claimOwner) {
|
|
937
|
+
return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => {
|
|
938
|
+
this.markProcessed(processedKey, result, now);
|
|
939
|
+
this.complete(id, now);
|
|
940
|
+
}, claimOwner);
|
|
941
|
+
}
|
|
942
|
+
renewClaim(id, now, leaseMs, claimOwner) {
|
|
943
|
+
const result = this.db.prepare(`UPDATE messages SET leasedUntil=?, updatedAt=?
|
|
944
|
+
WHERE id=? AND status='in_flight' AND workerId=?`).run(now + leaseMs, now, id, claimOwner);
|
|
945
|
+
return result.changes === 1;
|
|
946
|
+
}
|
|
947
|
+
setClaimedCheckpoint(id, checkpoint, now, claimOwner) {
|
|
948
|
+
const encoded = JSON.stringify(checkpoint);
|
|
949
|
+
return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => {
|
|
950
|
+
this.db.prepare('UPDATE messages SET claimCheckpoint=?, updatedAt=? WHERE id=?').run(encoded, now, id);
|
|
951
|
+
}, claimOwner);
|
|
952
|
+
}
|
|
953
|
+
getClaimedCheckpoint(id) {
|
|
954
|
+
const row = this.db.prepare('SELECT claimCheckpoint FROM messages WHERE id=?').get(id);
|
|
955
|
+
const encoded = row?.['claimCheckpoint'];
|
|
956
|
+
if (typeof encoded !== 'string')
|
|
957
|
+
return undefined;
|
|
958
|
+
try {
|
|
959
|
+
return JSON.parse(encoded);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
deferClaimed(id, err, now, retryAfterMs, claimOwner) {
|
|
966
|
+
return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.defer(id, err, now, retryAfterMs), claimOwner);
|
|
967
|
+
}
|
|
968
|
+
failClaimed(id, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
|
|
969
|
+
return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.fail(id, err, now, maxAttempts), claimOwner);
|
|
970
|
+
}
|
|
971
|
+
/** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
|
|
972
|
+
deferClaimedUnlessProcessed(id, processedKey, err, now, retryAfterMs, claimOwner) {
|
|
973
|
+
return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'in_flight', () => this.defer(id, err, now, retryAfterMs), claimOwner);
|
|
974
|
+
}
|
|
975
|
+
/** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
|
|
976
|
+
failClaimedUnlessProcessed(id, processedKey, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
|
|
977
|
+
return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'in_flight', () => this.fail(id, err, now, maxAttempts), claimOwner);
|
|
978
|
+
}
|
|
979
|
+
/** Atomically fail a non-leased intent and persist the replayable terminal result. */
|
|
980
|
+
failAndMarkProcessedUnlessProcessed(id, blockingProcessedKeys, resultKey, result, err, now, maxAttempts = MAX_ATTEMPTS) {
|
|
981
|
+
return this.transitionUnlessProcessed(id, blockingProcessedKeys, (current) => current.status === 'pending', () => {
|
|
982
|
+
this.fail(id, err, now, maxAttempts);
|
|
983
|
+
this.markProcessed(resultKey, result, now);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/** Atomically fail the current worker's leased intent and persist the replayable terminal result. */
|
|
987
|
+
failClaimedAndMarkProcessedUnlessProcessed(id, blockingProcessedKeys, resultKey, result, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
|
|
988
|
+
return this.transitionUnlessProcessed(id, blockingProcessedKeys, (current) => current.status === 'in_flight', () => {
|
|
989
|
+
this.fail(id, err, now, maxAttempts);
|
|
990
|
+
this.markProcessed(resultKey, result, now);
|
|
991
|
+
}, claimOwner);
|
|
992
|
+
}
|
|
993
|
+
transitionUnlessProcessed(id, processedKeys, isEligible, transition, expectedClaimOwner) {
|
|
994
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
995
|
+
try {
|
|
996
|
+
const current = this.getById(id);
|
|
997
|
+
if (processedKeys.some((key) => this.isProcessed(key))
|
|
998
|
+
|| !current
|
|
999
|
+
|| !isEligible(current)
|
|
1000
|
+
|| (expectedClaimOwner !== undefined && this.messageClaimOwner(id) !== expectedClaimOwner)) {
|
|
1001
|
+
this.db.exec('COMMIT');
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
transition();
|
|
1005
|
+
this.db.exec('COMMIT');
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
try {
|
|
1010
|
+
this.db.exec('ROLLBACK');
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
// Preserve the primary transaction error.
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
292
1018
|
/** DLQ 重放(人工/agent 显式, 不自动丢). */
|
|
293
1019
|
replayDlq(id, now) {
|
|
294
|
-
this.db.prepare(`UPDATE messages SET status='pending', dlq=0, attempts=0, nextRetryAt=NULL, leasedUntil=NULL, updatedAt=? WHERE id=?`).run(now, id);
|
|
1020
|
+
this.db.prepare(`UPDATE messages SET status='pending', dlq=0, attempts=0, nextRetryAt=NULL, lastError=NULL, leasedUntil=NULL, updatedAt=? WHERE id=?`).run(now, id);
|
|
295
1021
|
}
|
|
296
1022
|
dlq() { return this.db.prepare('SELECT * FROM messages WHERE dlq=1').all().map(rowToEnvelope); }
|
|
297
1023
|
/** TTL 扫描: ttlAt 过期且未 done/dlq → expired. */
|
|
@@ -326,13 +1052,15 @@ export class MailboxStore {
|
|
|
326
1052
|
}
|
|
327
1053
|
/** M2-5 轻量状态视图(运维/IPC 查询用). */
|
|
328
1054
|
getStatus(id) {
|
|
329
|
-
const r = this.db.prepare('SELECT id,type,status,attempts,nextRetryAt,ttlAt,dlq FROM messages WHERE id=?').get(id);
|
|
1055
|
+
const r = this.db.prepare('SELECT id,type,status,priority,attempts,nextRetryAt,ttlAt,dlq,lastError FROM messages WHERE id=?').get(id);
|
|
330
1056
|
if (!r)
|
|
331
1057
|
return undefined;
|
|
332
1058
|
return {
|
|
333
1059
|
id: r['id'], type: r['type'], status: r['status'],
|
|
1060
|
+
priority: normalizePriority(r['priority']),
|
|
334
1061
|
attempts: Number(r['attempts']), nextRetryAt: r['nextRetryAt'] ?? null,
|
|
335
1062
|
ttlAt: r['ttlAt'] ?? null, dlq: Number(r['dlq']) === 1,
|
|
1063
|
+
lastError: r['lastError'] ?? null,
|
|
336
1064
|
};
|
|
337
1065
|
}
|
|
338
1066
|
/** 幂等(A13): 副作用 handler 用 idempotencyKey 去重; 命中返缓存不重跑. */
|
|
@@ -344,17 +1072,143 @@ export class MailboxStore {
|
|
|
344
1072
|
markProcessed(key, result, now) {
|
|
345
1073
|
this.db.prepare('INSERT OR IGNORE INTO idempotency (key,result,at) VALUES (?,?,?)').run(key, JSON.stringify(result ?? null), now);
|
|
346
1074
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
1075
|
+
replaceProcessed(key, result, now) {
|
|
1076
|
+
this.db.prepare(`INSERT INTO idempotency (key,result,at) VALUES (?,?,?)
|
|
1077
|
+
ON CONFLICT(key) DO UPDATE SET result=excluded.result, at=excluded.at`).run(key, JSON.stringify(result ?? null), now);
|
|
1078
|
+
}
|
|
1079
|
+
/** Persist a monotonic outcome and its lightweight concurrency marker in one SQLite transaction. */
|
|
1080
|
+
replaceProcessedWithMarker(key, result, markerKey, markerResult, now) {
|
|
1081
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1082
|
+
try {
|
|
1083
|
+
this.replaceProcessed(key, result, now);
|
|
1084
|
+
this.replaceProcessed(markerKey, markerResult, now);
|
|
1085
|
+
this.db.exec('COMMIT');
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
try {
|
|
1089
|
+
this.db.exec('ROLLBACK');
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
// Preserve the primary transaction error.
|
|
1093
|
+
}
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/** Backfill a concurrency marker only when the durable source result still satisfies the caller's predicate. */
|
|
1098
|
+
markProcessedIf(sourceKey, markerKey, markerResult, now, matches) {
|
|
1099
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1100
|
+
try {
|
|
1101
|
+
const sourceResult = this.getProcessed(sourceKey);
|
|
1102
|
+
if (sourceResult === undefined || !matches(sourceResult)) {
|
|
1103
|
+
this.db.exec('COMMIT');
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
this.markProcessed(markerKey, markerResult, now);
|
|
1107
|
+
this.db.exec('COMMIT');
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
351
1111
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
1112
|
+
this.db.exec('ROLLBACK');
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
// Preserve the primary transaction error.
|
|
354
1116
|
}
|
|
355
|
-
|
|
1117
|
+
throw error;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
deleteProcessed(keys) {
|
|
1121
|
+
if (keys.length === 0)
|
|
1122
|
+
return;
|
|
1123
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1124
|
+
try {
|
|
1125
|
+
const remove = this.db.prepare('DELETE FROM idempotency WHERE key=?');
|
|
1126
|
+
for (const key of keys)
|
|
1127
|
+
remove.run(key);
|
|
1128
|
+
this.db.exec('COMMIT');
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
try {
|
|
1132
|
+
this.db.exec('ROLLBACK');
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
// Preserve the primary transaction error.
|
|
1136
|
+
}
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
messageClaimOwner(id) {
|
|
1141
|
+
const row = this.db.prepare('SELECT workerId FROM messages WHERE id=?').get(id);
|
|
1142
|
+
return typeof row?.['workerId'] === 'string' ? row['workerId'] : undefined;
|
|
1143
|
+
}
|
|
1144
|
+
/** Build the target-aware plan shared by preview and apply. */
|
|
1145
|
+
buildPreparedImportPlan(batch) {
|
|
1146
|
+
const readImportState = this.db.prepare('SELECT snapshot FROM v1_import_state WHERE id=?');
|
|
1147
|
+
return buildMailboxPreparedImportPlan(batch, (id) => this.getById(id), (id) => {
|
|
1148
|
+
const snapshot = readImportState.get(id)?.['snapshot'];
|
|
1149
|
+
return typeof snapshot === 'string' ? snapshot : undefined;
|
|
356
1150
|
});
|
|
357
|
-
|
|
1151
|
+
}
|
|
1152
|
+
/** Preview the exact import result against the current mailbox snapshot without writing to it. */
|
|
1153
|
+
previewPreparedImport(batch) {
|
|
1154
|
+
this.db.exec('BEGIN');
|
|
1155
|
+
try {
|
|
1156
|
+
const preview = this.buildPreparedImportPlan(batch).preview;
|
|
1157
|
+
this.db.exec('COMMIT');
|
|
1158
|
+
return preview;
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
try {
|
|
1162
|
+
this.db.exec('ROLLBACK');
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
// Preserve the primary preview error.
|
|
1166
|
+
}
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
writeImportedEnvelopeFields(current) {
|
|
1171
|
+
this.db.prepare(`UPDATE messages
|
|
1172
|
+
SET status=?, payload=?, priority=?, attempts=?, nextRetryAt=?, ttlAt=?, updatedAt=?, lastError=?
|
|
1173
|
+
WHERE id=?`).run(current.status, JSON.stringify(current.payload), current.priority, current.attempts, current.nextRetryAt, current.ttlAt, current.updatedAt, current.lastError, current.id);
|
|
1174
|
+
}
|
|
1175
|
+
/** Apply a parsed batch; planning and writes share one transaction and one normalized journal. */
|
|
1176
|
+
importPrepared(batch) {
|
|
1177
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1178
|
+
try {
|
|
1179
|
+
const plan = this.buildPreparedImportPlan(batch);
|
|
1180
|
+
for (const id of plan.insertedIds) {
|
|
1181
|
+
const envelope = plan.finalById.get(id);
|
|
1182
|
+
if (!envelope || !this.send(envelope).stored) {
|
|
1183
|
+
throw new Error(`Mailbox import plan became stale for ${id}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const id of plan.managedIds) {
|
|
1187
|
+
const envelope = plan.finalById.get(id);
|
|
1188
|
+
if (envelope)
|
|
1189
|
+
this.writeImportedEnvelopeFields(envelope);
|
|
1190
|
+
}
|
|
1191
|
+
const writeImportState = this.db.prepare('INSERT INTO v1_import_state (id,snapshot) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET snapshot=excluded.snapshot');
|
|
1192
|
+
for (const id of plan.eligibleIds) {
|
|
1193
|
+
const envelope = plan.finalById.get(id);
|
|
1194
|
+
if (envelope)
|
|
1195
|
+
writeImportState.run(id, JSON.stringify(envelope));
|
|
1196
|
+
}
|
|
1197
|
+
this.db.exec('COMMIT');
|
|
1198
|
+
return plan.preview.insertedMessages;
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
try {
|
|
1202
|
+
this.db.exec('ROLLBACK');
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
// Preserve the primary import error.
|
|
1206
|
+
}
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
importJsonl(source) {
|
|
1211
|
+
return this.importPrepared(prepareMailboxJsonlImport(source));
|
|
358
1212
|
}
|
|
359
1213
|
close() { this.db.close(); }
|
|
360
1214
|
}
|