context-mode 1.0.132 → 1.0.134
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/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/server.d.ts +17 -0
- package/build/server.js +88 -6
- package/build/session/analytics.d.ts +56 -0
- package/build/session/analytics.js +139 -8
- package/cli.bundle.mjs +149 -146
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +118 -115
|
@@ -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.134"
|
|
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.134",
|
|
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.134",
|
|
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.134",
|
|
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.134",
|
|
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.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
|
|
3
3
|
import { ContentStore } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
6
|
+
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|
|
7
|
+
* the current session — eventId stays undefined because MCP tool invocations
|
|
8
|
+
* are not paired with PostToolUse event rows at index time (the hook fires
|
|
9
|
+
* AFTER the tool returns). Empty-string fallback inside #insertChunks keeps
|
|
10
|
+
* legacy unattributed rows readable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function currentAttribution(): {
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
} | undefined;
|
|
15
|
+
/** v1.0.134 SLICE A: opts injection for testability. Production callers pass nothing. */
|
|
16
|
+
export declare function resolveSessionIdFromSessionDB(opts?: {
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
sessionsDir?: string;
|
|
19
|
+
bypassCache?: boolean;
|
|
20
|
+
}): string | undefined;
|
|
4
21
|
/**
|
|
5
22
|
* Parse FTS5 highlight markers to find match positions in the
|
|
6
23
|
* original (marker-free) text. Returns character offsets into the
|
package/build/server.js
CHANGED
|
@@ -29,7 +29,7 @@ import { getHookScriptPaths } from "./util/hook-config.js";
|
|
|
29
29
|
import { resolveClaudeConfigDir } from "./util/claude-config.js";
|
|
30
30
|
import { resolveProjectDir } from "./util/project-dir.js";
|
|
31
31
|
import { loadDatabase } from "./db-base.js";
|
|
32
|
-
import { AnalyticsEngine, formatReport, getConversationStats, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, OPUS_INPUT_PRICE_PER_TOKEN } from "./session/analytics.js";
|
|
32
|
+
import { AnalyticsEngine, formatReport, getConversationStats, getContentBytesAllSessions, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, OPUS_INPUT_PRICE_PER_TOKEN } from "./session/analytics.js";
|
|
33
33
|
const __pkg_dir = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const VERSION = (() => {
|
|
35
35
|
for (const rel of ["../package.json", "./package.json"]) {
|
|
@@ -86,12 +86,56 @@ let _store = null;
|
|
|
86
86
|
* AFTER the tool returns). Empty-string fallback inside #insertChunks keeps
|
|
87
87
|
* legacy unattributed rows readable.
|
|
88
88
|
*/
|
|
89
|
-
function currentAttribution() {
|
|
90
|
-
|
|
89
|
+
export function currentAttribution() {
|
|
90
|
+
// CLAUDE_SESSION_ID env var is NOT propagated to MCP servers (only to hooks).
|
|
91
|
+
// Cross-adapter resolution: every adapter (15 of them) sets *_PROJECT_DIR env
|
|
92
|
+
// and writes session_events via hooks. Read the most-recent session_id from
|
|
93
|
+
// THIS project's session DB. Works for claude-code/cursor/gemini-cli/codex/
|
|
94
|
+
// kiro/opencode/zed/kilo/openclaw/qwen-code/vscode-copilot/jetbrains-copilot/
|
|
95
|
+
// omp/pi/antigravity — no adapter-specific transcript path required.
|
|
96
|
+
const sessionId = process.env.CLAUDE_SESSION_ID ?? resolveSessionIdFromSessionDB();
|
|
91
97
|
if (!sessionId)
|
|
92
98
|
return undefined;
|
|
93
99
|
return { sessionId };
|
|
94
100
|
}
|
|
101
|
+
let __cachedSessionId;
|
|
102
|
+
/** v1.0.134 SLICE A: opts injection for testability. Production callers pass nothing. */
|
|
103
|
+
export function resolveSessionIdFromSessionDB(opts) {
|
|
104
|
+
// 2s cache — ctx_fetch_and_index can fire 5+ chunks/sec; DB open cost adds up.
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (!opts?.bypassCache && __cachedSessionId && now - __cachedSessionId.checkedAt < 2000) {
|
|
107
|
+
return __cachedSessionId.sid;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const projectDir = opts?.projectDir
|
|
111
|
+
?? process.env.CLAUDE_PROJECT_DIR
|
|
112
|
+
?? process.env.CONTEXT_MODE_PROJECT_DIR;
|
|
113
|
+
if (!projectDir)
|
|
114
|
+
return undefined;
|
|
115
|
+
const sessionsDir = opts?.sessionsDir ?? getSessionDir();
|
|
116
|
+
const dbPath = resolveSessionDbPath({ projectDir, sessionsDir });
|
|
117
|
+
if (!existsSync(dbPath))
|
|
118
|
+
return undefined;
|
|
119
|
+
const Database = loadDatabase();
|
|
120
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
121
|
+
try {
|
|
122
|
+
const row = db.prepare("SELECT session_id FROM session_events ORDER BY created_at DESC LIMIT 1").get();
|
|
123
|
+
const sid = row?.session_id;
|
|
124
|
+
if (sid)
|
|
125
|
+
__cachedSessionId = { sid, checkedAt: now };
|
|
126
|
+
return sid;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
try {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
catch { /* best-effort */ }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
95
139
|
/**
|
|
96
140
|
* Auto-index session events files written by SessionStart hook.
|
|
97
141
|
* Scans ~/.claude/context-mode/sessions/ for *-events.md files.
|
|
@@ -1701,7 +1745,24 @@ export function buildFetchCode(url, outputPath) {
|
|
|
1701
1745
|
// can serve a public IP for the parent's pre-flight ssrfGuard lookup and
|
|
1702
1746
|
// then a blocked IP (e.g. 169.254.169.254 IMDS) for the subprocess fetch's
|
|
1703
1747
|
// own lookup — classic DNS rebinding across the parent/child boundary.
|
|
1704
|
-
|
|
1748
|
+
//
|
|
1749
|
+
// CRITICAL: bundlers (esbuild) rename top-level identifiers — `classifyIp`
|
|
1750
|
+
// becomes e.g. `_h` in server.bundle.mjs. `classifyIp.toString()` returns
|
|
1751
|
+
// the renamed source `function _h(t){...}`, but the embedded subprocess
|
|
1752
|
+
// template references the literal name `classifyIp` (and the function's
|
|
1753
|
+
// own internal recursion is also `_h(...)`). Result: the subprocess sees
|
|
1754
|
+
// `function _h(t){...; return _h(...)}` injected, then references to
|
|
1755
|
+
// `classifyIp` blow up with `ReferenceError: classifyIp is not defined`.
|
|
1756
|
+
//
|
|
1757
|
+
// Fix: emit `var <fnName> = <fn-expr>; var classifyIp = <fnName>;`. The
|
|
1758
|
+
// named function expression preserves recursion under whatever name the
|
|
1759
|
+
// bundler chose, and the alias re-exposes the canonical `classifyIp`
|
|
1760
|
+
// identifier the rest of the embedded script depends on.
|
|
1761
|
+
const classifyIpInner = classifyIp.toString();
|
|
1762
|
+
const classifyIpFnName = classifyIp.name || "classifyIp";
|
|
1763
|
+
const classifyIpSrc = classifyIpFnName === "classifyIp"
|
|
1764
|
+
? `var classifyIp = ${classifyIpInner};`
|
|
1765
|
+
: `var ${classifyIpFnName} = ${classifyIpInner};\nvar classifyIp = ${classifyIpFnName};`;
|
|
1705
1766
|
const strictMode = process.env.CTX_FETCH_STRICT === "1";
|
|
1706
1767
|
return `
|
|
1707
1768
|
const TurndownService = require(${turndownPath});
|
|
@@ -2597,8 +2658,29 @@ server.registerTool("ctx_stats", {
|
|
|
2597
2658
|
}
|
|
2598
2659
|
if (sid) {
|
|
2599
2660
|
conversation = getConversationStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash });
|
|
2600
|
-
|
|
2601
|
-
|
|
2661
|
+
// v1.0.133 Slice 3: pass contentDbPath so getRealBytesStats can
|
|
2662
|
+
// join chunks WHERE session_id = sid and fold the indexed
|
|
2663
|
+
// content bytes into the per-conversation bar. Without this,
|
|
2664
|
+
// Mert's session showed ~200B (event metadata only) even with
|
|
2665
|
+
// 49 MB of indexed content sitting in the content DB.
|
|
2666
|
+
// Render-time read-only — no DB mutation, no backfill.
|
|
2667
|
+
const contentDbPath = getStorePath();
|
|
2668
|
+
const convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
|
|
2669
|
+
const lifeRealBase = getRealBytesStats({ sessionsDir: getSessionDir() });
|
|
2670
|
+
// v1.0.134 SLICE C: lifetime tier sums ALL chunks (no
|
|
2671
|
+
// session_id filter). Without this fold, lifetime "kept out"
|
|
2672
|
+
// only counts session_events.bytes_avoided and ignores the
|
|
2673
|
+
// bulk of indexed payload across every prior conversation.
|
|
2674
|
+
const lifeContentBytes = getContentBytesAllSessions(contentDbPath);
|
|
2675
|
+
const lifeReal = {
|
|
2676
|
+
...lifeRealBase,
|
|
2677
|
+
contentBytes: lifeRealBase.contentBytes + lifeContentBytes,
|
|
2678
|
+
bytesAvoided: lifeRealBase.bytesAvoided + lifeContentBytes,
|
|
2679
|
+
totalSavedTokens: Math.floor((lifeRealBase.eventDataBytes
|
|
2680
|
+
+ lifeRealBase.bytesAvoided
|
|
2681
|
+
+ lifeContentBytes
|
|
2682
|
+
+ lifeRealBase.snapshotBytes) / 4),
|
|
2683
|
+
};
|
|
2602
2684
|
realBytes = { conversation: convReal, lifetime: lifeReal };
|
|
2603
2685
|
}
|
|
2604
2686
|
}
|
|
@@ -358,8 +358,57 @@ export interface RealBytesStats {
|
|
|
358
358
|
bytesAvoided: number;
|
|
359
359
|
bytesReturned: number;
|
|
360
360
|
snapshotBytes: number;
|
|
361
|
+
/**
|
|
362
|
+
* v1.0.133 Slice 3: bytes attributed to this session in the FTS5 content
|
|
363
|
+
* DB — `SUM(LENGTH(title) + LENGTH(content)) FROM chunks WHERE session_id = ?`.
|
|
364
|
+
*
|
|
365
|
+
* Read-only, render-time computation. Populated only when
|
|
366
|
+
* `getRealBytesStats` is called with both `sessionId` AND `contentDbPath`
|
|
367
|
+
* (i.e. the conversation tier from ctx_stats). Lifetime / project tiers
|
|
368
|
+
* leave this at 0 — aggregating across every adapter's content DB is a
|
|
369
|
+
* separate concern.
|
|
370
|
+
*
|
|
371
|
+
* Legacy chunks with empty `session_id` (pre-Slice-1) are NOT backfilled:
|
|
372
|
+
* the architect rejected the time-window join as unsafe. Old conversations
|
|
373
|
+
* stay low; new conversations populate honestly.
|
|
374
|
+
*/
|
|
375
|
+
contentBytes: number;
|
|
361
376
|
totalSavedTokens: number;
|
|
362
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* v1.0.133 Slice 3: Sum the bytes attributed to one session in the FTS5
|
|
380
|
+
* content DB.
|
|
381
|
+
*
|
|
382
|
+
* Returns `LENGTH(title) + LENGTH(content)` summed across every chunk
|
|
383
|
+
* whose `session_id` column matches `sessionId`. Best-effort — returns 0
|
|
384
|
+
* when the DB file is missing, the schema lacks the `session_id` column
|
|
385
|
+
* (pre-Slice-1 content DBs), or the query fails. Never throws.
|
|
386
|
+
*
|
|
387
|
+
* Render-time only. Does NOT mutate the content DB. Architect-approved
|
|
388
|
+
* because the read-only join carries no risk of cross-session attribution
|
|
389
|
+
* (the FK was set at chunk insert time by Slice 1).
|
|
390
|
+
*/
|
|
391
|
+
export declare function getContentBytesForSession(sessionId: string, contentDbPath: string, opts?: {
|
|
392
|
+
loadDatabase?: () => unknown;
|
|
393
|
+
}): number;
|
|
394
|
+
/**
|
|
395
|
+
* v1.0.134 SLICE C — lifetime tier all-chunks aggregate.
|
|
396
|
+
*
|
|
397
|
+
* Sibling of {@link getContentBytesForSession} that omits the session_id
|
|
398
|
+
* filter so the lifetime tier sees every chunk in the content store —
|
|
399
|
+
* including legacy unattributed rows (sessionId === '') and chunks
|
|
400
|
+
* attributed to other adapters' sessions. Without this, the lifetime
|
|
401
|
+
* "kept out" headline only counts session_events.bytes_avoided and
|
|
402
|
+
* misses the bulk of indexed payload.
|
|
403
|
+
*
|
|
404
|
+
* Best-effort: returns 0 when the DB file is missing, the schema lacks
|
|
405
|
+
* the `chunks` table, or the query fails. Never throws — same contract
|
|
406
|
+
* as the rest of the analytics module so a corrupt content DB cannot
|
|
407
|
+
* crash ctx_stats.
|
|
408
|
+
*/
|
|
409
|
+
export declare function getContentBytesAllSessions(contentDbPath: string, opts?: {
|
|
410
|
+
loadDatabase?: () => unknown;
|
|
411
|
+
}): number;
|
|
363
412
|
/**
|
|
364
413
|
* Compute real-bytes stats across one session, one project (worktree
|
|
365
414
|
* filter), or every session on disk (lifetime).
|
|
@@ -378,6 +427,13 @@ export declare function getRealBytesStats(opts: {
|
|
|
378
427
|
sessionId?: string;
|
|
379
428
|
sessionsDir?: string;
|
|
380
429
|
worktreeHash?: string;
|
|
430
|
+
/**
|
|
431
|
+
* v1.0.133 Slice 3: when set alongside `sessionId`, the function joins
|
|
432
|
+
* the FTS5 content DB at this path and folds chunk bytes into
|
|
433
|
+
* `bytesAvoided` + `totalSavedTokens` + `contentBytes`. Render-time
|
|
434
|
+
* only — no DB writes.
|
|
435
|
+
*/
|
|
436
|
+
contentDbPath?: string;
|
|
381
437
|
loadDatabase?: () => unknown;
|
|
382
438
|
}): RealBytesStats;
|
|
383
439
|
/**
|
|
@@ -706,6 +706,96 @@ export function getConversationStats(opts) {
|
|
|
706
706
|
byDay,
|
|
707
707
|
};
|
|
708
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* v1.0.133 Slice 3: Sum the bytes attributed to one session in the FTS5
|
|
711
|
+
* content DB.
|
|
712
|
+
*
|
|
713
|
+
* Returns `LENGTH(title) + LENGTH(content)` summed across every chunk
|
|
714
|
+
* whose `session_id` column matches `sessionId`. Best-effort — returns 0
|
|
715
|
+
* when the DB file is missing, the schema lacks the `session_id` column
|
|
716
|
+
* (pre-Slice-1 content DBs), or the query fails. Never throws.
|
|
717
|
+
*
|
|
718
|
+
* Render-time only. Does NOT mutate the content DB. Architect-approved
|
|
719
|
+
* because the read-only join carries no risk of cross-session attribution
|
|
720
|
+
* (the FK was set at chunk insert time by Slice 1).
|
|
721
|
+
*/
|
|
722
|
+
export function getContentBytesForSession(sessionId, contentDbPath, opts) {
|
|
723
|
+
if (!sessionId || !contentDbPath)
|
|
724
|
+
return 0;
|
|
725
|
+
if (!existsSync(contentDbPath))
|
|
726
|
+
return 0;
|
|
727
|
+
let DatabaseCtor = null;
|
|
728
|
+
try {
|
|
729
|
+
DatabaseCtor = opts?.loadDatabase
|
|
730
|
+
? opts.loadDatabase()
|
|
731
|
+
: loadDatabaseImpl();
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return 0;
|
|
735
|
+
}
|
|
736
|
+
if (!DatabaseCtor)
|
|
737
|
+
return 0;
|
|
738
|
+
try {
|
|
739
|
+
const db = new DatabaseCtor(contentDbPath, { readonly: true });
|
|
740
|
+
try {
|
|
741
|
+
const row = db.prepare(`SELECT COALESCE(SUM(LENGTH(content) + LENGTH(title)), 0) AS bytes
|
|
742
|
+
FROM chunks WHERE session_id = ?`).get(sessionId);
|
|
743
|
+
return Number(row?.bytes ?? 0);
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
db.close();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* v1.0.134 SLICE C — lifetime tier all-chunks aggregate.
|
|
755
|
+
*
|
|
756
|
+
* Sibling of {@link getContentBytesForSession} that omits the session_id
|
|
757
|
+
* filter so the lifetime tier sees every chunk in the content store —
|
|
758
|
+
* including legacy unattributed rows (sessionId === '') and chunks
|
|
759
|
+
* attributed to other adapters' sessions. Without this, the lifetime
|
|
760
|
+
* "kept out" headline only counts session_events.bytes_avoided and
|
|
761
|
+
* misses the bulk of indexed payload.
|
|
762
|
+
*
|
|
763
|
+
* Best-effort: returns 0 when the DB file is missing, the schema lacks
|
|
764
|
+
* the `chunks` table, or the query fails. Never throws — same contract
|
|
765
|
+
* as the rest of the analytics module so a corrupt content DB cannot
|
|
766
|
+
* crash ctx_stats.
|
|
767
|
+
*/
|
|
768
|
+
export function getContentBytesAllSessions(contentDbPath, opts) {
|
|
769
|
+
if (!contentDbPath)
|
|
770
|
+
return 0;
|
|
771
|
+
if (!existsSync(contentDbPath))
|
|
772
|
+
return 0;
|
|
773
|
+
let DatabaseCtor = null;
|
|
774
|
+
try {
|
|
775
|
+
DatabaseCtor = opts?.loadDatabase
|
|
776
|
+
? opts.loadDatabase()
|
|
777
|
+
: loadDatabaseImpl();
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
if (!DatabaseCtor)
|
|
783
|
+
return 0;
|
|
784
|
+
try {
|
|
785
|
+
const db = new DatabaseCtor(contentDbPath, { readonly: true });
|
|
786
|
+
try {
|
|
787
|
+
const row = db.prepare(`SELECT COALESCE(SUM(LENGTH(content) + LENGTH(title)), 0) AS bytes
|
|
788
|
+
FROM chunks`).get();
|
|
789
|
+
return Number(row?.bytes ?? 0);
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
db.close();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
709
799
|
/**
|
|
710
800
|
* Compute real-bytes stats across one session, one project (worktree
|
|
711
801
|
* filter), or every session on disk (lifetime).
|
|
@@ -726,6 +816,7 @@ export function getRealBytesStats(opts) {
|
|
|
726
816
|
bytesAvoided: 0,
|
|
727
817
|
bytesReturned: 0,
|
|
728
818
|
snapshotBytes: 0,
|
|
819
|
+
contentBytes: 0,
|
|
729
820
|
totalSavedTokens: 0,
|
|
730
821
|
};
|
|
731
822
|
const sessionsDir = opts.sessionsDir
|
|
@@ -812,8 +903,19 @@ export function getRealBytesStats(opts) {
|
|
|
812
903
|
}
|
|
813
904
|
catch { /* missing tables / corrupt — skip */ }
|
|
814
905
|
}
|
|
906
|
+
// v1.0.133 Slice 3: fold content DB chunk bytes for this session into
|
|
907
|
+
// bytesAvoided. Skipped silently when caller didn't pass contentDbPath
|
|
908
|
+
// (lifetime / project tiers, or pre-Slice-3 callers). Treated as
|
|
909
|
+
// "avoided" because indexed chunks are bytes that would have been
|
|
910
|
+
// re-inflated into context on every search if the model had to
|
|
911
|
+
// re-read raw files.
|
|
912
|
+
let contentBytes = 0;
|
|
913
|
+
if (opts.sessionId && opts.contentDbPath) {
|
|
914
|
+
contentBytes = getContentBytesForSession(opts.sessionId, opts.contentDbPath, { loadDatabase: opts.loadDatabase });
|
|
915
|
+
bytesAvoided += contentBytes;
|
|
916
|
+
}
|
|
815
917
|
const totalSavedTokens = Math.floor((eventDataBytes + bytesAvoided + snapshotBytes) / 4);
|
|
816
|
-
return { eventDataBytes, bytesAvoided, bytesReturned, snapshotBytes, totalSavedTokens };
|
|
918
|
+
return { eventDataBytes, bytesAvoided, bytesReturned, snapshotBytes, contentBytes, totalSavedTokens };
|
|
817
919
|
}
|
|
818
920
|
const DEFAULT_REAL_USAGE_FILTER = {
|
|
819
921
|
minEvents: 100,
|
|
@@ -975,6 +1077,7 @@ export function getMultiAdapterRealBytesStats(opts) {
|
|
|
975
1077
|
bytesAvoided: 0,
|
|
976
1078
|
bytesReturned: 0,
|
|
977
1079
|
snapshotBytes: 0,
|
|
1080
|
+
contentBytes: 0,
|
|
978
1081
|
totalSavedTokens: 0,
|
|
979
1082
|
};
|
|
980
1083
|
const perAdapter = [];
|
|
@@ -987,6 +1090,21 @@ export function getMultiAdapterRealBytesStats(opts) {
|
|
|
987
1090
|
worktreeHash: opts?.worktreeHash,
|
|
988
1091
|
loadDatabase: opts?.loadDatabase,
|
|
989
1092
|
});
|
|
1093
|
+
// ARCH-REVIEW-V134-ABC SLICE C: aggregate this adapter's content DB
|
|
1094
|
+
// bytes into the lifetime sum. `getRealBytesStats` operates on
|
|
1095
|
+
// session events only and never touches the sibling content/ tree —
|
|
1096
|
+
// without this step the lifetime tier in ctx_stats reports 0 for
|
|
1097
|
+
// every adapter except whichever one happens to share the
|
|
1098
|
+
// sessionsDir of the caller. Lifetime tier ignores sessionId so
|
|
1099
|
+
// the all-sessions aggregator is the right helper here.
|
|
1100
|
+
if (!opts?.sessionId) {
|
|
1101
|
+
const contentDbPath = join(entry.contentDir, "content.db");
|
|
1102
|
+
const adapterContentBytes = getContentBytesAllSessions(contentDbPath, {
|
|
1103
|
+
loadDatabase: opts?.loadDatabase,
|
|
1104
|
+
});
|
|
1105
|
+
one.contentBytes += adapterContentBytes;
|
|
1106
|
+
sum.contentBytes += adapterContentBytes;
|
|
1107
|
+
}
|
|
990
1108
|
perAdapter.push({ name: entry.name, ...one });
|
|
991
1109
|
sum.eventDataBytes += one.eventDataBytes;
|
|
992
1110
|
sum.bytesAvoided += one.bytesAvoided;
|
|
@@ -1339,19 +1457,32 @@ function renderNarrative5Section(args) {
|
|
|
1339
1457
|
out.push("");
|
|
1340
1458
|
// Without/With bars — measured from real per-event bytes_returned / bytes_avoided.
|
|
1341
1459
|
//
|
|
1342
|
-
// Honest definitions:
|
|
1343
|
-
// Without = bytes the model WOULD have re-seen with no filtering
|
|
1344
|
-
//
|
|
1460
|
+
// Honest definitions (v1.0.134 SLICE B — eventDataBytes floor):
|
|
1461
|
+
// Without = bytes the model WOULD have re-seen with no filtering
|
|
1462
|
+
// = bytes_avoided + bytes_returned + eventDataBytes
|
|
1463
|
+
// With = bytes the model ACTUALLY re-saw after context-mode
|
|
1464
|
+
// = bytes_returned + eventDataBytes
|
|
1465
|
+
//
|
|
1466
|
+
// Why eventDataBytes belongs on BOTH sides:
|
|
1467
|
+
// `eventDataBytes` is the raw payload captured by the hook (tool args,
|
|
1468
|
+
// prompt body, etc). Those bytes were "kept out" — never inflated back
|
|
1469
|
+
// into context — but they still represent real measured signal. Pre-fix
|
|
1470
|
+
// the formula was `with = max(1, bytesReturned)`, which collapsed to 1
|
|
1471
|
+
// whenever the conversation hadn't accumulated any re-served bytes yet
|
|
1472
|
+
// (early in a session, or for tool-heavy work that never re-hits index).
|
|
1473
|
+
// That produced a degenerate ~100% kept-out bar even when the only
|
|
1474
|
+
// honest signal we had was a few KB of event payloads.
|
|
1345
1475
|
//
|
|
1346
1476
|
// No fallback to heuristic. If the schema has zero signal for this
|
|
1347
|
-
// conversation (no hook ever populated
|
|
1477
|
+
// conversation (no hook ever populated any of the three columns),
|
|
1348
1478
|
// the section is skipped entirely. Honesty over decoration.
|
|
1349
1479
|
const realConv = realBytes?.conversation;
|
|
1350
1480
|
const measuredAvoided = realConv?.bytesAvoided ?? 0;
|
|
1351
1481
|
const measuredReturned = realConv?.bytesReturned ?? 0;
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1482
|
+
const measuredEvent = realConv?.eventDataBytes ?? 0;
|
|
1483
|
+
if (measuredAvoided + measuredReturned + measuredEvent > 0) {
|
|
1484
|
+
const convBytesWithout = measuredAvoided + measuredReturned + measuredEvent;
|
|
1485
|
+
const convBytesWith = Math.max(1, measuredReturned + measuredEvent);
|
|
1355
1486
|
const convTokensWithout = Math.max(1, Math.floor(convBytesWithout / 4));
|
|
1356
1487
|
const convTokensWith = Math.max(1, Math.floor(convBytesWith / 4));
|
|
1357
1488
|
const withoutBar = dataBar(convTokensWithout, convTokensWithout, 32);
|