@iderouter/index-mcp 0.2.0-beta.1
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 +93 -0
- package/package.json +26 -0
- package/scripts/benchmark-all.mjs +177 -0
- package/scripts/benchmark-auto-continuation.mjs +188 -0
- package/scripts/benchmark-background-fine-resume.mjs +245 -0
- package/scripts/benchmark-background-fine-wait.mjs +76 -0
- package/scripts/benchmark-background-fine.mjs +132 -0
- package/scripts/benchmark-clean-snapshot.mjs +83 -0
- package/scripts/benchmark-coarse-ready-search.mjs +161 -0
- package/scripts/benchmark-deferred.mjs +62 -0
- package/scripts/benchmark-first-semantic-visible.mjs +151 -0
- package/scripts/benchmark-gate.mjs +107 -0
- package/scripts/benchmark-generic-resumed-single-chunk-embed.mjs +104 -0
- package/scripts/benchmark-noop.mjs +24 -0
- package/scripts/benchmark-priority-ready-search.mjs +165 -0
- package/scripts/benchmark-repeat-search.mjs +148 -0
- package/scripts/benchmark-resumed-retry-burst.mjs +187 -0
- package/scripts/benchmark-resumed-single-chunk-success.mjs +154 -0
- package/scripts/benchmark-resumed-single-chunk.mjs +146 -0
- package/scripts/benchmark-single-priority-chunk-embed.mjs +145 -0
- package/scripts/benchmark-small-change.mjs +146 -0
- package/scripts/benchmark-stage-summary.mjs +88 -0
- package/scripts/lib/auto-continuation-state.mjs +34 -0
- package/scripts/lib/benchmark-query-packs.mjs +123 -0
- package/scripts/lib/benchmark-snapshot.mjs +109 -0
- package/scripts/lib/mcp-bench.mjs +455 -0
- package/src/architecture-query-fallback.js +50 -0
- package/src/background-definition-chunks.js +199 -0
- package/src/background-embedding-profile.js +64 -0
- package/src/background-fine-budget.js +18 -0
- package/src/background-fine-runtime.js +179 -0
- package/src/background-fine-selection.js +332 -0
- package/src/checkpoint-policy.js +16 -0
- package/src/conflict-policy.js +17 -0
- package/src/deferred-retry-delay.js +14 -0
- package/src/deferred-retry-status.js +10 -0
- package/src/embedding-attempt-ordinal.js +17 -0
- package/src/embedding-failure-penalty.js +60 -0
- package/src/embedding-failure-policy.js +52 -0
- package/src/embedding-flush-timeout.js +33 -0
- package/src/embedding-inflight-status.js +18 -0
- package/src/embedding-model-policy.js +44 -0
- package/src/embedding-next-switch.js +18 -0
- package/src/embedding-request-status-detail.js +25 -0
- package/src/embedding-request-status.js +22 -0
- package/src/embedding-selection-order.js +23 -0
- package/src/fine-run-queue.js +14 -0
- package/src/index.js +7970 -0
- package/src/job-supersession.js +25 -0
- package/src/priority-progress.js +20 -0
- package/src/priority-ready-anchor-coverage-normalize.js +18 -0
- package/src/priority-ready-anchor-coverage.js +23 -0
- package/src/priority-ready-hotspots.js +344 -0
- package/src/priority-ready-status.js +30 -0
- package/src/priority-ready-targets.js +45 -0
- package/src/priority-usable-attempt-plan.js +44 -0
- package/src/priority-usable-attempt-timeout.js +18 -0
- package/src/priority-usable-fast-path.js +11 -0
- package/src/priority-usable-probe-order.js +34 -0
- package/src/remote-strategy-failure-cache.js +55 -0
- package/src/resume-seed.js +9 -0
- package/src/semantic-first-checkpoint.js +8 -0
- package/src/semantic-slow-path.js +10 -0
- package/src/single-chunk-attempt-timeout.js +13 -0
- package/src/single-chunk-embedding-content.js +26 -0
- package/src/single-chunk-embedding-policy.js +18 -0
- package/src/single-chunk-provider-order.js +12 -0
- package/src/single-chunk-provider-policy.js +63 -0
- package/src/worker-lock-retry.js +24 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import fsSync from "node:fs";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const MCP_PATH = path.resolve(__dirname, "..", "..", "src", "index.js");
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const INDEXABLE_EXTENSIONS = new Set([
|
|
14
|
+
".c", ".cc", ".cpp", ".cs", ".css", ".go", ".graphql", ".h", ".hpp", ".html",
|
|
15
|
+
".java", ".js", ".json", ".jsx", ".kt", ".kts", ".md", ".mjs", ".php", ".proto",
|
|
16
|
+
".py", ".rb", ".rs", ".scss", ".sh", ".sql", ".swift", ".toml", ".ts", ".tsx",
|
|
17
|
+
".vue", ".xml", ".yaml", ".yml",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function benchRepoId(targetPath) {
|
|
21
|
+
return crypto.createHash("sha256").update(targetPath).digest("hex").slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function benchIndexMetaPath(indexHome, targetPath) {
|
|
25
|
+
return path.join(indexHome, "indexes", `${benchRepoId(targetPath)}.meta.json`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function benchStatusPath(indexHome, targetPath) {
|
|
29
|
+
return path.join(indexHome, "status", `${benchRepoId(targetPath)}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readIndexMeta(indexHome, targetPath) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(benchIndexMetaPath(indexHome, targetPath), "utf8");
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readJobStatus(indexHome, targetPath) {
|
|
42
|
+
try {
|
|
43
|
+
const content = await fs.readFile(benchStatusPath(indexHome, targetPath), "utf8");
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseDiagnostics(text) {
|
|
51
|
+
const match = text.match(/Diagnostics:\s(.+?)(?:\.\s(?:Cloud storage:|Cloud sync:|Async indexing guide:)|$)/s);
|
|
52
|
+
if (!match) return {};
|
|
53
|
+
const fields = {};
|
|
54
|
+
for (const part of match[1].split(",")) {
|
|
55
|
+
const [rawKey, ...rest] = part.trim().split("=");
|
|
56
|
+
if (!rawKey || rest.length === 0) continue;
|
|
57
|
+
fields[rawKey.trim()] = rest.join("=").trim();
|
|
58
|
+
}
|
|
59
|
+
return fields;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeBenchmarkResult(targetPath, finalText, wallMs) {
|
|
63
|
+
const diagnostics = parseDiagnostics(finalText);
|
|
64
|
+
return {
|
|
65
|
+
path: targetPath,
|
|
66
|
+
wall_ms: wallMs,
|
|
67
|
+
scan_ms: Number(diagnostics.scan_ms || 0),
|
|
68
|
+
coarse_ready_ms: Number(diagnostics.coarse_ready_ms || 0),
|
|
69
|
+
priority_ready_ms: Number(diagnostics.priority_ready_ms || 0),
|
|
70
|
+
background_ms: Number(diagnostics.background_ms || 0),
|
|
71
|
+
total_ms: Number(diagnostics.total_ms || 0),
|
|
72
|
+
strategy_version: diagnostics.strategy_version || "",
|
|
73
|
+
strategy_source: diagnostics.strategy_source || "",
|
|
74
|
+
embedding_model_source: diagnostics.embedding_model_source || "",
|
|
75
|
+
background_fine_target_files: Number(diagnostics.background_fine_target_files || 0),
|
|
76
|
+
background_fine_target_chunks: Number(diagnostics.background_fine_target_chunks || 0),
|
|
77
|
+
final_status: finalText.split("\n")[0],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sleep(ms) {
|
|
82
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escapeRegex(value) {
|
|
86
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function decodeTomlBasicString(value) {
|
|
90
|
+
return String(value || "")
|
|
91
|
+
.replace(/\\\\/g, "\\")
|
|
92
|
+
.replace(/\\"/g, "\"")
|
|
93
|
+
.replace(/\\n/g, "\n")
|
|
94
|
+
.replace(/\\r/g, "\r")
|
|
95
|
+
.replace(/\\t/g, "\t");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractCodexMcpEnvValue(configText, serverName, envKey) {
|
|
99
|
+
const sectionRegex = new RegExp(
|
|
100
|
+
`^\\[mcp_servers\\.${escapeRegex(serverName)}\\.env\\]\\s*$([\\s\\S]*?)(?=^\\[|\\Z)`,
|
|
101
|
+
"m",
|
|
102
|
+
);
|
|
103
|
+
const sectionMatch = String(configText || "").match(sectionRegex);
|
|
104
|
+
if (!sectionMatch) return "";
|
|
105
|
+
const keyRegex = new RegExp(`^\\s*${escapeRegex(envKey)}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m");
|
|
106
|
+
const keyMatch = sectionMatch[1].match(keyRegex);
|
|
107
|
+
return keyMatch ? decodeTomlBasicString(keyMatch[1]) : "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readCodexMcpEnvFallback(serverName, configPath) {
|
|
111
|
+
try {
|
|
112
|
+
const content = fsSync.readFileSync(configPath, "utf8");
|
|
113
|
+
const result = {};
|
|
114
|
+
for (const envKey of [
|
|
115
|
+
"IDEROUTER_API_KEY",
|
|
116
|
+
"IDEROUTER_BASE_URL",
|
|
117
|
+
"IDEROUTER_MCP_CONFIG_API_BASE_URL",
|
|
118
|
+
"IDEROUTER_CLOUD_API_BASE_URL",
|
|
119
|
+
]) {
|
|
120
|
+
const value = extractCodexMcpEnvValue(content, serverName, envKey);
|
|
121
|
+
if (value) {
|
|
122
|
+
result[envKey] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
} catch {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveBenchmarkEnv(baseEnv = process.env, options = {}) {
|
|
132
|
+
const nextEnv = { ...baseEnv };
|
|
133
|
+
if (nextEnv.IDEROUTER_API_KEY) return nextEnv;
|
|
134
|
+
|
|
135
|
+
const serverName = String(options.serverName || nextEnv.IDEROUTER_BENCH_MCP_SERVER_NAME || "iderouter-index").trim();
|
|
136
|
+
const configPath = String(
|
|
137
|
+
options.configPath ||
|
|
138
|
+
nextEnv.IDEROUTER_BENCH_CODEX_CONFIG_PATH ||
|
|
139
|
+
path.join(os.homedir(), ".codex", "config.toml"),
|
|
140
|
+
);
|
|
141
|
+
const fallbackEnv = readCodexMcpEnvFallback(serverName, configPath);
|
|
142
|
+
for (const [key, value] of Object.entries(fallbackEnv)) {
|
|
143
|
+
if (!nextEnv[key] && value) {
|
|
144
|
+
nextEnv[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return nextEnv;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isWorkerProcessAlive(pid) {
|
|
151
|
+
const numericPid = Number(pid || 0);
|
|
152
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
|
|
153
|
+
try {
|
|
154
|
+
process.kill(numericPid, 0);
|
|
155
|
+
return true;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function terminateDetachedIndexWorker(indexHome, targetPath, options = {}) {
|
|
162
|
+
const graceMs = Math.max(100, Number(options.graceMs) || 1000);
|
|
163
|
+
const job = await readJobStatus(indexHome, targetPath);
|
|
164
|
+
const workerPid = Number(job?.workerPid || 0);
|
|
165
|
+
if (!workerPid || !isWorkerProcessAlive(workerPid)) return false;
|
|
166
|
+
try {
|
|
167
|
+
process.kill(workerPid, "SIGTERM");
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const deadline = Date.now() + graceMs;
|
|
172
|
+
while (Date.now() < deadline) {
|
|
173
|
+
if (!isWorkerProcessAlive(workerPid)) return true;
|
|
174
|
+
await sleep(100);
|
|
175
|
+
}
|
|
176
|
+
if (!isWorkerProcessAlive(workerPid)) return true;
|
|
177
|
+
try {
|
|
178
|
+
process.kill(workerPid, "SIGKILL");
|
|
179
|
+
} catch {
|
|
180
|
+
return !isWorkerProcessAlive(workerPid);
|
|
181
|
+
}
|
|
182
|
+
return !isWorkerProcessAlive(workerPid);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function terminateWorkersByTargetPath(targetPath) {
|
|
186
|
+
if (!targetPath) return;
|
|
187
|
+
let stdout = "";
|
|
188
|
+
try {
|
|
189
|
+
({ stdout } = await execFileAsync("pgrep", ["-f", targetPath], {
|
|
190
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
191
|
+
}));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.code !== 1) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const pids = String(stdout || "")
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((line) => Number(line.trim()))
|
|
200
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
|
|
201
|
+
for (const pid of pids) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, "SIGTERM");
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await sleep(250);
|
|
209
|
+
for (const pid of pids) {
|
|
210
|
+
if (!isWorkerProcessAlive(pid)) continue;
|
|
211
|
+
try {
|
|
212
|
+
process.kill(pid, "SIGKILL");
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function cleanupBenchmarkRun({
|
|
220
|
+
child,
|
|
221
|
+
indexHome,
|
|
222
|
+
targetPath,
|
|
223
|
+
snapshotDir,
|
|
224
|
+
}) {
|
|
225
|
+
try {
|
|
226
|
+
child?.kill?.("SIGTERM");
|
|
227
|
+
} catch {
|
|
228
|
+
// ignore
|
|
229
|
+
}
|
|
230
|
+
if (indexHome && targetPath) {
|
|
231
|
+
await terminateDetachedIndexWorker(indexHome, targetPath);
|
|
232
|
+
}
|
|
233
|
+
if (targetPath) {
|
|
234
|
+
await terminateWorkersByTargetPath(targetPath);
|
|
235
|
+
}
|
|
236
|
+
if (snapshotDir) {
|
|
237
|
+
await fs.rm(snapshotDir, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
if (indexHome) {
|
|
240
|
+
await fs.rm(indexHome, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function installBenchmarkSignalCleanup(factory) {
|
|
245
|
+
let cleaned = false;
|
|
246
|
+
async function runCleanupAndExit(signal) {
|
|
247
|
+
if (cleaned) return;
|
|
248
|
+
cleaned = true;
|
|
249
|
+
try {
|
|
250
|
+
const context = typeof factory === "function" ? await factory() : null;
|
|
251
|
+
if (context) {
|
|
252
|
+
await cleanupBenchmarkRun(context);
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
process.exit(128 + (signal === "SIGINT" ? 2 : 15));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const onSigint = () => {
|
|
259
|
+
void runCleanupAndExit("SIGINT");
|
|
260
|
+
};
|
|
261
|
+
const onSigterm = () => {
|
|
262
|
+
void runCleanupAndExit("SIGTERM");
|
|
263
|
+
};
|
|
264
|
+
process.once("SIGINT", onSigint);
|
|
265
|
+
process.once("SIGTERM", onSigterm);
|
|
266
|
+
return () => {
|
|
267
|
+
process.off("SIGINT", onSigint);
|
|
268
|
+
process.off("SIGTERM", onSigterm);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class McpClient {
|
|
273
|
+
constructor(child) {
|
|
274
|
+
this.child = child;
|
|
275
|
+
this.nextId = 1;
|
|
276
|
+
this.pending = new Map();
|
|
277
|
+
let buffer = "";
|
|
278
|
+
child.stdout.setEncoding("utf8");
|
|
279
|
+
child.stdout.on("data", (chunk) => {
|
|
280
|
+
buffer += chunk;
|
|
281
|
+
let newline = buffer.indexOf("\n");
|
|
282
|
+
while (newline >= 0) {
|
|
283
|
+
const line = buffer.slice(0, newline).trim();
|
|
284
|
+
buffer = buffer.slice(newline + 1);
|
|
285
|
+
if (line) {
|
|
286
|
+
const message = JSON.parse(line);
|
|
287
|
+
const pending = this.pending.get(message.id);
|
|
288
|
+
if (pending) {
|
|
289
|
+
this.pending.delete(message.id);
|
|
290
|
+
if (message.error) pending.reject(new Error(message.error.message || "MCP error"));
|
|
291
|
+
else pending.resolve(message.result);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
newline = buffer.indexOf("\n");
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
request(method, params) {
|
|
300
|
+
const id = this.nextId++;
|
|
301
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
this.pending.set(id, { resolve, reject });
|
|
304
|
+
this.child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async initialize() {
|
|
309
|
+
await this.request("initialize", {
|
|
310
|
+
protocolVersion: "2024-11-05",
|
|
311
|
+
capabilities: {},
|
|
312
|
+
clientInfo: { name: "iderouter-bench", version: "1.0.0" },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async callTool(name, args) {
|
|
317
|
+
const result = await this.request("tools/call", { name, arguments: args });
|
|
318
|
+
return result?.content?.[0]?.text || "";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function spawnMcpClient(env = process.env) {
|
|
323
|
+
const resolvedEnv = resolveBenchmarkEnv(env);
|
|
324
|
+
const child = spawn(process.execPath, [MCP_PATH], {
|
|
325
|
+
cwd: path.dirname(MCP_PATH),
|
|
326
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
327
|
+
env: resolvedEnv,
|
|
328
|
+
});
|
|
329
|
+
return { child, client: new McpClient(child) };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function isSettledIndexStatus(text) {
|
|
333
|
+
return (
|
|
334
|
+
/^Indexed\b/.test(text) ||
|
|
335
|
+
/^Priority semantic search is ready\b/.test(text) ||
|
|
336
|
+
/^Background fine rebuild is temporarily paused\b/.test(text) ||
|
|
337
|
+
/^Last index job .* failed\b/.test(text)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function isUsableIndexStatus(text) {
|
|
342
|
+
return (
|
|
343
|
+
/^Indexed\b/.test(text) ||
|
|
344
|
+
/^Priority semantic search is ready\b/.test(text) ||
|
|
345
|
+
/^Background fine rebuild is temporarily paused\b/.test(text)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function gitWorkspaceStats(targetPath) {
|
|
350
|
+
try {
|
|
351
|
+
const { stdout } = await execFileAsync(
|
|
352
|
+
"git",
|
|
353
|
+
["-C", targetPath, "status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
354
|
+
{ maxBuffer: 16 * 1024 * 1024 },
|
|
355
|
+
);
|
|
356
|
+
const entries = String(stdout || "").split("\0").filter(Boolean);
|
|
357
|
+
const dirtyPaths = entries.map((entry) => entry.slice(3).trim()).filter(Boolean);
|
|
358
|
+
const indexableDirtyPaths = dirtyPaths.filter((relativePath) => INDEXABLE_EXTENSIONS.has(path.extname(relativePath)));
|
|
359
|
+
return {
|
|
360
|
+
workspace_dirty: dirtyPaths.length > 0,
|
|
361
|
+
dirty_entries: dirtyPaths.length,
|
|
362
|
+
indexable_dirty_entries: indexableDirtyPaths.length,
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
workspace_dirty: null,
|
|
367
|
+
dirty_entries: 0,
|
|
368
|
+
indexable_dirty_entries: 0,
|
|
369
|
+
workspace_dirty_error: error instanceof Error ? error.message : String(error),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function runIndexBenchmark({
|
|
375
|
+
targetPath,
|
|
376
|
+
timeoutMs,
|
|
377
|
+
pollMs,
|
|
378
|
+
startArgs,
|
|
379
|
+
env = process.env,
|
|
380
|
+
isSettled = isSettledIndexStatus,
|
|
381
|
+
}) {
|
|
382
|
+
const resolvedEnv = resolveBenchmarkEnv(env);
|
|
383
|
+
const { child, client } = spawnMcpClient(resolvedEnv);
|
|
384
|
+
try {
|
|
385
|
+
await client.initialize();
|
|
386
|
+
const startedAt = Date.now();
|
|
387
|
+
const startText = await client.callTool("index_codebase", startArgs);
|
|
388
|
+
let finalText = startText;
|
|
389
|
+
const deadline = Date.now() + timeoutMs;
|
|
390
|
+
|
|
391
|
+
while (Date.now() < deadline) {
|
|
392
|
+
await sleep(pollMs);
|
|
393
|
+
const text = await client.callTool("get_indexing_status", { path: targetPath });
|
|
394
|
+
finalText = text;
|
|
395
|
+
if (isSettled(text)) break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const wallMs = Date.now() - startedAt;
|
|
399
|
+
return {
|
|
400
|
+
startText,
|
|
401
|
+
finalText,
|
|
402
|
+
wallMs,
|
|
403
|
+
parsed: {
|
|
404
|
+
...normalizeBenchmarkResult(targetPath, finalText, wallMs),
|
|
405
|
+
start_status: startText.split("\n")[0],
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
} finally {
|
|
409
|
+
await cleanupBenchmarkRun({
|
|
410
|
+
child,
|
|
411
|
+
indexHome: resolvedEnv.IDEROUTER_INDEX_HOME,
|
|
412
|
+
targetPath,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function waitForIndexStatus({
|
|
418
|
+
targetPath,
|
|
419
|
+
timeoutMs,
|
|
420
|
+
pollMs,
|
|
421
|
+
env = process.env,
|
|
422
|
+
isSettled = isUsableIndexStatus,
|
|
423
|
+
}) {
|
|
424
|
+
const resolvedEnv = resolveBenchmarkEnv(env);
|
|
425
|
+
const { child, client } = spawnMcpClient(resolvedEnv);
|
|
426
|
+
try {
|
|
427
|
+
await client.initialize();
|
|
428
|
+
let finalText = "";
|
|
429
|
+
const startedAt = Date.now();
|
|
430
|
+
const deadline = Date.now() + timeoutMs;
|
|
431
|
+
let settled = false;
|
|
432
|
+
while (Date.now() < deadline) {
|
|
433
|
+
const text = await client.callTool("get_indexing_status", { path: targetPath });
|
|
434
|
+
finalText = text;
|
|
435
|
+
if (isSettled(text)) {
|
|
436
|
+
settled = true;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
await sleep(pollMs);
|
|
440
|
+
}
|
|
441
|
+
const wallMs = Date.now() - startedAt;
|
|
442
|
+
return {
|
|
443
|
+
finalText,
|
|
444
|
+
wallMs,
|
|
445
|
+
settled,
|
|
446
|
+
parsed: normalizeBenchmarkResult(targetPath, finalText, wallMs),
|
|
447
|
+
};
|
|
448
|
+
} finally {
|
|
449
|
+
await cleanupBenchmarkRun({
|
|
450
|
+
child,
|
|
451
|
+
indexHome: resolvedEnv.IDEROUTER_INDEX_HOME,
|
|
452
|
+
targetPath,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function queryMentionsAny(query, words) {
|
|
2
|
+
const normalized = String(query || "").toLowerCase();
|
|
3
|
+
return words.some((word) => normalized.includes(word));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isArchitectureQuery(query) {
|
|
7
|
+
return queryMentionsAny(query, [
|
|
8
|
+
"architecture",
|
|
9
|
+
"architectural",
|
|
10
|
+
"overview",
|
|
11
|
+
"design",
|
|
12
|
+
"explain",
|
|
13
|
+
"how does",
|
|
14
|
+
"flow",
|
|
15
|
+
"end to end",
|
|
16
|
+
"e2e",
|
|
17
|
+
"架构",
|
|
18
|
+
"说明",
|
|
19
|
+
"总览",
|
|
20
|
+
"整体",
|
|
21
|
+
"链路",
|
|
22
|
+
"流程",
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRelayArchitectureQuery(query) {
|
|
27
|
+
return isArchitectureQuery(query) && queryMentionsAny(query, [
|
|
28
|
+
"relay",
|
|
29
|
+
"request",
|
|
30
|
+
"controller",
|
|
31
|
+
"service",
|
|
32
|
+
"model",
|
|
33
|
+
"router",
|
|
34
|
+
"handler",
|
|
35
|
+
"后端",
|
|
36
|
+
"请求",
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function architectureFallbackPathsForQuery(query) {
|
|
41
|
+
if (!isRelayArchitectureQuery(query)) return [];
|
|
42
|
+
return [
|
|
43
|
+
"controller/relay.go",
|
|
44
|
+
"relay/compatible_handler.go",
|
|
45
|
+
"relay/common/relay_info.go",
|
|
46
|
+
"router/relay-router.go",
|
|
47
|
+
"service/channel_select.go",
|
|
48
|
+
"model/channel.go",
|
|
49
|
+
];
|
|
50
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function backgroundChunkPolicyForPath(relativePathInput, defaults = {}) {
|
|
4
|
+
const relativePath = String(relativePathInput || "").toLowerCase().replaceAll("\\", "/");
|
|
5
|
+
const chunkCap = Math.max(1, Number(defaults.chunkCap) || 6);
|
|
6
|
+
const minimumDefaultChunkCount = Math.max(1, Number(defaults.minimumDefaultChunkCount) || 12);
|
|
7
|
+
if (relativePath.startsWith("relay/channel/") && relativePath.endsWith("/adaptor.go")) {
|
|
8
|
+
return {
|
|
9
|
+
chunkCap: Math.min(chunkCap, 4),
|
|
10
|
+
minimumDefaultChunkCount: Math.min(minimumDefaultChunkCount, 8),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
relativePath === "relay/helper/price.go" ||
|
|
15
|
+
relativePath === "service/tiered_settle.go" ||
|
|
16
|
+
relativePath === "service/user_memory.go"
|
|
17
|
+
) {
|
|
18
|
+
return {
|
|
19
|
+
chunkCap: Math.min(chunkCap, 4),
|
|
20
|
+
minimumDefaultChunkCount: Math.min(minimumDefaultChunkCount, 8),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (relativePath.startsWith("controller/") && relativePath.endsWith(".go")) {
|
|
24
|
+
return {
|
|
25
|
+
chunkCap: Math.min(chunkCap, 4),
|
|
26
|
+
minimumDefaultChunkCount: Math.min(minimumDefaultChunkCount, 8),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
chunkCap,
|
|
31
|
+
minimumDefaultChunkCount,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildLineOffsets(content) {
|
|
36
|
+
const offsets = [0];
|
|
37
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
38
|
+
if (content[index] === "\n") offsets.push(index + 1);
|
|
39
|
+
}
|
|
40
|
+
return offsets;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lineForOffsetFromIndex(lineOffsets, offset) {
|
|
44
|
+
let low = 0;
|
|
45
|
+
let high = lineOffsets.length - 1;
|
|
46
|
+
while (low <= high) {
|
|
47
|
+
const mid = Math.floor((low + high) / 2);
|
|
48
|
+
if (lineOffsets[mid] <= offset) low = mid + 1;
|
|
49
|
+
else high = mid - 1;
|
|
50
|
+
}
|
|
51
|
+
return Math.max(1, high + 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function splitFile(content, file, options = {}) {
|
|
55
|
+
const chunkSize = Math.max(1, Number(options.chunkSize) || 600);
|
|
56
|
+
const overlap = Math.max(0, Math.min(chunkSize - 1, Number(options.overlap) || 80));
|
|
57
|
+
const chunks = [];
|
|
58
|
+
const lineOffsets = buildLineOffsets(content);
|
|
59
|
+
let offset = 0;
|
|
60
|
+
while (offset < content.length) {
|
|
61
|
+
const end = Math.min(content.length, offset + chunkSize);
|
|
62
|
+
const chunkContent = content.slice(offset, end).trim();
|
|
63
|
+
if (chunkContent) {
|
|
64
|
+
chunks.push({
|
|
65
|
+
id: crypto
|
|
66
|
+
.createHash("sha256")
|
|
67
|
+
.update(`${file.relativePath}:${offset}:${chunkContent}`)
|
|
68
|
+
.digest("hex")
|
|
69
|
+
.slice(0, 24),
|
|
70
|
+
contentHash: crypto.createHash("sha256").update(chunkContent).digest("hex"),
|
|
71
|
+
relativePath: file.relativePath,
|
|
72
|
+
extension: file.extension,
|
|
73
|
+
startLine: lineForOffsetFromIndex(lineOffsets, offset),
|
|
74
|
+
endLine: lineForOffsetFromIndex(lineOffsets, end),
|
|
75
|
+
content: chunkContent,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (end === content.length) break;
|
|
79
|
+
offset = Math.max(end - overlap, offset + 1);
|
|
80
|
+
}
|
|
81
|
+
return chunks;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dedupeDefinitionOffsets(offsets, minGap) {
|
|
85
|
+
const deduped = [];
|
|
86
|
+
for (const offset of offsets) {
|
|
87
|
+
if (!deduped.length || offset - deduped[deduped.length - 1] >= minGap) {
|
|
88
|
+
deduped.push(offset);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return deduped;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defaultChunkCount(fileSize, chunkSize, overlap) {
|
|
95
|
+
return Math.max(1, Math.ceil(Number(fileSize || 0) / Math.max(1, Number(chunkSize || 0) - Number(overlap || 0))));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function estimateBackgroundDefinitionChunkCount({
|
|
99
|
+
fileSize,
|
|
100
|
+
defaultChunkSize = 600,
|
|
101
|
+
defaultChunkOverlap = 80,
|
|
102
|
+
chunkSize = 900,
|
|
103
|
+
overlap = 120,
|
|
104
|
+
chunkCap = 6,
|
|
105
|
+
minimumDefaultChunkCount = 16,
|
|
106
|
+
}) {
|
|
107
|
+
const defaultEstimate = defaultChunkCount(fileSize, defaultChunkSize, defaultChunkOverlap);
|
|
108
|
+
if (defaultEstimate <= Math.max(1, Number(minimumDefaultChunkCount) || 1)) {
|
|
109
|
+
return defaultEstimate;
|
|
110
|
+
}
|
|
111
|
+
const priorityEstimate = defaultChunkCount(fileSize, chunkSize, overlap);
|
|
112
|
+
return Math.max(1, Math.min(defaultEstimate, priorityEstimate, Math.max(1, Number(chunkCap) || 1)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function splitBackgroundDefinitionAnchors(content, file, options = {}) {
|
|
116
|
+
const chunkSize = Math.max(1, Number(options.chunkSize) || 900);
|
|
117
|
+
const overlap = Math.max(0, Math.min(chunkSize - 1, Number(options.overlap) || 120));
|
|
118
|
+
const policy = backgroundChunkPolicyForPath(file?.relativePath, options);
|
|
119
|
+
const chunkCap = policy.chunkCap;
|
|
120
|
+
const minimumDefaultChunkCount = policy.minimumDefaultChunkCount;
|
|
121
|
+
const defaultChunkSize = Math.max(1, Number(options.defaultChunkSize) || 600);
|
|
122
|
+
const defaultChunkOverlap = Math.max(0, Math.min(defaultChunkSize - 1, Number(options.defaultChunkOverlap) || 80));
|
|
123
|
+
const defaultChunks = splitFile(content, file, { chunkSize, overlap });
|
|
124
|
+
const estimatedCount = estimateBackgroundDefinitionChunkCount({
|
|
125
|
+
fileSize: content.length,
|
|
126
|
+
defaultChunkSize,
|
|
127
|
+
defaultChunkOverlap,
|
|
128
|
+
chunkSize,
|
|
129
|
+
overlap,
|
|
130
|
+
chunkCap,
|
|
131
|
+
minimumDefaultChunkCount,
|
|
132
|
+
});
|
|
133
|
+
if (defaultChunks.length <= estimatedCount) return defaultChunks;
|
|
134
|
+
|
|
135
|
+
const definitionPattern = /^(?:func|type|const|var)\s+[A-Za-z0-9_]+/gm;
|
|
136
|
+
const offsets = [];
|
|
137
|
+
let match;
|
|
138
|
+
while ((match = definitionPattern.exec(content)) !== null) {
|
|
139
|
+
offsets.push(match.index);
|
|
140
|
+
}
|
|
141
|
+
const gap = Math.max(160, Math.floor(chunkSize * 0.6));
|
|
142
|
+
const anchors = dedupeDefinitionOffsets(offsets, gap);
|
|
143
|
+
if (anchors.length === 0) return defaultChunks.slice(0, estimatedCount);
|
|
144
|
+
|
|
145
|
+
const selectedOffsets = dedupeDefinitionOffsets(
|
|
146
|
+
[0, ...anchors, Math.max(0, content.length - chunkSize)].sort((left, right) => left - right),
|
|
147
|
+
gap,
|
|
148
|
+
);
|
|
149
|
+
const windowCount = Math.max(1, Math.min(estimatedCount, selectedOffsets.length));
|
|
150
|
+
if (selectedOffsets.length <= windowCount) {
|
|
151
|
+
return selectedOffsets.map((offset) => {
|
|
152
|
+
const end = Math.min(content.length, offset + chunkSize);
|
|
153
|
+
const chunkContent = content.slice(offset, end).trim();
|
|
154
|
+
const lineOffsets = buildLineOffsets(content);
|
|
155
|
+
return {
|
|
156
|
+
id: crypto
|
|
157
|
+
.createHash("sha256")
|
|
158
|
+
.update(`${file.relativePath}:${offset}:${chunkContent}`)
|
|
159
|
+
.digest("hex")
|
|
160
|
+
.slice(0, 24),
|
|
161
|
+
contentHash: crypto.createHash("sha256").update(chunkContent).digest("hex"),
|
|
162
|
+
relativePath: file.relativePath,
|
|
163
|
+
extension: file.extension,
|
|
164
|
+
startLine: lineForOffsetFromIndex(lineOffsets, offset),
|
|
165
|
+
endLine: lineForOffsetFromIndex(lineOffsets, end),
|
|
166
|
+
content: chunkContent,
|
|
167
|
+
};
|
|
168
|
+
}).filter((chunk) => chunk.content);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const picks = new Set();
|
|
172
|
+
for (let slot = 0; slot < windowCount; slot += 1) {
|
|
173
|
+
const ratio = windowCount <= 1 ? 0 : slot / (windowCount - 1);
|
|
174
|
+
const index = Math.min(selectedOffsets.length - 1, Math.round(ratio * (selectedOffsets.length - 1)));
|
|
175
|
+
picks.add(index);
|
|
176
|
+
}
|
|
177
|
+
const lineOffsets = buildLineOffsets(content);
|
|
178
|
+
return Array.from(picks)
|
|
179
|
+
.sort((left, right) => left - right)
|
|
180
|
+
.map((pickIndex) => selectedOffsets[pickIndex])
|
|
181
|
+
.map((offset) => {
|
|
182
|
+
const end = Math.min(content.length, offset + chunkSize);
|
|
183
|
+
const chunkContent = content.slice(offset, end).trim();
|
|
184
|
+
return {
|
|
185
|
+
id: crypto
|
|
186
|
+
.createHash("sha256")
|
|
187
|
+
.update(`${file.relativePath}:${offset}:${chunkContent}`)
|
|
188
|
+
.digest("hex")
|
|
189
|
+
.slice(0, 24),
|
|
190
|
+
contentHash: crypto.createHash("sha256").update(chunkContent).digest("hex"),
|
|
191
|
+
relativePath: file.relativePath,
|
|
192
|
+
extension: file.extension,
|
|
193
|
+
startLine: lineForOffsetFromIndex(lineOffsets, offset),
|
|
194
|
+
endLine: lineForOffsetFromIndex(lineOffsets, end),
|
|
195
|
+
content: chunkContent,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((chunk) => chunk.content);
|
|
199
|
+
}
|