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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.131"
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.131",
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.131",
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.131",
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.131",
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 18+, Gemini CLI installed.
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 18+, VS Code with Copilot Chat v0.32+.
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 18+, JetBrains IDE with GitHub Copilot plugin v1.5.57+.
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 18+, Cursor with agent mode.
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 18+, OpenCode installed.
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 18+, KiloCode installed.
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 18+, Codex CLI installed.
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 18+, Qwen Code installed (`npm install -g @qwen-code/qwen-code`).
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 18+, Antigravity installed.
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 18+, Kiro with MCP enabled (Settings > search "MCP").
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 18+, Zed installed.
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 18+, Pi Coding Agent installed.
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 18+, Oh My Pi installed.
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.13:** 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. Falls back to `better-sqlite3` on older Node.js versions.
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 Linux + Node.js >= 22.13, 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.
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,
@@ -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 cleanup function.
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): () => void;
115
+ export declare function startLifecycleGuard(opts: LifecycleGuardOptions): LifecycleGuardHandle;
@@ -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 cleanup function.
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
- // P0: Periodic parent liveness check
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
- return () => {
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
  }