@evomap/evolver 1.89.11 → 1.89.13
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/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/gep/a2aProtocol.js +1 -1
- package/src/gep/antiAbuseTelemetry.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/conversationDistiller.js +1 -0
- 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/questionGenerator.js +103 -0
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.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/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +108 -7
- package/src/proxy/server/routes.js +31 -0
- 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 +1 -1
- package/src/proxy/trace/usage.js +1 -1
|
@@ -100,6 +100,29 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
100
100
|
return { body: result };
|
|
101
101
|
},
|
|
102
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
|
+
|
|
103
126
|
'GET /asset/submissions': async ({ query }) => {
|
|
104
127
|
const submissions = store.list({
|
|
105
128
|
type: 'asset_submit',
|
|
@@ -378,6 +401,10 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
378
401
|
const inPending = store.countPending({ direction: 'inbound' });
|
|
379
402
|
const nodeId = store.getState('node_id');
|
|
380
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');
|
|
381
408
|
return {
|
|
382
409
|
body: {
|
|
383
410
|
status: 'running',
|
|
@@ -387,6 +414,10 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
387
414
|
outbound_pending: outPending,
|
|
388
415
|
inbound_pending: inPending,
|
|
389
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,
|
|
390
421
|
},
|
|
391
422
|
};
|
|
392
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);
|