@evomap/evolver 1.89.14 → 1.89.17

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 (72) hide show
  1. package/index.js +219 -29
  2. package/package.json +1 -1
  3. package/src/adapters/claudeCode.js +2 -2
  4. package/src/adapters/codex.js +2 -2
  5. package/src/adapters/hookAdapter.js +14 -2
  6. package/src/adapters/scripts/evolver-session-start.js +182 -2
  7. package/src/config.js +11 -3
  8. package/src/evolve/guards.js +1 -1
  9. package/src/evolve/pipeline/collect.js +1 -1
  10. package/src/evolve/pipeline/dispatch.js +1 -1
  11. package/src/evolve/pipeline/enrich.js +1 -1
  12. package/src/evolve/pipeline/hub.js +1 -1
  13. package/src/evolve/pipeline/select.js +1 -1
  14. package/src/evolve/pipeline/signals.js +1 -1
  15. package/src/evolve/utils.js +1 -1
  16. package/src/evolve.js +1 -1
  17. package/src/forceUpdate.js +499 -119
  18. package/src/gep/a2aProtocol.js +1 -1
  19. package/src/gep/antiAbuseTelemetry.js +1 -1
  20. package/src/gep/autoDistillConv.js +1 -1
  21. package/src/gep/autoDistillLlm.js +1 -1
  22. package/src/gep/candidateEval.js +1 -1
  23. package/src/gep/candidates.js +1 -1
  24. package/src/gep/cliContracts.js +1154 -0
  25. package/src/gep/contentHash.js +1 -1
  26. package/src/gep/conversationDistiller.js +1 -1
  27. package/src/gep/conversationSniffer.js +1 -1
  28. package/src/gep/crypto.js +1 -1
  29. package/src/gep/curriculum.js +1 -1
  30. package/src/gep/deviceId.js +1 -1
  31. package/src/gep/envFingerprint.js +1 -1
  32. package/src/gep/epigenetics.js +1 -1
  33. package/src/gep/execBridge.js +1 -1
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hostErrorClassifier.js +34 -0
  37. package/src/gep/hubFetch.js +1 -1
  38. package/src/gep/hubReview.js +1 -1
  39. package/src/gep/hubSearch.js +1 -1
  40. package/src/gep/hubVerify.js +1 -1
  41. package/src/gep/issueReporter.js +87 -0
  42. package/src/gep/learningSignals.js +1 -1
  43. package/src/gep/memoryGraph.js +1 -1
  44. package/src/gep/memoryGraphAdapter.js +1 -1
  45. package/src/gep/mutation.js +1 -1
  46. package/src/gep/narrativeMemory.js +1 -1
  47. package/src/gep/openPRRegistry.js +1 -1
  48. package/src/gep/paths.js +20 -0
  49. package/src/gep/personality.js +1 -1
  50. package/src/gep/policyCheck.js +1 -1
  51. package/src/gep/prompt.js +1 -1
  52. package/src/gep/recallInject.js +1 -1
  53. package/src/gep/recallVerifier.js +1 -1
  54. package/src/gep/reflection.js +1 -1
  55. package/src/gep/sanitize.js +20 -4
  56. package/src/gep/savingsCore.js +1 -1
  57. package/src/gep/selector.js +1 -1
  58. package/src/gep/signals.js +70 -24
  59. package/src/gep/skillDistiller.js +1 -1
  60. package/src/gep/solidify.js +1 -1
  61. package/src/gep/strategy.js +1 -1
  62. package/src/gep/tokenSavings.js +1 -1
  63. package/src/gep/workspaceKeychain.js +1 -1
  64. package/src/ops/lifecycle.js +501 -31
  65. package/src/proxy/extensions/traceControl.js +1 -1
  66. package/src/proxy/index.js +4 -4
  67. package/src/proxy/inject.js +1 -1
  68. package/src/proxy/lifecycle/manager.js +233 -33
  69. package/src/proxy/sync/inbound.js +5 -4
  70. package/src/proxy/sync/outbound.js +3 -2
  71. package/src/proxy/trace/extractor.js +1 -1
  72. package/src/proxy/trace/usage.js +1 -1
@@ -11,6 +11,7 @@ const {
11
11
  isHubUnreachableError,
12
12
  readHubResponseJson,
13
13
  readHubResponseText,
14
+ sanitizeHubResponseForLog,
14
15
  throwIfHubUnreachableResponse,
15
16
  } = require('../../gep/hubFetch');
16
17
  const { getEvomapPath } = require('../../gep/paths');
@@ -367,6 +368,81 @@ function parseNodeSecretVersion(value) {
367
368
  return Number.isInteger(n) && n > 0 ? n : null;
368
369
  }
369
370
 
371
+ const NODE_SECRET_RE = /^[a-f0-9]{64}$/i;
372
+ const NODE_SECRET_SUPPRESSION_RE = /^sha256:[a-f0-9]{64}$/i;
373
+ const SECRET_DIVERGENCE_ERROR = 'secret_diverged_cleared';
374
+ const SECRET_DIVERGENCE_REASON_CODES = [
375
+ 'node_secret_invalid',
376
+ 'invalid_secret',
377
+ 'rotation_requires_current_secret',
378
+ ];
379
+
380
+ function validNodeSecret(value) {
381
+ return typeof value === 'string' && NODE_SECRET_RE.test(value);
382
+ }
383
+
384
+ function getEnvNodeSecret() {
385
+ return ((process.env.A2A_NODE_SECRET || process.env.EVOMAP_NODE_SECRET || '').trim() || null);
386
+ }
387
+
388
+ function fingerprintNodeSecret(secret) {
389
+ if (!validNodeSecret(secret)) return null;
390
+ const normalized = String(secret).trim().toLowerCase();
391
+ return 'sha256:' + crypto.createHash('sha256').update(normalized).digest('hex');
392
+ }
393
+
394
+ function truthyState(value) {
395
+ const v = String(value || '').trim().toLowerCase();
396
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
397
+ }
398
+
399
+ function matchSecretDivergenceReason(value) {
400
+ const reason = String(value || '').trim().toLowerCase();
401
+ if (!reason) return null;
402
+ return SECRET_DIVERGENCE_REASON_CODES.find((code) => reason.includes(code)) || null;
403
+ }
404
+
405
+ function extractSecretDivergenceReason(data) {
406
+ const candidates = [
407
+ data?.payload?.reason,
408
+ data?.payload?.error,
409
+ data?.reason,
410
+ data?.error,
411
+ ];
412
+ for (const value of candidates) {
413
+ const reason = matchSecretDivergenceReason(value);
414
+ if (reason) return reason;
415
+ }
416
+ return null;
417
+ }
418
+
419
+ function isSecretDivergenceError(error) {
420
+ return error === SECRET_DIVERGENCE_ERROR || Boolean(matchSecretDivergenceReason(error));
421
+ }
422
+
423
+ function safeHubErrorCode(data, statusCode) {
424
+ const candidates = [
425
+ data?.error,
426
+ data?.reason,
427
+ data?.payload?.error,
428
+ data?.payload?.reason,
429
+ ];
430
+ for (const value of candidates) {
431
+ if (typeof value !== 'string') continue;
432
+ const code = value.trim();
433
+ if (/^[a-z][a-z0-9_:-]{0,79}$/i.test(code)) return code;
434
+ }
435
+ return `http_${statusCode}`;
436
+ }
437
+
438
+ function normalizeNodeSecretEnvSuppression(value) {
439
+ const raw = String(value || '').trim();
440
+ if (!raw) return null;
441
+ if (truthyState(raw)) return 'true';
442
+ const lower = raw.toLowerCase();
443
+ return NODE_SECRET_SUPPRESSION_RE.test(lower) ? lower : null;
444
+ }
445
+
370
446
  class LifecycleManager {
371
447
  constructor({ hubUrl, store, logger, getTaskMeta } = {}) {
372
448
  this.hubUrl = (hubUrl || process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
@@ -386,6 +462,11 @@ class LifecycleManager {
386
462
  this._lastDriftCheckAt = 0;
387
463
  this._hubUnreachableFailures = 0;
388
464
  this._hubUnreachableUntil = 0;
465
+ this._envSecretSuppressionMarker = normalizeNodeSecretEnvSuppression(this.store && this.store.getState
466
+ ? this.store.getState('node_secret_env_suppressed')
467
+ : null);
468
+ this._envSuppressionClearedForSecret = null;
469
+ this._suppressEnvSecret = Boolean(this._envSecretSuppressionMarker && getEnvNodeSecret());
389
470
 
390
471
  // H4 fix: persist the legacy node_id file as soon as the in-memory
391
472
  // node_id is known, NOT only after a successful hello(). The original
@@ -439,14 +520,13 @@ class LifecycleManager {
439
520
  }
440
521
 
441
522
  get nodeSecretVersion() {
523
+ this._resolveNodeSecret();
442
524
  const storeVersion = parseNodeSecretVersion(this.store.getState('node_secret_version'));
443
525
  const storeSecret = this.store.getState('node_secret') || null;
444
526
  const storeSource = this.store.getState('node_secret_source') || null;
445
- const envSecret = this._suppressEnvSecret
446
- ? null
447
- : ((process.env.A2A_NODE_SECRET || process.env.EVOMAP_NODE_SECRET || '').trim() || null);
527
+ const envSecret = this._effectiveEnvNodeSecret();
448
528
  const envVersion = parseNodeSecretVersion(process.env.A2A_NODE_SECRET_VERSION || process.env.EVOMAP_NODE_SECRET_VERSION);
449
- const validStoreSecret = typeof storeSecret === 'string' && /^[a-f0-9]{64}$/i.test(storeSecret);
529
+ const validStoreSecret = validNodeSecret(storeSecret);
450
530
  if (this._suppressEnvSecret) return validStoreSecret ? storeVersion : null;
451
531
  if (storeSource === 'hub_rotate' && validStoreSecret) return storeVersion;
452
532
  if (envSecret) {
@@ -487,37 +567,41 @@ class LifecycleManager {
487
567
  * @returns {string|null}
488
568
  */
489
569
  _resolveNodeSecret() {
490
- const envSecret = this._suppressEnvSecret
491
- ? null
492
- : ((process.env.A2A_NODE_SECRET || process.env.EVOMAP_NODE_SECRET || '').trim() || null);
570
+ const envSecret = this._effectiveEnvNodeSecret();
571
+ const envUpdatedAfterSuppression = this._envSuppressionClearedForSecret === envSecret;
493
572
  const storeSecret = this.store.getState('node_secret') || null;
494
573
  const storeSource = this.store.getState('node_secret_source') || null;
495
- const valid = (s) => typeof s === 'string' && /^[a-f0-9]{64}$/i.test(s);
574
+
575
+ if (
576
+ envSecret &&
577
+ envUpdatedAfterSuppression &&
578
+ validNodeSecret(envSecret) &&
579
+ (!storeSecret || envSecret === storeSecret) &&
580
+ !(storeSecret && envSecret === storeSecret && storeSource === 'hub_rotate')
581
+ ) {
582
+ this._syncEnvNodeSecretToStore(envSecret);
583
+ return envSecret;
584
+ }
496
585
 
497
586
  if (envSecret && storeSecret && envSecret !== storeSecret) {
498
587
  // Store value came from a successful hub rotation -> trust it.
499
588
  // The env var is necessarily stale: it was captured by the parent
500
589
  // shell before the rotation and a child process cannot mutate it
501
590
  // back into its parent.
502
- if (storeSource === 'hub_rotate' && valid(storeSecret)) {
591
+ if (storeSource === 'hub_rotate' && validNodeSecret(storeSecret) && !envUpdatedAfterSuppression) {
503
592
  if (!this._storeSourceLogged) {
504
593
  this._storeSourceLogged = true;
505
594
  this.logger.warn(
506
595
  '[lifecycle] A2A_NODE_SECRET env var differs from MailboxStore; ' +
507
596
  'store value originated from a hub rotation, treating env as stale. ' +
508
- 'Run `evolver reset-local-secret` after a manual web reset, or ' +
509
- 'unset A2A_NODE_SECRET to silence this warning.'
597
+ 'After a manual web reset, run `evolver reset-local-secret` to clear local ' +
598
+ 'secret and env suppression state before relying only on updated env vars.'
510
599
  );
511
600
  }
512
601
  return storeSecret;
513
602
  }
514
- if (valid(envSecret)) {
515
- const envVersion = parseNodeSecretVersion(process.env.A2A_NODE_SECRET_VERSION || process.env.EVOMAP_NODE_SECRET_VERSION);
516
- this.store.setState('node_secret', envSecret);
517
- this.store.setState('node_secret_version', envVersion ? String(envVersion) : '');
518
- // Mark the new store value as env-seeded so a future rotation can
519
- // distinguish "operator pasted this in" from "hub returned this".
520
- this.store.setState('node_secret_source', 'env_seed');
603
+ if (validNodeSecret(envSecret)) {
604
+ this._syncEnvNodeSecretToStore(envSecret);
521
605
  if (!this._envOverrideLogged) {
522
606
  this._envOverrideLogged = true;
523
607
  this.logger.warn(
@@ -534,6 +618,68 @@ class LifecycleManager {
534
618
  return storeSecret || envSecret || null;
535
619
  }
536
620
 
621
+ _syncEnvNodeSecretToStore(envSecret) {
622
+ const envVersion = parseNodeSecretVersion(process.env.A2A_NODE_SECRET_VERSION || process.env.EVOMAP_NODE_SECRET_VERSION);
623
+ this.store.setState('node_secret', envSecret);
624
+ this.store.setState('node_secret_version', envVersion ? String(envVersion) : '');
625
+ // Mark the new store value as env-seeded so a future rotation can
626
+ // distinguish "operator pasted this in" from "hub returned this".
627
+ this.store.setState('node_secret_source', 'env_seed');
628
+ this._clearEnvSecretSuppression();
629
+ }
630
+
631
+ _effectiveEnvNodeSecret() {
632
+ const envSecret = getEnvNodeSecret();
633
+ return this._isEnvNodeSecretSuppressed(envSecret) ? null : envSecret;
634
+ }
635
+
636
+ _isEnvNodeSecretSuppressed(envSecret) {
637
+ this._envSuppressionClearedForSecret = null;
638
+ const persistedMarker = normalizeNodeSecretEnvSuppression(this.store && this.store.getState
639
+ ? this.store.getState('node_secret_env_suppressed')
640
+ : null);
641
+ const marker = persistedMarker || this._envSecretSuppressionMarker;
642
+ if (!marker) {
643
+ this._envSecretSuppressionMarker = null;
644
+ this._suppressEnvSecret = false;
645
+ return false;
646
+ }
647
+ this._envSecretSuppressionMarker = marker;
648
+ if (marker === 'true') {
649
+ this._suppressEnvSecret = Boolean(envSecret);
650
+ return Boolean(envSecret);
651
+ }
652
+ const envFingerprint = fingerprintNodeSecret(envSecret);
653
+ if (!envFingerprint) {
654
+ this._suppressEnvSecret = false;
655
+ return false;
656
+ }
657
+ if (envFingerprint === marker) {
658
+ this._suppressEnvSecret = true;
659
+ return true;
660
+ }
661
+ this._clearEnvSecretSuppression();
662
+ this._envSuppressionClearedForSecret = envSecret;
663
+ return false;
664
+ }
665
+
666
+ _markCurrentEnvSecretSuppressed() {
667
+ const marker = fingerprintNodeSecret(getEnvNodeSecret());
668
+ if (!marker) {
669
+ this._clearEnvSecretSuppression();
670
+ return;
671
+ }
672
+ try { this.store.setState('node_secret_env_suppressed', marker); } catch { /* best-effort */ }
673
+ this._envSecretSuppressionMarker = marker;
674
+ this._suppressEnvSecret = true;
675
+ }
676
+
677
+ _clearEnvSecretSuppression() {
678
+ try { this.store.setState('node_secret_env_suppressed', ''); } catch { /* best-effort */ }
679
+ this._envSecretSuppressionMarker = null;
680
+ this._suppressEnvSecret = false;
681
+ }
682
+
537
683
  _buildHeaders() {
538
684
  const headers = { 'Content-Type': 'application/json' };
539
685
  const secret = this.nodeSecret;
@@ -607,28 +753,64 @@ class LifecycleManager {
607
753
  await throwIfHubUnreachableResponse(res, 'lifecycle hello');
608
754
  this._recordHubReachable();
609
755
  if (!res.ok) {
610
- const errData = await readHubResponseJson(res).catch(() => ({}));
611
- const errMsg = errData?.error || `http_${res.status}`;
756
+ const errText = await readHubResponseText(res).catch(() => '');
757
+ let errData = {};
758
+ try { errData = errText ? JSON.parse(errText) : {}; } catch (_) { /* non-JSON API error */ }
759
+ const errMsg = safeHubErrorCode(errData, res.status);
612
760
  if (res.status === 429) {
613
761
  const retryAfter = parseInt(res.headers.get('retry-after') || '3600', 10);
614
762
  this._helloRateLimitUntil = Date.now() + retryAfter * 1000;
615
763
  this.logger.error(`[lifecycle] hello rate limited (429): retry after ${retryAfter}s`);
616
764
  return { ok: false, error: 'hello_rate_limited', retryAfter };
617
765
  }
618
- this.logger.error(`[lifecycle] hello HTTP ${res.status}: ${errMsg}`);
766
+ const divergenceReason = (res.status === 401 || res.status === 403)
767
+ ? extractSecretDivergenceReason(errData)
768
+ : null;
769
+ if (divergenceReason) {
770
+ this.logger.warn(
771
+ `[lifecycle] hello rejected secret rotation (${res.status}, reason=${divergenceReason}); ` +
772
+ 'local secret divergence detected'
773
+ );
774
+ return {
775
+ ok: false,
776
+ error: SECRET_DIVERGENCE_ERROR,
777
+ reason: divergenceReason,
778
+ statusCode: res.status,
779
+ };
780
+ }
781
+ const safeErrText = errText ? sanitizeHubResponseForLog(errText) : errMsg;
782
+ this.logger.error(`[lifecycle] hello HTTP ${res.status}: ${safeErrText}`);
619
783
  return { ok: false, error: errMsg, statusCode: res.status };
620
784
  }
621
785
 
622
786
  const data = await readHubResponseJson(res);
623
787
 
624
788
  if (data?.payload?.status === 'rejected') {
625
- this.logger.error(`[lifecycle] hello rejected: ${data.payload.reason || 'unknown'}`);
626
- return { ok: false, error: data.payload.reason || 'hello_rejected', response: data };
789
+ const divergenceReason = extractSecretDivergenceReason(data);
790
+ if (divergenceReason) {
791
+ this.logger.warn(
792
+ `[lifecycle] hello rejected secret rotation (reason=${divergenceReason}); ` +
793
+ 'local secret divergence detected'
794
+ );
795
+ return {
796
+ ok: false,
797
+ error: SECRET_DIVERGENCE_ERROR,
798
+ reason: divergenceReason,
799
+ response: data,
800
+ };
801
+ }
802
+ const reason = data.payload.reason || 'unknown';
803
+ const safeReason = sanitizeHubResponseForLog(reason);
804
+ this.logger.error(`[lifecycle] hello rejected: ${safeReason}`);
805
+ const error = String(reason || '').startsWith('node_id_already_claimed')
806
+ ? reason
807
+ : 'hello_rejected';
808
+ return { ok: false, error, response: data };
627
809
  }
628
810
 
629
811
  const secret = data?.payload?.node_secret || data?.node_secret || null;
630
812
  const secretVersion = parseNodeSecretVersion(data?.payload?.node_secret_version || data?.node_secret_version);
631
- if (secret && /^[a-f0-9]{64}$/i.test(secret)) {
813
+ if (secret && validNodeSecret(secret)) {
632
814
  this.store.setState('node_secret', secret);
633
815
  // Tag the store entry so the next process that boots into a stale
634
816
  // shell env can recognise this value as hub-authoritative and
@@ -643,7 +825,11 @@ class LifecycleManager {
643
825
  // and overwrite the freshly rotated secret with the stale one,
644
826
  // re-creating the auth loop the previous patch fixed (see #529
645
827
  // and the Bugbot review on PR #22).
646
- this._suppressEnvSecret = true;
828
+ if (getEnvNodeSecret() && getEnvNodeSecret() !== secret) {
829
+ this._markCurrentEnvSecretSuppressed();
830
+ } else {
831
+ this._clearEnvSecretSuppression();
832
+ }
647
833
  this.logger.log('[lifecycle] new node_secret stored from hello response');
648
834
  }
649
835
  if (secretVersion) {
@@ -713,6 +899,7 @@ class LifecycleManager {
713
899
  this._reauthInProgress = true;
714
900
  let manualResetRequired = false;
715
901
  let hubUnreachable = false;
902
+ let droppedDivergedSecret = false;
716
903
  try {
717
904
  for (let attempt = 1; attempt <= MAX_REAUTH_ATTEMPTS; attempt++) {
718
905
  this.logger.warn(`[lifecycle] re-auth attempt ${attempt}/${MAX_REAUTH_ATTEMPTS}: rotating secret via hello...`);
@@ -731,6 +918,18 @@ class LifecycleManager {
731
918
  hubUnreachable = true;
732
919
  break;
733
920
  }
921
+ if (isSecretDivergenceError(helloResult.error)) {
922
+ // The hub has explicitly rejected our current secret as diverged
923
+ // from its record. Drop store/env-derived auth once, then spend
924
+ // the second attempt on an unauthenticated rotate hello instead
925
+ // of backing off while still presenting the same stale Bearer.
926
+ if (attempt < MAX_REAUTH_ATTEMPTS && !droppedDivergedSecret) {
927
+ this._dropLocalSecret(SECRET_DIVERGENCE_ERROR);
928
+ droppedDivergedSecret = true;
929
+ continue;
930
+ }
931
+ break;
932
+ }
734
933
  if (typeof helloResult.error === 'string' && helloResult.error.startsWith('node_id_already_claimed')) {
735
934
  // Hub does not believe we own this nodeId. Our locally cached
736
935
  // secret(s) are useless. Drop them so attempt 2 retries WITHOUT
@@ -806,10 +1005,9 @@ class LifecycleManager {
806
1005
  try { this.store.setState('node_secret_version', ''); } catch { /* best-effort */ }
807
1006
  // Clear the source tag too -- nothing is stored, nothing to attribute.
808
1007
  try { this.store.setState('node_secret_source', ''); } catch { /* best-effort */ }
809
- try { this.store.setState('node_secret_env_suppressed', 'true'); } catch { /* best-effort */ }
810
- // Suppress the env override for this process so _resolveNodeSecret stops
811
- // re-seeding the store with the same stale env value next call.
812
- this._suppressEnvSecret = true;
1008
+ // Suppress only the exact env secret that was just proven stale. If no
1009
+ // env secret is present, do not leave a marker that blocks a future reset.
1010
+ this._markCurrentEnvSecretSuppressed();
813
1011
  }
814
1012
 
815
1013
  _emitManualResetNeeded() {
@@ -821,7 +1019,7 @@ class LifecycleManager {
821
1019
  payload: {
822
1020
  action: 'manual_secret_reset_required',
823
1021
  message:
824
- 'Hub disowns this node_id (node_id_already_claimed). Local node_secret in MailboxStore and A2A_NODE_SECRET env var are both invalid. Visit https://evomap.ai/account, click "Reset Secret" on the agent card, then update A2A_NODE_SECRET (or delete ~/.evomap/mailbox/state.json) and restart proxy.',
1022
+ 'Hub disowns this node_id (node_id_already_claimed). Local node_secret and the current node secret env value are invalid. Visit https://evomap.ai/account, click "Reset Secret" on the agent card, run `evolver reset-local-secret` to clear local secret and env suppression state, then update A2A_NODE_SECRET/EVOMAP_NODE_SECRET and restart proxy. Only changing env before clearing suppression state can keep an older local marker active.',
825
1023
  docs_url: 'https://evomap.ai/account',
826
1024
  },
827
1025
  });
@@ -924,7 +1122,8 @@ class LifecycleManager {
924
1122
  if (res.status === 403 || res.status === 401) {
925
1123
  this._consecutiveFailures++;
926
1124
  const errText = await readHubResponseText(res).catch(() => '');
927
- this.logger.error(`[lifecycle] heartbeat auth failed (${res.status}): ${errText}`);
1125
+ const safeErrText = sanitizeHubResponseForLog(errText);
1126
+ this.logger.error(`[lifecycle] heartbeat auth failed (${res.status}): ${safeErrText}`);
928
1127
  if (!_skipReauth) {
929
1128
  const recovered = await this.reAuthenticate();
930
1129
  if (recovered) {
@@ -937,6 +1136,7 @@ class LifecycleManager {
937
1136
 
938
1137
  if (!res.ok) {
939
1138
  const errText = await readHubResponseText(res).catch(() => '');
1139
+ const safeErrText = sanitizeHubResponseForLog(errText);
940
1140
  // 426 Upgrade Required: hub emits this when our evolver_version is
941
1141
  // below the minimum version it requires. The body is JSON of shape
942
1142
  // `{ error: 'evolver_min_version_required', force_update: {...} }`
@@ -961,7 +1161,7 @@ class LifecycleManager {
961
1161
  _maybeTriggerForceUpdateFromHeartbeat(fu, this.logger);
962
1162
  } else {
963
1163
  this.logger.warn(
964
- `[lifecycle] heartbeat HTTP 426 without parseable force_update payload: ${errText}`
1164
+ `[lifecycle] heartbeat HTTP 426 without parseable force_update payload: ${safeErrText}`
965
1165
  );
966
1166
  }
967
1167
  }
@@ -1000,7 +1200,7 @@ class LifecycleManager {
1000
1200
  }
1001
1201
  }
1002
1202
  this._consecutiveFailures++;
1003
- this.logger.error(`[lifecycle] heartbeat HTTP ${res.status}: ${errText}`);
1203
+ this.logger.error(`[lifecycle] heartbeat HTTP ${res.status}: ${safeErrText}`);
1004
1204
  return { ok: false, error: `http_${res.status}`, statusCode: res.status };
1005
1205
  }
1006
1206
 
@@ -9,6 +9,7 @@ const {
9
9
  isHubUnreachableError,
10
10
  readHubResponseJson,
11
11
  readHubResponseText,
12
+ sanitizeHubResponseForLog,
12
13
  throwIfHubUnreachableResponse,
13
14
  } = require('../../gep/hubFetch');
14
15
 
@@ -75,12 +76,12 @@ class InboundSync {
75
76
 
76
77
  if (res.status === 403 || res.status === 401) {
77
78
  const errText = await readHubResponseText(res).catch(() => 'unknown');
78
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
79
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
79
80
  }
80
81
 
81
82
  if (!res.ok) {
82
83
  const errText = await readHubResponseText(res).catch(() => 'unknown');
83
- throw new Error(`Hub returned ${res.status}: ${errText}`);
84
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
84
85
  }
85
86
 
86
87
  const data = await readHubResponseJson(res);
@@ -163,11 +164,11 @@ class InboundSync {
163
164
  this._recordHubReachable();
164
165
  if (res.status === 403 || res.status === 401) {
165
166
  const errText = await readHubResponseText(res).catch(() => 'unknown');
166
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
167
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
167
168
  }
168
169
  if (!res.ok) {
169
170
  const errText = await readHubResponseText(res).catch(() => 'unknown');
170
- throw new Error(`Hub returned ${res.status}: ${errText}`);
171
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
171
172
  }
172
173
  await drainHubResponse(res);
173
174
  return { acked: delivered.length };
@@ -9,6 +9,7 @@ const {
9
9
  isHubUnreachableError,
10
10
  readHubResponseJson,
11
11
  readHubResponseText,
12
+ sanitizeHubResponseForLog,
12
13
  throwIfHubUnreachableResponse,
13
14
  } = require('../../gep/hubFetch');
14
15
 
@@ -109,12 +110,12 @@ class OutboundSync {
109
110
 
110
111
  if (res.status === 403 || res.status === 401) {
111
112
  const errText = await readHubResponseText(res).catch(() => 'unknown');
112
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
113
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
113
114
  }
114
115
 
115
116
  if (!res.ok) {
116
117
  const errText = await readHubResponseText(res).catch(() => 'unknown');
117
- throw new Error(`Hub returned ${res.status}: ${errText}`);
118
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
118
119
  }
119
120
 
120
121
  const data = await readHubResponseJson(res);