context-mode 1.0.164 → 1.0.166
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/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/pi/extension.js +27 -9
- package/build/adapters/pi/mcp-bridge.d.ts +78 -1
- package/build/adapters/pi/mcp-bridge.js +105 -17
- package/build/lifecycle.d.ts +10 -0
- package/build/lifecycle.js +16 -1
- package/cli.bundle.mjs +53 -53
- package/configs/antigravity-cli/plugin.json +1 -1
- package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +44 -44
- package/start.mjs +87 -15
|
@@ -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.166"
|
|
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.166",
|
|
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.166",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.166",
|
|
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.166",
|
|
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.166",
|
|
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",
|
|
@@ -18,7 +18,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
18
18
|
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
|
|
19
19
|
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
20
20
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
21
|
-
import { bootstrapMCPTools } from "./mcp-bridge.js";
|
|
21
|
+
import { bootstrapMCPTools, makeBridgeDiag, isForegroundSession } from "./mcp-bridge.js";
|
|
22
22
|
import { PiAdapter } from "./index.js";
|
|
23
23
|
// ── Pi Tool Name Mapping ─────────────────────────────────
|
|
24
24
|
// Pi uses lowercase; shared extractors expect PascalCase (Claude Code convention).
|
|
@@ -298,9 +298,9 @@ function handleCommandText(text, ctx) {
|
|
|
298
298
|
// print-mode subagents (`pi --mode json -p --no-session`). We start and await
|
|
299
299
|
// the bridge from that hook so ctx_* tools are present before Pi snapshots the
|
|
300
300
|
// tool registry, while CLI-only commands never spawn a bridge at all.
|
|
301
|
-
function startPiMCPBridge(pi, serverBundle, shouldKeepHandle) {
|
|
301
|
+
function startPiMCPBridge(pi, serverBundle, shouldKeepHandle, foreground) {
|
|
302
302
|
if (existsSync(serverBundle)) {
|
|
303
|
-
_mcpBridgeReady = bootstrapMCPTools(pi, serverBundle).then((handle) => {
|
|
303
|
+
_mcpBridgeReady = bootstrapMCPTools(pi, serverBundle, { foreground }).then((handle) => {
|
|
304
304
|
if (shouldKeepHandle()) {
|
|
305
305
|
_mcpBridge = handle;
|
|
306
306
|
}
|
|
@@ -319,8 +319,9 @@ function startPiMCPBridge(pi, serverBundle, shouldKeepHandle) {
|
|
|
319
319
|
if (!shouldKeepHandle())
|
|
320
320
|
return;
|
|
321
321
|
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
// #868: route to Pi's file logger, never process.stderr (raw-mode TUI).
|
|
323
|
+
makeBridgeDiag(pi)(`[context-mode] WARNING: failed to bridge MCP tools to Pi (${msg}). ` +
|
|
324
|
+
`ctx_* tools will not be callable from this session.`);
|
|
324
325
|
});
|
|
325
326
|
}
|
|
326
327
|
else {
|
|
@@ -382,12 +383,12 @@ export default function piExtension(pi) {
|
|
|
382
383
|
const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
|
|
383
384
|
let mcpBridgeStarted = false;
|
|
384
385
|
let mcpBridgeGeneration = 0;
|
|
385
|
-
const ensureMCPBridge = () => {
|
|
386
|
+
const ensureMCPBridge = (foreground) => {
|
|
386
387
|
if (mcpBridgeStarted)
|
|
387
388
|
return _mcpBridgeReady;
|
|
388
389
|
mcpBridgeStarted = true;
|
|
389
390
|
const generation = ++mcpBridgeGeneration;
|
|
390
|
-
return startPiMCPBridge(pi, serverBundle, () => mcpBridgeStarted && mcpBridgeGeneration === generation);
|
|
391
|
+
return startPiMCPBridge(pi, serverBundle, () => mcpBridgeStarted && mcpBridgeGeneration === generation, foreground);
|
|
391
392
|
};
|
|
392
393
|
// Issue #545 — Pi workspace resolver. PI_CONFIG_DIR is Pi's CONFIG dir
|
|
393
394
|
// (~/.pi), NOT the user's workspace; using it as the project anchor
|
|
@@ -527,7 +528,7 @@ export default function piExtension(pi) {
|
|
|
527
528
|
}
|
|
528
529
|
});
|
|
529
530
|
// ── 4. before_agent_start — Routing + active_memory + resume injection ─
|
|
530
|
-
pi.on("before_agent_start", async (event) => {
|
|
531
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
531
532
|
try {
|
|
532
533
|
_pendingContext = ""; // Reset — will be filled below if events exist
|
|
533
534
|
// Lazily start and await the MCP bridge only when Pi is about to
|
|
@@ -538,7 +539,24 @@ export default function piExtension(pi) {
|
|
|
538
539
|
// here ensures ctx_* tools are registered before Pi snapshots the tool
|
|
539
540
|
// registry for the model call. Resolves on bootstrap failure too — the
|
|
540
541
|
// bridge is best-effort.
|
|
541
|
-
|
|
542
|
+
//
|
|
543
|
+
// #868: the FOREGROUND interactive session's bridge child is spawned with
|
|
544
|
+
// the #854 idle reaper disabled (via ctx.hasUI), so a multi-minute human
|
|
545
|
+
// pause never drops its ctx_* tools. Subagents (hasUI:false) keep the
|
|
546
|
+
// reaper so abandoned children can't accumulate (#854).
|
|
547
|
+
//
|
|
548
|
+
// INVARIANT — deciding foreground on the FIRST before_agent_start is safe
|
|
549
|
+
// even though the bridge spawns single-flight (first-wins, no sticky
|
|
550
|
+
// latch): Pi wires the interactive uiContext inside `mode.init()`, which
|
|
551
|
+
// main.ts AWAITS before dispatching the first prompt — and
|
|
552
|
+
// before_agent_start is emitted only from the per-turn prompt path. So the
|
|
553
|
+
// foreground session's first hook ALWAYS observes hasUI:true; subagents are
|
|
554
|
+
// provably hasUI:false. There is no early-init window where the foreground
|
|
555
|
+
// transiently reads hasUI:false (that window is scoped to a separate,
|
|
556
|
+
// buffered credential event). Do NOT add a latch here — it would guard an
|
|
557
|
+
// unreachable state. (Verified against oh-my-pi: main.ts init→prompt order,
|
|
558
|
+
// interactive-mode.ts uiContext wiring, executor.ts subagent hasUI:false.)
|
|
559
|
+
await ensureMCPBridge(isForegroundSession(ctx));
|
|
542
560
|
if (!_sessionId)
|
|
543
561
|
return;
|
|
544
562
|
const prompt = String(event?.prompt ?? "");
|
|
@@ -78,6 +78,12 @@ export declare class MCPStdioClient {
|
|
|
78
78
|
private readonly serverScript;
|
|
79
79
|
private readonly env;
|
|
80
80
|
private readonly runtimeOverride;
|
|
81
|
+
/**
|
|
82
|
+
* TUI-safe sink for the child's forwarded stderr (#868). Defaults to a
|
|
83
|
+
* no-op so direct callers (skippedBridge, tests) never leak to the
|
|
84
|
+
* terminal; bootstrapMCPTools wires this to the Pi host's file logger.
|
|
85
|
+
*/
|
|
86
|
+
private readonly diag;
|
|
81
87
|
private child;
|
|
82
88
|
private requestId;
|
|
83
89
|
private readonly pending;
|
|
@@ -99,7 +105,13 @@ export declare class MCPStdioClient {
|
|
|
99
105
|
* without needing to attach a process-tree probe.
|
|
100
106
|
*/
|
|
101
107
|
_spawnEnv: NodeJS.ProcessEnv | null;
|
|
102
|
-
constructor(serverScript: string, env?: NodeJS.ProcessEnv, runtimeOverride?: string | null
|
|
108
|
+
constructor(serverScript: string, env?: NodeJS.ProcessEnv, runtimeOverride?: string | null,
|
|
109
|
+
/**
|
|
110
|
+
* TUI-safe sink for the child's forwarded stderr (#868). Defaults to a
|
|
111
|
+
* no-op so direct callers (skippedBridge, tests) never leak to the
|
|
112
|
+
* terminal; bootstrapMCPTools wires this to the Pi host's file logger.
|
|
113
|
+
*/
|
|
114
|
+
diag?: BridgeDiag);
|
|
103
115
|
/** Spawn the MCP child. Idempotent. */
|
|
104
116
|
start(): void;
|
|
105
117
|
private onExit;
|
|
@@ -161,7 +173,64 @@ export interface PiToolRegistration {
|
|
|
161
173
|
}
|
|
162
174
|
export interface PiLikeAPI {
|
|
163
175
|
registerTool: (tool: PiToolRegistration) => void;
|
|
176
|
+
/**
|
|
177
|
+
* Pi's rotating file logger (`~/.omp/logs/`). Pi runs a raw-mode TUI that
|
|
178
|
+
* owns the terminal, so an in-process extension MUST NOT write to
|
|
179
|
+
* process.stdout/stderr — any console write is rendered straight into the
|
|
180
|
+
* editor input box and blocks typing (#868, confirmed against
|
|
181
|
+
* oh-my-pi tui/terminal.ts raw-mode + docs/skills/authoring-extensions.md:
|
|
182
|
+
* "nothing is written to the console, which would corrupt the TUI"). All
|
|
183
|
+
* bridge diagnostics route here instead. Optional: absent in tests / minimal
|
|
184
|
+
* hosts, in which case diagnostics are dropped — we NEVER fall back to the
|
|
185
|
+
* terminal.
|
|
186
|
+
*/
|
|
187
|
+
logger?: {
|
|
188
|
+
debug?: (message: string, context?: Record<string, unknown>) => void;
|
|
189
|
+
info?: (message: string, context?: Record<string, unknown>) => void;
|
|
190
|
+
warn?: (message: string, context?: Record<string, unknown>) => void;
|
|
191
|
+
error?: (message: string, context?: Record<string, unknown>) => void;
|
|
192
|
+
};
|
|
164
193
|
}
|
|
194
|
+
/** TUI-safe diagnostics sink: routes to Pi's file logger, never the terminal. */
|
|
195
|
+
export type BridgeDiag = (line: string, level?: "warn" | "debug") => void;
|
|
196
|
+
/**
|
|
197
|
+
* Build a {@link BridgeDiag} bound to a Pi host's file logger (#868). Writing to
|
|
198
|
+
* process.stderr from inside Pi's raw-mode TUI corrupts the editor, so every
|
|
199
|
+
* bridge diagnostic — the forwarded MCP child stderr included — goes to
|
|
200
|
+
* `pi.logger` instead. When no logger is reachable (tests, non-Pi hosts) the
|
|
201
|
+
* line is dropped; we never touch the terminal as a fallback.
|
|
202
|
+
*/
|
|
203
|
+
export declare function makeBridgeDiag(pi: PiLikeAPI | null | undefined): BridgeDiag;
|
|
204
|
+
/**
|
|
205
|
+
* Split a chunk of forwarded child output into lines without a regex (the repo
|
|
206
|
+
* forbids regex in source). Trailing `\r` is stripped so CRLF traces stay clean
|
|
207
|
+
* in the log; the final partial line (no trailing newline) is preserved.
|
|
208
|
+
*/
|
|
209
|
+
export declare function splitDiagLines(text: string): string[];
|
|
210
|
+
/**
|
|
211
|
+
* #868: is this the FOREGROUND interactive Pi session (vs a subagent / print /
|
|
212
|
+
* RPC session)? Pi passes an ExtensionContext as the 2nd arg to
|
|
213
|
+
* `before_agent_start`; `ctx.hasUI === true` only for the interactive session
|
|
214
|
+
* with a real UI attached (refs oh-my-pi runner.ts:330-331), while subagents
|
|
215
|
+
* are provably `hasUI: false` (refs executor.ts:2052). Fail-safe: treat anything
|
|
216
|
+
* that is NOT an explicit `hasUI === false` as foreground, so an
|
|
217
|
+
* ambiguous/absent ctx keeps the session's bridge ALIVE rather than risking the
|
|
218
|
+
* #868 idle-drop. Mis-classifying an abandoned non-interactive child as
|
|
219
|
+
* foreground only costs one lingering child until parent-death/ session_shutdown
|
|
220
|
+
* reaps it; the opposite error re-drops the user's tools mid-session.
|
|
221
|
+
*/
|
|
222
|
+
export declare function isForegroundSession(ctx: unknown): boolean;
|
|
223
|
+
/**
|
|
224
|
+
* #868: derive the bridge child's spawn env for a session kind. The FOREGROUND
|
|
225
|
+
* interactive session's child must never be idle-reaped — a multi-minute human
|
|
226
|
+
* pause should not drop its ctx_* tools — so we disable the #854 reaper for it
|
|
227
|
+
* via `CONTEXT_MODE_BRIDGE_IDLE_MS=0` (lifecycle.ts honors 0 → reaper not armed).
|
|
228
|
+
* Sub-context / non-interactive children keep the default reaper so abandoned
|
|
229
|
+
* children still can't accumulate (#854). The foreground child is still reaped
|
|
230
|
+
* on actual parent death by the ppid/signal watchdog (#311/#388) — only the
|
|
231
|
+
* idle-time path is disabled. Pure; does not mutate the input env.
|
|
232
|
+
*/
|
|
233
|
+
export declare function foregroundBridgeEnv(baseEnv: NodeJS.ProcessEnv, foreground: boolean): NodeJS.ProcessEnv;
|
|
165
234
|
/** Result of bootstrapping the bridge. */
|
|
166
235
|
export interface BridgeHandle {
|
|
167
236
|
/** Names of tools registered with Pi (for diagnostics / tests). */
|
|
@@ -186,6 +255,14 @@ export interface BootstrapOptions {
|
|
|
186
255
|
env?: NodeJS.ProcessEnv;
|
|
187
256
|
/** DI hook for tests: override the runtime resolver entirely. */
|
|
188
257
|
_resolveJsRuntime?: () => string | null;
|
|
258
|
+
/**
|
|
259
|
+
* #868: true for the foreground interactive Pi session → spawn the child with
|
|
260
|
+
* the #854 idle reaper DISABLED so a human pause never drops its tools.
|
|
261
|
+
* Defaults to false (keep the reaper) for any non-foreground / unspecified
|
|
262
|
+
* caller; the pi extension resolves this from `ctx.hasUI` via
|
|
263
|
+
* {@link isForegroundSession}.
|
|
264
|
+
*/
|
|
265
|
+
foreground?: boolean;
|
|
189
266
|
}
|
|
190
267
|
export declare function bootstrapMCPTools(pi: PiLikeAPI, serverScript: string, options?: BootstrapOptions): Promise<BridgeHandle>;
|
|
191
268
|
export {};
|
|
@@ -323,6 +323,7 @@ export class MCPStdioClient {
|
|
|
323
323
|
serverScript;
|
|
324
324
|
env;
|
|
325
325
|
runtimeOverride;
|
|
326
|
+
diag;
|
|
326
327
|
child = null;
|
|
327
328
|
requestId = 0;
|
|
328
329
|
pending = new Map();
|
|
@@ -344,10 +345,17 @@ export class MCPStdioClient {
|
|
|
344
345
|
* without needing to attach a process-tree probe.
|
|
345
346
|
*/
|
|
346
347
|
_spawnEnv = null;
|
|
347
|
-
constructor(serverScript, env = process.env, runtimeOverride = null
|
|
348
|
+
constructor(serverScript, env = process.env, runtimeOverride = null,
|
|
349
|
+
/**
|
|
350
|
+
* TUI-safe sink for the child's forwarded stderr (#868). Defaults to a
|
|
351
|
+
* no-op so direct callers (skippedBridge, tests) never leak to the
|
|
352
|
+
* terminal; bootstrapMCPTools wires this to the Pi host's file logger.
|
|
353
|
+
*/
|
|
354
|
+
diag = () => { }) {
|
|
348
355
|
this.serverScript = serverScript;
|
|
349
356
|
this.env = env;
|
|
350
357
|
this.runtimeOverride = runtimeOverride;
|
|
358
|
+
this.diag = diag;
|
|
351
359
|
}
|
|
352
360
|
/** Spawn the MCP child. Idempotent. */
|
|
353
361
|
start() {
|
|
@@ -434,21 +442,23 @@ export class MCPStdioClient {
|
|
|
434
442
|
this.child = spawn(runtime, [this.serverScript], {
|
|
435
443
|
// Pipe stderr (#472 round-3): swallowing it via "ignore" hides
|
|
436
444
|
// server crash diagnostics — the user only saw "ctx_* tools will
|
|
437
|
-
// not be callable" with no clue WHY.
|
|
438
|
-
//
|
|
445
|
+
// not be callable" with no clue WHY. We capture it so the diagnostic
|
|
446
|
+
// is preserved, but route it through `diag` (Pi's file logger), NOT
|
|
447
|
+
// process.stderr — Pi's raw-mode TUI owns the terminal and any console
|
|
448
|
+
// write is rendered into the editor input box, blocking typing (#868).
|
|
439
449
|
stdio: ["pipe", "pipe", "pipe"],
|
|
440
450
|
env: childEnv,
|
|
441
451
|
});
|
|
442
452
|
this.child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
443
453
|
this.child.stderr?.on("data", (chunk) => {
|
|
444
454
|
const text = chunk.toString("utf-8");
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
// Forward each non-empty line, [mcp-bridge]-prefixed so it stays
|
|
456
|
+
// grep-friendly in ~/.omp/logs. debug level: this is mostly routine
|
|
457
|
+
// child chatter (e.g. the #854 idle-reaper notice), not an alert.
|
|
458
|
+
for (const line of splitDiagLines(text)) {
|
|
459
|
+
if (line !== "")
|
|
460
|
+
this.diag(`[mcp-bridge] ${line}`, "debug");
|
|
461
|
+
}
|
|
452
462
|
});
|
|
453
463
|
this.child.on("exit", () => this.onExit());
|
|
454
464
|
this.child.on("error", () => this.onExit());
|
|
@@ -690,6 +700,78 @@ export class MCPStdioClient {
|
|
|
690
700
|
this.exited = true;
|
|
691
701
|
}
|
|
692
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Build a {@link BridgeDiag} bound to a Pi host's file logger (#868). Writing to
|
|
705
|
+
* process.stderr from inside Pi's raw-mode TUI corrupts the editor, so every
|
|
706
|
+
* bridge diagnostic — the forwarded MCP child stderr included — goes to
|
|
707
|
+
* `pi.logger` instead. When no logger is reachable (tests, non-Pi hosts) the
|
|
708
|
+
* line is dropped; we never touch the terminal as a fallback.
|
|
709
|
+
*/
|
|
710
|
+
export function makeBridgeDiag(pi) {
|
|
711
|
+
const logger = pi?.logger;
|
|
712
|
+
return (line, level = "warn") => {
|
|
713
|
+
try {
|
|
714
|
+
const fn = level === "debug" ? logger?.debug : logger?.warn;
|
|
715
|
+
if (typeof fn === "function")
|
|
716
|
+
fn(line);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
/* never throw from diagnostics — and never write to the TUI terminal */
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Split a chunk of forwarded child output into lines without a regex (the repo
|
|
725
|
+
* forbids regex in source). Trailing `\r` is stripped so CRLF traces stay clean
|
|
726
|
+
* in the log; the final partial line (no trailing newline) is preserved.
|
|
727
|
+
*/
|
|
728
|
+
export function splitDiagLines(text) {
|
|
729
|
+
const lines = [];
|
|
730
|
+
let start = 0;
|
|
731
|
+
for (let i = 0; i < text.length; i++) {
|
|
732
|
+
if (text[i] === "\n") {
|
|
733
|
+
let end = i;
|
|
734
|
+
if (end > start && text[end - 1] === "\r")
|
|
735
|
+
end--;
|
|
736
|
+
lines.push(text.slice(start, end));
|
|
737
|
+
start = i + 1;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (start < text.length)
|
|
741
|
+
lines.push(text.slice(start));
|
|
742
|
+
return lines;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* #868: is this the FOREGROUND interactive Pi session (vs a subagent / print /
|
|
746
|
+
* RPC session)? Pi passes an ExtensionContext as the 2nd arg to
|
|
747
|
+
* `before_agent_start`; `ctx.hasUI === true` only for the interactive session
|
|
748
|
+
* with a real UI attached (refs oh-my-pi runner.ts:330-331), while subagents
|
|
749
|
+
* are provably `hasUI: false` (refs executor.ts:2052). Fail-safe: treat anything
|
|
750
|
+
* that is NOT an explicit `hasUI === false` as foreground, so an
|
|
751
|
+
* ambiguous/absent ctx keeps the session's bridge ALIVE rather than risking the
|
|
752
|
+
* #868 idle-drop. Mis-classifying an abandoned non-interactive child as
|
|
753
|
+
* foreground only costs one lingering child until parent-death/ session_shutdown
|
|
754
|
+
* reaps it; the opposite error re-drops the user's tools mid-session.
|
|
755
|
+
*/
|
|
756
|
+
export function isForegroundSession(ctx) {
|
|
757
|
+
const hasUI = ctx?.hasUI;
|
|
758
|
+
return hasUI !== false;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* #868: derive the bridge child's spawn env for a session kind. The FOREGROUND
|
|
762
|
+
* interactive session's child must never be idle-reaped — a multi-minute human
|
|
763
|
+
* pause should not drop its ctx_* tools — so we disable the #854 reaper for it
|
|
764
|
+
* via `CONTEXT_MODE_BRIDGE_IDLE_MS=0` (lifecycle.ts honors 0 → reaper not armed).
|
|
765
|
+
* Sub-context / non-interactive children keep the default reaper so abandoned
|
|
766
|
+
* children still can't accumulate (#854). The foreground child is still reaped
|
|
767
|
+
* on actual parent death by the ppid/signal watchdog (#311/#388) — only the
|
|
768
|
+
* idle-time path is disabled. Pure; does not mutate the input env.
|
|
769
|
+
*/
|
|
770
|
+
export function foregroundBridgeEnv(baseEnv, foreground) {
|
|
771
|
+
if (!foreground)
|
|
772
|
+
return baseEnv;
|
|
773
|
+
return { ...baseEnv, CONTEXT_MODE_BRIDGE_IDLE_MS: "0" };
|
|
774
|
+
}
|
|
693
775
|
/**
|
|
694
776
|
* Empty-but-valid handle returned when bootstrap is skipped (#516).
|
|
695
777
|
* Keeps the shutdown contract intact so callers do not need null checks.
|
|
@@ -705,14 +787,16 @@ function skippedBridge() {
|
|
|
705
787
|
}
|
|
706
788
|
export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
707
789
|
const env = options.env ?? process.env;
|
|
790
|
+
// #868: all bridge diagnostics go to Pi's file logger, never the TUI terminal.
|
|
791
|
+
const diag = makeBridgeDiag(pi);
|
|
708
792
|
// Recursion guard (#516): if an ancestor bridge already incremented
|
|
709
793
|
// the depth counter, refuse to spawn another child — even if the
|
|
710
794
|
// binary-name check would let us through. Catches `node` shims that
|
|
711
795
|
// re-exec Pi and other host swaps that bypass basename detection.
|
|
712
796
|
const depth = Number.parseInt(env[BRIDGE_DEPTH_ENV] ?? "0", 10);
|
|
713
797
|
if (Number.isFinite(depth) && depth > 0) {
|
|
714
|
-
|
|
715
|
-
`indicates recursion (fork-bomb guard, #516). ctx_* tools will not be callable
|
|
798
|
+
diag(`[context-mode] WARNING: skipping MCP bridge — ${BRIDGE_DEPTH_ENV}=${depth} ` +
|
|
799
|
+
`indicates recursion (fork-bomb guard, #516). ctx_* tools will not be callable.`);
|
|
716
800
|
return skippedBridge();
|
|
717
801
|
}
|
|
718
802
|
// Runtime guard (#516): when neither node nor bun is on PATH and the
|
|
@@ -720,11 +804,15 @@ export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
|
720
804
|
// return an empty handle — the rest of the extension keeps working.
|
|
721
805
|
const runtime = (options._resolveJsRuntime ?? resolveJsRuntimeForBridge)();
|
|
722
806
|
if (runtime === null) {
|
|
723
|
-
|
|
724
|
-
`Skipping MCP bridge to avoid fork bomb (#516). ctx_* tools will not be callable
|
|
807
|
+
diag(`[context-mode] WARNING: no JS runtime found (need node or bun on PATH). ` +
|
|
808
|
+
`Skipping MCP bridge to avoid fork bomb (#516). ctx_* tools will not be callable.`);
|
|
725
809
|
return skippedBridge();
|
|
726
810
|
}
|
|
727
|
-
|
|
811
|
+
// #868: the foreground interactive session's child runs with the #854 idle
|
|
812
|
+
// reaper disabled (CONTEXT_MODE_BRIDGE_IDLE_MS=0) so a human pause never drops
|
|
813
|
+
// its tools; sub-context / non-interactive children keep the reaper (#854).
|
|
814
|
+
const spawnEnv = foregroundBridgeEnv(env, options.foreground ?? false);
|
|
815
|
+
const client = new MCPStdioClient(serverScript, spawnEnv, runtime, diag);
|
|
728
816
|
// Retry-on-slow-initialize (#647).
|
|
729
817
|
//
|
|
730
818
|
// Each attempt is independently bounded by DEFAULT_REQUEST_TIMEOUT_MS
|
|
@@ -747,8 +835,8 @@ export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
|
747
835
|
if (attempt === MAX_INIT_RETRIES)
|
|
748
836
|
break;
|
|
749
837
|
const msg = err instanceof Error ? err.message : String(err);
|
|
750
|
-
|
|
751
|
-
`(attempt ${attempt + 1}/${MAX_INIT_RETRIES + 1}): ${msg}. Retrying
|
|
838
|
+
diag(`[context-mode] WARNING: MCP bridge initialize failed ` +
|
|
839
|
+
`(attempt ${attempt + 1}/${MAX_INIT_RETRIES + 1}): ${msg}. Retrying…`);
|
|
752
840
|
// Reclaim the failed child's fds before respawning. shutdown() is
|
|
753
841
|
// idempotent and bounded by a 5s SIGKILL fallback (#472 round-3),
|
|
754
842
|
// so a child stuck in an uninterruptible syscall cannot block the
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -84,6 +84,16 @@ export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): n
|
|
|
84
84
|
* Exported for unit-testing.
|
|
85
85
|
*/
|
|
86
86
|
export declare function bridgeChildIdleTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
87
|
+
/**
|
|
88
|
+
* #854 / #868: human-readable notice emitted when an idle bridge child is
|
|
89
|
+
* released. DX-tuned — human units (seconds, not raw ms), reassures that the
|
|
90
|
+
* helper reconnects automatically (it respawns on the next ctx_* call, #583),
|
|
91
|
+
* and drops the alarming "self-shutdown" jargon. Pure + exported so the wording
|
|
92
|
+
* is pinned by a test and stays grep-friendly via the #854 tag. Note: after the
|
|
93
|
+
* #868 fix this fires ONLY for sub-context / non-interactive children — the
|
|
94
|
+
* foreground interactive session's child runs with the reaper disabled.
|
|
95
|
+
*/
|
|
96
|
+
export declare function idleReapMessage(idleMs: number): string;
|
|
87
97
|
/**
|
|
88
98
|
* #854: record MCP activity (inbound message or response). The server calls this
|
|
89
99
|
* so the bridge-child idle reaper in {@link startLifecycleGuard} can distinguish
|
package/build/lifecycle.js
CHANGED
|
@@ -123,6 +123,19 @@ export function bridgeChildIdleTimeoutMs(env = process.env) {
|
|
|
123
123
|
}
|
|
124
124
|
return 180_000;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* #854 / #868: human-readable notice emitted when an idle bridge child is
|
|
128
|
+
* released. DX-tuned — human units (seconds, not raw ms), reassures that the
|
|
129
|
+
* helper reconnects automatically (it respawns on the next ctx_* call, #583),
|
|
130
|
+
* and drops the alarming "self-shutdown" jargon. Pure + exported so the wording
|
|
131
|
+
* is pinned by a test and stays grep-friendly via the #854 tag. Note: after the
|
|
132
|
+
* #868 fix this fires ONLY for sub-context / non-interactive children — the
|
|
133
|
+
* foreground interactive session's child runs with the reaper disabled.
|
|
134
|
+
*/
|
|
135
|
+
export function idleReapMessage(idleMs) {
|
|
136
|
+
const seconds = Math.round(idleMs / 1000);
|
|
137
|
+
return `[context-mode] Released an idle MCP helper after ${seconds}s of inactivity to free memory; it reconnects automatically on next use. (#854)`;
|
|
138
|
+
}
|
|
126
139
|
// #854 idle-reaper state, module-level by design: an MCP server is exactly one
|
|
127
140
|
// process (one StdioServerTransport + one lifecycle guard), so these are never
|
|
128
141
|
// shared across concurrent servers in production. Multiple startLifecycleGuard()
|
|
@@ -245,7 +258,9 @@ export function startLifecycleGuard(opts) {
|
|
|
245
258
|
// further messages (#643 unbounded calls) — the false-reap regression the
|
|
246
259
|
// adversarial review flagged.
|
|
247
260
|
if (_inFlight === 0 && Date.now() - _lastMcpActivity >= idleMs) {
|
|
248
|
-
|
|
261
|
+
// Child's own stderr — the pi bridge forwards it to pi.logger, never the
|
|
262
|
+
// TUI terminal (#868). DX-tuned wording via idleReapMessage.
|
|
263
|
+
process.stderr.write(idleReapMessage(idleMs) + "\n");
|
|
249
264
|
shutdown();
|
|
250
265
|
}
|
|
251
266
|
}, Math.max(1000, Math.min(Math.floor(idleMs / 4), 30_000)));
|