@evomap/evolver-core 2.0.0-beta.18 → 2.0.0-beta.19

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 (68) hide show
  1. package/dist/algo/candidateAssembly.js +21 -2
  2. package/dist/algo/cycleEngine.d.ts +12 -0
  3. package/dist/algo/cycleEngine.js +36 -4
  4. package/dist/algo/geneHealth.d.ts +2 -2
  5. package/dist/algo/geneHealth.js +5 -4
  6. package/dist/algo/geneSelection.d.ts +1 -1
  7. package/dist/algo/orchestrator.js +9 -2
  8. package/dist/assetstore/assetSidecarRecords.js +4 -0
  9. package/dist/assetstore/assetStoreHealth.js +41 -24
  10. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  11. package/dist/assetstore/assetStoreStorage.js +16 -7
  12. package/dist/assetstore/localJsonl.d.ts +2 -1
  13. package/dist/assetstore/localJsonl.js +54 -10
  14. package/dist/assetstore/provenance.d.ts +24 -0
  15. package/dist/assetstore/provenance.js +219 -12
  16. package/dist/assetstore/provider.d.ts +20 -1
  17. package/dist/assetstore/provider.js +34 -1
  18. package/dist/bootstrap/index.d.ts +2 -1
  19. package/dist/bootstrap/index.js +2 -1
  20. package/dist/bootstrap/v1EnvCompat.d.ts +110 -0
  21. package/dist/bootstrap/v1EnvCompat.js +256 -0
  22. package/dist/events/public.d.ts +1 -1
  23. package/dist/events/public.js +1 -1
  24. package/dist/events/reports.d.ts +2 -0
  25. package/dist/events/reports.js +4 -0
  26. package/dist/exec/autoExec.d.ts +1 -1
  27. package/dist/exec/autoExec.js +8 -10
  28. package/dist/exec/autonomousCycle.d.ts +19 -4
  29. package/dist/exec/autonomousCycle.js +63 -13
  30. package/dist/exec/claudeBridge.d.ts +25 -7
  31. package/dist/exec/claudeBridge.js +264 -29
  32. package/dist/exec/prompt.js +5 -1
  33. package/dist/exec/runnerRegistry.d.ts +68 -26
  34. package/dist/exec/runnerRegistry.js +307 -72
  35. package/dist/exec/selfPr.js +1 -7
  36. package/dist/feedback/envelope.d.ts +61 -0
  37. package/dist/feedback/envelope.js +168 -0
  38. package/dist/feedback/index.d.ts +1 -0
  39. package/dist/feedback/index.js +1 -0
  40. package/dist/hub/assetCallLog.d.ts +35 -1
  41. package/dist/hub/assetCallLog.js +124 -1
  42. package/dist/hub/bindings.d.ts +8 -1
  43. package/dist/hub/bindings.js +17 -6
  44. package/dist/hub/capability.d.ts +11 -1
  45. package/dist/hub/fake.d.ts +2 -2
  46. package/dist/hub/fake.js +1 -1
  47. package/dist/index.d.ts +3 -1
  48. package/dist/index.js +4 -1
  49. package/dist/mailbox/dispatch.d.ts +1 -1
  50. package/dist/mailbox/dispatch.js +22 -6
  51. package/dist/mailbox/envelope.d.ts +7 -1
  52. package/dist/mailbox/envelope.js +9 -2
  53. package/dist/mailbox/ipcServer.d.ts +10 -2
  54. package/dist/mailbox/ipcServer.js +163 -13
  55. package/dist/mailbox/store.d.ts +38 -2
  56. package/dist/mailbox/store.js +416 -27
  57. package/dist/signals/curriculum.d.ts +55 -0
  58. package/dist/signals/curriculum.js +202 -0
  59. package/dist/signals/expand.js +17 -6
  60. package/dist/signals/index.d.ts +2 -1
  61. package/dist/signals/index.js +2 -1
  62. package/dist/strategy/constraintAblation.js +115 -369
  63. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  64. package/dist/strategy/constraintAblationPredicates.js +339 -0
  65. package/dist/trace/proxyTurns.js +15 -7
  66. package/dist/verify/validation.d.ts +11 -1
  67. package/dist/verify/validation.js +31 -0
  68. package/package.json +4 -1
@@ -2,29 +2,163 @@ import { mkdirSync, existsSync, openSync, readSync, closeSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { ulid as makeUlid } from 'ulid';
5
- import { createEnvelope } from './envelope.js';
5
+ import { createEnvelope, ENVELOPE_SCHEMA_VERSION, normalizePriority, sanitizePayload, specForType, } from './envelope.js';
6
6
  const nodeRequire = createRequire(import.meta.url);
7
7
  function isBunRuntime() {
8
8
  return typeof process.versions === 'object' && typeof process.versions.bun === 'string';
9
9
  }
10
- function openSqliteDatabase(path) {
10
+ function openSqliteDatabase(path, options = {}) {
11
11
  if (isBunRuntime()) {
12
12
  const { Database } = nodeRequire('bun:sqlite');
13
- return new Database(path);
13
+ return options.readOnly ? new Database(path, { readonly: true }) : new Database(path);
14
14
  }
15
15
  // node:sqlite is exposed only as `node:sqlite` (no bare `sqlite` alias). Load it through createRequire so
16
16
  // bundlers do not statically strip the prefix and break Vitest/Vite or Bun standalone builds.
17
17
  const { DatabaseSync } = nodeRequire('node:sqlite');
18
- return new DatabaseSync(path);
18
+ return options.readOnly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
19
19
  }
20
20
  export const MAX_ATTEMPTS = 5;
21
21
  export const DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES = 16 * 1024 * 1024;
22
22
  const JSONL_READ_CHUNK_BYTES = 64 * 1024;
23
+ export const MAILBOX_CLAIM_OWNER = Symbol('mailboxClaimOwner');
24
+ export function mailboxClaimOwner(envelope) {
25
+ return envelope[MAILBOX_CLAIM_OWNER];
26
+ }
23
27
  export function expBackoffMs(attempt) {
24
28
  return Math.min(4000 * 2 ** (attempt - 1), 5 * 60 * 1000); // 4s→8s→…→5min
25
29
  }
26
- function forEachJsonlRecord(path, onRecord) {
27
- if (!existsSync(path))
30
+ function asRow(value) {
31
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
32
+ }
33
+ /** Read one mailbox state value without creating, migrating, or taking write ownership of the database. */
34
+ export function readMailboxState(path, key) {
35
+ const database = openSqliteDatabase(path, { readOnly: true });
36
+ try {
37
+ const row = database.prepare('SELECT v FROM kv WHERE k = ?').get(key);
38
+ return typeof row?.['v'] === 'string' ? row['v'] : undefined;
39
+ }
40
+ finally {
41
+ database.close();
42
+ }
43
+ }
44
+ function nonEmptyString(value) {
45
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
46
+ }
47
+ function finiteNumber(value) {
48
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
49
+ }
50
+ function nullableTimestamp(value) {
51
+ if (value === null)
52
+ return null;
53
+ return finiteNumber(value);
54
+ }
55
+ function timestampField(record, ...keys) {
56
+ for (const key of keys) {
57
+ if (Object.prototype.hasOwnProperty.call(record, key))
58
+ return nullableTimestamp(record[key]);
59
+ }
60
+ return undefined;
61
+ }
62
+ function nullableStringField(record, ...keys) {
63
+ for (const key of keys) {
64
+ if (!Object.prototype.hasOwnProperty.call(record, key))
65
+ continue;
66
+ const value = record[key];
67
+ if (value === null)
68
+ return null;
69
+ if (typeof value === 'string')
70
+ return value;
71
+ return undefined;
72
+ }
73
+ return undefined;
74
+ }
75
+ function importedStatus(value) {
76
+ switch (value) {
77
+ case 'pending':
78
+ case 'in_flight':
79
+ case 'done':
80
+ case 'failed':
81
+ case 'expired':
82
+ return value;
83
+ case 'synced':
84
+ case 'delivered':
85
+ return 'done';
86
+ case 'rejected':
87
+ return 'failed';
88
+ default:
89
+ return undefined;
90
+ }
91
+ }
92
+ function recoverImportedStatus(type, requested) {
93
+ if (!specForType(type)) {
94
+ return requested === 'pending' || requested === 'in_flight' ? 'failed' : requested;
95
+ }
96
+ // A V1 worker and its lease cannot survive migration. Requeue in-flight work instead of creating a V2 row
97
+ // with status=in_flight and leasedUntil=NULL, which SQLite will never consider expired or claimable.
98
+ return requested === 'in_flight' ? 'pending' : requested;
99
+ }
100
+ function normalizeImportedEnvelope(record) {
101
+ const id = nonEmptyString(record['id']);
102
+ const type = nonEmptyString(record['type']);
103
+ if (!id || !type)
104
+ return undefined;
105
+ const createdAt = finiteNumber(record['createdAt']) ?? finiteNumber(record['created_at']) ?? Date.now();
106
+ const spec = specForType(type);
107
+ const directionValue = record['direction'];
108
+ const direction = directionValue === 'outbound' || directionValue === 'inbound' || directionValue === 'local'
109
+ ? directionValue
110
+ : spec?.direction;
111
+ if (!direction)
112
+ return undefined;
113
+ const handlerValue = record['handler'];
114
+ const handler = handlerValue === 'core' || handlerValue === 'proxy' || handlerValue === 'agent'
115
+ ? handlerValue
116
+ : spec?.handler ?? (direction === 'outbound' ? 'proxy' : 'agent');
117
+ const originalStatus = importedStatus(record['status']) ?? 'pending';
118
+ // Preserve unknown V1 types for audit without dispatching a contract that V2 cannot handle.
119
+ const status = recoverImportedStatus(type, originalStatus);
120
+ // V1 `channel` names a transport (normally `evomap-hub`); V2 runtimeNamespace is an isolation boundary.
121
+ const runtimeNamespace = nonEmptyString(record['runtimeNamespace']) ?? 'default';
122
+ const correlationId = nonEmptyString(record['correlationId'])
123
+ ?? nonEmptyString(record['correlation_id'])
124
+ ?? nonEmptyString(record['ref_id'])
125
+ ?? id;
126
+ const attempts = Math.max(0, Math.floor(finiteNumber(record['attempts']) ?? finiteNumber(record['retry_count']) ?? 0));
127
+ const importedTtlAt = timestampField(record, 'ttlAt', 'expires_at');
128
+ const ttlAt = importedTtlAt === undefined
129
+ ? (spec?.ttlClass === 'control' ? createdAt + 60 * 60 * 1000 : createdAt + 7 * 24 * 60 * 60 * 1000)
130
+ : importedTtlAt;
131
+ const updatedAt = finiteNumber(record['updatedAt'])
132
+ ?? finiteNumber(record['updated_at'])
133
+ ?? finiteNumber(record['synced_at'])
134
+ ?? createdAt;
135
+ return {
136
+ id,
137
+ type,
138
+ direction,
139
+ status,
140
+ handler,
141
+ payload: sanitizePayload(record['payload'] ?? {}),
142
+ correlationId,
143
+ replyTo: nonEmptyString(record['replyTo']) ?? nonEmptyString(record['reply_to']) ?? null,
144
+ receiptId: nonEmptyString(record['receiptId']) ?? nonEmptyString(record['receipt_id']) ?? id,
145
+ idempotencyKey: nonEmptyString(record['idempotencyKey']) ?? nonEmptyString(record['idempotency_key']) ?? id,
146
+ sourceAgent: nonEmptyString(record['sourceAgent']) ?? nonEmptyString(record['source_agent']) ?? '',
147
+ targetAgent: nonEmptyString(record['targetAgent']) ?? nonEmptyString(record['target_agent']) ?? '',
148
+ runtimeNamespace,
149
+ priority: normalizePriority(record['priority']),
150
+ attempts,
151
+ nextRetryAt: timestampField(record, 'nextRetryAt', 'next_retry_at') ?? null,
152
+ ttlAt,
153
+ createdAt,
154
+ updatedAt,
155
+ schemaVersion: nonEmptyString(record['schemaVersion']) ?? ENVELOPE_SCHEMA_VERSION,
156
+ feedsMaterial: typeof record['feedsMaterial'] === 'boolean' ? record['feedsMaterial'] : spec?.feedsMaterial ?? false,
157
+ lastError: nullableStringField(record, 'lastError', 'last_error', 'error') ?? null,
158
+ };
159
+ }
160
+ function forEachJsonlRecord(source, onRecord) {
161
+ if (typeof source === 'string' && !existsSync(source))
28
162
  return;
29
163
  const maxLineBytes = Number.isFinite(Number(process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES']))
30
164
  && Number(process.env['EVOLVER_IMPORT_JSONL_MAX_LINE_BYTES']) > 0
@@ -32,6 +166,8 @@ function forEachJsonlRecord(path, onRecord) {
32
166
  : DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES;
33
167
  const buf = Buffer.allocUnsafe(JSONL_READ_CHUNK_BYTES);
34
168
  let fd;
169
+ let ownsFd = false;
170
+ let position = 0;
35
171
  let parts = [];
36
172
  let lineBytes = 0;
37
173
  let dropping = false;
@@ -56,17 +192,28 @@ function forEachJsonlRecord(path, onRecord) {
56
192
  reset();
57
193
  if (!text)
58
194
  return;
195
+ let record;
59
196
  try {
60
- onRecord(JSON.parse(text));
197
+ record = JSON.parse(text);
61
198
  }
62
- catch { /* skip corrupt rows */ }
199
+ catch {
200
+ return; /* skip corrupt JSON */
201
+ }
202
+ onRecord(record);
63
203
  };
64
204
  try {
65
- fd = openSync(path, 'r');
205
+ if (typeof source === 'number') {
206
+ fd = source;
207
+ }
208
+ else {
209
+ fd = openSync(source, 'r');
210
+ ownsFd = true;
211
+ }
66
212
  for (;;) {
67
- const bytes = readSync(fd, buf, 0, buf.length, null);
213
+ const bytes = readSync(fd, buf, 0, buf.length, position);
68
214
  if (bytes <= 0)
69
215
  break;
216
+ position += bytes;
70
217
  let start = 0;
71
218
  for (let i = 0; i < bytes; i += 1) {
72
219
  if (buf[i] !== 0x0a)
@@ -81,7 +228,7 @@ function forEachJsonlRecord(path, onRecord) {
81
228
  finish();
82
229
  }
83
230
  finally {
84
- if (fd !== undefined)
231
+ if (ownsFd && fd !== undefined)
85
232
  closeSync(fd);
86
233
  }
87
234
  }
@@ -94,12 +241,20 @@ function rowToEnvelope(r) {
94
241
  receiptId: r['receiptId'], idempotencyKey: r['idempotencyKey'],
95
242
  sourceAgent: r['sourceAgent'], targetAgent: r['targetAgent'],
96
243
  runtimeNamespace: r['runtimeNamespace'],
244
+ priority: normalizePriority(r['priority']),
97
245
  attempts: r['attempts'], nextRetryAt: r['nextRetryAt'] ?? null,
98
246
  ttlAt: r['ttlAt'] ?? null,
99
247
  createdAt: r['createdAt'], updatedAt: r['updatedAt'],
100
248
  schemaVersion: r['schemaVersion'], feedsMaterial: Boolean(r['feedsMaterial']),
249
+ lastError: r['lastError'] ?? null,
101
250
  };
102
251
  }
252
+ function ensureMessageColumn(db, name, definition) {
253
+ const columns = db.prepare('PRAGMA table_info(messages)').all();
254
+ if (!columns.some((column) => column['name'] === name)) {
255
+ db.exec(`ALTER TABLE messages ADD COLUMN ${definition}`);
256
+ }
257
+ }
103
258
  /** mailbox sqlite 引擎 (WAL + busy_timeout). 状态机 pending→in_flight→done/failed/expired + 租约 + 重试/DLQ. */
104
259
  export class MailboxStore {
105
260
  db;
@@ -115,14 +270,19 @@ export class MailboxStore {
115
270
  this.db.exec(`CREATE TABLE IF NOT EXISTS messages (
116
271
  id TEXT PRIMARY KEY, type TEXT, direction TEXT, status TEXT, handler TEXT,
117
272
  payload TEXT, correlationId TEXT, replyTo TEXT, receiptId TEXT, idempotencyKey TEXT,
118
- sourceAgent TEXT, targetAgent TEXT, runtimeNamespace TEXT,
273
+ sourceAgent TEXT, targetAgent TEXT, runtimeNamespace TEXT, priority TEXT NOT NULL DEFAULT 'normal',
119
274
  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)`);
275
+ schemaVersion TEXT, feedsMaterial INTEGER, dlq INTEGER DEFAULT 0, leasedUntil INTEGER, workerId TEXT,
276
+ lastError TEXT, claimCheckpoint TEXT)`);
277
+ ensureMessageColumn(this.db, 'priority', "priority TEXT NOT NULL DEFAULT 'normal'");
278
+ ensureMessageColumn(this.db, 'lastError', 'lastError TEXT');
279
+ ensureMessageColumn(this.db, 'claimCheckpoint', 'claimCheckpoint TEXT');
121
280
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_status ON messages(status, handler)');
122
281
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_corr ON messages(correlationId)');
123
282
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_message_view ON messages(runtimeNamespace,type,direction,status,createdAt)');
124
283
  this.db.exec('CREATE TABLE IF NOT EXISTS idempotency (key TEXT PRIMARY KEY, result TEXT, at INTEGER)');
125
284
  this.db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)');
285
+ this.db.exec('CREATE TABLE IF NOT EXISTS v1_import_state (id TEXT PRIMARY KEY, snapshot TEXT NOT NULL)');
126
286
  }
127
287
  /** 通用 KV 状态(M6: sync 游标 inbound_cursor / lifecycle reauth 退避 / node_id 等). */
128
288
  getState(key) {
@@ -135,8 +295,8 @@ export class MailboxStore {
135
295
  /** 投递; 同 id 幂等(OR IGNORE). 返回 receiptId. */
136
296
  send(e) {
137
297
  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);
298
+ (id,type,direction,status,handler,payload,correlationId,replyTo,receiptId,idempotencyKey,sourceAgent,targetAgent,runtimeNamespace,priority,attempts,nextRetryAt,ttlAt,createdAt,updatedAt,schemaVersion,feedsMaterial,lastError,dlq)
299
+ 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
300
  return { receiptId: e.id, stored: Number(r.changes) > 0 };
141
301
  }
142
302
  getById(id) {
@@ -253,13 +413,17 @@ export class MailboxStore {
253
413
  const rows = this.db.prepare(`SELECT * FROM messages WHERE handler=? AND dlq=0
254
414
  AND (status='pending' OR (status='in_flight' AND leasedUntil < ?))
255
415
  AND (nextRetryAt IS NULL OR nextRetryAt <= ?)${nsClause}
256
- ORDER BY createdAt LIMIT ?`).all(handler, now, now, ...nsArgs, limit);
416
+ ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'low' THEN 2 ELSE 1 END, createdAt LIMIT ?`).all(handler, now, now, ...nsArgs, limit);
257
417
  const workerId = makeUlid();
258
418
  const upd = this.db.prepare(`UPDATE messages SET status='in_flight', leasedUntil=?, workerId=?, updatedAt=? WHERE id=?`);
259
419
  for (const r of rows)
260
420
  upd.run(now + leaseMs, workerId, now, r['id']);
261
421
  this.db.exec('COMMIT');
262
- return rows.map((r) => ({ ...rowToEnvelope(r), status: 'in_flight' }));
422
+ return rows.map((r) => ({
423
+ ...rowToEnvelope(r),
424
+ status: 'in_flight',
425
+ [MAILBOX_CLAIM_OWNER]: workerId,
426
+ }));
263
427
  }
264
428
  catch (e) {
265
429
  this.db.exec('ROLLBACK');
@@ -267,7 +431,7 @@ export class MailboxStore {
267
431
  }
268
432
  }
269
433
  complete(id, now) {
270
- this.db.prepare(`UPDATE messages SET status='done', leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`).run(now, id);
434
+ this.db.prepare(`UPDATE messages SET status='done', dlq=0, lastError=NULL, leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`).run(now, id);
271
435
  }
272
436
  /** 失败: attempts<N→退避回 pending; ≥N→DLQ(不自动丢). */
273
437
  fail(id, err, now, maxAttempts = MAX_ATTEMPTS) {
@@ -289,9 +453,101 @@ export class MailboxStore {
289
453
  this.db.prepare(`UPDATE messages SET status='pending', nextRetryAt=?, lastError=?, leasedUntil=NULL, workerId=NULL, updatedAt=? WHERE id=?`)
290
454
  .run(now + delay, err, now, id);
291
455
  }
456
+ deferUnlessProcessed(id, processedKey, err, now, retryAfterMs) {
457
+ return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'pending', () => this.defer(id, err, now, retryAfterMs));
458
+ }
459
+ failUnlessProcessed(id, processedKey, err, now, maxAttempts = 5) {
460
+ return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'pending', () => this.fail(id, err, now, maxAttempts));
461
+ }
462
+ completeClaimed(id, now, claimOwner) {
463
+ return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.complete(id, now), claimOwner);
464
+ }
465
+ /** Atomically complete the current worker's lease and persist its idempotency result. */
466
+ completeClaimedAndMarkProcessed(id, processedKey, result, now, claimOwner) {
467
+ return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => {
468
+ this.markProcessed(processedKey, result, now);
469
+ this.complete(id, now);
470
+ }, claimOwner);
471
+ }
472
+ renewClaim(id, now, leaseMs, claimOwner) {
473
+ const result = this.db.prepare(`UPDATE messages SET leasedUntil=?, updatedAt=?
474
+ WHERE id=? AND status='in_flight' AND workerId=?`).run(now + leaseMs, now, id, claimOwner);
475
+ return result.changes === 1;
476
+ }
477
+ setClaimedCheckpoint(id, checkpoint, now, claimOwner) {
478
+ const encoded = JSON.stringify(checkpoint);
479
+ return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => {
480
+ this.db.prepare('UPDATE messages SET claimCheckpoint=?, updatedAt=? WHERE id=?').run(encoded, now, id);
481
+ }, claimOwner);
482
+ }
483
+ getClaimedCheckpoint(id) {
484
+ const row = this.db.prepare('SELECT claimCheckpoint FROM messages WHERE id=?').get(id);
485
+ const encoded = row?.['claimCheckpoint'];
486
+ if (typeof encoded !== 'string')
487
+ return undefined;
488
+ try {
489
+ return JSON.parse(encoded);
490
+ }
491
+ catch {
492
+ return undefined;
493
+ }
494
+ }
495
+ deferClaimed(id, err, now, retryAfterMs, claimOwner) {
496
+ return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.defer(id, err, now, retryAfterMs), claimOwner);
497
+ }
498
+ failClaimed(id, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
499
+ return this.transitionUnlessProcessed(id, [], (current) => current.status === 'in_flight', () => this.fail(id, err, now, maxAttempts), claimOwner);
500
+ }
501
+ /** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
502
+ deferClaimedUnlessProcessed(id, processedKey, err, now, retryAfterMs, claimOwner) {
503
+ return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'in_flight', () => this.defer(id, err, now, retryAfterMs), claimOwner);
504
+ }
505
+ /** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
506
+ failClaimedUnlessProcessed(id, processedKey, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
507
+ return this.transitionUnlessProcessed(id, [processedKey], (current) => current.status === 'in_flight', () => this.fail(id, err, now, maxAttempts), claimOwner);
508
+ }
509
+ /** Atomically fail a non-leased intent and persist the replayable terminal result. */
510
+ failAndMarkProcessedUnlessProcessed(id, blockingProcessedKeys, resultKey, result, err, now, maxAttempts = MAX_ATTEMPTS) {
511
+ return this.transitionUnlessProcessed(id, blockingProcessedKeys, (current) => current.status === 'pending', () => {
512
+ this.fail(id, err, now, maxAttempts);
513
+ this.markProcessed(resultKey, result, now);
514
+ });
515
+ }
516
+ /** Atomically fail the current worker's leased intent and persist the replayable terminal result. */
517
+ failClaimedAndMarkProcessedUnlessProcessed(id, blockingProcessedKeys, resultKey, result, err, now, claimOwner, maxAttempts = MAX_ATTEMPTS) {
518
+ return this.transitionUnlessProcessed(id, blockingProcessedKeys, (current) => current.status === 'in_flight', () => {
519
+ this.fail(id, err, now, maxAttempts);
520
+ this.markProcessed(resultKey, result, now);
521
+ }, claimOwner);
522
+ }
523
+ transitionUnlessProcessed(id, processedKeys, isEligible, transition, expectedClaimOwner) {
524
+ this.db.exec('BEGIN IMMEDIATE');
525
+ try {
526
+ const current = this.getById(id);
527
+ if (processedKeys.some((key) => this.isProcessed(key))
528
+ || !current
529
+ || !isEligible(current)
530
+ || (expectedClaimOwner !== undefined && this.messageClaimOwner(id) !== expectedClaimOwner)) {
531
+ this.db.exec('COMMIT');
532
+ return false;
533
+ }
534
+ transition();
535
+ this.db.exec('COMMIT');
536
+ return true;
537
+ }
538
+ catch (error) {
539
+ try {
540
+ this.db.exec('ROLLBACK');
541
+ }
542
+ catch {
543
+ // Preserve the primary transaction error.
544
+ }
545
+ throw error;
546
+ }
547
+ }
292
548
  /** DLQ 重放(人工/agent 显式, 不自动丢). */
293
549
  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);
550
+ 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
551
  }
296
552
  dlq() { return this.db.prepare('SELECT * FROM messages WHERE dlq=1').all().map(rowToEnvelope); }
297
553
  /** TTL 扫描: ttlAt 过期且未 done/dlq → expired. */
@@ -326,13 +582,15 @@ export class MailboxStore {
326
582
  }
327
583
  /** M2-5 轻量状态视图(运维/IPC 查询用). */
328
584
  getStatus(id) {
329
- const r = this.db.prepare('SELECT id,type,status,attempts,nextRetryAt,ttlAt,dlq FROM messages WHERE id=?').get(id);
585
+ const r = this.db.prepare('SELECT id,type,status,priority,attempts,nextRetryAt,ttlAt,dlq,lastError FROM messages WHERE id=?').get(id);
330
586
  if (!r)
331
587
  return undefined;
332
588
  return {
333
589
  id: r['id'], type: r['type'], status: r['status'],
590
+ priority: normalizePriority(r['priority']),
334
591
  attempts: Number(r['attempts']), nextRetryAt: r['nextRetryAt'] ?? null,
335
592
  ttlAt: r['ttlAt'] ?? null, dlq: Number(r['dlq']) === 1,
593
+ lastError: r['lastError'] ?? null,
336
594
  };
337
595
  }
338
596
  /** 幂等(A13): 副作用 handler 用 idempotencyKey 去重; 命中返缓存不重跑. */
@@ -344,17 +602,148 @@ export class MailboxStore {
344
602
  markProcessed(key, result, now) {
345
603
  this.db.prepare('INSERT OR IGNORE INTO idempotency (key,result,at) VALUES (?,?,?)').run(key, JSON.stringify(result ?? null), now);
346
604
  }
605
+ replaceProcessed(key, result, now) {
606
+ this.db.prepare(`INSERT INTO idempotency (key,result,at) VALUES (?,?,?)
607
+ ON CONFLICT(key) DO UPDATE SET result=excluded.result, at=excluded.at`).run(key, JSON.stringify(result ?? null), now);
608
+ }
609
+ /** Persist a monotonic outcome and its lightweight concurrency marker in one SQLite transaction. */
610
+ replaceProcessedWithMarker(key, result, markerKey, markerResult, now) {
611
+ this.db.exec('BEGIN IMMEDIATE');
612
+ try {
613
+ this.replaceProcessed(key, result, now);
614
+ this.replaceProcessed(markerKey, markerResult, now);
615
+ this.db.exec('COMMIT');
616
+ }
617
+ catch (error) {
618
+ try {
619
+ this.db.exec('ROLLBACK');
620
+ }
621
+ catch {
622
+ // Preserve the primary transaction error.
623
+ }
624
+ throw error;
625
+ }
626
+ }
627
+ /** Backfill a concurrency marker only when the durable source result still satisfies the caller's predicate. */
628
+ markProcessedIf(sourceKey, markerKey, markerResult, now, matches) {
629
+ this.db.exec('BEGIN IMMEDIATE');
630
+ try {
631
+ const sourceResult = this.getProcessed(sourceKey);
632
+ if (sourceResult === undefined || !matches(sourceResult)) {
633
+ this.db.exec('COMMIT');
634
+ return false;
635
+ }
636
+ this.markProcessed(markerKey, markerResult, now);
637
+ this.db.exec('COMMIT');
638
+ return true;
639
+ }
640
+ catch (error) {
641
+ try {
642
+ this.db.exec('ROLLBACK');
643
+ }
644
+ catch {
645
+ // Preserve the primary transaction error.
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ deleteProcessed(keys) {
651
+ if (keys.length === 0)
652
+ return;
653
+ this.db.exec('BEGIN IMMEDIATE');
654
+ try {
655
+ const remove = this.db.prepare('DELETE FROM idempotency WHERE key=?');
656
+ for (const key of keys)
657
+ remove.run(key);
658
+ this.db.exec('COMMIT');
659
+ }
660
+ catch (error) {
661
+ try {
662
+ this.db.exec('ROLLBACK');
663
+ }
664
+ catch {
665
+ // Preserve the primary transaction error.
666
+ }
667
+ throw error;
668
+ }
669
+ }
670
+ messageClaimOwner(id) {
671
+ const row = this.db.prepare('SELECT workerId FROM messages WHERE id=?').get(id);
672
+ return typeof row?.['workerId'] === 'string' ? row['workerId'] : undefined;
673
+ }
347
674
  /** v1 messages.jsonl → sqlite 迁移(幂等可重入). */
348
- importJsonl(path) {
675
+ importJsonl(source) {
349
676
  let n = 0;
350
- forEachJsonlRecord(path, (record) => {
351
- try {
352
- if (this.send(record).stored)
677
+ const eligibleIds = new Set();
678
+ const readImportState = this.db.prepare('SELECT snapshot FROM v1_import_state WHERE id=?');
679
+ const writeImportState = this.db.prepare('INSERT INTO v1_import_state (id,snapshot) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET snapshot=excluded.snapshot');
680
+ this.db.exec('BEGIN IMMEDIATE');
681
+ try {
682
+ forEachJsonlRecord(source, (record) => {
683
+ const row = asRow(record);
684
+ if (!row)
685
+ return;
686
+ if (row['_op'] === 'update') {
687
+ const id = nonEmptyString(row['id']);
688
+ const fields = asRow(row['fields']);
689
+ if (id && fields && eligibleIds.has(id))
690
+ this.applyImportedV1Update(id, fields);
691
+ return;
692
+ }
693
+ const envelope = normalizeImportedEnvelope(row);
694
+ if (!envelope)
695
+ return;
696
+ const inserted = this.send(envelope).stored;
697
+ if (inserted) {
353
698
  n += 1;
699
+ eligibleIds.add(envelope.id);
700
+ return;
701
+ }
702
+ const baseline = readImportState.get(envelope.id)?.['snapshot'];
703
+ const current = this.getById(envelope.id);
704
+ if (typeof baseline === 'string' && current && JSON.stringify(current) === baseline) {
705
+ eligibleIds.add(envelope.id);
706
+ }
707
+ });
708
+ for (const id of eligibleIds) {
709
+ const current = this.getById(id);
710
+ if (current)
711
+ writeImportState.run(id, JSON.stringify(current));
354
712
  }
355
- catch { /* skip */ }
356
- });
357
- return n;
713
+ this.db.exec('COMMIT');
714
+ return n;
715
+ }
716
+ catch (error) {
717
+ this.db.exec('ROLLBACK');
718
+ throw error;
719
+ }
720
+ }
721
+ applyImportedV1Update(id, fields) {
722
+ const current = this.getById(id);
723
+ if (!current)
724
+ return;
725
+ const requestedStatus = importedStatus(fields['status']) ?? current.status;
726
+ const status = recoverImportedStatus(current.type, requestedStatus);
727
+ const attempts = Math.max(0, Math.floor(finiteNumber(fields['attempts']) ?? finiteNumber(fields['retry_count']) ?? current.attempts));
728
+ const importedNextRetryAt = timestampField(fields, 'nextRetryAt', 'next_retry_at');
729
+ const nextRetryAt = importedNextRetryAt === undefined ? current.nextRetryAt : importedNextRetryAt;
730
+ const importedTtlAt = timestampField(fields, 'ttlAt', 'expires_at');
731
+ const ttlAt = importedTtlAt === undefined ? current.ttlAt : importedTtlAt;
732
+ const updatedAt = finiteNumber(fields['updatedAt'])
733
+ ?? finiteNumber(fields['updated_at'])
734
+ ?? finiteNumber(fields['synced_at'])
735
+ ?? current.updatedAt;
736
+ const payload = Object.prototype.hasOwnProperty.call(fields, 'payload')
737
+ ? sanitizePayload(fields['payload'])
738
+ : current.payload;
739
+ const priority = Object.prototype.hasOwnProperty.call(fields, 'priority')
740
+ ? normalizePriority(fields['priority'])
741
+ : current.priority;
742
+ const importedLastError = nullableStringField(fields, 'lastError', 'last_error', 'error');
743
+ const lastError = importedLastError === undefined ? current.lastError : importedLastError;
744
+ this.db.prepare(`UPDATE messages
745
+ SET status=?, payload=?, priority=?, attempts=?, nextRetryAt=?, ttlAt=?, updatedAt=?, lastError=?
746
+ WHERE id=?`).run(status, JSON.stringify(payload), priority, attempts, nextRetryAt, ttlAt, updatedAt, lastError, id);
358
747
  }
359
748
  close() { this.db.close(); }
360
749
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Progressive curriculum signals, adapted from V1's curriculum producer.
3
+ *
4
+ * V2 keeps the policy pure and derives its history from the append-only root event log. This avoids V1's
5
+ * separate mutable curriculum_state.json sidecar while preserving the behavior that matters to selection:
6
+ * classify recent outcomes and add at most one capability-gap target plus one frontier target.
7
+ */
8
+ export interface CurriculumOutcome {
9
+ key: string;
10
+ status: 'success' | 'failed';
11
+ }
12
+ export interface CurriculumBucket {
13
+ key: string;
14
+ success: number;
15
+ failed: number;
16
+ total: number;
17
+ rate: number;
18
+ }
19
+ export interface CurriculumClassification {
20
+ mastered: CurriculumBucket[];
21
+ failing: CurriculumBucket[];
22
+ frontier: CurriculumBucket[];
23
+ }
24
+ export interface CurriculumEventLike {
25
+ type: string;
26
+ payload?: unknown;
27
+ }
28
+ export interface GenerateCurriculumSignalsInput {
29
+ outcomes: readonly CurriculumOutcome[];
30
+ capabilityGaps?: readonly string[];
31
+ }
32
+ export declare const MAX_CAPABILITY_GAPS = 16;
33
+ export declare const CAPABILITY_GAPS_STATE_KEY = "curriculum:capability_gaps";
34
+ /** Normalize untrusted capability names before they cross adapter, persistence, or selection boundaries. */
35
+ export declare function normalizeCapabilityGaps(value: unknown): string[];
36
+ /** Versioned, bounded snapshot stored atomically in the lifecycle mailbox KV table. */
37
+ export declare function serializeCapabilityGapsState(capabilityGaps: unknown, observedAt: number): string;
38
+ /** Parse only the current bounded snapshot format. Unknown versions fail open without influencing selection. */
39
+ export declare function capabilityGapsFromState(value: unknown): string[];
40
+ /** Stable V2 equivalent of V1 computeSignalKey, excluding previously generated curriculum targets. */
41
+ export declare function curriculumSignalKey(signals: readonly string[]): string;
42
+ /** Classify signal-key outcomes using V1's thresholds. */
43
+ export declare function classifyCurriculumOutcomes(outcomes: readonly CurriculumOutcome[]): CurriculumClassification;
44
+ /** Generate no more than the two target families V1 produced: gap first, then closest frontier. */
45
+ export declare function generateCurriculumSignals(input: GenerateCurriculumSignalsInput): string[];
46
+ /**
47
+ * Map V2's capability signal conventions to concrete curriculum gaps. A bare cap:* tag is not sufficient by
48
+ * itself: it becomes a curriculum gap only when the same signal set explicitly declares capability_gap.
49
+ */
50
+ export declare function capabilityGapsFromSignals(signals: readonly string[]): string[];
51
+ /**
52
+ * Join cycle.signals_collected to terminal cycle events and retain the latest outcome window. `baseSignals`
53
+ * wins over `signals`, so history-derived meta-signals do not become a self-reinforcing curriculum key.
54
+ */
55
+ export declare function curriculumOutcomesFromEvents(events: readonly CurriculumEventLike[], maxOutcomes?: number): CurriculumOutcome[];