@evomap/evolver 1.89.2 → 1.89.3
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/README.ja-JP.md +1 -3
- package/README.ko-KR.md +1 -3
- package/README.md +1 -3
- package/README.zh-CN.md +1 -3
- package/assets/gep/genes.seed.json +251 -0
- package/index.js +14 -47
- package/package.json +1 -1
- package/scripts/refresh_stars_badge.js +168 -0
- package/src/adapters/hookAdapter.js +2 -0
- package/src/adapters/scripts/_lockPaths.js +74 -0
- package/src/adapters/scripts/evolver-session-start.js +19 -27
- 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 +200 -7
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/autoDistillConv.js +1 -1
- package/src/gep/autoDistillLlm.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/conversationSniffer.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/execBridge.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.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/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/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +4 -4
- package/src/proxy/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +11 -0
- package/src/proxy/router/messages_route.js +8 -0
- package/src/proxy/trace/extractor.js +1 -1
- package/src/proxy/trace/usage.js +1 -1
package/src/forceUpdate.js
CHANGED
|
@@ -38,6 +38,102 @@ const FORCE_UPDATE_NOOP = Symbol('FORCE_UPDATE_NOOP');
|
|
|
38
38
|
// fire its own reportForceUpdateOutcome. See test/forceUpdateConcurrencyGuard.test.js.
|
|
39
39
|
const FORCE_UPDATE_BUSY = Symbol('FORCE_UPDATE_BUSY');
|
|
40
40
|
|
|
41
|
+
// Structured failure taxonomy. Historically every failing branch of
|
|
42
|
+
// _executeForceUpdateInner just `return false`, so the only thing that ever
|
|
43
|
+
// reached the hub (via reportForceUpdateOutcome) was the literal string
|
|
44
|
+
// "executeForceUpdate returned false" — degit-missing, tag-404, version
|
|
45
|
+
// mismatch and copy-EPERM were all indistinguishable in EvolverUpgradeAttempt.
|
|
46
|
+
// Each branch now returns _fail(code, detail); the reporter encodes it as
|
|
47
|
+
// `error = code + ': ' + detail`, so operators can GROUP BY the code prefix
|
|
48
|
+
// without any hub schema / DB migration. Codes are a small stable set — keep
|
|
49
|
+
// new ones coarse and additive so historical `error LIKE 'code%'` queries
|
|
50
|
+
// don't churn.
|
|
51
|
+
const FORCE_UPDATE_FAIL_CODES = Object.freeze({
|
|
52
|
+
INSTALL_GUARD_NAME_MISMATCH: 'install_guard_name_mismatch',
|
|
53
|
+
INSTALL_GUARD_UNREADABLE: 'install_guard_unreadable',
|
|
54
|
+
BAD_REQUIRED_VERSION: 'bad_required_version',
|
|
55
|
+
CURRENT_VERSION_UNPARSABLE: 'current_version_unparsable',
|
|
56
|
+
NPX_NOT_FOUND: 'npx_not_found',
|
|
57
|
+
DEGIT_TIMEOUT: 'degit_timeout',
|
|
58
|
+
DEGIT_FAILED: 'degit_failed',
|
|
59
|
+
DOWNLOAD_INCOMPLETE: 'download_incomplete',
|
|
60
|
+
DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
|
|
61
|
+
COPY_FAILED: 'copy_failed',
|
|
62
|
+
ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Build the structured failure result that replaces a bare `return false`.
|
|
66
|
+
// Shape: { ok:false, code, detail }. Distinct from `true`, FORCE_UPDATE_NOOP
|
|
67
|
+
// and FORCE_UPDATE_BUSY, so the three call sites' `result === true` /
|
|
68
|
+
// `result === SENTINEL` checks keep classifying it as "failed" unchanged —
|
|
69
|
+
// this is backward compatible. Frozen so a downstream consumer cannot mutate
|
|
70
|
+
// the code/detail before it is reported. detail is best-effort context (an
|
|
71
|
+
// errno, a version delta, an entry name); it is redacted + truncated to
|
|
72
|
+
// ERROR_MAX by the reporter before it leaves the process.
|
|
73
|
+
function _fail(code, detail) {
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
ok: false,
|
|
76
|
+
code: String(code),
|
|
77
|
+
detail: detail == null ? '' : String(detail),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compact "CODE: message" rendering of a thrown error for the detail field.
|
|
82
|
+
function _errStr(e) {
|
|
83
|
+
if (!e) return 'unknown';
|
|
84
|
+
var code = e.code ? String(e.code) + ': ' : '';
|
|
85
|
+
return code + (e.message != null ? String(e.message) : String(e));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Map a Channel 1 (GitHub Release / degit) throw to a structured failure.
|
|
89
|
+
// `phase` records how far the try block got before throwing, so a readFileSync
|
|
90
|
+
// ENOENT (truncated download) is not misread as an npx ENOENT (npx missing):
|
|
91
|
+
// 'degit' -> the npx/degit spawn itself
|
|
92
|
+
// 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
|
|
93
|
+
// 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
|
|
94
|
+
function _classifyChannel1Error(e, phase) {
|
|
95
|
+
if (phase === 'copy') {
|
|
96
|
+
var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
|
|
97
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
|
|
98
|
+
}
|
|
99
|
+
if (phase === 'parse') {
|
|
100
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
|
|
101
|
+
'missing/invalid package.json in downloaded tree: ' + _errStr(e));
|
|
102
|
+
}
|
|
103
|
+
// phase === 'degit' (the spawn). ENOENT here is the npx binary itself, not a
|
|
104
|
+
// file inside the download — that distinction is exactly why `phase` exists.
|
|
105
|
+
if (e && e.code === 'ENOENT') {
|
|
106
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.NPX_NOT_FOUND, _errStr(e));
|
|
107
|
+
}
|
|
108
|
+
// execFileSync timeout kills the child with SIGTERM (and sets .killed); some
|
|
109
|
+
// platforms surface ETIMEDOUT instead. Either way it is a 60s timeout.
|
|
110
|
+
if (e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT')) {
|
|
111
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_TIMEOUT,
|
|
112
|
+
'degit timed out after 60s' + (e.signal ? ' (signal=' + e.signal + ')' : ''));
|
|
113
|
+
}
|
|
114
|
+
// Generic degit/network/tag-not-found failure. degit prints the real reason
|
|
115
|
+
// ("could not find commit hash for v…", "could not resolve host") to stderr,
|
|
116
|
+
// so keep a tail of it. Redact + strip control chars HERE, before the tail
|
|
117
|
+
// slice: the downstream reporter redact (a2aProtocol.reportForceUpdateOutcome)
|
|
118
|
+
// runs after this, so slicing first could chop a token's prefix anchor and
|
|
119
|
+
// let the bare value slip past the prefix-anchored redact patterns. Stripping
|
|
120
|
+
// ANSI/NUL/newlines also keeps the persisted error free of terminal-injection
|
|
121
|
+
// sequences and log-line noise.
|
|
122
|
+
var detail = _errStr(e);
|
|
123
|
+
var stderr = '';
|
|
124
|
+
if (e && e.stderr != null) {
|
|
125
|
+
try {
|
|
126
|
+
var redactString = require('./gep/sanitize').redactString;
|
|
127
|
+
stderr = redactString(String(e.stderr)).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
|
|
128
|
+
} catch (_) {
|
|
129
|
+
// sanitize unavailable — still strip control chars so logs stay clean.
|
|
130
|
+
stderr = String(e.stderr).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (stderr) detail += ' | stderr=' + stderr.slice(-300);
|
|
134
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
|
|
135
|
+
}
|
|
136
|
+
|
|
41
137
|
// Module-level mutex: shared by every caller that requires('../forceUpdate'),
|
|
42
138
|
// so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
|
|
43
139
|
// in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
|
|
@@ -163,12 +259,14 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
163
259
|
console.warn('[ForceUpdate] Refusing — ' + INSTALL_ROOT +
|
|
164
260
|
'/package.json has name="' + (pkg && pkg.name) +
|
|
165
261
|
'", expected "@evomap/evolver". Aborting to avoid data loss.');
|
|
166
|
-
return
|
|
262
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
|
|
263
|
+
'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
|
|
167
264
|
}
|
|
168
265
|
} catch (e) {
|
|
169
266
|
console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
|
|
170
267
|
'/package.json: ' + (e && e.message || e));
|
|
171
|
-
return
|
|
268
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
|
|
269
|
+
'cannot read install root package.json: ' + _errStr(e));
|
|
172
270
|
}
|
|
173
271
|
|
|
174
272
|
const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
|
|
@@ -176,7 +274,8 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
176
274
|
console.warn('[ForceUpdate] Refusing — required_version "' +
|
|
177
275
|
String(forceUpdate.required_version || '').replace(/^[>=^~\s]+/, '') +
|
|
178
276
|
'" is not a concrete semver (ranges not accepted).');
|
|
179
|
-
return
|
|
277
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.BAD_REQUIRED_VERSION,
|
|
278
|
+
'required_version=' + JSON.stringify(forceUpdate && forceUpdate.required_version) + ' is not a concrete semver');
|
|
180
279
|
}
|
|
181
280
|
|
|
182
281
|
function getCurrentVersion() {
|
|
@@ -207,7 +306,8 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
207
306
|
if (versionCmp === null) {
|
|
208
307
|
console.warn('[ForceUpdate] Refusing — current installed version "' +
|
|
209
308
|
currentVersion + '" is not a concrete semver.');
|
|
210
|
-
return
|
|
309
|
+
return _fail(FORCE_UPDATE_FAIL_CODES.CURRENT_VERSION_UNPARSABLE,
|
|
310
|
+
'current installed version "' + currentVersion + '" is not a concrete semver');
|
|
211
311
|
}
|
|
212
312
|
if (versionCmp >= 0) {
|
|
213
313
|
console.log('[ForceUpdate] already satisfies required version, no-op (current=' +
|
|
@@ -230,6 +330,14 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
230
330
|
const TMP_TARGET = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-tmp-'));
|
|
231
331
|
|
|
232
332
|
// Channel 1: GitHub Release (via degit pinned to exact version tag)
|
|
333
|
+
//
|
|
334
|
+
// channel1Failure captures the structured reason this channel failed, so the
|
|
335
|
+
// terminal `return` can surface it instead of a bare `false`. `phase` tracks
|
|
336
|
+
// how far we got before any throw, so _classifyChannel1Error can tell a
|
|
337
|
+
// degit-spawn failure (phase 'degit') from a truncated download (phase
|
|
338
|
+
// 'parse') from a copy-into-INSTALL_ROOT failure (phase 'copy').
|
|
339
|
+
var channel1Failure = null;
|
|
340
|
+
var phase = 'degit';
|
|
233
341
|
try {
|
|
234
342
|
console.log('[ForceUpdate] Channel 1: GitHub Release download (v' + requiredVersion + ')...');
|
|
235
343
|
var npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
@@ -240,19 +348,34 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
240
348
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
241
349
|
timeout: 60000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
|
|
242
350
|
});
|
|
351
|
+
phase = 'parse';
|
|
243
352
|
var tmpPkg = JSON.parse(fs.readFileSync(path.join(TMP_TARGET, 'package.json'), 'utf8'));
|
|
244
353
|
// Require exact version match — a ">=" check would allow a compromised hub to
|
|
245
354
|
// request version "0.0.1" and install any version including unreleased HEAD code.
|
|
246
355
|
if (tmpPkg.version && tmpPkg.version === requiredVersion) {
|
|
356
|
+
phase = 'copy';
|
|
247
357
|
var entries = fs.readdirSync(INSTALL_ROOT, { withFileTypes: true });
|
|
248
358
|
for (var ei = 0; ei < entries.length; ei++) {
|
|
249
359
|
var eName = entries[ei].name;
|
|
360
|
+
// package.json is the install's commit marker: keep the OLD one in
|
|
361
|
+
// place through the entire delete+copy below and swap in the new one
|
|
362
|
+
// atomically at the very end (see "commit marker" block). If it were
|
|
363
|
+
// deleted here and any later cpSync threw (ENOSPC, a Windows lock that
|
|
364
|
+
// outlasts the retries, a kill), the install root would be left with
|
|
365
|
+
// no package.json — and the install-guard at the top of this function
|
|
366
|
+
// refuses on an unreadable package.json, wedging the node in
|
|
367
|
+
// install_guard_unreadable on every subsequent attempt with no path
|
|
368
|
+
// that ever re-copies it. Deferring it keeps the install self-healing.
|
|
250
369
|
if (eName === 'node_modules' || eName === 'memory' || eName === '.git' || eName === 'MEMORY.md'
|
|
251
|
-
|| eName === '.env' || eName === '.env.local' || eName === 'USER.md' || eName === '.evolver'
|
|
370
|
+
|| eName === '.env' || eName === '.env.local' || eName === 'USER.md' || eName === '.evolver'
|
|
371
|
+
|| eName === 'package.json') continue;
|
|
252
372
|
try { fs.rmSync(path.join(INSTALL_ROOT, eName), { recursive: true, force: true }); } catch (_) {}
|
|
253
373
|
}
|
|
254
374
|
var newEntries = fs.readdirSync(TMP_TARGET, { withFileTypes: true });
|
|
255
375
|
for (var ni = 0; ni < newEntries.length; ni++) {
|
|
376
|
+
// Deferred: package.json is the commit marker, written last + atomically
|
|
377
|
+
// after every other entry has copied successfully (see below).
|
|
378
|
+
if (newEntries[ni].name === 'package.json') continue;
|
|
256
379
|
var src = path.join(TMP_TARGET, newEntries[ni].name);
|
|
257
380
|
var dst = path.join(INSTALL_ROOT, newEntries[ni].name);
|
|
258
381
|
// On Windows, files held open by antivirus or the OS itself raise EPERM/EBUSY.
|
|
@@ -275,16 +398,70 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
275
398
|
}
|
|
276
399
|
if (copyErr) {
|
|
277
400
|
console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' + (copyErr.message || copyErr));
|
|
401
|
+
// Tag the failing entry so _classifyChannel1Error can name it in the
|
|
402
|
+
// copy_failed detail. phase is already 'copy' here.
|
|
403
|
+
try { copyErr._evolverEntry = newEntries[ni].name; } catch (_) {}
|
|
278
404
|
throw copyErr;
|
|
279
405
|
}
|
|
280
406
|
}
|
|
407
|
+
// Commit marker: every other entry copied successfully, so swap in the
|
|
408
|
+
// new package.json LAST and atomically. The old package.json was kept in
|
|
409
|
+
// place above; only this rename makes the new version visible. Net effect:
|
|
410
|
+
// - any throw before this point leaves the OLD package.json intact, so
|
|
411
|
+
// the install-guard still reads a valid package.json next tick and the
|
|
412
|
+
// force-update simply retries (no install_guard_unreadable wedge);
|
|
413
|
+
// - the new version becomes "current" only once the tree is fully in
|
|
414
|
+
// place, so a partial install never reports as already-satisfied.
|
|
415
|
+
// tmp + rename in INSTALL_ROOT (same filesystem) is an atomic replace on
|
|
416
|
+
// POSIX; Windows renameSync throws EPERM over an existing dest, so unlink
|
|
417
|
+
// first there. Mirrors src/proxy/mailbox/store.js _persistState.
|
|
418
|
+
var pkgSrc = path.join(TMP_TARGET, 'package.json');
|
|
419
|
+
var pkgDst = path.join(INSTALL_ROOT, 'package.json');
|
|
420
|
+
var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
|
|
421
|
+
var pkgErr = null;
|
|
422
|
+
for (var pa = 0; pa < 3; pa++) {
|
|
423
|
+
try {
|
|
424
|
+
fs.cpSync(pkgSrc, pkgTmp);
|
|
425
|
+
if (process.platform === 'win32') {
|
|
426
|
+
try { fs.unlinkSync(pkgDst); } catch (ue) { if (ue && ue.code !== 'ENOENT') throw ue; }
|
|
427
|
+
}
|
|
428
|
+
fs.renameSync(pkgTmp, pkgDst);
|
|
429
|
+
pkgErr = null;
|
|
430
|
+
break;
|
|
431
|
+
} catch (pErr) {
|
|
432
|
+
pkgErr = pErr;
|
|
433
|
+
try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
|
|
434
|
+
var pcode = pErr && pErr.code;
|
|
435
|
+
if (pcode !== 'EPERM' && pcode !== 'EBUSY' && pcode !== 'EACCES') break;
|
|
436
|
+
var puntil = Date.now() + 200;
|
|
437
|
+
while (Date.now() < puntil) { /* spin */ }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (pkgErr) {
|
|
441
|
+
console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
|
|
442
|
+
throw pkgErr;
|
|
443
|
+
}
|
|
281
444
|
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|
|
282
445
|
console.log('[ForceUpdate] GitHub Release update successful: ' + tmpPkg.version);
|
|
283
446
|
return true;
|
|
284
447
|
}
|
|
448
|
+
// degit succeeded and produced a parseable package.json, but it did not
|
|
449
|
+
// satisfy the exact-version check above. Two distinct causes, two codes:
|
|
450
|
+
if (!tmpPkg.version) {
|
|
451
|
+
// degit produced a parseable package.json with no version field — a
|
|
452
|
+
// malformed/incomplete download, not a stale/tampered tag mismatch.
|
|
453
|
+
channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
|
|
454
|
+
'downloaded package.json has no version field');
|
|
455
|
+
} else {
|
|
456
|
+
// version present but not the exact tag we asked for (stale tag, mirror
|
|
457
|
+
// lag, or a tampered/redirected tag). Refuse and record the delta.
|
|
458
|
+
channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH,
|
|
459
|
+
'downloaded version=' + JSON.stringify(tmpPkg.version) + ', expected ' + requiredVersion);
|
|
460
|
+
}
|
|
285
461
|
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|
|
286
462
|
} catch (e) {
|
|
287
|
-
|
|
463
|
+
channel1Failure = _classifyChannel1Error(e, phase);
|
|
464
|
+
console.warn('[ForceUpdate] GitHub Release failed (' + channel1Failure.code + '):', e && e.message || e);
|
|
288
465
|
try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
|
|
289
466
|
// Fall through to Channel 2 (manual download URL hint) instead of
|
|
290
467
|
// returning. A Channel 1 error (degit missing, network down, tag not
|
|
@@ -301,7 +478,12 @@ function _executeForceUpdateInner(forceUpdate) {
|
|
|
301
478
|
} catch (_) {}
|
|
302
479
|
|
|
303
480
|
console.warn('[ForceUpdate] All automatic channels exhausted. Current version: ' + getCurrentVersion());
|
|
304
|
-
|
|
481
|
+
// Surface the concrete Channel 1 failure when we have one (the common case:
|
|
482
|
+
// degit/network/copy/version-mismatch). channel1Failure is null only when
|
|
483
|
+
// Channel 1 was never entered, which cannot happen here — but fall back to a
|
|
484
|
+
// terminal code so the reporter never lands on the legacy "returned false".
|
|
485
|
+
return channel1Failure || _fail(FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED,
|
|
486
|
+
'no automatic channel succeeded; current=' + getCurrentVersion() + ' target=' + requiredVersion);
|
|
305
487
|
}
|
|
306
488
|
|
|
307
489
|
// Test-only hook: re-implements the EXACT same operator-strip + semver
|
|
@@ -318,10 +500,21 @@ function _isAcceptedRequiredVersionForTesting(raw) {
|
|
|
318
500
|
return normalizeRequiredVersion(raw) !== '';
|
|
319
501
|
}
|
|
320
502
|
|
|
503
|
+
// Type guard: is `result` a structured failure (vs true / NOOP / BUSY)?
|
|
504
|
+
// Call sites use this to decide whether to forward result as opts.failure to
|
|
505
|
+
// reportForceUpdateOutcome. Kept tiny and dependency-free so all three
|
|
506
|
+
// duplicated triggers (a2aProtocol heartbeat, proxy manager, enrich tick) can
|
|
507
|
+
// share one definition.
|
|
508
|
+
function isForceUpdateFailure(result) {
|
|
509
|
+
return !!result && typeof result === 'object' && result.ok === false && typeof result.code === 'string';
|
|
510
|
+
}
|
|
511
|
+
|
|
321
512
|
module.exports = {
|
|
322
513
|
executeForceUpdate,
|
|
323
514
|
FORCE_UPDATE_NOOP,
|
|
324
515
|
FORCE_UPDATE_BUSY,
|
|
516
|
+
FORCE_UPDATE_FAIL_CODES,
|
|
517
|
+
isForceUpdateFailure,
|
|
325
518
|
// Test-only hook: reset the in-flight mutex so unit tests do not leak state
|
|
326
519
|
// across cases. Production callers must NOT touch this -- the mutex is the
|
|
327
520
|
// load-bearing invariant that prevents concurrent state-file writes.
|