@evomap/evolver 1.82.0 → 1.82.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/recall-verify-report.js +225 -0
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/forceUpdate.js +50 -16
- package/src/gep/.integrity +0 -0
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/integrityCheck.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +20 -0
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +306 -0
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/shield.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// recallVerifier — confirm that assets we publish to Hub can actually be
|
|
5
|
+
// recalled later. After a successful publish (capsule bundle, anti-pattern,
|
|
6
|
+
// or skill bundle), enqueue the asset_id; a background worker uses Hub's
|
|
7
|
+
// Phase 2 deterministic lookup to verify the asset round-trips, with
|
|
8
|
+
// exponential backoff to absorb Hub indexing latency. Outcomes land as
|
|
9
|
+
// MemoryGraphEvents of kind 'recall_verify' so a report script can compute
|
|
10
|
+
// per-asset-type success rates and gate releases.
|
|
11
|
+
//
|
|
12
|
+
// Scope: only assets with a content-hash asset_id are verified. MemoryGraph
|
|
13
|
+
// events themselves go through a different transport (POST /a2a/memory/event,
|
|
14
|
+
// not the asset store) and produce no asset_id, so they cannot be subject to
|
|
15
|
+
// roundtrip verification. This naturally terminates any recursion of
|
|
16
|
+
// recall_verify events that would otherwise mirror to Hub.
|
|
17
|
+
//
|
|
18
|
+
// Graceful degradation: if fetchAssetById fails for any reason (network,
|
|
19
|
+
// auth, schema), the worker emits 'verification_skipped' rather than
|
|
20
|
+
// throwing. Verification must NEVER block the daemon cycle.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const { envInt, envFloat } = require('../config');
|
|
24
|
+
const { fetchAssetById } = require('./hubSearch');
|
|
25
|
+
const { computeAssetId } = require('./contentHash');
|
|
26
|
+
const { writeMemoryGraphEvent } = require('./memoryGraph');
|
|
27
|
+
|
|
28
|
+
// --- Module state (per-process lifetime, bounded) ---
|
|
29
|
+
|
|
30
|
+
let _queue = [];
|
|
31
|
+
const _inflightAssetIds = new Set();
|
|
32
|
+
let _workerStarted = false;
|
|
33
|
+
let _workerTimer = null;
|
|
34
|
+
let _lastReport = { ok: 0, missing: 0, mismatch: 0, skipped: 0 };
|
|
35
|
+
|
|
36
|
+
// Backoff schedule between attempts. attempt 1 fires INITIAL_WAIT_MS after
|
|
37
|
+
// publish; if it returns missing, retries fire after these intervals.
|
|
38
|
+
const BACKOFF_MS = [5000, 15000, 60000];
|
|
39
|
+
|
|
40
|
+
// --- Env getters (read at call time, never module-load) ---
|
|
41
|
+
|
|
42
|
+
function _isFeatureEnabled() {
|
|
43
|
+
return String(process.env.EVOLVE_RECALL_VERIFY || '1') !== '0';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _getSampleRate() {
|
|
47
|
+
return envFloat('EVOLVE_RECALL_VERIFY_SAMPLE_RATE', 1.0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _getQueueMax() {
|
|
51
|
+
return envInt('EVOLVE_RECALL_VERIFY_QUEUE_MAX', 256);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _getPollMs() {
|
|
55
|
+
return envInt('EVOLVE_RECALL_VERIFY_POLL_MS', 5000);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _getInitialWaitMs() {
|
|
59
|
+
return envInt('EVOLVE_RECALL_VERIFY_INITIAL_WAIT_MS', 5000);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _getMaxAttempts() {
|
|
63
|
+
return envInt('EVOLVE_RECALL_VERIFY_ATTEMPTS', 3);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _getFetchTimeoutMs() {
|
|
67
|
+
return envInt('EVOLVE_RECALL_VERIFY_FETCH_TIMEOUT_MS', 8000);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Event emission ---
|
|
71
|
+
|
|
72
|
+
function _emitEvent(entry, outcome, reason, extras) {
|
|
73
|
+
const ts = Date.now();
|
|
74
|
+
const verification = {
|
|
75
|
+
outcome,
|
|
76
|
+
reason: reason || null,
|
|
77
|
+
attempts: extras && Number.isFinite(extras.attempts) ? extras.attempts : (entry.attempts || 0),
|
|
78
|
+
latency_ms: extras && Number.isFinite(extras.latency_ms) ? extras.latency_ms : 0,
|
|
79
|
+
age_at_verify_ms: entry.publishedAt ? (ts - entry.publishedAt) : 0,
|
|
80
|
+
recalled_hash: extras && extras.recalled_hash ? extras.recalled_hash : null,
|
|
81
|
+
};
|
|
82
|
+
const ev = {
|
|
83
|
+
type: 'MemoryGraphEvent',
|
|
84
|
+
kind: 'recall_verify',
|
|
85
|
+
id: 'mge_' + ts + '_' + Math.random().toString(36).slice(2, 10),
|
|
86
|
+
ts,
|
|
87
|
+
asset: {
|
|
88
|
+
type: entry.type || 'Unknown',
|
|
89
|
+
id: entry.asset_id || null,
|
|
90
|
+
},
|
|
91
|
+
verification,
|
|
92
|
+
signal: { signals: Array.isArray(entry.signals) ? entry.signals.slice(0, 8) : [] },
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
writeMemoryGraphEvent(ev);
|
|
96
|
+
} catch (writeErr) {
|
|
97
|
+
if (process.env.EVOLVE_RECALL_VERIFY_DEBUG === '1') {
|
|
98
|
+
console.warn('[RecallVerify] writeMemoryGraphEvent failed: ' + (writeErr && writeErr.message || writeErr));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (outcome === 'roundtrip_ok') _lastReport.ok++;
|
|
102
|
+
else if (outcome === 'roundtrip_missing') _lastReport.missing++;
|
|
103
|
+
else if (outcome === 'roundtrip_mismatch') _lastReport.mismatch++;
|
|
104
|
+
else _lastReport.skipped++;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Public API ---
|
|
108
|
+
|
|
109
|
+
function enqueuePublishedAsset(input) {
|
|
110
|
+
const entry = {
|
|
111
|
+
asset_id: input && input.asset_id ? String(input.asset_id) : null,
|
|
112
|
+
type: input && input.type ? String(input.type) : 'Unknown',
|
|
113
|
+
signals: Array.isArray(input && input.signals) ? input.signals : [],
|
|
114
|
+
publishedAt: input && Number.isFinite(input.publishedAt) ? input.publishedAt : Date.now(),
|
|
115
|
+
attempts: 0,
|
|
116
|
+
nextEligibleAt: 0,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (!_isFeatureEnabled()) {
|
|
120
|
+
_emitEvent(entry, 'verification_skipped', 'feature_disabled');
|
|
121
|
+
return { enqueued: false, reason: 'feature_disabled' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!entry.asset_id) {
|
|
125
|
+
_emitEvent(entry, 'verification_skipped', 'missing_asset_id');
|
|
126
|
+
return { enqueued: false, reason: 'missing_asset_id' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sampleRate = _getSampleRate();
|
|
130
|
+
if (sampleRate < 1.0 && Math.random() >= sampleRate) {
|
|
131
|
+
_emitEvent(entry, 'verification_skipped', 'sample_rate');
|
|
132
|
+
return { enqueued: false, reason: 'sample_rate' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
entry.nextEligibleAt = entry.publishedAt + _getInitialWaitMs();
|
|
136
|
+
|
|
137
|
+
const queueMax = _getQueueMax();
|
|
138
|
+
while (_queue.length >= queueMax) {
|
|
139
|
+
const dropped = _queue.shift();
|
|
140
|
+
_emitEvent(dropped, 'verification_skipped', 'queue_full');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_queue.push(entry);
|
|
144
|
+
return { enqueued: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Single-asset verification. Returns one of the four outcomes.
|
|
148
|
+
// outcome ∈ { roundtrip_ok | roundtrip_missing | roundtrip_mismatch | verification_skipped }
|
|
149
|
+
async function verifyOnce(assetId, assetType) {
|
|
150
|
+
const t0 = Date.now();
|
|
151
|
+
if (!assetId) {
|
|
152
|
+
return { outcome: 'verification_skipped', reason: 'missing_asset_id', latency_ms: 0, recalled_hash: null };
|
|
153
|
+
}
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = await fetchAssetById(assetId, { timeoutMs: _getFetchTimeoutMs(), bypassCache: true });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return {
|
|
159
|
+
outcome: 'verification_skipped',
|
|
160
|
+
reason: 'hub_unreachable',
|
|
161
|
+
latency_ms: Date.now() - t0,
|
|
162
|
+
recalled_hash: null,
|
|
163
|
+
error: err && err.message || String(err),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const latency = Date.now() - t0;
|
|
168
|
+
|
|
169
|
+
if (!result || !result.ok) {
|
|
170
|
+
return {
|
|
171
|
+
outcome: 'verification_skipped',
|
|
172
|
+
reason: 'hub_unreachable',
|
|
173
|
+
latency_ms: latency,
|
|
174
|
+
recalled_hash: null,
|
|
175
|
+
error: (result && result.error) || 'fetch_not_ok',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!result.asset) {
|
|
180
|
+
return { outcome: 'roundtrip_missing', reason: null, latency_ms: latency, recalled_hash: null };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const recalled = result.asset;
|
|
184
|
+
if (recalled.asset_id !== assetId) {
|
|
185
|
+
return { outcome: 'roundtrip_missing', reason: 'wrong_asset', latency_ms: latency, recalled_hash: recalled.asset_id || null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Recompute the asset_id from the recalled body. If it doesn't match what
|
|
189
|
+
// the recalled asset claims its asset_id is, the Hub re-encoded or
|
|
190
|
+
// corrupted the asset between publish and fetch.
|
|
191
|
+
let recomputed;
|
|
192
|
+
try {
|
|
193
|
+
recomputed = computeAssetId(recalled);
|
|
194
|
+
} catch (hashErr) {
|
|
195
|
+
return {
|
|
196
|
+
outcome: 'verification_skipped',
|
|
197
|
+
reason: 'hash_recompute_failed',
|
|
198
|
+
latency_ms: latency,
|
|
199
|
+
recalled_hash: null,
|
|
200
|
+
error: hashErr && hashErr.message || String(hashErr),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (recomputed !== recalled.asset_id) {
|
|
205
|
+
return { outcome: 'roundtrip_mismatch', reason: 'hash_drift', latency_ms: latency, recalled_hash: recomputed };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { outcome: 'roundtrip_ok', reason: null, latency_ms: latency, recalled_hash: recomputed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Drain the queue once. Public + idempotent so tests can drive it
|
|
212
|
+
// synchronously without waiting on setInterval.
|
|
213
|
+
async function _runWorkerOnce() {
|
|
214
|
+
if (_queue.length === 0) return { processed: 0 };
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const maxAttempts = _getMaxAttempts();
|
|
217
|
+
const ready = [];
|
|
218
|
+
for (let i = 0; i < _queue.length; i++) {
|
|
219
|
+
const entry = _queue[i];
|
|
220
|
+
if (entry.nextEligibleAt <= now && !_inflightAssetIds.has(entry.asset_id)) {
|
|
221
|
+
ready.push(entry);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (ready.length === 0) return { processed: 0 };
|
|
225
|
+
|
|
226
|
+
let processed = 0;
|
|
227
|
+
for (const entry of ready) {
|
|
228
|
+
_inflightAssetIds.add(entry.asset_id);
|
|
229
|
+
try {
|
|
230
|
+
entry.attempts += 1;
|
|
231
|
+
const verifyResult = await verifyOnce(entry.asset_id, entry.type);
|
|
232
|
+
|
|
233
|
+
const isRetryable = (verifyResult.outcome === 'roundtrip_missing' ||
|
|
234
|
+
(verifyResult.outcome === 'verification_skipped' && verifyResult.reason === 'hub_unreachable'));
|
|
235
|
+
const hasAttemptsLeft = entry.attempts < maxAttempts;
|
|
236
|
+
|
|
237
|
+
if (isRetryable && hasAttemptsLeft) {
|
|
238
|
+
const backoffIdx = Math.min(entry.attempts - 1, BACKOFF_MS.length - 1);
|
|
239
|
+
entry.nextEligibleAt = Date.now() + BACKOFF_MS[backoffIdx];
|
|
240
|
+
} else {
|
|
241
|
+
_emitEvent(entry, verifyResult.outcome, verifyResult.reason, {
|
|
242
|
+
attempts: entry.attempts,
|
|
243
|
+
latency_ms: verifyResult.latency_ms,
|
|
244
|
+
recalled_hash: verifyResult.recalled_hash,
|
|
245
|
+
});
|
|
246
|
+
const idx = _queue.indexOf(entry);
|
|
247
|
+
if (idx !== -1) _queue.splice(idx, 1);
|
|
248
|
+
}
|
|
249
|
+
processed += 1;
|
|
250
|
+
} finally {
|
|
251
|
+
// Always release inflight (including on retry) — next poll re-adds.
|
|
252
|
+
_inflightAssetIds.delete(entry.asset_id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { processed };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function startWorker() {
|
|
259
|
+
if (_workerStarted) return;
|
|
260
|
+
_workerStarted = true;
|
|
261
|
+
const pollMs = _getPollMs();
|
|
262
|
+
_workerTimer = setInterval(function () {
|
|
263
|
+
_runWorkerOnce().catch(function (err) {
|
|
264
|
+
if (process.env.EVOLVE_RECALL_VERIFY_DEBUG === '1') {
|
|
265
|
+
console.warn('[RecallVerify] worker tick failed: ' + (err && err.message || err));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}, pollMs);
|
|
269
|
+
if (_workerTimer && typeof _workerTimer.unref === 'function') {
|
|
270
|
+
_workerTimer.unref();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function stopWorker() {
|
|
275
|
+
if (_workerTimer) {
|
|
276
|
+
clearInterval(_workerTimer);
|
|
277
|
+
_workerTimer = null;
|
|
278
|
+
}
|
|
279
|
+
_workerStarted = false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getLastReport() {
|
|
283
|
+
return Object.assign({}, _lastReport);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function _resetForTesting() {
|
|
287
|
+
stopWorker();
|
|
288
|
+
_queue = [];
|
|
289
|
+
_inflightAssetIds.clear();
|
|
290
|
+
_lastReport = { ok: 0, missing: 0, mismatch: 0, skipped: 0 };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _getQueueLengthForTesting() {
|
|
294
|
+
return _queue.length;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
enqueuePublishedAsset,
|
|
299
|
+
verifyOnce,
|
|
300
|
+
startWorker,
|
|
301
|
+
stopWorker,
|
|
302
|
+
getLastReport,
|
|
303
|
+
_runWorkerOnce,
|
|
304
|
+
_resetForTesting,
|
|
305
|
+
_getQueueLengthForTesting,
|
|
306
|
+
};
|