@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.
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +9 -8
- package/README.zh-CN.md +9 -8
- package/index.js +30 -11
- package/package.json +1 -1
- package/scripts/build_binaries.js +31 -7
- package/src/atp/atpExecute.js +35 -8
- package/src/atp/autoBuyer.js +155 -21
- package/src/atp/autoDeliver.js +16 -0
- package/src/atp/cli.js +98 -0
- package/src/atp/cliAutobuyPrompt.js +57 -64
- package/src/atp/hubClient.js +42 -4
- 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/forceUpdate.js +2 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +52 -5
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.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/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/paths.js +6 -2
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +57 -3
- package/src/gep/selector.js +1 -1
- package/src/gep/selfPR.js +34 -1
- package/src/gep/skill2gep.js +108 -29
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/index.js +29 -9
- package/src/proxy/lifecycle/manager.js +97 -37
- package/src/proxy/router/messages_route.js +105 -5
- 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-
|
|
32
|
-
mid: 'global.anthropic.claude-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/proxy/sync/engine.js
CHANGED
|
@@ -65,23 +65,47 @@ class SyncEngine {
|
|
|
65
65
|
_scheduleOutbound(delayMs) {
|
|
66
66
|
if (!this._running) return;
|
|
67
67
|
this._outTimer = setTimeout(async () => {
|
|
68
|
-
|
|
69
|
-
this.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
this.logger.error(`[sync]
|
|
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
|
}
|