@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
package/build/db-base.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db-base — Reusable SQLite infrastructure for context-mode packages.
|
|
3
|
+
*
|
|
4
|
+
* Provides lazy-loading of better-sqlite3, WAL pragma setup, prepared
|
|
5
|
+
* statement caching interface, and DB file cleanup helpers. Both
|
|
6
|
+
* ContentStore and SessionDB build on top of these primitives.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { existsSync, unlinkSync, renameSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
// ─────────────────────────────────────────────────────────
|
|
13
|
+
// bun:sqlite adapter (#45)
|
|
14
|
+
// ─────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a bun:sqlite Database to provide better-sqlite3-compatible API.
|
|
17
|
+
* Bridges: .pragma(), multi-statement .exec(), .get() null→undefined.
|
|
18
|
+
*/
|
|
19
|
+
export class BunSQLiteAdapter {
|
|
20
|
+
#raw;
|
|
21
|
+
constructor(rawDb) {
|
|
22
|
+
this.#raw = rawDb;
|
|
23
|
+
}
|
|
24
|
+
pragma(source) {
|
|
25
|
+
const stmt = this.#raw.prepare(`PRAGMA ${source}`);
|
|
26
|
+
const rows = stmt.all();
|
|
27
|
+
if (!rows || rows.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Multi-row pragmas (table_xinfo, etc.) → return array
|
|
30
|
+
if (rows.length > 1)
|
|
31
|
+
return rows;
|
|
32
|
+
// Single-row: extract scalar value (e.g. journal_mode = "wal")
|
|
33
|
+
const values = Object.values(rows[0]);
|
|
34
|
+
return values.length === 1 ? values[0] : rows[0];
|
|
35
|
+
}
|
|
36
|
+
exec(sql) {
|
|
37
|
+
// bun:sqlite .exec() is single-statement only.
|
|
38
|
+
// Split multi-statement SQL respecting string literals (don't split on ; inside quotes).
|
|
39
|
+
let current = "";
|
|
40
|
+
let inString = null;
|
|
41
|
+
for (let i = 0; i < sql.length; i++) {
|
|
42
|
+
const ch = sql[i];
|
|
43
|
+
if (inString) {
|
|
44
|
+
current += ch;
|
|
45
|
+
if (ch === inString)
|
|
46
|
+
inString = null;
|
|
47
|
+
}
|
|
48
|
+
else if (ch === "'" || ch === '"') {
|
|
49
|
+
current += ch;
|
|
50
|
+
inString = ch;
|
|
51
|
+
}
|
|
52
|
+
else if (ch === ";") {
|
|
53
|
+
const trimmed = current.trim();
|
|
54
|
+
if (trimmed)
|
|
55
|
+
this.#raw.prepare(trimmed).run();
|
|
56
|
+
current = "";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
current += ch;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const trimmed = current.trim();
|
|
63
|
+
if (trimmed)
|
|
64
|
+
this.#raw.prepare(trimmed).run();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
prepare(sql) {
|
|
68
|
+
const stmt = this.#raw.prepare(sql);
|
|
69
|
+
return {
|
|
70
|
+
run: (...args) => stmt.run(...args),
|
|
71
|
+
get: (...args) => {
|
|
72
|
+
const r = stmt.get(...args);
|
|
73
|
+
return r === null ? undefined : r;
|
|
74
|
+
},
|
|
75
|
+
all: (...args) => stmt.all(...args),
|
|
76
|
+
iterate: (...args) => stmt.iterate(...args),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
transaction(fn) {
|
|
80
|
+
return this.#raw.transaction(fn);
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
this.#raw.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ─────────────────────────────────────────────────────────
|
|
87
|
+
// node:sqlite adapter (#228)
|
|
88
|
+
// ─────────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Wraps node:sqlite's DatabaseSync to provide better-sqlite3-compatible API.
|
|
91
|
+
* Bridges: .pragma(), .transaction(). Everything else is passthrough.
|
|
92
|
+
* Eliminates native addon SIGSEGV on Linux (nodejs/node#62515).
|
|
93
|
+
*/
|
|
94
|
+
export class NodeSQLiteAdapter {
|
|
95
|
+
#raw; // DatabaseSync instance
|
|
96
|
+
constructor(rawDb) {
|
|
97
|
+
this.#raw = rawDb;
|
|
98
|
+
}
|
|
99
|
+
pragma(source) {
|
|
100
|
+
// "journal_mode = WAL" → PRAGMA journal_mode = WAL
|
|
101
|
+
// "table_xinfo(session_events)" → PRAGMA table_xinfo(session_events)
|
|
102
|
+
// "wal_checkpoint(TRUNCATE)" → PRAGMA wal_checkpoint(TRUNCATE)
|
|
103
|
+
const stmt = this.#raw.prepare(`PRAGMA ${source}`);
|
|
104
|
+
const rows = stmt.all();
|
|
105
|
+
if (!rows || rows.length === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (rows.length > 1)
|
|
108
|
+
return rows;
|
|
109
|
+
const values = Object.values(rows[0]);
|
|
110
|
+
return values.length === 1 ? values[0] : rows[0];
|
|
111
|
+
}
|
|
112
|
+
exec(sql) {
|
|
113
|
+
// node:sqlite's exec() supports multi-statement natively
|
|
114
|
+
this.#raw.exec(sql);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
prepare(sql) {
|
|
118
|
+
const stmt = this.#raw.prepare(sql);
|
|
119
|
+
return {
|
|
120
|
+
run: (...args) => stmt.run(...args),
|
|
121
|
+
get: (...args) => stmt.get(...args),
|
|
122
|
+
all: (...args) => stmt.all(...args),
|
|
123
|
+
iterate: (...args) => {
|
|
124
|
+
// node:sqlite uses Symbol.iterator on StatementSync, not .iterate()
|
|
125
|
+
// Check if iterate exists, otherwise use Symbol.iterator
|
|
126
|
+
if (typeof stmt.iterate === 'function') {
|
|
127
|
+
return stmt.iterate(...args);
|
|
128
|
+
}
|
|
129
|
+
// Fallback: use all() to create an iterator
|
|
130
|
+
const rows = stmt.all(...args);
|
|
131
|
+
return rows[Symbol.iterator]();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
transaction(fn) {
|
|
136
|
+
// node:sqlite has no transaction() method — manual BEGIN/COMMIT/ROLLBACK
|
|
137
|
+
return (...args) => {
|
|
138
|
+
this.#raw.exec("BEGIN");
|
|
139
|
+
try {
|
|
140
|
+
const result = fn(...args);
|
|
141
|
+
this.#raw.exec("COMMIT");
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.#raw.exec("ROLLBACK");
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
this.#raw.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ─────────────────────────────────────────────────────────
|
|
155
|
+
// Lazy loader
|
|
156
|
+
// ─────────────────────────────────────────────────────────
|
|
157
|
+
let _Database = null;
|
|
158
|
+
/**
|
|
159
|
+
* Probe whether the supplied node:sqlite DatabaseSync constructor links a
|
|
160
|
+
* SQLite build that includes the FTS5 module. Some Node.js Linux builds
|
|
161
|
+
* (e.g. v22.14.0 on Ubuntu) ship node:sqlite without FTS5 even though the
|
|
162
|
+
* import succeeds, which silently breaks ctx_search/ctx_batch_execute and
|
|
163
|
+
* the doctor's FTS5 check (issue #461).
|
|
164
|
+
*
|
|
165
|
+
* Returns true only when a `CREATE VIRTUAL TABLE … USING fts5(x)` statement
|
|
166
|
+
* succeeds. Always returns false on any failure (constructor throw, missing
|
|
167
|
+
* module, etc.) so the caller can fall through to better-sqlite3, whose
|
|
168
|
+
* bundled SQLite always ships with FTS5.
|
|
169
|
+
*/
|
|
170
|
+
export function nodeSqliteHasFts5(DatabaseSync) {
|
|
171
|
+
let probe = null;
|
|
172
|
+
try {
|
|
173
|
+
probe = new DatabaseSync(":memory:");
|
|
174
|
+
probe.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)");
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
try {
|
|
182
|
+
probe?.close();
|
|
183
|
+
}
|
|
184
|
+
catch { /* probe never opened or already closed */ }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Returns true when the current runtime ships a built-in SQLite binding:
|
|
189
|
+
* - Bun has `bun:sqlite` always
|
|
190
|
+
* - Node has `node:sqlite` since 22.5 (no flag since 22.13)
|
|
191
|
+
*
|
|
192
|
+
* Mirrors the helper in hooks/ensure-deps.mjs:61. Exported so the platform
|
|
193
|
+
* gate in loadDatabase() can be unit-tested without spawning a child
|
|
194
|
+
* process. `versionsOverride` and `bunOverride` are injection points for
|
|
195
|
+
* tests — production callers pass nothing.
|
|
196
|
+
*
|
|
197
|
+
* Widening the gate from `process.platform === "linux"` to this helper is
|
|
198
|
+
* required for Node 26 on macOS arm64 (#551): Node 26 removed
|
|
199
|
+
* `info.This()` from V8 PropertyCallbackInfo, breaking better-sqlite3
|
|
200
|
+
* 12.9.0's native compile. Using node:sqlite sidesteps the native addon
|
|
201
|
+
* entirely on every platform that has it.
|
|
202
|
+
*/
|
|
203
|
+
export function hasModernSqlite(versionsOverride, bunOverride) {
|
|
204
|
+
const bun = bunOverride !== undefined ? bunOverride : globalThis.Bun;
|
|
205
|
+
if (typeof bun !== "undefined" && bun !== null)
|
|
206
|
+
return true;
|
|
207
|
+
const versions = versionsOverride ?? process.versions;
|
|
208
|
+
const [majorStr, minorStr] = (versions.node ?? "0.0.0").split(".");
|
|
209
|
+
const major = Number(majorStr);
|
|
210
|
+
const minor = Number(minorStr);
|
|
211
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor))
|
|
212
|
+
return false;
|
|
213
|
+
return major > 22 || (major === 22 && minor >= 5);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Lazy-load the SQLite driver for the current runtime.
|
|
217
|
+
* Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
|
|
218
|
+
* Modern Node (>= 22.5) → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461, #551).
|
|
219
|
+
* Other Node (or modern Node without FTS5) → better-sqlite3 (native addon).
|
|
220
|
+
*/
|
|
221
|
+
export function loadDatabase() {
|
|
222
|
+
if (!_Database) {
|
|
223
|
+
const require = createRequire(import.meta.url);
|
|
224
|
+
if (globalThis.Bun) {
|
|
225
|
+
// Bun runtime — use bun:sqlite directly.
|
|
226
|
+
// Array.join() prevents esbuild from resolving the specifier at bundle time.
|
|
227
|
+
const BunDB = require(["bun", "sqlite"].join(":")).Database;
|
|
228
|
+
_Database = function BunDatabaseFactory(path, opts) {
|
|
229
|
+
const raw = new BunDB(path, {
|
|
230
|
+
readonly: opts?.readonly,
|
|
231
|
+
create: true,
|
|
232
|
+
});
|
|
233
|
+
const adapter = new BunSQLiteAdapter(raw);
|
|
234
|
+
// Propagate busy_timeout — better-sqlite3 does this via constructor
|
|
235
|
+
// option but bun:sqlite does not, so we set it via pragma (#243)
|
|
236
|
+
if (opts?.timeout) {
|
|
237
|
+
adapter.pragma(`busy_timeout = ${opts.timeout}`);
|
|
238
|
+
}
|
|
239
|
+
return adapter;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
else if (hasModernSqlite()) {
|
|
243
|
+
// Any Node >= 22.5 — try node:sqlite to avoid the native addon path
|
|
244
|
+
// entirely. Historically this was Linux-only (avoiding the Linux
|
|
245
|
+
// SIGSEGV per nodejs/node#62515, #228), but Node 26 also broke
|
|
246
|
+
// better-sqlite3's native compile on macOS arm64 by removing
|
|
247
|
+
// V8 `info.This()` (#551). The built-in `node:sqlite` ships its
|
|
248
|
+
// own SQLite, so it sidesteps both issues at once.
|
|
249
|
+
//
|
|
250
|
+
// Probe FTS5 support before committing — some Node builds ship
|
|
251
|
+
// node:sqlite without FTS5, which would silently break ctx_search
|
|
252
|
+
// (#461). The probe runs at most once per process (cached via
|
|
253
|
+
// _Database below), so the cost of an in-memory DatabaseSync is
|
|
254
|
+
// negligible.
|
|
255
|
+
let DatabaseSync = null;
|
|
256
|
+
try {
|
|
257
|
+
// Array.join() prevents esbuild from resolving the specifier at bundle time
|
|
258
|
+
// (mirrors the bun:sqlite branch above).
|
|
259
|
+
({ DatabaseSync } = require(["node", "sqlite"].join(":")));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
DatabaseSync = null;
|
|
263
|
+
}
|
|
264
|
+
if (DatabaseSync && nodeSqliteHasFts5(DatabaseSync)) {
|
|
265
|
+
_Database = function NodeDatabaseFactory(path, opts) {
|
|
266
|
+
const raw = new DatabaseSync(path, {
|
|
267
|
+
readOnly: opts?.readonly ?? false,
|
|
268
|
+
});
|
|
269
|
+
const adapter = new NodeSQLiteAdapter(raw);
|
|
270
|
+
// Propagate busy_timeout — node:sqlite's DatabaseSync constructor
|
|
271
|
+
// silently ignores `{ timeout }` (unlike better-sqlite3's native
|
|
272
|
+
// C++ constructor), so we set it via PRAGMA, mirroring the Bun
|
|
273
|
+
// branch above. Without this, the default is 0 and the first
|
|
274
|
+
// write contention surfaces as immediate `SQLITE_BUSY`/`database
|
|
275
|
+
// is locked` — defeating the 30s grace `withRetry()` is built
|
|
276
|
+
// around. See issue #642 and ADR-0001 (multi-writer contract).
|
|
277
|
+
if (opts?.timeout) {
|
|
278
|
+
adapter.pragma(`busy_timeout = ${opts.timeout}`);
|
|
279
|
+
}
|
|
280
|
+
return adapter;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// node:sqlite missing or built without FTS5 — fall through to
|
|
285
|
+
// better-sqlite3. Trade-off: on Node 26 + macOS this may now hit
|
|
286
|
+
// the V8 ABI break (#551). A visible crash on the rare
|
|
287
|
+
// unstable build is preferable to silent "no such module: fts5"
|
|
288
|
+
// on every ctx_search call.
|
|
289
|
+
_Database = require("better-sqlite3");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Old Node (< 22.5) without bun:sqlite — fall back to better-sqlite3.
|
|
294
|
+
_Database = require("better-sqlite3");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return _Database;
|
|
298
|
+
}
|
|
299
|
+
// ─────────────────────────────────────────────────────────
|
|
300
|
+
// WAL setup
|
|
301
|
+
// ─────────────────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Apply WAL mode and NORMAL synchronous pragma to a database instance.
|
|
304
|
+
* Should be called immediately after opening a new database connection.
|
|
305
|
+
*
|
|
306
|
+
* WAL mode provides:
|
|
307
|
+
* - Concurrent readers while a write is in progress
|
|
308
|
+
* - Dramatically faster writes (no full-page sync on each commit)
|
|
309
|
+
* NORMAL synchronous is safe under WAL and avoids an extra fsync per
|
|
310
|
+
* transaction.
|
|
311
|
+
*
|
|
312
|
+
* v1.0.187 — mmap_size is OPT-IN (upstream #992/#905; PRs #1030/#1056):
|
|
313
|
+
* a default 256MB mmap over DB files that are shared across processes
|
|
314
|
+
* turns transient resource pressure into SQLITE_IOERR ("disk I/O error")
|
|
315
|
+
* on otherwise-healthy disks. The pragma is now applied ONLY when
|
|
316
|
+
* CONTEXT_MODE_DB_MMAP_SIZE (bytes) is set to a valid non-negative
|
|
317
|
+
* integer; when unset, mmap_size is NOT touched at all (SQLite default).
|
|
318
|
+
*/
|
|
319
|
+
export function applyWALPragmas(db, env = process.env) {
|
|
320
|
+
db.pragma("journal_mode = WAL");
|
|
321
|
+
db.pragma("synchronous = NORMAL");
|
|
322
|
+
// Memory-map the DB file for read-heavy FTS5 search workloads — opt-in
|
|
323
|
+
// only (see docblock above). Falls back gracefully on platforms where
|
|
324
|
+
// mmap is unavailable or restricted.
|
|
325
|
+
const mmapSize = resolveMmapSizeFromEnv(env);
|
|
326
|
+
if (mmapSize !== null) {
|
|
327
|
+
try {
|
|
328
|
+
db.pragma(`mmap_size = ${mmapSize}`);
|
|
329
|
+
}
|
|
330
|
+
catch { /* unsupported runtime */ }
|
|
331
|
+
}
|
|
332
|
+
// NOTE: `locking_mode = EXCLUSIVE` is intentionally NOT applied here.
|
|
333
|
+
// ALL DBs built on this helper — ContentStore (FTS5 shared knowledge
|
|
334
|
+
// base) AND SessionDB (per-project events) — are multi-writer-safe by
|
|
335
|
+
// contract. WAL + busy_timeout + the withRetry() wrapper below handle
|
|
336
|
+
// SQLITE_BUSY natively. EXCLUSIVE locking is opt-out, never opt-in
|
|
337
|
+
// from a base class shared by multi-writer consumers.
|
|
338
|
+
// See docs/adr/0001-sessiondb-multi-writer.md for the v1.0.130 ADR.
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Parse the opt-in mmap size (bytes) from CONTEXT_MODE_DB_MMAP_SIZE.
|
|
342
|
+
* Returns null when the variable is unset, empty, or not a non-negative
|
|
343
|
+
* integer — callers must then skip the mmap_size pragma entirely so the
|
|
344
|
+
* SQLite default applies.
|
|
345
|
+
*
|
|
346
|
+
* Exported so the opt-in contract is unit-testable without mutating
|
|
347
|
+
* process.env at call sites.
|
|
348
|
+
*/
|
|
349
|
+
export function resolveMmapSizeFromEnv(env = process.env) {
|
|
350
|
+
const raw = env.CONTEXT_MODE_DB_MMAP_SIZE;
|
|
351
|
+
if (raw === undefined || raw === null)
|
|
352
|
+
return null;
|
|
353
|
+
const trimmed = String(raw).trim();
|
|
354
|
+
if (trimmed === "")
|
|
355
|
+
return null;
|
|
356
|
+
const n = Number(trimmed);
|
|
357
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0)
|
|
358
|
+
return null;
|
|
359
|
+
return n;
|
|
360
|
+
}
|
|
361
|
+
// ─────────────────────────────────────────────────────────
|
|
362
|
+
// DB file helpers
|
|
363
|
+
// ─────────────────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Remove orphaned WAL/SHM files when the main DB file doesn't exist.
|
|
366
|
+
* On Windows, stale -wal/-shm files from crashed processes cause
|
|
367
|
+
* "file is not a database" errors when creating a fresh DB.
|
|
368
|
+
*/
|
|
369
|
+
export function cleanOrphanedWALFiles(dbPath) {
|
|
370
|
+
if (!existsSync(dbPath)) {
|
|
371
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
372
|
+
try {
|
|
373
|
+
unlinkSync(dbPath + suffix);
|
|
374
|
+
}
|
|
375
|
+
catch { /* ignore */ }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Delete all three SQLite files for a given db path (main, WAL, SHM).
|
|
381
|
+
* Silently ignores individual deletion errors so a partial cleanup
|
|
382
|
+
* does not abort the rest.
|
|
383
|
+
*/
|
|
384
|
+
export function deleteDBFiles(dbPath) {
|
|
385
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
386
|
+
try {
|
|
387
|
+
unlinkSync(dbPath + suffix);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// ignore — file may not exist
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Safely close a database connection. Swallows errors so callers can
|
|
396
|
+
* always call this in a finally/cleanup path without try/catch.
|
|
397
|
+
*
|
|
398
|
+
* v1.0.187 — the close-time `wal_checkpoint(TRUNCATE)` was REMOVED
|
|
399
|
+
* (upstream #992/#905; PRs #1030/#1056/#880). A TRUNCATE checkpoint is a
|
|
400
|
+
* *cross-process mutation*: when a hook/CLI/statusline process still holds
|
|
401
|
+
* the same DB, the checkpoint races its WAL readers/writers and surfaces
|
|
402
|
+
* as SQLITE_IOERR ("disk I/O error") on healthy disks. SQLite already
|
|
403
|
+
* performs a best-effort (non-destructive) checkpoint inside close();
|
|
404
|
+
* the only sidecar cleanup this layer does is cleanOrphanedWALFiles,
|
|
405
|
+
* which deletes -wal/-shm when the main DB file is absent.
|
|
406
|
+
*/
|
|
407
|
+
export function closeDB(db) {
|
|
408
|
+
try {
|
|
409
|
+
db.close();
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// ignore
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ─────────────────────────────────────────────────────────
|
|
416
|
+
// Default path helper
|
|
417
|
+
// ─────────────────────────────────────────────────────────
|
|
418
|
+
/**
|
|
419
|
+
* Return the default per-process DB path for context-mode databases.
|
|
420
|
+
* Uses the OS temp directory and embeds the current PID so multiple
|
|
421
|
+
* server instances never share a file.
|
|
422
|
+
*/
|
|
423
|
+
export function defaultDBPath(prefix = "context-mode") {
|
|
424
|
+
return join(tmpdir(), `${prefix}-${process.pid}.db`);
|
|
425
|
+
}
|
|
426
|
+
// ─────────────────────────────────────────────────────────
|
|
427
|
+
// Retry helper
|
|
428
|
+
// ─────────────────────────────────────────────────────────
|
|
429
|
+
/**
|
|
430
|
+
* Build a single searchable signature string from an arbitrary thrown
|
|
431
|
+
* value. Covers the shapes SQLite drivers actually throw:
|
|
432
|
+
* - Error with `code` (better-sqlite3 / node:sqlite SqliteError): code + message
|
|
433
|
+
* - Plain Error / string (bun:sqlite): message text
|
|
434
|
+
* - Non-Error objects with `code` and/or `message` properties
|
|
435
|
+
*/
|
|
436
|
+
function errorSignature(err) {
|
|
437
|
+
if (err instanceof Error) {
|
|
438
|
+
const code = err.code;
|
|
439
|
+
return typeof code === "string" ? `${code} ${err.message}` : err.message;
|
|
440
|
+
}
|
|
441
|
+
if (typeof err === "string")
|
|
442
|
+
return err;
|
|
443
|
+
if (err !== null && typeof err === "object") {
|
|
444
|
+
const code = err.code;
|
|
445
|
+
const message = err.message;
|
|
446
|
+
const parts = [
|
|
447
|
+
typeof code === "string" ? code : "",
|
|
448
|
+
typeof message === "string" ? message : "",
|
|
449
|
+
].filter(Boolean);
|
|
450
|
+
return parts.length > 0 ? parts.join(" ") : String(err);
|
|
451
|
+
}
|
|
452
|
+
return String(err);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Transient SQLite errors that warrant a retry (v1.0.187, upstream
|
|
456
|
+
* #992/#905; PRs #1030/#1056/#880):
|
|
457
|
+
* - SQLITE_BUSY / "database is locked" — writer contention; the existing
|
|
458
|
+
* busy_timeout + backoff contract.
|
|
459
|
+
* - SQLITE_IOERR / "disk I/O error" — frequently TRANSIENT on healthy
|
|
460
|
+
* disks (mmap pressure, checkpoint races, AV/EDR scans). Treating it
|
|
461
|
+
* as fatal turned recoverable blips into user-visible failures.
|
|
462
|
+
*
|
|
463
|
+
* Corruption signatures (SQLITE_CORRUPT / SQLITE_NOTADB / "file is not a
|
|
464
|
+
* database") are deliberately NOT retried — see isSQLiteCorruptionError.
|
|
465
|
+
* Non-Error throw shapes ({ code } objects, strings) are classified the
|
|
466
|
+
* same way as Errors via errorSignature().
|
|
467
|
+
*/
|
|
468
|
+
export function isTransientSqliteError(err) {
|
|
469
|
+
const sig = errorSignature(err);
|
|
470
|
+
return (sig.includes("SQLITE_BUSY") ||
|
|
471
|
+
sig.includes("database is locked") ||
|
|
472
|
+
sig.includes("SQLITE_IOERR") ||
|
|
473
|
+
/disk i\/o error/i.test(sig));
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Retry a DB operation with exponential backoff on transient SQLite
|
|
477
|
+
* errors: SQLITE_BUSY ("database is locked") and SQLITE_IOERR
|
|
478
|
+
* ("disk I/O error"). Retries up to 3 times with delays: 100ms, 500ms,
|
|
479
|
+
* 2000ms. If all retries fail, throws a descriptive error.
|
|
480
|
+
* Pass custom delays for testing (e.g., [0, 0, 0] to skip waits).
|
|
481
|
+
*/
|
|
482
|
+
export function withRetry(fn, delays = [100, 500, 2000]) {
|
|
483
|
+
let lastError;
|
|
484
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
485
|
+
try {
|
|
486
|
+
return fn();
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
if (!isTransientSqliteError(err)) {
|
|
490
|
+
throw err;
|
|
491
|
+
}
|
|
492
|
+
lastError = err instanceof Error ? err : new Error(errorSignature(err));
|
|
493
|
+
if (attempt < delays.length) {
|
|
494
|
+
const delay = delays[attempt];
|
|
495
|
+
const start = Date.now();
|
|
496
|
+
while (Date.now() - start < delay) { /* busy-wait for sync retry */ }
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
throw new Error(`SQLITE_BUSY/SQLITE_IOERR: transient SQLite error after ${delays.length} retries. ` +
|
|
501
|
+
`Original error: ${lastError?.message}`);
|
|
502
|
+
}
|
|
503
|
+
// ─────────────────────────────────────────────────────────
|
|
504
|
+
// Corrupt DB recovery (#244)
|
|
505
|
+
// ─────────────────────────────────────────────────────────
|
|
506
|
+
/**
|
|
507
|
+
* Detect SQLite corruption errors that warrant a rename-and-recreate.
|
|
508
|
+
* Matches SQLITE_CORRUPT, SQLITE_NOTADB, and their human-readable equivalents.
|
|
509
|
+
*/
|
|
510
|
+
export function isSQLiteCorruptionError(msg) {
|
|
511
|
+
return (msg.includes("SQLITE_CORRUPT") ||
|
|
512
|
+
msg.includes("SQLITE_NOTADB") ||
|
|
513
|
+
msg.includes("database disk image is malformed") ||
|
|
514
|
+
msg.includes("file is not a database"));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Rename a corrupt DB and its WAL/SHM files so a fresh DB can be created.
|
|
518
|
+
* Best-effort — individual rename failures are silently ignored.
|
|
519
|
+
*/
|
|
520
|
+
export function renameCorruptDB(dbPath) {
|
|
521
|
+
const ts = Date.now();
|
|
522
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
523
|
+
try {
|
|
524
|
+
renameSync(dbPath + suffix, `${dbPath}${suffix}.corrupt-${ts}`);
|
|
525
|
+
}
|
|
526
|
+
catch { /* file may not exist */ }
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ─────────────────────────────────────────────────────────
|
|
530
|
+
// DB error logging (v1.0.187)
|
|
531
|
+
// ─────────────────────────────────────────────────────────
|
|
532
|
+
/**
|
|
533
|
+
* Stable log prefix for swallowed DB failures. Neither db-base.ts nor
|
|
534
|
+
* session/db.ts has a host logger, so failures that were previously
|
|
535
|
+
* silent (best-effort schema migrations, read paths returning []/"" on
|
|
536
|
+
* error) go to stderr via console.error under this prefix.
|
|
537
|
+
*/
|
|
538
|
+
export const DB_LOG_PREFIX = "[context-mode:db]";
|
|
539
|
+
/** Same op + code + message is logged at most once per window. */
|
|
540
|
+
const DB_LOG_INTERVAL_MS = 30_000;
|
|
541
|
+
/** Cap on the dedupe table so a high-cardinality error source can't grow it unbounded. */
|
|
542
|
+
const DB_LOG_MAX_KEYS = 256;
|
|
543
|
+
const _recentDbErrors = new Map();
|
|
544
|
+
/**
|
|
545
|
+
* Extract the SQLite error code (e.g. "SQLITE_IOERR") from an arbitrary
|
|
546
|
+
* thrown value, or "" when absent. better-sqlite3 and node:sqlite set
|
|
547
|
+
* `code` on SqliteError; bun:sqlite encodes it in the message instead.
|
|
548
|
+
*/
|
|
549
|
+
function extractErrorCode(err) {
|
|
550
|
+
if (err instanceof Error) {
|
|
551
|
+
const code = err.code;
|
|
552
|
+
return typeof code === "string" ? code : "";
|
|
553
|
+
}
|
|
554
|
+
if (err !== null && typeof err === "object") {
|
|
555
|
+
const code = err.code;
|
|
556
|
+
return typeof code === "string" ? code : "";
|
|
557
|
+
}
|
|
558
|
+
return "";
|
|
559
|
+
}
|
|
560
|
+
function errorMessage(err) {
|
|
561
|
+
if (err instanceof Error)
|
|
562
|
+
return err.message;
|
|
563
|
+
if (typeof err === "string")
|
|
564
|
+
return err;
|
|
565
|
+
if (err !== null && typeof err === "object") {
|
|
566
|
+
const message = err.message;
|
|
567
|
+
if (typeof message === "string" && message)
|
|
568
|
+
return message;
|
|
569
|
+
}
|
|
570
|
+
return String(err);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Log a swallowed DB failure to stderr with the stable
|
|
574
|
+
* `[context-mode:db]` prefix: operation name, error code, error message,
|
|
575
|
+
* and (when OPENCODE_DEBUG is set) the stack. The same op + code +
|
|
576
|
+
* message combination is rate-limited to one line per ~30s to avoid log
|
|
577
|
+
* storms from hot retry loops or batch scans. Logging must never throw.
|
|
578
|
+
*
|
|
579
|
+
* `detail` (e.g. a DB path) is optional context appended to the line and
|
|
580
|
+
* included in the dedupe key so different files don't suppress each
|
|
581
|
+
* other's diagnostics.
|
|
582
|
+
*/
|
|
583
|
+
export function logDbError(op, err, detail, now = Date.now()) {
|
|
584
|
+
try {
|
|
585
|
+
const code = extractErrorCode(err);
|
|
586
|
+
const message = errorMessage(err);
|
|
587
|
+
const detailSuffix = detail ? ` (${detail})` : "";
|
|
588
|
+
const dedupeKey = `${op}|${code}|${message}${detailSuffix}`;
|
|
589
|
+
const last = _recentDbErrors.get(dedupeKey);
|
|
590
|
+
if (last !== undefined && now - last < DB_LOG_INTERVAL_MS)
|
|
591
|
+
return;
|
|
592
|
+
_recentDbErrors.set(dedupeKey, now);
|
|
593
|
+
if (_recentDbErrors.size > DB_LOG_MAX_KEYS) {
|
|
594
|
+
// A burst of >256 distinct errors inside one window can exceed the
|
|
595
|
+
// cap. First free genuinely expired keys, then HARD-evict the oldest
|
|
596
|
+
// entries (Map preserves insertion order) until the cap is restored —
|
|
597
|
+
// the table can never exceed DB_LOG_MAX_KEYS, and keys still within
|
|
598
|
+
// the cap keep their normal dedupe semantics.
|
|
599
|
+
for (const [key, ts] of _recentDbErrors) {
|
|
600
|
+
if (now - ts >= DB_LOG_INTERVAL_MS)
|
|
601
|
+
_recentDbErrors.delete(key);
|
|
602
|
+
}
|
|
603
|
+
while (_recentDbErrors.size > DB_LOG_MAX_KEYS) {
|
|
604
|
+
const oldestKey = _recentDbErrors.keys().next().value;
|
|
605
|
+
if (oldestKey === undefined)
|
|
606
|
+
break;
|
|
607
|
+
_recentDbErrors.delete(oldestKey);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const debugRaw = process.env.OPENCODE_DEBUG;
|
|
611
|
+
const debug = debugRaw !== undefined && debugRaw !== "" && debugRaw !== "0" && debugRaw !== "false";
|
|
612
|
+
const stack = debug && err instanceof Error && err.stack ? `\n${err.stack}` : "";
|
|
613
|
+
const codePart = code ? ` [${code}]` : "";
|
|
614
|
+
console.error(`${DB_LOG_PREFIX} ${op}${codePart}: ${message}${detailSuffix}${stack}`);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// logging must never break the caller
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Clear the log-dedupe table. Test-only: lets a test observe the first
|
|
622
|
+
* emission for a given op regardless of what earlier tests logged.
|
|
623
|
+
*/
|
|
624
|
+
export function resetDbErrorLogForTests() {
|
|
625
|
+
_recentDbErrors.clear();
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Number of keys currently tracked in the log-dedupe table. Test-only:
|
|
629
|
+
* lets a test pin the DB_LOG_MAX_KEYS hard-cap (the table must never grow
|
|
630
|
+
* past the cap even under a burst of distinct errors inside one window).
|
|
631
|
+
*/
|
|
632
|
+
export function getRecentDbErrorCountForTests() {
|
|
633
|
+
return _recentDbErrors.size;
|
|
634
|
+
}
|
|
635
|
+
// ─────────────────────────────────────────────────────────
|
|
636
|
+
// Base class
|
|
637
|
+
// ─────────────────────────────────────────────────────────
|
|
638
|
+
/**
|
|
639
|
+
* SQLiteBase — minimal base class that handles open/close/cleanup lifecycle.
|
|
640
|
+
*
|
|
641
|
+
* Subclasses call `super(dbPath)` to open the database with WAL pragmas
|
|
642
|
+
* applied, then implement `initSchema()` and `prepareStatements()`.
|
|
643
|
+
*
|
|
644
|
+
* The `db` getter exposes the raw `DatabaseInstance` to subclasses only.
|
|
645
|
+
*/
|
|
646
|
+
/**
|
|
647
|
+
* Track all live DatabaseInstance objects so we can close them on process exit.
|
|
648
|
+
* Prevents better-sqlite3 segfaults caused by V8 garbage-collecting Database
|
|
649
|
+
* objects after the native addon context is already torn down.
|
|
650
|
+
*
|
|
651
|
+
* Uses a global symbol so the set and exit handler survive vitest's module
|
|
652
|
+
* re-imports within the same fork process (ESM isolate mode clears
|
|
653
|
+
* module-level state but globalThis persists).
|
|
654
|
+
*/
|
|
655
|
+
// v1.0.130 — symbol name bumped because the value type reverted from
|
|
656
|
+
// Map<DatabaseInstance, string> (v1.0.128 lockfile pairing) back to
|
|
657
|
+
// Set<DatabaseInstance>. A persistent global slot from a v1.0.128 or
|
|
658
|
+
// v1.0.129 module would deserialize as the wrong shape and crash the
|
|
659
|
+
// exit hook iteration.
|
|
660
|
+
const _kLiveDBs = Symbol.for("__context_mode_live_dbs_v3__");
|
|
661
|
+
const _liveDBs = (() => {
|
|
662
|
+
const g = globalThis;
|
|
663
|
+
if (!g[_kLiveDBs]) {
|
|
664
|
+
g[_kLiveDBs] = new Set();
|
|
665
|
+
process.on("exit", () => {
|
|
666
|
+
for (const db of g[_kLiveDBs]) {
|
|
667
|
+
closeDB(db);
|
|
668
|
+
}
|
|
669
|
+
g[_kLiveDBs].clear();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return g[_kLiveDBs];
|
|
673
|
+
})();
|
|
674
|
+
export class SQLiteBase {
|
|
675
|
+
#dbPath;
|
|
676
|
+
#db;
|
|
677
|
+
/**
|
|
678
|
+
* Open (or create) a SQLite DB at `dbPath`.
|
|
679
|
+
*
|
|
680
|
+
* v1.0.130 — multi-writer is the contract. ALL SQLiteBase consumers
|
|
681
|
+
* (SessionDB, ContentStore) may open the same on-disk dbPath from
|
|
682
|
+
* multiple processes simultaneously — that is the legitimate multi-
|
|
683
|
+
* window UX shape and the WAL handles it natively. SQLITE_BUSY on
|
|
684
|
+
* write contention is absorbed by `withRetry()` below (busy_timeout
|
|
685
|
+
* = 30000ms inside `new Database(...)`).
|
|
686
|
+
*
|
|
687
|
+
* v1.0.128 introduced a single-writer guard here as a defense against
|
|
688
|
+
* #560. That defense was an over-correction — the actual root causes
|
|
689
|
+
* of #560 were #559 (zombie MCP child accumulation) and #561 (Pi
|
|
690
|
+
* misdetection writing to the wrong DB path), both fixed in v1.0.128
|
|
691
|
+
* + v1.0.129. The single-writer guard broke legitimate multi-window
|
|
692
|
+
* users; v1.0.130 rolls it out. See
|
|
693
|
+
* docs/adr/0001-sessiondb-multi-writer.md and the v1.0.130 INVARIANT
|
|
694
|
+
* block in tests/util/db-base-platform-gate.test.ts for the
|
|
695
|
+
* regression-proof anchor (source-pin + behavioural).
|
|
696
|
+
*/
|
|
697
|
+
constructor(dbPath) {
|
|
698
|
+
const Database = loadDatabase();
|
|
699
|
+
this.#dbPath = dbPath;
|
|
700
|
+
cleanOrphanedWALFiles(dbPath);
|
|
701
|
+
let db;
|
|
702
|
+
try {
|
|
703
|
+
db = new Database(dbPath, { timeout: 30000 });
|
|
704
|
+
applyWALPragmas(db);
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
708
|
+
if (isSQLiteCorruptionError(msg)) {
|
|
709
|
+
// Surface the corruption + quarantine instead of silently swapping
|
|
710
|
+
// the file — users lose session history here and deserve a trace.
|
|
711
|
+
logDbError("SQLiteBase.open", err, dbPath);
|
|
712
|
+
renameCorruptDB(dbPath);
|
|
713
|
+
cleanOrphanedWALFiles(dbPath);
|
|
714
|
+
try {
|
|
715
|
+
db = new Database(dbPath, { timeout: 30000 });
|
|
716
|
+
applyWALPragmas(db);
|
|
717
|
+
}
|
|
718
|
+
catch (retryErr) {
|
|
719
|
+
throw new Error(`Failed to create fresh DB after renaming corrupt file: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
throw err;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
this.#db = db;
|
|
727
|
+
_liveDBs.add(this.#db);
|
|
728
|
+
this.initSchema();
|
|
729
|
+
this.prepareStatements();
|
|
730
|
+
}
|
|
731
|
+
/** Raw database instance — available to subclasses only. */
|
|
732
|
+
get db() {
|
|
733
|
+
return this.#db;
|
|
734
|
+
}
|
|
735
|
+
/** The path this database was opened from. */
|
|
736
|
+
get dbPath() {
|
|
737
|
+
return this.#dbPath;
|
|
738
|
+
}
|
|
739
|
+
/** Close the database connection without deleting files. */
|
|
740
|
+
close() {
|
|
741
|
+
_liveDBs.delete(this.#db);
|
|
742
|
+
closeDB(this.#db);
|
|
743
|
+
}
|
|
744
|
+
withRetry(fn) {
|
|
745
|
+
return withRetry(fn);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Close the connection and delete all associated DB files (main, WAL, SHM).
|
|
749
|
+
* Call on process exit or at end of session lifecycle.
|
|
750
|
+
*/
|
|
751
|
+
cleanup() {
|
|
752
|
+
_liveDBs.delete(this.#db);
|
|
753
|
+
closeDB(this.#db);
|
|
754
|
+
deleteDBFiles(this.#dbPath);
|
|
755
|
+
}
|
|
756
|
+
}
|