context-mode 1.0.144 → 1.0.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +1 -11
- package/build/adapters/base.d.ts +34 -0
- package/build/adapters/base.js +50 -2
- package/build/adapters/claude-code/index.js +9 -1
- package/build/adapters/codex/index.js +16 -2
- package/build/adapters/omp/plugin.js +34 -8
- package/build/adapters/openclaw/plugin.js +40 -8
- package/build/adapters/opencode/index.js +9 -2
- package/build/adapters/pi/extension.js +36 -8
- package/build/adapters/pi/mcp-bridge.js +58 -2
- package/build/adapters/vscode-copilot/index.js +11 -0
- package/build/store.js +17 -5
- package/cli.bundle.mjs +135 -135
- package/hooks/sessionstart.mjs +8 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +97 -97
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.146"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.146",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.146",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.146",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.146",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.146",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/README.md
CHANGED
|
@@ -1387,17 +1387,7 @@ export CTX_FETCH_STRICT=1
|
|
|
1387
1387
|
|
|
1388
1388
|
That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. Useful when context-mode runs as a shared service, not on a developer's own machine.
|
|
1389
1389
|
|
|
1390
|
-
`tool_input` for any `mcp__*` tool call is also redacted before persistence —
|
|
1391
|
-
|
|
1392
|
-
### Lifecycle environment variables
|
|
1393
|
-
|
|
1394
|
-
One runtime knob controls MCP sibling cleanup. Idle self-shutdown was removed after [#592](https://github.com/mksglu/context-mode/issues/592): hosts can keep registered tool handles after a clean MCP exit, making a timer-driven exit unsafe.
|
|
1395
|
-
|
|
1396
|
-
| Variable | Default | Purpose |
|
|
1397
|
-
|---|---|---|
|
|
1398
|
-
| `CONTEXT_MODE_STARTUP_SWEEP` | `1` (enabled) | At boot, a newly-spawned MCP child reaps any other context-mode MCP server pids that share its parent process (`sameParentOnly: true` — never touches MCP children of a different host). This reclaims accumulated siblings immediately instead of waiting for each idle timer to fire. Set to `0` or `false` to disable (useful when you intentionally want multiple concurrent MCP children under the same host, e.g. multi-tenant test runners). |
|
|
1399
|
-
|
|
1400
|
-
`CONTEXT_MODE_STARTUP_SWEEP` is read fresh at MCP server start — no restart of the host CLI is required, just spawn a new MCP child (open a new session) for changes to take effect. Unrecognized values fall back to enabled.
|
|
1390
|
+
`tool_input` for any `mcp__*` tool call is also redacted before persistence — the regex matcher in `hooks/posttooluse.mjs` masks `authorization`, `auth_token`, `access_token`, `refresh_token`, `bearer`, `token`, `secret`, `password`, `passwd`, `pwd`, `api_key` / `apikey` / `x_api_key`, `cookie` / `set-cookie`, `signature`, `private_key`, and `client_secret` (case-insensitive, hyphen/underscore-insensitive) to `[REDACTED]` so credentials in MCP arguments don't end up in the session DB.
|
|
1401
1391
|
|
|
1402
1392
|
### Routing-guidance environment variables
|
|
1403
1393
|
|
package/build/adapters/base.d.ts
CHANGED
|
@@ -20,7 +20,30 @@
|
|
|
20
20
|
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
|
21
21
|
* in `src/session/db.ts`. Adapters expose only `getSessionDir()` for
|
|
22
22
|
* storage-related path concerns.
|
|
23
|
+
*
|
|
24
|
+
* Issue #649 — `CONTEXT_MODE_DATA_DIR` universal storage override. Many
|
|
25
|
+
* adapters (Pi, OMP, Gemini CLI, Codex, Cursor, …) had storage hardcoded to
|
|
26
|
+
* `~/.<platform>/context-mode/sessions/` with no env-var escape hatch. CI
|
|
27
|
+
* runners on NFS homes, dev containers, and shared-workspace setups need to
|
|
28
|
+
* point context-mode storage at a writable volume without patching source or
|
|
29
|
+
* abusing the host platform's own config-dir variable. The override applies
|
|
30
|
+
* only to context-mode-owned state (`getSessionDir`, `getMemoryDir`) — never
|
|
31
|
+
* to platform-native config (`getConfigDir`, `getSettingsPath`), which must
|
|
32
|
+
* stay where the host platform's own tooling expects it. Adapters that
|
|
33
|
+
* override `getSessionDir`/`getMemoryDir` directly (claude-code, codex,
|
|
34
|
+
* opencode, vscode-copilot) honor the override by routing through
|
|
35
|
+
* `resolveContextModeDataRoot()` at the top of their override.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Universal storage-root override. Returns the resolved absolute path when
|
|
39
|
+
* `CONTEXT_MODE_DATA_DIR` is set to a non-blank value, otherwise `null` so
|
|
40
|
+
* callers fall back to their platform-native default.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors the `resolveClaudeConfigDir` contract for env-var handling
|
|
43
|
+
* (whitespace guard, tilde expansion, relative-path resolution) so users
|
|
44
|
+
* get one consistent set of rules across every override site.
|
|
23
45
|
*/
|
|
46
|
+
export declare function resolveContextModeDataRoot(env?: NodeJS.ProcessEnv): string | null;
|
|
24
47
|
export declare abstract class BaseAdapter {
|
|
25
48
|
protected readonly sessionDirSegments: string[];
|
|
26
49
|
constructor(sessionDirSegments: string[]);
|
|
@@ -33,6 +56,13 @@ export declare abstract class BaseAdapter {
|
|
|
33
56
|
* openclaw, opencode) override this and resolve their segments against
|
|
34
57
|
* `projectDir` (or `process.cwd()` when omitted).
|
|
35
58
|
*
|
|
59
|
+
* NOT relocated by `CONTEXT_MODE_DATA_DIR` (#649). The platform owns its
|
|
60
|
+
* settings.json / hooks.json / config.toml location — relocating that
|
|
61
|
+
* would silently fork platform behaviour from the platform's own tooling.
|
|
62
|
+
* Use `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `XDG_CONFIG_HOME`, etc. to move
|
|
63
|
+
* platform-native config; use `CONTEXT_MODE_DATA_DIR` to move context-mode
|
|
64
|
+
* storage independently.
|
|
65
|
+
*
|
|
36
66
|
* @param _projectDir Unused by the home-rooted default — accepted so
|
|
37
67
|
* project-scoped overrides honor the same signature.
|
|
38
68
|
*/
|
|
@@ -46,6 +76,10 @@ export declare abstract class BaseAdapter {
|
|
|
46
76
|
* Default: <configDir>/memory. Always absolute (configDir is absolute by
|
|
47
77
|
* contract). Adapters with a different memory dir name (e.g., codex uses
|
|
48
78
|
* "memories" plural) override this.
|
|
79
|
+
*
|
|
80
|
+
* Issue #649: when `CONTEXT_MODE_DATA_DIR` is set, memory follows storage
|
|
81
|
+
* to `<DATA_DIR>/context-mode/memory/` since persistent memory is
|
|
82
|
+
* context-mode-owned state, not platform-native config.
|
|
49
83
|
*/
|
|
50
84
|
getMemoryDir(): string;
|
|
51
85
|
backupSettings(): string | null;
|
package/build/adapters/base.js
CHANGED
|
@@ -20,17 +20,51 @@
|
|
|
20
20
|
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
|
21
21
|
* in `src/session/db.ts`. Adapters expose only `getSessionDir()` for
|
|
22
22
|
* storage-related path concerns.
|
|
23
|
+
*
|
|
24
|
+
* Issue #649 — `CONTEXT_MODE_DATA_DIR` universal storage override. Many
|
|
25
|
+
* adapters (Pi, OMP, Gemini CLI, Codex, Cursor, …) had storage hardcoded to
|
|
26
|
+
* `~/.<platform>/context-mode/sessions/` with no env-var escape hatch. CI
|
|
27
|
+
* runners on NFS homes, dev containers, and shared-workspace setups need to
|
|
28
|
+
* point context-mode storage at a writable volume without patching source or
|
|
29
|
+
* abusing the host platform's own config-dir variable. The override applies
|
|
30
|
+
* only to context-mode-owned state (`getSessionDir`, `getMemoryDir`) — never
|
|
31
|
+
* to platform-native config (`getConfigDir`, `getSettingsPath`), which must
|
|
32
|
+
* stay where the host platform's own tooling expects it. Adapters that
|
|
33
|
+
* override `getSessionDir`/`getMemoryDir` directly (claude-code, codex,
|
|
34
|
+
* opencode, vscode-copilot) honor the override by routing through
|
|
35
|
+
* `resolveContextModeDataRoot()` at the top of their override.
|
|
23
36
|
*/
|
|
24
|
-
import { join } from "node:path";
|
|
37
|
+
import { join, resolve } from "node:path";
|
|
25
38
|
import { accessSync, copyFileSync, constants, mkdirSync } from "node:fs";
|
|
26
39
|
import { homedir } from "node:os";
|
|
40
|
+
/**
|
|
41
|
+
* Universal storage-root override. Returns the resolved absolute path when
|
|
42
|
+
* `CONTEXT_MODE_DATA_DIR` is set to a non-blank value, otherwise `null` so
|
|
43
|
+
* callers fall back to their platform-native default.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the `resolveClaudeConfigDir` contract for env-var handling
|
|
46
|
+
* (whitespace guard, tilde expansion, relative-path resolution) so users
|
|
47
|
+
* get one consistent set of rules across every override site.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveContextModeDataRoot(env = process.env) {
|
|
50
|
+
const raw = env.CONTEXT_MODE_DATA_DIR;
|
|
51
|
+
if (!raw || raw.trim() === "")
|
|
52
|
+
return null;
|
|
53
|
+
if (raw.startsWith("~")) {
|
|
54
|
+
return resolve(homedir(), raw.replace(/^~[/\\]?/, ""));
|
|
55
|
+
}
|
|
56
|
+
return resolve(raw);
|
|
57
|
+
}
|
|
27
58
|
export class BaseAdapter {
|
|
28
59
|
sessionDirSegments;
|
|
29
60
|
constructor(sessionDirSegments) {
|
|
30
61
|
this.sessionDirSegments = sessionDirSegments;
|
|
31
62
|
}
|
|
32
63
|
getSessionDir() {
|
|
33
|
-
const
|
|
64
|
+
const override = resolveContextModeDataRoot();
|
|
65
|
+
const dir = override
|
|
66
|
+
? join(override, "context-mode", "sessions")
|
|
67
|
+
: join(homedir(), ...this.sessionDirSegments, "context-mode", "sessions");
|
|
34
68
|
mkdirSync(dir, { recursive: true });
|
|
35
69
|
return dir;
|
|
36
70
|
}
|
|
@@ -42,6 +76,13 @@ export class BaseAdapter {
|
|
|
42
76
|
* openclaw, opencode) override this and resolve their segments against
|
|
43
77
|
* `projectDir` (or `process.cwd()` when omitted).
|
|
44
78
|
*
|
|
79
|
+
* NOT relocated by `CONTEXT_MODE_DATA_DIR` (#649). The platform owns its
|
|
80
|
+
* settings.json / hooks.json / config.toml location — relocating that
|
|
81
|
+
* would silently fork platform behaviour from the platform's own tooling.
|
|
82
|
+
* Use `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `XDG_CONFIG_HOME`, etc. to move
|
|
83
|
+
* platform-native config; use `CONTEXT_MODE_DATA_DIR` to move context-mode
|
|
84
|
+
* storage independently.
|
|
85
|
+
*
|
|
45
86
|
* @param _projectDir Unused by the home-rooted default — accepted so
|
|
46
87
|
* project-scoped overrides honor the same signature.
|
|
47
88
|
*/
|
|
@@ -59,8 +100,15 @@ export class BaseAdapter {
|
|
|
59
100
|
* Default: <configDir>/memory. Always absolute (configDir is absolute by
|
|
60
101
|
* contract). Adapters with a different memory dir name (e.g., codex uses
|
|
61
102
|
* "memories" plural) override this.
|
|
103
|
+
*
|
|
104
|
+
* Issue #649: when `CONTEXT_MODE_DATA_DIR` is set, memory follows storage
|
|
105
|
+
* to `<DATA_DIR>/context-mode/memory/` since persistent memory is
|
|
106
|
+
* context-mode-owned state, not platform-native config.
|
|
62
107
|
*/
|
|
63
108
|
getMemoryDir() {
|
|
109
|
+
const override = resolveContextModeDataRoot();
|
|
110
|
+
if (override)
|
|
111
|
+
return join(override, "context-mode", "memory");
|
|
64
112
|
return join(this.getConfigDir(), "memory");
|
|
65
113
|
}
|
|
66
114
|
backupSettings() {
|
|
@@ -15,6 +15,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, chmodSync, access
|
|
|
15
15
|
import { resolve, join } from "node:path";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
18
|
+
import { resolveContextModeDataRoot } from "../base.js";
|
|
18
19
|
import { resolveClaudeConfigDir } from "../../util/claude-config.js";
|
|
19
20
|
import { checkPluginCacheIntegritySync } from "../../util/plugin-cache-integrity.js";
|
|
20
21
|
import { buildNodeCommand, } from "../types.js";
|
|
@@ -61,7 +62,14 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
61
62
|
return resolveClaudeConfigDir();
|
|
62
63
|
}
|
|
63
64
|
getSessionDir() {
|
|
64
|
-
|
|
65
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
66
|
+
// before falling back to the Claude-rooted default. The override moves
|
|
67
|
+
// ONLY context-mode-owned state; settings.json + CLAUDE_CONFIG_DIR stay
|
|
68
|
+
// intact below.
|
|
69
|
+
const override = resolveContextModeDataRoot();
|
|
70
|
+
const dir = override
|
|
71
|
+
? join(override, "context-mode", "sessions")
|
|
72
|
+
: join(this.getConfigDir(), "context-mode", "sessions");
|
|
65
73
|
mkdirSync(dir, { recursive: true });
|
|
66
74
|
return dir;
|
|
67
75
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
|
|
17
17
|
import { resolve, dirname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { BaseAdapter } from "../base.js";
|
|
19
|
+
import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
|
|
20
20
|
import { resolveCodexConfigDir } from "./paths.js";
|
|
21
21
|
// PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
|
|
22
22
|
// names + external MCP catch-all literal (#529, #547 hotfix).
|
|
@@ -236,7 +236,14 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
236
236
|
return join(this.getConfigDir(), "config.toml");
|
|
237
237
|
}
|
|
238
238
|
getSessionDir() {
|
|
239
|
-
|
|
239
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
240
|
+
// before falling back to the $CODEX_HOME-rooted default. Settings.toml
|
|
241
|
+
// and hooks.json continue to live under getConfigDir() so the Codex CLI
|
|
242
|
+
// sees its own config in the expected place.
|
|
243
|
+
const override = resolveContextModeDataRoot();
|
|
244
|
+
const dir = override
|
|
245
|
+
? join(override, "context-mode", "sessions")
|
|
246
|
+
: join(this.getConfigDir(), "context-mode", "sessions");
|
|
240
247
|
mkdirSync(dir, { recursive: true });
|
|
241
248
|
return dir;
|
|
242
249
|
}
|
|
@@ -252,6 +259,13 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
252
259
|
}
|
|
253
260
|
getMemoryDir() {
|
|
254
261
|
// Codex uses "memories" (plural), not the default "memory".
|
|
262
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR for context-mode-owned
|
|
263
|
+
// persistent memory while preserving the platform-native plural folder
|
|
264
|
+
// name so legacy Codex tooling continues to find it when DATA_DIR is
|
|
265
|
+
// unset. Under the override, layout is `<DATA_DIR>/context-mode/memories`.
|
|
266
|
+
const override = resolveContextModeDataRoot();
|
|
267
|
+
if (override)
|
|
268
|
+
return join(override, "context-mode", "memories");
|
|
255
269
|
return join(this.getConfigDir(), "memories");
|
|
256
270
|
}
|
|
257
271
|
generateHookConfig(_pluginRoot) {
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { createHash } from "node:crypto";
|
|
27
27
|
import { mkdirSync } from "node:fs";
|
|
28
|
-
import {
|
|
29
|
-
import { SessionDB } from "../../session/db.js";
|
|
28
|
+
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
|
|
30
29
|
import { extractEvents } from "../../session/extract.js";
|
|
31
30
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
32
31
|
import { OMPAdapter } from "./index.js";
|
|
@@ -62,6 +61,7 @@ const BLOCKED_BASH_PATTERNS = [
|
|
|
62
61
|
// session_start so multi-session reuse within a long-lived plugin
|
|
63
62
|
// process keeps event attribution correct.
|
|
64
63
|
let _db = null;
|
|
64
|
+
let _dbPath = "";
|
|
65
65
|
let _sessionId = "";
|
|
66
66
|
const _ompAdapter = new OMPAdapter();
|
|
67
67
|
function getSessionDir() {
|
|
@@ -69,12 +69,31 @@ function getSessionDir() {
|
|
|
69
69
|
mkdirSync(dir, { recursive: true });
|
|
70
70
|
return dir;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// Issue #645 — route through the canonical per-project resolver the MCP
|
|
73
|
+
// server uses (src/server.ts ctx_stats / ctx_search timeline). The
|
|
74
|
+
// previous shared `context-mode.db` literal was a different file from
|
|
75
|
+
// the `<canonical-hash>.db` the server reads, so every OMP user's
|
|
76
|
+
// `ctx_stats` reported zero history and `ctx_search(sort: "timeline")`
|
|
77
|
+
// silently dropped the sort. Mirrors the matching Pi fix and the
|
|
78
|
+
// opencode plugin pattern (src/adapters/opencode/plugin.ts:307).
|
|
79
|
+
function getDBPath(projectDir) {
|
|
80
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
74
81
|
}
|
|
75
|
-
function getOrCreateDB() {
|
|
76
|
-
if
|
|
77
|
-
|
|
82
|
+
function getOrCreateDB(projectDir) {
|
|
83
|
+
// Reopen the singleton if the resolved DB path changes. See the
|
|
84
|
+
// matching Pi extension comment — defensive re-keying on projectDir
|
|
85
|
+
// hash keeps tests deterministic and stops a stale singleton from
|
|
86
|
+
// pointing at an earlier projectDir's `<hash>.db`. (#645)
|
|
87
|
+
const dbPath = getDBPath(projectDir);
|
|
88
|
+
if (!_db || _dbPath !== dbPath) {
|
|
89
|
+
if (_db) {
|
|
90
|
+
try {
|
|
91
|
+
_db.close();
|
|
92
|
+
}
|
|
93
|
+
catch { /* best effort */ }
|
|
94
|
+
}
|
|
95
|
+
_db = new SessionDB({ dbPath });
|
|
96
|
+
_dbPath = dbPath;
|
|
78
97
|
}
|
|
79
98
|
return _db;
|
|
80
99
|
}
|
|
@@ -102,7 +121,14 @@ function deriveSessionId(ctx) {
|
|
|
102
121
|
// The plugin's default export is the OMP factory; this helper is only
|
|
103
122
|
// imported by tests to clear singletons between cases.
|
|
104
123
|
export function _resetOmpPluginStateForTests() {
|
|
124
|
+
if (_db) {
|
|
125
|
+
try {
|
|
126
|
+
_db.close();
|
|
127
|
+
}
|
|
128
|
+
catch { /* best effort */ }
|
|
129
|
+
}
|
|
105
130
|
_db = null;
|
|
131
|
+
_dbPath = "";
|
|
106
132
|
_sessionId = "";
|
|
107
133
|
}
|
|
108
134
|
/**
|
|
@@ -129,7 +155,7 @@ export default function ompPlugin(pi) {
|
|
|
129
155
|
// earlier `OMP_PROJECT_DIR` read was an EM mistake — no upstream code
|
|
130
156
|
// ever sets it. Drop it; fall through PI_PROJECT_DIR → cwd().
|
|
131
157
|
const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
|
|
132
|
-
const db = getOrCreateDB();
|
|
158
|
+
const db = getOrCreateDB(projectDir);
|
|
133
159
|
// ── 1. session_start — initialize session row ─────────
|
|
134
160
|
pi.on("session_start", (_event, ctx) => {
|
|
135
161
|
try {
|
|
@@ -33,6 +33,8 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
33
33
|
import { homedir } from "node:os";
|
|
34
34
|
import { dirname, join, resolve } from "node:path";
|
|
35
35
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
36
|
+
import { resolveContextModeDataRoot } from "../base.js";
|
|
37
|
+
import { resolveSessionDbPath } from "../../session/db.js";
|
|
36
38
|
import { OpenClawSessionDB } from "./session-db.js";
|
|
37
39
|
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
38
40
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
@@ -70,26 +72,56 @@ const configSchema = {
|
|
|
70
72
|
};
|
|
71
73
|
// ── Helpers ───────────────────────────────────────────────
|
|
72
74
|
function getSessionDir() {
|
|
73
|
-
|
|
75
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
76
|
+
// ahead of the hardcoded ~/.openclaw root so dev-container/CI/NFS-home
|
|
77
|
+
// users can relocate context-mode storage without patching the source.
|
|
78
|
+
// Kept in sync with OpenClawAdapter.getSessionDir() (inherited from
|
|
79
|
+
// BaseAdapter) — both call sites must agree byte-for-byte.
|
|
80
|
+
const override = resolveContextModeDataRoot();
|
|
81
|
+
const dir = override
|
|
82
|
+
? join(override, "context-mode", "sessions")
|
|
83
|
+
: join(homedir(), ".openclaw", "context-mode", "sessions");
|
|
74
84
|
mkdirSync(dir, { recursive: true });
|
|
75
85
|
return dir;
|
|
76
86
|
}
|
|
87
|
+
// Issue #645 follow-up — route through the canonical per-project
|
|
88
|
+
// resolver the MCP server uses (src/server.ts ctx_stats / ctx_search
|
|
89
|
+
// timeline). The previous raw `sha256(projectDir).slice(0, 16)` shape
|
|
90
|
+
// produced a different file from the `<canonical-hash>.db` the server
|
|
91
|
+
// reads on darwin/win32 (case-fold) and inside linked worktrees
|
|
92
|
+
// (suffix). The result was silent degradation of `ctx_stats` (zero
|
|
93
|
+
// history) and `ctx_search(sort: "timeline")` (sort dropped) for any
|
|
94
|
+
// OpenClaw user with an uppercase character in their projectDir.
|
|
95
|
+
// Mirrors the matching Pi (src/adapters/pi/extension.ts:223) and OMP
|
|
96
|
+
// (src/adapters/omp/plugin.ts:90) fixes, and the opencode plugin
|
|
97
|
+
// pattern (src/adapters/opencode/plugin.ts:307).
|
|
77
98
|
function getDBPath(projectDir) {
|
|
78
|
-
|
|
79
|
-
.update(projectDir)
|
|
80
|
-
.digest("hex")
|
|
81
|
-
.slice(0, 16);
|
|
82
|
-
return join(getSessionDir(), `${hash}.db`);
|
|
99
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
83
100
|
}
|
|
84
101
|
// ── Module-level DB singleton ─────────────────────────────
|
|
85
102
|
// Shared across all register() calls (one per agent session).
|
|
86
103
|
// Lazy-initialized on first register() using the first projectDir seen.
|
|
87
104
|
// Uses OpenClawSessionDB for session_key mapping and rename support.
|
|
88
105
|
let _dbSingleton = null;
|
|
106
|
+
let _dbSingletonPath = "";
|
|
89
107
|
function getOrCreateDB(projectDir) {
|
|
90
|
-
if
|
|
91
|
-
|
|
108
|
+
// Reopen the singleton if the resolved DB path changes. Production
|
|
109
|
+
// code normally loads the plugin once per process with a single
|
|
110
|
+
// workspace, but defensive re-keying on resolved path keeps the
|
|
111
|
+
// contract honest if a host ever calls register() twice with
|
|
112
|
+
// different projectDirs, and removes a subtle test-isolation
|
|
113
|
+
// foot-gun where a stale singleton pointed at a prior test's
|
|
114
|
+
// `<hash>.db`. Mirrors the Pi/OMP fix (#645).
|
|
115
|
+
const dbPath = getDBPath(projectDir);
|
|
116
|
+
if (!_dbSingleton || _dbSingletonPath !== dbPath) {
|
|
117
|
+
if (_dbSingleton) {
|
|
118
|
+
try {
|
|
119
|
+
_dbSingleton.close();
|
|
120
|
+
}
|
|
121
|
+
catch { /* best effort */ }
|
|
122
|
+
}
|
|
92
123
|
_dbSingleton = new OpenClawSessionDB({ dbPath });
|
|
124
|
+
_dbSingletonPath = dbPath;
|
|
93
125
|
_dbSingleton.cleanupOldSessions(7);
|
|
94
126
|
}
|
|
95
127
|
return _dbSingleton;
|
|
@@ -25,7 +25,7 @@ function stripJsonComments(str) {
|
|
|
25
25
|
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, constants, } from "node:fs";
|
|
26
26
|
import { resolve, join } from "node:path";
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
|
-
import { BaseAdapter } from "../base.js";
|
|
28
|
+
import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
|
|
29
29
|
// ─────────────────────────────────────────────────────────
|
|
30
30
|
// Hook constants (re-exported from hooks.ts)
|
|
31
31
|
// ─────────────────────────────────────────────────────────
|
|
@@ -177,7 +177,14 @@ export class OpenCodeAdapter extends BaseAdapter {
|
|
|
177
177
|
];
|
|
178
178
|
}
|
|
179
179
|
getSessionDir() {
|
|
180
|
-
|
|
180
|
+
// Issue #649: honor CONTEXT_MODE_DATA_DIR universal storage override
|
|
181
|
+
// ahead of OpenCode/Kilo's XDG-rooted default. opencode.json + plugin
|
|
182
|
+
// discovery stay under getConfigDir() so OpenCode itself sees its own
|
|
183
|
+
// config in the expected location.
|
|
184
|
+
const override = resolveContextModeDataRoot();
|
|
185
|
+
const dir = override
|
|
186
|
+
? join(override, "context-mode", "sessions")
|
|
187
|
+
: join(this.getConfigDir(), "context-mode", "sessions");
|
|
181
188
|
mkdirSync(dir, { recursive: true });
|
|
182
189
|
return dir;
|
|
183
190
|
}
|
|
@@ -15,7 +15,7 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join, resolve, dirname } from "node:path";
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
-
import { SessionDB } from "../../session/db.js";
|
|
18
|
+
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
|
|
19
19
|
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
20
20
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
21
21
|
import { bootstrapMCPTools } from "./mcp-bridge.js";
|
|
@@ -121,6 +121,7 @@ export function isSafeCurlWget(segment) {
|
|
|
121
121
|
}
|
|
122
122
|
// ── Module-level DB singleton ────────────────────────────
|
|
123
123
|
let _db = null;
|
|
124
|
+
let _dbPath = "";
|
|
124
125
|
let _sessionId = "";
|
|
125
126
|
// MCP bridge handle. The bridge spawns server.bundle.mjs once and
|
|
126
127
|
// registers each MCP tool through pi.registerTool() so the Pi LLM can
|
|
@@ -181,12 +182,38 @@ function getSessionDir() {
|
|
|
181
182
|
mkdirSync(dir, { recursive: true });
|
|
182
183
|
return dir;
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
// Issue #645 — the MCP server (src/server.ts ctx_stats / ctx_search
|
|
186
|
+
// timeline) resolves the SessionDB filename via
|
|
187
|
+
// `resolveSessionDbPath({ projectDir, sessionsDir })`, which produces a
|
|
188
|
+
// per-project canonical `<16-hex-hash>.db` (case-folded on darwin/win32,
|
|
189
|
+
// suffixed for non-main worktrees). The Pi extension previously wrote
|
|
190
|
+
// every session to a shared `context-mode.db` literal — a different
|
|
191
|
+
// file the server never reads. The result was silent degradation of
|
|
192
|
+
// `ctx_stats` (zero history) and `ctx_search(sort: "timeline")` (sort
|
|
193
|
+
// dropped) for every Pi user. Routing through the same helper keeps the
|
|
194
|
+
// extension-side writes and the server-side reads aligned across
|
|
195
|
+
// case-fold migrations, worktree suffixes, and any future change to the
|
|
196
|
+
// canonical filename contract.
|
|
197
|
+
function getDBPath(projectDir) {
|
|
198
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
186
199
|
}
|
|
187
|
-
function getOrCreateDB() {
|
|
188
|
-
if
|
|
189
|
-
|
|
200
|
+
function getOrCreateDB(projectDir) {
|
|
201
|
+
// Reopen the singleton if the resolved DB path changes. Production code
|
|
202
|
+
// normally loads the extension once per process with a single workspace,
|
|
203
|
+
// but defensive re-keying on path keeps the contract honest if a host
|
|
204
|
+
// ever calls piExtension(pi) twice with different projectDirs, and
|
|
205
|
+
// removes a subtle test-isolation foot-gun where stale singletons
|
|
206
|
+
// pointed at a prior test's `<hash>.db`. (#645)
|
|
207
|
+
const dbPath = getDBPath(projectDir);
|
|
208
|
+
if (!_db || _dbPath !== dbPath) {
|
|
209
|
+
if (_db) {
|
|
210
|
+
try {
|
|
211
|
+
_db.close();
|
|
212
|
+
}
|
|
213
|
+
catch { /* best effort */ }
|
|
214
|
+
}
|
|
215
|
+
_db = new SessionDB({ dbPath });
|
|
216
|
+
_dbPath = dbPath;
|
|
190
217
|
}
|
|
191
218
|
return _db;
|
|
192
219
|
}
|
|
@@ -368,7 +395,7 @@ export default function piExtension(pi) {
|
|
|
368
395
|
// events default to project_dir="" which causes cross-project data leakage
|
|
369
396
|
// in shared SessionDB instances.
|
|
370
397
|
const _attribution = { projectDir, source: "workspace_root", confidence: 0.98 };
|
|
371
|
-
const db = getOrCreateDB();
|
|
398
|
+
const db = getOrCreateDB(projectDir);
|
|
372
399
|
// ── 1. session_start — Initialize session ──────────────
|
|
373
400
|
pi.on("session_start", (_event, ctx) => {
|
|
374
401
|
try {
|
|
@@ -658,6 +685,7 @@ export default function piExtension(pi) {
|
|
|
658
685
|
_db.cleanupOldSessions(7);
|
|
659
686
|
}
|
|
660
687
|
_db = null;
|
|
688
|
+
_dbPath = "";
|
|
661
689
|
_sessionId = "";
|
|
662
690
|
}
|
|
663
691
|
catch {
|
|
@@ -704,7 +732,7 @@ export default function piExtension(pi) {
|
|
|
704
732
|
description: "Run context-mode diagnostics",
|
|
705
733
|
handler: async (argsOrCtx, maybeCtx) => {
|
|
706
734
|
const ctx = resolveCommandContext(argsOrCtx, maybeCtx);
|
|
707
|
-
const dbPath = getDBPath();
|
|
735
|
+
const dbPath = getDBPath(projectDir);
|
|
708
736
|
const dbExists = existsSync(dbPath);
|
|
709
737
|
const lines = [
|
|
710
738
|
"## ctx-doctor (Pi)",
|
|
@@ -102,6 +102,25 @@ export function resolveJsRuntimeForBridge(deps = {}) {
|
|
|
102
102
|
// layer (per-tool timeout / background mode / Pi-level cancel), not
|
|
103
103
|
// to the transport.
|
|
104
104
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
105
|
+
// Retry budget for the bridge bootstrap `initialize` handshake (#647).
|
|
106
|
+
//
|
|
107
|
+
// On cold NFS home dirs, first JIT compile of server.bundle.mjs, or
|
|
108
|
+
// constrained CI runners, the first `initialize` can exceed the 60s
|
|
109
|
+
// ceiling above. Before this fix, bootstrapMCPTools propagated the
|
|
110
|
+
// rejection up to extension.ts, which logged once and continued with
|
|
111
|
+
// NO ctx_* tools registered — silently degrading the session for its
|
|
112
|
+
// entire lifetime while the routing block kept emitting ~2.5K tokens
|
|
113
|
+
// of dead instructions per turn.
|
|
114
|
+
//
|
|
115
|
+
// Retry pattern mirrors the existing #583 single-flight respawn shape:
|
|
116
|
+
// on failure, shut the prior child cleanly, sleep a short backoff so
|
|
117
|
+
// the OS reclaims fds, then start + initialize again. After the budget
|
|
118
|
+
// is exhausted we re-throw and the existing extension.ts handler runs
|
|
119
|
+
// the degrade-and-log path — preserving the contract for genuinely
|
|
120
|
+
// broken servers (binary missing, runtime crash, etc.) while
|
|
121
|
+
// self-healing the transient warm-up case.
|
|
122
|
+
const MAX_INIT_RETRIES = 2;
|
|
123
|
+
const INIT_RETRY_DELAY_MS = 1_000;
|
|
105
124
|
class PiTextComponent {
|
|
106
125
|
text;
|
|
107
126
|
constructor(text = "") {
|
|
@@ -566,8 +585,45 @@ export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
|
566
585
|
return skippedBridge();
|
|
567
586
|
}
|
|
568
587
|
const client = new MCPStdioClient(serverScript, env, runtime);
|
|
569
|
-
|
|
570
|
-
|
|
588
|
+
// Retry-on-slow-initialize (#647).
|
|
589
|
+
//
|
|
590
|
+
// Each attempt is independently bounded by DEFAULT_REQUEST_TIMEOUT_MS
|
|
591
|
+
// (60s) inside request(). On failure we shutdown the child to release
|
|
592
|
+
// its fds before respawning — this is the same sequencing the #583
|
|
593
|
+
// respawn path uses, just hoisted into the bootstrap layer where the
|
|
594
|
+
// failure happens before any tool was registered. Final attempt's
|
|
595
|
+
// rejection is re-thrown so extension.ts's existing then/onRejected
|
|
596
|
+
// handler runs the degrade-and-log path for genuinely broken servers.
|
|
597
|
+
let lastError;
|
|
598
|
+
for (let attempt = 0; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
599
|
+
try {
|
|
600
|
+
client.start();
|
|
601
|
+
await client.initialize();
|
|
602
|
+
lastError = undefined;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
lastError = err;
|
|
607
|
+
if (attempt === MAX_INIT_RETRIES)
|
|
608
|
+
break;
|
|
609
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
610
|
+
process.stderr.write(`[context-mode] WARNING: MCP bridge initialize failed ` +
|
|
611
|
+
`(attempt ${attempt + 1}/${MAX_INIT_RETRIES + 1}): ${msg}. Retrying…\n`);
|
|
612
|
+
// Reclaim the failed child's fds before respawning. shutdown() is
|
|
613
|
+
// idempotent and bounded by a 5s SIGKILL fallback (#472 round-3),
|
|
614
|
+
// so a child stuck in an uninterruptible syscall cannot block the
|
|
615
|
+
// retry loop indefinitely.
|
|
616
|
+
try {
|
|
617
|
+
client.shutdown();
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// best effort — we are already on the failure path
|
|
621
|
+
}
|
|
622
|
+
await new Promise((resolve) => setTimeout(resolve, INIT_RETRY_DELAY_MS));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (lastError !== undefined)
|
|
626
|
+
throw lastError;
|
|
571
627
|
const tools = await client.listTools();
|
|
572
628
|
const registered = [];
|
|
573
629
|
for (const tool of tools) {
|
|
@@ -13,6 +13,7 @@ import { readFileSync, mkdirSync, accessSync, existsSync, constants, } from "nod
|
|
|
13
13
|
import { resolve, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { CopilotBaseAdapter } from "../copilot-base.js";
|
|
16
|
+
import { resolveContextModeDataRoot } from "../base.js";
|
|
16
17
|
// ─────────────────────────────────────────────────────────
|
|
17
18
|
// Hook constants (re-exported from hooks.ts)
|
|
18
19
|
// ─────────────────────────────────────────────────────────
|
|
@@ -45,6 +46,16 @@ export class VSCodeCopilotAdapter extends CopilotBaseAdapter {
|
|
|
45
46
|
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
46
47
|
}
|
|
47
48
|
getSessionDir() {
|
|
49
|
+
// Issue #649: CONTEXT_MODE_DATA_DIR wins over both the .github project
|
|
50
|
+
// dir and the ~/.vscode fallback so dev-container/CI users can pin
|
|
51
|
+
// storage to a writable volume regardless of whether a .github tree
|
|
52
|
+
// happens to exist in cwd.
|
|
53
|
+
const override = resolveContextModeDataRoot();
|
|
54
|
+
if (override) {
|
|
55
|
+
const overrideDir = join(override, "context-mode", "sessions");
|
|
56
|
+
mkdirSync(overrideDir, { recursive: true });
|
|
57
|
+
return overrideDir;
|
|
58
|
+
}
|
|
48
59
|
// Prefer .github/context-mode/sessions/ if .github exists,
|
|
49
60
|
// otherwise fall back to ~/.vscode/context-mode/sessions/
|
|
50
61
|
const githubDir = resolve(".github", "context-mode", "sessions");
|