@evomap/evolver 1.89.20 → 2.0.0-beta.0
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.md +27 -564
- package/bin/evolver-llm-proxy.js +3 -0
- package/bin/evolver-mcp.js +2 -0
- package/bin/evolver-proxy.js +3 -0
- package/bin/evolver.js +4 -0
- package/index.js +1 -3586
- package/package.json +32 -62
- package/CONTRIBUTING.md +0 -19
- package/LICENSE +0 -641
- package/README.ja-JP.md +0 -521
- package/README.ko-KR.md +0 -520
- package/README.zh-CN.md +0 -531
- package/SKILL.md +0 -365
- package/assets/cover.png +0 -0
- package/assets/gep/genes.seed.json +0 -496
- package/conformance/savings-core/constants.json +0 -30
- package/conformance/savings-core/golden-vectors.json +0 -333
- package/scripts/a2a_export.js +0 -63
- package/scripts/a2a_ingest.js +0 -79
- package/scripts/a2a_promote.js +0 -118
- package/scripts/analyze_by_skill.js +0 -121
- package/scripts/build_binaries.js +0 -479
- package/scripts/check-changelog.js +0 -166
- package/scripts/extract_log.js +0 -85
- package/scripts/generate_history.js +0 -75
- package/scripts/gep_append_event.js +0 -96
- package/scripts/gep_personality_report.js +0 -234
- package/scripts/human_report.js +0 -147
- package/scripts/recall-verify-report.js +0 -234
- package/scripts/recover_loop.js +0 -61
- package/scripts/refresh_stars_badge.js +0 -168
- package/scripts/seed-merchants.js +0 -91
- package/scripts/skill2recipes.js +0 -118
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/skills/_meta/SKILL.md +0 -41
- package/skills/index.json +0 -14
- package/src/adapters/claudeCode.js +0 -194
- package/src/adapters/codex.js +0 -216
- package/src/adapters/cursor.js +0 -91
- package/src/adapters/hookAdapter.js +0 -469
- package/src/adapters/kiro.js +0 -195
- package/src/adapters/opencode.js +0 -326
- package/src/adapters/scripts/_lockPaths.js +0 -74
- package/src/adapters/scripts/_memoryFiltering.js +0 -35
- package/src/adapters/scripts/_runtimePaths.js +0 -440
- package/src/adapters/scripts/evolver-session-end.js +0 -321
- package/src/adapters/scripts/evolver-session-start.js +0 -587
- package/src/adapters/scripts/evolver-signal-detect.js +0 -98
- package/src/adapters/scripts/evolver-task-recall.js +0 -173
- package/src/atp/atpExecute.js +0 -283
- package/src/atp/atpTaskPickup.js +0 -233
- package/src/atp/autoBuyer.js +0 -382
- package/src/atp/autoDeliver.js +0 -215
- package/src/atp/cli.js +0 -354
- package/src/atp/cliAutobuyPrompt.js +0 -154
- package/src/atp/consumerAgent.js +0 -157
- package/src/atp/defaultHandler.js +0 -69
- package/src/atp/heartbeatSignalsHandler.js +0 -254
- package/src/atp/hubClient.js +0 -317
- package/src/atp/index.js +0 -38
- package/src/atp/merchantAgent.js +0 -118
- package/src/atp/protocol.js +0 -41
- package/src/atp/questionComposer.js +0 -133
- package/src/atp/serviceHelper.js +0 -92
- package/src/canary.js +0 -13
- package/src/config.js +0 -351
- package/src/evolve/guards.js +0 -1
- package/src/evolve/pipeline/collect.js +0 -1
- package/src/evolve/pipeline/dispatch.js +0 -1
- package/src/evolve/pipeline/enrich.js +0 -1
- package/src/evolve/pipeline/hub.js +0 -1
- package/src/evolve/pipeline/select.js +0 -1
- package/src/evolve/pipeline/signals.js +0 -1
- package/src/evolve/utils.js +0 -1
- package/src/evolve.js +0 -1
- package/src/experiment/agentRunner.js +0 -229
- package/src/experiment/cli.js +0 -159
- package/src/experiment/comparison.js +0 -233
- package/src/experiment/metrics.js +0 -75
- package/src/forceUpdate.js +0 -991
- package/src/gep/a2a.js +0 -173
- package/src/gep/a2aProtocol.js +0 -1
- package/src/gep/analyzer.js +0 -35
- package/src/gep/antiAbuseTelemetry.js +0 -1
- package/src/gep/assetCallLog.js +0 -197
- package/src/gep/assetStore.js +0 -723
- package/src/gep/assets.js +0 -36
- package/src/gep/autoDistillConv.js +0 -1
- package/src/gep/autoDistillLlm.js +0 -1
- package/src/gep/bridge.js +0 -138
- package/src/gep/candidateEval.js +0 -1
- package/src/gep/candidates.js +0 -1
- package/src/gep/claimNudge.js +0 -121
- package/src/gep/cliContracts.js +0 -1190
- package/src/gep/contentHash.js +0 -1
- package/src/gep/conversationDistiller.js +0 -1
- package/src/gep/conversationSniffer.js +0 -1
- package/src/gep/crypto.js +0 -1
- package/src/gep/curriculum.js +0 -1
- package/src/gep/deviceId.js +0 -1
- package/src/gep/directoryClient.js +0 -115
- package/src/gep/envFingerprint.js +0 -1
- package/src/gep/epigenetics.js +0 -1
- package/src/gep/execBridge.js +0 -1
- package/src/gep/executionTrace.js +0 -291
- package/src/gep/explore.js +0 -1
- package/src/gep/featureFlags.js +0 -121
- package/src/gep/gitOps.js +0 -265
- package/src/gep/hash.js +0 -1
- package/src/gep/hostErrorClassifier.js +0 -34
- package/src/gep/hubFetch.js +0 -1
- package/src/gep/hubReview.js +0 -1
- package/src/gep/hubSearch.js +0 -1
- package/src/gep/hubVerify.js +0 -1
- package/src/gep/idleScheduler.js +0 -400
- package/src/gep/issueReporter.js +0 -416
- package/src/gep/learningSignals.js +0 -1
- package/src/gep/llmReview.js +0 -92
- package/src/gep/localStateAwareness.js +0 -243
- package/src/gep/mailboxTransport.js +0 -119
- package/src/gep/memoryGraph.js +0 -1
- package/src/gep/memoryGraphAdapter.js +0 -1
- package/src/gep/mutation.js +0 -1
- package/src/gep/narrativeMemory.js +0 -1
- package/src/gep/oauthLogin.js +0 -181
- package/src/gep/openPRRegistry.js +0 -1
- package/src/gep/paths.js +0 -522
- package/src/gep/personality.js +0 -1
- package/src/gep/policyCheck.js +0 -1
- package/src/gep/portable.js +0 -103
- package/src/gep/privacyClient.js +0 -235
- package/src/gep/prompt.js +0 -1
- package/src/gep/questionGenerator.js +0 -518
- package/src/gep/recallInject.js +0 -1
- package/src/gep/recallVerifier.js +0 -1
- package/src/gep/reflection.js +0 -1
- package/src/gep/sanitize.js +0 -264
- package/src/gep/savingsCore.js +0 -1
- package/src/gep/schemas/capsule.js +0 -170
- package/src/gep/schemas/gene.js +0 -154
- package/src/gep/schemas/index.js +0 -8
- package/src/gep/schemas/protocol.js +0 -51
- package/src/gep/schemas/task.js +0 -74
- package/src/gep/selector.js +0 -1
- package/src/gep/selfPR.js +0 -469
- package/src/gep/signals.js +0 -776
- package/src/gep/skill2gep.js +0 -1056
- package/src/gep/skill2gepAudit.js +0 -303
- package/src/gep/skill2recipes.js +0 -511
- package/src/gep/skillDistiller.js +0 -1
- package/src/gep/skillPublisher.js +0 -358
- package/src/gep/solidify.js +0 -1
- package/src/gep/strategy.js +0 -1
- package/src/gep/taskReceiver.js +0 -575
- package/src/gep/tokenSavings.js +0 -1
- package/src/gep/trajectoryExport.js +0 -1
- package/src/gep/validationReport.js +0 -55
- package/src/gep/validator/index.js +0 -411
- package/src/gep/validator/reporter.js +0 -210
- package/src/gep/validator/sandboxExecutor.js +0 -480
- package/src/gep/validator/stakeBootstrap.js +0 -357
- package/src/gep/workspaceKeychain.js +0 -1
- package/src/ops/cleanup.js +0 -80
- package/src/ops/commentary.js +0 -60
- package/src/ops/health_check.js +0 -104
- package/src/ops/index.js +0 -11
- package/src/ops/innovation.js +0 -67
- package/src/ops/lifecycle.js +0 -798
- package/src/ops/self_repair.js +0 -76
- package/src/ops/skills_monitor.js +0 -147
- package/src/ops/trigger.js +0 -33
- package/src/proxy/clientSettings.js +0 -405
- package/src/proxy/envelope.js +0 -59
- package/src/proxy/extensions/dmHandler.js +0 -45
- package/src/proxy/extensions/sessionHandler.js +0 -141
- package/src/proxy/extensions/skillUpdater.js +0 -64
- package/src/proxy/extensions/traceControl.js +0 -1
- package/src/proxy/index.js +0 -1395
- package/src/proxy/inject.js +0 -1
- package/src/proxy/lifecycle/manager.js +0 -1568
- package/src/proxy/mailbox/state.js +0 -207
- package/src/proxy/mailbox/store.js +0 -590
- package/src/proxy/router/cache_passthrough.js +0 -26
- package/src/proxy/router/features.js +0 -84
- package/src/proxy/router/gemini_route.js +0 -154
- package/src/proxy/router/messages_route.js +0 -535
- package/src/proxy/router/model_router.js +0 -113
- package/src/proxy/router/models_route.js +0 -52
- package/src/proxy/router/ollama_route.js +0 -103
- package/src/proxy/router/responses_route.js +0 -170
- package/src/proxy/router/vertex_route.js +0 -110
- package/src/proxy/server/http.js +0 -363
- package/src/proxy/server/routes.js +0 -558
- package/src/proxy/server/settings.js +0 -115
- package/src/proxy/sync/engine.js +0 -179
- package/src/proxy/sync/inbound.js +0 -211
- package/src/proxy/sync/outbound.js +0 -320
- package/src/proxy/task/monitor.js +0 -131
- package/src/proxy/trace/extractor.js +0 -1
- package/src/proxy/trace/usage.js +0 -1
- package/src/webui/client/clientJs/assets.js +0 -111
- package/src/webui/client/clientJs/bootstrap.js +0 -92
- package/src/webui/client/clientJs/common.js +0 -77
- package/src/webui/client/clientJs/i18n.js +0 -366
- package/src/webui/client/clientJs/index.js +0 -35
- package/src/webui/client/clientJs/interactions.js +0 -351
- package/src/webui/client/clientJs/overview.js +0 -152
- package/src/webui/client/clientJs/personality.js +0 -285
- package/src/webui/client/clientJs/pipelines.js +0 -330
- package/src/webui/client/indexHtml.js +0 -221
- package/src/webui/client/static.js +0 -23
- package/src/webui/client/stylesCss.js +0 -639
- package/src/webui/client/vendor/README.md +0 -15
- package/src/webui/client/vendor/echarts.min.js +0 -45
- package/src/webui/index.js +0 -14
- package/src/webui/observer/assets.js +0 -146
- package/src/webui/observer/index.js +0 -37
- package/src/webui/observer/interactions.js +0 -127
- package/src/webui/observer/jsonl.js +0 -75
- package/src/webui/observer/paths.js +0 -46
- package/src/webui/observer/personality.js +0 -43
- package/src/webui/observer/pipelineEvents.js +0 -58
- package/src/webui/observer/redact.js +0 -63
- package/src/webui/observer/runs.js +0 -356
- package/src/webui/observer/safety.js +0 -57
- package/src/webui/observer/skills.js +0 -70
- package/src/webui/observer/status.js +0 -71
- package/src/webui/server/http.js +0 -138
- package/src/webui/server/routes.js +0 -41
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const {
|
|
7
|
-
PRIVATE_FILE_MODE,
|
|
8
|
-
bestEffortChmod,
|
|
9
|
-
ensurePrivateDir,
|
|
10
|
-
writePrivateFile,
|
|
11
|
-
readMailboxStateFile,
|
|
12
|
-
writeMergedMailboxStateFile,
|
|
13
|
-
} = require('./state');
|
|
14
|
-
|
|
15
|
-
const DEFAULT_CHANNEL = 'evomap-hub';
|
|
16
|
-
const SCHEMA_VERSION = 1;
|
|
17
|
-
const PROXY_PROTOCOL_VERSION = '0.1.0';
|
|
18
|
-
const DEFAULT_MAX_JSONL_LINE_BYTES = 16 * 1024 * 1024;
|
|
19
|
-
const JSONL_READ_CHUNK_BYTES = 64 * 1024;
|
|
20
|
-
|
|
21
|
-
// Merge `fields` into `target` while stripping keys that can mutate the
|
|
22
|
-
// prototype chain. Mailbox rows are persisted as JSONL and rebuilt on
|
|
23
|
-
// startup; without this filter a crafted messages.jsonl line containing
|
|
24
|
-
// __proto__/constructor/prototype could pollute Object.prototype during
|
|
25
|
-
// _rebuildIndex (see GHSA-2cjr-5v3h-v2w4).
|
|
26
|
-
function safeAssign(target, fields) {
|
|
27
|
-
if (!fields || typeof fields !== 'object') return target;
|
|
28
|
-
const keys = Object.keys(fields);
|
|
29
|
-
for (let i = 0; i < keys.length; i++) {
|
|
30
|
-
const k = keys[i];
|
|
31
|
-
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
32
|
-
target[k] = fields[k];
|
|
33
|
-
}
|
|
34
|
-
return target;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sanitizeRow(row) {
|
|
38
|
-
if (!row || typeof row !== 'object') return row;
|
|
39
|
-
const out = {};
|
|
40
|
-
safeAssign(out, row);
|
|
41
|
-
if (out.fields && typeof out.fields === 'object') {
|
|
42
|
-
const cleanFields = {};
|
|
43
|
-
safeAssign(cleanFields, out.fields);
|
|
44
|
-
out.fields = cleanFields;
|
|
45
|
-
}
|
|
46
|
-
return out;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// --- UUID v7 (RFC 9562) ---
|
|
50
|
-
// Bits 0-47: unix_ts_ms, Bits 48-51: ver=0b0111, Bits 52-63: rand_a,
|
|
51
|
-
// Bits 64-65: var=0b10, Bits 66-127: rand_b
|
|
52
|
-
|
|
53
|
-
function generateUUIDv7() {
|
|
54
|
-
const now = Date.now();
|
|
55
|
-
const msHex = now.toString(16).padStart(12, '0');
|
|
56
|
-
|
|
57
|
-
const bytes = crypto.randomBytes(10);
|
|
58
|
-
bytes[0] = (bytes[0] & 0x0f) | 0x70; // version 7
|
|
59
|
-
bytes[2] = (bytes[2] & 0x3f) | 0x80; // variant 10
|
|
60
|
-
|
|
61
|
-
const randHex = bytes.toString('hex');
|
|
62
|
-
|
|
63
|
-
// Standard UUID format: 8-4-4-4-12 (32 hex total)
|
|
64
|
-
return [
|
|
65
|
-
msHex.slice(0, 8),
|
|
66
|
-
msHex.slice(8, 12),
|
|
67
|
-
randHex.slice(0, 4),
|
|
68
|
-
randHex.slice(4, 8),
|
|
69
|
-
randHex.slice(8, 20),
|
|
70
|
-
].join('-');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- JSONL file helpers ---
|
|
74
|
-
|
|
75
|
-
function safeParse(payload) {
|
|
76
|
-
if (payload == null) return null;
|
|
77
|
-
if (typeof payload !== 'string') return payload;
|
|
78
|
-
try { return JSON.parse(payload); } catch { return payload; }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Round-9: the round-8 cross-process append lock (§21.7) was REMOVED.
|
|
82
|
-
// Its premise -- that fs.appendFileSync to a regular file can interleave
|
|
83
|
-
// bytes mid-line unless each write stays under PIPE_BUF (512 B darwin,
|
|
84
|
-
// 4096 B linux) -- conflated two different POSIX guarantees. PIPE_BUF
|
|
85
|
-
// atomicity is defined for PIPES/FIFOs, not regular files. A single
|
|
86
|
-
// O_APPEND write() to a regular file is positioned atomically at EOF and
|
|
87
|
-
// is not interleaved with other appenders on the local filesystems evolver
|
|
88
|
-
// uses (~/.evomap); this was verified empirically on darwin/APFS --
|
|
89
|
-
// concurrent 4 KB..1 MB appends from 6 writers produced zero torn lines.
|
|
90
|
-
// So the lock guarded a non-problem. Worse, its 5 s deadline with a
|
|
91
|
-
// busy-wait (Atomics.wait, then a spin-loop fallback) ran on the single
|
|
92
|
-
// JS thread, so under any real contention it BLOCKED the event loop --
|
|
93
|
-
// starving the very heartbeat/SSE/HTTP it shared the process with, i.e.
|
|
94
|
-
// it could itself produce the "process alive but inert" symptom it claimed
|
|
95
|
-
// to prevent. fs.appendFileSync writes the whole buffer with O_APPEND, so
|
|
96
|
-
// a single record lands as one atomic append.
|
|
97
|
-
//
|
|
98
|
-
// Windows note: PIPE_BUF is a POSIX concept; it does not exist on Windows.
|
|
99
|
-
// Windows NTFS provides the same atomicity guarantee for O_APPEND writes to
|
|
100
|
-
// regular files that POSIX local filesystems do, so the removal above is
|
|
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
|
-
|
|
116
|
-
function appendLine(filePath, obj) {
|
|
117
|
-
const line = jsonlLineForWrite(obj);
|
|
118
|
-
fs.appendFileSync(filePath, line, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
119
|
-
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
120
|
-
}
|
|
121
|
-
|
|
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
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return stats;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- In-memory index that backs JSONL persistence ---
|
|
216
|
-
|
|
217
|
-
class MailboxStore {
|
|
218
|
-
constructor(dataDir) {
|
|
219
|
-
if (!dataDir) throw new Error('dataDir is required');
|
|
220
|
-
ensurePrivateDir(dataDir);
|
|
221
|
-
this.dataDir = dataDir;
|
|
222
|
-
|
|
223
|
-
this._messagesFile = path.join(dataDir, 'messages.jsonl');
|
|
224
|
-
this._stateFile = path.join(dataDir, 'state.json');
|
|
225
|
-
bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
|
|
226
|
-
bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
|
|
227
|
-
|
|
228
|
-
// in-memory indexes
|
|
229
|
-
this._messages = new Map(); // id -> message object
|
|
230
|
-
this._outbound = []; // ordered outbound refs (id)
|
|
231
|
-
this._inbound = []; // ordered inbound refs (id)
|
|
232
|
-
|
|
233
|
-
this._state = {}; // key-value state (cursors, node_id, etc.)
|
|
234
|
-
|
|
235
|
-
this._loadState();
|
|
236
|
-
this._rebuildIndex();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
_loadState() {
|
|
240
|
-
this._state = readMailboxStateFile(this._stateFile) || {};
|
|
241
|
-
const existingVersion = this._state._schema_version || 0;
|
|
242
|
-
const beforeMigrationState = { ...this._state };
|
|
243
|
-
if (existingVersion < SCHEMA_VERSION) {
|
|
244
|
-
this._runMigrations(existingVersion, SCHEMA_VERSION);
|
|
245
|
-
}
|
|
246
|
-
this._state._schema_version = SCHEMA_VERSION;
|
|
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);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
_runMigrations(fromVersion, toVersion) {
|
|
258
|
-
for (let v = fromVersion + 1; v <= toVersion; v++) {
|
|
259
|
-
const migrator = MIGRATIONS[v];
|
|
260
|
-
if (typeof migrator === 'function') {
|
|
261
|
-
migrator(this);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
_persistState(updatedKeys) {
|
|
267
|
-
// Round-7 (§20.5): per-PID tmp path. Two evolver processes (daemon +
|
|
268
|
-
// ad-hoc CLI / proxy + loop) writing to the same `${stateFile}.tmp`
|
|
269
|
-
// would otherwise interleave: process B's writeFileSync truncates
|
|
270
|
-
// A's tmp mid-write, then B's rename completes with B's truncated
|
|
271
|
-
// payload as the final state.json. `state.json` holds the cached
|
|
272
|
-
// node_secret after a hub rotation -- a torn write here is the
|
|
273
|
-
// load-bearing trigger for the "401-loop -> reauth backoff -> dead
|
|
274
|
-
// for 30 min..4 h" symptom this branch targets. Matches the
|
|
275
|
-
// precedent set by _persistNodeSecret in src/gep/a2aProtocol.js.
|
|
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);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
_rebuildIndex() {
|
|
284
|
-
this._messages.clear();
|
|
285
|
-
this._outbound = [];
|
|
286
|
-
this._inbound = [];
|
|
287
|
-
|
|
288
|
-
const TERMINAL = new Set(['synced', 'delivered', 'failed', 'rejected']);
|
|
289
|
-
const stats = forEachJsonLine(this._messagesFile, (rawRow) => {
|
|
290
|
-
const row = sanitizeRow(rawRow);
|
|
291
|
-
if (!row || typeof row !== 'object') return;
|
|
292
|
-
if (row._op === 'update') {
|
|
293
|
-
const existing = this._messages.get(row.id);
|
|
294
|
-
if (existing) safeAssign(existing, row.fields);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (!row.id) return;
|
|
298
|
-
this._messages.set(row.id, row);
|
|
299
|
-
});
|
|
300
|
-
this._lastRebuildStats = stats;
|
|
301
|
-
for (const [id, msg] of this._messages) {
|
|
302
|
-
if (!msg || typeof msg !== 'object') continue;
|
|
303
|
-
if (TERMINAL.has(msg.status)) continue;
|
|
304
|
-
if (msg.direction === 'outbound') this._outbound.push(id);
|
|
305
|
-
else if (msg.direction === 'inbound') this._inbound.push(id);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
_appendMessage(msg) {
|
|
310
|
-
appendLine(this._messagesFile, msg);
|
|
311
|
-
this._messages.set(msg.id, msg);
|
|
312
|
-
if (msg.direction === 'outbound') this._outbound.push(msg.id);
|
|
313
|
-
else if (msg.direction === 'inbound') this._inbound.push(msg.id);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
_appendUpdate(id, fields) {
|
|
317
|
-
appendLine(this._messagesFile, { _op: 'update', id, fields });
|
|
318
|
-
const existing = this._messages.get(id);
|
|
319
|
-
if (existing) safeAssign(existing, fields);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
_evictFromIndex(id) {
|
|
323
|
-
const msg = this._messages.get(id);
|
|
324
|
-
if (!msg) return;
|
|
325
|
-
const arr = msg.direction === 'outbound' ? this._outbound : this._inbound;
|
|
326
|
-
const idx = arr.indexOf(id);
|
|
327
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// --- Public API: send / writeInbound ---
|
|
331
|
-
|
|
332
|
-
send({ type, payload, channel, priority, refId, expiresAt }) {
|
|
333
|
-
const id = generateUUIDv7();
|
|
334
|
-
const now = Date.now();
|
|
335
|
-
const msg = {
|
|
336
|
-
id,
|
|
337
|
-
channel: channel || DEFAULT_CHANNEL,
|
|
338
|
-
direction: 'outbound',
|
|
339
|
-
type,
|
|
340
|
-
status: 'pending',
|
|
341
|
-
payload: safeParse(payload),
|
|
342
|
-
priority: priority || 'normal',
|
|
343
|
-
ref_id: refId || null,
|
|
344
|
-
created_at: now,
|
|
345
|
-
synced_at: null,
|
|
346
|
-
expires_at: expiresAt || null,
|
|
347
|
-
retry_count: 0,
|
|
348
|
-
error: null,
|
|
349
|
-
};
|
|
350
|
-
this._appendMessage(msg);
|
|
351
|
-
return { message_id: id, status: 'pending' };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
writeInbound({ id, type, payload, channel, priority, refId, expiresAt }) {
|
|
355
|
-
const msgId = id || generateUUIDv7();
|
|
356
|
-
// At-least-once delivery from the Hub / retry loops can send the same
|
|
357
|
-
// message id twice. Without idempotency, poll() and countPending() would
|
|
358
|
-
// double-count the retry. See community PR #515.
|
|
359
|
-
if (this._messages.has(msgId)) return msgId;
|
|
360
|
-
|
|
361
|
-
const now = Date.now();
|
|
362
|
-
const msg = {
|
|
363
|
-
id: msgId,
|
|
364
|
-
channel: channel || DEFAULT_CHANNEL,
|
|
365
|
-
direction: 'inbound',
|
|
366
|
-
type,
|
|
367
|
-
status: 'pending',
|
|
368
|
-
payload: safeParse(payload),
|
|
369
|
-
priority: priority || 'normal',
|
|
370
|
-
ref_id: refId || null,
|
|
371
|
-
created_at: now,
|
|
372
|
-
synced_at: null,
|
|
373
|
-
expires_at: expiresAt || null,
|
|
374
|
-
retry_count: 0,
|
|
375
|
-
error: null,
|
|
376
|
-
};
|
|
377
|
-
this._appendMessage(msg);
|
|
378
|
-
return msgId;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
writeInboundBatch(messages) {
|
|
382
|
-
const ids = [];
|
|
383
|
-
for (const m of messages) {
|
|
384
|
-
ids.push(this.writeInbound(m));
|
|
385
|
-
}
|
|
386
|
-
return ids;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// --- Public API: query ---
|
|
390
|
-
|
|
391
|
-
getById(id) {
|
|
392
|
-
const msg = this._messages.get(id);
|
|
393
|
-
return msg ? { ...msg } : null;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
poll({ channel, type, limit } = {}) {
|
|
397
|
-
const max = Math.max(1, Math.min(limit || 20, 100));
|
|
398
|
-
const results = [];
|
|
399
|
-
for (const id of this._inbound) {
|
|
400
|
-
if (results.length >= max) break;
|
|
401
|
-
const msg = this._messages.get(id);
|
|
402
|
-
if (!msg || msg.status !== 'pending') continue;
|
|
403
|
-
if (channel && msg.channel !== channel) continue;
|
|
404
|
-
if (type && msg.type !== type) continue;
|
|
405
|
-
results.push({ ...msg });
|
|
406
|
-
}
|
|
407
|
-
return results;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
pollOutbound({ channel, limit } = {}) {
|
|
411
|
-
const ch = channel || DEFAULT_CHANNEL;
|
|
412
|
-
const max = Math.max(1, Math.min(limit || 50, 200));
|
|
413
|
-
const results = [];
|
|
414
|
-
|
|
415
|
-
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
416
|
-
const candidates = [];
|
|
417
|
-
for (const id of this._outbound) {
|
|
418
|
-
const msg = this._messages.get(id);
|
|
419
|
-
if (!msg || msg.status !== 'pending') continue;
|
|
420
|
-
if (msg.channel !== ch) continue;
|
|
421
|
-
candidates.push(msg);
|
|
422
|
-
}
|
|
423
|
-
candidates.sort((a, b) => {
|
|
424
|
-
const pa = priorityOrder[a.priority] ?? 1;
|
|
425
|
-
const pb = priorityOrder[b.priority] ?? 1;
|
|
426
|
-
if (pa !== pb) return pa - pb;
|
|
427
|
-
return a.created_at - b.created_at;
|
|
428
|
-
});
|
|
429
|
-
for (let i = 0; i < Math.min(candidates.length, max); i++) {
|
|
430
|
-
results.push({ ...candidates[i] });
|
|
431
|
-
}
|
|
432
|
-
return results;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// --- Public API: status updates ---
|
|
436
|
-
|
|
437
|
-
ack(messageIds) {
|
|
438
|
-
const ids = Array.isArray(messageIds) ? messageIds : [messageIds];
|
|
439
|
-
let count = 0;
|
|
440
|
-
for (const id of ids) {
|
|
441
|
-
const msg = this._messages.get(id);
|
|
442
|
-
if (msg && msg.direction === 'inbound') {
|
|
443
|
-
this._appendUpdate(id, { status: 'delivered' });
|
|
444
|
-
this._evictFromIndex(id);
|
|
445
|
-
count++;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return count;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
updateStatus(id, status, { error, syncedAt } = {}) {
|
|
452
|
-
const TERMINAL = new Set(['synced', 'delivered', 'failed', 'rejected']);
|
|
453
|
-
const fields = { status };
|
|
454
|
-
if (syncedAt) fields.synced_at = syncedAt;
|
|
455
|
-
else if (status === 'synced') fields.synced_at = Date.now();
|
|
456
|
-
if (error !== undefined) fields.error = error;
|
|
457
|
-
else if (status !== 'failed' && status !== 'rejected') fields.error = null;
|
|
458
|
-
this._appendUpdate(id, fields);
|
|
459
|
-
if (TERMINAL.has(status)) {
|
|
460
|
-
this._evictFromIndex(id);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
updateStatusBatch(updates) {
|
|
465
|
-
for (const u of updates) {
|
|
466
|
-
this.updateStatus(u.id, u.status, { error: u.error, syncedAt: u.syncedAt });
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
incrementRetry(id, error) {
|
|
471
|
-
const msg = this._messages.get(id);
|
|
472
|
-
const newCount = msg ? (msg.retry_count || 0) + 1 : 1;
|
|
473
|
-
this._appendUpdate(id, { retry_count: newCount, error: error || null });
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
list({ type, direction, status, limit, offset } = {}) {
|
|
477
|
-
if (!type) throw new Error('type is required for list()');
|
|
478
|
-
const max = Math.max(1, Math.min(limit || 20, 100));
|
|
479
|
-
const skip = Math.max(0, offset || 0);
|
|
480
|
-
|
|
481
|
-
const all = [];
|
|
482
|
-
for (const [, msg] of this._messages) {
|
|
483
|
-
if (type !== '%' && msg.type !== type) continue;
|
|
484
|
-
if (direction && msg.direction !== direction) continue;
|
|
485
|
-
if (status && msg.status !== status) continue;
|
|
486
|
-
all.push(msg);
|
|
487
|
-
}
|
|
488
|
-
all.sort((a, b) => b.created_at - a.created_at);
|
|
489
|
-
return all.slice(skip, skip + max).map(m => ({ ...m }));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
countPending({ direction, channel, type } = {}) {
|
|
493
|
-
const dir = direction || 'outbound';
|
|
494
|
-
let count = 0;
|
|
495
|
-
const idList = dir === 'outbound' ? this._outbound : this._inbound;
|
|
496
|
-
for (const id of idList) {
|
|
497
|
-
const msg = this._messages.get(id);
|
|
498
|
-
if (!msg || msg.status !== 'pending') continue;
|
|
499
|
-
if (channel && msg.channel !== channel) continue;
|
|
500
|
-
if (type && msg.type !== type) continue;
|
|
501
|
-
count++;
|
|
502
|
-
}
|
|
503
|
-
return count;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// --- Public API: state / cursors ---
|
|
507
|
-
|
|
508
|
-
getCursor(key) {
|
|
509
|
-
const val = this._state[`cursor:${key}`];
|
|
510
|
-
return val !== undefined ? val : null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
setCursor(key, value) {
|
|
514
|
-
const stateKey = `cursor:${key}`;
|
|
515
|
-
this._state[stateKey] = value;
|
|
516
|
-
this._persistState([stateKey]);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
getState(key) {
|
|
520
|
-
const val = this._state[key];
|
|
521
|
-
return val !== undefined ? val : null;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
setState(key, value) {
|
|
525
|
-
this._state[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
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
|
-
]);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// --- Compaction (reduces JSONL file size by rewriting only current state) ---
|
|
543
|
-
|
|
544
|
-
compact() {
|
|
545
|
-
// Round-7 (§20.5): same per-PID tmp rationale as _persistState.
|
|
546
|
-
// Two concurrent compact() calls (daemon + ad-hoc CLI) racing on
|
|
547
|
-
// the same `${messagesFile}.tmp` lose the loser's compacted log.
|
|
548
|
-
const tmpFile = `${this._messagesFile}.${process.pid}.tmp`;
|
|
549
|
-
const entries = [];
|
|
550
|
-
for (const [, msg] of this._messages) {
|
|
551
|
-
entries.push(msg);
|
|
552
|
-
}
|
|
553
|
-
entries.sort((a, b) => a.created_at - b.created_at);
|
|
554
|
-
|
|
555
|
-
const lines = entries.map((msg) => jsonlLineForWrite(msg));
|
|
556
|
-
|
|
557
|
-
const fd = fs.openSync(tmpFile, 'w', PRIVATE_FILE_MODE);
|
|
558
|
-
try {
|
|
559
|
-
for (const line of lines) {
|
|
560
|
-
fs.writeSync(fd, line);
|
|
561
|
-
}
|
|
562
|
-
} finally {
|
|
563
|
-
fs.closeSync(fd);
|
|
564
|
-
}
|
|
565
|
-
bestEffortChmod(tmpFile, PRIVATE_FILE_MODE);
|
|
566
|
-
// Windows: renameSync throws EPERM when the destination already exists.
|
|
567
|
-
// Remove it first so the swap succeeds on all platforms.
|
|
568
|
-
if (process.platform === 'win32') {
|
|
569
|
-
try { fs.unlinkSync(this._messagesFile); } catch (e) {
|
|
570
|
-
if (e.code !== 'ENOENT') throw e;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
fs.renameSync(tmpFile, this._messagesFile);
|
|
574
|
-
bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
|
|
575
|
-
this._rebuildIndex();
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
close() {
|
|
579
|
-
// no-op for JSONL (no file handles to close), but kept for API compatibility
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Migration registry: key = target schema version, value = function(store)
|
|
584
|
-
// Each migration mutates in-memory state or rewrites JSONL as needed.
|
|
585
|
-
// Add new entries when SCHEMA_VERSION is bumped.
|
|
586
|
-
const MIGRATIONS = {
|
|
587
|
-
// version 1 is the initial schema -- no migration needed from 0 (fresh install)
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
module.exports = { MailboxStore, generateUUIDv7, DEFAULT_CHANNEL, SCHEMA_VERSION, PROXY_PROTOCOL_VERSION };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Rewrites the `model` field on an Anthropic /v1/messages request body
|
|
4
|
-
// without touching the `messages` array. The Anthropic prompt cache keys
|
|
5
|
-
// off the (model + messages + system + tools) tuple including any
|
|
6
|
-
// per-block `cache_control: { type: "ephemeral" }` markers. Since we
|
|
7
|
-
// only swap the top-level model string, all cache_control breakpoints
|
|
8
|
-
// the client placed remain intact and the cache prefix the client
|
|
9
|
-
// established with prior turns continues to hit.
|
|
10
|
-
//
|
|
11
|
-
// This is a pure function: it returns a shallow clone, never mutates
|
|
12
|
-
// the input. Callers pass a fresh body each request so deep cloning
|
|
13
|
-
// would be wasted work.
|
|
14
|
-
//
|
|
15
|
-
// Why a fresh module under src/proxy/router/: this is the second
|
|
16
|
-
// utility for Phase C alongside model_router.js. Bundling them into a
|
|
17
|
-
// shared dir keeps slice 4+ from sprawling helpers across src/proxy/.
|
|
18
|
-
|
|
19
|
-
function rewriteModel(body, newModel) {
|
|
20
|
-
if (!body || typeof body !== 'object') return body;
|
|
21
|
-
if (!newModel || typeof newModel !== 'string') return body;
|
|
22
|
-
if (body.model === newModel) return body;
|
|
23
|
-
return { ...body, model: newModel };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
module.exports = { rewriteModel };
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Stateless feature extraction for the Anthropic /v1/messages request body.
|
|
4
|
-
// Mirrors the field shapes consumed by model_router.js so pickForTurn can
|
|
5
|
-
// classify without per-session state. Phase C's "empty history" mode means
|
|
6
|
-
// last_assistant_output_tokens / last_assistant_stop_reason are always 0 /
|
|
7
|
-
// null; the JS classifier never reads stop_reason anyway (verified in the
|
|
8
|
-
// fixture round-trip; documented in model_router.js).
|
|
9
|
-
|
|
10
|
-
const PLAN_RE = /\b(plan|design|architect|brainstorm|outline|think through|let'?s think)\b/i;
|
|
11
|
-
const SIMPLE_LOOKUP_MAX_CHARS = 80;
|
|
12
|
-
|
|
13
|
-
function lastMessageOfRole(messages, role) {
|
|
14
|
-
if (!Array.isArray(messages)) return null;
|
|
15
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16
|
-
const m = messages[i];
|
|
17
|
-
if (m && m.role === role) return m;
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function blocksOf(msg) {
|
|
23
|
-
if (!msg) return [];
|
|
24
|
-
const c = msg.content;
|
|
25
|
-
if (typeof c === 'string') return [{ type: 'text', text: c }];
|
|
26
|
-
return Array.isArray(c) ? c : [];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function tailUserText(messages) {
|
|
30
|
-
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
31
|
-
const tail = messages[messages.length - 1];
|
|
32
|
-
if (!tail || tail.role !== 'user') return '';
|
|
33
|
-
return blocksOf(tail)
|
|
34
|
-
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
35
|
-
.map((b) => b.text)
|
|
36
|
-
.join('\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function extractFeatures(body) {
|
|
40
|
-
const messages = Array.isArray(body && body.messages) ? body.messages : [];
|
|
41
|
-
|
|
42
|
-
const lastAssistant = lastMessageOfRole(messages, 'assistant');
|
|
43
|
-
const lastAssistantBlocks = blocksOf(lastAssistant);
|
|
44
|
-
const toolCallCount = lastAssistantBlocks.filter((b) => b && b.type === 'tool_use').length;
|
|
45
|
-
|
|
46
|
-
const tail = messages[messages.length - 1];
|
|
47
|
-
const tailIsUser = !!(tail && tail.role === 'user');
|
|
48
|
-
const tailBlocks = blocksOf(tail);
|
|
49
|
-
const lastUserIsToolResultOnly = tailIsUser
|
|
50
|
-
&& tailBlocks.length > 0
|
|
51
|
-
&& tailBlocks.every((b) => b && b.type === 'tool_result');
|
|
52
|
-
|
|
53
|
-
const userText = tailUserText(messages);
|
|
54
|
-
const userRequestedPlanning = userText.length > 0 && PLAN_RE.test(userText);
|
|
55
|
-
const userSimpleLookup = userText.length > 0
|
|
56
|
-
&& !lastUserIsToolResultOnly
|
|
57
|
-
&& !userRequestedPlanning
|
|
58
|
-
&& userText.trim().length <= SIMPLE_LOOKUP_MAX_CHARS;
|
|
59
|
-
|
|
60
|
-
// last_assistant_stop_reason is gated by the classifier's
|
|
61
|
-
// post_tool_result_synthesis branch (model_router.js parity invariant).
|
|
62
|
-
// We don't have the real Anthropic response on the request side, but the
|
|
63
|
-
// shape of the last assistant message lets us reconstruct it deterministically:
|
|
64
|
-
// an assistant message with any tool_use block must have ended with
|
|
65
|
-
// stop_reason: 'tool_use' (otherwise the API would not have returned that
|
|
66
|
-
// block); a text-only assistant message ended with 'end_turn' ('Stop' in the
|
|
67
|
-
// Rust enum). Only 'ToolUse' is read by the classifier.
|
|
68
|
-
let stopReason = null;
|
|
69
|
-
if (lastAssistant) {
|
|
70
|
-
stopReason = toolCallCount > 0 ? 'ToolUse' : 'Stop';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
last_assistant_tool_call_count: toolCallCount,
|
|
75
|
-
last_assistant_had_tool_call: toolCallCount > 0,
|
|
76
|
-
last_user_is_tool_result_only: lastUserIsToolResultOnly,
|
|
77
|
-
user_requested_planning: userRequestedPlanning,
|
|
78
|
-
user_simple_lookup: userSimpleLookup,
|
|
79
|
-
last_assistant_output_tokens: 0,
|
|
80
|
-
last_assistant_stop_reason: stopReason,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
module.exports = { extractFeatures, PLAN_RE, SIMPLE_LOOKUP_MAX_CHARS };
|