@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.
Files changed (201) hide show
  1. package/dist/algo/antiDistill.js +1 -167
  2. package/dist/algo/bans.js +1 -33
  3. package/dist/algo/candidateAssembly.d.ts +7 -2
  4. package/dist/algo/candidateAssembly.js +1 -205
  5. package/dist/algo/capabilityCandidates.js +1 -136
  6. package/dist/algo/confidence.js +1 -196
  7. package/dist/algo/conversationSniffer.js +1 -149
  8. package/dist/algo/cycleEngine.d.ts +23 -2
  9. package/dist/algo/cycleEngine.js +1 -425
  10. package/dist/algo/cycleFailureClassifier.js +1 -112
  11. package/dist/algo/epigenetics.js +1 -41
  12. package/dist/algo/evolutionEvent.js +1 -24
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +1 -55
  15. package/dist/algo/geneHealth.d.ts +2 -2
  16. package/dist/algo/geneHealth.js +1 -16
  17. package/dist/algo/geneIntake.js +1 -128
  18. package/dist/algo/genePromotion.js +1 -53
  19. package/dist/algo/geneSelection.d.ts +63 -5
  20. package/dist/algo/geneSelection.js +1 -246
  21. package/dist/algo/index.d.ts +2 -0
  22. package/dist/algo/index.js +1 -20
  23. package/dist/algo/memoryGraph.js +1 -86
  24. package/dist/algo/mutation.js +1 -22
  25. package/dist/algo/orchestrator.d.ts +8 -1
  26. package/dist/algo/orchestrator.js +1 -75
  27. package/dist/algo/publishEligibility.d.ts +34 -0
  28. package/dist/algo/publishEligibility.js +1 -0
  29. package/dist/algo/solidify.js +1 -63
  30. package/dist/algo/strategyPresets.js +1 -61
  31. package/dist/algo/ucb1.d.ts +53 -0
  32. package/dist/algo/ucb1.js +1 -0
  33. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  34. package/dist/assetstore/assetSidecarRecords.js +238 -0
  35. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  36. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  37. package/dist/assetstore/assetStoreHealth.js +56 -14
  38. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  39. package/dist/assetstore/assetStoreStorage.js +27 -9
  40. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  41. package/dist/assetstore/assetSyncLedger.js +683 -4
  42. package/dist/assetstore/index.d.ts +1 -0
  43. package/dist/assetstore/index.js +1 -0
  44. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  45. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  46. package/dist/assetstore/localJsonl.d.ts +4 -2
  47. package/dist/assetstore/localJsonl.js +89 -9
  48. package/dist/assetstore/provenance.d.ts +80 -4
  49. package/dist/assetstore/provenance.js +313 -5
  50. package/dist/assetstore/provider.d.ts +58 -1
  51. package/dist/assetstore/provider.js +97 -6
  52. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  53. package/dist/benchmark/antiGeneBenchmark.js +1 -251
  54. package/dist/benchmark/antiGeneImpact.js +1 -34
  55. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  56. package/dist/benchmark/antiGeneRollout.js +1 -268
  57. package/dist/benchmark/benchmark.js +1 -26
  58. package/dist/benchmark/evolutionThesisSolver.js +1 -41
  59. package/dist/benchmark/index.d.ts +2 -1
  60. package/dist/benchmark/index.js +1 -6
  61. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  62. package/dist/benchmark/selectionFlatAbstention.js +1 -0
  63. package/dist/benchmark/thesis.js +1 -150
  64. package/dist/benchmark/triggerShift.js +1 -106
  65. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  66. package/dist/bootstrap/envFingerprint.js +5 -0
  67. package/dist/bootstrap/index.d.ts +2 -1
  68. package/dist/bootstrap/index.js +2 -1
  69. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  70. package/dist/bootstrap/v1EnvCompat.js +280 -0
  71. package/dist/cycle/cycleTimeline.js +1 -74
  72. package/dist/cycle/index.js +1 -2
  73. package/dist/cycle/stateMachine.js +1 -25
  74. package/dist/events/eventArchive.d.ts +2 -0
  75. package/dist/events/eventArchive.js +13 -3
  76. package/dist/events/eventSchema.d.ts +7 -7
  77. package/dist/events/eventStore.d.ts +2 -0
  78. package/dist/events/eventStore.js +5 -1
  79. package/dist/events/ingest.d.ts +1 -0
  80. package/dist/events/ingest.js +1 -0
  81. package/dist/events/paths.d.ts +3 -1
  82. package/dist/events/paths.js +4 -0
  83. package/dist/events/public.d.ts +2 -2
  84. package/dist/events/public.js +2 -2
  85. package/dist/events/reports.d.ts +2 -0
  86. package/dist/events/reports.js +4 -0
  87. package/dist/exec/autoExec.d.ts +43 -2
  88. package/dist/exec/autoExec.js +76 -9
  89. package/dist/exec/autonomousCycle.d.ts +22 -4
  90. package/dist/exec/autonomousCycle.js +64 -13
  91. package/dist/exec/claudeBridge.d.ts +32 -7
  92. package/dist/exec/claudeBridge.js +281 -29
  93. package/dist/exec/prompt.js +5 -1
  94. package/dist/exec/runnerRegistry.d.ts +68 -26
  95. package/dist/exec/runnerRegistry.js +307 -72
  96. package/dist/exec/selfPr.js +1 -7
  97. package/dist/feedback/envelope.d.ts +61 -0
  98. package/dist/feedback/envelope.js +168 -0
  99. package/dist/feedback/index.d.ts +1 -0
  100. package/dist/feedback/index.js +1 -0
  101. package/dist/hooks/hooks.js +1 -0
  102. package/dist/hub/agentDirectory.js +1 -104
  103. package/dist/hub/assetCallLog.d.ts +35 -1
  104. package/dist/hub/assetCallLog.js +1 -72
  105. package/dist/hub/bindings.d.ts +8 -1
  106. package/dist/hub/bindings.js +1 -110
  107. package/dist/hub/capability.d.ts +90 -3
  108. package/dist/hub/capability.js +1 -1
  109. package/dist/hub/conversationDistiller.js +1 -264
  110. package/dist/hub/fake.d.ts +2 -2
  111. package/dist/hub/fake.js +1 -70
  112. package/dist/hub/hubReview.js +1 -106
  113. package/dist/hub/index.js +1 -11
  114. package/dist/hub/ingest.js +1 -15
  115. package/dist/hub/questionGenerator.d.ts +5 -1
  116. package/dist/hub/questionGenerator.js +1 -406
  117. package/dist/hub/reuseDecision.js +1 -127
  118. package/dist/hub/sanitize.js +1 -322
  119. package/dist/index.d.ts +3 -1
  120. package/dist/index.js +4 -1
  121. package/dist/mailbox/dispatch.d.ts +1 -1
  122. package/dist/mailbox/dispatch.js +22 -6
  123. package/dist/mailbox/envelope.d.ts +7 -1
  124. package/dist/mailbox/envelope.js +9 -2
  125. package/dist/mailbox/ipcServer.d.ts +10 -2
  126. package/dist/mailbox/ipcServer.js +163 -13
  127. package/dist/mailbox/store.d.ts +89 -3
  128. package/dist/mailbox/store.js +895 -41
  129. package/dist/material/boundary.js +1 -14
  130. package/dist/material/consumer.js +1 -55
  131. package/dist/material/emit.js +1 -52
  132. package/dist/material/factory.js +1 -25
  133. package/dist/material/index.js +1 -9
  134. package/dist/material/materialArchive.js +1 -466
  135. package/dist/material/materialStore.js +1 -69
  136. package/dist/material/sampling.js +1 -39
  137. package/dist/material/sources.js +1 -33
  138. package/dist/material/watermark.js +1 -76
  139. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  140. package/dist/ops/evolutionGraphProjection.js +315 -0
  141. package/dist/ops/index.d.ts +2 -1
  142. package/dist/ops/index.js +2 -1
  143. package/dist/ops/selfUpdate.d.ts +8 -0
  144. package/dist/ops/selfUpdate.js +24 -8
  145. package/dist/personality/drift.js +1 -106
  146. package/dist/personality/events.js +1 -37
  147. package/dist/personality/evolveOps.js +1 -93
  148. package/dist/personality/index.js +1 -11
  149. package/dist/personality/mutate.js +1 -39
  150. package/dist/personality/pivot.js +1 -22
  151. package/dist/personality/prompt.js +1 -62
  152. package/dist/personality/riskGate.js +1 -80
  153. package/dist/personality/schema.d.ts +16 -16
  154. package/dist/personality/schema.js +1 -119
  155. package/dist/personality/select.js +1 -71
  156. package/dist/personality/stats.js +1 -82
  157. package/dist/personality/store.js +1 -92
  158. package/dist/schema/common.js +1 -17
  159. package/dist/schema/evolutionGraph.d.ts +784 -0
  160. package/dist/schema/evolutionGraph.js +1 -0
  161. package/dist/schema/index.d.ts +1 -0
  162. package/dist/schema/index.js +1 -5
  163. package/dist/schema/material.js +1 -50
  164. package/dist/schema/problem.js +1 -45
  165. package/dist/schema/proofOfWork.js +1 -9
  166. package/dist/schema/signal.d.ts +3 -3
  167. package/dist/schema/signal.js +1 -19
  168. package/dist/signals/curriculum.d.ts +55 -0
  169. package/dist/signals/curriculum.js +1 -0
  170. package/dist/signals/cycleHistoryFromEvents.js +1 -96
  171. package/dist/signals/expand.d.ts +15 -1
  172. package/dist/signals/expand.js +1 -125
  173. package/dist/signals/extractor.d.ts +2 -2
  174. package/dist/signals/extractor.js +1 -57
  175. package/dist/signals/index.d.ts +3 -1
  176. package/dist/signals/index.js +1 -6
  177. package/dist/signals/metaSignals.d.ts +4 -0
  178. package/dist/signals/metaSignals.js +1 -158
  179. package/dist/signals/signalGate.js +1 -40
  180. package/dist/signals/taskDomain.d.ts +22 -0
  181. package/dist/signals/taskDomain.js +1 -0
  182. package/dist/signals/traceSignals.js +1 -123
  183. package/dist/strategy/constraintAblation.js +1 -3074
  184. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  185. package/dist/strategy/constraintAblationPredicates.js +1 -0
  186. package/dist/strategy/experiment.js +1 -63
  187. package/dist/strategy/index.js +1 -3
  188. package/dist/strategy/strategyPoint.js +1 -23
  189. package/dist/trace/index.d.ts +3 -1
  190. package/dist/trace/index.js +3 -1
  191. package/dist/trace/learningTrace.d.ts +209 -0
  192. package/dist/trace/learningTrace.js +276 -0
  193. package/dist/trace/proxyTurns.d.ts +31 -0
  194. package/dist/trace/proxyTurns.js +137 -0
  195. package/dist/verify/sandboxRunner.d.ts +28 -0
  196. package/dist/verify/sandboxRunner.js +218 -19
  197. package/dist/verify/sandboxedValidation.d.ts +9 -0
  198. package/dist/verify/sandboxedValidation.js +113 -13
  199. package/dist/verify/validation.d.ts +11 -1
  200. package/dist/verify/validation.js +31 -0
  201. package/package.json +5 -2
@@ -1,72 +1,516 @@
1
- import { mkdirSync, existsSync, openSync, readSync, closeSync } from 'node:fs';
2
- import { dirname } from 'node:path';
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 forEachJsonlRecord(path, onRecord) {
27
- if (!existsSync(path))
28
- return;
29
- const maxLineBytes = Number.isFinite(Number(process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES']))
30
- && Number(process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES']) > 0
31
- ? Math.floor(Number(process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES']))
32
- : DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES;
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 dropping = false;
38
- const reset = () => { parts = []; lineBytes = 0; dropping = false; };
433
+ let lineNumber = 1;
434
+ let overlong = false;
435
+ const reset = () => { parts = []; lineBytes = 0; overlong = false; };
39
436
  const append = (segment) => {
40
- if (dropping || segment.length === 0)
437
+ if (overlong || segment.length === 0)
41
438
  return;
42
439
  if (lineBytes + segment.length > maxLineBytes) {
43
440
  reset();
44
- dropping = true;
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
- if (dropping || lineBytes === 0) {
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).toString('utf8').trim();
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
- onRecord(JSON.parse(text));
468
+ record = JSON.parse(text);
469
+ }
470
+ catch {
471
+ throw new MailboxJsonlImportError('invalid_json', currentLine, 'invalid JSON');
61
472
  }
62
- catch { /* skip corrupt rows */ }
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
- fd = openSync(path, 'r');
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, null);
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, lastError 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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,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.attempts, e.nextRetryAt, e.ttlAt, e.createdAt, e.updatedAt, e.schemaVersion, e.feedsMaterial ? 1 : 0);
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) => ({ ...rowToEnvelope(r), status: 'in_flight' }));
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
- /** v1 messages.jsonl → sqlite 迁移(幂等可重入). */
348
- importJsonl(path) {
349
- let n = 0;
350
- forEachJsonlRecord(path, (record) => {
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
- if (this.send(record).stored)
353
- n += 1;
1112
+ this.db.exec('ROLLBACK');
1113
+ }
1114
+ catch {
1115
+ // Preserve the primary transaction error.
354
1116
  }
355
- catch { /* skip */ }
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
- return n;
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
  }