@evomap/evolver 1.89.4 → 1.89.5
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/CONTRIBUTING.md +19 -0
- package/README.md +536 -86
- package/assets/cover.png +0 -0
- package/index.js +87 -7
- package/package.json +17 -6
- package/scripts/a2a_export.js +63 -0
- package/scripts/a2a_ingest.js +79 -0
- package/scripts/a2a_promote.js +118 -0
- package/scripts/analyze_by_skill.js +121 -0
- package/scripts/build_binaries.js +479 -0
- package/scripts/check-changelog.js +166 -0
- package/scripts/extract_log.js +85 -0
- package/scripts/generate_history.js +75 -0
- package/scripts/gep_append_event.js +96 -0
- package/scripts/gep_personality_report.js +234 -0
- package/scripts/human_report.js +147 -0
- package/scripts/recall-verify-report.js +234 -0
- package/scripts/recover_loop.js +61 -0
- package/scripts/refresh_stars_badge.js +168 -0
- package/scripts/seed-merchants.js +91 -0
- package/scripts/suggest_version.js +89 -0
- package/scripts/validate-modules.js +38 -0
- package/scripts/validate-suite.js +78 -0
- package/skills/index.json +14 -0
- package/src/adapters/scripts/_runtimePaths.js +1 -0
- package/src/adapters/scripts/evolver-session-end.js +1 -0
- package/src/adapters/scripts/evolver-session-start.js +1 -0
- package/src/evolve/guards.js +1 -721
- package/src/evolve/pipeline/collect.js +1 -1283
- package/src/evolve/pipeline/dispatch.js +1 -421
- package/src/evolve/pipeline/enrich.js +1 -440
- package/src/evolve/pipeline/hub.js +1 -319
- package/src/evolve/pipeline/select.js +1 -274
- package/src/evolve/pipeline/signals.js +1 -206
- package/src/evolve/utils.js +1 -264
- package/src/evolve.js +1 -350
- package/src/gep/a2aProtocol.js +1 -4455
- package/src/gep/antiAbuseTelemetry.js +1 -233
- package/src/gep/autoDistillConv.js +1 -205
- package/src/gep/autoDistillLlm.js +1 -315
- package/src/gep/candidateEval.js +1 -92
- package/src/gep/candidates.js +1 -198
- package/src/gep/contentHash.js +1 -30
- package/src/gep/conversationSniffer.js +1 -266
- package/src/gep/crypto.js +1 -89
- package/src/gep/curriculum.js +1 -163
- package/src/gep/deviceId.js +1 -218
- package/src/gep/envFingerprint.js +1 -118
- package/src/gep/epigenetics.js +1 -31
- package/src/gep/execBridge.js +1 -711
- package/src/gep/explore.js +1 -289
- package/src/gep/hash.js +1 -15
- package/src/gep/hubFetch.js +1 -359
- package/src/gep/hubReview.js +1 -207
- package/src/gep/hubSearch.js +1 -526
- package/src/gep/hubVerify.js +1 -306
- package/src/gep/idleScheduler.js +6 -1
- package/src/gep/learningSignals.js +1 -89
- package/src/gep/memoryGraph.js +1 -1374
- package/src/gep/memoryGraphAdapter.js +1 -203
- package/src/gep/mutation.js +1 -203
- package/src/gep/narrativeMemory.js +1 -108
- package/src/gep/openPRRegistry.js +1 -205
- package/src/gep/personality.js +1 -423
- package/src/gep/policyCheck.js +1 -599
- package/src/gep/prompt.js +1 -836
- package/src/gep/recallInject.js +1 -409
- package/src/gep/recallVerifier.js +1 -318
- package/src/gep/reflection.js +1 -177
- package/src/gep/selector.js +1 -602
- package/src/gep/skillDistiller.js +1 -1294
- package/src/gep/solidify.js +1 -1699
- package/src/gep/strategy.js +1 -136
- package/src/gep/tokenSavings.js +1 -88
- package/src/gep/workspaceKeychain.js +1 -174
- package/src/ops/lifecycle.js +17 -4
- package/src/proxy/extensions/traceControl.js +1 -99
- package/src/proxy/index.js +206 -1
- package/src/proxy/inject.js +1 -52
- package/src/proxy/lifecycle/manager.js +12 -0
- package/src/proxy/mailbox/store.js +29 -6
- package/src/proxy/router/responses_route.js +157 -0
- package/src/proxy/server/http.js +13 -4
- package/src/proxy/server/routes.js +11 -1
- package/src/proxy/sync/engine.js +7 -1
- package/src/proxy/sync/outbound.js +32 -4
- package/src/proxy/trace/extractor.js +1 -646
- package/src/proxy/trace/usage.js +1 -105
- package/.cursor/BUGBOT.md +0 -182
- package/.env.example +0 -68
- package/.git-commit-guard-token +0 -1
- package/.github/CODEOWNERS +0 -63
- package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -23
- package/.github/pull_request_template.md +0 -45
- package/.github/workflows/test.yml +0 -75
- package/CHANGELOG.md +0 -1237
- package/README.public.md +0 -569
- package/SECURITY.md +0 -108
- package/assets/gep/events.jsonl +0 -3
- package/examples/atp-consumer-quickstart.md +0 -100
- package/examples/hello-world.md +0 -38
- package/proxy-package.json +0 -39
- package/public.manifest.json +0 -143
- /package/assets/gep/{genes.json → genes.seed.json} +0 -0
- /package/{bundled-skills → skills}/_meta/SKILL.md +0 -0
|
@@ -7,6 +7,24 @@ const crypto = require('crypto');
|
|
|
7
7
|
const DEFAULT_CHANNEL = 'evomap-hub';
|
|
8
8
|
const SCHEMA_VERSION = 1;
|
|
9
9
|
const PROXY_PROTOCOL_VERSION = '0.1.0';
|
|
10
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
11
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
12
|
+
|
|
13
|
+
function bestEffortChmod(filePath, mode) {
|
|
14
|
+
try { fs.chmodSync(filePath, mode); } catch { /* best effort; no-op on Windows */ }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensurePrivateDir(dir) {
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
20
|
+
}
|
|
21
|
+
bestEffortChmod(dir, PRIVATE_DIR_MODE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writePrivateFile(filePath, content) {
|
|
25
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
26
|
+
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
// Merge `fields` into `target` while stripping keys that can mutate the
|
|
12
30
|
// prototype chain. Mailbox rows are persisted as JSONL and rebuilt on
|
|
@@ -90,8 +108,8 @@ function safeParse(payload) {
|
|
|
90
108
|
// regular files that POSIX local filesystems do, so the removal above is
|
|
91
109
|
// equally valid on Windows. No platform-specific code is needed here.
|
|
92
110
|
function appendLine(filePath, obj) {
|
|
93
|
-
fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
|
|
94
|
-
|
|
111
|
+
fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
112
|
+
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
function readLines(filePath) {
|
|
@@ -112,11 +130,13 @@ function readLines(filePath) {
|
|
|
112
130
|
class MailboxStore {
|
|
113
131
|
constructor(dataDir) {
|
|
114
132
|
if (!dataDir) throw new Error('dataDir is required');
|
|
115
|
-
|
|
133
|
+
ensurePrivateDir(dataDir);
|
|
116
134
|
this.dataDir = dataDir;
|
|
117
135
|
|
|
118
136
|
this._messagesFile = path.join(dataDir, 'messages.jsonl');
|
|
119
137
|
this._stateFile = path.join(dataDir, 'state.json');
|
|
138
|
+
bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
|
|
139
|
+
bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
|
|
120
140
|
|
|
121
141
|
// in-memory indexes
|
|
122
142
|
this._messages = new Map(); // id -> message object
|
|
@@ -156,7 +176,7 @@ class MailboxStore {
|
|
|
156
176
|
|
|
157
177
|
_persistState() {
|
|
158
178
|
const dir = path.dirname(this._stateFile);
|
|
159
|
-
|
|
179
|
+
ensurePrivateDir(dir);
|
|
160
180
|
// Round-7 (§20.5): per-PID tmp path. Two evolver processes (daemon +
|
|
161
181
|
// ad-hoc CLI / proxy + loop) writing to the same `${stateFile}.tmp`
|
|
162
182
|
// would otherwise interleave: process B's writeFileSync truncates
|
|
@@ -167,7 +187,7 @@ class MailboxStore {
|
|
|
167
187
|
// for 30 min..4 h" symptom this branch targets. Matches the
|
|
168
188
|
// precedent set by _persistNodeSecret in src/gep/a2aProtocol.js.
|
|
169
189
|
const tmp = `${this._stateFile}.${process.pid}.tmp`;
|
|
170
|
-
|
|
190
|
+
writePrivateFile(tmp, JSON.stringify(this._state, null, 2) + '\n');
|
|
171
191
|
// Windows: fs.renameSync throws EPERM when the destination file already
|
|
172
192
|
// exists, unlike POSIX where rename(2) atomically replaces the target.
|
|
173
193
|
// Remove the destination first so the rename succeeds on all platforms.
|
|
@@ -179,6 +199,7 @@ class MailboxStore {
|
|
|
179
199
|
}
|
|
180
200
|
}
|
|
181
201
|
fs.renameSync(tmp, this._stateFile);
|
|
202
|
+
bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
_rebuildIndex() {
|
|
@@ -436,11 +457,12 @@ class MailboxStore {
|
|
|
436
457
|
}
|
|
437
458
|
entries.sort((a, b) => a.created_at - b.created_at);
|
|
438
459
|
|
|
439
|
-
const fd = fs.openSync(tmpFile, 'w');
|
|
460
|
+
const fd = fs.openSync(tmpFile, 'w', PRIVATE_FILE_MODE);
|
|
440
461
|
for (const msg of entries) {
|
|
441
462
|
fs.writeSync(fd, JSON.stringify(msg) + '\n');
|
|
442
463
|
}
|
|
443
464
|
fs.closeSync(fd);
|
|
465
|
+
bestEffortChmod(tmpFile, PRIVATE_FILE_MODE);
|
|
444
466
|
// Windows: renameSync throws EPERM when the destination already exists.
|
|
445
467
|
// Remove it first so the swap succeeds on all platforms.
|
|
446
468
|
if (process.platform === 'win32') {
|
|
@@ -449,6 +471,7 @@ class MailboxStore {
|
|
|
449
471
|
}
|
|
450
472
|
}
|
|
451
473
|
fs.renameSync(tmpFile, this._messagesFile);
|
|
474
|
+
bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
|
|
452
475
|
this._rebuildIndex();
|
|
453
476
|
}
|
|
454
477
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createProxyTrace } = require('../trace/extractor');
|
|
4
|
+
|
|
5
|
+
const OPENAI_RESPONSE_HEADER_ALLOWLIST = new Set([
|
|
6
|
+
'openai-processing-ms',
|
|
7
|
+
'openai-version',
|
|
8
|
+
'retry-after',
|
|
9
|
+
'x-request-id',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function hasOpenAIUpstreamCredential() {
|
|
13
|
+
if (process.env.EVOMAP_OPENAI_API_KEY || process.env.OPENAI_API_KEY) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function upstreamStatus(err, fallback = 502) {
|
|
18
|
+
const status = Number(err && err.statusCode);
|
|
19
|
+
return Number.isFinite(status) ? status : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeOpenAIConfigDiagnostic(err) {
|
|
23
|
+
const message = err && typeof err.message === 'string' ? err.message : '';
|
|
24
|
+
if (message.startsWith('[proxy] EVOMAP_OPENAI_BASE_URL ')) return message;
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function asUpstreamError(err, fallback = 502) {
|
|
29
|
+
if (err && err.statusCode && /^openai upstream /.test(err.message || '')) return err;
|
|
30
|
+
const diagnostic = safeOpenAIConfigDiagnostic(err);
|
|
31
|
+
const message = diagnostic
|
|
32
|
+
? `openai upstream request failed: ${diagnostic}`
|
|
33
|
+
: 'openai upstream request failed';
|
|
34
|
+
const out = new Error(message);
|
|
35
|
+
out.statusCode = upstreamStatus(err, fallback);
|
|
36
|
+
out.cause = err;
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function responseToBody(raw, status, headers, log) {
|
|
41
|
+
if (!raw) return {};
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
log.warn?.(JSON.stringify({
|
|
46
|
+
event: 'openai_responses_fallback',
|
|
47
|
+
reason: 'upstream_non_json',
|
|
48
|
+
upstream_status: status,
|
|
49
|
+
content_type: headers && headers['content-type'] || '',
|
|
50
|
+
response_bytes: Buffer.byteLength(raw),
|
|
51
|
+
}));
|
|
52
|
+
return { error: raw };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function copyOpenAIResponseHeaders(headers = {}) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
59
|
+
const lower = String(name || '').toLowerCase();
|
|
60
|
+
if (!OPENAI_RESPONSE_HEADER_ALLOWLIST.has(lower) && !lower.startsWith('x-ratelimit-')) continue;
|
|
61
|
+
if (value === undefined || value === null) continue;
|
|
62
|
+
const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
|
|
63
|
+
if (/[\r\n]/.test(headerValue)) continue;
|
|
64
|
+
out[lower] = headerValue;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued } = {}) {
|
|
70
|
+
if (typeof openAIProxy !== 'function') {
|
|
71
|
+
throw new Error('buildResponsesHandler requires openAIProxy(path, body, opts)');
|
|
72
|
+
}
|
|
73
|
+
const log = logger || console;
|
|
74
|
+
|
|
75
|
+
return async ({ body, headers }) => {
|
|
76
|
+
const inboundHeaders = headers || {};
|
|
77
|
+
if (!hasOpenAIUpstreamCredential()) {
|
|
78
|
+
throw Object.assign(new Error('openai api key required'), { statusCode: 401 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const originalModel = body && typeof body.model === 'string' ? body.model : null;
|
|
82
|
+
let trace = null;
|
|
83
|
+
try {
|
|
84
|
+
trace = createProxyTrace({
|
|
85
|
+
route: 'POST /v1/responses',
|
|
86
|
+
headers: inboundHeaders,
|
|
87
|
+
body,
|
|
88
|
+
upstreamMode: 'openai',
|
|
89
|
+
originalModel,
|
|
90
|
+
chosenModel: originalModel,
|
|
91
|
+
store: traceStore,
|
|
92
|
+
logger: traceStore ? log : null,
|
|
93
|
+
onTraceQueued,
|
|
94
|
+
});
|
|
95
|
+
} catch (_) { /* best-effort trace; never break the request */ }
|
|
96
|
+
|
|
97
|
+
let upstream;
|
|
98
|
+
try {
|
|
99
|
+
upstream = await openAIProxy('/responses', body, {
|
|
100
|
+
inboundHeaders,
|
|
101
|
+
upstreamMode: 'openai',
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const wrapped = asUpstreamError(err, upstreamStatus(err));
|
|
105
|
+
trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'openai', model: originalModel });
|
|
106
|
+
throw wrapped;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (upstream.stream) {
|
|
110
|
+
const forwardHeaders = copyOpenAIResponseHeaders(upstream.headers);
|
|
111
|
+
const ct = upstream.headers && upstream.headers['content-type'];
|
|
112
|
+
if (ct) forwardHeaders['Content-Type'] = ct;
|
|
113
|
+
trace?.recordStreamStart({
|
|
114
|
+
status: upstream.status,
|
|
115
|
+
upstreamMode: 'openai',
|
|
116
|
+
model: originalModel,
|
|
117
|
+
headers: forwardHeaders,
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
status: upstream.status,
|
|
121
|
+
stream: upstream.stream,
|
|
122
|
+
headers: forwardHeaders,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let raw = '';
|
|
127
|
+
if (upstream.text) {
|
|
128
|
+
try {
|
|
129
|
+
raw = await upstream.text();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const wrapped = asUpstreamError(err, upstreamStatus(err));
|
|
132
|
+
trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'openai', model: originalModel });
|
|
133
|
+
throw wrapped;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const respBody = responseToBody(raw, upstream.status, upstream.headers, log);
|
|
137
|
+
trace?.record({
|
|
138
|
+
status: upstream.status,
|
|
139
|
+
responseBody: respBody,
|
|
140
|
+
upstreamMode: 'openai',
|
|
141
|
+
model: originalModel,
|
|
142
|
+
headers: upstream.headers,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
status: upstream.status,
|
|
146
|
+
body: respBody,
|
|
147
|
+
headers: copyOpenAIResponseHeaders(upstream.headers),
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
buildResponsesHandler,
|
|
154
|
+
copyOpenAIResponseHeaders,
|
|
155
|
+
hasOpenAIUpstreamCredential,
|
|
156
|
+
responseToBody,
|
|
157
|
+
};
|
package/src/proxy/server/http.js
CHANGED
|
@@ -64,12 +64,21 @@ function parseBody(req, opts) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function sendJson(res, status, body) {
|
|
67
|
+
function sendJson(res, status, body, extraHeaders = {}) {
|
|
68
68
|
const payload = JSON.stringify(body);
|
|
69
|
-
|
|
69
|
+
const headers = {
|
|
70
70
|
'Content-Type': 'application/json',
|
|
71
71
|
'Content-Length': Buffer.byteLength(payload),
|
|
72
|
-
}
|
|
72
|
+
};
|
|
73
|
+
for (const [name, value] of Object.entries(extraHeaders || {})) {
|
|
74
|
+
if (value === undefined || value === null) continue;
|
|
75
|
+
const lower = String(name).toLowerCase();
|
|
76
|
+
if (lower === 'content-length' || lower === 'transfer-encoding' || lower === 'connection') continue;
|
|
77
|
+
const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
|
|
78
|
+
if (/[\r\n]/.test(headerValue)) continue;
|
|
79
|
+
headers[name] = headerValue;
|
|
80
|
+
}
|
|
81
|
+
res.writeHead(status, headers);
|
|
73
82
|
res.end(payload);
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -196,7 +205,7 @@ class ProxyHttpServer {
|
|
|
196
205
|
if (result && result.stream) {
|
|
197
206
|
await this._streamResponse(res, result);
|
|
198
207
|
} else {
|
|
199
|
-
sendJson(res, result.status || 200, result.body || result);
|
|
208
|
+
sendJson(res, result.status || 200, result.body || result, result.headers);
|
|
200
209
|
}
|
|
201
210
|
} catch (err) {
|
|
202
211
|
this.logger.error(`[proxy] ${routeKey} error:`, err.message);
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
const { PROXY_PROTOCOL_VERSION, SCHEMA_VERSION } = require('../mailbox/store');
|
|
4
4
|
|
|
5
5
|
function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
dmHandler,
|
|
8
|
+
skillUpdater,
|
|
9
|
+
getHubMailboxStatus,
|
|
10
|
+
sessionHandler,
|
|
11
|
+
messagesHandler,
|
|
12
|
+
responsesHandler,
|
|
13
|
+
} = extensions || {};
|
|
7
14
|
const routes = {
|
|
8
15
|
// -- Mailbox --
|
|
9
16
|
'POST /mailbox/send': async ({ body }) => {
|
|
@@ -466,6 +473,9 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
|
|
|
466
473
|
if (messagesHandler) {
|
|
467
474
|
routes['POST /v1/messages'] = messagesHandler;
|
|
468
475
|
}
|
|
476
|
+
if (responsesHandler) {
|
|
477
|
+
routes['POST /v1/responses'] = responsesHandler;
|
|
478
|
+
}
|
|
469
479
|
|
|
470
480
|
return routes;
|
|
471
481
|
}
|
package/src/proxy/sync/engine.js
CHANGED
|
@@ -8,13 +8,14 @@ const DEFAULT_OUTBOUND_INTERVAL = 5_000;
|
|
|
8
8
|
const IDLE_THRESHOLD = 5 * 60_000;
|
|
9
9
|
|
|
10
10
|
class SyncEngine {
|
|
11
|
-
constructor({ store, hubUrl, getHeaders, logger, onInboundReceived, onAuthError }) {
|
|
11
|
+
constructor({ store, hubUrl, getHeaders, logger, onInboundReceived, onAuthError, onOutboundFlushed }) {
|
|
12
12
|
this.store = store;
|
|
13
13
|
this.hubUrl = hubUrl;
|
|
14
14
|
this.logger = logger || console;
|
|
15
15
|
this.getHeaders = getHeaders;
|
|
16
16
|
this.onInboundReceived = onInboundReceived || null;
|
|
17
17
|
this.onAuthError = onAuthError || null;
|
|
18
|
+
this.onOutboundFlushed = onOutboundFlushed || null;
|
|
18
19
|
|
|
19
20
|
this.outbound = new OutboundSync({ store, hubUrl, getHeaders, logger });
|
|
20
21
|
this.inbound = new InboundSync({ store, hubUrl, getHeaders, logger });
|
|
@@ -82,6 +83,11 @@ class SyncEngine {
|
|
|
82
83
|
try {
|
|
83
84
|
const result = await this.outbound.flush();
|
|
84
85
|
if (result.sent > 0) this._lastActivity = Date.now();
|
|
86
|
+
if ((result.sent > 0 || result.dropped > 0) && typeof this.onOutboundFlushed === 'function') {
|
|
87
|
+
try { this.onOutboundFlushed(result); } catch (e) {
|
|
88
|
+
this.logger.warn?.('[sync] onOutboundFlushed callback failed:', e.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
85
91
|
} catch (err) {
|
|
86
92
|
if (err instanceof AuthError) {
|
|
87
93
|
await this._handleAuthError('outbound');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { PROXY_PROTOCOL_VERSION } = require('../mailbox/store');
|
|
4
4
|
const { AuthError } = require('../lifecycle/manager');
|
|
5
|
+
const { isProxyTraceUploadPayloadAllowed, resolveTraceMode } = require('../trace/extractor');
|
|
5
6
|
const { hubFetch } = require('../../gep/hubFetch');
|
|
6
7
|
|
|
7
8
|
const MAX_BATCH = 50;
|
|
@@ -16,8 +17,31 @@ class OutboundSync {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async flush(channel = 'evomap-hub') {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
20
|
+
const pendingBatch = this.store.pollOutbound({ channel, limit: MAX_BATCH });
|
|
21
|
+
if (pendingBatch.length === 0) return { sent: 0 };
|
|
22
|
+
|
|
23
|
+
let pending = pendingBatch;
|
|
24
|
+
const rejectedTraceUploads = [];
|
|
25
|
+
const traceUploadEnabled = resolveTraceMode(process.env, { store: this.store });
|
|
26
|
+
for (const m of pendingBatch) {
|
|
27
|
+
if (m.type !== 'proxy_trace') continue;
|
|
28
|
+
if (!traceUploadEnabled) {
|
|
29
|
+
rejectedTraceUploads.push({ id: m.id, error: 'proxy trace upload disabled' });
|
|
30
|
+
} else if (!isProxyTraceUploadPayloadAllowed(m.payload, process.env)) {
|
|
31
|
+
rejectedTraceUploads.push({ id: m.id, error: 'proxy trace payload rejected' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (rejectedTraceUploads.length > 0) {
|
|
35
|
+
this.store.updateStatusBatch(rejectedTraceUploads.map(m => ({
|
|
36
|
+
id: m.id,
|
|
37
|
+
status: 'rejected',
|
|
38
|
+
error: m.error,
|
|
39
|
+
})));
|
|
40
|
+
const rejectedIds = new Set(rejectedTraceUploads.map(m => m.id));
|
|
41
|
+
pending = pendingBatch.filter(m => !rejectedIds.has(m.id));
|
|
42
|
+
if (pending.length === 0) return { sent: 0, dropped: rejectedTraceUploads.length };
|
|
43
|
+
}
|
|
44
|
+
const dropped = rejectedTraceUploads.length;
|
|
21
45
|
|
|
22
46
|
const endpoint = `${this.hubUrl}/a2a/mailbox/outbound`;
|
|
23
47
|
|
|
@@ -83,14 +107,18 @@ class OutboundSync {
|
|
|
83
107
|
if (inboundMessages.length > 0) this.store.writeInboundBatch(inboundMessages);
|
|
84
108
|
|
|
85
109
|
this.store.setState('last_sync_at', new Date().toISOString());
|
|
86
|
-
|
|
110
|
+
const result = { sent: pending.length, synced: updates.length, responses: inboundMessages.length };
|
|
111
|
+
if (dropped > 0) result.dropped = dropped;
|
|
112
|
+
return result;
|
|
87
113
|
} catch (err) {
|
|
88
114
|
if (err instanceof AuthError) throw err;
|
|
89
115
|
this.logger.error(`[outbound] flush failed: ${err.message}`);
|
|
90
116
|
for (const m of pending) {
|
|
91
117
|
this.store.incrementRetry(m.id, err.message);
|
|
92
118
|
}
|
|
93
|
-
|
|
119
|
+
const result = { sent: 0, error: err.message };
|
|
120
|
+
if (dropped > 0) result.dropped = dropped;
|
|
121
|
+
return result;
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
124
|
}
|