context-mode 1.0.128 → 1.0.130

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.128"
9
+ "version": "1.0.130"
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.128",
16
+ "version": "1.0.130",
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.128",
3
+ "version": "1.0.130",
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.128",
6
+ "version": "1.0.130",
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.128",
3
+ "version": "1.0.130",
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",
@@ -34,12 +34,20 @@ export declare function __seedClaudeCodePluginCacheMissForTests(): void;
34
34
  * `resolveProjectDir({ strictPlatform })` to form the candidate list,
35
35
  * and by Pi's bridge to scrub foreign workspace vars on child spawn.
36
36
  * - `identification`: env var only signals which host is running; carries
37
- * no project path. NEVER scrubbed (some are load-bearing, e.g.
38
- * CLAUDE_PLUGIN_ROOT for hook integrations).
37
+ * no project path. PRESERVED in normal operation (some are load-bearing
38
+ * for hook integrations on the host that owns them, e.g. CLAUDE_PLUGIN_ROOT
39
+ * for Claude Code's hook context).
39
40
  *
40
41
  * Issue #545 — algorithmic env-leak fix. The split allows resolveProjectDir
41
42
  * to derive ALLOW (own workspace vars) and BAN (other platforms' workspace
42
43
  * vars) sets from a single registry, satisfying MUST-3 (15 adapters equal).
44
+ *
45
+ * Issue #561 — FOREIGN identification vars MUST be scrubbed when spawning a
46
+ * child under a different host (e.g. Pi spawning context-mode child must
47
+ * scrub Claude Code identification vars CLAUDE_CODE_ENTRYPOINT /
48
+ * CLAUDE_PLUGIN_ROOT to prevent detectPlatform() in the child from
49
+ * misidentifying the host as claude-code and writing Pi's data into
50
+ * ~/.claude/context-mode/). See `foreignIdentificationEnv()` below.
43
51
  */
44
52
  export type EnvVarRole = "workspace" | "identification";
45
53
  export interface PlatformEnvEntry {
@@ -78,6 +86,26 @@ export declare function workspaceEnvVarsFor(platform: PlatformId): string[];
78
86
  * workspace vars from spawned MCP child) and by the matrix regression test.
79
87
  */
80
88
  export declare function foreignWorkspaceEnv(platform: PlatformId): Set<string>;
89
+ /**
90
+ * Issue #561 — return the union of identification env vars from ALL
91
+ * platforms EXCEPT the given one. Sibling of `foreignWorkspaceEnv`,
92
+ * filtered on `role === "identification"` instead of "workspace".
93
+ *
94
+ * Consumed by Pi's bridge env scrub: when Pi spawns the context-mode
95
+ * MCP child, the child inherits the host shell env including any
96
+ * identification vars set by a co-resident Claude Code session
97
+ * (CLAUDE_CODE_ENTRYPOINT / CLAUDE_PLUGIN_ROOT). Without scrubbing,
98
+ * `detectPlatform()` in the child falls through env priority order and
99
+ * resolves to claude-code first — Pi's session data then writes into
100
+ * `~/.claude/context-mode/` instead of Pi's own dir. Scrubbing FOREIGN
101
+ * identification vars (everyone else's) preserves Pi's OWN identification
102
+ * vars (PI_CONFIG_DIR / PI_SESSION_FILE / PI_COMPILED) so the child still
103
+ * detects pi correctly.
104
+ *
105
+ * Algorithmic, registry-driven — adding adapter #16 grows the scrub
106
+ * automatically (no edit to mcp-bridge.ts).
107
+ */
108
+ export declare function foreignIdentificationEnv(platform: PlatformId): Set<string>;
81
109
  /**
82
110
  * Sync map from platform identifier → home-relative path segments where that
83
111
  * platform stores its config. Mirrors the `super([...])` argument passed by
@@ -222,6 +222,37 @@ export function foreignWorkspaceEnv(platform) {
222
222
  }
223
223
  return ban;
224
224
  }
225
+ /**
226
+ * Issue #561 — return the union of identification env vars from ALL
227
+ * platforms EXCEPT the given one. Sibling of `foreignWorkspaceEnv`,
228
+ * filtered on `role === "identification"` instead of "workspace".
229
+ *
230
+ * Consumed by Pi's bridge env scrub: when Pi spawns the context-mode
231
+ * MCP child, the child inherits the host shell env including any
232
+ * identification vars set by a co-resident Claude Code session
233
+ * (CLAUDE_CODE_ENTRYPOINT / CLAUDE_PLUGIN_ROOT). Without scrubbing,
234
+ * `detectPlatform()` in the child falls through env priority order and
235
+ * resolves to claude-code first — Pi's session data then writes into
236
+ * `~/.claude/context-mode/` instead of Pi's own dir. Scrubbing FOREIGN
237
+ * identification vars (everyone else's) preserves Pi's OWN identification
238
+ * vars (PI_CONFIG_DIR / PI_SESSION_FILE / PI_COMPILED) so the child still
239
+ * detects pi correctly.
240
+ *
241
+ * Algorithmic, registry-driven — adding adapter #16 grows the scrub
242
+ * automatically (no edit to mcp-bridge.ts).
243
+ */
244
+ export function foreignIdentificationEnv(platform) {
245
+ const ban = new Set();
246
+ for (const [p, vars] of PLATFORM_ENV_VARS) {
247
+ if (p === platform)
248
+ continue;
249
+ for (const v of vars) {
250
+ if (v.role === "identification")
251
+ ban.add(v.name);
252
+ }
253
+ }
254
+ return ban;
255
+ }
225
256
  /**
226
257
  * Sync map from platform identifier → home-relative path segments where that
227
258
  * platform stores its config. Mirrors the `super([...])` argument passed by
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { spawn, execSync } from "node:child_process";
24
24
  import { detectRuntimes } from "../../runtime.js";
25
- import { foreignWorkspaceEnv } from "../detect.js";
25
+ import { foreignWorkspaceEnv, foreignIdentificationEnv } from "../detect.js";
26
26
  // ── Fork-bomb prevention (#516) ──────────────────────────────────────
27
27
  //
28
28
  // Original bug: `spawn(process.execPath, [serverScript])` recursively
@@ -154,12 +154,27 @@ export class MCPStdioClient {
154
154
  // and Pi's sessions write into the wrong project. The ban list is
155
155
  // derived ALGORITHMICALLY from PLATFORM_ENV_VARS (every other adapter's
156
156
  // workspace-role vars), so adding adapter #16 grows the scrub
157
- // automatically — no edit to this file. Identification vars
158
- // (CLAUDE_PLUGIN_ROOT etc.) and the universal escape hatch
159
- // (CONTEXT_MODE_PROJECT_DIR) are NEVER scrubbed.
157
+ // automatically — no edit to this file. Pi's own workspace vars and
158
+ // the universal escape hatch (CONTEXT_MODE_PROJECT_DIR) are NEVER
159
+ // scrubbed.
160
160
  for (const banned of foreignWorkspaceEnv("pi")) {
161
161
  delete childEnv[banned];
162
162
  }
163
+ // Issue #561 — scrub foreign IDENTIFICATION env vars before spawn.
164
+ //
165
+ // Foreign identification vars hijack detectPlatform() — must scrub
166
+ // when spawning child under a different host (#561). When Pi runs
167
+ // co-resident with Claude Code, the inherited shell env carries
168
+ // CLAUDE_CODE_ENTRYPOINT and CLAUDE_PLUGIN_ROOT; the spawned MCP
169
+ // child's detectPlatform() then walks PLATFORM_ENV_VARS in priority
170
+ // order (claude-code first), returns claude-code, and Pi's session
171
+ // data lands in ~/.claude/context-mode/ instead of Pi's own dir.
172
+ // Pi's OWN identification vars (PI_CONFIG_DIR / PI_SESSION_FILE /
173
+ // PI_COMPILED) are excluded from the ban set so the child still
174
+ // detects pi correctly.
175
+ for (const banned of foreignIdentificationEnv("pi")) {
176
+ delete childEnv[banned];
177
+ }
163
178
  this._spawnEnv = childEnv;
164
179
  this.child = spawn(runtime, [this.serverScript], {
165
180
  // Pipe stderr (#472 round-3): swallowing it via "ignore" hides
@@ -139,6 +139,26 @@ export declare function isSQLiteCorruptionError(msg: string): boolean;
139
139
  export declare function renameCorruptDB(dbPath: string): void;
140
140
  export declare abstract class SQLiteBase {
141
141
  #private;
142
+ /**
143
+ * Open (or create) a SQLite DB at `dbPath`.
144
+ *
145
+ * v1.0.130 — multi-writer is the contract. ALL SQLiteBase consumers
146
+ * (SessionDB, ContentStore) may open the same on-disk dbPath from
147
+ * multiple processes simultaneously — that is the legitimate multi-
148
+ * window UX shape and the WAL handles it natively. SQLITE_BUSY on
149
+ * write contention is absorbed by `withRetry()` below (busy_timeout
150
+ * = 30000ms inside `new Database(...)`).
151
+ *
152
+ * v1.0.128 introduced a single-writer guard here as a defense against
153
+ * #560. That defense was an over-correction — the actual root causes
154
+ * of #560 were #559 (zombie MCP child accumulation) and #561 (Pi
155
+ * misdetection writing to the wrong DB path), both fixed in v1.0.128
156
+ * + v1.0.129. The single-writer guard broke legitimate multi-window
157
+ * users; v1.0.130 rolls it out. See
158
+ * docs/adr/0001-sessiondb-multi-writer.md and the v1.0.130 INVARIANT
159
+ * block in tests/util/db-base-platform-gate.test.ts for the
160
+ * regression-proof anchor (source-pin + behavioural).
161
+ */
142
162
  constructor(dbPath: string);
143
163
  /** Called once after WAL pragmas are applied. Subclasses run CREATE TABLE/VIRTUAL TABLE here. */
144
164
  protected abstract initSchema(): void;
package/build/db-base.js CHANGED
@@ -9,12 +9,6 @@ import { createRequire } from "node:module";
9
9
  import { existsSync, unlinkSync, renameSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
- // v1.0.128 — Issue #560 single-writer enforcement.
13
- // Lockfile is the PRIMARY defense (clean UX with conflicting PID),
14
- // `locking_mode = EXCLUSIVE` (applied in applyWALPragmas below) is the
15
- // SECONDARY defense for the narrow race window between lockfile claim
16
- // and the actual `new Database(...)` open. Both skip-gate on tmpdir paths.
17
- import { acquireDbLock, releaseDbLock } from "./util/db-lock.js";
18
12
  // ─────────────────────────────────────────────────────────
19
13
  // bun:sqlite adapter (#45)
20
14
  // ─────────────────────────────────────────────────────────
@@ -316,18 +310,13 @@ export function applyWALPragmas(db) {
316
310
  db.pragma("mmap_size = 268435456");
317
311
  }
318
312
  catch { /* unsupported runtime */ }
319
- // v1.0.128 Issue #560 SECONDARY defense for single-writer enforcement.
320
- // The .lock file (acquireDbLock in SQLiteBase ctor) is PRIMARY it
321
- // surfaces the conflicting PID with a clear UX message. EXCLUSIVE locking
322
- // closes the narrow race window between lockfile claim + the actual
323
- // `new Database(...)` open: a parallel process passing the lockfile
324
- // check would still get SQLITE_BUSY from this pragma. Wrapped in
325
- // try/catch identical to mmap_size — backends that don't expose
326
- // locking_mode (or pragma at all) still get the lockfile floor.
327
- try {
328
- db.pragma("locking_mode = EXCLUSIVE");
329
- }
330
- catch { /* unsupported runtime */ }
313
+ // NOTE: `locking_mode = EXCLUSIVE` is intentionally NOT applied here.
314
+ // ALL DBs built on this helper ContentStore (FTS5 shared knowledge
315
+ // base) AND SessionDB (per-project events) are multi-writer-safe by
316
+ // contract. WAL + busy_timeout + the withRetry() wrapper below handle
317
+ // SQLITE_BUSY natively. EXCLUSIVE locking is opt-out, never opt-in
318
+ // from a base class shared by multi-writer consumers.
319
+ // See docs/adr/0001-sessiondb-multi-writer.md for the v1.0.130 ADR.
331
320
  }
332
321
  // ─────────────────────────────────────────────────────────
333
322
  // DB file helpers
@@ -468,26 +457,19 @@ export function renameCorruptDB(dbPath) {
468
457
  * re-imports within the same fork process (ESM isolate mode clears
469
458
  * module-level state but globalThis persists).
470
459
  */
471
- // v1.0.128 — symbol name versioned because the value type changed from
472
- // Set<DatabaseInstance> to Map<DatabaseInstance, string> (issue #560).
473
- // A persistent global slot from a pre-v128 module would deserialize as
474
- // the wrong shape and crash the exit hook iteration.
475
- const _kLiveDBs = Symbol.for("__context_mode_live_dbs_v2__");
476
- // v1.0.128 issue #560: pair each DatabaseInstance with the dbPath that
477
- // owns its lockfile. The exit hook needs both — closeDB(db) handles the
478
- // WAL checkpoint, releaseDbLock(dbPath) drops the .lock file. We use a
479
- // Map keyed by DatabaseInstance to keep API call sites unchanged.
460
+ // v1.0.130 — symbol name bumped because the value type reverted from
461
+ // Map<DatabaseInstance, string> (v1.0.128 lockfile pairing) back to
462
+ // Set<DatabaseInstance>. A persistent global slot from a v1.0.128 or
463
+ // v1.0.129 module would deserialize as the wrong shape and crash the
464
+ // exit hook iteration.
465
+ const _kLiveDBs = Symbol.for("__context_mode_live_dbs_v3__");
480
466
  const _liveDBs = (() => {
481
467
  const g = globalThis;
482
468
  if (!g[_kLiveDBs]) {
483
- g[_kLiveDBs] = new Map();
469
+ g[_kLiveDBs] = new Set();
484
470
  process.on("exit", () => {
485
- for (const [db, dbPath] of g[_kLiveDBs]) {
471
+ for (const db of g[_kLiveDBs]) {
486
472
  closeDB(db);
487
- // Release lock AFTER close so the WAL checkpoint inside closeDB
488
- // runs while we still own the writer slot (no second-opener can
489
- // race in mid-checkpoint).
490
- releaseDbLock({ dbPath });
491
473
  }
492
474
  g[_kLiveDBs].clear();
493
475
  });
@@ -497,16 +479,29 @@ const _liveDBs = (() => {
497
479
  export class SQLiteBase {
498
480
  #dbPath;
499
481
  #db;
482
+ /**
483
+ * Open (or create) a SQLite DB at `dbPath`.
484
+ *
485
+ * v1.0.130 — multi-writer is the contract. ALL SQLiteBase consumers
486
+ * (SessionDB, ContentStore) may open the same on-disk dbPath from
487
+ * multiple processes simultaneously — that is the legitimate multi-
488
+ * window UX shape and the WAL handles it natively. SQLITE_BUSY on
489
+ * write contention is absorbed by `withRetry()` below (busy_timeout
490
+ * = 30000ms inside `new Database(...)`).
491
+ *
492
+ * v1.0.128 introduced a single-writer guard here as a defense against
493
+ * #560. That defense was an over-correction — the actual root causes
494
+ * of #560 were #559 (zombie MCP child accumulation) and #561 (Pi
495
+ * misdetection writing to the wrong DB path), both fixed in v1.0.128
496
+ * + v1.0.129. The single-writer guard broke legitimate multi-window
497
+ * users; v1.0.130 rolls it out. See
498
+ * docs/adr/0001-sessiondb-multi-writer.md and the v1.0.130 INVARIANT
499
+ * block in tests/util/db-base-platform-gate.test.ts for the
500
+ * regression-proof anchor (source-pin + behavioural).
501
+ */
500
502
  constructor(dbPath) {
501
503
  const Database = loadDatabase();
502
504
  this.#dbPath = dbPath;
503
- // v1.0.128 — Issue #560 PRIMARY single-writer guard. Must claim
504
- // BEFORE `new Database(...)` so a contending opener gets the clean
505
- // DatabaseLockedError UX (PID + verbatim message) instead of the
506
- // SQLITE_BUSY surfaced by EXCLUSIVE locking. Skip-gate via
507
- // tmpdir-prefix check inside the helper — defaultDBPath() output
508
- // (per-process tmp DBs) does not contend, so it never claims a lock.
509
- acquireDbLock({ dbPath });
510
505
  cleanOrphanedWALFiles(dbPath);
511
506
  let db;
512
507
  try {
@@ -523,19 +518,15 @@ export class SQLiteBase {
523
518
  applyWALPragmas(db);
524
519
  }
525
520
  catch (retryErr) {
526
- // Free the lock before bubbling — caller can never reach
527
- // close()/cleanup() if the ctor throws.
528
- releaseDbLock({ dbPath });
529
521
  throw new Error(`Failed to create fresh DB after renaming corrupt file: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
530
522
  }
531
523
  }
532
524
  else {
533
- releaseDbLock({ dbPath });
534
525
  throw err;
535
526
  }
536
527
  }
537
528
  this.#db = db;
538
- _liveDBs.set(this.#db, dbPath);
529
+ _liveDBs.add(this.#db);
539
530
  this.initSchema();
540
531
  this.prepareStatements();
541
532
  }
@@ -551,10 +542,6 @@ export class SQLiteBase {
551
542
  close() {
552
543
  _liveDBs.delete(this.#db);
553
544
  closeDB(this.#db);
554
- // v1.0.128 — Issue #560: drop the .lock file AFTER closeDB so the
555
- // WAL checkpoint inside closeDB completes while we still own the
556
- // writer slot. releaseDbLock is no-op for tmpdir paths (skip-gate).
557
- releaseDbLock({ dbPath: this.#dbPath });
558
545
  }
559
546
  withRetry(fn) {
560
547
  return withRetry(fn);
@@ -567,9 +554,5 @@ export class SQLiteBase {
567
554
  _liveDBs.delete(this.#db);
568
555
  closeDB(this.#db);
569
556
  deleteDBFiles(this.#dbPath);
570
- // v1.0.128 — Issue #560: also drop the lockfile during cleanup. Per-
571
- // process tmp DBs (defaultDBPath()) skip-gate inside the helper, so
572
- // this is only a side-effect for shared on-disk content stores.
573
- releaseDbLock({ dbPath: this.#dbPath });
574
557
  }
575
558
  }