@evomap/evolver 1.89.10 → 1.89.12
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/.cursor/BUGBOT.md +182 -0
- package/.env.example +68 -0
- package/.git-commit-guard-token +1 -0
- package/.github/CODEOWNERS +63 -0
- package/.github/ISSUE_TEMPLATE/good_first_issue.md +23 -0
- package/.github/pull_request_template.md +45 -0
- package/.github/workflows/test.yml +75 -0
- package/CHANGELOG.md +1367 -0
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +90 -536
- package/README.public.md +578 -0
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +108 -0
- package/assets/gep/events.jsonl +3 -0
- package/examples/atp-consumer-quickstart.md +100 -0
- package/examples/hello-world.md +38 -0
- package/index.js +5 -3
- package/package.json +6 -18
- package/proxy-package.json +39 -0
- package/public.manifest.json +145 -0
- package/src/adapters/scripts/evolver-session-end.js +18 -37
- package/src/atp/atpExecute.js +27 -71
- package/src/atp/serviceHelper.js +28 -36
- package/src/config.js +17 -0
- package/src/evolve/guards.js +721 -1
- package/src/evolve/pipeline/collect.js +1283 -1
- package/src/evolve/pipeline/dispatch.js +421 -1
- package/src/evolve/pipeline/enrich.js +440 -1
- package/src/evolve/pipeline/hub.js +319 -1
- package/src/evolve/pipeline/select.js +274 -1
- package/src/evolve/pipeline/signals.js +206 -1
- package/src/evolve/utils.js +264 -1
- package/src/evolve.js +350 -1
- package/src/gep/a2aProtocol.js +4463 -1
- package/src/gep/antiAbuseTelemetry.js +233 -1
- package/src/gep/autoDistillConv.js +205 -1
- package/src/gep/autoDistillLlm.js +315 -1
- package/src/gep/candidateEval.js +92 -1
- package/src/gep/candidates.js +198 -1
- package/src/gep/contentHash.js +30 -1
- package/src/gep/conversationDistiller.js +270 -0
- package/src/gep/conversationSniffer.js +266 -1
- package/src/gep/crypto.js +89 -1
- package/src/gep/curriculum.js +163 -1
- package/src/gep/deviceId.js +218 -1
- package/src/gep/envFingerprint.js +118 -1
- package/src/gep/epigenetics.js +31 -1
- package/src/gep/execBridge.js +712 -1
- package/src/gep/explore.js +289 -1
- package/src/gep/hash.js +15 -1
- package/src/gep/hubFetch.js +608 -1
- package/src/gep/hubReview.js +207 -1
- package/src/gep/hubSearch.js +526 -1
- package/src/gep/hubVerify.js +306 -1
- package/src/gep/learningSignals.js +89 -1
- package/src/gep/memoryGraph.js +1449 -1
- package/src/gep/memoryGraphAdapter.js +203 -1
- package/src/gep/mutation.js +203 -1
- package/src/gep/narrativeMemory.js +108 -1
- package/src/gep/oauthLogin.js +9 -3
- package/src/gep/openPRRegistry.js +205 -1
- package/src/gep/personality.js +423 -1
- package/src/gep/policyCheck.js +599 -1
- package/src/gep/privacyClient.js +10 -9
- package/src/gep/prompt.js +836 -1
- package/src/gep/questionGenerator.js +103 -0
- package/src/gep/recallInject.js +409 -1
- package/src/gep/recallVerifier.js +318 -1
- package/src/gep/reflection.js +177 -1
- package/src/gep/savingsCore.js +112 -1
- package/src/gep/selector.js +602 -1
- package/src/gep/signals.js +85 -17
- package/src/gep/skillDistiller.js +1294 -1
- package/src/gep/solidify.js +1699 -1
- package/src/gep/strategy.js +136 -1
- package/src/gep/tokenSavings.js +95 -1
- package/src/gep/workspaceKeychain.js +174 -1
- package/src/proxy/extensions/traceControl.js +109 -1
- package/src/proxy/index.js +100 -3
- package/src/proxy/inject.js +52 -1
- package/src/proxy/lifecycle/manager.js +108 -7
- package/src/proxy/server/routes.js +41 -7
- package/src/proxy/server/settings.js +6 -2
- package/src/proxy/sync/engine.js +31 -15
- package/src/proxy/sync/inbound.js +87 -9
- package/src/proxy/sync/outbound.js +57 -4
- package/src/proxy/trace/extractor.js +1403 -1
- package/src/proxy/trace/usage.js +105 -1
- package/CONTRIBUTING.md +0 -19
- package/assets/cover.png +0 -0
- package/scripts/a2a_export.js +0 -63
- package/scripts/a2a_ingest.js +0 -79
- package/scripts/a2a_promote.js +0 -118
- package/scripts/analyze_by_skill.js +0 -121
- package/scripts/build_binaries.js +0 -479
- package/scripts/check-changelog.js +0 -166
- package/scripts/extract_log.js +0 -85
- package/scripts/generate_history.js +0 -75
- package/scripts/gep_append_event.js +0 -96
- package/scripts/gep_personality_report.js +0 -234
- package/scripts/human_report.js +0 -147
- package/scripts/recall-verify-report.js +0 -234
- package/scripts/recover_loop.js +0 -61
- package/scripts/refresh_stars_badge.js +0 -168
- package/scripts/seed-merchants.js +0 -91
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/skills/index.json +0 -14
- /package/assets/gep/{genes.seed.json → genes.json} +0 -0
- /package/{skills → bundled-skills}/_meta/SKILL.md +0 -0
|
@@ -5,7 +5,14 @@ const path = require('path');
|
|
|
5
5
|
const { PROXY_PROTOCOL_VERSION } = require('../mailbox/store');
|
|
6
6
|
const { buildEnvelope } = require('../envelope');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
hubFetch,
|
|
10
|
+
hubUnreachableBackoffMs,
|
|
11
|
+
isHubUnreachableError,
|
|
12
|
+
readHubResponseJson,
|
|
13
|
+
readHubResponseText,
|
|
14
|
+
throwIfHubUnreachableResponse,
|
|
15
|
+
} = require('../../gep/hubFetch');
|
|
9
16
|
const { getEvomapPath } = require('../../gep/paths');
|
|
10
17
|
// last_update transit (PR #188): proxy heartbeat ferries a pending
|
|
11
18
|
// force_update outcome to the hub, then clears the state file on 2xx.
|
|
@@ -372,6 +379,8 @@ class LifecycleManager {
|
|
|
372
379
|
this._consecutiveReauthFailures = 0;
|
|
373
380
|
this._driftInterval = null;
|
|
374
381
|
this._lastDriftCheckAt = 0;
|
|
382
|
+
this._hubUnreachableFailures = 0;
|
|
383
|
+
this._hubUnreachableUntil = 0;
|
|
375
384
|
|
|
376
385
|
// H4 fix: persist the legacy node_id file as soon as the in-memory
|
|
377
386
|
// node_id is known, NOT only after a successful hello(). The original
|
|
@@ -508,6 +517,26 @@ class LifecycleManager {
|
|
|
508
517
|
return headers;
|
|
509
518
|
}
|
|
510
519
|
|
|
520
|
+
_hubUnreachableWaitMs() {
|
|
521
|
+
return Math.max(0, this._hubUnreachableUntil - Date.now());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_recordHubReachable() {
|
|
525
|
+
this._hubUnreachableFailures = 0;
|
|
526
|
+
this._hubUnreachableUntil = 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_recordHubUnreachable(err) {
|
|
530
|
+
this._hubUnreachableFailures += 1;
|
|
531
|
+
const retryAfterMs = hubUnreachableBackoffMs(this._hubUnreachableFailures);
|
|
532
|
+
this._hubUnreachableUntil = Date.now() + retryAfterMs;
|
|
533
|
+
this.logger.warn?.(
|
|
534
|
+
`[lifecycle] Hub unreachable; backing off for ${Math.ceil(retryAfterMs / 1000)}s: ` +
|
|
535
|
+
`${err && err.message || err}`
|
|
536
|
+
);
|
|
537
|
+
return retryAfterMs;
|
|
538
|
+
}
|
|
539
|
+
|
|
511
540
|
async hello({ rotateSecret = false } = {}) {
|
|
512
541
|
if (!this.hubUrl) return { ok: false, error: 'no_hub_url' };
|
|
513
542
|
|
|
@@ -517,6 +546,15 @@ class LifecycleManager {
|
|
|
517
546
|
return { ok: false, error: 'hello_rate_limit_active', waitSec };
|
|
518
547
|
}
|
|
519
548
|
|
|
549
|
+
const waitMs = this._hubUnreachableWaitMs();
|
|
550
|
+
if (waitMs > 0) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
error: 'hub_unreachable_backoff',
|
|
554
|
+
retryAfterMs: waitMs,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
520
558
|
const endpoint = `${this.hubUrl}/a2a/hello`;
|
|
521
559
|
const nodeId = this.store.getState('node_id')
|
|
522
560
|
|| _readLegacyNodeId()
|
|
@@ -539,8 +577,10 @@ class LifecycleManager {
|
|
|
539
577
|
body: JSON.stringify(body),
|
|
540
578
|
signal: AbortSignal.timeout(HELLO_TIMEOUT),
|
|
541
579
|
});
|
|
580
|
+
await throwIfHubUnreachableResponse(res, 'lifecycle hello');
|
|
581
|
+
this._recordHubReachable();
|
|
542
582
|
if (!res.ok) {
|
|
543
|
-
const errData = await res
|
|
583
|
+
const errData = await readHubResponseJson(res).catch(() => ({}));
|
|
544
584
|
const errMsg = errData?.error || `http_${res.status}`;
|
|
545
585
|
if (res.status === 429) {
|
|
546
586
|
const retryAfter = parseInt(res.headers.get('retry-after') || '3600', 10);
|
|
@@ -552,7 +592,7 @@ class LifecycleManager {
|
|
|
552
592
|
return { ok: false, error: errMsg, statusCode: res.status };
|
|
553
593
|
}
|
|
554
594
|
|
|
555
|
-
const data = await res
|
|
595
|
+
const data = await readHubResponseJson(res);
|
|
556
596
|
|
|
557
597
|
if (data?.payload?.status === 'rejected') {
|
|
558
598
|
this.logger.error(`[lifecycle] hello rejected: ${data.payload.reason || 'unknown'}`);
|
|
@@ -604,6 +644,15 @@ class LifecycleManager {
|
|
|
604
644
|
this.logger.log(`[lifecycle] hello OK, node_id=${nodeId}${rotateSecret ? ' (secret rotated)' : ''}`);
|
|
605
645
|
return { ok: true, nodeId, response: data };
|
|
606
646
|
} catch (err) {
|
|
647
|
+
if (isHubUnreachableError(err)) {
|
|
648
|
+
const retryAfterMs = this._recordHubUnreachable(err);
|
|
649
|
+
return {
|
|
650
|
+
ok: false,
|
|
651
|
+
error: 'hub_unreachable',
|
|
652
|
+
detail: err.message,
|
|
653
|
+
retryAfterMs,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
607
656
|
this.logger.error(`[lifecycle] hello failed: ${err.message}`);
|
|
608
657
|
return { ok: false, error: err.message };
|
|
609
658
|
}
|
|
@@ -630,6 +679,7 @@ class LifecycleManager {
|
|
|
630
679
|
}
|
|
631
680
|
this._reauthInProgress = true;
|
|
632
681
|
let manualResetRequired = false;
|
|
682
|
+
let hubUnreachable = false;
|
|
633
683
|
try {
|
|
634
684
|
for (let attempt = 1; attempt <= MAX_REAUTH_ATTEMPTS; attempt++) {
|
|
635
685
|
this.logger.warn(`[lifecycle] re-auth attempt ${attempt}/${MAX_REAUTH_ATTEMPTS}: rotating secret via hello...`);
|
|
@@ -637,6 +687,17 @@ class LifecycleManager {
|
|
|
637
687
|
if (!helloResult.ok) {
|
|
638
688
|
this.logger.error(`[lifecycle] re-auth hello failed: ${helloResult.error}`);
|
|
639
689
|
if (helloResult.error === 'hello_rate_limited' || helloResult.error === 'hello_rate_limit_active') break;
|
|
690
|
+
// Hub link is down (WAF HTML, network error, timeout) -- NOT an auth
|
|
691
|
+
// failure. Bail without arming the re-auth backoff: otherwise a
|
|
692
|
+
// transient outage that arrives mid-rotate would burn both attempts
|
|
693
|
+
// (attempt 2's hello short-circuits on the hub-unreachable window)
|
|
694
|
+
// and suppress genuine auth recovery for up to REAUTH_BACKOFF_MAX_MS,
|
|
695
|
+
// even though a JSON 401/403 retry would succeed once the hub is
|
|
696
|
+
// reachable again. The hub-unreachable window already gates re-entry.
|
|
697
|
+
if (helloResult.error === 'hub_unreachable' || helloResult.error === 'hub_unreachable_backoff') {
|
|
698
|
+
hubUnreachable = true;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
640
701
|
if (typeof helloResult.error === 'string' && helloResult.error.startsWith('node_id_already_claimed')) {
|
|
641
702
|
// Hub does not believe we own this nodeId. Our locally cached
|
|
642
703
|
// secret(s) are useless. Drop them so attempt 2 retries WITHOUT
|
|
@@ -667,8 +728,19 @@ class LifecycleManager {
|
|
|
667
728
|
// fire a second time anyway. Resetting the flag was misleading.
|
|
668
729
|
return true;
|
|
669
730
|
}
|
|
731
|
+
// Same as the hello path: if the verification heartbeat fails because
|
|
732
|
+
// the hub link dropped (not an auth rejection), defer without arming
|
|
733
|
+
// the re-auth backoff.
|
|
734
|
+
if (hbResult.error === 'hub_unreachable' || hbResult.error === 'hub_unreachable_backoff') {
|
|
735
|
+
hubUnreachable = true;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
670
738
|
this.logger.warn(`[lifecycle] re-auth attempt ${attempt}: heartbeat still failing after rotate`);
|
|
671
739
|
}
|
|
740
|
+
if (hubUnreachable) {
|
|
741
|
+
this.logger.warn('[lifecycle] re-auth deferred: hub unreachable (no re-auth backoff armed)');
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
672
744
|
if (manualResetRequired) {
|
|
673
745
|
this._emitManualResetNeeded();
|
|
674
746
|
}
|
|
@@ -734,6 +806,15 @@ class LifecycleManager {
|
|
|
734
806
|
// unchanged; only the boundary moved earlier.
|
|
735
807
|
if (!this.hubUrl) return { ok: false, error: 'no_hub_url' };
|
|
736
808
|
try {
|
|
809
|
+
const waitMs = this._hubUnreachableWaitMs();
|
|
810
|
+
if (waitMs > 0) {
|
|
811
|
+
return {
|
|
812
|
+
ok: false,
|
|
813
|
+
error: 'hub_unreachable_backoff',
|
|
814
|
+
retryAfterMs: waitMs,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
737
818
|
const nodeId = this.nodeId;
|
|
738
819
|
if (!nodeId) {
|
|
739
820
|
const helloResult = await this.hello();
|
|
@@ -798,9 +879,12 @@ class LifecycleManager {
|
|
|
798
879
|
signal: AbortSignal.timeout(HEARTBEAT_TIMEOUT),
|
|
799
880
|
});
|
|
800
881
|
|
|
882
|
+
await throwIfHubUnreachableResponse(res, 'lifecycle heartbeat');
|
|
883
|
+
this._recordHubReachable();
|
|
884
|
+
|
|
801
885
|
if (res.status === 403 || res.status === 401) {
|
|
802
886
|
this._consecutiveFailures++;
|
|
803
|
-
const errText = await res
|
|
887
|
+
const errText = await readHubResponseText(res).catch(() => '');
|
|
804
888
|
this.logger.error(`[lifecycle] heartbeat auth failed (${res.status}): ${errText}`);
|
|
805
889
|
if (!_skipReauth) {
|
|
806
890
|
const recovered = await this.reAuthenticate();
|
|
@@ -813,7 +897,7 @@ class LifecycleManager {
|
|
|
813
897
|
}
|
|
814
898
|
|
|
815
899
|
if (!res.ok) {
|
|
816
|
-
const errText = await res
|
|
900
|
+
const errText = await readHubResponseText(res).catch(() => '');
|
|
817
901
|
// 426 Upgrade Required: hub emits this when our evolver_version is
|
|
818
902
|
// below the minimum version it requires. The body is JSON of shape
|
|
819
903
|
// `{ error: 'evolver_min_version_required', force_update: {...} }`
|
|
@@ -881,7 +965,7 @@ class LifecycleManager {
|
|
|
881
965
|
return { ok: false, error: `http_${res.status}`, statusCode: res.status };
|
|
882
966
|
}
|
|
883
967
|
|
|
884
|
-
const data = await res
|
|
968
|
+
const data = await readHubResponseJson(res);
|
|
885
969
|
|
|
886
970
|
this._consecutiveFailures = 0;
|
|
887
971
|
this.store.setState('last_heartbeat_at', new Date().toISOString());
|
|
@@ -981,6 +1065,16 @@ class LifecycleManager {
|
|
|
981
1065
|
|
|
982
1066
|
return { ok: true, response: data };
|
|
983
1067
|
} catch (err) {
|
|
1068
|
+
if (isHubUnreachableError(err)) {
|
|
1069
|
+
this._consecutiveFailures++;
|
|
1070
|
+
const retryAfterMs = this._recordHubUnreachable(err);
|
|
1071
|
+
return {
|
|
1072
|
+
ok: false,
|
|
1073
|
+
error: 'hub_unreachable',
|
|
1074
|
+
detail: err.message,
|
|
1075
|
+
retryAfterMs,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
984
1078
|
this._consecutiveFailures++;
|
|
985
1079
|
this.logger.error(`[lifecycle] heartbeat failed (${this._consecutiveFailures}): ${err.message}`);
|
|
986
1080
|
return { ok: false, error: err.message };
|
|
@@ -1074,7 +1168,14 @@ class LifecycleManager {
|
|
|
1074
1168
|
// `DEFAULT_HEARTBEAT_INTERVAL` (6min) or the exponential branch
|
|
1075
1169
|
// retries faster than the success branch (Bugbot #147 finding).
|
|
1076
1170
|
const interval = this._heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
|
|
1077
|
-
const
|
|
1171
|
+
const hubWaitMs = this._hubUnreachableWaitMs();
|
|
1172
|
+
// While a hub-unreachable window is active, wake exactly when it elapses
|
|
1173
|
+
// (1s floor to avoid a busy 0ms reschedule), mirroring SyncEngine's
|
|
1174
|
+
// `Math.max(1_000, retryAfterMs)`. A 30s floor over-waited up to ~30s past
|
|
1175
|
+
// the window's end, delaying recovery once the hub was reachable again.
|
|
1176
|
+
const backoff = hubWaitMs > 0
|
|
1177
|
+
? Math.max(1_000, hubWaitMs)
|
|
1178
|
+
: this._consecutiveFailures > 0
|
|
1078
1179
|
? Math.min(interval * Math.pow(2, this._consecutiveFailures), HEARTBEAT_BACKOFF_CAP_MS)
|
|
1079
1180
|
: interval;
|
|
1080
1181
|
const armedGen = this._heartbeatGen;
|
|
@@ -86,17 +86,43 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
86
86
|
},
|
|
87
87
|
|
|
88
88
|
'POST /asset/submit': async ({ body }) => {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// The publish path builds a bundle from full asset objects; a bare
|
|
90
|
+
// asset_id is not a valid input here (Bugbot #256 Medium — route used to
|
|
91
|
+
// accept asset_id the handler then ignored).
|
|
92
|
+
if (!body.assets && !body.asset) {
|
|
93
|
+
throw Object.assign(new Error('assets (array) or asset (single object) is required'), { statusCode: 400 });
|
|
91
94
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Publish synchronously via the signed Gene+Capsule bundle path
|
|
96
|
+
// (POST /a2a/publish). The old `asset_submit` mailbox dispatch is gated
|
|
97
|
+
// off at the Hub (A2A_MAILBOX_ASSET_SUBMIT_ENABLED), so it silently
|
|
98
|
+
// failed; the Hub now enforces bundles. Returns the per-asset Hub result.
|
|
99
|
+
const result = await proxyHandlers.assetPublish(body);
|
|
97
100
|
return { body: result };
|
|
98
101
|
},
|
|
99
102
|
|
|
103
|
+
'POST /conversation/distill': async ({ body }) => {
|
|
104
|
+
const { distillConversation } = require('../../gep/conversationDistiller');
|
|
105
|
+
const input = body || {};
|
|
106
|
+
const distill = distillConversation(input, { persist: input.persist !== false });
|
|
107
|
+
if (!distill.ok) return { body: distill };
|
|
108
|
+
|
|
109
|
+
let publishResult = null;
|
|
110
|
+
if (input.publish !== false) {
|
|
111
|
+
publishResult = await proxyHandlers.assetPublish({
|
|
112
|
+
assets: [distill.gene, distill.capsule].filter(Boolean),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
body: {
|
|
118
|
+
...distill,
|
|
119
|
+
queued: false,
|
|
120
|
+
published: Boolean(publishResult),
|
|
121
|
+
publish_result: publishResult,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
|
|
100
126
|
'GET /asset/submissions': async ({ query }) => {
|
|
101
127
|
const submissions = store.list({
|
|
102
128
|
type: 'asset_submit',
|
|
@@ -375,6 +401,10 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
375
401
|
const inPending = store.countPending({ direction: 'inbound' });
|
|
376
402
|
const nodeId = store.getState('node_id');
|
|
377
403
|
const lastSync = store.getState('last_sync_at');
|
|
404
|
+
const lastSyncError = store.getState('last_sync_error');
|
|
405
|
+
const hubAuthStatus = store.getState('hub_auth_status');
|
|
406
|
+
const reauthBackoffUntil = store.getState('reauth_backoff_until');
|
|
407
|
+
const helloRateLimitUntil = store.getState('hello_rate_limit_until');
|
|
378
408
|
return {
|
|
379
409
|
body: {
|
|
380
410
|
status: 'running',
|
|
@@ -384,6 +414,10 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
384
414
|
outbound_pending: outPending,
|
|
385
415
|
inbound_pending: inPending,
|
|
386
416
|
last_sync_at: lastSync,
|
|
417
|
+
last_sync_error: lastSyncError || null,
|
|
418
|
+
hub_auth_status: hubAuthStatus || null,
|
|
419
|
+
reauth_backoff_until: reauthBackoffUntil || null,
|
|
420
|
+
hello_rate_limit_until: helloRateLimitUntil || null,
|
|
387
421
|
},
|
|
388
422
|
};
|
|
389
423
|
},
|
|
@@ -47,15 +47,19 @@ function writeSettings(data) {
|
|
|
47
47
|
return merged;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function clearSettings() {
|
|
50
|
+
function clearSettings(opts = {}) {
|
|
51
51
|
try {
|
|
52
52
|
const file = getSettingsFile();
|
|
53
53
|
if (fs.existsSync(file)) {
|
|
54
54
|
const current = readSettings();
|
|
55
|
+
const proxyPid = current.proxy?.pid;
|
|
56
|
+
if (!opts.force && proxyPid && proxyPid !== process.pid) return false;
|
|
55
57
|
delete current.proxy;
|
|
56
58
|
fs.writeFileSync(file, JSON.stringify(current, null, 2), 'utf8');
|
|
59
|
+
return true;
|
|
57
60
|
}
|
|
58
61
|
} catch {}
|
|
62
|
+
return false;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
function isStaleProxy() {
|
|
@@ -82,7 +86,7 @@ function isStaleProxy() {
|
|
|
82
86
|
|
|
83
87
|
function clearIfStale() {
|
|
84
88
|
if (isStaleProxy()) {
|
|
85
|
-
clearSettings();
|
|
89
|
+
clearSettings({ force: true });
|
|
86
90
|
return true;
|
|
87
91
|
}
|
|
88
92
|
return false;
|
package/src/proxy/sync/engine.js
CHANGED
|
@@ -77,12 +77,14 @@ class SyncEngine {
|
|
|
77
77
|
// the whole tick, schedule the next iteration in `finally` so a
|
|
78
78
|
// surprise throw cannot park the loop.
|
|
79
79
|
let nextDelay = DEFAULT_OUTBOUND_INTERVAL;
|
|
80
|
+
let hubRetryAfterMs = 0;
|
|
80
81
|
try {
|
|
81
82
|
if (!this._running) return;
|
|
82
83
|
this._outPending = true;
|
|
83
84
|
try {
|
|
84
85
|
const result = await this.outbound.flush();
|
|
85
86
|
if (result.sent > 0) this._lastActivity = Date.now();
|
|
87
|
+
if (result.retryAfterMs > 0) hubRetryAfterMs = result.retryAfterMs;
|
|
86
88
|
if ((result.sent > 0 || result.dropped > 0) && typeof this.onOutboundFlushed === 'function') {
|
|
87
89
|
try { this.onOutboundFlushed(result); } catch (e) {
|
|
88
90
|
this.logger.warn?.('[sync] onOutboundFlushed callback failed:', e.message);
|
|
@@ -96,13 +98,17 @@ class SyncEngine {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
this._outPending = false;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
if (hubRetryAfterMs > 0) {
|
|
102
|
+
nextDelay = Math.max(1_000, hubRetryAfterMs);
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
const pending = this.store.countPending({ direction: 'outbound' });
|
|
106
|
+
if (pending > 0) nextDelay = 1_000;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// countPending threw (corrupt store, FS hiccup): keep the
|
|
109
|
+
// default cadence rather than parking the loop.
|
|
110
|
+
this.logger.error(`[sync] countPending threw (non-fatal): ${err && err.message}`);
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
} catch (err) {
|
|
108
114
|
// Anything that escaped the inner blocks above. Log and let
|
|
@@ -123,11 +129,14 @@ class SyncEngine {
|
|
|
123
129
|
// from inbound.pull / ackDelivered / _isIdle used to escape the
|
|
124
130
|
// setTimeout callback and silently park the inbound loop.
|
|
125
131
|
let nextDelay = DEFAULT_POLL_INTERVAL_ACTIVE;
|
|
132
|
+
let hubRetryAfterMs = 0;
|
|
126
133
|
try {
|
|
127
134
|
if (!this._running) return;
|
|
128
135
|
try {
|
|
129
136
|
const result = await this.inbound.pull();
|
|
130
|
-
if (result.
|
|
137
|
+
if (result.retryAfterMs > 0) {
|
|
138
|
+
hubRetryAfterMs = result.retryAfterMs;
|
|
139
|
+
} else if (result.received > 0) {
|
|
131
140
|
this._lastActivity = Date.now();
|
|
132
141
|
if (typeof this.onInboundReceived === 'function') {
|
|
133
142
|
try { this.onInboundReceived(result.received); } catch (e) {
|
|
@@ -135,7 +144,10 @@ class SyncEngine {
|
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
|
-
|
|
147
|
+
if (!hubRetryAfterMs) {
|
|
148
|
+
const ack = await this.inbound.ackDelivered();
|
|
149
|
+
if (ack.retryAfterMs > 0) hubRetryAfterMs = ack.retryAfterMs;
|
|
150
|
+
}
|
|
139
151
|
} catch (err) {
|
|
140
152
|
if (err instanceof AuthError) {
|
|
141
153
|
await this._handleAuthError('inbound');
|
|
@@ -143,12 +155,16 @@ class SyncEngine {
|
|
|
143
155
|
this.logger.error(`[sync] inbound error: ${err.message}`);
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
|
-
|
|
147
|
-
nextDelay =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
if (hubRetryAfterMs > 0) {
|
|
159
|
+
nextDelay = Math.max(1_000, hubRetryAfterMs);
|
|
160
|
+
} else {
|
|
161
|
+
try {
|
|
162
|
+
nextDelay = this._isIdle()
|
|
163
|
+
? DEFAULT_POLL_INTERVAL_IDLE
|
|
164
|
+
: DEFAULT_POLL_INTERVAL_ACTIVE;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.logger.error(`[sync] _isIdle threw (non-fatal): ${err && err.message}`);
|
|
167
|
+
}
|
|
152
168
|
}
|
|
153
169
|
} catch (err) {
|
|
154
170
|
this.logger.error(`[sync] inbound tick threw (non-fatal): ${err && err.message}`);
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { PROXY_PROTOCOL_VERSION } = require('../mailbox/store');
|
|
4
4
|
const { AuthError } = require('../lifecycle/manager');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
drainHubResponse,
|
|
7
|
+
hubFetch,
|
|
8
|
+
hubUnreachableBackoffMs,
|
|
9
|
+
isHubUnreachableError,
|
|
10
|
+
readHubResponseJson,
|
|
11
|
+
readHubResponseText,
|
|
12
|
+
throwIfHubUnreachableResponse,
|
|
13
|
+
} = require('../../gep/hubFetch');
|
|
6
14
|
|
|
7
15
|
const DEFAULT_POLL_INTERVAL_ACTIVE = 10_000;
|
|
8
16
|
const DEFAULT_POLL_INTERVAL_IDLE = 60_000;
|
|
@@ -13,9 +21,41 @@ class InboundSync {
|
|
|
13
21
|
this.hubUrl = hubUrl;
|
|
14
22
|
this.logger = logger || console;
|
|
15
23
|
this.getHeaders = getHeaders;
|
|
24
|
+
this._hubUnreachableFailures = 0;
|
|
25
|
+
this._hubUnreachableUntil = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_hubUnreachableWaitMs() {
|
|
29
|
+
return Math.max(0, this._hubUnreachableUntil - Date.now());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_recordHubReachable() {
|
|
33
|
+
this._hubUnreachableFailures = 0;
|
|
34
|
+
this._hubUnreachableUntil = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_recordHubUnreachable(err) {
|
|
38
|
+
this._hubUnreachableFailures += 1;
|
|
39
|
+
const retryAfterMs = hubUnreachableBackoffMs(this._hubUnreachableFailures);
|
|
40
|
+
this._hubUnreachableUntil = Date.now() + retryAfterMs;
|
|
41
|
+
this.logger.warn?.(
|
|
42
|
+
`[inbound] Hub unreachable; backing off for ${Math.ceil(retryAfterMs / 1000)}s: ` +
|
|
43
|
+
`${err && err.message || err}`
|
|
44
|
+
);
|
|
45
|
+
return retryAfterMs;
|
|
16
46
|
}
|
|
17
47
|
|
|
18
48
|
async pull(channel = 'evomap-hub', limit = 50) {
|
|
49
|
+
const waitMs = this._hubUnreachableWaitMs();
|
|
50
|
+
if (waitMs > 0) {
|
|
51
|
+
return {
|
|
52
|
+
received: 0,
|
|
53
|
+
error: 'hub_unreachable_backoff',
|
|
54
|
+
hubUnreachable: true,
|
|
55
|
+
retryAfterMs: waitMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
const cursorKey = `${channel}:inbound_cursor`;
|
|
20
60
|
const cursor = this.store.getCursor(cursorKey);
|
|
21
61
|
|
|
@@ -30,17 +70,20 @@ class InboundSync {
|
|
|
30
70
|
signal: AbortSignal.timeout(35_000),
|
|
31
71
|
});
|
|
32
72
|
|
|
73
|
+
await throwIfHubUnreachableResponse(res, 'inbound pull');
|
|
74
|
+
this._recordHubReachable();
|
|
75
|
+
|
|
33
76
|
if (res.status === 403 || res.status === 401) {
|
|
34
|
-
const errText = await res
|
|
77
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
35
78
|
throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
|
|
36
79
|
}
|
|
37
80
|
|
|
38
81
|
if (!res.ok) {
|
|
39
|
-
const errText = await res
|
|
82
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
40
83
|
throw new Error(`Hub returned ${res.status}: ${errText}`);
|
|
41
84
|
}
|
|
42
85
|
|
|
43
|
-
const data = await res
|
|
86
|
+
const data = await readHubResponseJson(res);
|
|
44
87
|
const messages = data.messages || [];
|
|
45
88
|
|
|
46
89
|
if (messages.length > 0) {
|
|
@@ -64,12 +107,31 @@ class InboundSync {
|
|
|
64
107
|
return { received: messages.length, cursor: data.next_cursor || cursor };
|
|
65
108
|
} catch (err) {
|
|
66
109
|
if (err instanceof AuthError) throw err;
|
|
110
|
+
if (isHubUnreachableError(err)) {
|
|
111
|
+
const retryAfterMs = this._recordHubUnreachable(err);
|
|
112
|
+
return {
|
|
113
|
+
received: 0,
|
|
114
|
+
error: 'hub_unreachable',
|
|
115
|
+
hubUnreachable: true,
|
|
116
|
+
retryAfterMs,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
67
119
|
this.logger.error(`[inbound] pull failed: ${err.message}`);
|
|
68
120
|
return { received: 0, error: err.message };
|
|
69
121
|
}
|
|
70
122
|
}
|
|
71
123
|
|
|
72
124
|
async ackDelivered(channel = 'evomap-hub') {
|
|
125
|
+
const waitMs = this._hubUnreachableWaitMs();
|
|
126
|
+
if (waitMs > 0) {
|
|
127
|
+
return {
|
|
128
|
+
acked: 0,
|
|
129
|
+
error: 'hub_unreachable_backoff',
|
|
130
|
+
hubUnreachable: true,
|
|
131
|
+
retryAfterMs: waitMs,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
73
135
|
const delivered = this.store.list({
|
|
74
136
|
type: '%',
|
|
75
137
|
direction: 'inbound',
|
|
@@ -97,13 +159,29 @@ class InboundSync {
|
|
|
97
159
|
body: JSON.stringify({ sender_id: senderId, message_ids: delivered.map(m => m.id) }),
|
|
98
160
|
signal: AbortSignal.timeout(10_000),
|
|
99
161
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
162
|
+
await throwIfHubUnreachableResponse(res, 'inbound ack');
|
|
163
|
+
this._recordHubReachable();
|
|
164
|
+
if (res.status === 403 || res.status === 401) {
|
|
165
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
166
|
+
throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
|
|
167
|
+
}
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
170
|
+
throw new Error(`Hub returned ${res.status}: ${errText}`);
|
|
171
|
+
}
|
|
172
|
+
await drainHubResponse(res);
|
|
105
173
|
return { acked: delivered.length };
|
|
106
174
|
} catch (err) {
|
|
175
|
+
if (err instanceof AuthError) throw err;
|
|
176
|
+
if (isHubUnreachableError(err)) {
|
|
177
|
+
const retryAfterMs = this._recordHubUnreachable(err);
|
|
178
|
+
return {
|
|
179
|
+
acked: 0,
|
|
180
|
+
error: 'hub_unreachable',
|
|
181
|
+
hubUnreachable: true,
|
|
182
|
+
retryAfterMs,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
107
185
|
this.logger.error(`[inbound] ack failed: ${err.message}`);
|
|
108
186
|
return { acked: 0, error: err.message };
|
|
109
187
|
}
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
const { PROXY_PROTOCOL_VERSION } = require('../mailbox/store');
|
|
4
4
|
const { AuthError } = require('../lifecycle/manager');
|
|
5
5
|
const { isProxyTraceUploadPayloadAllowed, resolveTraceMode } = require('../trace/extractor');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
hubFetch,
|
|
8
|
+
hubUnreachableBackoffMs,
|
|
9
|
+
isHubUnreachableError,
|
|
10
|
+
readHubResponseJson,
|
|
11
|
+
readHubResponseText,
|
|
12
|
+
throwIfHubUnreachableResponse,
|
|
13
|
+
} = require('../../gep/hubFetch');
|
|
7
14
|
|
|
8
15
|
const MAX_BATCH = 50;
|
|
9
16
|
const MAX_RETRIES = 10;
|
|
@@ -14,9 +21,41 @@ class OutboundSync {
|
|
|
14
21
|
this.hubUrl = hubUrl;
|
|
15
22
|
this.logger = logger || console;
|
|
16
23
|
this.getHeaders = getHeaders;
|
|
24
|
+
this._hubUnreachableFailures = 0;
|
|
25
|
+
this._hubUnreachableUntil = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_hubUnreachableWaitMs() {
|
|
29
|
+
return Math.max(0, this._hubUnreachableUntil - Date.now());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_recordHubReachable() {
|
|
33
|
+
this._hubUnreachableFailures = 0;
|
|
34
|
+
this._hubUnreachableUntil = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_recordHubUnreachable(err) {
|
|
38
|
+
this._hubUnreachableFailures += 1;
|
|
39
|
+
const retryAfterMs = hubUnreachableBackoffMs(this._hubUnreachableFailures);
|
|
40
|
+
this._hubUnreachableUntil = Date.now() + retryAfterMs;
|
|
41
|
+
this.logger.warn?.(
|
|
42
|
+
`[outbound] Hub unreachable; backing off for ${Math.ceil(retryAfterMs / 1000)}s: ` +
|
|
43
|
+
`${err && err.message || err}`
|
|
44
|
+
);
|
|
45
|
+
return retryAfterMs;
|
|
17
46
|
}
|
|
18
47
|
|
|
19
48
|
async flush(channel = 'evomap-hub') {
|
|
49
|
+
const waitMs = this._hubUnreachableWaitMs();
|
|
50
|
+
if (waitMs > 0) {
|
|
51
|
+
return {
|
|
52
|
+
sent: 0,
|
|
53
|
+
error: 'hub_unreachable_backoff',
|
|
54
|
+
hubUnreachable: true,
|
|
55
|
+
retryAfterMs: waitMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
20
59
|
const pendingBatch = this.store.pollOutbound({ channel, limit: MAX_BATCH });
|
|
21
60
|
if (pendingBatch.length === 0) return { sent: 0 };
|
|
22
61
|
|
|
@@ -65,17 +104,20 @@ class OutboundSync {
|
|
|
65
104
|
signal: AbortSignal.timeout(30_000),
|
|
66
105
|
});
|
|
67
106
|
|
|
107
|
+
await throwIfHubUnreachableResponse(res, 'outbound flush');
|
|
108
|
+
this._recordHubReachable();
|
|
109
|
+
|
|
68
110
|
if (res.status === 403 || res.status === 401) {
|
|
69
|
-
const errText = await res
|
|
111
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
70
112
|
throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
if (!res.ok) {
|
|
74
|
-
const errText = await res
|
|
116
|
+
const errText = await readHubResponseText(res).catch(() => 'unknown');
|
|
75
117
|
throw new Error(`Hub returned ${res.status}: ${errText}`);
|
|
76
118
|
}
|
|
77
119
|
|
|
78
|
-
const data = await res
|
|
120
|
+
const data = await readHubResponseJson(res);
|
|
79
121
|
const results = data.results || [];
|
|
80
122
|
|
|
81
123
|
const updates = [];
|
|
@@ -112,6 +154,17 @@ class OutboundSync {
|
|
|
112
154
|
return result;
|
|
113
155
|
} catch (err) {
|
|
114
156
|
if (err instanceof AuthError) throw err;
|
|
157
|
+
if (isHubUnreachableError(err)) {
|
|
158
|
+
const retryAfterMs = this._recordHubUnreachable(err);
|
|
159
|
+
const result = {
|
|
160
|
+
sent: 0,
|
|
161
|
+
error: 'hub_unreachable',
|
|
162
|
+
hubUnreachable: true,
|
|
163
|
+
retryAfterMs,
|
|
164
|
+
};
|
|
165
|
+
if (dropped > 0) result.dropped = dropped;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
115
168
|
this.logger.error(`[outbound] flush failed: ${err.message}`);
|
|
116
169
|
for (const m of pending) {
|
|
117
170
|
this.store.incrementRetry(m.id, err.message);
|