context-mode 1.0.135 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +9 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +53 -24
- package/build/adapters/codex/index.js +24 -3
- package/build/adapters/opencode/index.d.ts +1 -0
- package/build/adapters/opencode/index.js +25 -0
- package/build/adapters/opencode/plugin.d.ts +22 -0
- package/build/adapters/opencode/plugin.js +52 -0
- package/build/adapters/pi/extension.js +20 -4
- package/build/adapters/pi/mcp-bridge.d.ts +39 -2
- package/build/adapters/pi/mcp-bridge.js +184 -24
- package/build/lifecycle.d.ts +2 -51
- package/build/lifecycle.js +3 -67
- package/build/server.d.ts +19 -0
- package/build/server.js +141 -58
- package/build/session/db.d.ts +6 -0
- package/build/session/db.js +17 -3
- package/build/session/extract.js +39 -1
- package/build/util/sibling-mcp.d.ts +0 -40
- package/build/util/sibling-mcp.js +11 -116
- package/cli.bundle.mjs +131 -129
- package/configs/kilo/kilo.json +3 -7
- package/configs/opencode/opencode.json +3 -7
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +1 -0
- package/hooks/codex/precompact.mjs +1 -0
- package/hooks/codex/pretooluse.mjs +1 -0
- package/hooks/codex/sessionstart.mjs +1 -0
- package/hooks/codex/stop.mjs +1 -0
- package/hooks/codex/userpromptsubmit.mjs +1 -0
- package/hooks/core/routing.mjs +112 -10
- package/hooks/ensure-deps.mjs +14 -3
- package/hooks/normalize-hooks.mjs +101 -19
- package/hooks/session-db.bundle.mjs +3 -3
- package/hooks/session-extract.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +112 -110
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -372
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/util/db-lock.d.ts +0 -65
- package/build/util/db-lock.js +0 -166
|
@@ -96,6 +96,87 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
|
96
96
|
// Tools/call may run shell commands or fetch URLs — wider window than
|
|
97
97
|
// initialize/list, but still bounded so a hung server can't block Pi.
|
|
98
98
|
const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
99
|
+
class PiTextComponent {
|
|
100
|
+
text;
|
|
101
|
+
constructor(text = "") {
|
|
102
|
+
this.text = text;
|
|
103
|
+
}
|
|
104
|
+
setText(text) {
|
|
105
|
+
this.text = text;
|
|
106
|
+
}
|
|
107
|
+
invalidate() {
|
|
108
|
+
// Stateless renderer: no cached layout to invalidate.
|
|
109
|
+
}
|
|
110
|
+
render(width) {
|
|
111
|
+
if (!this.text || this.text.trim() === "")
|
|
112
|
+
return [];
|
|
113
|
+
return this.text
|
|
114
|
+
.replace(/\t/g, " ")
|
|
115
|
+
.split(/\r?\n/)
|
|
116
|
+
.map((line) => truncateAnsiLine(line, Math.max(1, width)));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
120
|
+
function truncateAnsiLine(line, maxWidth) {
|
|
121
|
+
if (maxWidth <= 0)
|
|
122
|
+
return "";
|
|
123
|
+
let output = "";
|
|
124
|
+
let visible = 0;
|
|
125
|
+
let index = 0;
|
|
126
|
+
ANSI_PATTERN.lastIndex = 0;
|
|
127
|
+
for (;;) {
|
|
128
|
+
const match = ANSI_PATTERN.exec(line);
|
|
129
|
+
const end = match?.index ?? line.length;
|
|
130
|
+
const chunk = line.slice(index, end);
|
|
131
|
+
for (const char of chunk) {
|
|
132
|
+
if (visible >= maxWidth)
|
|
133
|
+
return output;
|
|
134
|
+
output += char;
|
|
135
|
+
visible++;
|
|
136
|
+
}
|
|
137
|
+
if (!match)
|
|
138
|
+
return output;
|
|
139
|
+
output += match[0];
|
|
140
|
+
index = ANSI_PATTERN.lastIndex;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function createContextModeCallRenderer(toolName) {
|
|
144
|
+
return (_args, theme, context) => {
|
|
145
|
+
const text = context.lastComponent instanceof PiTextComponent
|
|
146
|
+
? context.lastComponent
|
|
147
|
+
: new PiTextComponent();
|
|
148
|
+
text.setText(theme.fg("toolTitle", theme.bold(toolName)));
|
|
149
|
+
return text;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function createContextModeResultRenderer(toolName) {
|
|
153
|
+
return (result, { expanded, isPartial }, theme, context) => {
|
|
154
|
+
const text = context.lastComponent instanceof PiTextComponent
|
|
155
|
+
? context.lastComponent
|
|
156
|
+
: new PiTextComponent();
|
|
157
|
+
if (isPartial) {
|
|
158
|
+
text.setText(theme.fg("warning", "indexing/searching..."));
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
const output = (result.content ?? [])
|
|
162
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
163
|
+
.map((c) => c.text)
|
|
164
|
+
.join("\n");
|
|
165
|
+
if (expanded) {
|
|
166
|
+
text.setText(theme.fg("toolOutput", output));
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
const firstLine = output
|
|
170
|
+
.split(/\r?\n/)
|
|
171
|
+
.find((line) => line.trim().length > 0)
|
|
172
|
+
?.trim();
|
|
173
|
+
const status = firstLine && firstLine.length <= 180
|
|
174
|
+
? firstLine
|
|
175
|
+
: `${toolName} completed`;
|
|
176
|
+
text.setText(theme.fg("toolOutput", status));
|
|
177
|
+
return text;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
99
180
|
/**
|
|
100
181
|
* Minimal stdio JSON-RPC client targeting the context-mode MCP server.
|
|
101
182
|
*
|
|
@@ -118,6 +199,15 @@ export class MCPStdioClient {
|
|
|
118
199
|
buffer = "";
|
|
119
200
|
initialized = false;
|
|
120
201
|
exited = false;
|
|
202
|
+
/**
|
|
203
|
+
* In-flight respawn promise — set while {@link respawn} runs so
|
|
204
|
+
* concurrent callers awaiting `request()` after an idle exit observe
|
|
205
|
+
* the SAME respawn, not N parallel ones. Without this guard, two
|
|
206
|
+
* simultaneous `callTool` calls would each see `this.exited === true`,
|
|
207
|
+
* each fire their own `respawn()`, and the loser leaks an orphaned
|
|
208
|
+
* child process the GC cannot reach (no `.kill()` reference).
|
|
209
|
+
*/
|
|
210
|
+
respawnPromise = null;
|
|
121
211
|
/**
|
|
122
212
|
* Live env passed to the spawned child — exposed (read-only intent)
|
|
123
213
|
* so tests can pin the fork-bomb-prevention env counter (#516)
|
|
@@ -232,11 +322,31 @@ export class MCPStdioClient {
|
|
|
232
322
|
handler.resolve(msg.result);
|
|
233
323
|
}
|
|
234
324
|
}
|
|
235
|
-
request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
325
|
+
async request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
326
|
+
// Respawn-on-idle-exit (#583, #583-followup).
|
|
327
|
+
//
|
|
328
|
+
// Initial #583 fix patched callTool() only. The structural location is
|
|
329
|
+
// here: `request()` is the single chokepoint for `initialize`,
|
|
330
|
+
// `tools/list`, `tools/call`, and any future method. Patching at this
|
|
331
|
+
// layer means listTools / re-initialize paths after an idle exit also
|
|
332
|
+
// self-heal, not just the registered-tool happy path.
|
|
333
|
+
//
|
|
334
|
+
// Sequencing is critical: respawn() resets `exited`, `child`, and
|
|
335
|
+
// `buffer` BEFORE start() + initialize(). The initialize() call inside
|
|
336
|
+
// respawn() goes through this same request() — recursion is safe
|
|
337
|
+
// because by the time we re-enter, `exited` is false again. We use a
|
|
338
|
+
// single-flight `respawnPromise` so concurrent callers share the same
|
|
339
|
+
// respawn (orphan-child guard, see field comment).
|
|
340
|
+
if (this.exited) {
|
|
341
|
+
if (!this.respawnPromise) {
|
|
342
|
+
this.respawnPromise = this.respawn().finally(() => {
|
|
343
|
+
this.respawnPromise = null;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
await this.respawnPromise;
|
|
347
|
+
}
|
|
236
348
|
if (!this.child)
|
|
237
349
|
throw new Error("MCP client not started");
|
|
238
|
-
if (this.exited)
|
|
239
|
-
return Promise.reject(new Error("MCP server has exited"));
|
|
240
350
|
const id = ++this.requestId;
|
|
241
351
|
return new Promise((resolve, reject) => {
|
|
242
352
|
const timer = setTimeout(() => {
|
|
@@ -256,14 +366,60 @@ export class MCPStdioClient {
|
|
|
256
366
|
},
|
|
257
367
|
});
|
|
258
368
|
const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
259
|
-
|
|
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);
|
|
260
379
|
});
|
|
261
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
|
+
}
|
|
262
418
|
notify(method, params) {
|
|
263
419
|
if (!this.child)
|
|
264
420
|
return;
|
|
265
421
|
const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
266
|
-
this.
|
|
422
|
+
this.writeFrame(frame);
|
|
267
423
|
}
|
|
268
424
|
async initialize() {
|
|
269
425
|
if (this.initialized)
|
|
@@ -284,33 +440,35 @@ export class MCPStdioClient {
|
|
|
284
440
|
return Array.isArray(result.tools) ? result.tools : [];
|
|
285
441
|
}
|
|
286
442
|
async callTool(name, args) {
|
|
287
|
-
// Respawn-on-idle-exit
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// "MCP server has exited" — leaving Pi's ctx_* tools permanently
|
|
293
|
-
// broken until the user restarts Pi.
|
|
294
|
-
//
|
|
295
|
-
// The structural fix is here, not in lifecycle.ts: the bridge owns
|
|
296
|
-
// the child lifecycle, so it transparently respawns + re-initialises
|
|
297
|
-
// the server on the next call. Restores parity with adapters whose
|
|
298
|
-
// host MCP client respawns on EOF (Claude Code, Codex, etc.).
|
|
299
|
-
if (this.exited)
|
|
300
|
-
await this.respawn();
|
|
443
|
+
// Respawn-on-idle-exit is now handled centrally in `request()`
|
|
444
|
+
// (#583 follow-up). Originally patched here in #583 — moving it up
|
|
445
|
+
// one layer covers `listTools` / `initialize` paths too, with a
|
|
446
|
+
// single-flight guard against orphan child processes from
|
|
447
|
+
// concurrent callers.
|
|
301
448
|
return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
|
|
302
449
|
}
|
|
303
450
|
/**
|
|
304
|
-
* Respawn the MCP child after an exit (clean
|
|
451
|
+
* Respawn the MCP child after an exit (clean shutdown or crash).
|
|
305
452
|
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
306
453
|
* the caller's pending request flows through the new child.
|
|
307
454
|
*
|
|
308
|
-
*
|
|
455
|
+
* Single-flight — concurrent callers share one in-flight respawn via
|
|
456
|
+
* {@link respawnPromise}. Internal — only entered via {@link request}.
|
|
457
|
+
*
|
|
458
|
+
* Sequencing pinned (do not reorder without updating the regression
|
|
459
|
+
* test in tests/adapters/pi-mcp-bridge.test.ts):
|
|
460
|
+
* 1. `this.child = null` — drop stale handle
|
|
461
|
+
* 2. `this.buffer = ""` — discard leftover bytes from old child
|
|
462
|
+
* 3. `this.exited = false` — must precede `start()` + `initialize()`,
|
|
463
|
+
* because `request("initialize", …)`
|
|
464
|
+
* inside `initialize()` re-checks this
|
|
465
|
+
* flag and would otherwise re-enter
|
|
466
|
+
* respawn in an infinite loop
|
|
467
|
+
* 4. `this.initialized = false`
|
|
468
|
+
* 5. `this.start()`
|
|
469
|
+
* 6. `await this.initialize()` — flows through `request()` recursively
|
|
309
470
|
*/
|
|
310
471
|
async respawn() {
|
|
311
|
-
// Drop the dead child handle and clear stream buffer so leftover
|
|
312
|
-
// bytes from the previous incarnation don't get parsed as JSON-RPC
|
|
313
|
-
// for the new one. Pending map is already cleared by onExit().
|
|
314
472
|
this.child = null;
|
|
315
473
|
this.buffer = "";
|
|
316
474
|
this.exited = false;
|
|
@@ -398,6 +556,8 @@ export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
|
398
556
|
// for type inference). Empty-object fallback keeps tools that
|
|
399
557
|
// declare no parameters callable.
|
|
400
558
|
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
559
|
+
renderCall: createContextModeCallRenderer(tool.name),
|
|
560
|
+
renderResult: createContextModeResultRenderer(tool.name),
|
|
401
561
|
async execute(_toolCallId, params) {
|
|
402
562
|
const result = await client.callTool(tool.name, params ?? {});
|
|
403
563
|
const text = (result.content ?? [])
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -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 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;
|
|
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
|
-
* 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;
|
|
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
|
|
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):
|
|
66
|
+
export declare function startLifecycleGuard(opts: LifecycleGuardOptions): () => void;
|
package/build/lifecycle.js
CHANGED
|
@@ -14,35 +14,6 @@
|
|
|
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
|
-
}
|
|
46
17
|
/** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
|
|
47
18
|
function readGrandparentPpidImpl() {
|
|
48
19
|
if (process.platform === "win32")
|
|
@@ -124,52 +95,25 @@ export function lifecycleGuardIntervalForEnv(env = process.env) {
|
|
|
124
95
|
return 1000;
|
|
125
96
|
}
|
|
126
97
|
/**
|
|
127
|
-
* Start the lifecycle guard. Returns a
|
|
128
|
-
* on every MCP request to keep idle timer from firing) and `stop`.
|
|
129
|
-
*
|
|
98
|
+
* Start the lifecycle guard. Returns a cleanup function.
|
|
130
99
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
131
100
|
*/
|
|
132
101
|
export function startLifecycleGuard(opts) {
|
|
133
102
|
const interval = opts.checkIntervalMs ?? lifecycleGuardIntervalForEnv();
|
|
134
103
|
const check = opts.isParentAlive ?? defaultIsParentAlive;
|
|
135
|
-
const idleTimeoutMs = opts.idleTimeoutMs ?? idleTimeoutForEnv();
|
|
136
|
-
const now = opts.now ?? Date.now;
|
|
137
104
|
let stopped = false;
|
|
138
|
-
let lastActivity = now();
|
|
139
105
|
const shutdown = () => {
|
|
140
106
|
if (stopped)
|
|
141
107
|
return;
|
|
142
108
|
stopped = true;
|
|
143
109
|
opts.onShutdown();
|
|
144
110
|
};
|
|
145
|
-
|
|
146
|
-
lastActivity = now();
|
|
147
|
-
};
|
|
148
|
-
// P0: Periodic parent liveness check.
|
|
111
|
+
// P0: Periodic parent liveness check
|
|
149
112
|
const timer = setInterval(() => {
|
|
150
113
|
if (!check())
|
|
151
114
|
shutdown();
|
|
152
115
|
}, interval);
|
|
153
116
|
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
|
-
}
|
|
173
117
|
// P0: OS signals — terminal close, kill, ctrl+c
|
|
174
118
|
const signals = ["SIGTERM", "SIGINT"];
|
|
175
119
|
if (process.platform !== "win32")
|
|
@@ -198,19 +142,11 @@ export function startLifecycleGuard(opts) {
|
|
|
198
142
|
if (!process.stdin.isTTY) {
|
|
199
143
|
process.stdin.on("end", onStdinEnd);
|
|
200
144
|
}
|
|
201
|
-
|
|
145
|
+
return () => {
|
|
202
146
|
stopped = true;
|
|
203
147
|
clearInterval(timer);
|
|
204
|
-
if (idleTimer)
|
|
205
|
-
clearInterval(idleTimer);
|
|
206
148
|
for (const sig of signals)
|
|
207
149
|
process.removeListener(sig, shutdown);
|
|
208
150
|
process.stdin.removeListener("end", onStdinEnd);
|
|
209
151
|
};
|
|
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;
|
|
216
152
|
}
|
package/build/server.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
|
|
3
4
|
import { ContentStore } from "./store.js";
|
|
5
|
+
import { type PlatformId } from "./adapters/types.js";
|
|
6
|
+
export declare const server: McpServer;
|
|
7
|
+
export interface RegisteredCtxTool {
|
|
8
|
+
name: string;
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare const REGISTERED_CTX_TOOLS: RegisteredCtxTool[];
|
|
13
|
+
export declare function shouldSuppressMcpToolsForNativePluginHost(opts?: {
|
|
14
|
+
embedded?: string;
|
|
15
|
+
platform?: PlatformId;
|
|
16
|
+
settings?: Record<string, unknown> | null;
|
|
17
|
+
}): boolean;
|
|
18
|
+
type ToolContextOverride = {
|
|
19
|
+
projectDir: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function withProjectDirOverride<T>(projectDir: string | ToolContextOverride, fn: () => Promise<T>): Promise<T>;
|
|
4
23
|
/**
|
|
5
24
|
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
6
25
|
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|