context-mode 1.0.168 → 1.0.169

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.168"
9
+ "version": "1.0.169"
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.168",
16
+ "version": "1.0.169",
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.168",
3
+ "version": "1.0.169",
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.168",
3
+ "version": "1.0.169",
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.168",
6
+ "version": "1.0.169",
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.168",
3
+ "version": "1.0.169",
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/build/server.js CHANGED
@@ -23,6 +23,7 @@ import { describeStorageDirectorySource, ensureWritableStorageDir, formatStorage
23
23
  import { purgeSession } from "./session/purge.js";
24
24
  import { emitCacheHitEvent, emitIndexWriteEvent, emitSandboxExecuteEvent, } from "./session/event-emit.js";
25
25
  import { persistToolCallCounter, restoreSessionStats } from "./session/persist-tool-calls.js";
26
+ import { appendRetrievalBytes } from "./session/retrieval-marker.js";
26
27
  import { searchAllSources } from "./search/unified.js";
27
28
  import { buildCtxSearchInputSchema, CTX_SEARCH_SHARED_MODE, resolveProjectScope, } from "./search/ctx-search-schema.js";
28
29
  import { FloodGuard } from "./search/flood-guard.js";
@@ -34,7 +35,7 @@ import { stripJsonComments } from "./util/jsonc.js";
34
35
  import { resolveClaudeConfigDir } from "./util/claude-config.js";
35
36
  import { resolveProjectDir } from "./util/project-dir.js";
36
37
  import { loadDatabase } from "./db-base.js";
37
- import { AnalyticsEngine, formatReport, getConversationStats, getContentBytesAllSessions, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, pricePerToken } from "./session/analytics.js";
38
+ import { AnalyticsEngine, formatReport, getConversationStats, getContentBytesAllSessions, getConversationWindowStats, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, pricePerToken } from "./session/analytics.js";
38
39
  const __pkg_dir = dirname(fileURLToPath(import.meta.url));
39
40
  const VERSION = (() => {
40
41
  for (const rel of ["../package.json", "./package.json"]) {
@@ -857,6 +858,16 @@ function trackResponse(toolName, response) {
857
858
  bytesReturned: bytes,
858
859
  }));
859
860
  }
861
+ // Retrieval ("With context-mode") bridge — ctx_search / ctx_fetch_and_index
862
+ // response bytes are the kept-out content the model paid to access. The
863
+ // PostToolUse hook never fires for the plugin's OWN MCP tools, so the
864
+ // hook-side extractMcpToolCall can never see these calls (bytes_retrieved
865
+ // was 0/124454 in prod). Drop the count into a marker keyed by the session
866
+ // DB; the next ordinary-tool PostToolUse consumes it and emits a forwardable
867
+ // bytes_retrieved event. Off the hot path; never throws.
868
+ if (toolName === "ctx_search" || toolName === "ctx_fetch_and_index") {
869
+ setImmediate(() => appendRetrievalBytes(getSessionDbPath(), bytes));
870
+ }
860
871
  return response;
861
872
  }
862
873
  function trackIndexed(bytes, source = "unknown") {
@@ -3654,12 +3665,22 @@ server.registerTool("ctx_stats", {
3654
3665
  }
3655
3666
  catch { /* skip unreadable DB */ }
3656
3667
  }
3657
- convReal = projectDirForSid
3658
- ? getRealBytesStats({ projectDir: projectDirForSid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath })
3659
- : getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
3668
+ // Section 1 "Where you are now" = the LIVE conversation window.
3669
+ // Sub-agents + ctx_execute sub-process sessions write to this
3670
+ // SAME worktree DB (same worktreeHash = sha256(cwd)) under their
3671
+ // own session_ids; their retrieval hit their own disposable
3672
+ // windows, not yours. getConversationWindowStats credits the
3673
+ // whole worktree's kept-out bytes while counting only THIS
3674
+ // session's retrieval as "With context-mode", and the
3675
+ // worktreeHash scope keeps the user's OTHER parallel worktrees
3676
+ // out. projectDirForSid is intentionally dropped — it
3677
+ // under-counted (missed empty-project_dir sub-process sessions)
3678
+ // and could not separate sub-agent retrieval from the window's.
3679
+ void projectDirForSid;
3680
+ convReal = getConversationWindowStats({ sessionId: sid, worktreeHash: dbHash, sessionsDir: getSessionDir(), contentDbPath });
3660
3681
  }
3661
3682
  catch {
3662
- convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
3683
+ convReal = getConversationWindowStats({ sessionId: sid, worktreeHash: dbHash, sessionsDir: getSessionDir(), contentDbPath });
3663
3684
  }
3664
3685
  const lifeRealBase = getRealBytesStats({ sessionsDir: getSessionDir() });
3665
3686
  // v1.0.134 SLICE C: lifetime tier sums ALL chunks (no
@@ -468,6 +468,34 @@ export declare function getRealBytesStats(opts: {
468
468
  contentDbPath?: string;
469
469
  loadDatabase?: () => unknown;
470
470
  }): RealBytesStats;
471
+ /**
472
+ * v1.0.169 — Section 1 "Where you are now" = the LIVE conversation window.
473
+ *
474
+ * A single live conversation fans out into sub-agents and ctx_execute
475
+ * sub-process sessions. Each runs in its OWN, disposable context window (its
476
+ * own session_id) — but all under the SAME worktree DB, because the worktree
477
+ * hash is sha256(cwd) and they share the cwd. Their retrieval (ctx_search /
478
+ * ctx_fetch_and_index returns) entered THOSE windows and was thrown away when
479
+ * each returned its short summary; it never touched the window the user is
480
+ * reading now. So the live-window savings bar must split the worktree by
481
+ * which retrieval actually landed in the user's window:
482
+ *
483
+ * bytesReturned ("With context-mode") = THIS session's retrieval only —
484
+ * what genuinely entered the live window.
485
+ * bytesAvoided ("kept out") = everything the whole worktree moved
486
+ * (avoided + every session's retrieval) MINUS what landed in your window.
487
+ *
488
+ * Scoping by `worktreeHash` (not project-root + time) means the user's OTHER
489
+ * parallel worktrees never bleed in — a different worktree is a different
490
+ * cwd-hash, hence a different DB file the prefix filter excludes — while the
491
+ * sub-agent fan-out this conversation actually spawned is fully credited.
492
+ */
493
+ export declare function getConversationWindowStats(opts: {
494
+ sessionId: string;
495
+ worktreeHash: string;
496
+ sessionsDir?: string;
497
+ contentDbPath?: string;
498
+ }): RealBytesStats;
471
499
  /**
472
500
  * Real-usage filter thresholds. Decided in the B3a /diagnose conversation
473
501
  * to suppress fixture-noise dirs (test runs that touched ~/.X but never
@@ -1000,6 +1000,57 @@ export function getRealBytesStats(opts) {
1000
1000
  const totalSavedTokens = Math.floor((eventDataBytes + bytesAvoided + snapshotBytes) / 4);
1001
1001
  return { eventDataBytes, bytesAvoided, bytesReturned, snapshotBytes, contentBytes, totalSavedTokens };
1002
1002
  }
1003
+ /**
1004
+ * v1.0.169 — Section 1 "Where you are now" = the LIVE conversation window.
1005
+ *
1006
+ * A single live conversation fans out into sub-agents and ctx_execute
1007
+ * sub-process sessions. Each runs in its OWN, disposable context window (its
1008
+ * own session_id) — but all under the SAME worktree DB, because the worktree
1009
+ * hash is sha256(cwd) and they share the cwd. Their retrieval (ctx_search /
1010
+ * ctx_fetch_and_index returns) entered THOSE windows and was thrown away when
1011
+ * each returned its short summary; it never touched the window the user is
1012
+ * reading now. So the live-window savings bar must split the worktree by
1013
+ * which retrieval actually landed in the user's window:
1014
+ *
1015
+ * bytesReturned ("With context-mode") = THIS session's retrieval only —
1016
+ * what genuinely entered the live window.
1017
+ * bytesAvoided ("kept out") = everything the whole worktree moved
1018
+ * (avoided + every session's retrieval) MINUS what landed in your window.
1019
+ *
1020
+ * Scoping by `worktreeHash` (not project-root + time) means the user's OTHER
1021
+ * parallel worktrees never bleed in — a different worktree is a different
1022
+ * cwd-hash, hence a different DB file the prefix filter excludes — while the
1023
+ * sub-agent fan-out this conversation actually spawned is fully credited.
1024
+ */
1025
+ export function getConversationWindowStats(opts) {
1026
+ // Whole current worktree: every session that shares this cwd-hash DB.
1027
+ const pool = getRealBytesStats({
1028
+ worktreeHash: opts.worktreeHash,
1029
+ sessionsDir: opts.sessionsDir,
1030
+ });
1031
+ // Just the live window: this session_id (folds its own ctx_search/ctx_fetch
1032
+ // retrieval + content chunks).
1033
+ const mine = getRealBytesStats({
1034
+ sessionId: opts.sessionId,
1035
+ worktreeHash: opts.worktreeHash,
1036
+ sessionsDir: opts.sessionsDir,
1037
+ contentDbPath: opts.contentDbPath,
1038
+ });
1039
+ const windowReturned = mine.bytesReturned;
1040
+ const movedTotal = pool.bytesAvoided + pool.bytesReturned;
1041
+ // What context-mode kept OUT of the live window = everything moved across the
1042
+ // worktree minus the slice that actually entered this window. Clamp at 0 so a
1043
+ // stale/edge DB can never produce a negative bar.
1044
+ const keptOut = Math.max(0, movedTotal - windowReturned);
1045
+ return {
1046
+ eventDataBytes: pool.eventDataBytes,
1047
+ bytesAvoided: keptOut,
1048
+ bytesReturned: windowReturned,
1049
+ snapshotBytes: pool.snapshotBytes,
1050
+ contentBytes: mine.contentBytes,
1051
+ totalSavedTokens: Math.floor((pool.eventDataBytes + keptOut + pool.snapshotBytes) / 4),
1052
+ };
1053
+ }
1003
1054
  const DEFAULT_REAL_USAGE_FILTER = {
1004
1055
  minEvents: 100,
1005
1056
  minProjects: 5,
@@ -1650,7 +1701,7 @@ function renderNarrative5Section(args) {
1650
1701
  const convMult = Math.max(1, Math.round(convTokensWithout / convTokensWith));
1651
1702
  out.push(` Without context-mode ${kb(convBytesWithout).padStart(8)} ${withoutBar} ${fmtNum(convTokensWithout).padStart(7)} tokens`);
1652
1703
  out.push(` With context-mode ${kb(convBytesWith).padStart(8)} ${withBar} ${fmtNum(convTokensWith).padStart(7)} tokens`);
1653
- out.push(` ${convPct.toFixed(0)}% kept out of context · your AI ran ${convMult}× longer before /compact fired`);
1704
+ out.push(` ${convPct.toFixed(1)}% kept out of context · your AI ran ${convMult}× longer before /compact fired`);
1654
1705
  out.push("");
1655
1706
  }
1656
1707
  // Timeline — drop-in if conversation has byDay.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Server→hook bridge for the retrieval ("With context-mode") byte count.
3
+ *
4
+ * WHY THIS EXISTS — context-mode's OWN MCP retrieval tools (ctx_search /
5
+ * ctx_fetch_and_index) never fire a PostToolUse hook for the plugin's own
6
+ * server, so the hook-side `extractMcpToolCall` path can never observe them
7
+ * (verified empirically: 0 `mcp_tool_call` events locally, bytes_retrieved
8
+ * 0/124454 in production D1). The MCP server, however, measures each
9
+ * retrieval response's byte length directly.
10
+ *
11
+ * The server appends that count to a tmp marker keyed by the session DB
12
+ * *basename* — the one identifier the server process and the hook process
13
+ * both resolve reliably (CLAUDE_SESSION_ID is not guaranteed in the server
14
+ * env; the per-project session DB path is). The next PostToolUse fire — which
15
+ * DOES run for ordinary tools (Bash/Read/Edit) — consumes the marker and
16
+ * emits a forwardable event carrying `bytes_retrieved`. Mirrors the existing
17
+ * redirect / latency / rejected marker handshake in posttooluse.mjs.
18
+ */
19
+ /**
20
+ * Tmp marker path for a session DB. Keyed by basename so the server (which
21
+ * holds the DB path via getSessionDbPath) and the hook (getSessionDBPath)
22
+ * derive the SAME file. Session DB filenames embed the worktree hash
23
+ * (`<hash>__<suffix>.db`), so basename collisions across projects are
24
+ * negligible.
25
+ */
26
+ export declare function retrievalMarkerPath(sessionDbPath: string, tmpDir?: string): string;
27
+ /**
28
+ * Record one retrieval's response byte count. Positive-only (a 0-byte or
29
+ * failed retrieval is not a context cost). Append-only so several retrievals
30
+ * between two hook fires accumulate. Best-effort — never throws into the
31
+ * MCP response path.
32
+ */
33
+ export declare function appendRetrievalBytes(sessionDbPath: string, bytes: number, tmpDir?: string): void;
34
+ /**
35
+ * Sum every recorded retrieval and delete the marker (consume-once) so the
36
+ * next PostToolUse fire cannot re-forward the same bytes. Returns 0 when no
37
+ * marker exists (phantom-event guard).
38
+ */
39
+ export declare function consumeRetrievalBytes(sessionDbPath: string, tmpDir?: string): number;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server→hook bridge for the retrieval ("With context-mode") byte count.
3
+ *
4
+ * WHY THIS EXISTS — context-mode's OWN MCP retrieval tools (ctx_search /
5
+ * ctx_fetch_and_index) never fire a PostToolUse hook for the plugin's own
6
+ * server, so the hook-side `extractMcpToolCall` path can never observe them
7
+ * (verified empirically: 0 `mcp_tool_call` events locally, bytes_retrieved
8
+ * 0/124454 in production D1). The MCP server, however, measures each
9
+ * retrieval response's byte length directly.
10
+ *
11
+ * The server appends that count to a tmp marker keyed by the session DB
12
+ * *basename* — the one identifier the server process and the hook process
13
+ * both resolve reliably (CLAUDE_SESSION_ID is not guaranteed in the server
14
+ * env; the per-project session DB path is). The next PostToolUse fire — which
15
+ * DOES run for ordinary tools (Bash/Read/Edit) — consumes the marker and
16
+ * emits a forwardable event carrying `bytes_retrieved`. Mirrors the existing
17
+ * redirect / latency / rejected marker handshake in posttooluse.mjs.
18
+ */
19
+ import { appendFileSync, readFileSync, rmSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { basename, join } from "node:path";
22
+ /**
23
+ * Tmp marker path for a session DB. Keyed by basename so the server (which
24
+ * holds the DB path via getSessionDbPath) and the hook (getSessionDBPath)
25
+ * derive the SAME file. Session DB filenames embed the worktree hash
26
+ * (`<hash>__<suffix>.db`), so basename collisions across projects are
27
+ * negligible.
28
+ */
29
+ export function retrievalMarkerPath(sessionDbPath, tmpDir = tmpdir()) {
30
+ return join(tmpDir, `context-mode-retrieval-${basename(sessionDbPath)}.txt`);
31
+ }
32
+ /**
33
+ * Record one retrieval's response byte count. Positive-only (a 0-byte or
34
+ * failed retrieval is not a context cost). Append-only so several retrievals
35
+ * between two hook fires accumulate. Best-effort — never throws into the
36
+ * MCP response path.
37
+ */
38
+ export function appendRetrievalBytes(sessionDbPath, bytes, tmpDir) {
39
+ if (!Number.isFinite(bytes) || bytes <= 0)
40
+ return;
41
+ try {
42
+ appendFileSync(retrievalMarkerPath(sessionDbPath, tmpDir), `${Math.floor(bytes)}\n`);
43
+ }
44
+ catch { /* best-effort — never block the MCP response */ }
45
+ }
46
+ /**
47
+ * Sum every recorded retrieval and delete the marker (consume-once) so the
48
+ * next PostToolUse fire cannot re-forward the same bytes. Returns 0 when no
49
+ * marker exists (phantom-event guard).
50
+ */
51
+ export function consumeRetrievalBytes(sessionDbPath, tmpDir) {
52
+ const path = retrievalMarkerPath(sessionDbPath, tmpDir);
53
+ let total = 0;
54
+ try {
55
+ const raw = readFileSync(path, "utf8");
56
+ for (const line of raw.split("\n")) {
57
+ const n = Number.parseInt(line, 10);
58
+ if (Number.isFinite(n) && n > 0)
59
+ total += n;
60
+ }
61
+ rmSync(path, { force: true });
62
+ }
63
+ catch { /* no marker — phantom-event guard */ }
64
+ return total;
65
+ }