@evomap/evolver 1.87.1 → 1.87.3

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 (63) hide show
  1. package/README.ja-JP.md +1 -1
  2. package/README.ko-KR.md +1 -1
  3. package/README.md +9 -8
  4. package/README.zh-CN.md +9 -8
  5. package/index.js +30 -11
  6. package/package.json +1 -1
  7. package/scripts/build_binaries.js +31 -7
  8. package/src/atp/atpExecute.js +35 -8
  9. package/src/atp/autoBuyer.js +155 -21
  10. package/src/atp/autoDeliver.js +16 -0
  11. package/src/atp/cli.js +98 -0
  12. package/src/atp/cliAutobuyPrompt.js +57 -64
  13. package/src/atp/hubClient.js +42 -4
  14. package/src/evolve/guards.js +1 -1
  15. package/src/evolve/pipeline/collect.js +1 -1
  16. package/src/evolve/pipeline/dispatch.js +1 -1
  17. package/src/evolve/pipeline/enrich.js +1 -1
  18. package/src/evolve/pipeline/hub.js +1 -1
  19. package/src/evolve/pipeline/select.js +1 -1
  20. package/src/evolve/pipeline/signals.js +1 -1
  21. package/src/evolve/utils.js +1 -1
  22. package/src/evolve.js +1 -1
  23. package/src/forceUpdate.js +2 -1
  24. package/src/gep/a2aProtocol.js +1 -1
  25. package/src/gep/assetStore.js +52 -5
  26. package/src/gep/candidateEval.js +1 -1
  27. package/src/gep/candidates.js +1 -1
  28. package/src/gep/contentHash.js +1 -1
  29. package/src/gep/crypto.js +1 -1
  30. package/src/gep/curriculum.js +1 -1
  31. package/src/gep/deviceId.js +1 -1
  32. package/src/gep/envFingerprint.js +1 -1
  33. package/src/gep/epigenetics.js +1 -1
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hubFetch.js +1 -1
  37. package/src/gep/hubReview.js +1 -1
  38. package/src/gep/hubSearch.js +1 -1
  39. package/src/gep/hubVerify.js +1 -1
  40. package/src/gep/learningSignals.js +1 -1
  41. package/src/gep/memoryGraph.js +1 -1
  42. package/src/gep/memoryGraphAdapter.js +1 -1
  43. package/src/gep/mutation.js +1 -1
  44. package/src/gep/narrativeMemory.js +1 -1
  45. package/src/gep/openPRRegistry.js +1 -1
  46. package/src/gep/paths.js +6 -2
  47. package/src/gep/personality.js +1 -1
  48. package/src/gep/policyCheck.js +1 -1
  49. package/src/gep/prompt.js +1 -1
  50. package/src/gep/recallVerifier.js +1 -1
  51. package/src/gep/reflection.js +1 -1
  52. package/src/gep/sanitize.js +57 -3
  53. package/src/gep/selector.js +1 -1
  54. package/src/gep/selfPR.js +34 -1
  55. package/src/gep/skill2gep.js +108 -29
  56. package/src/gep/skillDistiller.js +1 -1
  57. package/src/gep/solidify.js +1 -1
  58. package/src/gep/strategy.js +1 -1
  59. package/src/gep/workspaceKeychain.js +1 -1
  60. package/src/proxy/index.js +29 -9
  61. package/src/proxy/lifecycle/manager.js +97 -37
  62. package/src/proxy/router/messages_route.js +105 -5
  63. package/src/proxy/sync/engine.js +68 -31
@@ -28,8 +28,8 @@ const { extractFeatures } = require('./features');
28
28
  // inbound sonnet-4-7 → sonnet-4-6 rewrite, so callers
29
29
  // pinned to 4-7 stay on 4-7.
30
30
  const DEFAULT_TIER_MODELS = Object.freeze({
31
- cheap: 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
32
- mid: 'global.anthropic.claude-sonnet-4-6',
31
+ cheap: 'global.anthropic.claude-opus-4-7',
32
+ mid: 'global.anthropic.claude-opus-4-7',
33
33
  expensive: 'global.anthropic.claude-opus-4-7',
34
34
  });
35
35
 
@@ -66,6 +66,28 @@ function isIntraFamilyDowngrade(chosen, original) {
66
66
  return c.minor < o.minor;
67
67
  }
68
68
 
69
+ // Bedrock InvokeModel rejects bare short IDs like `claude-opus-4-7` with
70
+ // ValidationException — it only accepts the explicit ARN-shaped aliases
71
+ // below. CC clients and many SDKs route via short IDs since that's what
72
+ // api.anthropic.com expects, so when upstreamMode === 'bedrock' we
73
+ // canonicalize at the proxy boundary. Unknown / non-Claude IDs pass
74
+ // through untouched (Bedrock owns the rejection in that case).
75
+ //
76
+ // Map keys are `family/major/minor` from parseClaudeId. Add new entries
77
+ // here as Anthropic ships new Bedrock aliases.
78
+ const KNOWN_BEDROCK_ALIASES = Object.freeze({
79
+ 'opus/4/7': 'global.anthropic.claude-opus-4-7',
80
+ 'haiku/4/5': 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
81
+ 'sonnet/4/6': 'global.anthropic.claude-sonnet-4-6',
82
+ });
83
+
84
+ function canonicalizeForBedrock(modelId) {
85
+ const parsed = parseClaudeId(modelId);
86
+ if (!parsed) return modelId;
87
+ const key = `${parsed.family}/${parsed.major}/${parsed.minor}`;
88
+ return KNOWN_BEDROCK_ALIASES[key] || modelId;
89
+ }
90
+
69
91
  function resolveTierModels() {
70
92
  return {
71
93
  cheap: process.env.EVOMAP_MODEL_CHEAP || DEFAULT_TIER_MODELS.cheap,
@@ -86,6 +108,31 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
86
108
  ? routerEnabled
87
109
  : process.env.EVOMAP_ROUTER_ENABLED === '1';
88
110
 
111
+ // Degenerate-tier guard (#152). The shipped DEFAULT_TIER_MODELS pins all
112
+ // three tiers to the same model on purpose: operators tuning tier mapping
113
+ // run tier-uniform so the no-downgrade guard never engages and 5xx retries
114
+ // always replay the same model (PR #135). Per-tier `EVOMAP_MODEL_*` env
115
+ // overrides are how a real deployment opts into cost-saving. The trap is the
116
+ // user who flips `EVOMAP_ROUTER_ENABLED=1` expecting savings (per README)
117
+ // but leaves the overrides unset: every tier resolves to one model, so
118
+ // routing is a silent no-op and — for anyone previously on a cheaper model —
119
+ // a cost *increase*. Emit one loud boot WARN so the degenerate config is
120
+ // visible in logs instead of manifesting as a surprise bill. Resolved at
121
+ // construction (proxy start) to match how `enabled` is read.
122
+ if (enabled) {
123
+ const tiers = resolveTierModels();
124
+ const distinct = new Set([tiers.cheap, tiers.mid, tiers.expensive]);
125
+ if (distinct.size === 1) {
126
+ log.warn?.(JSON.stringify({
127
+ event: 'router_degenerate_tiers',
128
+ message: 'router enabled but all tiers map to the same model — no '
129
+ + 'cost-saving effect. Set EVOMAP_MODEL_CHEAP / EVOMAP_MODEL_MID / '
130
+ + 'EVOMAP_MODEL_EXPENSIVE to enable tier-based routing.',
131
+ model: tiers.cheap,
132
+ }));
133
+ }
134
+ }
135
+
89
136
  return async ({ body, headers }) => {
90
137
  const inboundHeaders = headers || {};
91
138
  // x-api-key is satisfied by either the inbound header OR a proxy-side
@@ -111,7 +158,19 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
111
158
  }
112
159
  }
113
160
 
114
- const originalModel = body && typeof body.model === 'string' ? body.model : null;
161
+ // Phase C ABC fix: in bedrock mode, normalize the inbound model to the
162
+ // Bedrock-resolvable form up front. Client-side IDs like
163
+ // `claude-opus-4-7` are valid on api.anthropic.com but Bedrock's
164
+ // InvokeModel rejects them with ValidationException; the retry path
165
+ // would replay that exact rejected ID and turn an upstream blip into
166
+ // 100% failure. Canonicalizing here makes router decisions, the
167
+ // outbound rewrite, the no-downgrade comparison, and the retry body
168
+ // all see the same Bedrock-OK ID. anthropic mode passes through
169
+ // unchanged so api.anthropic.com keeps accepting short IDs.
170
+ const rawInboundModel = body && typeof body.model === 'string' ? body.model : null;
171
+ const originalModel = upstreamMode === 'bedrock'
172
+ ? canonicalizeForBedrock(rawInboundModel)
173
+ : rawInboundModel;
115
174
  let chosenModel = originalModel;
116
175
  let decisionTier = null;
117
176
  let decisionReason = null;
@@ -159,7 +218,12 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
159
218
  }
160
219
 
161
220
  let outboundBody = body;
162
- if (enabled && chosenModel && chosenModel !== originalModel) {
221
+ // Rewrite the outbound body when chosenModel differs from what the
222
+ // CLIENT actually sent (rawInboundModel), not just from the canonical
223
+ // originalModel. Otherwise bedrock-mode short-ID inbounds where the
224
+ // router didn't change tier (chosenModel === canonical(rawInbound))
225
+ // would forward the original body — leaking the short ID to Bedrock.
226
+ if (enabled && chosenModel && chosenModel !== rawInboundModel) {
163
227
  try {
164
228
  outboundBody = rewriteModel(body, chosenModel);
165
229
  } catch (err) {
@@ -242,7 +306,41 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
242
306
  // restore it on the throw path.
243
307
  let drainedFirst = '';
244
308
  if (upstream.text) {
245
- try { drainedFirst = await upstream.text(); } catch { /* socket already gone */ }
309
+ // Bound response-body drain to 10s to prevent hanging on large or
310
+ // slow-streaming error responses. If the drain times out, log it
311
+ // but continue — the original 5xx is still cached and will be
312
+ // returned to the caller if the retry throws.
313
+ //
314
+ // Clear the timer when text() resolves first, otherwise the
315
+ // setTimeout sits in the event loop for 10s holding a closure
316
+ // reference. Under sustained 5xx storms (the exact scenario this
317
+ // branch targets) one such timer per retry would accumulate.
318
+ let drainTimer;
319
+ try {
320
+ drainedFirst = await Promise.race([
321
+ upstream.text(),
322
+ new Promise((_, reject) => {
323
+ drainTimer = setTimeout(
324
+ () => reject(new Error('response drain timeout')),
325
+ 10_000,
326
+ );
327
+ }),
328
+ ]);
329
+ } catch (e) {
330
+ // socket already gone, timeout, or parse error. Log drain errors
331
+ // and continue with empty body — the retry response will carry the
332
+ // actual error to the caller.
333
+ if (e?.message?.includes('timeout')) {
334
+ log.warn?.(JSON.stringify({
335
+ event: 'router_fallback',
336
+ reason: 'upstream_5xx_drain_timeout',
337
+ original_model: originalModel,
338
+ would_have_been: chosenModel,
339
+ }));
340
+ }
341
+ } finally {
342
+ if (drainTimer) clearTimeout(drainTimer);
343
+ }
246
344
  }
247
345
  try {
248
346
  const retryBody = rewriteModel(body, originalModel);
@@ -317,4 +415,6 @@ module.exports = {
317
415
  resolveTierModels,
318
416
  parseClaudeId,
319
417
  isIntraFamilyDowngrade,
418
+ canonicalizeForBedrock,
419
+ KNOWN_BEDROCK_ALIASES,
320
420
  };
@@ -65,23 +65,47 @@ class SyncEngine {
65
65
  _scheduleOutbound(delayMs) {
66
66
  if (!this._running) return;
67
67
  this._outTimer = setTimeout(async () => {
68
- if (!this._running) return;
69
- this._outPending = true;
68
+ // Defence-in-depth: a throw from this.outbound.flush(),
69
+ // this.store.countPending(), or any post-flush bookkeeping used
70
+ // to escape the setTimeout callback. Node logs the unhandled
71
+ // rejection and the next setTimeout was never armed — the
72
+ // outbound sync loop silently died until the process restarted,
73
+ // while `_running` stayed true (no signal to the caller).
74
+ //
75
+ // Mirrors the heartbeat-loop fix in PR #147 (issue #544): wrap
76
+ // the whole tick, schedule the next iteration in `finally` so a
77
+ // surprise throw cannot park the loop.
78
+ let nextDelay = DEFAULT_OUTBOUND_INTERVAL;
70
79
  try {
71
- const result = await this.outbound.flush();
72
- if (result.sent > 0) this._lastActivity = Date.now();
73
- } catch (err) {
74
- if (err instanceof AuthError) {
75
- await this._handleAuthError('outbound');
76
- } else {
77
- this.logger.error(`[sync] outbound error: ${err.message}`);
80
+ if (!this._running) return;
81
+ this._outPending = true;
82
+ try {
83
+ const result = await this.outbound.flush();
84
+ if (result.sent > 0) this._lastActivity = Date.now();
85
+ } catch (err) {
86
+ if (err instanceof AuthError) {
87
+ await this._handleAuthError('outbound');
88
+ } else {
89
+ this.logger.error(`[sync] outbound error: ${err.message}`);
90
+ }
78
91
  }
92
+ this._outPending = false;
93
+ try {
94
+ const pending = this.store.countPending({ direction: 'outbound' });
95
+ if (pending > 0) nextDelay = 1_000;
96
+ } catch (err) {
97
+ // countPending threw (corrupt store, FS hiccup): keep the
98
+ // default cadence rather than parking the loop.
99
+ this.logger.error(`[sync] countPending threw (non-fatal): ${err && err.message}`);
100
+ }
101
+ } catch (err) {
102
+ // Anything that escaped the inner blocks above. Log and let
103
+ // finally re-arm the timer.
104
+ this.logger.error(`[sync] outbound tick threw (non-fatal): ${err && err.message}`);
105
+ this._outPending = false;
106
+ } finally {
107
+ if (this._running) this._scheduleOutbound(nextDelay);
79
108
  }
80
- this._outPending = false;
81
- const nextDelay = this.store.countPending({ direction: 'outbound' }) > 0
82
- ? 1_000
83
- : DEFAULT_OUTBOUND_INTERVAL;
84
- this._scheduleOutbound(nextDelay);
85
109
  }, delayMs);
86
110
  if (this._outTimer.unref) this._outTimer.unref();
87
111
  }
@@ -89,29 +113,42 @@ class SyncEngine {
89
113
  _scheduleInbound(delayMs) {
90
114
  if (!this._running) return;
91
115
  this._inTimer = setTimeout(async () => {
92
- if (!this._running) return;
116
+ // Same defence-in-depth pattern as _scheduleOutbound: a throw
117
+ // from inbound.pull / ackDelivered / _isIdle used to escape the
118
+ // setTimeout callback and silently park the inbound loop.
119
+ let nextDelay = DEFAULT_POLL_INTERVAL_ACTIVE;
93
120
  try {
94
- const result = await this.inbound.pull();
95
- if (result.received > 0) {
96
- this._lastActivity = Date.now();
97
- if (typeof this.onInboundReceived === 'function') {
98
- try { this.onInboundReceived(result.received); } catch (e) {
99
- this.logger.warn?.('[sync] onInboundReceived callback failed:', e.message);
121
+ if (!this._running) return;
122
+ try {
123
+ const result = await this.inbound.pull();
124
+ if (result.received > 0) {
125
+ this._lastActivity = Date.now();
126
+ if (typeof this.onInboundReceived === 'function') {
127
+ try { this.onInboundReceived(result.received); } catch (e) {
128
+ this.logger.warn?.('[sync] onInboundReceived callback failed:', e.message);
129
+ }
100
130
  }
101
131
  }
132
+ await this.inbound.ackDelivered();
133
+ } catch (err) {
134
+ if (err instanceof AuthError) {
135
+ await this._handleAuthError('inbound');
136
+ } else {
137
+ this.logger.error(`[sync] inbound error: ${err.message}`);
138
+ }
102
139
  }
103
- await this.inbound.ackDelivered();
104
- } catch (err) {
105
- if (err instanceof AuthError) {
106
- await this._handleAuthError('inbound');
107
- } else {
108
- this.logger.error(`[sync] inbound error: ${err.message}`);
140
+ try {
141
+ nextDelay = this._isIdle()
142
+ ? DEFAULT_POLL_INTERVAL_IDLE
143
+ : DEFAULT_POLL_INTERVAL_ACTIVE;
144
+ } catch (err) {
145
+ this.logger.error(`[sync] _isIdle threw (non-fatal): ${err && err.message}`);
109
146
  }
147
+ } catch (err) {
148
+ this.logger.error(`[sync] inbound tick threw (non-fatal): ${err && err.message}`);
149
+ } finally {
150
+ if (this._running) this._scheduleInbound(nextDelay);
110
151
  }
111
- const nextDelay = this._isIdle()
112
- ? DEFAULT_POLL_INTERVAL_IDLE
113
- : DEFAULT_POLL_INTERVAL_ACTIVE;
114
- this._scheduleInbound(nextDelay);
115
152
  }, delayMs);
116
153
  if (this._inTimer.unref) this._inTimer.unref();
117
154
  }