@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,818 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw TypeScript plugin entry point for context-mode.
|
|
3
|
+
*
|
|
4
|
+
* Exports an object with { id, name, configSchema, register(api) } for
|
|
5
|
+
* declarative metadata and config validation before code execution.
|
|
6
|
+
*
|
|
7
|
+
* register(api) registers:
|
|
8
|
+
* - before_tool_call hook — Routing enforcement (deny/modify/passthrough)
|
|
9
|
+
* - after_tool_call hook — Session event capture
|
|
10
|
+
* - command:new hook — Session initialization and cleanup
|
|
11
|
+
* - session_start hook — Re-key DB session to OpenClaw's session ID
|
|
12
|
+
* - before_compaction hook — Flush events to resume snapshot
|
|
13
|
+
* - after_compaction hook — Increment compact count
|
|
14
|
+
* - before_prompt_build (p=10) — Resume snapshot injection into system context
|
|
15
|
+
* - before_prompt_build (p=5) — Routing instruction injection into system context
|
|
16
|
+
* - context-mode engine — Context engine with compaction management
|
|
17
|
+
* - /ctx-stats command — Auto-reply command for session statistics
|
|
18
|
+
* - /ctx-doctor command — Auto-reply command for diagnostics
|
|
19
|
+
* - /ctx-upgrade command — Auto-reply command for upgrade
|
|
20
|
+
*
|
|
21
|
+
* Loaded by OpenClaw via: openclaw.extensions entry in package.json
|
|
22
|
+
*
|
|
23
|
+
* OpenClaw plugin paradigm:
|
|
24
|
+
* - Plugins export { id, name, configSchema, register(api) } for metadata
|
|
25
|
+
* - api.registerHook() for event-driven hooks
|
|
26
|
+
* - api.on() for typed lifecycle hooks
|
|
27
|
+
* - api.registerContextEngine() for compaction ownership
|
|
28
|
+
* - api.registerCommand() for auto-reply slash commands
|
|
29
|
+
* - Plugins run in-process with the Gateway (trusted code)
|
|
30
|
+
*/
|
|
31
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { dirname, join, resolve } from "node:path";
|
|
35
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
36
|
+
import { resolveContextModeDataRoot } from "../base.js";
|
|
37
|
+
import { resolveSessionDbPath } from "../../session/db.js";
|
|
38
|
+
import { OpenClawSessionDB } from "./session-db.js";
|
|
39
|
+
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
40
|
+
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
41
|
+
import { WorkspaceRouter } from "./workspace-router.js";
|
|
42
|
+
import { handleOpenclawUsageEvent } from "./usage.js";
|
|
43
|
+
import { buildNodeCommand } from "../types.js";
|
|
44
|
+
import { OPENCLAW_TOOL_DEFS } from "./mcp-tools.js";
|
|
45
|
+
// ── System-reminder filter (CCv2 — SLICE OClaw-3) ─────────
|
|
46
|
+
// Mirror hooks/userpromptsubmit.mjs:30-33: skip system-generated wrappers
|
|
47
|
+
// so before_model_resolve never inserts spurious user-prompt events.
|
|
48
|
+
const SYSTEM_REMINDER_PREFIXES = [
|
|
49
|
+
"<system-reminder>",
|
|
50
|
+
"<task-notification>",
|
|
51
|
+
"<context_guidance>",
|
|
52
|
+
"<tool-result>",
|
|
53
|
+
];
|
|
54
|
+
const SKILL_FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n?/;
|
|
55
|
+
const OPENCLAW_SKILL_PROMPT_CHAR_LIMIT = 6000;
|
|
56
|
+
const SKILL_SECTION_HEADINGS = [
|
|
57
|
+
"MANDATORY RULE",
|
|
58
|
+
"Decision Tree",
|
|
59
|
+
"When to Use Each Tool",
|
|
60
|
+
"Critical Rules",
|
|
61
|
+
];
|
|
62
|
+
function isSystemReminderMessage(msg) {
|
|
63
|
+
const trimmed = msg.trimStart();
|
|
64
|
+
for (const prefix of SYSTEM_REMINDER_PREFIXES) {
|
|
65
|
+
if (trimmed.startsWith(prefix))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function stripMarkdownFrontmatter(markdown) {
|
|
71
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
72
|
+
return normalized.replace(SKILL_FRONTMATTER_REGEX, "").trim();
|
|
73
|
+
}
|
|
74
|
+
function extractMarkdownSection(markdown, heading) {
|
|
75
|
+
const sectionHeader = `## ${heading}`;
|
|
76
|
+
const start = markdown.indexOf(sectionHeader);
|
|
77
|
+
if (start === -1)
|
|
78
|
+
return null;
|
|
79
|
+
const afterHeader = markdown.slice(start + sectionHeader.length);
|
|
80
|
+
const nextHeadingOffset = afterHeader.search(/\n##\s+|\n#\s+/);
|
|
81
|
+
const body = (nextHeadingOffset === -1 ? afterHeader : afterHeader.slice(0, nextHeadingOffset)).trim();
|
|
82
|
+
if (!body)
|
|
83
|
+
return null;
|
|
84
|
+
return `${sectionHeader}\n\n${body}`;
|
|
85
|
+
}
|
|
86
|
+
function buildOpenClawSkillLikeGuidance(skillMarkdown) {
|
|
87
|
+
const skillBody = stripMarkdownFrontmatter(skillMarkdown);
|
|
88
|
+
const skillTitle = skillBody.match(/^#\s+.+$/m)?.[0]?.trim() ?? "# Context Mode Skill";
|
|
89
|
+
const selectedSections = SKILL_SECTION_HEADINGS
|
|
90
|
+
.map((heading) => extractMarkdownSection(skillBody, heading))
|
|
91
|
+
.filter((section) => Boolean(section));
|
|
92
|
+
const rawContent = (selectedSections.length > 0
|
|
93
|
+
? [skillTitle, ...selectedSections].join("\n\n")
|
|
94
|
+
: skillBody).trim();
|
|
95
|
+
const boundedContent = rawContent.length > OPENCLAW_SKILL_PROMPT_CHAR_LIMIT
|
|
96
|
+
? `${rawContent.slice(0, OPENCLAW_SKILL_PROMPT_CHAR_LIMIT).trimEnd()}\n\n[skill excerpt truncated for prompt budget]`
|
|
97
|
+
: rawContent;
|
|
98
|
+
return [
|
|
99
|
+
"<context_mode_skill_like_guidance source=\"skills/context-mode/SKILL.md\">",
|
|
100
|
+
boundedContent,
|
|
101
|
+
"</context_mode_skill_like_guidance>",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
/** Plugin config schema for OpenClaw validation. */
|
|
105
|
+
const configSchema = {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
enabled: {
|
|
109
|
+
type: "boolean",
|
|
110
|
+
default: true,
|
|
111
|
+
description: "Enable or disable the context-mode plugin.",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
};
|
|
116
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
117
|
+
function getSessionDir() {
|
|
118
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
119
|
+
// ahead of the hardcoded ~/.openclaw root so dev-container/CI/NFS-home
|
|
120
|
+
// users can relocate context-mode storage without patching the source.
|
|
121
|
+
// Kept in sync with OpenClawAdapter.getSessionDir() (inherited from
|
|
122
|
+
// BaseAdapter) — both call sites must agree byte-for-byte.
|
|
123
|
+
const override = resolveContextModeDataRoot();
|
|
124
|
+
const dir = override
|
|
125
|
+
? join(override, "context-mode", "sessions")
|
|
126
|
+
: join(homedir(), ".openclaw", "context-mode", "sessions");
|
|
127
|
+
mkdirSync(dir, { recursive: true });
|
|
128
|
+
return dir;
|
|
129
|
+
}
|
|
130
|
+
// Issue #645 follow-up — route through the canonical per-project
|
|
131
|
+
// resolver the MCP server uses (src/server.ts ctx_stats / ctx_search
|
|
132
|
+
// timeline). The previous raw `sha256(projectDir).slice(0, 16)` shape
|
|
133
|
+
// produced a different file from the `<canonical-hash>.db` the server
|
|
134
|
+
// reads on darwin/win32 (case-fold) and inside linked worktrees
|
|
135
|
+
// (suffix). The result was silent degradation of `ctx_stats` (zero
|
|
136
|
+
// history) and `ctx_search(sort: "timeline")` (sort dropped) for any
|
|
137
|
+
// OpenClaw user with an uppercase character in their projectDir.
|
|
138
|
+
// Mirrors the matching Pi (src/adapters/pi/extension.ts:223) and OMP
|
|
139
|
+
// (src/adapters/omp/plugin.ts:90) fixes, and the opencode plugin
|
|
140
|
+
// pattern (src/adapters/opencode/plugin.ts:307).
|
|
141
|
+
function getDBPath(projectDir) {
|
|
142
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
143
|
+
}
|
|
144
|
+
// ── Module-level DB singleton ─────────────────────────────
|
|
145
|
+
// Shared across all register() calls (one per agent session).
|
|
146
|
+
// Lazy-initialized on first register() using the first projectDir seen.
|
|
147
|
+
// Uses OpenClawSessionDB for session_key mapping and rename support.
|
|
148
|
+
let _dbSingleton = null;
|
|
149
|
+
let _dbSingletonPath = "";
|
|
150
|
+
function getOrCreateDB(projectDir) {
|
|
151
|
+
// Reopen the singleton if the resolved DB path changes. Production
|
|
152
|
+
// code normally loads the plugin once per process with a single
|
|
153
|
+
// workspace, but defensive re-keying on resolved path keeps the
|
|
154
|
+
// contract honest if a host ever calls register() twice with
|
|
155
|
+
// different projectDirs, and removes a subtle test-isolation
|
|
156
|
+
// foot-gun where a stale singleton pointed at a prior test's
|
|
157
|
+
// `<hash>.db`. Mirrors the Pi/OMP fix (#645).
|
|
158
|
+
const dbPath = getDBPath(projectDir);
|
|
159
|
+
if (!_dbSingleton || _dbSingletonPath !== dbPath) {
|
|
160
|
+
if (_dbSingleton) {
|
|
161
|
+
try {
|
|
162
|
+
_dbSingleton.close();
|
|
163
|
+
}
|
|
164
|
+
catch { /* best effort */ }
|
|
165
|
+
}
|
|
166
|
+
_dbSingleton = new OpenClawSessionDB({ dbPath });
|
|
167
|
+
_dbSingletonPath = dbPath;
|
|
168
|
+
_dbSingleton.cleanupOldSessions(7);
|
|
169
|
+
}
|
|
170
|
+
return _dbSingleton;
|
|
171
|
+
}
|
|
172
|
+
// ── Module-level state for command handlers ───────────────
|
|
173
|
+
// Commands are re-registered on each register() call (OpenClaw's registerCommand
|
|
174
|
+
// is idempotent). These refs give handlers access to the current session's state.
|
|
175
|
+
let _latestDb = null;
|
|
176
|
+
let _latestSessionId = "";
|
|
177
|
+
let _latestPluginRoot = "";
|
|
178
|
+
// ── Plugin Definition (object export) ─────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* OpenClaw plugin definition. The object form provides declarative metadata
|
|
181
|
+
* (id, name, configSchema) that OpenClaw can read without executing code.
|
|
182
|
+
* register() is called once per agent session with a fresh api object.
|
|
183
|
+
* Each call creates isolated closures (db, sessionId, hooks) — no shared state.
|
|
184
|
+
*/
|
|
185
|
+
export default {
|
|
186
|
+
id: "context-mode",
|
|
187
|
+
name: "Context Mode",
|
|
188
|
+
configSchema,
|
|
189
|
+
// OpenClaw calls register() synchronously — returning a Promise causes hooks
|
|
190
|
+
// to be silently ignored. Async init runs eagerly; hooks await it on first use.
|
|
191
|
+
register(api) {
|
|
192
|
+
// Resolve build dir from compiled JS location
|
|
193
|
+
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
194
|
+
const projectDir = process.cwd();
|
|
195
|
+
const pluginRoot = resolve(buildDir, "..", "..", "..");
|
|
196
|
+
// Structured logger — wraps api.logger, falls back to no-op.
|
|
197
|
+
// info/error always emit; debug only when api.logger.debug is present
|
|
198
|
+
// (i.e. OpenClaw running with --log-level debug or lower).
|
|
199
|
+
const log = {
|
|
200
|
+
info: (...args) => api.logger?.info?.("[context-mode]", ...args),
|
|
201
|
+
error: (...args) => api.logger?.error?.("[context-mode]", ...args),
|
|
202
|
+
debug: (...args) => api.logger?.debug?.("[context-mode]", ...args),
|
|
203
|
+
warn: (...args) => api.logger?.warn?.("[context-mode]", ...args),
|
|
204
|
+
};
|
|
205
|
+
const registerCommandHook = (event, handler, meta) => {
|
|
206
|
+
if (api.registerHook) {
|
|
207
|
+
api.registerHook(event, handler, meta);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
api.on(event, handler);
|
|
212
|
+
log.debug("command hook registered via api.on fallback", { event });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
log.warn?.("command hook registration skipped", { event }, err);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const registerAutoReplyCommand = (command) => {
|
|
219
|
+
if (!api.registerCommand)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
api.registerCommand(command);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
log.warn?.("registerCommand failed; skipping auto-reply command", { name: command.name }, err);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// Get shared DB singleton (lazy-init on first register() call)
|
|
229
|
+
const db = getOrCreateDB(projectDir);
|
|
230
|
+
// Start with temp UUID — session_start will assign the real ID + sessionKey
|
|
231
|
+
let sessionId = randomUUID();
|
|
232
|
+
log.info("register() called, sessionId:", sessionId.slice(0, 8));
|
|
233
|
+
// SLICE OClaw-6 (F6 retraction): `resumeInjected` is correctly scoped
|
|
234
|
+
// per-register() singleton — Phase 7 confirmed F6 fabrication-as-tech-debt.
|
|
235
|
+
// Each OpenClaw agent session calls register() once and gets its own
|
|
236
|
+
// closure; the flag prevents double-injection of the resume snapshot in
|
|
237
|
+
// back-to-back before_prompt_build calls within the same session. Do not
|
|
238
|
+
// promote to module scope.
|
|
239
|
+
let resumeInjected = false;
|
|
240
|
+
let sessionKey;
|
|
241
|
+
// Create temp session so after_tool_call events before session_start have a valid row
|
|
242
|
+
db.ensureSession(sessionId, projectDir);
|
|
243
|
+
const workspaceRouter = new WorkspaceRouter();
|
|
244
|
+
// Async init: load routing module + dynamic routing-block factory.
|
|
245
|
+
// SLICE OClaw-2: replaced static readFileSync(configs/openclaw/AGENTS.md)
|
|
246
|
+
// with createRoutingBlock(createToolNamer("openclaw")) so OpenClaw-specific
|
|
247
|
+
// MCP-prefix substitution stays in lockstep with hooks/routing-block.mjs.
|
|
248
|
+
let routingInstructions = "";
|
|
249
|
+
let skillLikeInstructions = "";
|
|
250
|
+
const initPromise = (async () => {
|
|
251
|
+
const routingPath = resolve(buildDir, "..", "..", "..", "hooks", "core", "routing.mjs");
|
|
252
|
+
const routing = await import(pathToFileURL(routingPath).href);
|
|
253
|
+
// initSecurity() looks for `<dir>/security.js`, which lives at the
|
|
254
|
+
// top of build/ — two levels up from this adapter directory.
|
|
255
|
+
const buildRoot = resolve(buildDir, "..", "..");
|
|
256
|
+
await routing.initSecurity(buildRoot);
|
|
257
|
+
try {
|
|
258
|
+
const blockMod = await import(pathToFileURL(resolve(buildDir, "..", "..", "..", "hooks", "routing-block.mjs")).href);
|
|
259
|
+
const namingMod = await import(pathToFileURL(resolve(buildDir, "..", "..", "..", "hooks", "core", "tool-naming.mjs")).href);
|
|
260
|
+
const toolNamer = namingMod.createToolNamer("openclaw");
|
|
261
|
+
routingInstructions = blockMod.createRoutingBlock(toolNamer);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
log.warn?.("failed to build dynamic routing block", err);
|
|
265
|
+
// Fallback: legacy disk-read of AGENTS.md (kept for resilience only —
|
|
266
|
+
// primary path is the dynamic factory above).
|
|
267
|
+
try {
|
|
268
|
+
const instructionsPath = resolve(buildDir, "..", "configs", "openclaw", "AGENTS.md");
|
|
269
|
+
if (existsSync(instructionsPath)) {
|
|
270
|
+
routingInstructions = readFileSync(instructionsPath, "utf-8");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// best effort
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const skillPath = resolve(pluginRoot, "skills", "context-mode", "SKILL.md");
|
|
279
|
+
if (existsSync(skillPath)) {
|
|
280
|
+
skillLikeInstructions = buildOpenClawSkillLikeGuidance(readFileSync(skillPath, "utf-8"));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
log.debug("context-mode skill file missing; skipping skill-like prompt injection", { skillPath });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
log.warn?.("failed to build skill-like guidance from SKILL.md", err);
|
|
288
|
+
}
|
|
289
|
+
return { routing };
|
|
290
|
+
})();
|
|
291
|
+
// ── 1. tool_call:before — Routing enforcement ──────────
|
|
292
|
+
// NOTE: api.on() was broken in OpenClaw ≤2026.1.29 (fixed in PR #9761, issue #5513).
|
|
293
|
+
// api.on() is the correct API for typed lifecycle hooks (session_start, before_tool_call, etc.).
|
|
294
|
+
// api.registerHook() is for generic/command hooks (command:new, command:reset, command:stop).
|
|
295
|
+
api.on("before_tool_call", async (event) => {
|
|
296
|
+
const { routing } = await initPromise;
|
|
297
|
+
const e = event;
|
|
298
|
+
const toolName = e.toolName ?? "";
|
|
299
|
+
const toolInput = e.params ?? {};
|
|
300
|
+
let decision;
|
|
301
|
+
try {
|
|
302
|
+
decision = routing.routePreToolUse(toolName, toolInput, projectDir, "openclaw");
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return; // Routing failure → allow passthrough
|
|
306
|
+
}
|
|
307
|
+
if (!decision)
|
|
308
|
+
return; // No routing match → passthrough
|
|
309
|
+
log.debug("before_tool_call", { tool: toolName, action: decision.action });
|
|
310
|
+
if (decision.action === "deny" || decision.action === "ask") {
|
|
311
|
+
return {
|
|
312
|
+
block: true,
|
|
313
|
+
blockReason: decision.reason ?? "Blocked by context-mode",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (decision.action === "modify" && decision.updatedInput) {
|
|
317
|
+
// In-place mutation — OpenClaw reads the mutated params object.
|
|
318
|
+
Object.assign(toolInput, decision.updatedInput);
|
|
319
|
+
}
|
|
320
|
+
// "context" action → handled by before_prompt_build, not inline
|
|
321
|
+
});
|
|
322
|
+
// ── 2. after_tool_call — Session event capture ─────────
|
|
323
|
+
// Map OpenClaw tool names → Claude Code equivalents so extractEvents
|
|
324
|
+
// can recognize them. OpenClaw uses lowercase names; CC uses PascalCase.
|
|
325
|
+
const OPENCLAW_TOOL_MAP = {
|
|
326
|
+
exec: "Bash",
|
|
327
|
+
read: "Read",
|
|
328
|
+
write: "Write",
|
|
329
|
+
edit: "Edit",
|
|
330
|
+
apply_patch: "Edit",
|
|
331
|
+
glob: "Glob",
|
|
332
|
+
grep: "Grep",
|
|
333
|
+
search: "Grep",
|
|
334
|
+
};
|
|
335
|
+
api.on("after_tool_call", async (event) => {
|
|
336
|
+
try {
|
|
337
|
+
const e = event;
|
|
338
|
+
const rawToolName = e.toolName ?? "";
|
|
339
|
+
const mappedToolName = OPENCLAW_TOOL_MAP[rawToolName] ?? rawToolName;
|
|
340
|
+
// Accept both result (v2+) and output (older builds)
|
|
341
|
+
const rawResult = e.result ?? e.output;
|
|
342
|
+
const resultStr = typeof rawResult === "string"
|
|
343
|
+
? rawResult
|
|
344
|
+
: rawResult != null
|
|
345
|
+
? JSON.stringify(rawResult)
|
|
346
|
+
: undefined;
|
|
347
|
+
// Accept both error (string, v2+) and isError (boolean, older builds)
|
|
348
|
+
const hasError = Boolean(e.error || e.isError);
|
|
349
|
+
const hookInput = {
|
|
350
|
+
tool_name: mappedToolName,
|
|
351
|
+
tool_input: e.params ?? {},
|
|
352
|
+
tool_response: resultStr,
|
|
353
|
+
tool_output: hasError ? { isError: true } : undefined,
|
|
354
|
+
};
|
|
355
|
+
const events = extractEvents(hookInput);
|
|
356
|
+
// Resolve agent-specific sessionId from workspace paths in params
|
|
357
|
+
const routedSessionId = workspaceRouter.resolveSessionId(e.params ?? {}) ?? sessionId;
|
|
358
|
+
if (events.length > 0) {
|
|
359
|
+
for (const ev of events) {
|
|
360
|
+
db.insertEvent(routedSessionId, ev, "PostToolUse");
|
|
361
|
+
}
|
|
362
|
+
log.debug("after_tool_call", { tool: rawToolName, mapped: mappedToolName, sessionId: routedSessionId.slice(0, 8), events: events.length, durationMs: e.durationMs });
|
|
363
|
+
}
|
|
364
|
+
else if (rawToolName) {
|
|
365
|
+
// Fallback: record any unrecognized tool call as a generic event
|
|
366
|
+
const data = JSON.stringify({
|
|
367
|
+
tool: rawToolName,
|
|
368
|
+
params: e.params,
|
|
369
|
+
durationMs: e.durationMs,
|
|
370
|
+
});
|
|
371
|
+
db.insertEvent(routedSessionId, {
|
|
372
|
+
type: "tool_call",
|
|
373
|
+
category: "openclaw",
|
|
374
|
+
data,
|
|
375
|
+
priority: 1,
|
|
376
|
+
data_hash: createHash("sha256")
|
|
377
|
+
.update(data)
|
|
378
|
+
.digest("hex")
|
|
379
|
+
.slice(0, 16),
|
|
380
|
+
}, "PostToolUse");
|
|
381
|
+
log.debug("after_tool_call", { tool: rawToolName, mapped: rawToolName, sessionId: routedSessionId.slice(0, 8), events: 1, durationMs: e.durationMs });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Silent — session capture must never break the tool call
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
// ── 2b. model.usage — Per-turn token + cost capture ──────
|
|
389
|
+
// openclaw emits a first-class `model.usage` diagnostic event once per turn
|
|
390
|
+
// carrying the full usage breakdown + a pre-computed costUsd. We subscribe
|
|
391
|
+
// to the diagnostic-event bus (NOT the tool-call hook — before/after_tool_call
|
|
392
|
+
// carry approval/policy data only, no token usage). The handler
|
|
393
|
+
// (handleOpenclawUsageEvent) is decoupled + unit-tested; it parses → builds →
|
|
394
|
+
// inserts and never throws.
|
|
395
|
+
//
|
|
396
|
+
// tsc-safe SDK access: `onDiagnosticEvent` comes from the openclaw plugin SDK
|
|
397
|
+
// (`openclaw/plugin-sdk/diagnostic-runtime`), which is NOT in this repo's
|
|
398
|
+
// node_modules — a static import would break the build. We resolve it at
|
|
399
|
+
// runtime two ways, both best-effort:
|
|
400
|
+
// 1. directly off the activation `api` if the host surfaces it there;
|
|
401
|
+
// 2. otherwise a guarded dynamic import via a COMPUTED specifier string
|
|
402
|
+
// (TS treats it as Promise<any> and skips module resolution, mirroring
|
|
403
|
+
// the pathToFileURL(...).href dynamic imports above). Missing SDK → no-op.
|
|
404
|
+
const subscribeDiagnostics = (onDiag) => {
|
|
405
|
+
try {
|
|
406
|
+
onDiag((evt) => {
|
|
407
|
+
try {
|
|
408
|
+
const sid = sessionId; // snapshot to avoid race with session_start re-key
|
|
409
|
+
handleOpenclawUsageEvent(evt, (e) => db.insertEvent(sid, e, "Diagnostic"));
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Usage capture must never break the agent turn.
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
log.debug("model.usage diagnostic subscription registered");
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
log.warn?.("model.usage diagnostic subscription failed", err);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
if (typeof api.onDiagnosticEvent === "function") {
|
|
422
|
+
subscribeDiagnostics(api.onDiagnosticEvent.bind(api));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Computed specifier so NodeNext does not try to resolve the (absent) SDK
|
|
426
|
+
// module at build time — the dynamic import is typed as Promise<any>.
|
|
427
|
+
const diagnosticRuntimeSpecifier = ["openclaw", "plugin-sdk", "diagnostic-runtime"].join("/");
|
|
428
|
+
void (async () => {
|
|
429
|
+
try {
|
|
430
|
+
const mod = (await import(diagnosticRuntimeSpecifier));
|
|
431
|
+
if (typeof mod?.onDiagnosticEvent === "function") {
|
|
432
|
+
subscribeDiagnostics(mod.onDiagnosticEvent);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
log.debug("diagnostic-runtime loaded but onDiagnosticEvent missing");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// SDK not installed (dev/test) — usage capture silently inert.
|
|
440
|
+
log.debug("openclaw plugin-sdk/diagnostic-runtime unavailable — usage capture inert");
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
443
|
+
}
|
|
444
|
+
// ── 3. command:new — Session initialization ────────────
|
|
445
|
+
registerCommandHook("command:new", async () => {
|
|
446
|
+
try {
|
|
447
|
+
log.debug("command:new", { sessionId: sessionId.slice(0, 8) });
|
|
448
|
+
db.cleanupOldSessions(7);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// best effort
|
|
452
|
+
}
|
|
453
|
+
}, {
|
|
454
|
+
name: "context-mode.session-new",
|
|
455
|
+
description: "Session initialization — cleans up old sessions on /new command",
|
|
456
|
+
});
|
|
457
|
+
// ── 3b. command:reset / command:stop — Session cleanup ────
|
|
458
|
+
registerCommandHook("command:reset", async () => {
|
|
459
|
+
try {
|
|
460
|
+
log.debug("command:reset", { sessionId: sessionId.slice(0, 8) });
|
|
461
|
+
db.cleanupOldSessions(7);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// best effort
|
|
465
|
+
}
|
|
466
|
+
}, {
|
|
467
|
+
name: "context-mode.session-reset",
|
|
468
|
+
description: "Session cleanup on /reset command",
|
|
469
|
+
});
|
|
470
|
+
registerCommandHook("command:stop", async () => {
|
|
471
|
+
try {
|
|
472
|
+
log.debug("command:stop", { sessionId: sessionId.slice(0, 8), sessionKey });
|
|
473
|
+
if (sessionKey) {
|
|
474
|
+
workspaceRouter.removeSession(sessionKey);
|
|
475
|
+
}
|
|
476
|
+
db.cleanupOldSessions(7);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// best effort
|
|
480
|
+
}
|
|
481
|
+
}, {
|
|
482
|
+
name: "context-mode.session-stop",
|
|
483
|
+
description: "Session cleanup on /stop command",
|
|
484
|
+
});
|
|
485
|
+
// ── 4. session_start — Re-key DB session to OpenClaw's session ID ─
|
|
486
|
+
api.on("session_start", async (event) => {
|
|
487
|
+
try {
|
|
488
|
+
const e = event;
|
|
489
|
+
const sid = e?.sessionId;
|
|
490
|
+
if (!sid)
|
|
491
|
+
return;
|
|
492
|
+
const key = e?.sessionKey;
|
|
493
|
+
const resumedFrom = e?.resumedFrom;
|
|
494
|
+
log.debug("session_start", { sessionId: sid.slice(0, 8), sessionKey: key, resumedFrom });
|
|
495
|
+
if (key) {
|
|
496
|
+
// Per-agent session lookup via sessionKey
|
|
497
|
+
const prevId = db.getMostRecentSession(key);
|
|
498
|
+
if (prevId && prevId !== sid) {
|
|
499
|
+
db.renameSession(prevId, sid);
|
|
500
|
+
log.info(`session re-keyed ${prevId.slice(0, 8)}… → ${sid.slice(0, 8)}… (key=${key})`);
|
|
501
|
+
}
|
|
502
|
+
else if (!prevId) {
|
|
503
|
+
db.ensureSessionWithKey(sid, projectDir, key);
|
|
504
|
+
log.info(`new session ${sid.slice(0, 8)}… (key=${key})`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
// Fallback: no sessionKey → fresh session (Option A)
|
|
509
|
+
db.ensureSession(sid, projectDir);
|
|
510
|
+
log.info(`session ${sid.slice(0, 8)}… (no sessionKey — fallback)`);
|
|
511
|
+
}
|
|
512
|
+
sessionId = sid;
|
|
513
|
+
_latestSessionId = sessionId;
|
|
514
|
+
sessionKey = key;
|
|
515
|
+
if (key) {
|
|
516
|
+
workspaceRouter.registerSession(key, sessionId);
|
|
517
|
+
}
|
|
518
|
+
resumeInjected = false;
|
|
519
|
+
// Write routing instructions (AGENTS.md) now that we know the real
|
|
520
|
+
// workspace. Derive the workspace directory from the sessionKey so we
|
|
521
|
+
// only write into recognised /.openclaw/workspace* paths, never into
|
|
522
|
+
// the gateway's cwd or any other arbitrary directory.
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// best effort — never break session start
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// ── 5. before_compaction — Flush events to snapshot before compaction ─
|
|
529
|
+
// NOTE: OpenClaw compaction hooks were broken until #4967/#3728 fix.
|
|
530
|
+
// Adapter gracefully degrades — session recovery falls back to DB snapshot
|
|
531
|
+
// reconstruction when compaction events don't fire.
|
|
532
|
+
api.on("before_compaction", async () => {
|
|
533
|
+
try {
|
|
534
|
+
const sid = sessionId; // snapshot to avoid race with concurrent session_start
|
|
535
|
+
const allEvents = db.getEvents(sid);
|
|
536
|
+
log.debug("before_compaction", { sessionId: sid.slice(0, 8), events: allEvents.length });
|
|
537
|
+
if (allEvents.length === 0)
|
|
538
|
+
return;
|
|
539
|
+
const freshStats = db.getSessionStats(sid);
|
|
540
|
+
const snapshot = buildResumeSnapshot(allEvents, {
|
|
541
|
+
compactCount: (freshStats?.compact_count ?? 0) + 1,
|
|
542
|
+
});
|
|
543
|
+
db.upsertResume(sid, snapshot, allEvents.length);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// best effort — never break compaction
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
// ── 6. after_compaction — Increment compact count ─────
|
|
550
|
+
api.on("after_compaction", async () => {
|
|
551
|
+
try {
|
|
552
|
+
const sid = sessionId;
|
|
553
|
+
log.debug("after_compaction", { sessionId: sid.slice(0, 8) });
|
|
554
|
+
db.incrementCompactCount(sid); // sessionId consistent with before_compaction within same sync cycle
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// best effort
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
// ── 7. before_model_resolve — User message capture ────────
|
|
561
|
+
api.on("before_model_resolve", async (event) => {
|
|
562
|
+
try {
|
|
563
|
+
const sid = sessionId; // snapshot to avoid race with concurrent session_start
|
|
564
|
+
const e = event;
|
|
565
|
+
const messageText = e?.userMessage ?? e?.message ?? e?.content ?? "";
|
|
566
|
+
log.debug("before_model_resolve", { hasMessage: !!messageText });
|
|
567
|
+
if (!messageText)
|
|
568
|
+
return;
|
|
569
|
+
// SLICE OClaw-3: skip system-generated wrappers so we never
|
|
570
|
+
// misclassify them as user prompts. Mirrors hooks/userpromptsubmit.mjs:30-33.
|
|
571
|
+
if (isSystemReminderMessage(messageText)) {
|
|
572
|
+
log.debug("before_model_resolve[skip-system-reminder]");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const events = extractUserEvents(messageText);
|
|
576
|
+
for (const ev of events) {
|
|
577
|
+
db.insertEvent(sid, ev, "PostToolUse");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// best effort — never break model resolution
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
// ── 8. before_prompt_build — Resume snapshot injection ────
|
|
585
|
+
api.on("before_prompt_build", () => {
|
|
586
|
+
try {
|
|
587
|
+
const sid = sessionId; // snapshot to avoid race with concurrent session_start
|
|
588
|
+
const resume = db.getResume(sid);
|
|
589
|
+
log.debug("before_prompt_build[resume]", { sessionId: sid.slice(0, 8), hasResume: !!resume, injected: !resumeInjected });
|
|
590
|
+
if (resumeInjected)
|
|
591
|
+
return undefined;
|
|
592
|
+
if (!resume)
|
|
593
|
+
return undefined;
|
|
594
|
+
const freshStats = db.getSessionStats(sid);
|
|
595
|
+
if ((freshStats?.compact_count ?? 0) === 0)
|
|
596
|
+
return undefined;
|
|
597
|
+
resumeInjected = true;
|
|
598
|
+
return { prependSystemContext: resume.snapshot };
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
}, { priority: 10 });
|
|
604
|
+
// ── 8. before_prompt_build — Routing instruction injection ──
|
|
605
|
+
// SLICE OClaw-2: register unconditionally; routingInstructions is populated
|
|
606
|
+
// asynchronously by initPromise. The closure resolves the latest value at
|
|
607
|
+
// call-time, so the first prompt-build firing after dynamic-import resolution
|
|
608
|
+
// sees the dynamic ROUTING_BLOCK XML (matching hooks/routing-block.mjs).
|
|
609
|
+
api.on("before_prompt_build", () => {
|
|
610
|
+
if (!routingInstructions && !skillLikeInstructions)
|
|
611
|
+
return undefined;
|
|
612
|
+
log.debug("before_prompt_build[routing+skill]", {
|
|
613
|
+
hasRoutingInstructions: !!routingInstructions,
|
|
614
|
+
hasSkillLikeInstructions: !!skillLikeInstructions,
|
|
615
|
+
});
|
|
616
|
+
const injectedBlocks = [];
|
|
617
|
+
if (routingInstructions) {
|
|
618
|
+
// v1.0.107 — visible marker so OpenClaw users can verify the routing
|
|
619
|
+
// block reached the model (Mickey-class verification path; mirrors
|
|
620
|
+
// OpenCode + Pi adapters).
|
|
621
|
+
const marker = `<!-- context-mode: routing block injected (sessionID=${String(sessionId).slice(0, 8)}) -->`;
|
|
622
|
+
injectedBlocks.push(marker + "\n" + routingInstructions);
|
|
623
|
+
}
|
|
624
|
+
if (skillLikeInstructions)
|
|
625
|
+
injectedBlocks.push(skillLikeInstructions);
|
|
626
|
+
return { appendSystemContext: injectedBlocks.join("\n\n") };
|
|
627
|
+
}, { priority: 5 });
|
|
628
|
+
// ── 8b. registerTool — Expose 11 ctx_* tools (SLICE OClaw-1) ────
|
|
629
|
+
// Phase 7 audit (v1.0.107-adapter-openclaw.json) flagged severity=CRITICAL:
|
|
630
|
+
// routing block tells agents to call ctx_execute / ctx_search / etc. but
|
|
631
|
+
// nothing called api.registerTool, so the tools didn't exist in the
|
|
632
|
+
// OpenClaw session. This loop fixes that — mirrors swarmvault MCP pattern
|
|
633
|
+
// (refs/plugin-examples/openclaw/swarmvault/packages/engine/src/mcp.ts:46-51).
|
|
634
|
+
if (api.registerTool) {
|
|
635
|
+
for (const def of OPENCLAW_TOOL_DEFS) {
|
|
636
|
+
try {
|
|
637
|
+
api.registerTool(def);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
log.warn?.("registerTool failed", { name: def.name }, err);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
log.debug("registerTool[ctx_*]", { count: OPENCLAW_TOOL_DEFS.length });
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
log.warn?.("api.registerTool unavailable — ctx_* tools not exposed in this OpenClaw build");
|
|
647
|
+
}
|
|
648
|
+
// ── 8c. session_end — Finalize resume snapshot (SLICE OClaw-4) ───
|
|
649
|
+
// OpenClaw fires session_end at session lifecycle boundaries (per
|
|
650
|
+
// refs/platforms/openclaw/docs/plugins/hooks.md:110). We persist a final
|
|
651
|
+
// resume snapshot so a future session_start with resumedFrom can re-attach.
|
|
652
|
+
api.on("session_end", async () => {
|
|
653
|
+
try {
|
|
654
|
+
const sid = sessionId;
|
|
655
|
+
const allEvents = db.getEvents(sid);
|
|
656
|
+
log.debug("session_end", { sessionId: sid.slice(0, 8), events: allEvents.length });
|
|
657
|
+
if (allEvents.length === 0)
|
|
658
|
+
return;
|
|
659
|
+
const freshStats = db.getSessionStats(sid);
|
|
660
|
+
const snapshot = buildResumeSnapshot(allEvents, {
|
|
661
|
+
compactCount: freshStats?.compact_count ?? 0,
|
|
662
|
+
});
|
|
663
|
+
db.upsertResume(sid, snapshot, allEvents.length);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
// best effort — never break session shutdown
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
// ── 8d. subagent_spawning — Inject routing block (SLICE OClaw-5) ─
|
|
670
|
+
// OpenClaw's subagent lifecycle (hooks.md:116) gives us a chance to seed
|
|
671
|
+
// every spawned subagent with the same routing block the parent agent
|
|
672
|
+
// sees. Without this, subagents have no MCP-routing guidance and degrade
|
|
673
|
+
// back to flooding the context with raw tool output.
|
|
674
|
+
api.on("subagent_spawning", (event) => {
|
|
675
|
+
try {
|
|
676
|
+
const e = (event ?? {});
|
|
677
|
+
const basePrompt = e?.input?.prompt ?? "";
|
|
678
|
+
if (!routingInstructions && !skillLikeInstructions)
|
|
679
|
+
return undefined;
|
|
680
|
+
const injectedBlocks = [];
|
|
681
|
+
if (routingInstructions)
|
|
682
|
+
injectedBlocks.push(routingInstructions);
|
|
683
|
+
if (skillLikeInstructions)
|
|
684
|
+
injectedBlocks.push(skillLikeInstructions);
|
|
685
|
+
const injectedPromptBlock = injectedBlocks.join("\n\n");
|
|
686
|
+
const newPrompt = basePrompt
|
|
687
|
+
? `${basePrompt}\n\n${injectedPromptBlock}`
|
|
688
|
+
: injectedPromptBlock;
|
|
689
|
+
log.debug("subagent_spawning[inject-routing+skill]", {
|
|
690
|
+
basePromptLen: basePrompt.length,
|
|
691
|
+
hasRoutingInstructions: !!routingInstructions,
|
|
692
|
+
hasSkillLikeInstructions: !!skillLikeInstructions,
|
|
693
|
+
blockLen: injectedPromptBlock.length,
|
|
694
|
+
});
|
|
695
|
+
return { inputOverride: { ...(e.input ?? {}), prompt: newPrompt } };
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// ── 9. Context engine — Compaction management ──────────
|
|
702
|
+
if (api.registerContextEngine) {
|
|
703
|
+
api.registerContextEngine("context-mode", () => ({
|
|
704
|
+
info: {
|
|
705
|
+
id: "context-mode",
|
|
706
|
+
name: "Context Mode",
|
|
707
|
+
ownsCompaction: false,
|
|
708
|
+
},
|
|
709
|
+
async ingest() {
|
|
710
|
+
return { ingested: true };
|
|
711
|
+
},
|
|
712
|
+
async assemble({ messages }) {
|
|
713
|
+
return { messages, estimatedTokens: 0 };
|
|
714
|
+
},
|
|
715
|
+
async compact() {
|
|
716
|
+
// No-op: session continuity is handled by before_compaction / after_compaction hooks.
|
|
717
|
+
// Returning ownsCompaction: false + compacted: false lets the host platform (OpenClaw)
|
|
718
|
+
// manage conversation truncation, preserving Anthropic thinking/redacted_thinking blocks.
|
|
719
|
+
// See: https://github.com/mksglu/context-mode/issues/191
|
|
720
|
+
return { ok: true, compacted: false };
|
|
721
|
+
},
|
|
722
|
+
}));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
log.warn?.("api.registerContextEngine unavailable — skipping context engine registration");
|
|
726
|
+
}
|
|
727
|
+
// ── 10. Auto-reply commands — ctx slash commands ──────
|
|
728
|
+
// Update module-level refs so command handlers (registered once) always
|
|
729
|
+
// read the latest session's db/sessionId/pluginRoot.
|
|
730
|
+
_latestDb = db;
|
|
731
|
+
_latestSessionId = sessionId;
|
|
732
|
+
_latestPluginRoot = pluginRoot;
|
|
733
|
+
if (api.registerCommand) {
|
|
734
|
+
registerAutoReplyCommand({
|
|
735
|
+
name: "ctx-stats",
|
|
736
|
+
description: "Show context-mode session statistics",
|
|
737
|
+
handler: () => {
|
|
738
|
+
const text = buildStatsText(_latestDb, _latestSessionId);
|
|
739
|
+
return { text };
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
registerAutoReplyCommand({
|
|
743
|
+
name: "ctx-doctor",
|
|
744
|
+
description: "Run context-mode diagnostics",
|
|
745
|
+
handler: () => {
|
|
746
|
+
const bundlePath = resolve(_latestPluginRoot, "cli.bundle.mjs");
|
|
747
|
+
const fallbackPath = resolve(_latestPluginRoot, "build", "cli.js");
|
|
748
|
+
const cliPath = existsSync(bundlePath) ? bundlePath : fallbackPath;
|
|
749
|
+
const cmd = `${buildNodeCommand(cliPath)} doctor`;
|
|
750
|
+
return {
|
|
751
|
+
text: [
|
|
752
|
+
"## ctx-doctor",
|
|
753
|
+
"",
|
|
754
|
+
"Run this command to diagnose context-mode:",
|
|
755
|
+
"",
|
|
756
|
+
"```",
|
|
757
|
+
cmd,
|
|
758
|
+
"```",
|
|
759
|
+
].join("\n"),
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
registerAutoReplyCommand({
|
|
764
|
+
name: "ctx-upgrade",
|
|
765
|
+
description: "Upgrade context-mode to the latest version",
|
|
766
|
+
handler: () => {
|
|
767
|
+
const bundlePath = resolve(_latestPluginRoot, "cli.bundle.mjs");
|
|
768
|
+
const fallbackPath = resolve(_latestPluginRoot, "build", "cli.js");
|
|
769
|
+
const cliPath = existsSync(bundlePath) ? bundlePath : fallbackPath;
|
|
770
|
+
const cmd = `${buildNodeCommand(cliPath)} upgrade`;
|
|
771
|
+
return {
|
|
772
|
+
text: [
|
|
773
|
+
"## ctx-upgrade",
|
|
774
|
+
"",
|
|
775
|
+
"Run this command to upgrade context-mode:",
|
|
776
|
+
"",
|
|
777
|
+
"```",
|
|
778
|
+
cmd,
|
|
779
|
+
"```",
|
|
780
|
+
"",
|
|
781
|
+
"Restart your session after upgrade.",
|
|
782
|
+
].join("\n"),
|
|
783
|
+
};
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
// ── Stats helper ──────────────────────────────────────────
|
|
790
|
+
function buildStatsText(db, sessionId) {
|
|
791
|
+
try {
|
|
792
|
+
const events = db.getEvents(sessionId);
|
|
793
|
+
const stats = db.getSessionStats(sessionId);
|
|
794
|
+
const lines = [
|
|
795
|
+
"## context-mode stats",
|
|
796
|
+
"",
|
|
797
|
+
`- Session: \`${sessionId.slice(0, 8)}…\``,
|
|
798
|
+
`- Events captured: ${events.length}`,
|
|
799
|
+
`- Compactions: ${stats?.compact_count ?? 0}`,
|
|
800
|
+
];
|
|
801
|
+
// Summarize events by type
|
|
802
|
+
const byType = {};
|
|
803
|
+
for (const ev of events) {
|
|
804
|
+
const key = ev.type ?? "unknown";
|
|
805
|
+
byType[key] = (byType[key] ?? 0) + 1;
|
|
806
|
+
}
|
|
807
|
+
if (Object.keys(byType).length > 0) {
|
|
808
|
+
lines.push("- Event breakdown:");
|
|
809
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
810
|
+
lines.push(` - ${type}: ${count}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return lines.join("\n");
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return "context-mode stats unavailable (session DB error)";
|
|
817
|
+
}
|
|
818
|
+
}
|