@mxalbert/context-mode 2.0.0
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 +33 -0
- package/.claude-plugin/plugin.json +31 -0
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +13 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/index.ts +12 -0
- package/.openclaw-plugin/openclaw.plugin.json +38 -0
- package/.openclaw-plugin/package.json +28 -0
- package/LICENSE +94 -0
- package/README.md +1619 -0
- package/bin/statusline.mjs +378 -0
- package/build/adapters/antigravity/index.d.ts +51 -0
- package/build/adapters/antigravity/index.js +169 -0
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/base.d.ts +92 -0
- package/build/adapters/base.js +135 -0
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +106 -0
- package/build/adapters/claude-code/hooks.js +183 -0
- package/build/adapters/claude-code/index.d.ts +80 -0
- package/build/adapters/claude-code/index.js +569 -0
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.d.ts +10 -0
- package/build/adapters/client-map.js +42 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +55 -0
- package/build/adapters/codex/hooks.js +64 -0
- package/build/adapters/codex/index.d.ts +87 -0
- package/build/adapters/codex/index.js +995 -0
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/codex/usage.d.ts +107 -0
- package/build/adapters/codex/usage.js +227 -0
- package/build/adapters/copilot-base.d.ts +94 -0
- package/build/adapters/copilot-base.js +297 -0
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/cursor/config.d.ts +4 -0
- package/build/adapters/cursor/config.js +4 -0
- package/build/adapters/cursor/hooks.d.ts +55 -0
- package/build/adapters/cursor/hooks.js +82 -0
- package/build/adapters/cursor/index.d.ts +64 -0
- package/build/adapters/cursor/index.js +517 -0
- package/build/adapters/detect.d.ts +136 -0
- package/build/adapters/detect.js +623 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +83 -0
- package/build/adapters/gemini-cli/hooks.js +115 -0
- package/build/adapters/gemini-cli/index.d.ts +80 -0
- package/build/adapters/gemini-cli/index.js +483 -0
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
- package/build/adapters/jetbrains-copilot/hooks.js +87 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
- package/build/adapters/jetbrains-copilot/index.js +131 -0
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +21 -0
- package/build/adapters/kimi/paths.js +52 -0
- package/build/adapters/kimi/usage.d.ts +82 -0
- package/build/adapters/kimi/usage.js +217 -0
- package/build/adapters/kiro/hooks.d.ts +61 -0
- package/build/adapters/kiro/hooks.js +94 -0
- package/build/adapters/kiro/index.d.ts +61 -0
- package/build/adapters/kiro/index.js +301 -0
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +81 -0
- package/build/adapters/omp/plugin.js +331 -0
- package/build/adapters/omp/usage.d.ts +49 -0
- package/build/adapters/omp/usage.js +110 -0
- package/build/adapters/openclaw/config.d.ts +8 -0
- package/build/adapters/openclaw/config.js +8 -0
- package/build/adapters/openclaw/hooks.d.ts +50 -0
- package/build/adapters/openclaw/hooks.js +61 -0
- package/build/adapters/openclaw/index.d.ts +74 -0
- package/build/adapters/openclaw/index.js +439 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +207 -0
- package/build/adapters/openclaw/plugin.d.ts +141 -0
- package/build/adapters/openclaw/plugin.js +818 -0
- package/build/adapters/openclaw/session-db.d.ts +55 -0
- package/build/adapters/openclaw/session-db.js +88 -0
- package/build/adapters/openclaw/usage.d.ts +34 -0
- package/build/adapters/openclaw/usage.js +52 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +144 -0
- package/build/adapters/opencode/index.js +638 -0
- package/build/adapters/opencode/plugin.d.ts +249 -0
- package/build/adapters/opencode/plugin.js +1536 -0
- package/build/adapters/opencode/v2.d.ts +117 -0
- package/build/adapters/opencode/v2.js +224 -0
- package/build/adapters/opencode/zod3tov4.d.ts +1 -0
- package/build/adapters/opencode/zod3tov4.js +132 -0
- package/build/adapters/pi/extension.d.ts +81 -0
- package/build/adapters/pi/extension.js +861 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +268 -0
- package/build/adapters/pi/mcp-bridge.js +894 -0
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.d.ts +40 -0
- package/build/adapters/qwen-code/index.js +361 -0
- package/build/adapters/qwen-code/usage.d.ts +90 -0
- package/build/adapters/qwen-code/usage.js +222 -0
- package/build/adapters/types.d.ts +360 -0
- package/build/adapters/types.js +118 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
- package/build/adapters/vscode-copilot/hooks.js +98 -0
- package/build/adapters/vscode-copilot/index.d.ts +32 -0
- package/build/adapters/vscode-copilot/index.js +222 -0
- package/build/adapters/zed/index.d.ts +40 -0
- package/build/adapters/zed/index.js +163 -0
- package/build/cli.d.ts +34 -0
- package/build/cli.js +1828 -0
- package/build/db-base.d.ts +250 -0
- package/build/db-base.js +756 -0
- package/build/executor.d.ts +72 -0
- package/build/executor.js +678 -0
- package/build/exit-classify.d.ts +19 -0
- package/build/exit-classify.js +12 -0
- package/build/fetch/blocks.d.ts +145 -0
- package/build/fetch/blocks.js +244 -0
- package/build/fetch/extract.d.ts +68 -0
- package/build/fetch/extract.js +123 -0
- package/build/fetch/page-store.d.ts +66 -0
- package/build/fetch/page-store.js +174 -0
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +124 -0
- package/build/lifecycle.js +278 -0
- package/build/package-identity.d.ts +26 -0
- package/build/package-identity.js +28 -0
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.d.ts +71 -0
- package/build/runtime.js +659 -0
- package/build/search/auto-memory.d.ts +47 -0
- package/build/search/auto-memory.js +170 -0
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/search/unified.d.ts +56 -0
- package/build/search/unified.js +104 -0
- package/build/security.d.ts +206 -0
- package/build/security.js +780 -0
- package/build/server.d.ts +240 -0
- package/build/server.js +5034 -0
- package/build/session/analytics.d.ts +773 -0
- package/build/session/analytics.js +2420 -0
- package/build/session/db.d.ts +468 -0
- package/build/session/db.js +1330 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +333 -0
- package/build/session/extract.js +2688 -0
- package/build/session/model-prices.json +429 -0
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/pricing.d.ts +64 -0
- package/build/session/pricing.js +151 -0
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +245 -0
- package/build/session/purge.d.ts +138 -0
- package/build/session/purge.js +240 -0
- package/build/session/retrieval-marker.d.ts +39 -0
- package/build/session/retrieval-marker.js +65 -0
- package/build/session/snapshot.d.ts +46 -0
- package/build/session/snapshot.js +532 -0
- package/build/store-directory.d.ts +56 -0
- package/build/store-directory.js +254 -0
- package/build/store.d.ts +147 -0
- package/build/store.js +1714 -0
- package/build/truncate.d.ts +57 -0
- package/build/truncate.js +142 -0
- package/build/types.d.ts +117 -0
- package/build/types.js +20 -0
- package/build/util/claude-config.d.ts +32 -0
- package/build/util/claude-config.js +84 -0
- package/build/util/hook-config.d.ts +27 -0
- package/build/util/hook-config.js +76 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/build/util/plugin-cache-integrity.d.ts +51 -0
- package/build/util/plugin-cache-integrity.js +146 -0
- package/build/util/project-dir.d.ts +149 -0
- package/build/util/project-dir.js +340 -0
- package/build/util/sibling-mcp.d.ts +79 -0
- package/build/util/sibling-mcp.js +181 -0
- package/cli.bundle.mjs +1495 -0
- package/configs/antigravity/GEMINI.md +68 -0
- package/configs/antigravity/mcp_config.json +7 -0
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +91 -0
- package/configs/codex/AGENTS.md +99 -0
- package/configs/codex/config.toml +8 -0
- package/configs/codex/hooks.json +47 -0
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/cursor/context-mode.mdc +76 -0
- package/configs/cursor/hooks.json +21 -0
- package/configs/cursor/mcp.json +7 -0
- package/configs/gemini-cli/GEMINI.md +88 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +60 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +7 -0
- package/configs/kilo/AGENTS.md +84 -0
- package/configs/kilo/kilo.json +6 -0
- package/configs/kimi/hooks.json +54 -0
- package/configs/kiro/KIRO.md +84 -0
- package/configs/kiro/agent.json +18 -0
- package/configs/kiro/mcp.json +7 -0
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +87 -0
- package/configs/openclaw/openclaw.json +13 -0
- package/configs/opencode/AGENTS.md +84 -0
- package/configs/opencode/opencode.json +6 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/qwen-code/QWEN.md +91 -0
- package/configs/vscode-copilot/copilot-instructions.md +80 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +7 -0
- package/configs/zed/AGENTS.md +68 -0
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/auto-injection.mjs +102 -0
- package/hooks/cache-heal-utils.mjs +379 -0
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +66 -0
- package/hooks/codex/precompact.mjs +70 -0
- package/hooks/codex/pretooluse.mjs +42 -0
- package/hooks/codex/sessionstart.mjs +121 -0
- package/hooks/codex/stop.mjs +138 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +356 -0
- package/hooks/core/mcp-ready.mjs +109 -0
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +1050 -0
- package/hooks/core/stdin.mjs +90 -0
- package/hooks/core/tool-naming.mjs +57 -0
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +72 -0
- package/hooks/cursor/pretooluse.mjs +26 -0
- package/hooks/cursor/sessionstart.mjs +97 -0
- package/hooks/cursor/stop.mjs +48 -0
- package/hooks/ensure-deps.mjs +254 -0
- package/hooks/formatters/claude-code.mjs +113 -0
- package/hooks/formatters/cursor.mjs +37 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftermodel.mjs +70 -0
- package/hooks/gemini-cli/aftertool.mjs +61 -0
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/gemini-cli/beforetool.mjs +27 -0
- package/hooks/gemini-cli/precompress.mjs +55 -0
- package/hooks/gemini-cli/sessionstart.mjs +137 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/hooks.json +143 -0
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +55 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +132 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/posttooluse.mjs +51 -0
- package/hooks/kiro/pretooluse.mjs +64 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/normalize-hooks.mjs +323 -0
- package/hooks/platform-bridge.mjs +340 -0
- package/hooks/posttooluse.mjs +225 -0
- package/hooks/precompact.mjs +100 -0
- package/hooks/pretooluse.mjs +227 -0
- package/hooks/qwen-code/platform.mjs +1 -0
- package/hooks/qwen-code/stop.mjs +168 -0
- package/hooks/routing-block.mjs +105 -0
- package/hooks/run-hook.mjs +95 -0
- package/hooks/security.bundle.mjs +2 -0
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +145 -0
- package/hooks/session-directive.mjs +520 -0
- package/hooks/session-extract.bundle.mjs +3 -0
- package/hooks/session-helpers.mjs +428 -0
- package/hooks/session-loaders.mjs +448 -0
- package/hooks/session-snapshot.bundle.mjs +32 -0
- package/hooks/sessionstart.mjs +464 -0
- package/hooks/stop.mjs +82 -0
- package/hooks/suppress-stderr.mjs +25 -0
- package/hooks/userpromptsubmit.mjs +108 -0
- package/hooks/vscode-copilot/posttooluse.mjs +61 -0
- package/hooks/vscode-copilot/precompact.mjs +55 -0
- package/hooks/vscode-copilot/pretooluse.mjs +27 -0
- package/hooks/vscode-copilot/sessionstart.mjs +129 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +132 -0
- package/scripts/heal-better-sqlite3.mjs +430 -0
- package/scripts/heal-installed-plugins.mjs +635 -0
- package/scripts/plugin-cache-integrity.mjs +248 -0
- package/scripts/postinstall.mjs +397 -0
- package/server.bundle.mjs +1421 -0
- package/server.js +5 -0
- package/skills/.ignore +7 -0
- package/skills/context-mode/SKILL.md +300 -0
- package/skills/context-mode/references/anti-patterns.md +283 -0
- package/skills/context-mode/references/patterns-javascript.md +298 -0
- package/skills/context-mode/references/patterns-python.md +304 -0
- package/skills/context-mode/references/patterns-shell.md +277 -0
- package/skills/ctx-doctor/SKILL.md +22 -0
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-insight/SKILL.md +24 -0
- package/skills/ctx-purge/SKILL.md +49 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +26 -0
- package/skills/ctx-upgrade/SKILL.md +31 -0
- package/start.mjs +616 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page-store — the lossless side of fetch extraction.
|
|
3
|
+
*
|
|
4
|
+
* Holds, per project, the COMPLETE converted document for every page ever
|
|
5
|
+
* fetched, plus every block of it with its `content` / `template` label. The
|
|
6
|
+
* FTS index receives only `content` blocks; this store is what makes that
|
|
7
|
+
* safe, because nothing has been thrown away — the whole document and every
|
|
8
|
+
* template block remain here, verbatim and retrievable.
|
|
9
|
+
*
|
|
10
|
+
* It is also the comparison set. Classification asks one question — "was this
|
|
11
|
+
* exact block already seen on a DIFFERENT page of this host?" — and that
|
|
12
|
+
* question is answered by `page_blocks` joined to `pages`.
|
|
13
|
+
*
|
|
14
|
+
* Nothing in this file truncates. Full documents are stored whole; there is no
|
|
15
|
+
* size cap, no prefix, no summary. (The fetch path upstream already refuses
|
|
16
|
+
* responses above 50 MB before conversion.)
|
|
17
|
+
*
|
|
18
|
+
* No regular expressions (repo-wide ban).
|
|
19
|
+
*/
|
|
20
|
+
import { loadDatabase, applyWALPragmas, cleanOrphanedWALFiles, withRetry } from "../db-base.js";
|
|
21
|
+
/**
|
|
22
|
+
* Canonical identity of a fetched page. The URL minus its fragment: two
|
|
23
|
+
* fetches of the same page must be the same row, or a page compared against
|
|
24
|
+
* an older copy of itself marks its own article as template.
|
|
25
|
+
*/
|
|
26
|
+
export function pageKeyFor(url) {
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(url);
|
|
29
|
+
u.hash = "";
|
|
30
|
+
return u.toString();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Host of a URL, lower-cased. Empty string when the URL will not parse. */
|
|
37
|
+
export function hostFor(url) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(url).host.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class PageStore {
|
|
46
|
+
#db;
|
|
47
|
+
constructor(dbPath) {
|
|
48
|
+
const Database = loadDatabase();
|
|
49
|
+
cleanOrphanedWALFiles(dbPath);
|
|
50
|
+
this.#db = new Database(dbPath, { timeout: 30000 });
|
|
51
|
+
applyWALPragmas(this.#db);
|
|
52
|
+
this.#db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
54
|
+
page_key TEXT PRIMARY KEY,
|
|
55
|
+
host TEXT NOT NULL,
|
|
56
|
+
url TEXT NOT NULL,
|
|
57
|
+
source_label TEXT NOT NULL,
|
|
58
|
+
route TEXT NOT NULL,
|
|
59
|
+
provisional INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
61
|
+
full_text TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS page_blocks (
|
|
65
|
+
page_key TEXT NOT NULL,
|
|
66
|
+
ordinal INTEGER NOT NULL,
|
|
67
|
+
hash TEXT NOT NULL,
|
|
68
|
+
kind TEXT NOT NULL,
|
|
69
|
+
raw TEXT NOT NULL,
|
|
70
|
+
text TEXT NOT NULL,
|
|
71
|
+
PRIMARY KEY (page_key, ordinal)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_pages_host ON pages(host);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_page_blocks_hash ON page_blocks(hash);
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
/** Distinct pages already recorded for a host, excluding `exceptPageKey`. */
|
|
79
|
+
hostPageCount(host, exceptPageKey) {
|
|
80
|
+
const row = this.#db
|
|
81
|
+
.prepare("SELECT COUNT(*) AS n FROM pages WHERE host = ? AND page_key <> ?")
|
|
82
|
+
.get(host, exceptPageKey);
|
|
83
|
+
return row ? row.n : 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How many DISTINCT other pages of this host carry this exact block. The
|
|
87
|
+
* `page_key <> ?` clause is what keeps a page from classifying itself.
|
|
88
|
+
*/
|
|
89
|
+
otherPageCounts(host, exceptPageKey, hashes) {
|
|
90
|
+
const counts = new Map();
|
|
91
|
+
if (hashes.length === 0)
|
|
92
|
+
return counts;
|
|
93
|
+
const unique = Array.from(new Set(hashes));
|
|
94
|
+
const stmt = this.#db.prepare(`SELECT COUNT(DISTINCT b.page_key) AS n
|
|
95
|
+
FROM page_blocks b
|
|
96
|
+
JOIN pages p ON p.page_key = b.page_key
|
|
97
|
+
WHERE b.hash = ? AND p.host = ? AND b.page_key <> ?`);
|
|
98
|
+
for (const h of unique) {
|
|
99
|
+
const row = stmt.get(h, host, exceptPageKey);
|
|
100
|
+
counts.set(h, row ? row.n : 0);
|
|
101
|
+
}
|
|
102
|
+
return counts;
|
|
103
|
+
}
|
|
104
|
+
/** Store one page whole: the complete document plus every labelled block. */
|
|
105
|
+
recordPage(page, blocks) {
|
|
106
|
+
const insertPage = this.#db.prepare(`INSERT INTO pages (page_key, host, url, source_label, route, provisional, fetched_at, full_text)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), ?)
|
|
108
|
+
ON CONFLICT(page_key) DO UPDATE SET
|
|
109
|
+
host = excluded.host, url = excluded.url, source_label = excluded.source_label,
|
|
110
|
+
route = excluded.route, provisional = excluded.provisional,
|
|
111
|
+
fetched_at = excluded.fetched_at, full_text = excluded.full_text`);
|
|
112
|
+
const delBlocks = this.#db.prepare("DELETE FROM page_blocks WHERE page_key = ?");
|
|
113
|
+
const insBlock = this.#db.prepare("INSERT INTO page_blocks (page_key, ordinal, hash, kind, raw, text) VALUES (?, ?, ?, ?, ?, ?)");
|
|
114
|
+
const tx = this.#db.transaction(() => {
|
|
115
|
+
insertPage.run(page.pageKey, page.host, page.url, page.sourceLabel, page.route, page.provisional ? 1 : 0, page.fullText);
|
|
116
|
+
delBlocks.run(page.pageKey);
|
|
117
|
+
for (const b of blocks) {
|
|
118
|
+
insBlock.run(page.pageKey, b.ordinal, b.hash, b.kind, b.raw, b.text);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
withRetry(() => tx());
|
|
122
|
+
}
|
|
123
|
+
/** Update the stored labels after a re-run, and clear the provisional flag. */
|
|
124
|
+
relabelPage(pageKey, blocks, provisional) {
|
|
125
|
+
const upd = this.#db.prepare("UPDATE page_blocks SET kind = ? WHERE page_key = ? AND ordinal = ?");
|
|
126
|
+
const updPage = this.#db.prepare("UPDATE pages SET provisional = ? WHERE page_key = ?");
|
|
127
|
+
const tx = this.#db.transaction(() => {
|
|
128
|
+
for (const b of blocks)
|
|
129
|
+
upd.run(b.kind, pageKey, b.ordinal);
|
|
130
|
+
updPage.run(provisional ? 1 : 0, pageKey);
|
|
131
|
+
});
|
|
132
|
+
withRetry(() => tx());
|
|
133
|
+
}
|
|
134
|
+
/** Every page of a host still carrying a cold-start (provisional) labelling. */
|
|
135
|
+
provisionalPages(host, exceptPageKey) {
|
|
136
|
+
const rows = this.#db
|
|
137
|
+
.prepare(`SELECT page_key, host, url, source_label, route, provisional, full_text
|
|
138
|
+
FROM pages WHERE host = ? AND provisional = 1 AND page_key <> ?`)
|
|
139
|
+
.all(host, exceptPageKey);
|
|
140
|
+
return rows.map((r) => ({
|
|
141
|
+
pageKey: String(r.page_key),
|
|
142
|
+
host: String(r.host),
|
|
143
|
+
url: String(r.url),
|
|
144
|
+
sourceLabel: String(r.source_label),
|
|
145
|
+
route: String(r.route),
|
|
146
|
+
provisional: Number(r.provisional) === 1,
|
|
147
|
+
fullText: String(r.full_text),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/** Every stored block of a page, in document order — content and template alike. */
|
|
151
|
+
blocksOf(pageKey) {
|
|
152
|
+
const rows = this.#db
|
|
153
|
+
.prepare("SELECT ordinal, hash, kind, raw, text FROM page_blocks WHERE page_key = ? ORDER BY ordinal")
|
|
154
|
+
.all(pageKey);
|
|
155
|
+
return rows.map((r) => ({
|
|
156
|
+
ordinal: Number(r.ordinal),
|
|
157
|
+
hash: String(r.hash),
|
|
158
|
+
kind: String(r.kind) === "template" ? "template" : "content",
|
|
159
|
+
raw: String(r.raw),
|
|
160
|
+
text: String(r.text),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
/** The complete converted document as it was stored, or null. */
|
|
164
|
+
fullTextOf(pageKey) {
|
|
165
|
+
const row = this.#db.prepare("SELECT full_text FROM pages WHERE page_key = ?").get(pageKey);
|
|
166
|
+
return row ? row.full_text : null;
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
try {
|
|
170
|
+
this.#db.close();
|
|
171
|
+
}
|
|
172
|
+
catch { /* already closed */ }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key / storage-label composition for ctx_fetch_and_index.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct URLs that share a user-supplied `source` label MUST NOT collide
|
|
5
|
+
* in the cache (or in FTS5 storage, since indexing dedups by label). Compose
|
|
6
|
+
* `${source}::${url}` whenever a `source` is explicitly provided so cache
|
|
7
|
+
* lookup, dedup, and re-indexing are all per-(source,url). When no `source`
|
|
8
|
+
* is provided the URL itself is the unique key — no composition needed.
|
|
9
|
+
*
|
|
10
|
+
* `ctx_search(source: "Docs")` continues to work because LIKE-mode source
|
|
11
|
+
* filtering matches on the substring "Docs" inside "Docs::https://…".
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeFetchCacheKey(source: string | undefined, url: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key / storage-label composition for ctx_fetch_and_index.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct URLs that share a user-supplied `source` label MUST NOT collide
|
|
5
|
+
* in the cache (or in FTS5 storage, since indexing dedups by label). Compose
|
|
6
|
+
* `${source}::${url}` whenever a `source` is explicitly provided so cache
|
|
7
|
+
* lookup, dedup, and re-indexing are all per-(source,url). When no `source`
|
|
8
|
+
* is provided the URL itself is the unique key — no composition needed.
|
|
9
|
+
*
|
|
10
|
+
* `ctx_search(source: "Docs")` continues to work because LIKE-mode source
|
|
11
|
+
* filtering matches on the substring "Docs" inside "Docs::https://…".
|
|
12
|
+
*/
|
|
13
|
+
export function composeFetchCacheKey(source, url) {
|
|
14
|
+
return source === undefined ? url : `${source}::${url}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle — Process lifecycle guard for MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Detects parent process death (ppid polling) and OS signals to prevent
|
|
5
|
+
* orphaned MCP server processes consuming 100% CPU (issue #103).
|
|
6
|
+
*
|
|
7
|
+
* Stdin close is NOT used as a *standalone* shutdown signal — the MCP stdio
|
|
8
|
+
* transport owns stdin and transient pipe events cause spurious -32000
|
|
9
|
+
* errors (#236). We do, however, treat stdin EOF as a hint to re-run the
|
|
10
|
+
* parent-liveness probe immediately (instead of waiting up to 30 s for the
|
|
11
|
+
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
|
+
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
13
|
+
*
|
|
14
|
+
* Additionally, for MCP BRIDGE CHILDREN only (CONTEXT_MODE_BRIDGE_DEPTH>0), a
|
|
15
|
+
* request-idle self-shutdown reaps a child that a pi/omp sub-context abandoned
|
|
16
|
+
* while its long-lived parent keeps running (#854) — gated so the depth-0
|
|
17
|
+
* keep-alive servers #602 restored are never reaped, never via stdin EOF, and
|
|
18
|
+
* never while a tool call is in flight (#643).
|
|
19
|
+
*
|
|
20
|
+
* Cross-platform: macOS, Linux, Windows.
|
|
21
|
+
*/
|
|
22
|
+
export interface LifecycleGuardOptions {
|
|
23
|
+
/** Interval in ms to check parent liveness. Default: 30_000 */
|
|
24
|
+
checkIntervalMs?: number;
|
|
25
|
+
/** Called when parent death or OS signal is detected. */
|
|
26
|
+
onShutdown: () => void;
|
|
27
|
+
/** Injectable parent-alive check (for testing). Default: ppid-based check. */
|
|
28
|
+
isParentAlive?: () => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* #854: request-idle shutdown timeout (ms) for MCP bridge children. Default:
|
|
31
|
+
* {@link bridgeChildIdleTimeoutMs}() — 0 (disabled) unless CONTEXT_MODE_BRIDGE_DEPTH>0.
|
|
32
|
+
* Exposed for testing.
|
|
33
|
+
*/
|
|
34
|
+
bridgeIdleMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
|
|
37
|
+
export interface IsParentAliveDeps {
|
|
38
|
+
/** Read the current ppid. Default: `() => process.ppid`. */
|
|
39
|
+
getPpid?: () => number;
|
|
40
|
+
/** Read the grandparent ppid. Default: ps-based POSIX probe, NaN on Windows. */
|
|
41
|
+
readGrandparentPpid?: () => number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a parent-liveness check that handles the npm-exec wrapper case (#311).
|
|
45
|
+
*
|
|
46
|
+
* A plain ppid comparison misses Claude Code sessions launched via
|
|
47
|
+
* `start.mjs → npm exec → context-mode server`: when Claude Code dies,
|
|
48
|
+
* `start.mjs` reparents to init but `npm exec` stays alive, so the server's
|
|
49
|
+
* direct ppid never changes. We additionally check whether the grandparent
|
|
50
|
+
* process has been reparented to init (PID 1). When the original grandparent
|
|
51
|
+
* was already 1 (daemonized startup) the check is skipped, and on Windows
|
|
52
|
+
* where there's no cheap `ps` equivalent we also skip — so this change is
|
|
53
|
+
* strictly additive to the previous behavior.
|
|
54
|
+
*
|
|
55
|
+
* Exported for unit-testing with injected readers. Production code uses
|
|
56
|
+
* {@link defaultIsParentAlive} (captured once at module load).
|
|
57
|
+
*/
|
|
58
|
+
export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the parent-liveness poll interval based on context (#534).
|
|
61
|
+
*
|
|
62
|
+
* When this process is the MCP bridge child spawned by the Pi adapter
|
|
63
|
+
* (`bootstrapMCPTools` in `src/adapters/pi/mcp-bridge.ts` sets
|
|
64
|
+
* `CONTEXT_MODE_BRIDGE_DEPTH=1` in the child env), we tighten the poll to
|
|
65
|
+
* 1 s. The Pi parent can disappear in under 50 ms (`pi --help` prints
|
|
66
|
+
* usage and returns), so the default 30 s window leaves a long-lived
|
|
67
|
+
* CPU-spinning orphan. For top-level MCP servers (depth 0 / absent) we
|
|
68
|
+
* keep the original 30 s cadence — the existing #311/#388 ppid + stdin
|
|
69
|
+
* recovery paths already cover Claude Code style hosts.
|
|
70
|
+
*
|
|
71
|
+
* Exported for unit-testing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
|
|
74
|
+
/**
|
|
75
|
+
* #854: idle-shutdown timeout (ms) for an MCP BRIDGE CHILD. Returns 0 (disabled)
|
|
76
|
+
* unless this process is a bridge child (CONTEXT_MODE_BRIDGE_DEPTH>0). depth-0 /
|
|
77
|
+
* absent always returns 0, so the long-lived keep-alive servers that #602
|
|
78
|
+
* restored are NEVER reaped on idle. Default for bridge children is 3 min;
|
|
79
|
+
* override with CONTEXT_MODE_BRIDGE_IDLE_MS (a non-positive value disables it).
|
|
80
|
+
* The reaper additionally never fires while a tool call is in flight (see
|
|
81
|
+
* {@link noteRequestStart}), so the window only bounds how fast *abandoned*
|
|
82
|
+
* children drain — it does not cap legitimate long-running calls.
|
|
83
|
+
*
|
|
84
|
+
* Exported for unit-testing.
|
|
85
|
+
*/
|
|
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;
|
|
97
|
+
/**
|
|
98
|
+
* #854: record MCP activity (inbound message or response). The server calls this
|
|
99
|
+
* so the bridge-child idle reaper in {@link startLifecycleGuard} can distinguish
|
|
100
|
+
* an actively-used child from an abandoned one. Cheap; safe on the hot path.
|
|
101
|
+
*/
|
|
102
|
+
export declare function noteMcpActivity(): void;
|
|
103
|
+
/**
|
|
104
|
+
* #854: mark a tool call as started. Suppresses the bridge-child idle reaper so a
|
|
105
|
+
* single long-running ctx_execute / ctx_batch_execute (which sends one inbound
|
|
106
|
+
* frame then runs unbounded, #643) is never reaped mid-execution.
|
|
107
|
+
*/
|
|
108
|
+
export declare function noteRequestStart(): void;
|
|
109
|
+
/** #854: mark a tool call as finished (success or error). */
|
|
110
|
+
export declare function noteRequestEnd(): void;
|
|
111
|
+
/**
|
|
112
|
+
* #854: wrap an MCP stdio transport's `onmessage` so each inbound message
|
|
113
|
+
* refreshes the idle clock. Best-effort: call after `connect()` (onmessage set);
|
|
114
|
+
* a no-op if it isn't a function, and a throw in noteMcpActivity never breaks
|
|
115
|
+
* dispatch. No stdin touch (preserves the #236 contract). Exported for testing.
|
|
116
|
+
*/
|
|
117
|
+
export declare function attachMcpActivityTap(transport: {
|
|
118
|
+
onmessage?: (message: unknown, extra?: unknown) => unknown;
|
|
119
|
+
} | null | undefined): void;
|
|
120
|
+
/**
|
|
121
|
+
* Start the lifecycle guard. Returns a cleanup function.
|
|
122
|
+
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
123
|
+
*/
|
|
124
|
+
export declare function startLifecycleGuard(opts: LifecycleGuardOptions): () => void;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle — Process lifecycle guard for MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Detects parent process death (ppid polling) and OS signals to prevent
|
|
5
|
+
* orphaned MCP server processes consuming 100% CPU (issue #103).
|
|
6
|
+
*
|
|
7
|
+
* Stdin close is NOT used as a *standalone* shutdown signal — the MCP stdio
|
|
8
|
+
* transport owns stdin and transient pipe events cause spurious -32000
|
|
9
|
+
* errors (#236). We do, however, treat stdin EOF as a hint to re-run the
|
|
10
|
+
* parent-liveness probe immediately (instead of waiting up to 30 s for the
|
|
11
|
+
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
|
+
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
13
|
+
*
|
|
14
|
+
* Additionally, for MCP BRIDGE CHILDREN only (CONTEXT_MODE_BRIDGE_DEPTH>0), a
|
|
15
|
+
* request-idle self-shutdown reaps a child that a pi/omp sub-context abandoned
|
|
16
|
+
* while its long-lived parent keeps running (#854) — gated so the depth-0
|
|
17
|
+
* keep-alive servers #602 restored are never reaped, never via stdin EOF, and
|
|
18
|
+
* never while a tool call is in flight (#643).
|
|
19
|
+
*
|
|
20
|
+
* Cross-platform: macOS, Linux, Windows.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
/** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
|
|
24
|
+
function readGrandparentPpidImpl() {
|
|
25
|
+
if (process.platform === "win32")
|
|
26
|
+
return NaN;
|
|
27
|
+
const ppid = process.ppid;
|
|
28
|
+
if (!ppid || ppid <= 1)
|
|
29
|
+
return NaN;
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync("ps", ["-o", "ppid=", "-p", String(ppid)], {
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
timeout: 2000,
|
|
34
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
35
|
+
}).trim();
|
|
36
|
+
const n = parseInt(out, 10);
|
|
37
|
+
return Number.isFinite(n) ? n : NaN;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return NaN;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a parent-liveness check that handles the npm-exec wrapper case (#311).
|
|
45
|
+
*
|
|
46
|
+
* A plain ppid comparison misses Claude Code sessions launched via
|
|
47
|
+
* `start.mjs → npm exec → context-mode server`: when Claude Code dies,
|
|
48
|
+
* `start.mjs` reparents to init but `npm exec` stays alive, so the server's
|
|
49
|
+
* direct ppid never changes. We additionally check whether the grandparent
|
|
50
|
+
* process has been reparented to init (PID 1). When the original grandparent
|
|
51
|
+
* was already 1 (daemonized startup) the check is skipped, and on Windows
|
|
52
|
+
* where there's no cheap `ps` equivalent we also skip — so this change is
|
|
53
|
+
* strictly additive to the previous behavior.
|
|
54
|
+
*
|
|
55
|
+
* Exported for unit-testing with injected readers. Production code uses
|
|
56
|
+
* {@link defaultIsParentAlive} (captured once at module load).
|
|
57
|
+
*/
|
|
58
|
+
export function makeDefaultIsParentAlive(deps = {}) {
|
|
59
|
+
const getPpid = deps.getPpid ?? (() => process.ppid);
|
|
60
|
+
const readGp = deps.readGrandparentPpid ?? readGrandparentPpidImpl;
|
|
61
|
+
const originalPpid = getPpid();
|
|
62
|
+
const originalGrandparentPpid = readGp();
|
|
63
|
+
return () => {
|
|
64
|
+
const ppid = getPpid();
|
|
65
|
+
if (ppid !== originalPpid)
|
|
66
|
+
return false;
|
|
67
|
+
if (ppid === 0 || ppid === 1)
|
|
68
|
+
return false;
|
|
69
|
+
// Grandparent orphan check (#311): npm-exec wrappers stay alive past the
|
|
70
|
+
// session owner. If our grandparent is now PID 1 but wasn't at startup,
|
|
71
|
+
// the wrapping chain is orphaned and we should shut down.
|
|
72
|
+
if (!Number.isNaN(originalGrandparentPpid) && originalGrandparentPpid > 1) {
|
|
73
|
+
if (readGp() === 1)
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const defaultIsParentAlive = makeDefaultIsParentAlive();
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the parent-liveness poll interval based on context (#534).
|
|
82
|
+
*
|
|
83
|
+
* When this process is the MCP bridge child spawned by the Pi adapter
|
|
84
|
+
* (`bootstrapMCPTools` in `src/adapters/pi/mcp-bridge.ts` sets
|
|
85
|
+
* `CONTEXT_MODE_BRIDGE_DEPTH=1` in the child env), we tighten the poll to
|
|
86
|
+
* 1 s. The Pi parent can disappear in under 50 ms (`pi --help` prints
|
|
87
|
+
* usage and returns), so the default 30 s window leaves a long-lived
|
|
88
|
+
* CPU-spinning orphan. For top-level MCP servers (depth 0 / absent) we
|
|
89
|
+
* keep the original 30 s cadence — the existing #311/#388 ppid + stdin
|
|
90
|
+
* recovery paths already cover Claude Code style hosts.
|
|
91
|
+
*
|
|
92
|
+
* Exported for unit-testing.
|
|
93
|
+
*/
|
|
94
|
+
export function lifecycleGuardIntervalForEnv(env = process.env) {
|
|
95
|
+
const raw = env.CONTEXT_MODE_BRIDGE_DEPTH;
|
|
96
|
+
if (raw === undefined)
|
|
97
|
+
return 30_000;
|
|
98
|
+
const depth = Number.parseInt(raw, 10);
|
|
99
|
+
if (!Number.isFinite(depth) || depth <= 0)
|
|
100
|
+
return 30_000;
|
|
101
|
+
return 1000;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* #854: idle-shutdown timeout (ms) for an MCP BRIDGE CHILD. Returns 0 (disabled)
|
|
105
|
+
* unless this process is a bridge child (CONTEXT_MODE_BRIDGE_DEPTH>0). depth-0 /
|
|
106
|
+
* absent always returns 0, so the long-lived keep-alive servers that #602
|
|
107
|
+
* restored are NEVER reaped on idle. Default for bridge children is 3 min;
|
|
108
|
+
* override with CONTEXT_MODE_BRIDGE_IDLE_MS (a non-positive value disables it).
|
|
109
|
+
* The reaper additionally never fires while a tool call is in flight (see
|
|
110
|
+
* {@link noteRequestStart}), so the window only bounds how fast *abandoned*
|
|
111
|
+
* children drain — it does not cap legitimate long-running calls.
|
|
112
|
+
*
|
|
113
|
+
* Exported for unit-testing.
|
|
114
|
+
*/
|
|
115
|
+
export function bridgeChildIdleTimeoutMs(env = process.env) {
|
|
116
|
+
const depth = Number.parseInt(env.CONTEXT_MODE_BRIDGE_DEPTH ?? "", 10);
|
|
117
|
+
if (!Number.isFinite(depth) || depth <= 0)
|
|
118
|
+
return 0;
|
|
119
|
+
const raw = env.CONTEXT_MODE_BRIDGE_IDLE_MS;
|
|
120
|
+
if (raw !== undefined) {
|
|
121
|
+
const v = Number.parseInt(raw, 10);
|
|
122
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
123
|
+
}
|
|
124
|
+
return 180_000;
|
|
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
|
+
}
|
|
139
|
+
// #854 idle-reaper state, module-level by design: an MCP server is exactly one
|
|
140
|
+
// process (one StdioServerTransport + one lifecycle guard), so these are never
|
|
141
|
+
// shared across concurrent servers in production. Multiple startLifecycleGuard()
|
|
142
|
+
// instances arise only in tests, which pair/reset these explicitly.
|
|
143
|
+
/** Last MCP activity timestamp (inbound message, tool-call start/end, or response). */
|
|
144
|
+
let _lastMcpActivity = Date.now();
|
|
145
|
+
/** In-flight tool-call count — the reaper never fires while this is > 0. */
|
|
146
|
+
let _inFlight = 0;
|
|
147
|
+
/**
|
|
148
|
+
* #854: record MCP activity (inbound message or response). The server calls this
|
|
149
|
+
* so the bridge-child idle reaper in {@link startLifecycleGuard} can distinguish
|
|
150
|
+
* an actively-used child from an abandoned one. Cheap; safe on the hot path.
|
|
151
|
+
*/
|
|
152
|
+
export function noteMcpActivity() {
|
|
153
|
+
_lastMcpActivity = Date.now();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* #854: mark a tool call as started. Suppresses the bridge-child idle reaper so a
|
|
157
|
+
* single long-running ctx_execute / ctx_batch_execute (which sends one inbound
|
|
158
|
+
* frame then runs unbounded, #643) is never reaped mid-execution.
|
|
159
|
+
*/
|
|
160
|
+
export function noteRequestStart() {
|
|
161
|
+
_inFlight++;
|
|
162
|
+
_lastMcpActivity = Date.now();
|
|
163
|
+
}
|
|
164
|
+
/** #854: mark a tool call as finished (success or error). */
|
|
165
|
+
export function noteRequestEnd() {
|
|
166
|
+
if (_inFlight > 0)
|
|
167
|
+
_inFlight--;
|
|
168
|
+
_lastMcpActivity = Date.now();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* #854: wrap an MCP stdio transport's `onmessage` so each inbound message
|
|
172
|
+
* refreshes the idle clock. Best-effort: call after `connect()` (onmessage set);
|
|
173
|
+
* a no-op if it isn't a function, and a throw in noteMcpActivity never breaks
|
|
174
|
+
* dispatch. No stdin touch (preserves the #236 contract). Exported for testing.
|
|
175
|
+
*/
|
|
176
|
+
export function attachMcpActivityTap(transport) {
|
|
177
|
+
if (!transport)
|
|
178
|
+
return;
|
|
179
|
+
const prev = typeof transport.onmessage === "function" ? transport.onmessage.bind(transport) : null;
|
|
180
|
+
if (!prev)
|
|
181
|
+
return;
|
|
182
|
+
transport.onmessage = (message, extra) => {
|
|
183
|
+
try {
|
|
184
|
+
noteMcpActivity();
|
|
185
|
+
}
|
|
186
|
+
catch { /* never break message dispatch */ }
|
|
187
|
+
return prev(message, extra);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Start the lifecycle guard. Returns a cleanup function.
|
|
192
|
+
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
193
|
+
*/
|
|
194
|
+
export function startLifecycleGuard(opts) {
|
|
195
|
+
const interval = opts.checkIntervalMs ?? lifecycleGuardIntervalForEnv();
|
|
196
|
+
const check = opts.isParentAlive ?? defaultIsParentAlive;
|
|
197
|
+
let stopped = false;
|
|
198
|
+
const shutdown = () => {
|
|
199
|
+
if (stopped)
|
|
200
|
+
return;
|
|
201
|
+
stopped = true;
|
|
202
|
+
opts.onShutdown();
|
|
203
|
+
};
|
|
204
|
+
// P0: Periodic parent liveness check
|
|
205
|
+
const timer = setInterval(() => {
|
|
206
|
+
if (!check())
|
|
207
|
+
shutdown();
|
|
208
|
+
}, interval);
|
|
209
|
+
timer.unref();
|
|
210
|
+
// P0: OS signals — terminal close, kill, ctrl+c
|
|
211
|
+
const signals = ["SIGTERM", "SIGINT"];
|
|
212
|
+
if (process.platform !== "win32")
|
|
213
|
+
signals.push("SIGHUP");
|
|
214
|
+
for (const sig of signals)
|
|
215
|
+
process.on(sig, shutdown);
|
|
216
|
+
// P0: Stdin-EOF assist (#311/#388). The vendored MCP SDK's
|
|
217
|
+
// StdioServerTransport only registers 'data' / 'error' listeners — not
|
|
218
|
+
// 'end' — so when the parent (e.g. Claude Code) dies abruptly without
|
|
219
|
+
// sending SIGTERM, the server keeps reading from a half-closed pipe and
|
|
220
|
+
// CPU-spins until the 30 s ppid poll catches up. Observed in #388 with
|
|
221
|
+
// single processes accumulating ~80 h of CPU time before SIGKILL.
|
|
222
|
+
//
|
|
223
|
+
// We deliberately DO NOT call shutdown() unconditionally on 'end' — that
|
|
224
|
+
// is exactly the false-positive behavior #236 tore out. Instead we run
|
|
225
|
+
// the same isParentAlive() check the periodic timer uses, just earlier.
|
|
226
|
+
// If the parent is alive, this is a no-op and the existing #236
|
|
227
|
+
// regression test still passes; if the parent is gone, we collapse the
|
|
228
|
+
// 30 s detection window to ~0.
|
|
229
|
+
//
|
|
230
|
+
// Skipped on TTY (OpenCode ts-plugin) where stdin is not the MCP channel.
|
|
231
|
+
const onStdinEnd = () => {
|
|
232
|
+
if (!check())
|
|
233
|
+
shutdown();
|
|
234
|
+
};
|
|
235
|
+
if (!process.stdin.isTTY) {
|
|
236
|
+
process.stdin.on("end", onStdinEnd);
|
|
237
|
+
}
|
|
238
|
+
// #854: request-idle self-shutdown for MCP BRIDGE CHILDREN only
|
|
239
|
+
// (CONTEXT_MODE_BRIDGE_DEPTH>0). Pi/omp loads the extension once per
|
|
240
|
+
// sub-context and spawns one bridge child each, tearing them down only at
|
|
241
|
+
// session_shutdown — which never fires for sub-contexts while the long-lived
|
|
242
|
+
// parent stays alive, so idle children accumulate (#854, same class as #565).
|
|
243
|
+
// A bridge child that receives no inbound MCP message for `idleMs` exits
|
|
244
|
+
// itself; the extension's single-flight path respawns one on the next call.
|
|
245
|
+
//
|
|
246
|
+
// Scoped strictly to depth>0 so the depth-0 keep-alive servers that #602
|
|
247
|
+
// restored are never reaped on idle. The trigger is idle TIME via
|
|
248
|
+
// noteMcpActivity() (NOT stdin EOF), so the #236 contract — and lifecycle's
|
|
249
|
+
// hands-off-stdin invariant — are untouched.
|
|
250
|
+
const idleMs = opts.bridgeIdleMs ?? bridgeChildIdleTimeoutMs();
|
|
251
|
+
let idleTimer;
|
|
252
|
+
if (idleMs > 0) {
|
|
253
|
+
_lastMcpActivity = Date.now();
|
|
254
|
+
idleTimer = setInterval(() => {
|
|
255
|
+
// Reap only when truly quiescent: NO tool call in flight AND no MCP
|
|
256
|
+
// activity for `idleMs`. The in-flight guard prevents reaping a child
|
|
257
|
+
// mid-execution during a long single ctx_execute/batch that sends no
|
|
258
|
+
// further messages (#643 unbounded calls) — the false-reap regression the
|
|
259
|
+
// adversarial review flagged.
|
|
260
|
+
if (_inFlight === 0 && Date.now() - _lastMcpActivity >= idleMs) {
|
|
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");
|
|
264
|
+
shutdown();
|
|
265
|
+
}
|
|
266
|
+
}, Math.max(1000, Math.min(Math.floor(idleMs / 4), 30_000)));
|
|
267
|
+
idleTimer.unref();
|
|
268
|
+
}
|
|
269
|
+
return () => {
|
|
270
|
+
stopped = true;
|
|
271
|
+
clearInterval(timer);
|
|
272
|
+
if (idleTimer)
|
|
273
|
+
clearInterval(idleTimer);
|
|
274
|
+
for (const sig of signals)
|
|
275
|
+
process.removeListener(sig, shutdown);
|
|
276
|
+
process.stdin.removeListener("end", onStdinEnd);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the npm package identity of context-mode.
|
|
3
|
+
*
|
|
4
|
+
* v2.0.0 fork rename: the npm package name is scoped
|
|
5
|
+
* ("@mxalbert/context-mode") while the plugin/display id stays
|
|
6
|
+
* "context-mode" (bin name, MCP server name, hook names, storage dirs).
|
|
7
|
+
* Mirrors the constants in scripts/postinstall.mjs and
|
|
8
|
+
* scripts/heal-installed-plugins.mjs.
|
|
9
|
+
*/
|
|
10
|
+
/** npm package name — what `npm install -g` / registry URLs resolve. */
|
|
11
|
+
export declare const PACKAGE_NAME = "@mxalbert/context-mode";
|
|
12
|
+
/** Plugin/display id — unchanged brand string (NOT the npm package name). */
|
|
13
|
+
export declare const PACKAGE_SLUG = "context-mode";
|
|
14
|
+
/**
|
|
15
|
+
* Claude Code registry key: "<pluginId>@<npmPackage>". A scoped package
|
|
16
|
+
* yields a double-@ key ("context-mode@@mxalbert/context-mode").
|
|
17
|
+
*/
|
|
18
|
+
export declare const PLUGIN_KEY = "context-mode@@mxalbert/context-mode";
|
|
19
|
+
/** npm registry endpoint for the latest published version. */
|
|
20
|
+
export declare const NPM_LATEST_URL: string;
|
|
21
|
+
/**
|
|
22
|
+
* Per-package cache layout OpenCode/KiloCode use for npm-installed plugins:
|
|
23
|
+
* POSIX : ~/.cache/<platform>/packages/@mxalbert/context-mode@latest/node_modules/@mxalbert/context-mode
|
|
24
|
+
* Windows: %LOCALAPPDATA%\<platform>\packages\@mxalbert\context-mode@latest\node_modules\@mxalbert\context-mode
|
|
25
|
+
*/
|
|
26
|
+
export declare function packageCachePath(): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the npm package identity of context-mode.
|
|
3
|
+
*
|
|
4
|
+
* v2.0.0 fork rename: the npm package name is scoped
|
|
5
|
+
* ("@mxalbert/context-mode") while the plugin/display id stays
|
|
6
|
+
* "context-mode" (bin name, MCP server name, hook names, storage dirs).
|
|
7
|
+
* Mirrors the constants in scripts/postinstall.mjs and
|
|
8
|
+
* scripts/heal-installed-plugins.mjs.
|
|
9
|
+
*/
|
|
10
|
+
/** npm package name — what `npm install -g` / registry URLs resolve. */
|
|
11
|
+
export const PACKAGE_NAME = "@mxalbert/context-mode";
|
|
12
|
+
/** Plugin/display id — unchanged brand string (NOT the npm package name). */
|
|
13
|
+
export const PACKAGE_SLUG = "context-mode";
|
|
14
|
+
/**
|
|
15
|
+
* Claude Code registry key: "<pluginId>@<npmPackage>". A scoped package
|
|
16
|
+
* yields a double-@ key ("context-mode@@mxalbert/context-mode").
|
|
17
|
+
*/
|
|
18
|
+
export const PLUGIN_KEY = `${PACKAGE_SLUG}@${PACKAGE_NAME}`;
|
|
19
|
+
/** npm registry endpoint for the latest published version. */
|
|
20
|
+
export const NPM_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
21
|
+
/**
|
|
22
|
+
* Per-package cache layout OpenCode/KiloCode use for npm-installed plugins:
|
|
23
|
+
* POSIX : ~/.cache/<platform>/packages/@mxalbert/context-mode@latest/node_modules/@mxalbert/context-mode
|
|
24
|
+
* Windows: %LOCALAPPDATA%\<platform>\packages\@mxalbert\context-mode@latest\node_modules\@mxalbert\context-mode
|
|
25
|
+
*/
|
|
26
|
+
export function packageCachePath() {
|
|
27
|
+
return ["packages", `${PACKAGE_NAME}@latest`, "node_modules", PACKAGE_NAME];
|
|
28
|
+
}
|