context-mode 1.0.105 → 1.0.106

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.105"
9
+ "version": "1.0.106"
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.105",
16
+ "version": "1.0.106",
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.105",
3
+ "version": "1.0.106",
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.105",
6
+ "version": "1.0.106",
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.105",
3
+ "version": "1.0.106",
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",
@@ -20,11 +20,27 @@
20
20
  */
21
21
  import { dirname, resolve } from "node:path";
22
22
  import { fileURLToPath, pathToFileURL } from "node:url";
23
+ import { existsSync, readFileSync } from "node:fs";
23
24
  import { SessionDB } from "./session/db.js";
24
25
  import { extractEvents } from "./session/extract.js";
25
26
  import { buildResumeSnapshot } from "./session/snapshot.js";
26
27
  import { OpenCodeAdapter } from "./adapters/opencode/index.js";
27
28
  import { PLATFORM_ENV_VARS } from "./adapters/detect.js";
29
+ // Read package.json version once at module load (not on every hook call).
30
+ // Used in the resume-injection visible signal so users can confirm in
31
+ // OPENCODE_DEBUG logs which plugin version actually injected.
32
+ const VERSION = (() => {
33
+ try {
34
+ const pkgRoot = dirname(fileURLToPath(import.meta.url));
35
+ for (const rel of ["../package.json", "./package.json"]) {
36
+ const p = resolve(pkgRoot, rel);
37
+ if (existsSync(p))
38
+ return JSON.parse(readFileSync(p, "utf8")).version ?? "unknown";
39
+ }
40
+ }
41
+ catch { /* fall through */ }
42
+ return "unknown";
43
+ })();
28
44
  // ── Helpers ───────────────────────────────────────────────
29
45
  /**
30
46
  * Detect whether the plugin is running under KiloCode or OpenCode.
@@ -166,12 +182,21 @@ async function createContextModePlugin(ctx) {
166
182
  return;
167
183
  if (resumeInjected.has(sessionId))
168
184
  return;
169
- resumeInjected.add(sessionId);
170
185
  try {
171
- const row = db.claimLatestUnconsumedResume();
186
+ // Pass current sessionId so SQL excludes self-injection (v1.0.106 — Mickey #376
187
+ // follow-up): if Session B compacts mid-flight and produces its own row,
188
+ // B's next system.transform must NOT claim that row back into B's prompt.
189
+ const row = db.claimLatestUnconsumedResume(sessionId);
172
190
  if (!row || !row.snapshot)
173
- return;
191
+ return; // no row → leave `resumeInjected` unset → retry on next turn
174
192
  if (Array.isArray(output?.system)) {
193
+ // Visible signal — without this, the injection is silent and users
194
+ // cannot tell the feature is active (Mickey: "I can't find use case
195
+ // for it"). The XML comment is harmless to the model and shows up in
196
+ // OPENCODE_DEBUG logs as proof the snapshot landed.
197
+ const eventCount = row.snapshot.match(/events="(\d+)"/)?.[1] ?? "?";
198
+ const marker = `<!-- context-mode v${VERSION}: resumed prior session ${row.sessionId.slice(0, 8)} ` +
199
+ `(${eventCount} events, ${row.snapshot.length} chars) -->\n`;
175
200
  // Insert at index 1 (after the header) — NOT unshift.
176
201
  // OpenCode's llm.ts:117-128 saves `header = system[0]` BEFORE this
177
202
  // hook runs and then folds the rest into a 2-part structure
@@ -182,7 +207,9 @@ async function createContextModePlugin(ctx) {
182
207
  // provider prompt cache is invalidated on every resume injection.
183
208
  // Inserting at index 1 keeps the header invariant and lets the
184
209
  // snapshot ride along inside the cached body block.
185
- output.system.splice(1, 0, row.snapshot);
210
+ output.system.splice(1, 0, marker + row.snapshot);
211
+ // Mark consumed only AFTER successful splice so failed paths can retry
212
+ resumeInjected.add(sessionId);
186
213
  }
187
214
  }
188
215
  catch {
package/build/server.js CHANGED
@@ -2805,9 +2805,17 @@ async function main() {
2805
2805
  }
2806
2806
  }
2807
2807
  catch { /* best effort — _detectedAdapter stays null, falls back to .claude */ }
2808
- // Non-blocking version check — result stored for trackResponse warnings
2808
+ // Non-blocking version check — result stored for trackResponse warnings.
2809
+ // First fetch at startup, then refresh every hour so long-running sessions
2810
+ // (some users keep the MCP server alive 24h+) catch new releases without a
2811
+ // restart. `.unref()` lets the process exit normally on SIGTERM regardless
2812
+ // of pending intervals.
2809
2813
  fetchLatestVersion().then(v => { if (v !== "unknown")
2810
2814
  _latestVersion = v; });
2815
+ setInterval(() => {
2816
+ fetchLatestVersion().then(v => { if (v !== "unknown")
2817
+ _latestVersion = v; });
2818
+ }, 60 * 60 * 1000).unref();
2811
2819
  console.error(`Context Mode MCP server v${VERSION} running on stdio`);
2812
2820
  console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
2813
2821
  if (!hasBunRuntime()) {
@@ -149,16 +149,25 @@ export declare class SessionDB extends SQLiteBase {
149
149
  */
150
150
  markResumeConsumed(sessionId: string): void;
151
151
  /**
152
- * Atomically claim the most recent unconsumed resume snapshot in this DB.
152
+ * Atomically claim the most recent unconsumed resume snapshot in this DB,
153
+ * EXCLUDING any row that belongs to `currentSessionId`.
153
154
  *
154
155
  * `SessionDB` is sharded per project (see `getSessionDBPath` — SHA-256 of
155
156
  * project dir), so "this DB" already implies "this project". The atomic
156
157
  * `UPDATE … RETURNING` ensures concurrent processes for the same project
157
158
  * cannot both inject the same snapshot (Mickey / PR #376 race).
158
159
  *
159
- * Returns null when no unconsumed snapshot exists.
160
+ * The `currentSessionId` parameter prevents self-injection: when a session
161
+ * compacts mid-flight and produces its own row, that session's next chat
162
+ * turn must NOT claim that row back (wasted tokens AND it would consume
163
+ * the snapshot meant for the next fresh session).
164
+ *
165
+ * Pass an empty string to allow self-claim (legacy behaviour, only useful
166
+ * in tests or one-off harnesses).
167
+ *
168
+ * Returns null when no unconsumed snapshot exists for any other session.
160
169
  */
161
- claimLatestUnconsumedResume(): {
170
+ claimLatestUnconsumedResume(currentSessionId: string): {
162
171
  sessionId: string;
163
172
  snapshot: string;
164
173
  } | null;
@@ -256,11 +256,17 @@ export class SessionDB extends SQLiteBase {
256
256
  // statement". Required for race-safe cross-session resume injection
257
257
  // (Mickey / PR #376) — two parallel chat-turn hooks must not both read
258
258
  // the same row before either one writes consumed=1.
259
+ //
260
+ // The `session_id != ?` clause prevents self-injection (v1.0.106): when
261
+ // Session B compacts mid-flight and produces its own row, B's next chat
262
+ // turn must NOT claim that row back into its own prompt — that's wasted
263
+ // tokens and steals the snapshot meant for the next fresh session.
259
264
  p(S.claimLatestUnconsumedResume, `UPDATE session_resume
260
265
  SET consumed = 1
261
266
  WHERE id = (
262
267
  SELECT id FROM session_resume
263
268
  WHERE consumed = 0
269
+ AND session_id != ?
264
270
  ORDER BY created_at DESC, id DESC
265
271
  LIMIT 1
266
272
  )
@@ -493,17 +499,26 @@ export class SessionDB extends SQLiteBase {
493
499
  this.stmt(S.markResumeConsumed).run(sessionId);
494
500
  }
495
501
  /**
496
- * Atomically claim the most recent unconsumed resume snapshot in this DB.
502
+ * Atomically claim the most recent unconsumed resume snapshot in this DB,
503
+ * EXCLUDING any row that belongs to `currentSessionId`.
497
504
  *
498
505
  * `SessionDB` is sharded per project (see `getSessionDBPath` — SHA-256 of
499
506
  * project dir), so "this DB" already implies "this project". The atomic
500
507
  * `UPDATE … RETURNING` ensures concurrent processes for the same project
501
508
  * cannot both inject the same snapshot (Mickey / PR #376 race).
502
509
  *
503
- * Returns null when no unconsumed snapshot exists.
510
+ * The `currentSessionId` parameter prevents self-injection: when a session
511
+ * compacts mid-flight and produces its own row, that session's next chat
512
+ * turn must NOT claim that row back (wasted tokens AND it would consume
513
+ * the snapshot meant for the next fresh session).
514
+ *
515
+ * Pass an empty string to allow self-claim (legacy behaviour, only useful
516
+ * in tests or one-off harnesses).
517
+ *
518
+ * Returns null when no unconsumed snapshot exists for any other session.
504
519
  */
505
- claimLatestUnconsumedResume() {
506
- const row = this.stmt(S.claimLatestUnconsumedResume).get();
520
+ claimLatestUnconsumedResume(currentSessionId) {
521
+ const row = this.stmt(S.claimLatestUnconsumedResume).get(currentSessionId);
507
522
  if (!row)
508
523
  return null;
509
524
  return { sessionId: row.session_id, snapshot: row.snapshot };