context-mode 1.0.136 → 1.0.137

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.136"
9
+ "version": "1.0.137"
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.136",
16
+ "version": "1.0.137",
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.136",
3
+ "version": "1.0.137",
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",
@@ -27,5 +27,5 @@
27
27
  ]
28
28
  }
29
29
  },
30
- "skills": "./skills/"
30
+ "skills": "./.claude/skills/"
31
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.136",
3
+ "version": "1.0.137",
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.136",
6
+ "version": "1.0.137",
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.136",
3
+ "version": "1.0.137",
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
@@ -421,20 +421,11 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
421
421
  ```json
422
422
  {
423
423
  "$schema": "https://opencode.ai/config.json",
424
- "mcp": {
425
- "context-mode": {
426
- "type": "local",
427
- "command": ["context-mode"],
428
- "environment": {
429
- "CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
430
- }
431
- }
432
- },
433
424
  "plugin": ["context-mode"]
434
425
  }
435
426
  ```
436
427
 
437
- The `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — OpenCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
428
+ The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — OpenCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
438
429
 
439
430
  3. *(Optional)* Copy the routing rules file. The model needs an `AGENTS.md` file for routing awareness:
440
431
 
@@ -474,20 +465,11 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
474
465
  ```json
475
466
  {
476
467
  "$schema": "https://app.kilo.ai/config.json",
477
- "mcp": {
478
- "context-mode": {
479
- "type": "local",
480
- "command": ["context-mode"],
481
- "environment": {
482
- "CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
483
- }
484
- }
485
- },
486
468
  "plugin": ["context-mode"]
487
469
  }
488
470
  ```
489
471
 
490
- The `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — KiloCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
472
+ The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — KiloCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
491
473
 
492
474
  3. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture, so the model needs an `AGENTS.md` file for routing awareness:
493
475
 
@@ -1405,14 +1387,13 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
1405
1387
 
1406
1388
  ### Lifecycle environment variables
1407
1389
 
1408
- Two runtime knobs control how MCP server processes self-manage. Defaults are conservative after [#592](https://github.com/mksglu/context-mode/issues/592): idle self-shutdown is disabled unless a host config explicitly opts in. OpenCode and KiloCode opt in because they open one MCP child per session/subagent; Claude Code/Codex/editor hosts keep registered tool handles after a clean MCP exit and therefore must not idle-exit by default.
1390
+ One runtime knob controls MCP sibling cleanup. Idle self-shutdown was removed after [#592](https://github.com/mksglu/context-mode/issues/592): hosts can keep registered tool handles after a clean MCP exit, making a timer-driven exit unsafe.
1409
1391
 
1410
1392
  | Variable | Default | Purpose |
1411
1393
  |---|---|---|
1412
- | `CONTEXT_MODE_IDLE_TIMEOUT_MS` | `0` (disabled) | When set to a positive integer, an MCP child self-exits cleanly after this many milliseconds of stdin/request inactivity. OpenCode and KiloCode configs set `900000` (15 min) because those hosts can accumulate one MCP child per session/subagent. Leave disabled for hosts that do not auto-respawn after MCP EOF (Claude Code, Codex, editor MCP clients) or ctx_* tools may go stale after idle. |
1413
1394
  | `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). |
1414
1395
 
1415
- 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/non-numeric `CONTEXT_MODE_IDLE_TIMEOUT_MS` values fall back to `0` (disabled); unrecognized `CONTEXT_MODE_STARTUP_SWEEP` values fall back to enabled.
1396
+ `CONTEXT_MODE_STARTUP_SWEEP` is 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. Unrecognized values fall back to enabled.
1416
1397
 
1417
1398
  ### Routing-guidance environment variables
1418
1399
 
@@ -402,7 +402,7 @@ export class CodexAdapter extends BaseAdapter {
402
402
  }]);
403
403
  }
404
404
  const expected = this.generateHookConfig("");
405
- return results.concat(Object.entries(expected).map(([hookName, entries]) => {
405
+ const hookChecks = Object.entries(expected).map(([hookName, entries]) => {
406
406
  const actualEntries = hookConfig.config.hooks?.[hookName];
407
407
  const expectedEntry = entries[0];
408
408
  const ok = Array.isArray(actualEntries)
@@ -410,7 +410,7 @@ export class CodexAdapter extends BaseAdapter {
410
410
  const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
411
411
  return {
412
412
  check: `${hookName} hook`,
413
- status: ok ? "pass" : missingStatus,
413
+ status: (ok ? "pass" : missingStatus),
414
414
  message: ok
415
415
  ? `${hookName} hook configured in ${this.getHooksPath()}`
416
416
  : hookName === "PreCompact"
@@ -418,7 +418,28 @@ export class CodexAdapter extends BaseAdapter {
418
418
  : `${hookName} hook missing or not pointing to context-mode`,
419
419
  fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
420
420
  };
421
- }));
421
+ });
422
+ // #603: surface duplicate context-mode entries per hook event. Codex fires
423
+ // every matching entry, so duplicates double the work, can saturate the
424
+ // MCP transport (`Transport closed`), and have been observed to inflate
425
+ // codex-tui.log into the multi-GB range. `context-mode upgrade` collapses
426
+ // them via `upsertManagedHookEntry`, so the fix is one command away.
427
+ const duplicateChecks = [];
428
+ for (const hookName of Object.keys(expected)) {
429
+ const actualEntries = hookConfig.config.hooks?.[hookName];
430
+ if (!Array.isArray(actualEntries))
431
+ continue;
432
+ const managedCount = actualEntries.filter((entry) => this.isManagedContextModeEntry(hookName, entry)).length;
433
+ if (managedCount > 1) {
434
+ duplicateChecks.push({
435
+ check: `${hookName} duplicates`,
436
+ status: "warn",
437
+ message: `${managedCount} context-mode entries found for ${hookName} in ${this.getHooksPath()}; Codex will fire all of them`,
438
+ fix: "context-mode upgrade (collapses duplicate context-mode entries; preserves unrelated hooks)",
439
+ });
440
+ }
441
+ }
442
+ return results.concat(hookChecks, duplicateChecks);
422
443
  }
423
444
  checkPluginRegistration() {
424
445
  // Check for context-mode in [mcp_servers] section of config.toml
@@ -58,6 +58,7 @@ export declare class OpenCodeAdapter extends BaseAdapter implements HookAdapter
58
58
  * Check whether a settings object has the context-mode plugin registered.
59
59
  */
60
60
  private hasContextModePlugin;
61
+ private hasLegacyContextModeMcp;
61
62
  /**
62
63
  * Extract session ID from OpenCode hook input.
63
64
  * OpenCode uses camelCase sessionID.
@@ -310,6 +310,14 @@ export class OpenCodeAdapter extends BaseAdapter {
310
310
  fix: "context-mode upgrade",
311
311
  });
312
312
  }
313
+ if (this.hasLegacyContextModeMcp(settings)) {
314
+ results.push({
315
+ check: "Legacy MCP registration",
316
+ status: "warn",
317
+ message: "mcp.context-mode is redundant: ctx_* tools are now provided by the plugin",
318
+ fix: "context-mode upgrade (removes only mcp.context-mode; preserves other MCP servers)",
319
+ });
320
+ }
313
321
  // Note: SessionStart handled via experimental.chat.system.transform surrogate
314
322
  results.push({
315
323
  check: "SessionStart hook",
@@ -368,6 +376,16 @@ export class OpenCodeAdapter extends BaseAdapter {
368
376
  changes.push("context-mode already in plugin array");
369
377
  }
370
378
  settings.plugin = plugins;
379
+ const mcp = settings.mcp;
380
+ if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
381
+ const servers = mcp;
382
+ if (Object.prototype.hasOwnProperty.call(servers, "context-mode")) {
383
+ delete servers["context-mode"];
384
+ changes.push("Removed legacy context-mode MCP block (plugin-native tools)");
385
+ }
386
+ if (Object.keys(servers).length === 0)
387
+ delete settings.mcp;
388
+ }
371
389
  this.writeSettings(settings);
372
390
  return changes;
373
391
  }
@@ -405,6 +423,13 @@ export class OpenCodeAdapter extends BaseAdapter {
405
423
  const plugins = settings.plugin;
406
424
  return Array.isArray(plugins) && plugins.some((p) => typeof p === "string" && p.includes("context-mode"));
407
425
  }
426
+ hasLegacyContextModeMcp(settings) {
427
+ const mcp = settings.mcp;
428
+ return !!(mcp &&
429
+ typeof mcp === "object" &&
430
+ !Array.isArray(mcp) &&
431
+ Object.prototype.hasOwnProperty.call(mcp, "context-mode"));
432
+ }
408
433
  /**
409
434
  * Extract session ID from OpenCode hook input.
410
435
  * OpenCode uses camelCase sessionID.
@@ -44,6 +44,27 @@ type PluginContext = {
44
44
  client: PluginClient;
45
45
  directory: string;
46
46
  };
47
+ type NativeToolContext = {
48
+ sessionID: string;
49
+ messageID: string;
50
+ agent: string;
51
+ directory: string;
52
+ worktree?: string;
53
+ abort?: AbortSignal;
54
+ metadata?: (input: {
55
+ title?: string;
56
+ metadata?: Record<string, unknown>;
57
+ }) => void;
58
+ };
59
+ type NativeToolDefinition = {
60
+ description: string;
61
+ args: Record<string, unknown>;
62
+ execute: (args: Record<string, unknown>, ctx: NativeToolContext) => Promise<string | {
63
+ title?: string;
64
+ output: string;
65
+ metadata?: Record<string, unknown>;
66
+ }>;
67
+ };
47
68
  /** OpenCode tool.execute.before — first parameter */
48
69
  interface BeforeHookInput {
49
70
  tool: string;
@@ -130,6 +151,7 @@ declare function systemHasRoutingInstructions(system: string[]): boolean;
130
151
  * OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
131
152
  */
132
153
  declare function createContextModePlugin(ctx: PluginContext): Promise<{
154
+ tool: Record<string, NativeToolDefinition>;
133
155
  "tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
134
156
  "tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
135
157
  "chat.message": (input: ChatMessageHookInput, output: ChatMessageHookOutput) => Promise<void>;
@@ -231,7 +231,59 @@ async function createContextModePlugin(ctx) {
231
231
  // Never break the turn on debug-log failure.
232
232
  }
233
233
  }
234
+ async function buildNativeTools() {
235
+ // Import the existing MCP server registry without starting its stdio
236
+ // transport. This is the plugin-only bridge for #574: OpenCode/Kilo
237
+ // call ctx_* tools in-process through Hooks.tool instead of spawning
238
+ // a separate MCP child per session.
239
+ const prevEmbedded = process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS;
240
+ process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS = "1";
241
+ let mod;
242
+ try {
243
+ mod = await import("../../server.js");
244
+ }
245
+ finally {
246
+ if (prevEmbedded === undefined)
247
+ delete process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS;
248
+ else
249
+ process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS = prevEmbedded;
250
+ }
251
+ const tools = {};
252
+ for (const registered of mod.REGISTERED_CTX_TOOLS) {
253
+ const config = registered.config;
254
+ const schema = config.inputSchema;
255
+ const shape = typeof schema?.shape === "object" && schema.shape !== null
256
+ ? schema.shape
257
+ : typeof schema?._def?.shape === "function"
258
+ ? schema._def.shape()
259
+ : {};
260
+ tools[registered.name] = {
261
+ description: String(config.description ?? ""),
262
+ args: shape,
263
+ async execute(args, toolCtx) {
264
+ toolCtx.metadata?.({ title: String(config.title ?? registered.name) });
265
+ const project = toolCtx.directory || projectDir;
266
+ const result = await mod.withProjectDirOverride({ projectDir: project, sessionId: toolCtx.sessionID }, async () => registered.handler(args ?? {}));
267
+ const r = result;
268
+ const text = Array.isArray(r?.content)
269
+ ? r.content
270
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
271
+ .map((c) => c.text)
272
+ .join("\n")
273
+ : typeof result === "string"
274
+ ? result
275
+ : JSON.stringify(result ?? "");
276
+ if (r?.isError)
277
+ throw new Error(text || `${registered.name} returned an error`);
278
+ return { title: String(config.title ?? registered.name), output: text };
279
+ },
280
+ };
281
+ }
282
+ return tools;
283
+ }
284
+ const nativeTools = await buildNativeTools();
234
285
  return {
286
+ tool: nativeTools,
235
287
  // ── PreToolUse: Routing enforcement ─────────────────
236
288
  "tool.execute.before": async (input, output) => {
237
289
  const toolName = input.tool ?? "";
@@ -288,6 +288,11 @@ export default function piExtension(pi) {
288
288
  pwd: process.env.PWD,
289
289
  cwd: process.cwd(),
290
290
  });
291
+ // Attribution object for project isolation — ensures every event recorded
292
+ // by the pi adapter carries the correct project_dir. Without this, all
293
+ // events default to project_dir="" which causes cross-project data leakage
294
+ // in shared SessionDB instances.
295
+ const _attribution = { projectDir, source: "workspace_root", confidence: 0.98 };
291
296
  const db = getOrCreateDB();
292
297
  // ── 1. session_start — Initialize session ──────────────
293
298
  pi.on("session_start", (_event, ctx) => {
@@ -351,7 +356,7 @@ export default function piExtension(pi) {
351
356
  const events = extractEvents(hookInput);
352
357
  if (events.length > 0) {
353
358
  for (const ev of events) {
354
- db.insertEvent(_sessionId, ev, "PostToolUse");
359
+ db.insertEvent(_sessionId, ev, "PostToolUse", _attribution);
355
360
  }
356
361
  }
357
362
  else if (rawToolName) {
@@ -369,7 +374,7 @@ export default function piExtension(pi) {
369
374
  .update(data)
370
375
  .digest("hex")
371
376
  .slice(0, 16),
372
- }, "PostToolUse");
377
+ }, "PostToolUse", _attribution);
373
378
  }
374
379
  }
375
380
  catch {
@@ -398,7 +403,7 @@ export default function piExtension(pi) {
398
403
  if (prompt) {
399
404
  const userEvents = extractUserEvents(prompt);
400
405
  for (const ev of userEvents) {
401
- db.insertEvent(_sessionId, ev, "UserPromptSubmit");
406
+ db.insertEvent(_sessionId, ev, "UserPromptSubmit", _attribution);
402
407
  }
403
408
  }
404
409
  const existingPrompt = String(event?.systemPrompt ?? "");
@@ -420,6 +425,7 @@ export default function piExtension(pi) {
420
425
  minPriority: 3,
421
426
  limit: 50,
422
427
  });
428
+ let behavioralDirective = "";
423
429
  if (activeEvents.length > 0) {
424
430
  const buildAuto = await getAutoInjection(pluginRoot);
425
431
  let memoryContext = "";
@@ -428,6 +434,14 @@ export default function piExtension(pi) {
428
434
  category: String(e.category ?? ""),
429
435
  data: String(e.data ?? ""),
430
436
  })));
437
+ const bdMatch = memoryContext.match(/(<behavioral_directive>\n[^<]*\n<\/behavioral_directive>)/);
438
+ if (bdMatch) {
439
+ behavioralDirective = bdMatch[1];
440
+ memoryContext = memoryContext.replace(bdMatch[1], "");
441
+ if (memoryContext.match(/^<session_state[^>]*>\s*<\/session_state>\s*$/)) {
442
+ memoryContext = "";
443
+ }
444
+ }
431
445
  }
432
446
  // Fallback (or if helper produced empty output): inline 500-token cap.
433
447
  if (!memoryContext) {
@@ -453,6 +467,8 @@ export default function piExtension(pi) {
453
467
  parts.push(resume.snapshot);
454
468
  db.markResumeConsumed(_sessionId);
455
469
  }
470
+ if (behavioralDirective)
471
+ parts.push(behavioralDirective);
456
472
  // Return modified systemPrompt only if we added something beyond existing.
457
473
  const baseLen = existingPrompt ? 1 : 0;
458
474
  if (parts.length > baseLen) {
@@ -491,7 +507,7 @@ export default function piExtension(pi) {
491
507
  data,
492
508
  priority: 1,
493
509
  data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
494
- }, "PostToolUse");
510
+ }, "PostToolUse", _attribution);
495
511
  }
496
512
  catch {
497
513
  // best effort — never break provider response
@@ -97,12 +97,13 @@ export declare class MCPStdioClient {
97
97
  private onExit;
98
98
  private onData;
99
99
  request<T = unknown>(method: string, params: unknown, timeoutMs?: number): Promise<T>;
100
+ private writeFrame;
100
101
  notify(method: string, params: unknown): void;
101
102
  initialize(): Promise<void>;
102
103
  listTools(): Promise<MCPTool[]>;
103
104
  callTool(name: string, args: unknown): Promise<MCPCallResult>;
104
105
  /**
105
- * Respawn the MCP child after an exit (clean idle shutdown or crash).
106
+ * Respawn the MCP child after an exit (clean shutdown or crash).
106
107
  * Resets state so a fresh `start()` + `initialize()` cycle runs, then
107
108
  * the caller's pending request flows through the new child.
108
109
  *
@@ -366,14 +366,60 @@ export class MCPStdioClient {
366
366
  },
367
367
  });
368
368
  const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params });
369
- this.child.stdin?.write(frame + "\n");
369
+ const rejectWrite = (err) => {
370
+ const handler = this.pending.get(id);
371
+ if (handler) {
372
+ this.pending.delete(id);
373
+ handler.reject(err);
374
+ return;
375
+ }
376
+ reject(err);
377
+ };
378
+ this.writeFrame(frame, rejectWrite);
370
379
  });
371
380
  }
381
+ writeFrame(frame, onError) {
382
+ if (!this.child || this.exited) {
383
+ onError?.(new Error("MCP server exited"));
384
+ return false;
385
+ }
386
+ const stdin = this.child.stdin;
387
+ if (!stdin || stdin.destroyed || stdin.writableEnded || stdin.closed) {
388
+ this.onExit();
389
+ onError?.(new Error("MCP server stdin unavailable"));
390
+ return false;
391
+ }
392
+ try {
393
+ stdin.write(frame + "\n", (err) => {
394
+ if (!err)
395
+ return;
396
+ const code = err.code;
397
+ if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
398
+ this.onExit();
399
+ onError?.(err);
400
+ return;
401
+ }
402
+ onError?.(err);
403
+ });
404
+ return true;
405
+ }
406
+ catch (err) {
407
+ const code = err && typeof err === "object" && "code" in err
408
+ ? err.code
409
+ : undefined;
410
+ if (err instanceof Error && (code === "EPIPE" || code === "ERR_STREAM_DESTROYED")) {
411
+ this.onExit();
412
+ onError?.(err);
413
+ return false;
414
+ }
415
+ throw err;
416
+ }
417
+ }
372
418
  notify(method, params) {
373
419
  if (!this.child)
374
420
  return;
375
421
  const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
376
- this.child.stdin?.write(frame + "\n");
422
+ this.writeFrame(frame);
377
423
  }
378
424
  async initialize() {
379
425
  if (this.initialized)
@@ -402,7 +448,7 @@ export class MCPStdioClient {
402
448
  return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
403
449
  }
404
450
  /**
405
- * Respawn the MCP child after an exit (clean idle shutdown or crash).
451
+ * Respawn the MCP child after an exit (clean shutdown or crash).
406
452
  * Resets state so a fresh `start()` + `initialize()` cycle runs, then
407
453
  * the caller's pending request flows through the new child.
408
454
  *
@@ -20,54 +20,7 @@ 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 0 (disabled).
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;
35
23
  }
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
- * Idle shutdown is OFF by default (#592) because most hosts (Claude
54
- * Code, Codex, editor MCP clients) keep registered tool handles after a
55
- * clean MCP child exit and do NOT transparently respawn on the next call.
56
- * The global 15 min default introduced in #568 solved OpenCode's child
57
- * accumulation, but stranded ctx_* tools in Claude Code/Codex-style
58
- * hosts once the MCP server exited cleanly while the editor stayed alive.
59
- *
60
- * Hosts that are known to benefit from idle shutdown MUST opt in via
61
- * CONTEXT_MODE_IDLE_TIMEOUT_MS in their MCP config. Today that is
62
- * OpenCode/KiloCode (their configs set 900000 = 15 min). Users and test
63
- * harnesses can also opt in explicitly with any positive integer.
64
- *
65
- * Missing or malformed env = 0 (disabled, safe default). Set env to
66
- * `0` to disable explicitly.
67
- *
68
- * Exported for unit-testing.
69
- */
70
- export declare function idleTimeoutForEnv(env?: NodeJS.ProcessEnv): number;
71
24
  /** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
72
25
  export interface IsParentAliveDeps {
73
26
  /** Read the current ppid. Default: `() => process.ppid`. */
@@ -107,9 +60,7 @@ export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () =
107
60
  */
108
61
  export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
109
62
  /**
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
- *
63
+ * Start the lifecycle guard. Returns a cleanup function.
113
64
  * Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
114
65
  */
115
- export declare function startLifecycleGuard(opts: LifecycleGuardOptions): LifecycleGuardHandle;
66
+ export declare function startLifecycleGuard(opts: LifecycleGuardOptions): () => void;