@evomap/evolver 1.89.15 → 1.89.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.ja-JP.md +1 -1
  2. package/README.ko-KR.md +1 -1
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/index.js +89 -17
  6. package/package.json +1 -1
  7. package/src/adapters/claudeCode.js +38 -9
  8. package/src/adapters/codex.js +2 -2
  9. package/src/adapters/hookAdapter.js +38 -2
  10. package/src/adapters/scripts/evolver-session-start.js +182 -2
  11. package/src/atp/atpExecute.js +3 -1
  12. package/src/atp/hubClient.js +5 -3
  13. package/src/atp/serviceHelper.js +3 -2
  14. package/src/config.js +11 -3
  15. package/src/evolve/guards.js +1 -1
  16. package/src/evolve/pipeline/collect.js +1 -1
  17. package/src/evolve/pipeline/dispatch.js +1 -1
  18. package/src/evolve/pipeline/enrich.js +1 -1
  19. package/src/evolve/pipeline/hub.js +1 -1
  20. package/src/evolve/pipeline/select.js +1 -1
  21. package/src/evolve/pipeline/signals.js +1 -1
  22. package/src/evolve/utils.js +1 -1
  23. package/src/evolve.js +1 -1
  24. package/src/gep/a2aProtocol.js +1 -1
  25. package/src/gep/antiAbuseTelemetry.js +1 -1
  26. package/src/gep/autoDistillConv.js +1 -1
  27. package/src/gep/autoDistillLlm.js +1 -1
  28. package/src/gep/candidateEval.js +1 -1
  29. package/src/gep/candidates.js +1 -1
  30. package/src/gep/cliContracts.js +37 -1
  31. package/src/gep/contentHash.js +1 -1
  32. package/src/gep/conversationDistiller.js +1 -1
  33. package/src/gep/conversationSniffer.js +1 -1
  34. package/src/gep/crypto.js +1 -1
  35. package/src/gep/curriculum.js +1 -1
  36. package/src/gep/deviceId.js +1 -1
  37. package/src/gep/directoryClient.js +7 -3
  38. package/src/gep/envFingerprint.js +1 -1
  39. package/src/gep/epigenetics.js +1 -1
  40. package/src/gep/execBridge.js +1 -1
  41. package/src/gep/explore.js +1 -1
  42. package/src/gep/hash.js +1 -1
  43. package/src/gep/hostErrorClassifier.js +34 -0
  44. package/src/gep/hubFetch.js +1 -1
  45. package/src/gep/hubReview.js +1 -1
  46. package/src/gep/hubSearch.js +1 -1
  47. package/src/gep/hubVerify.js +1 -1
  48. package/src/gep/issueReporter.js +5 -4
  49. package/src/gep/learningSignals.js +1 -1
  50. package/src/gep/memoryGraph.js +1 -1
  51. package/src/gep/memoryGraphAdapter.js +1 -1
  52. package/src/gep/mutation.js +1 -1
  53. package/src/gep/narrativeMemory.js +1 -1
  54. package/src/gep/oauthLogin.js +3 -5
  55. package/src/gep/openPRRegistry.js +1 -1
  56. package/src/gep/paths.js +20 -0
  57. package/src/gep/personality.js +1 -1
  58. package/src/gep/policyCheck.js +1 -1
  59. package/src/gep/privacyClient.js +28 -10
  60. package/src/gep/prompt.js +1 -1
  61. package/src/gep/recallInject.js +1 -1
  62. package/src/gep/recallVerifier.js +1 -1
  63. package/src/gep/reflection.js +1 -1
  64. package/src/gep/savingsCore.js +1 -1
  65. package/src/gep/selector.js +1 -1
  66. package/src/gep/signals.js +37 -15
  67. package/src/gep/skillDistiller.js +1 -1
  68. package/src/gep/skillPublisher.js +8 -3
  69. package/src/gep/solidify.js +1 -1
  70. package/src/gep/strategy.js +1 -1
  71. package/src/gep/taskReceiver.js +3 -2
  72. package/src/gep/tokenSavings.js +1 -1
  73. package/src/gep/validator/index.js +7 -2
  74. package/src/gep/validator/reporter.js +7 -2
  75. package/src/gep/validator/stakeBootstrap.js +7 -2
  76. package/src/gep/workspaceKeychain.js +1 -1
  77. package/src/ops/lifecycle.js +501 -31
  78. package/src/proxy/clientSettings.js +405 -0
  79. package/src/proxy/extensions/traceControl.js +1 -1
  80. package/src/proxy/index.js +10 -5
  81. package/src/proxy/inject.js +1 -1
  82. package/src/proxy/lifecycle/manager.js +82 -16
  83. package/src/proxy/mailbox/state.js +207 -0
  84. package/src/proxy/mailbox/store.js +164 -64
  85. package/src/proxy/router/messages_route.js +4 -3
  86. package/src/proxy/server/http.js +40 -6
  87. package/src/proxy/sync/inbound.js +31 -12
  88. package/src/proxy/sync/outbound.js +157 -22
  89. package/src/proxy/trace/extractor.js +1 -1
  90. package/src/proxy/trace/usage.js +1 -1
@@ -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 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
- }
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
- fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
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 readLines(filePath) {
116
- if (!fs.existsSync(filePath)) return [];
117
- const content = fs.readFileSync(filePath, 'utf8');
118
- const lines = content.split('\n');
119
- const results = [];
120
- for (const line of lines) {
121
- const trimmed = line.trim();
122
- if (!trimmed) continue;
123
- try { results.push(JSON.parse(trimmed)); } catch { /* skip corrupt lines */ }
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 results;
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
- try {
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
- this._persistState();
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
- const tmp = `${this._stateFile}.${process.pid}.tmp`;
190
- writePrivateFile(tmp, JSON.stringify(this._state, null, 2) + '\n');
191
- // Windows: fs.renameSync throws EPERM when the destination file already
192
- // exists, unlike POSIX where rename(2) atomically replaces the target.
193
- // Remove the destination first so the rename succeeds on all platforms.
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 rows = readLines(this._messagesFile);
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
- continue;
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
- this._state[`cursor:${key}`] = value;
434
- this._persistState();
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
- for (const msg of entries) {
462
- fs.writeSync(fd, JSON.stringify(msg) + '\n');
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 (token mediation, see
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.ANTHROPIC_API_KEY
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
  );
@@ -3,6 +3,11 @@
3
3
  const crypto = require('crypto');
4
4
  const http = require('http');
5
5
  const { writeSettings, readSettings, clearSettings, clearIfStale } = require('./settings');
6
+ const {
7
+ isValidReusableProxyToken,
8
+ readReusableClientProxyToken,
9
+ syncClaudeProxySettings,
10
+ } = require('../clientSettings');
6
11
 
7
12
  const MAX_PORT_ATTEMPTS = 100;
8
13
  const DEFAULT_PORT = 19820;
@@ -93,13 +98,14 @@ function tryListen(server, port) {
93
98
  }
94
99
 
95
100
  class ProxyHttpServer {
96
- constructor(routes, { port, logger } = {}) {
101
+ constructor(routes, { port, logger, clientSettings } = {}) {
97
102
  this.routes = routes;
98
103
  this.basePort = port || Number(process.env.EVOMAP_PROXY_PORT) || DEFAULT_PORT;
99
104
  this.actualPort = null;
100
105
  this.logger = logger || console;
101
106
  this.server = null;
102
107
  this.token = null;
108
+ this.clientSettings = clientSettings || null;
103
109
  }
104
110
 
105
111
  async start() {
@@ -109,16 +115,22 @@ class ProxyHttpServer {
109
115
  // already exported into long-lived shells (the .bashrc auto-source only
110
116
  // runs once per terminal).
111
117
  const priorProxy = readSettings().proxy || {};
112
- const previous = typeof priorProxy.token === 'string' ? priorProxy.token : null;
118
+ const previous = isValidReusableProxyToken(priorProxy.token) ? priorProxy.token.trim() : null;
113
119
  // settings.json is operator-edited; previous_tokens may contain non-strings
114
120
  // (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8')
115
121
  // in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that
116
122
  // takes the daemon down under default --unhandled-rejections=throw.
117
123
  const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens)
118
- ? priorProxy.previous_tokens.filter((t) => typeof t === 'string' && t.length > 0)
124
+ ? priorProxy.previous_tokens
125
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
126
+ .filter((t) => isValidReusableProxyToken(t))
119
127
  : [];
120
128
  clearIfStale();
121
- this.token = previous || crypto.randomBytes(32).toString('hex');
129
+ const clientSettingsOpts = this.clientSettings || {};
130
+ const clientToken = this.clientSettings
131
+ ? readReusableClientProxyToken({ ...clientSettingsOpts, port: this.basePort })
132
+ : null;
133
+ this.token = previous || clientToken || crypto.randomBytes(32).toString('hex');
122
134
  this._priorPreviousTokens = priorPreviousTokens;
123
135
  this.server = http.createServer((req, res) => this._handleRequest(req, res));
124
136
 
@@ -138,6 +150,24 @@ class ProxyHttpServer {
138
150
  proxyBlock.previous_tokens = this._priorPreviousTokens;
139
151
  }
140
152
  writeSettings({ proxy: proxyBlock });
153
+ if (this.clientSettings) {
154
+ try {
155
+ const syncResult = syncClaudeProxySettings({
156
+ ...clientSettingsOpts,
157
+ url,
158
+ port: this.basePort,
159
+ token: this.token,
160
+ runtimeEnv: process.env,
161
+ });
162
+ if (syncResult.synced && syncResult.changed) {
163
+ this.logger.log(`[proxy] Synced Claude client settings at ${syncResult.file}`);
164
+ } else if (syncResult.reason === 'invalid_settings_json') {
165
+ this.logger.warn?.(`[proxy] Skipped Claude client settings sync because ${syncResult.file} is not valid JSON`);
166
+ }
167
+ } catch (err) {
168
+ this.logger.warn?.('[proxy] Claude client settings sync failed: ' + (err && err.message ? err.message : err));
169
+ }
170
+ }
141
171
  this.logger.log(`[proxy] HTTP server listening on ${url}`);
142
172
  return { port, url, token: this.token };
143
173
  }
@@ -171,9 +201,13 @@ class ProxyHttpServer {
171
201
  // and unhandled-reject through the auth path.
172
202
  const previous = readSettings().proxy?.previous_tokens;
173
203
  const extras = Array.isArray(previous)
174
- ? previous.filter((t) => typeof t === 'string' && t.length > 0)
204
+ ? previous
205
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
206
+ .filter((t) => isValidReusableProxyToken(t))
175
207
  : [];
176
- const candidates = [this.token, ...extras].filter((t) => typeof t === 'string' && t.length > 0);
208
+ const candidates = [this.token, ...extras]
209
+ .filter((t) => isValidReusableProxyToken(t))
210
+ .map((t) => t.trim());
177
211
  let valid = false;
178
212
  for (const cand of candidates) {
179
213
  const expBuf = Buffer.from(cand, 'utf8');
@@ -16,6 +16,18 @@ const {
16
16
  const DEFAULT_POLL_INTERVAL_ACTIVE = 10_000;
17
17
  const DEFAULT_POLL_INTERVAL_IDLE = 60_000;
18
18
 
19
+ function toInboundStoreMessage(m, channel) {
20
+ return {
21
+ id: m.id,
22
+ type: m.type,
23
+ payload: m.payload,
24
+ channel,
25
+ priority: m.priority || 'normal',
26
+ refId: m.ref_id,
27
+ expiresAt: m.expires_at,
28
+ };
29
+ }
30
+
19
31
  class InboundSync {
20
32
  constructor({ store, hubUrl, getHeaders, logger }) {
21
33
  this.store = store;
@@ -86,26 +98,33 @@ class InboundSync {
86
98
 
87
99
  const data = await readHubResponseJson(res);
88
100
  const messages = data.messages || [];
101
+ let stored = 0;
102
+ let dropped = 0;
89
103
 
90
104
  if (messages.length > 0) {
91
- this.store.writeInboundBatch(
92
- messages.map(m => ({
93
- id: m.id,
94
- type: m.type,
95
- payload: m.payload,
96
- channel,
97
- priority: m.priority || 'normal',
98
- refId: m.ref_id,
99
- expiresAt: m.expires_at,
100
- }))
101
- );
105
+ for (const m of messages) {
106
+ try {
107
+ this.store.writeInbound(toInboundStoreMessage(m, channel));
108
+ stored++;
109
+ } catch (err) {
110
+ if (err && err.code === 'MAILBOX_JSONL_LINE_TOO_LARGE') {
111
+ dropped++;
112
+ const msgId = m && m.id ? String(m.id) : '(missing id)';
113
+ this.logger.warn?.(`[inbound] dropped oversized inbound message ${msgId}: ${err.message}`);
114
+ continue;
115
+ }
116
+ throw err;
117
+ }
118
+ }
102
119
  }
103
120
 
104
121
  if (data.next_cursor) {
105
122
  this.store.setCursor(cursorKey, data.next_cursor);
106
123
  }
107
124
 
108
- return { received: messages.length, cursor: data.next_cursor || cursor };
125
+ const result = { received: stored, cursor: data.next_cursor || cursor };
126
+ if (dropped > 0) result.dropped = dropped;
127
+ return result;
109
128
  } catch (err) {
110
129
  if (err instanceof AuthError) throw err;
111
130
  if (isHubUnreachableError(err)) {