context-mode 1.0.131 → 1.0.132
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +26 -15
- package/build/cli.js +32 -0
- package/build/lifecycle.d.ts +51 -2
- package/build/lifecycle.js +67 -3
- package/build/server.js +92 -14
- package/build/session/extract.d.ts +7 -0
- package/build/session/extract.js +22 -6
- package/build/store.d.ts +17 -2
- package/build/store.js +17 -13
- package/build/util/sibling-mcp.d.ts +40 -0
- package/build/util/sibling-mcp.js +116 -11
- package/cli.bundle.mjs +161 -154
- package/configs/jetbrains-copilot/mcp.json +1 -2
- package/configs/vscode-copilot/mcp.json +1 -2
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-loaders.mjs +15 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/scripts/heal-better-sqlite3.mjs +99 -2
- package/scripts/postinstall.mjs +58 -0
- package/server.bundle.mjs +96 -96
- package/skills/context-mode/SKILL.md +1 -0
- package/skills/context-mode/references/anti-patterns.md +26 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.132"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.132",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.132",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.132",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.132",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ This gives you all 11 MCP tools without automatic routing. The model can still u
|
|
|
124
124
|
<details>
|
|
125
125
|
<summary><strong>Gemini CLI</strong> — one config file, hooks included</summary>
|
|
126
126
|
|
|
127
|
-
**Prerequisites:** Node.js
|
|
127
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Gemini CLI installed.
|
|
128
128
|
|
|
129
129
|
**Install:**
|
|
130
130
|
|
|
@@ -197,7 +197,7 @@ Full config reference: [`configs/gemini-cli/settings.json`](configs/gemini-cli/s
|
|
|
197
197
|
<details>
|
|
198
198
|
<summary><strong>VS Code Copilot</strong> — hooks with SessionStart</summary>
|
|
199
199
|
|
|
200
|
-
**Prerequisites:** Node.js
|
|
200
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), VS Code with Copilot Chat v0.32+.
|
|
201
201
|
|
|
202
202
|
**Install:**
|
|
203
203
|
|
|
@@ -254,7 +254,7 @@ Full hook config including PreCompact: [`configs/vscode-copilot/hooks.json`](con
|
|
|
254
254
|
<details>
|
|
255
255
|
<summary><strong>JetBrains Copilot</strong> — hooks with SessionStart</summary>
|
|
256
256
|
|
|
257
|
-
**Prerequisites:** Node.js
|
|
257
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), JetBrains IDE with GitHub Copilot plugin v1.5.57+.
|
|
258
258
|
|
|
259
259
|
**Install:**
|
|
260
260
|
|
|
@@ -305,7 +305,7 @@ Full setup guide: [`docs/jetbrains-copilot.md`](docs/jetbrains-copilot.md)
|
|
|
305
305
|
<details>
|
|
306
306
|
<summary><strong>Cursor</strong> — hooks with stop support</summary>
|
|
307
307
|
|
|
308
|
-
**Prerequisites:** Node.js
|
|
308
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Cursor with agent mode.
|
|
309
309
|
|
|
310
310
|
> **🚧 Work in progress** — the Marketplace plugin is **awaiting Cursor team review**. Until it's listed, install via the local-folder path described in Option A. Tracking in [#485](https://github.com/mksglu/context-mode/issues/485) / [#489](https://github.com/mksglu/context-mode/pull/489).
|
|
311
311
|
|
|
@@ -406,7 +406,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
|
|
|
406
406
|
<details>
|
|
407
407
|
<summary><strong>OpenCode</strong> — TypeScript plugin with hooks</summary>
|
|
408
408
|
|
|
409
|
-
**Prerequisites:** Node.js
|
|
409
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), OpenCode installed.
|
|
410
410
|
|
|
411
411
|
**Install:**
|
|
412
412
|
|
|
@@ -456,7 +456,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
456
456
|
<details>
|
|
457
457
|
<summary><strong>KiloCode</strong> — TypeScript plugin with hooks</summary>
|
|
458
458
|
|
|
459
|
-
**Prerequisites:** Node.js
|
|
459
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), KiloCode installed.
|
|
460
460
|
|
|
461
461
|
**Install:**
|
|
462
462
|
|
|
@@ -541,7 +541,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
541
541
|
<details>
|
|
542
542
|
<summary><strong>Codex CLI</strong> — MCP + hooks</summary>
|
|
543
543
|
|
|
544
|
-
**Prerequisites:** Node.js
|
|
544
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Codex CLI installed.
|
|
545
545
|
|
|
546
546
|
**Install:**
|
|
547
547
|
|
|
@@ -606,7 +606,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
606
606
|
<details>
|
|
607
607
|
<summary><strong>Qwen Code</strong> — MCP + hooks (identical wire protocol to Claude Code)</summary>
|
|
608
608
|
|
|
609
|
-
**Prerequisites:** Node.js
|
|
609
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Qwen Code installed (`npm install -g @qwen-code/qwen-code`).
|
|
610
610
|
|
|
611
611
|
1. Install context-mode:
|
|
612
612
|
|
|
@@ -660,7 +660,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
660
660
|
<details>
|
|
661
661
|
<summary><strong>Antigravity</strong> — MCP-only, no hooks</summary>
|
|
662
662
|
|
|
663
|
-
**Prerequisites:** Node.js
|
|
663
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Antigravity installed.
|
|
664
664
|
|
|
665
665
|
**Install:**
|
|
666
666
|
|
|
@@ -701,7 +701,7 @@ Full configs: [`configs/antigravity/mcp_config.json`](configs/antigravity/mcp_co
|
|
|
701
701
|
<details>
|
|
702
702
|
<summary><strong>Kiro</strong> — hooks with steering file</summary>
|
|
703
703
|
|
|
704
|
-
**Prerequisites:** Node.js
|
|
704
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Kiro with MCP enabled (Settings > search "MCP").
|
|
705
705
|
|
|
706
706
|
**Install:**
|
|
707
707
|
|
|
@@ -759,7 +759,7 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
|
|
759
759
|
<details>
|
|
760
760
|
<summary><strong>Zed</strong> — MCP-only, no hooks</summary>
|
|
761
761
|
|
|
762
|
-
**Prerequisites:** Node.js
|
|
762
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Zed installed.
|
|
763
763
|
|
|
764
764
|
**Install:**
|
|
765
765
|
|
|
@@ -802,7 +802,7 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
|
|
802
802
|
<details>
|
|
803
803
|
<summary><strong>Pi Coding Agent</strong> — extension with full hook support</summary>
|
|
804
804
|
|
|
805
|
-
**Prerequisites:** Node.js
|
|
805
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Pi Coding Agent installed.
|
|
806
806
|
|
|
807
807
|
**Install:**
|
|
808
808
|
|
|
@@ -849,7 +849,7 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
|
|
849
849
|
<details>
|
|
850
850
|
<summary><strong>OMP (Oh My Pi)</strong> — plugin with full hook support</summary>
|
|
851
851
|
|
|
852
|
-
**Prerequisites:** Node.js
|
|
852
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Oh My Pi installed.
|
|
853
853
|
|
|
854
854
|
**Install — plugin path (recommended):**
|
|
855
855
|
|
|
@@ -924,7 +924,7 @@ Full configs: [`configs/omp/mcp.json`](configs/omp/mcp.json) | [`configs/omp/SYS
|
|
|
924
924
|
|
|
925
925
|
Context Mode uses [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) on Node.js, which ships prebuilt native binaries for most platforms. On glibc >= 2.31 systems (Ubuntu 20.04+, Debian 11+, Fedora 34+, macOS, Windows), `npm install` works without any build tools.
|
|
926
926
|
|
|
927
|
-
**Linux + Node.js >= 22.
|
|
927
|
+
**Linux + Node.js >= 22.5:** Context Mode automatically uses the built-in `node:sqlite` module instead of `better-sqlite3`. This eliminates the native addon entirely, avoiding [sporadic SIGSEGV crashes](https://github.com/nodejs/node/issues/62515) caused by V8's `madvise(MADV_DONTNEED)` corrupting the addon's `.got.plt` section on Linux. No configuration needed — detection is automatic. **Linux + Node < 22.5 is unsupported** ([#564](https://github.com/mksglu/context-mode/issues/564)) — `npm install` will fail with remediation instructions.
|
|
928
928
|
|
|
929
929
|
**Bun users:** No native compilation needed. Context Mode automatically detects Bun and uses the built-in `bun:sqlite` module via a compatibility adapter. `better-sqlite3` and all its build dependencies are skipped entirely.
|
|
930
930
|
|
|
@@ -986,7 +986,7 @@ When output exceeds 5 KB and an `intent` is provided, Context Mode switches to i
|
|
|
986
986
|
|
|
987
987
|
## How the Knowledge Base Works
|
|
988
988
|
|
|
989
|
-
The `ctx_index` tool chunks markdown content by headings while keeping code blocks intact, then stores them in a **SQLite FTS5** (Full-Text Search 5) virtual table. The SQLite backend is selected automatically at runtime: `bun:sqlite` on Bun, `node:sqlite` on
|
|
989
|
+
The `ctx_index` tool chunks markdown content by headings while keeping code blocks intact, then stores them in a **SQLite FTS5** (Full-Text Search 5) virtual table. The SQLite backend is selected automatically at runtime: `bun:sqlite` on Bun, `node:sqlite` on Node.js >= 22.5, and `better-sqlite3` everywhere else. Search uses **BM25 ranking** — a probabilistic relevance algorithm that scores documents based on term frequency, inverse document frequency, and document length normalization. **Porter stemming** is applied at index time so "running", "runs", and "ran" match the same stem. Titles and headings are weighted **5x** in BM25 scoring for precise navigational queries.
|
|
990
990
|
|
|
991
991
|
When you call `ctx_search`, it returns relevant content snippets focused around matching query terms — not full documents, not approximations, the actual indexed content with smart extraction around what you're looking for. `ctx_fetch_and_index` extends this to URLs: fetch, convert HTML to markdown, chunk, index. The raw page never enters context. Use the `contentType` parameter to filter results by type (e.g. `code` or `prose`).
|
|
992
992
|
|
|
@@ -1361,6 +1361,17 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
|
|
|
1361
1361
|
|
|
1362
1362
|
`tool_input` for any `mcp__*` tool call is also redacted before persistence — keys matching `authorization`, `token`, `secret`, `password`, `api_key`, `cookie`, `signature`, `private_key` get masked to `[REDACTED]` so credentials in MCP arguments don't end up in the session DB.
|
|
1363
1363
|
|
|
1364
|
+
### Lifecycle environment variables
|
|
1365
|
+
|
|
1366
|
+
Two runtime knobs control how MCP server processes self-manage. Defaults are safe — only set these to opt-out of the leak-fix introduced in v1.0.132 ([#565](https://github.com/mksglu/context-mode/issues/565) / [#568](https://github.com/mksglu/context-mode/pull/568)).
|
|
1367
|
+
|
|
1368
|
+
| Variable | Default | Purpose |
|
|
1369
|
+
|---|---|---|
|
|
1370
|
+
| `CONTEXT_MODE_IDLE_TIMEOUT_MS` | `900000` (15 min) | An MCP child self-exits cleanly after this many milliseconds of stdin/request inactivity. Hosts like OpenCode and KiloCode open one MCP child per session and per subagent — without this, idle children accumulate to 25+ processes / 1.6 GB RSS in long-lived shells. Set to `0` to disable self-shutdown (rarely needed; useful only for daemons that must outlive their parent). |
|
|
1371
|
+
| `CONTEXT_MODE_STARTUP_SWEEP` | `1` (enabled) | At boot, a newly-spawned MCP child reaps any other context-mode MCP server pids that share its parent process (`sameParentOnly: true` — never touches MCP children of a different host). This reclaims accumulated siblings immediately instead of waiting for each idle timer to fire. Set to `0` or `false` to disable (useful when you intentionally want multiple concurrent MCP children under the same host, e.g. multi-tenant test runners). |
|
|
1372
|
+
|
|
1373
|
+
Both vars are read fresh at MCP server start — no restart of the host CLI is required, just spawn a new MCP child (open a new session) for changes to take effect. Invalid values (non-numeric `CONTEXT_MODE_IDLE_TIMEOUT_MS`, unrecognized `CONTEXT_MODE_STARTUP_SWEEP`) fall back to defaults silently.
|
|
1374
|
+
|
|
1364
1375
|
## Contributing
|
|
1365
1376
|
|
|
1366
1377
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and TDD guidelines.
|
package/build/cli.js
CHANGED
|
@@ -28,6 +28,8 @@ import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mc
|
|
|
28
28
|
// mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
|
|
29
29
|
// @ts-expect-error — JS module, no TS declarations
|
|
30
30
|
import { healPluginJsonMcpServers, healMcpJsonArgs } from "../scripts/heal-installed-plugins.mjs";
|
|
31
|
+
// @ts-expect-error — JS module, no TS declarations
|
|
32
|
+
import { detectWindowsVsYear } from "../scripts/heal-better-sqlite3.mjs";
|
|
31
33
|
// Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
|
|
32
34
|
// duplicated here so the cli bundle does not pull server.ts top-level boot side effects.
|
|
33
35
|
// Keep in sync — pure data, no I/O.
|
|
@@ -300,6 +302,34 @@ async function doctor() {
|
|
|
300
302
|
s.stop("Diagnostics complete");
|
|
301
303
|
// Runtime check
|
|
302
304
|
p.note(getRuntimeSummary(runtimes), "Runtimes");
|
|
305
|
+
// ── Issue #564 — Linux + Node < 22.5 + no Bun is unsafe ────────────
|
|
306
|
+
// V8's madvise(MADV_DONTNEED) can corrupt better-sqlite3's native addon
|
|
307
|
+
// `.got.plt` on Linux, causing sporadic SIGSEGV (1-4/hour). The 22.5
|
|
308
|
+
// gate (`hasModernSqlite()` in src/db-base.ts:226-244) is the contract:
|
|
309
|
+
// at or above it we use node:sqlite (built-in, no native addon, no
|
|
310
|
+
// .got.plt to corrupt); below it we fall through to better-sqlite3
|
|
311
|
+
// which WILL crash. engines.node + a hard-fail postinstall guard this
|
|
312
|
+
// at install time, but doctor() surfaces it for already-installed users
|
|
313
|
+
// (and for adapters whose MCP host swallows stderr during install).
|
|
314
|
+
// Refs:
|
|
315
|
+
// - https://github.com/nodejs/node/issues/62515
|
|
316
|
+
// - https://github.com/mksglu/context-mode/issues/564
|
|
317
|
+
{
|
|
318
|
+
const { hasModernSqlite } = await import("./db-base.js");
|
|
319
|
+
if (process.platform === "linux" &&
|
|
320
|
+
!hasModernSqlite() &&
|
|
321
|
+
!hasBunRuntime()) {
|
|
322
|
+
criticalFails++;
|
|
323
|
+
p.log.error(color.red("Node version: FAIL") +
|
|
324
|
+
` — Linux + Node ${process.versions.node} is unsafe (SIGSEGV)` +
|
|
325
|
+
color.dim("\n context-mode requires Node.js >= 22.5 (or Bun) on Linux to avoid the" +
|
|
326
|
+
"\n V8 madvise(MADV_DONTNEED) SIGSEGV in better-sqlite3 (1-4/hour)." +
|
|
327
|
+
"\n Refs: https://github.com/nodejs/node/issues/62515" +
|
|
328
|
+
"\n https://github.com/mksglu/context-mode/issues/564" +
|
|
329
|
+
"\n Fix: nvm install 22.5 && nvm use 22.5 && npm install -g context-mode" +
|
|
330
|
+
"\n Or: curl -fsSL https://bun.sh/install | bash && bun add -g context-mode"));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
303
333
|
// Speed tier
|
|
304
334
|
if (hasBunRuntime()) {
|
|
305
335
|
p.log.success(color.green("Performance: FAST") +
|
|
@@ -736,10 +766,12 @@ async function upgrade(opts) {
|
|
|
736
766
|
catch { /* never block upgrade on discovery/kill failure */ }
|
|
737
767
|
// Step 2: Install dependencies + build
|
|
738
768
|
s.start("Installing dependencies & building");
|
|
769
|
+
const vsYear = detectWindowsVsYear();
|
|
739
770
|
npmExecFile(["install", "--no-audit", "--no-fund"], {
|
|
740
771
|
cwd: srcDir,
|
|
741
772
|
stdio: "pipe",
|
|
742
773
|
timeout: 120000,
|
|
774
|
+
...(vsYear ? { env: { ...process.env, npm_config_msvs_version: vsYear } } : {}),
|
|
743
775
|
});
|
|
744
776
|
npmExecFile(["run", "build"], {
|
|
745
777
|
cwd: srcDir,
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -20,7 +20,54 @@ export interface LifecycleGuardOptions {
|
|
|
20
20
|
onShutdown: () => void;
|
|
21
21
|
/** Injectable parent-alive check (for testing). Default: ppid-based check. */
|
|
22
22
|
isParentAlive?: () => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Idle shutdown threshold in ms (#565). When the server has handled no
|
|
25
|
+
* MCP activity for this long, `onShutdown` fires. `0` disables.
|
|
26
|
+
* Default: env `CONTEXT_MODE_IDLE_TIMEOUT_MS`, else 15 minutes.
|
|
27
|
+
* Skipped on TTY stdin (interactive dev / OpenCode ts-plugin standalone).
|
|
28
|
+
*
|
|
29
|
+
* Pair with the returned `recordActivity()` callback — call it on every
|
|
30
|
+
* MCP request the server handles so genuinely busy servers never trip.
|
|
31
|
+
*/
|
|
32
|
+
idleTimeoutMs?: number;
|
|
33
|
+
/** Test injection — defaults to `Date.now`. */
|
|
34
|
+
now?: () => number;
|
|
23
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Hybrid return type: callable like the original `() => void` cleanup (kept
|
|
38
|
+
* for backwards compatibility with #103/#236/#311/#388/#534 test suites),
|
|
39
|
+
* and additionally exposes `recordActivity` for the idle-timeout path (#565)
|
|
40
|
+
* and `stop` as an explicit alias.
|
|
41
|
+
*/
|
|
42
|
+
export interface LifecycleGuardHandle {
|
|
43
|
+
/** Stop the guard. Calling the handle directly is equivalent. */
|
|
44
|
+
(): void;
|
|
45
|
+
/** Bumps the "last activity" timestamp so the idle timer doesn't fire. */
|
|
46
|
+
recordActivity: () => void;
|
|
47
|
+
/** Stop the guard. Alias for invoking the handle. */
|
|
48
|
+
stop: () => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the idle-shutdown threshold (#565).
|
|
52
|
+
*
|
|
53
|
+
* OpenCode + KiloCode open a fresh MCP client per session AND per subagent
|
|
54
|
+
* task, but never tear them down for the host's lifetime. A host alive for
|
|
55
|
+
* a working day accumulates one stdio child per session — observed live at
|
|
56
|
+
* 26 children / 1.6 GB RSS under a single `opencode serve` parent.
|
|
57
|
+
*
|
|
58
|
+
* None of the existing exit paths (ppid poll, grandparent reparent, stdin
|
|
59
|
+
* EOF, SIGTERM) fire while the host stays alive. Idle shutdown is the
|
|
60
|
+
* structural fix: a server with no work to do should release its memory.
|
|
61
|
+
*
|
|
62
|
+
* Default 15 min strikes a balance — long enough that a paused
|
|
63
|
+
* conversation does not pay a cold-start on every resume, short enough
|
|
64
|
+
* that 8 hours of unused sessions do not pin GB of RAM.
|
|
65
|
+
*
|
|
66
|
+
* Set env to `0` to disable entirely.
|
|
67
|
+
*
|
|
68
|
+
* Exported for unit-testing.
|
|
69
|
+
*/
|
|
70
|
+
export declare function idleTimeoutForEnv(env?: NodeJS.ProcessEnv): number;
|
|
24
71
|
/** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
|
|
25
72
|
export interface IsParentAliveDeps {
|
|
26
73
|
/** Read the current ppid. Default: `() => process.ppid`. */
|
|
@@ -60,7 +107,9 @@ export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () =
|
|
|
60
107
|
*/
|
|
61
108
|
export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
|
|
62
109
|
/**
|
|
63
|
-
* Start the lifecycle guard. Returns a
|
|
110
|
+
* Start the lifecycle guard. Returns a handle with `recordActivity` (call
|
|
111
|
+
* on every MCP request to keep idle timer from firing) and `stop`.
|
|
112
|
+
*
|
|
64
113
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
65
114
|
*/
|
|
66
|
-
export declare function startLifecycleGuard(opts: LifecycleGuardOptions):
|
|
115
|
+
export declare function startLifecycleGuard(opts: LifecycleGuardOptions): LifecycleGuardHandle;
|
package/build/lifecycle.js
CHANGED
|
@@ -14,6 +14,35 @@
|
|
|
14
14
|
* Cross-platform: macOS, Linux, Windows.
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the idle-shutdown threshold (#565).
|
|
19
|
+
*
|
|
20
|
+
* OpenCode + KiloCode open a fresh MCP client per session AND per subagent
|
|
21
|
+
* task, but never tear them down for the host's lifetime. A host alive for
|
|
22
|
+
* a working day accumulates one stdio child per session — observed live at
|
|
23
|
+
* 26 children / 1.6 GB RSS under a single `opencode serve` parent.
|
|
24
|
+
*
|
|
25
|
+
* None of the existing exit paths (ppid poll, grandparent reparent, stdin
|
|
26
|
+
* EOF, SIGTERM) fire while the host stays alive. Idle shutdown is the
|
|
27
|
+
* structural fix: a server with no work to do should release its memory.
|
|
28
|
+
*
|
|
29
|
+
* Default 15 min strikes a balance — long enough that a paused
|
|
30
|
+
* conversation does not pay a cold-start on every resume, short enough
|
|
31
|
+
* that 8 hours of unused sessions do not pin GB of RAM.
|
|
32
|
+
*
|
|
33
|
+
* Set env to `0` to disable entirely.
|
|
34
|
+
*
|
|
35
|
+
* Exported for unit-testing.
|
|
36
|
+
*/
|
|
37
|
+
export function idleTimeoutForEnv(env = process.env) {
|
|
38
|
+
const raw = env.CONTEXT_MODE_IDLE_TIMEOUT_MS;
|
|
39
|
+
if (raw === undefined)
|
|
40
|
+
return 15 * 60 * 1000;
|
|
41
|
+
const n = Number.parseInt(raw, 10);
|
|
42
|
+
if (!Number.isFinite(n) || n < 0)
|
|
43
|
+
return 15 * 60 * 1000;
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
17
46
|
/** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
|
|
18
47
|
function readGrandparentPpidImpl() {
|
|
19
48
|
if (process.platform === "win32")
|
|
@@ -95,25 +124,52 @@ export function lifecycleGuardIntervalForEnv(env = process.env) {
|
|
|
95
124
|
return 1000;
|
|
96
125
|
}
|
|
97
126
|
/**
|
|
98
|
-
* Start the lifecycle guard. Returns a
|
|
127
|
+
* Start the lifecycle guard. Returns a handle with `recordActivity` (call
|
|
128
|
+
* on every MCP request to keep idle timer from firing) and `stop`.
|
|
129
|
+
*
|
|
99
130
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
100
131
|
*/
|
|
101
132
|
export function startLifecycleGuard(opts) {
|
|
102
133
|
const interval = opts.checkIntervalMs ?? lifecycleGuardIntervalForEnv();
|
|
103
134
|
const check = opts.isParentAlive ?? defaultIsParentAlive;
|
|
135
|
+
const idleTimeoutMs = opts.idleTimeoutMs ?? idleTimeoutForEnv();
|
|
136
|
+
const now = opts.now ?? Date.now;
|
|
104
137
|
let stopped = false;
|
|
138
|
+
let lastActivity = now();
|
|
105
139
|
const shutdown = () => {
|
|
106
140
|
if (stopped)
|
|
107
141
|
return;
|
|
108
142
|
stopped = true;
|
|
109
143
|
opts.onShutdown();
|
|
110
144
|
};
|
|
111
|
-
|
|
145
|
+
const recordActivity = () => {
|
|
146
|
+
lastActivity = now();
|
|
147
|
+
};
|
|
148
|
+
// P0: Periodic parent liveness check.
|
|
112
149
|
const timer = setInterval(() => {
|
|
113
150
|
if (!check())
|
|
114
151
|
shutdown();
|
|
115
152
|
}, interval);
|
|
116
153
|
timer.unref();
|
|
154
|
+
// P0+: Idle shutdown (#565). Runs on its OWN tick — distinct from the
|
|
155
|
+
// 30 s parent-liveness poll — so a 15 min idle timeout actually reacts
|
|
156
|
+
// close to 15 min instead of "next 30 s tick after 15 min". Pick the
|
|
157
|
+
// tick as min(idleTimeoutMs / 6, 30 s) so a short timeout (e.g. 3 s in
|
|
158
|
+
// e2e tests, 60 s in dev) reacts within ~16 % of its window while a
|
|
159
|
+
// production 15 min timeout still polls every 30 s (cheap).
|
|
160
|
+
//
|
|
161
|
+
// Skipped on TTY because interactive dev sessions are expected to
|
|
162
|
+
// sit idle between commands, and also when idleTimeoutMs is 0 (env
|
|
163
|
+
// opt-out via CONTEXT_MODE_IDLE_TIMEOUT_MS=0).
|
|
164
|
+
let idleTimer = null;
|
|
165
|
+
if (idleTimeoutMs > 0 && !process.stdin.isTTY) {
|
|
166
|
+
const idleTick = Math.max(50, Math.min(Math.floor(idleTimeoutMs / 6), 30_000));
|
|
167
|
+
idleTimer = setInterval(() => {
|
|
168
|
+
if (now() - lastActivity > idleTimeoutMs)
|
|
169
|
+
shutdown();
|
|
170
|
+
}, idleTick);
|
|
171
|
+
idleTimer.unref();
|
|
172
|
+
}
|
|
117
173
|
// P0: OS signals — terminal close, kill, ctrl+c
|
|
118
174
|
const signals = ["SIGTERM", "SIGINT"];
|
|
119
175
|
if (process.platform !== "win32")
|
|
@@ -142,11 +198,19 @@ export function startLifecycleGuard(opts) {
|
|
|
142
198
|
if (!process.stdin.isTTY) {
|
|
143
199
|
process.stdin.on("end", onStdinEnd);
|
|
144
200
|
}
|
|
145
|
-
|
|
201
|
+
const cleanup = () => {
|
|
146
202
|
stopped = true;
|
|
147
203
|
clearInterval(timer);
|
|
204
|
+
if (idleTimer)
|
|
205
|
+
clearInterval(idleTimer);
|
|
148
206
|
for (const sig of signals)
|
|
149
207
|
process.removeListener(sig, shutdown);
|
|
150
208
|
process.stdin.removeListener("end", onStdinEnd);
|
|
151
209
|
};
|
|
210
|
+
// Hybrid: callable for legacy `const cleanup = startLifecycleGuard(...)`
|
|
211
|
+
// sites, with `.recordActivity` / `.stop` properties for the new contract.
|
|
212
|
+
const handle = cleanup;
|
|
213
|
+
handle.recordActivity = recordActivity;
|
|
214
|
+
handle.stop = cleanup;
|
|
215
|
+
return handle;
|
|
152
216
|
}
|
package/build/server.js
CHANGED
|
@@ -78,6 +78,20 @@ const CM_FS_PRELOAD = join(tmpdir(), `cm-fs-preload-${process.pid}.js`);
|
|
|
78
78
|
writeFileSync(CM_FS_PRELOAD, `(function(){var __cm_fs=0;process.on('exit',function(){if(__cm_fs>0)try{process.stderr.write('__CM_FS__:'+__cm_fs+'\\n')}catch(e){}});try{var f=require('fs');var ors=f.readFileSync;f.readFileSync=function(){var r=ors.apply(this,arguments);if(Buffer.isBuffer(r))__cm_fs+=r.length;else if(typeof r==='string')__cm_fs+=Buffer.byteLength(r);return r;};}catch(e){}})();\n`);
|
|
79
79
|
// Lazy singleton — no DB overhead unless index/search is used
|
|
80
80
|
let _store = null;
|
|
81
|
+
/**
|
|
82
|
+
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
83
|
+
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|
|
84
|
+
* the current session — eventId stays undefined because MCP tool invocations
|
|
85
|
+
* are not paired with PostToolUse event rows at index time (the hook fires
|
|
86
|
+
* AFTER the tool returns). Empty-string fallback inside #insertChunks keeps
|
|
87
|
+
* legacy unattributed rows readable.
|
|
88
|
+
*/
|
|
89
|
+
function currentAttribution() {
|
|
90
|
+
const sessionId = process.env.CLAUDE_SESSION_ID;
|
|
91
|
+
if (!sessionId)
|
|
92
|
+
return undefined;
|
|
93
|
+
return { sessionId };
|
|
94
|
+
}
|
|
81
95
|
/**
|
|
82
96
|
* Auto-index session events files written by SessionStart hook.
|
|
83
97
|
* Scans ~/.claude/context-mode/sessions/ for *-events.md files.
|
|
@@ -95,7 +109,7 @@ function maybeIndexSessionEvents(store) {
|
|
|
95
109
|
for (const file of files) {
|
|
96
110
|
const filePath = join(sessionsDir, file);
|
|
97
111
|
try {
|
|
98
|
-
store.index({ path: filePath, source: "session-events" });
|
|
112
|
+
store.index({ path: filePath, source: "session-events", attribution: currentAttribution() });
|
|
99
113
|
unlinkSync(filePath);
|
|
100
114
|
}
|
|
101
115
|
catch { /* best-effort per file */ }
|
|
@@ -1153,7 +1167,7 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
|
|
|
1153
1167
|
function indexStdout(stdout, source) {
|
|
1154
1168
|
const store = getStore();
|
|
1155
1169
|
trackIndexed(Buffer.byteLength(stdout));
|
|
1156
|
-
const indexed = store.index({ content: stdout, source });
|
|
1170
|
+
const indexed = store.index({ content: stdout, source, attribution: currentAttribution() });
|
|
1157
1171
|
return {
|
|
1158
1172
|
content: [
|
|
1159
1173
|
{
|
|
@@ -1173,7 +1187,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
1173
1187
|
const totalBytes = Buffer.byteLength(stdout);
|
|
1174
1188
|
// Index into the PERSISTENT store so user can ctx_search() later
|
|
1175
1189
|
const persistent = getStore();
|
|
1176
|
-
const indexed = persistent.indexPlainText(stdout, source);
|
|
1190
|
+
const indexed = persistent.indexPlainText(stdout, source, undefined, currentAttribution());
|
|
1177
1191
|
// Search the persistent store directly (porter → trigram → fuzzy)
|
|
1178
1192
|
let results = persistent.searchWithFallback(intent, maxResults, source);
|
|
1179
1193
|
// Extract distinctive terms as vocabulary hints for the LLM
|
|
@@ -1407,7 +1421,7 @@ server.registerTool("ctx_index", {
|
|
|
1407
1421
|
catch { /* ignore — file read errors handled by store */ }
|
|
1408
1422
|
}
|
|
1409
1423
|
const store = getStore();
|
|
1410
|
-
const result = store.index({ content, path: resolvedPath, source: source ?? resolvedPath });
|
|
1424
|
+
const result = store.index({ content, path: resolvedPath, source: source ?? resolvedPath, attribution: currentAttribution() });
|
|
1411
1425
|
return trackResponse("ctx_index", {
|
|
1412
1426
|
content: [
|
|
1413
1427
|
{
|
|
@@ -2145,15 +2159,16 @@ function indexFetched(f) {
|
|
|
2145
2159
|
// `source` label do not overwrite each other (commit 1f1243e). ctx_search()
|
|
2146
2160
|
// still finds both via LIKE-mode source filter on the `source` substring.
|
|
2147
2161
|
const storageLabel = composeFetchCacheKey(f.source, f.url);
|
|
2162
|
+
const attribution = currentAttribution();
|
|
2148
2163
|
let indexed;
|
|
2149
2164
|
if (f.header === "__CM_CT__:json") {
|
|
2150
|
-
indexed = store.indexJSON(f.markdown, storageLabel);
|
|
2165
|
+
indexed = store.indexJSON(f.markdown, storageLabel, undefined, attribution);
|
|
2151
2166
|
}
|
|
2152
2167
|
else if (f.header === "__CM_CT__:text") {
|
|
2153
|
-
indexed = store.indexPlainText(f.markdown, storageLabel);
|
|
2168
|
+
indexed = store.indexPlainText(f.markdown, storageLabel, undefined, attribution);
|
|
2154
2169
|
}
|
|
2155
2170
|
else {
|
|
2156
|
-
indexed = store.index({ content: f.markdown, source: storageLabel });
|
|
2171
|
+
indexed = store.index({ content: f.markdown, source: storageLabel, attribution });
|
|
2157
2172
|
}
|
|
2158
2173
|
// Track AFTER the FTS5 write succeeds — failed indexes shouldn't inflate the counter.
|
|
2159
2174
|
trackIndexed(Buffer.byteLength(f.markdown));
|
|
@@ -2460,7 +2475,7 @@ server.registerTool("ctx_batch_execute", {
|
|
|
2460
2475
|
.map((c) => c.label)
|
|
2461
2476
|
.join(",")
|
|
2462
2477
|
.slice(0, 80)}`;
|
|
2463
|
-
const indexed = store.index({ content: stdout, source });
|
|
2478
|
+
const indexed = store.index({ content: stdout, source, attribution: currentAttribution() });
|
|
2464
2479
|
// Build section inventory — direct query by source_id (no FTS5 MATCH needed)
|
|
2465
2480
|
const allSections = store.getChunksBySource(indexed.sourceId);
|
|
2466
2481
|
const inventory = ["## Indexed Sections", ""];
|
|
@@ -2865,7 +2880,12 @@ server.registerTool("ctx_upgrade", {
|
|
|
2865
2880
|
// files (events.md, FTS5 store file, stats file) are preserved.
|
|
2866
2881
|
// Passing both sessionId AND scope:"project" is ambiguous (does the
|
|
2867
2882
|
// caller want a per-session wipe or a project-wide one?) and is
|
|
2868
|
-
// rejected by the schema
|
|
2883
|
+
// rejected by an explicit check in the handler body — NOT a schema-level
|
|
2884
|
+
// .refine(). MCP SDK's normalizeObjectSchema() reads `.shape` to project
|
|
2885
|
+
// inputSchema → JSON Schema for tools/list; a ZodEffects (refine wrapper)
|
|
2886
|
+
// has no `.shape`, so the SDK silently emits `properties: {}`, and Claude
|
|
2887
|
+
// Code's strict-input-validation gate then rejects EVERY call to this
|
|
2888
|
+
// tool with "input_schema does not support fields". Issue #563.
|
|
2869
2889
|
server.registerTool("ctx_purge", {
|
|
2870
2890
|
title: "Purge Knowledge Base",
|
|
2871
2891
|
description: "DESTRUCTIVE — permanently delete indexed content. CANNOT be undone.\n\n" +
|
|
@@ -2886,6 +2906,9 @@ server.registerTool("ctx_purge", {
|
|
|
2886
2906
|
"Use sessionId when the user asks to clear a specific conversation's data.\n" +
|
|
2887
2907
|
"Use scope:'project' ONLY when the user explicitly asks to reset everything.\n" +
|
|
2888
2908
|
"NEVER call with bare {confirm:true} — always specify the scope.",
|
|
2909
|
+
// NOTE: schema MUST be a plain z.object — no .refine()/.transform()/
|
|
2910
|
+
// .superRefine() wrapper. See block comment above & issue #563. The
|
|
2911
|
+
// cross-field ambiguity check lives in the handler body below.
|
|
2889
2912
|
inputSchema: z.object({
|
|
2890
2913
|
confirm: z.boolean().describe("MUST be true. Destructive operation; false returns 'purge cancelled'."),
|
|
2891
2914
|
sessionId: z.string().optional().describe("UUID of a single session. Pairs with confirm:true to wipe only that " +
|
|
@@ -2894,12 +2917,22 @@ server.registerTool("ctx_purge", {
|
|
|
2894
2917
|
scope: z.enum(["session", "project"]).optional().describe("Explicit scope selector. 'session' REQUIRES sessionId. 'project' wipes " +
|
|
2895
2918
|
"the entire project (FTS5 + every session + stats). Omit only for the " +
|
|
2896
2919
|
"deprecated bare-{confirm:true} back-compat path."),
|
|
2897
|
-
}).refine((v) => !(v.sessionId && v.scope === "project"), {
|
|
2898
|
-
message: "Ambiguous purge: sessionId implies scope:'session', cannot combine with scope:'project'. " +
|
|
2899
|
-
"Use scope:'project' WITHOUT sessionId for the legacy whole-project wipe.",
|
|
2900
|
-
path: ["scope"],
|
|
2901
2920
|
}),
|
|
2902
2921
|
}, async ({ confirm, sessionId, scope }) => {
|
|
2922
|
+
// Cross-field ambiguity check — formerly a schema .refine(), moved
|
|
2923
|
+
// into the handler so the inputSchema stays a plain ZodObject and
|
|
2924
|
+
// the MCP SDK can serialize `.shape` into JSON Schema (issue #563).
|
|
2925
|
+
// Same human-readable message as the original refine() preserved.
|
|
2926
|
+
if (sessionId && scope === "project") {
|
|
2927
|
+
return trackResponse("ctx_purge", {
|
|
2928
|
+
content: [{
|
|
2929
|
+
type: "text",
|
|
2930
|
+
text: "Ambiguous purge: sessionId implies scope:'session', cannot combine with scope:'project'. " +
|
|
2931
|
+
"Use scope:'project' WITHOUT sessionId for the legacy whole-project wipe.",
|
|
2932
|
+
}],
|
|
2933
|
+
isError: true,
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2903
2936
|
if (!confirm) {
|
|
2904
2937
|
return trackResponse("ctx_purge", {
|
|
2905
2938
|
content: [{
|
|
@@ -3365,6 +3398,20 @@ server.registerTool("ctx_insight", {
|
|
|
3365
3398
|
// Server startup
|
|
3366
3399
|
// ─────────────────────────────────────────────────────────
|
|
3367
3400
|
async function main() {
|
|
3401
|
+
// Startup sibling sweep (#565). OpenCode/KiloCode spawn one MCP child
|
|
3402
|
+
// per session/subagent and never reap them. When a new MCP child boots
|
|
3403
|
+
// under a host that already has N stale idle siblings (sharing OUR
|
|
3404
|
+
// ppid), reclaim them before opening our own DB / sentinel / stdio.
|
|
3405
|
+
// Best effort — never blocks startup.
|
|
3406
|
+
try {
|
|
3407
|
+
const { startupSiblingSweep } = await import("./util/sibling-mcp.js");
|
|
3408
|
+
const report = await startupSiblingSweep();
|
|
3409
|
+
if (report.totalKilled > 0) {
|
|
3410
|
+
console.error(`Reaped ${report.totalKilled} stale sibling MCP server(s) ` +
|
|
3411
|
+
`(SIGTERM: ${report.terminatedBySigterm}, SIGKILL: ${report.terminatedBySigkill})`);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
catch { /* best effort */ }
|
|
3368
3415
|
// Clean up stale DB files from previous sessions
|
|
3369
3416
|
const cleaned = cleanupStaleDBs();
|
|
3370
3417
|
if (cleaned > 0) {
|
|
@@ -3414,7 +3461,38 @@ async function main() {
|
|
|
3414
3461
|
process.on("SIGINT", () => { gracefulShutdown(); });
|
|
3415
3462
|
process.on("SIGTERM", () => { gracefulShutdown(); });
|
|
3416
3463
|
// Lifecycle guard: detect parent death + stdin close to prevent orphaned processes (#103)
|
|
3417
|
-
|
|
3464
|
+
// Also: idle self-shutdown (#565) — OpenCode/KiloCode open one MCP child per
|
|
3465
|
+
// session AND per subagent and never tear them down for the host's lifetime,
|
|
3466
|
+
// accumulating one stdio child per session (observed: 26 children / 1.6 GB
|
|
3467
|
+
// RSS under a single `opencode serve` parent). Idle timeout reaps quiescent
|
|
3468
|
+
// servers; live ones bump `recordActivity()` on every JSON-RPC request via
|
|
3469
|
+
// the MCP SDK's `_onrequest` hook wrapped below.
|
|
3470
|
+
const lifecycle = startLifecycleGuard({ onShutdown: () => gracefulShutdown() });
|
|
3471
|
+
// Wrap the SDK's internal request entry so every JSON-RPC `tools/call`,
|
|
3472
|
+
// `tools/list`, etc. resets the idle timer. We intercept at this layer
|
|
3473
|
+
// rather than per-tool because (a) it covers ALL requests, including
|
|
3474
|
+
// listTools / listPrompts / listResources / ping, and (b) it survives
|
|
3475
|
+
// future tool additions without each handler needing to remember to opt in.
|
|
3476
|
+
//
|
|
3477
|
+
// The cast is necessary because `_onrequest` is intentionally undocumented
|
|
3478
|
+
// in the SDK's public types. Best effort — if the field shape changes in
|
|
3479
|
+
// a future SDK release the lifecycle still works, idle reset just degrades
|
|
3480
|
+
// to "untriggered" which simply means the server lives until the next
|
|
3481
|
+
// ppid/signal-based exit path fires. We never block the request path.
|
|
3482
|
+
try {
|
|
3483
|
+
const inner = server.server;
|
|
3484
|
+
const origOnRequest = inner._onrequest;
|
|
3485
|
+
if (typeof origOnRequest === "function") {
|
|
3486
|
+
inner._onrequest = function (...args) {
|
|
3487
|
+
try {
|
|
3488
|
+
lifecycle.recordActivity();
|
|
3489
|
+
}
|
|
3490
|
+
catch { /* never break request path */ }
|
|
3491
|
+
return origOnRequest.apply(this, args);
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
catch { /* best effort — see comment above */ }
|
|
3418
3496
|
const transport = new StdioServerTransport();
|
|
3419
3497
|
await server.connect(transport);
|
|
3420
3498
|
// Write MCP readiness sentinel (#230)
|
|
@@ -15,6 +15,13 @@ export interface SessionEvent {
|
|
|
15
15
|
data: string;
|
|
16
16
|
/** 1=critical (rules, files, tasks) … 5=low */
|
|
17
17
|
priority: number;
|
|
18
|
+
/**
|
|
19
|
+
* Optional — bytes context-mode prevented from entering the model context
|
|
20
|
+
* window for this event. Currently populated by external_ref when a
|
|
21
|
+
* ctx_fetch_and_index tool_response carries the
|
|
22
|
+
* `Fetched and indexed N sections (XKB)` preamble.
|
|
23
|
+
*/
|
|
24
|
+
bytes_avoided?: number;
|
|
18
25
|
}
|
|
19
26
|
export interface ToolCall {
|
|
20
27
|
toolName: string;
|