context-mode 1.0.131 → 1.0.133
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 +118 -16
- package/build/session/analytics.d.ts +38 -0
- package/build/session/analytics.js +58 -1
- 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 +174 -165
- 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 +106 -104
- 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.133"
|
|
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.133",
|
|
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.133",
|
|
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.133",
|
|
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.133",
|
|
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
|
}
|