@evomap/evolver-core 2.0.0-beta.17 → 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.
- package/dist/algo/candidateAssembly.js +21 -2
- package/dist/algo/cycleEngine.d.ts +12 -0
- package/dist/algo/cycleEngine.js +36 -4
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +5 -4
- package/dist/algo/geneSelection.d.ts +1 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/assetstore/assetSidecarRecords.js +4 -0
- package/dist/assetstore/assetStoreHealth.js +41 -24
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +16 -7
- package/dist/assetstore/localJsonl.d.ts +2 -1
- package/dist/assetstore/localJsonl.js +54 -10
- package/dist/assetstore/provenance.d.ts +24 -0
- package/dist/assetstore/provenance.js +219 -12
- package/dist/assetstore/provider.d.ts +20 -1
- package/dist/assetstore/provider.js +34 -1
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +110 -0
- package/dist/bootstrap/v1EnvCompat.js +256 -0
- package/dist/events/public.d.ts +1 -1
- package/dist/events/public.js +1 -1
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +18 -1
- package/dist/exec/autoExec.js +24 -9
- package/dist/exec/autonomousCycle.d.ts +19 -4
- package/dist/exec/autonomousCycle.js +63 -13
- package/dist/exec/claudeBridge.d.ts +25 -7
- package/dist/exec/claudeBridge.js +264 -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/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +17 -6
- package/dist/hub/capability.d.ts +11 -1
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -1
- 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 +38 -2
- package/dist/mailbox/store.js +416 -27
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/expand.js +17 -6
- package/dist/signals/index.d.ts +2 -1
- package/dist/signals/index.js +2 -1
- package/dist/strategy/constraintAblation.js +115 -369
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/trace/index.d.ts +2 -1
- package/dist/trace/index.js +2 -1
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- package/package.json +4 -1
package/dist/mailbox/store.js
CHANGED
|
@@ -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
|
|
27
|
-
|
|
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
|
-
|
|
197
|
+
record = JSON.parse(text);
|
|
61
198
|
}
|
|
62
|
-
catch {
|
|
199
|
+
catch {
|
|
200
|
+
return; /* skip corrupt JSON */
|
|
201
|
+
}
|
|
202
|
+
onRecord(record);
|
|
63
203
|
};
|
|
64
204
|
try {
|
|
65
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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) => ({
|
|
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(
|
|
675
|
+
importJsonl(source) {
|
|
349
676
|
let n = 0;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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[];
|