@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
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# IDERouter Index MCP
|
|
2
|
+
|
|
3
|
+
Hybrid local code indexing MCP for Cursor, Codex, and Claude Code.
|
|
4
|
+
|
|
5
|
+
The MCP process runs locally so it can read your repository files. It uses your
|
|
6
|
+
IDERouter API key for embedding requests and stores the generated index under:
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
~/.iderouter/index-mcp/indexes
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Re-running `index_codebase` is incremental: unchanged files are reused from the
|
|
13
|
+
local manifest without being read again, and unchanged chunks reuse their
|
|
14
|
+
existing vectors. Only new or modified chunks call the embedding API.
|
|
15
|
+
|
|
16
|
+
The MCP uses a fixed two-model embedding policy. New fine indexes prefer
|
|
17
|
+
`qwen/qwen3-embedding-8b` and automatically fall back to
|
|
18
|
+
`openai/text-embedding-3-small` when the primary model is unavailable or slow.
|
|
19
|
+
Users only configure an IDERouter API key; the endpoint and model policy are
|
|
20
|
+
built in. Existing completed indexes keep using the model stored with that index
|
|
21
|
+
so vector spaces are not mixed.
|
|
22
|
+
|
|
23
|
+
`search_code` also checks the workspace manifest before searching. If files were
|
|
24
|
+
added, removed, or changed, it performs the same incremental refresh first so
|
|
25
|
+
agents do not search stale code.
|
|
26
|
+
|
|
27
|
+
Indexing starts as a background job by default, so AI agents can keep working
|
|
28
|
+
while the local index is being built. To use async indexing:
|
|
29
|
+
|
|
30
|
+
1. Call `index_codebase` with the target `path` and omit `wait` (or set
|
|
31
|
+
`wait=false`).
|
|
32
|
+
2. Continue with other work while the job runs in the background.
|
|
33
|
+
3. Call `get_indexing_status` with the same `path` whenever progress is needed.
|
|
34
|
+
|
|
35
|
+
The product is local-first: repository files stay on the machine running the
|
|
36
|
+
MCP. Remote usage is limited to embedding requests and aggregate billing
|
|
37
|
+
metadata.
|
|
38
|
+
|
|
39
|
+
MCP installation itself cannot reliably auto-install client-side skills or
|
|
40
|
+
rules across Cursor, Codex, and Claude Code. The supported path is to pair the
|
|
41
|
+
MCP config with a companion prompt or rules block in the client so the agent
|
|
42
|
+
prefers `iderouter-index` before broad file reads.
|
|
43
|
+
|
|
44
|
+
## Environment
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export IDEROUTER_API_KEY="ir-..."
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Optional advanced tuning:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Primary probe slower than this will compare the built-in fallback model.
|
|
54
|
+
export IDEROUTER_EMBEDDING_HEALTH_SLOW_MS=8000
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Optional Codex Skill Install
|
|
58
|
+
|
|
59
|
+
If you want Codex to prefer `iderouter-index` automatically, install the
|
|
60
|
+
companion skill explicitly:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @iderouter/index-mcp init codex
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This writes:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
~/.codex/skills/iderouter-index/SKILL.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The install step is explicit on purpose; normal MCP startup does not modify the
|
|
73
|
+
client skill catalog automatically.
|
|
74
|
+
|
|
75
|
+
## Tools
|
|
76
|
+
|
|
77
|
+
- `index_codebase`: index a local directory.
|
|
78
|
+
- `search_code`: semantic search against the local index.
|
|
79
|
+
- `ask_codebase`: answer a repository question from indexed code and cite matching snippets.
|
|
80
|
+
- `clear_index`: delete an index.
|
|
81
|
+
- `get_indexing_status`: show indexed file/chunk counts and reuse diagnostics.
|
|
82
|
+
|
|
83
|
+
## Codex Config
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[mcp_servers.iderouter-index]
|
|
87
|
+
type = "stdio"
|
|
88
|
+
command = "npx"
|
|
89
|
+
args = ["-y", "@iderouter/index-mcp"]
|
|
90
|
+
|
|
91
|
+
[mcp_servers.iderouter-index.env]
|
|
92
|
+
IDEROUTER_API_KEY = "ir-..."
|
|
93
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iderouter/index-mcp",
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"description": "Hybrid local code index MCP powered by IDERouter embeddings.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"iderouter-index-mcp": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"scripts",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"check": "node --check src/index.js",
|
|
19
|
+
"benchmark:noop": "node scripts/benchmark-noop.mjs",
|
|
20
|
+
"benchmark:small-change": "node scripts/benchmark-small-change.mjs",
|
|
21
|
+
"benchmark:deferred": "node scripts/benchmark-deferred.mjs",
|
|
22
|
+
"benchmark:all": "node scripts/benchmark-all.mjs",
|
|
23
|
+
"benchmark:clean-snapshot": "node scripts/benchmark-clean-snapshot.mjs",
|
|
24
|
+
"benchmark:stage-summary": "node scripts/benchmark-stage-summary.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { gitWorkspaceStats } from "./lib/mcp-bench.mjs";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const TARGET_PATH = path.resolve(process.argv[2] || process.cwd());
|
|
11
|
+
const SCRIPT_DIR = path.resolve("scripts");
|
|
12
|
+
|
|
13
|
+
async function runScript(scriptName) {
|
|
14
|
+
const scriptPath = path.join(SCRIPT_DIR, scriptName);
|
|
15
|
+
const { stdout } = await execFileAsync(process.execPath, [scriptPath, TARGET_PATH], {
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
env: process.env,
|
|
18
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
19
|
+
});
|
|
20
|
+
return JSON.parse(String(stdout || "").trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function summarize(result) {
|
|
24
|
+
if (result?.priority_ready && Array.isArray(result?.searches)) {
|
|
25
|
+
return {
|
|
26
|
+
priority_ready_ms: result.priority_ready?.parsed?.priority_ready_ms ?? result.priority_ready?.wallMs ?? 0,
|
|
27
|
+
priority_ready_anchor_completed: result.priority_ready_anchor_coverage?.completed ?? null,
|
|
28
|
+
priority_ready_anchor_total: result.priority_ready_anchor_coverage?.total ?? null,
|
|
29
|
+
query_count: result.searches.length,
|
|
30
|
+
all_top1_hit: result.searches.every((item) => item?.top1_hit === true),
|
|
31
|
+
max_search_ms: Math.max(...result.searches.map((item) => Number(item?.wall_ms || 0)), 0),
|
|
32
|
+
searches: result.searches.map((item) => ({
|
|
33
|
+
name: item.name,
|
|
34
|
+
wall_ms: item.wall_ms,
|
|
35
|
+
top1_hit: item.top1_hit,
|
|
36
|
+
top1: item.top1,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (result?.priority_ready && result?.first && result?.second) {
|
|
41
|
+
return {
|
|
42
|
+
priority_ready_ms: result.priority_ready?.parsed?.priority_ready_ms ?? result.priority_ready?.wallMs ?? 0,
|
|
43
|
+
priority_ready_anchor_completed: result.priority_ready_anchor_coverage?.completed ?? null,
|
|
44
|
+
priority_ready_anchor_total: result.priority_ready_anchor_coverage?.total ?? null,
|
|
45
|
+
repeat_query: result.repeat_query || "",
|
|
46
|
+
first_ms: result.first?.wall_ms ?? null,
|
|
47
|
+
second_ms: result.second?.wall_ms ?? null,
|
|
48
|
+
speedup_ratio: result.speedup_ratio ?? null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
wall_ms: result.wall_ms ?? 0,
|
|
53
|
+
scan_ms: result.scan_ms ?? 0,
|
|
54
|
+
total_ms: result.total_ms ?? 0,
|
|
55
|
+
strategy_version: result.strategy_version || "",
|
|
56
|
+
strategy_source: result.strategy_source || "",
|
|
57
|
+
embedding_model_source: result.embedding_model_source || "",
|
|
58
|
+
final_status: result.final_status || "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const workspace = await gitWorkspaceStats(TARGET_PATH);
|
|
64
|
+
const output = {
|
|
65
|
+
path: TARGET_PATH,
|
|
66
|
+
api_key_present: Boolean(process.env.IDEROUTER_API_KEY),
|
|
67
|
+
generated_at: new Date().toISOString(),
|
|
68
|
+
workspace,
|
|
69
|
+
benchmarks: {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const cases = [
|
|
73
|
+
["noop", "benchmark-noop.mjs"],
|
|
74
|
+
["small_change", "benchmark-small-change.mjs"],
|
|
75
|
+
["deferred", "benchmark-deferred.mjs"],
|
|
76
|
+
["priority_ready_search", "benchmark-priority-ready-search.mjs"],
|
|
77
|
+
["repeat_search", "benchmark-repeat-search.mjs"],
|
|
78
|
+
["stage_summary", "benchmark-stage-summary.mjs"],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const [name, scriptName] of cases) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await runScript(scriptName);
|
|
84
|
+
output.benchmarks[name] = {
|
|
85
|
+
ok: true,
|
|
86
|
+
summary: summarize(result),
|
|
87
|
+
result,
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
output.benchmarks[name] = {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (String(process.env.IDEROUTER_BENCH_INCLUDE_CLEAN_SNAPSHOT || "").toLowerCase() === "true") {
|
|
98
|
+
try {
|
|
99
|
+
const result = await runScript("benchmark-clean-snapshot.mjs");
|
|
100
|
+
output.benchmarks.clean_snapshot = {
|
|
101
|
+
ok: true,
|
|
102
|
+
summary: summarize(result.measure || result),
|
|
103
|
+
result,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
output.benchmarks.clean_snapshot = {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
output.comparison = buildComparison(output);
|
|
114
|
+
|
|
115
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildComparison(output) {
|
|
119
|
+
const noop = output.benchmarks.noop?.result;
|
|
120
|
+
const smallChange = output.benchmarks.small_change?.result;
|
|
121
|
+
const deferred = output.benchmarks.deferred?.result;
|
|
122
|
+
const priorityReadySearch = output.benchmarks.priority_ready_search?.result;
|
|
123
|
+
const repeatSearch = output.benchmarks.repeat_search?.result;
|
|
124
|
+
const cleanSnapshot = output.benchmarks.clean_snapshot?.result;
|
|
125
|
+
const stageSummary = output.benchmarks.stage_summary?.result;
|
|
126
|
+
const resumeRounds = Array.isArray(stageSummary?.resume_rounds) ? stageSummary.resume_rounds : [];
|
|
127
|
+
const finalResumeAnchorCoverage = resumeRounds.length > 0 ? resumeRounds[resumeRounds.length - 1]?.priority_ready_anchor_coverage : null;
|
|
128
|
+
const workspace = output.workspace || {};
|
|
129
|
+
const noOpComparable = workspace.workspace_dirty === false || Number(workspace.indexable_dirty_entries || 0) === 0;
|
|
130
|
+
const cleanComparable = Boolean(cleanSnapshot?.comparable_to_auggie);
|
|
131
|
+
return {
|
|
132
|
+
no_op_comparable_to_auggie: noOpComparable,
|
|
133
|
+
clean_snapshot_available: Boolean(cleanSnapshot),
|
|
134
|
+
clean_snapshot_comparable_to_auggie: cleanComparable,
|
|
135
|
+
note: noOpComparable
|
|
136
|
+
? "No-op benchmark is comparable to Auggie daily ready path."
|
|
137
|
+
: cleanComparable
|
|
138
|
+
? "Workspace is dirty, but clean snapshot benchmark is available for fair Auggie comparison."
|
|
139
|
+
: "Workspace has indexable dirty files; no-op benchmark is not a true clean-repo comparison against Auggie.",
|
|
140
|
+
current_signals: {
|
|
141
|
+
noop_total_ms: noop?.total_ms ?? null,
|
|
142
|
+
clean_snapshot_noop_total_ms: cleanSnapshot?.measure?.total_ms ?? null,
|
|
143
|
+
clean_snapshot_strategy_mode: cleanSnapshot?.strategy_mode ?? null,
|
|
144
|
+
small_change_changed: smallChange?.observed_index_change ?? null,
|
|
145
|
+
deferred_stable: deferred?.stable_status ?? null,
|
|
146
|
+
deferred_resumed_unexpectedly: deferred?.resumed_unexpectedly ?? null,
|
|
147
|
+
priority_ready_ms: priorityReadySearch?.priority_ready?.parsed?.priority_ready_ms ?? null,
|
|
148
|
+
priority_ready_anchor_completed: priorityReadySearch?.priority_ready_anchor_coverage?.completed ?? null,
|
|
149
|
+
priority_ready_anchor_total: priorityReadySearch?.priority_ready_anchor_coverage?.total ?? null,
|
|
150
|
+
priority_ready_all_top1_hit: Array.isArray(priorityReadySearch?.searches)
|
|
151
|
+
? priorityReadySearch.searches.every((item) => item?.top1_hit === true)
|
|
152
|
+
: null,
|
|
153
|
+
priority_ready_max_search_ms: Array.isArray(priorityReadySearch?.searches)
|
|
154
|
+
? Math.max(...priorityReadySearch.searches.map((item) => Number(item?.wall_ms || 0)), 0)
|
|
155
|
+
: null,
|
|
156
|
+
repeat_search_first_ms: repeatSearch?.first?.wall_ms ?? null,
|
|
157
|
+
repeat_search_second_ms: repeatSearch?.second?.wall_ms ?? null,
|
|
158
|
+
repeat_search_speedup_ratio: repeatSearch?.speedup_ratio ?? null,
|
|
159
|
+
repeat_search_priority_ready_anchor_completed: repeatSearch?.priority_ready_anchor_coverage?.completed ?? null,
|
|
160
|
+
repeat_search_priority_ready_anchor_total: repeatSearch?.priority_ready_anchor_coverage?.total ?? null,
|
|
161
|
+
stage_summary_priority_ready_ms: stageSummary?.priority_ready_ms ?? null,
|
|
162
|
+
stage_summary_priority_ready_anchor_completed: stageSummary?.priority_ready_anchor_completed ?? null,
|
|
163
|
+
stage_summary_priority_ready_anchor_total: stageSummary?.priority_ready_anchor_total ?? null,
|
|
164
|
+
stage_summary_resume_rounds_run: stageSummary?.resume_rounds_run ?? null,
|
|
165
|
+
stage_summary_resume_final_anchor_completed: finalResumeAnchorCoverage?.completed ?? null,
|
|
166
|
+
stage_summary_resume_final_anchor_total: finalResumeAnchorCoverage?.total ?? null,
|
|
167
|
+
stage_summary_resume_cumulative_semantic_files: stageSummary?.resume_cumulative_semantic_files ?? null,
|
|
168
|
+
stage_summary_composite_score: stageSummary?.composite_score ?? null,
|
|
169
|
+
stage_summary_composite_band: stageSummary?.composite_band ?? null,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((error) => {
|
|
175
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
cleanupBenchmarkRun,
|
|
10
|
+
installBenchmarkSignalCleanup,
|
|
11
|
+
readIndexMeta,
|
|
12
|
+
readJobStatus,
|
|
13
|
+
sleep,
|
|
14
|
+
spawnMcpClient,
|
|
15
|
+
} from "./lib/mcp-bench.mjs";
|
|
16
|
+
import {
|
|
17
|
+
continuationAlreadyMaterialized,
|
|
18
|
+
hasAdvanced,
|
|
19
|
+
isCoarseDeferredReady,
|
|
20
|
+
isFullReadyState,
|
|
21
|
+
} from "./lib/auto-continuation-state.mjs";
|
|
22
|
+
import { createBenchmarkSnapshot } from "./lib/benchmark-snapshot.mjs";
|
|
23
|
+
|
|
24
|
+
const TARGET_PATH = path.resolve(process.argv[2] || process.cwd());
|
|
25
|
+
const POLL_MS = Number(process.env.IDEROUTER_BENCH_POLL_MS || 1000);
|
|
26
|
+
const FIRST_PHASE_TIMEOUT_MS = Number(process.env.IDEROUTER_BENCH_AUTO_CONTINUE_FIRST_PHASE_TIMEOUT_MS || 180000);
|
|
27
|
+
const CONTINUATION_TIMEOUT_MS = Number(process.env.IDEROUTER_BENCH_AUTO_CONTINUE_TIMEOUT_MS || 180000);
|
|
28
|
+
|
|
29
|
+
function snapshotState(meta, job, statusText) {
|
|
30
|
+
return {
|
|
31
|
+
status_text: statusText.split("\n")[0],
|
|
32
|
+
job_id: job?.id || "",
|
|
33
|
+
job_status: job?.status || "",
|
|
34
|
+
current_step: job?.currentStep || "",
|
|
35
|
+
continuation_pass_count: Number(job?.continuationPassCount || 0),
|
|
36
|
+
progress: Number(job?.progress || 0),
|
|
37
|
+
embedded_count: Number(job?.embeddedCount || 0),
|
|
38
|
+
total_chunks: Number(job?.totalChunks || 0),
|
|
39
|
+
background_fine_deferred: Boolean(job?.backgroundFineDeferred),
|
|
40
|
+
semantic_retry_deferred: Boolean(job?.semanticRetryDeferred),
|
|
41
|
+
searchable_modes: job?.searchableModes || meta?.searchableModes || "",
|
|
42
|
+
priority_fine_progress: Number(job?.priorityFineProgress || meta?.priorityFineProgress || 0),
|
|
43
|
+
fine_semantic_files: Number(meta?.fineSemanticFileCount || 0),
|
|
44
|
+
fine_semantic_chunks: Number(meta?.fineSemanticChunkCount || 0),
|
|
45
|
+
background_bucket_cursor: String(meta?.backgroundBucketCursor || ""),
|
|
46
|
+
background_fine_target_files: Number(meta?.backgroundFineTargetFiles || job?.diagnostics?.backgroundFineTargetFiles || 0),
|
|
47
|
+
background_fine_target_chunks: Number(meta?.backgroundFineTargetChunks || job?.diagnostics?.backgroundFineTargetChunks || 0),
|
|
48
|
+
semantic_retry_deferred_at: String(job?.semanticRetryDeferredAt || ""),
|
|
49
|
+
deferred_resume_worker_pid: Number(job?.deferredResumeWorkerPid || 0),
|
|
50
|
+
updated_at: job?.updatedAt || meta?.updatedAt || "",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitForDeferredReady(client, snapshotDir, indexHome) {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
let lastText = "";
|
|
57
|
+
while (Date.now() - startedAt < FIRST_PHASE_TIMEOUT_MS) {
|
|
58
|
+
lastText = await client.callTool("get_indexing_status", { path: snapshotDir });
|
|
59
|
+
const meta = await readIndexMeta(indexHome, snapshotDir);
|
|
60
|
+
const job = await readJobStatus(indexHome, snapshotDir);
|
|
61
|
+
const deferredReady =
|
|
62
|
+
Boolean(job?.backgroundFineDeferred) &&
|
|
63
|
+
Boolean(job?.priorityFineReady) &&
|
|
64
|
+
/^Priority semantic search is ready\b/.test(lastText);
|
|
65
|
+
const coarseDeferredReady =
|
|
66
|
+
Boolean(job?.backgroundFineDeferred) &&
|
|
67
|
+
!Boolean(job?.priorityFineReady) &&
|
|
68
|
+
isCoarseDeferredReady(snapshotState(meta, job, lastText));
|
|
69
|
+
const fullReady = Boolean(meta?.complete) || isFullReadyState(snapshotState(meta, job, lastText));
|
|
70
|
+
if (fullReady) {
|
|
71
|
+
return {
|
|
72
|
+
settled: true,
|
|
73
|
+
wall_ms: Date.now() - startedAt,
|
|
74
|
+
state: snapshotState(meta, job, lastText),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (deferredReady) {
|
|
78
|
+
return {
|
|
79
|
+
settled: true,
|
|
80
|
+
wall_ms: Date.now() - startedAt,
|
|
81
|
+
state: snapshotState(meta, job, lastText),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (coarseDeferredReady) {
|
|
85
|
+
return {
|
|
86
|
+
settled: true,
|
|
87
|
+
wall_ms: Date.now() - startedAt,
|
|
88
|
+
state: snapshotState(meta, job, lastText),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
await sleep(POLL_MS);
|
|
92
|
+
}
|
|
93
|
+
const meta = await readIndexMeta(indexHome, snapshotDir);
|
|
94
|
+
const job = await readJobStatus(indexHome, snapshotDir);
|
|
95
|
+
return {
|
|
96
|
+
settled: false,
|
|
97
|
+
wall_ms: Date.now() - startedAt,
|
|
98
|
+
state: snapshotState(meta, job, lastText),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitForAutoContinuation(client, snapshotDir, indexHome, baselineState) {
|
|
103
|
+
const startedAt = Date.now();
|
|
104
|
+
let lastText = "";
|
|
105
|
+
while (Date.now() - startedAt < CONTINUATION_TIMEOUT_MS) {
|
|
106
|
+
lastText = await client.callTool("get_indexing_status", { path: snapshotDir });
|
|
107
|
+
const meta = await readIndexMeta(indexHome, snapshotDir);
|
|
108
|
+
const job = await readJobStatus(indexHome, snapshotDir);
|
|
109
|
+
const currentState = snapshotState(meta, job, lastText);
|
|
110
|
+
const continuationOccurred = Number(currentState.continuation_pass_count || 0) >= Number(baselineState.continuation_pass_count || 0);
|
|
111
|
+
const activeOrNewJob =
|
|
112
|
+
String(currentState.job_id || "") !== String(baselineState.job_id || "") ||
|
|
113
|
+
["queued", "scanning", "indexing"].includes(String(currentState.job_status || ""));
|
|
114
|
+
if (continuationOccurred && activeOrNewJob && hasAdvanced(baselineState, currentState)) {
|
|
115
|
+
return {
|
|
116
|
+
settled: true,
|
|
117
|
+
wall_ms: Date.now() - startedAt,
|
|
118
|
+
state: currentState,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
await sleep(POLL_MS);
|
|
122
|
+
}
|
|
123
|
+
const meta = await readIndexMeta(indexHome, snapshotDir);
|
|
124
|
+
const job = await readJobStatus(indexHome, snapshotDir);
|
|
125
|
+
return {
|
|
126
|
+
settled: false,
|
|
127
|
+
wall_ms: Date.now() - startedAt,
|
|
128
|
+
state: snapshotState(meta, job, lastText),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
const snapshot = await createBenchmarkSnapshot(TARGET_PATH, process.env);
|
|
134
|
+
const snapshotDir = snapshot.snapshotDir;
|
|
135
|
+
const isolatedIndexHome = await fs.mkdtemp(path.join(os.tmpdir(), "iderouter-bench-auto-continue-"));
|
|
136
|
+
const env = {
|
|
137
|
+
...process.env,
|
|
138
|
+
IDEROUTER_INDEX_HOME: isolatedIndexHome,
|
|
139
|
+
};
|
|
140
|
+
const { child, client } = spawnMcpClient(env);
|
|
141
|
+
const uninstallSignalCleanup = installBenchmarkSignalCleanup(() => ({
|
|
142
|
+
child,
|
|
143
|
+
indexHome: isolatedIndexHome,
|
|
144
|
+
targetPath: snapshotDir,
|
|
145
|
+
snapshotDir,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await client.initialize();
|
|
150
|
+
const startText = await client.callTool("index_codebase", { path: snapshotDir, wait: false, cloud: false });
|
|
151
|
+
const firstPhase = await waitForDeferredReady(client, snapshotDir, isolatedIndexHome);
|
|
152
|
+
const continuation = !firstPhase.settled
|
|
153
|
+
? { settled: false, wall_ms: 0, state: firstPhase.state }
|
|
154
|
+
: continuationAlreadyMaterialized(firstPhase.state)
|
|
155
|
+
? { settled: true, wall_ms: 0, state: firstPhase.state }
|
|
156
|
+
: await waitForAutoContinuation(client, snapshotDir, isolatedIndexHome, firstPhase.state);
|
|
157
|
+
|
|
158
|
+
process.stdout.write(
|
|
159
|
+
`${JSON.stringify(
|
|
160
|
+
{
|
|
161
|
+
path: TARGET_PATH,
|
|
162
|
+
snapshot_path: snapshotDir,
|
|
163
|
+
snapshot_mode: snapshot.mode,
|
|
164
|
+
isolated_index_home: isolatedIndexHome,
|
|
165
|
+
start_status: startText.split("\n")[0],
|
|
166
|
+
first_phase: firstPhase,
|
|
167
|
+
continuation,
|
|
168
|
+
auto_continuation_confirmed: Boolean(firstPhase.settled && continuation.settled),
|
|
169
|
+
},
|
|
170
|
+
null,
|
|
171
|
+
2,
|
|
172
|
+
)}\n`,
|
|
173
|
+
);
|
|
174
|
+
} finally {
|
|
175
|
+
uninstallSignalCleanup();
|
|
176
|
+
await cleanupBenchmarkRun({
|
|
177
|
+
child,
|
|
178
|
+
indexHome: isolatedIndexHome,
|
|
179
|
+
targetPath: snapshotDir,
|
|
180
|
+
snapshotDir,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main().catch((error) => {
|
|
186
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
});
|