@evomap/evolver 1.89.17 → 1.89.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/index.js +272 -18
- package/package.json +7 -2
- package/src/adapters/claudeCode.js +16 -18
- package/src/adapters/hookAdapter.js +92 -5
- package/src/adapters/scripts/evolver-session-start.js +98 -0
- package/src/atp/atpExecute.js +3 -1
- package/src/atp/hubClient.js +5 -3
- package/src/atp/serviceHelper.js +3 -2
- 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/cliContracts.js +37 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationDistiller.js +1 -1
- package/src/gep/conversationSniffer.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/directoryClient.js +7 -3
- 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/oauthLogin.js +3 -5
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/privacyClient.js +28 -10
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skill2gep.js +329 -43
- package/src/gep/skill2gepAudit.js +303 -0
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/skillPublisher.js +8 -3
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/taskReceiver.js +3 -2
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/trajectoryExport.js +1 -0
- package/src/gep/validator/index.js +7 -2
- package/src/gep/validator/reporter.js +7 -2
- package/src/gep/validator/stakeBootstrap.js +7 -2
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/clientSettings.js +405 -0
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +13 -5
- package/src/proxy/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +82 -16
- package/src/proxy/mailbox/state.js +207 -0
- package/src/proxy/mailbox/store.js +164 -64
- package/src/proxy/router/messages_route.js +56 -4
- package/src/proxy/server/http.js +40 -6
- package/src/proxy/sync/inbound.js +31 -12
- package/src/proxy/sync/outbound.js +164 -24
- package/src/proxy/trace/extractor.js +1 -1
- package/src/proxy/trace/usage.js +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
7
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
8
|
+
const STATE_LOCK_STALE_MS = 10_000;
|
|
9
|
+
const STATE_LOCK_WAIT_MS = 10;
|
|
10
|
+
|
|
11
|
+
const MAILBOX_NODE_SECRET_STATE_KEYS = Object.freeze([
|
|
12
|
+
'node_secret',
|
|
13
|
+
'node_secret_version',
|
|
14
|
+
'node_secret_source',
|
|
15
|
+
'node_secret_env_suppressed',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const MAILBOX_NODE_SECRET_STATE_KEY_SET = new Set(MAILBOX_NODE_SECRET_STATE_KEYS);
|
|
19
|
+
const MAILBOX_NODE_SECRET_TUPLE_KEYS = Object.freeze([
|
|
20
|
+
'node_secret',
|
|
21
|
+
'node_secret_version',
|
|
22
|
+
'node_secret_source',
|
|
23
|
+
]);
|
|
24
|
+
const MAILBOX_NODE_SECRET_TUPLE_KEY_SET = new Set(MAILBOX_NODE_SECRET_TUPLE_KEYS);
|
|
25
|
+
|
|
26
|
+
function bestEffortChmod(filePath, mode) {
|
|
27
|
+
try { fs.chmodSync(filePath, mode); } catch { /* best effort; no-op on Windows */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensurePrivateDir(dir) {
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
33
|
+
}
|
|
34
|
+
bestEffortChmod(dir, PRIVATE_DIR_MODE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writePrivateFile(filePath, content) {
|
|
38
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
39
|
+
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sleepSync(ms) {
|
|
43
|
+
try {
|
|
44
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
45
|
+
} catch {
|
|
46
|
+
const end = Date.now() + ms;
|
|
47
|
+
while (Date.now() < end) {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function acquireStateFileLock(stateFile) {
|
|
52
|
+
ensurePrivateDir(path.dirname(stateFile));
|
|
53
|
+
const lockDir = `${stateFile}.lock`;
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
while (true) {
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(lockDir, { mode: PRIVATE_DIR_MODE });
|
|
58
|
+
bestEffortChmod(lockDir, PRIVATE_DIR_MODE);
|
|
59
|
+
return function releaseStateFileLock() {
|
|
60
|
+
try { fs.rmdirSync(lockDir); } catch {}
|
|
61
|
+
};
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (!e || e.code !== 'EEXIST') throw e;
|
|
64
|
+
try {
|
|
65
|
+
const ageMs = Date.now() - fs.statSync(lockDir).mtimeMs;
|
|
66
|
+
if (ageMs > STATE_LOCK_STALE_MS) {
|
|
67
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
} catch (statErr) {
|
|
71
|
+
if (statErr && statErr.code === 'ENOENT') continue;
|
|
72
|
+
throw statErr;
|
|
73
|
+
}
|
|
74
|
+
if (Date.now() - startedAt > STATE_LOCK_STALE_MS) {
|
|
75
|
+
const err = new Error('timed out waiting for mailbox state lock');
|
|
76
|
+
err.code = 'MAILBOX_STATE_LOCK_TIMEOUT';
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
sleepSync(STATE_LOCK_WAIT_MS);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isPlainState(value) {
|
|
85
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isHubRotatedNodeSecretState(state) {
|
|
89
|
+
return isPlainState(state)
|
|
90
|
+
&& state.node_secret_source === 'hub_rotate'
|
|
91
|
+
&& typeof state.node_secret === 'string'
|
|
92
|
+
&& Boolean(state.node_secret.trim());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseNodeSecretVersion(value) {
|
|
96
|
+
const n = Number(value);
|
|
97
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isFullNodeSecretTupleUpdate(updatedSet) {
|
|
101
|
+
return Boolean(updatedSet) && MAILBOX_NODE_SECRET_TUPLE_KEYS.every((key) => updatedSet.has(key));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canApplyPartialNodeSecretTupleWrite(key, disk, next) {
|
|
105
|
+
if (key !== 'node_secret_version') return false;
|
|
106
|
+
if (!isHubRotatedNodeSecretState(disk)) return true;
|
|
107
|
+
if (!isHubRotatedNodeSecretState(next)) return false;
|
|
108
|
+
if (String(next.node_secret).trim() !== String(disk.node_secret).trim()) return false;
|
|
109
|
+
const nextVersion = parseNodeSecretVersion(next.node_secret_version);
|
|
110
|
+
const diskVersion = parseNodeSecretVersion(disk.node_secret_version);
|
|
111
|
+
return Boolean(nextVersion && (!diskVersion || nextVersion >= diskVersion));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} stateFile
|
|
116
|
+
* @returns {Record<string, unknown>|null}
|
|
117
|
+
*/
|
|
118
|
+
function readMailboxStateFile(stateFile) {
|
|
119
|
+
try {
|
|
120
|
+
if (!fs.existsSync(stateFile)) return null;
|
|
121
|
+
const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
122
|
+
return isPlainState(parsed) ? parsed : null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function replaceStateFile(stateFile, state) {
|
|
129
|
+
const dir = path.dirname(stateFile);
|
|
130
|
+
ensurePrivateDir(dir);
|
|
131
|
+
const tmp = `${stateFile}.${process.pid}.tmp`;
|
|
132
|
+
writePrivateFile(tmp, JSON.stringify(state || {}, null, 2) + '\n');
|
|
133
|
+
// Windows: renameSync throws EPERM when the destination file already
|
|
134
|
+
// exists, unlike POSIX where rename(2) atomically replaces the target.
|
|
135
|
+
if (process.platform === 'win32') {
|
|
136
|
+
try { fs.unlinkSync(stateFile); } catch (e) {
|
|
137
|
+
if (e.code !== 'ENOENT') throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
fs.renameSync(tmp, stateFile);
|
|
141
|
+
bestEffortChmod(stateFile, PRIVATE_FILE_MODE);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Merge a partial state write with the latest on-disk state.
|
|
146
|
+
*
|
|
147
|
+
* Callers pass the keys they intentionally changed. Secret-bearing keys are
|
|
148
|
+
* only written when they are in `updatedKeys`, so an old long-running
|
|
149
|
+
* MailboxStore cannot overwrite a fresher Hub-rotated or divergence-cleared
|
|
150
|
+
* secret tuple that was written by another process between load and persist.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} stateFile
|
|
153
|
+
* @param {Record<string, unknown>} nextState
|
|
154
|
+
* @param {Iterable<string>} updatedKeys
|
|
155
|
+
* @returns {Record<string, unknown>}
|
|
156
|
+
*/
|
|
157
|
+
function writeMergedMailboxStateFile(stateFile, nextState, updatedKeys) {
|
|
158
|
+
const releaseLock = acquireStateFileLock(stateFile);
|
|
159
|
+
try {
|
|
160
|
+
const next = isPlainState(nextState) ? nextState : {};
|
|
161
|
+
const disk = readMailboxStateFile(stateFile);
|
|
162
|
+
const hasDiskState = isPlainState(disk);
|
|
163
|
+
const merged = hasDiskState ? { ...disk } : {};
|
|
164
|
+
const updatedSet = updatedKeys ? new Set(Array.from(updatedKeys)) : null;
|
|
165
|
+
const keys = updatedSet ? Array.from(updatedSet) : Object.keys(next);
|
|
166
|
+
const touchesNodeSecretTuple = keys.some((key) => MAILBOX_NODE_SECRET_TUPLE_KEY_SET.has(key));
|
|
167
|
+
const preserveDiskNodeSecretTuple = touchesNodeSecretTuple
|
|
168
|
+
&& hasDiskState
|
|
169
|
+
&& isHubRotatedNodeSecretState(disk)
|
|
170
|
+
&& !isFullNodeSecretTupleUpdate(updatedSet);
|
|
171
|
+
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
if (MAILBOX_NODE_SECRET_STATE_KEY_SET.has(key) && hasDiskState && (!updatedSet || !updatedSet.has(key))) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
MAILBOX_NODE_SECRET_TUPLE_KEY_SET.has(key) &&
|
|
178
|
+
preserveDiskNodeSecretTuple &&
|
|
179
|
+
!canApplyPartialNodeSecretTupleWrite(key, disk, next)
|
|
180
|
+
) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
184
|
+
delete merged[key];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
merged[key] = next[key];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
replaceStateFile(stateFile, merged);
|
|
191
|
+
return merged;
|
|
192
|
+
} finally {
|
|
193
|
+
releaseLock();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
PRIVATE_DIR_MODE,
|
|
199
|
+
PRIVATE_FILE_MODE,
|
|
200
|
+
MAILBOX_NODE_SECRET_STATE_KEYS,
|
|
201
|
+
bestEffortChmod,
|
|
202
|
+
ensurePrivateDir,
|
|
203
|
+
writePrivateFile,
|
|
204
|
+
readMailboxStateFile,
|
|
205
|
+
isHubRotatedNodeSecretState,
|
|
206
|
+
writeMergedMailboxStateFile,
|
|
207
|
+
};
|
|
@@ -3,28 +3,20 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
const {
|
|
7
|
+
PRIVATE_FILE_MODE,
|
|
8
|
+
bestEffortChmod,
|
|
9
|
+
ensurePrivateDir,
|
|
10
|
+
writePrivateFile,
|
|
11
|
+
readMailboxStateFile,
|
|
12
|
+
writeMergedMailboxStateFile,
|
|
13
|
+
} = require('./state');
|
|
6
14
|
|
|
7
15
|
const DEFAULT_CHANNEL = 'evomap-hub';
|
|
8
16
|
const SCHEMA_VERSION = 1;
|
|
9
17
|
const PROXY_PROTOCOL_VERSION = '0.1.0';
|
|
10
|
-
const
|
|
11
|
-
const
|
|
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
|
-
}
|
|
18
|
+
const DEFAULT_MAX_JSONL_LINE_BYTES = 16 * 1024 * 1024;
|
|
19
|
+
const JSONL_READ_CHUNK_BYTES = 64 * 1024;
|
|
28
20
|
|
|
29
21
|
// Merge `fields` into `target` while stripping keys that can mutate the
|
|
30
22
|
// prototype chain. Mailbox rows are persisted as JSONL and rebuilt on
|
|
@@ -107,22 +99,117 @@ function safeParse(payload) {
|
|
|
107
99
|
// Windows NTFS provides the same atomicity guarantee for O_APPEND writes to
|
|
108
100
|
// regular files that POSIX local filesystems do, so the removal above is
|
|
109
101
|
// equally valid on Windows. No platform-specific code is needed here.
|
|
102
|
+
function jsonlLineForWrite(obj, opts = {}) {
|
|
103
|
+
const line = JSON.stringify(obj) + '\n';
|
|
104
|
+
const maxLineBytes = Math.max(1, Math.floor(Number(opts.maxLineBytes) || resolveMaxJsonlLineBytes()));
|
|
105
|
+
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
106
|
+
if (lineBytes > maxLineBytes) {
|
|
107
|
+
const err = new Error(`mailbox JSONL line is ${lineBytes} bytes; maximum is ${maxLineBytes} bytes`);
|
|
108
|
+
err.code = 'MAILBOX_JSONL_LINE_TOO_LARGE';
|
|
109
|
+
err.lineBytes = lineBytes;
|
|
110
|
+
err.maxLineBytes = maxLineBytes;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return line;
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
function appendLine(filePath, obj) {
|
|
111
|
-
|
|
117
|
+
const line = jsonlLineForWrite(obj);
|
|
118
|
+
fs.appendFileSync(filePath, line, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
112
119
|
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
function resolveMaxJsonlLineBytes(env = process.env) {
|
|
123
|
+
const raw = Number(env.EVOMAP_MAILBOX_MAX_LINE_BYTES);
|
|
124
|
+
if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
|
|
125
|
+
return DEFAULT_MAX_JSONL_LINE_BYTES;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function forEachJsonLine(filePath, onRow, opts = {}) {
|
|
129
|
+
const stats = { parsed: 0, corrupt: 0, overlong: 0 };
|
|
130
|
+
if (!fs.existsSync(filePath)) return stats;
|
|
131
|
+
|
|
132
|
+
const maxLineBytes = Math.max(1, Math.floor(Number(opts.maxLineBytes) || resolveMaxJsonlLineBytes()));
|
|
133
|
+
const chunk = Buffer.allocUnsafe(JSONL_READ_CHUNK_BYTES);
|
|
134
|
+
let fd = null;
|
|
135
|
+
let parts = [];
|
|
136
|
+
let lineBytes = 0;
|
|
137
|
+
let dropping = false;
|
|
138
|
+
|
|
139
|
+
const resetLine = () => {
|
|
140
|
+
parts = [];
|
|
141
|
+
lineBytes = 0;
|
|
142
|
+
dropping = false;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const appendSegment = (segment) => {
|
|
146
|
+
if (!segment || segment.length === 0 || dropping) return;
|
|
147
|
+
if (lineBytes + segment.length > maxLineBytes) {
|
|
148
|
+
parts = [];
|
|
149
|
+
lineBytes = 0;
|
|
150
|
+
dropping = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
parts.push(Buffer.from(segment));
|
|
154
|
+
lineBytes += segment.length;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const finishLine = (hasNewline = false) => {
|
|
158
|
+
const totalLineBytes = lineBytes + (hasNewline ? 1 : 0);
|
|
159
|
+
if (dropping) {
|
|
160
|
+
stats.overlong += 1;
|
|
161
|
+
resetLine();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (totalLineBytes > maxLineBytes) {
|
|
165
|
+
stats.overlong += 1;
|
|
166
|
+
resetLine();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (lineBytes === 0) {
|
|
170
|
+
resetLine();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const trimmed = Buffer.concat(parts, lineBytes).toString('utf8').trim();
|
|
174
|
+
resetLine();
|
|
175
|
+
if (!trimmed) return;
|
|
176
|
+
try {
|
|
177
|
+
onRow(JSON.parse(trimmed));
|
|
178
|
+
stats.parsed += 1;
|
|
179
|
+
} catch {
|
|
180
|
+
stats.corrupt += 1;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
fd = fs.openSync(filePath, 'r');
|
|
186
|
+
while (true) {
|
|
187
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunk.length, null);
|
|
188
|
+
if (bytesRead <= 0) break;
|
|
189
|
+
let start = 0;
|
|
190
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
191
|
+
if (chunk[i] !== 0x0a) continue;
|
|
192
|
+
appendSegment(chunk.subarray(start, i));
|
|
193
|
+
finishLine(true);
|
|
194
|
+
start = i + 1;
|
|
195
|
+
}
|
|
196
|
+
if (start < bytesRead) appendSegment(chunk.subarray(start, bytesRead));
|
|
197
|
+
}
|
|
198
|
+
finishLine();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
stats.read_failed = 1;
|
|
201
|
+
const detail = err && err.message ? err.message : String(err);
|
|
202
|
+
const wrapped = new Error(`failed to read mailbox JSONL ${filePath}: ${detail}`);
|
|
203
|
+
wrapped.code = 'MAILBOX_JSONL_READ_FAILED';
|
|
204
|
+
wrapped.stats = stats;
|
|
205
|
+
wrapped.cause = err;
|
|
206
|
+
throw wrapped;
|
|
207
|
+
} finally {
|
|
208
|
+
if (fd !== null) {
|
|
209
|
+
try { fs.closeSync(fd); } catch { /* ignore close errors */ }
|
|
210
|
+
}
|
|
124
211
|
}
|
|
125
|
-
return
|
|
212
|
+
return stats;
|
|
126
213
|
}
|
|
127
214
|
|
|
128
215
|
// --- In-memory index that backs JSONL persistence ---
|
|
@@ -150,19 +237,21 @@ class MailboxStore {
|
|
|
150
237
|
}
|
|
151
238
|
|
|
152
239
|
_loadState() {
|
|
153
|
-
|
|
154
|
-
if (fs.existsSync(this._stateFile)) {
|
|
155
|
-
this._state = JSON.parse(fs.readFileSync(this._stateFile, 'utf8'));
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
this._state = {};
|
|
159
|
-
}
|
|
240
|
+
this._state = readMailboxStateFile(this._stateFile) || {};
|
|
160
241
|
const existingVersion = this._state._schema_version || 0;
|
|
242
|
+
const beforeMigrationState = { ...this._state };
|
|
161
243
|
if (existingVersion < SCHEMA_VERSION) {
|
|
162
244
|
this._runMigrations(existingVersion, SCHEMA_VERSION);
|
|
163
245
|
}
|
|
164
246
|
this._state._schema_version = SCHEMA_VERSION;
|
|
165
|
-
|
|
247
|
+
const updatedKeys = new Set(['_schema_version']);
|
|
248
|
+
for (const key of Object.keys(this._state)) {
|
|
249
|
+
if (this._state[key] !== beforeMigrationState[key]) updatedKeys.add(key);
|
|
250
|
+
}
|
|
251
|
+
for (const key of Object.keys(beforeMigrationState)) {
|
|
252
|
+
if (!Object.prototype.hasOwnProperty.call(this._state, key)) updatedKeys.add(key);
|
|
253
|
+
}
|
|
254
|
+
this._persistState(updatedKeys);
|
|
166
255
|
}
|
|
167
256
|
|
|
168
257
|
_runMigrations(fromVersion, toVersion) {
|
|
@@ -174,9 +263,7 @@ class MailboxStore {
|
|
|
174
263
|
}
|
|
175
264
|
}
|
|
176
265
|
|
|
177
|
-
_persistState() {
|
|
178
|
-
const dir = path.dirname(this._stateFile);
|
|
179
|
-
ensurePrivateDir(dir);
|
|
266
|
+
_persistState(updatedKeys) {
|
|
180
267
|
// Round-7 (§20.5): per-PID tmp path. Two evolver processes (daemon +
|
|
181
268
|
// ad-hoc CLI / proxy + loop) writing to the same `${stateFile}.tmp`
|
|
182
269
|
// would otherwise interleave: process B's writeFileSync truncates
|
|
@@ -186,20 +273,11 @@ class MailboxStore {
|
|
|
186
273
|
// load-bearing trigger for the "401-loop -> reauth backoff -> dead
|
|
187
274
|
// for 30 min..4 h" symptom this branch targets. Matches the
|
|
188
275
|
// precedent set by _persistNodeSecret in src/gep/a2aProtocol.js.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
// The window between unlink and rename is intentionally tiny; a crash in
|
|
195
|
-
// that window leaves the tmp file behind (recovered on next _persistState).
|
|
196
|
-
if (process.platform === 'win32') {
|
|
197
|
-
try { fs.unlinkSync(this._stateFile); } catch (e) {
|
|
198
|
-
if (e.code !== 'ENOENT') throw e;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
fs.renameSync(tmp, this._stateFile);
|
|
202
|
-
bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
|
|
276
|
+
//
|
|
277
|
+
// Use the shared merge helper so stale long-running stores cannot
|
|
278
|
+
// resurrect an older node_secret tuple after another path rotates or clears
|
|
279
|
+
// it directly on disk.
|
|
280
|
+
this._state = writeMergedMailboxStateFile(this._stateFile, this._state, updatedKeys);
|
|
203
281
|
}
|
|
204
282
|
|
|
205
283
|
_rebuildIndex() {
|
|
@@ -208,17 +286,20 @@ class MailboxStore {
|
|
|
208
286
|
this._inbound = [];
|
|
209
287
|
|
|
210
288
|
const TERMINAL = new Set(['synced', 'delivered', 'failed', 'rejected']);
|
|
211
|
-
const
|
|
212
|
-
for (const rawRow of rows) {
|
|
289
|
+
const stats = forEachJsonLine(this._messagesFile, (rawRow) => {
|
|
213
290
|
const row = sanitizeRow(rawRow);
|
|
291
|
+
if (!row || typeof row !== 'object') return;
|
|
214
292
|
if (row._op === 'update') {
|
|
215
293
|
const existing = this._messages.get(row.id);
|
|
216
294
|
if (existing) safeAssign(existing, row.fields);
|
|
217
|
-
|
|
295
|
+
return;
|
|
218
296
|
}
|
|
297
|
+
if (!row.id) return;
|
|
219
298
|
this._messages.set(row.id, row);
|
|
220
|
-
}
|
|
299
|
+
});
|
|
300
|
+
this._lastRebuildStats = stats;
|
|
221
301
|
for (const [id, msg] of this._messages) {
|
|
302
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
222
303
|
if (TERMINAL.has(msg.status)) continue;
|
|
223
304
|
if (msg.direction === 'outbound') this._outbound.push(id);
|
|
224
305
|
else if (msg.direction === 'inbound') this._inbound.push(id);
|
|
@@ -430,8 +511,9 @@ class MailboxStore {
|
|
|
430
511
|
}
|
|
431
512
|
|
|
432
513
|
setCursor(key, value) {
|
|
433
|
-
|
|
434
|
-
this.
|
|
514
|
+
const stateKey = `cursor:${key}`;
|
|
515
|
+
this._state[stateKey] = value;
|
|
516
|
+
this._persistState([stateKey]);
|
|
435
517
|
}
|
|
436
518
|
|
|
437
519
|
getState(key) {
|
|
@@ -441,7 +523,20 @@ class MailboxStore {
|
|
|
441
523
|
|
|
442
524
|
setState(key, value) {
|
|
443
525
|
this._state[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
444
|
-
this._persistState();
|
|
526
|
+
this._persistState([key]);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
setNodeSecretState({ secret = '', version = '', source = '', envSuppressed = '' } = {}) {
|
|
530
|
+
this._state.node_secret = secret;
|
|
531
|
+
this._state.node_secret_version = version ? String(version) : '';
|
|
532
|
+
this._state.node_secret_source = source;
|
|
533
|
+
this._state.node_secret_env_suppressed = envSuppressed;
|
|
534
|
+
this._persistState([
|
|
535
|
+
'node_secret',
|
|
536
|
+
'node_secret_version',
|
|
537
|
+
'node_secret_source',
|
|
538
|
+
'node_secret_env_suppressed',
|
|
539
|
+
]);
|
|
445
540
|
}
|
|
446
541
|
|
|
447
542
|
// --- Compaction (reduces JSONL file size by rewriting only current state) ---
|
|
@@ -457,11 +552,16 @@ class MailboxStore {
|
|
|
457
552
|
}
|
|
458
553
|
entries.sort((a, b) => a.created_at - b.created_at);
|
|
459
554
|
|
|
555
|
+
const lines = entries.map((msg) => jsonlLineForWrite(msg));
|
|
556
|
+
|
|
460
557
|
const fd = fs.openSync(tmpFile, 'w', PRIVATE_FILE_MODE);
|
|
461
|
-
|
|
462
|
-
|
|
558
|
+
try {
|
|
559
|
+
for (const line of lines) {
|
|
560
|
+
fs.writeSync(fd, line);
|
|
561
|
+
}
|
|
562
|
+
} finally {
|
|
563
|
+
fs.closeSync(fd);
|
|
463
564
|
}
|
|
464
|
-
fs.closeSync(fd);
|
|
465
565
|
bestEffortChmod(tmpFile, PRIVATE_FILE_MODE);
|
|
466
566
|
// Windows: renameSync throws EPERM when the destination already exists.
|
|
467
567
|
// Remove it first so the swap succeeds on all platforms.
|
|
@@ -144,8 +144,8 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
144
144
|
return async ({ body, headers }) => {
|
|
145
145
|
const inboundHeaders = headers || {};
|
|
146
146
|
// x-api-key is satisfied by either the inbound header OR a proxy-side
|
|
147
|
-
// ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env
|
|
148
|
-
// _proxyAnthropic). The proxy server itself has already auth-checked
|
|
147
|
+
// EVOMAP_ANTHROPIC_API_KEY / ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env
|
|
148
|
+
// (token mediation, see _proxyAnthropic). The proxy server itself has already auth-checked
|
|
149
149
|
// `Authorization: Bearer <proxy_token>` before reaching this handler.
|
|
150
150
|
//
|
|
151
151
|
// Bedrock upstream uses SigV4 with AWS_* env, so neither inbound
|
|
@@ -161,7 +161,8 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
161
161
|
if (upstreamMode !== 'bedrock') {
|
|
162
162
|
const hasInboundKey = !!inboundHeaders['x-api-key'];
|
|
163
163
|
const hasProxyEnvCreds = !!(
|
|
164
|
-
process.env.
|
|
164
|
+
process.env.EVOMAP_ANTHROPIC_API_KEY
|
|
165
|
+
|| process.env.ANTHROPIC_API_KEY
|
|
165
166
|
|| process.env.EVOMAP_ANTHROPIC_AUTH_TOKEN
|
|
166
167
|
|| (process.env.EVOMAP_PROXY_AUTO_INJECTED === '1' ? '' : process.env.ANTHROPIC_AUTH_TOKEN)
|
|
167
168
|
);
|
|
@@ -289,6 +290,7 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
289
290
|
inboundHeaders,
|
|
290
291
|
upstreamMode,
|
|
291
292
|
});
|
|
293
|
+
if (upstream.traceRequestBody !== undefined) trace?.setRequestBody(upstream.traceRequestBody);
|
|
292
294
|
} catch (err) {
|
|
293
295
|
trace?.record({ status: 502, error: err, upstreamMode, model: chosenModel });
|
|
294
296
|
throw err;
|
|
@@ -351,6 +353,7 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
351
353
|
// — but that loses the original 503 body, so cache it here too and
|
|
352
354
|
// restore it on the throw path.
|
|
353
355
|
let drainedFirst = '';
|
|
356
|
+
let firstAttemptResponseBody = {};
|
|
354
357
|
if (upstream.text) {
|
|
355
358
|
// Bound response-body drain to 10s to prevent hanging on large or
|
|
356
359
|
// slow-streaming error responses. If the drain times out, log it
|
|
@@ -388,14 +391,47 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
388
391
|
if (drainTimer) clearTimeout(drainTimer);
|
|
389
392
|
}
|
|
390
393
|
}
|
|
394
|
+
if (drainedFirst.length > 0) {
|
|
395
|
+
try {
|
|
396
|
+
firstAttemptResponseBody = JSON.parse(drainedFirst);
|
|
397
|
+
} catch {
|
|
398
|
+
firstAttemptResponseBody = { error: drainedFirst };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
trace?.recordAttempt({
|
|
402
|
+
attempt_index: 0,
|
|
403
|
+
status: upstream.status,
|
|
404
|
+
requestBody: upstream.traceRequestBody !== undefined ? upstream.traceRequestBody : outboundBody,
|
|
405
|
+
responseBody: firstAttemptResponseBody,
|
|
406
|
+
upstreamMode,
|
|
407
|
+
model: chosenModel,
|
|
408
|
+
headers: upstream.headers,
|
|
409
|
+
});
|
|
410
|
+
let retryBody = body;
|
|
391
411
|
try {
|
|
392
|
-
|
|
412
|
+
retryBody = rewriteModel(body, originalModel);
|
|
393
413
|
finalUpstream = await anthropicProxy('/v1/messages', retryBody, {
|
|
394
414
|
inboundHeaders,
|
|
395
415
|
upstreamMode,
|
|
396
416
|
});
|
|
417
|
+
trace?.setRequestBody(finalUpstream.traceRequestBody !== undefined ? finalUpstream.traceRequestBody : retryBody);
|
|
418
|
+
trace?.recordAttempt({
|
|
419
|
+
attempt_index: 1,
|
|
420
|
+
requestBody: finalUpstream.traceRequestBody !== undefined ? finalUpstream.traceRequestBody : retryBody,
|
|
421
|
+
upstreamMode,
|
|
422
|
+
model: originalModel,
|
|
423
|
+
headers: finalUpstream.headers,
|
|
424
|
+
});
|
|
397
425
|
finalModel = originalModel;
|
|
398
426
|
} catch (err) {
|
|
427
|
+
trace?.recordAttempt({
|
|
428
|
+
attempt_index: 1,
|
|
429
|
+
status: 502,
|
|
430
|
+
requestBody: retryBody,
|
|
431
|
+
error: err,
|
|
432
|
+
upstreamMode,
|
|
433
|
+
model: originalModel,
|
|
434
|
+
});
|
|
399
435
|
// Replay the drained first response so the caller still sees the
|
|
400
436
|
// original 503 + body, not an empty stream.
|
|
401
437
|
finalUpstream = {
|
|
@@ -424,6 +460,7 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
424
460
|
upstreamMode,
|
|
425
461
|
model: finalModel,
|
|
426
462
|
headers: forwardHeaders,
|
|
463
|
+
attempt_index: finalModel === originalModel && chosenModel !== originalModel ? 1 : undefined,
|
|
427
464
|
});
|
|
428
465
|
return {
|
|
429
466
|
status: finalUpstream.status,
|
|
@@ -460,6 +497,21 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
460
497
|
}));
|
|
461
498
|
}
|
|
462
499
|
}
|
|
500
|
+
if (
|
|
501
|
+
enabled
|
|
502
|
+
&& chosenModel
|
|
503
|
+
&& chosenModel !== originalModel
|
|
504
|
+
&& finalModel === originalModel
|
|
505
|
+
) {
|
|
506
|
+
trace?.recordAttempt({
|
|
507
|
+
attempt_index: 1,
|
|
508
|
+
status: finalUpstream.status,
|
|
509
|
+
responseBody: respBody,
|
|
510
|
+
upstreamMode,
|
|
511
|
+
model: finalModel,
|
|
512
|
+
headers: finalUpstream.headers,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
463
515
|
trace?.record({
|
|
464
516
|
status: finalUpstream.status,
|
|
465
517
|
responseBody: respBody,
|