context-mode 1.0.147 → 1.0.149
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/build/adapters/codex/index.d.ts +7 -0
- package/build/adapters/codex/index.js +33 -2
- package/build/adapters/types.d.ts +4 -1
- package/build/cli.js +5 -1
- package/build/executor.d.ts +9 -0
- package/build/executor.js +6 -2
- package/build/server.d.ts +12 -0
- package/build/server.js +109 -5
- package/build/session/analytics.d.ts +19 -0
- package/build/session/analytics.js +71 -21
- package/build/session/db.d.ts +44 -0
- package/build/session/db.js +85 -18
- package/build/store-directory.d.ts +56 -0
- package/build/store-directory.js +254 -0
- package/build/store.d.ts +29 -0
- package/build/store.js +46 -0
- package/build/util/project-dir.d.ts +43 -0
- package/build/util/project-dir.js +102 -1
- package/cli.bundle.mjs +165 -152
- package/hooks/session-db.bundle.mjs +5 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +146 -133
|
@@ -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.149"
|
|
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.149",
|
|
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.149",
|
|
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.149",
|
|
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.149",
|
|
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.149",
|
|
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",
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { BaseAdapter } from "../base.js";
|
|
17
17
|
import { type HookAdapter, type HookParadigm, type PlatformCapabilities, type DiagnosticResult, type PreToolUseEvent, type PostToolUseEvent, type PreCompactEvent, type SessionStartEvent, type PreToolUseResponse, type PostToolUseResponse, type PreCompactResponse, type SessionStartResponse, type HookRegistration } from "../types.js";
|
|
18
|
+
type CodexVersionRunner = (file: string, args: string[], options: {
|
|
19
|
+
encoding: BufferEncoding;
|
|
20
|
+
stdio: ["ignore", "pipe", "ignore"];
|
|
21
|
+
timeout: number;
|
|
22
|
+
}) => string | Buffer;
|
|
23
|
+
export declare function probeCodexCliVersion(runCommand?: CodexVersionRunner): string | null;
|
|
18
24
|
export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
19
25
|
constructor();
|
|
20
26
|
readonly name = "Codex CLI";
|
|
@@ -67,3 +73,4 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
67
73
|
*/
|
|
68
74
|
private extractSessionId;
|
|
69
75
|
}
|
|
76
|
+
export {};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* while input rewriting remains blocked on upstream updatedInput support.
|
|
14
14
|
* Track: https://github.com/openai/codex/issues/18491
|
|
15
15
|
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
16
17
|
import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
|
|
17
18
|
import { resolve, dirname, join } from "node:path";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +55,26 @@ const LEGACY_HOOK_PATH_SUFFIXES = {
|
|
|
54
55
|
UserPromptSubmit: ["hooks/userpromptsubmit.mjs", "hooks/codex/userpromptsubmit.mjs"],
|
|
55
56
|
Stop: ["hooks/stop.mjs", "hooks/codex/stop.mjs"],
|
|
56
57
|
};
|
|
58
|
+
export function probeCodexCliVersion(runCommand = execFileSync) {
|
|
59
|
+
try {
|
|
60
|
+
const output = process.platform === "win32"
|
|
61
|
+
? runCommand("cmd.exe", ["/d", "/s", "/c", "codex --version"], {
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
64
|
+
timeout: 5000,
|
|
65
|
+
})
|
|
66
|
+
: runCommand("codex", ["--version"], {
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
69
|
+
timeout: 1500,
|
|
70
|
+
});
|
|
71
|
+
const version = String(output).trim();
|
|
72
|
+
return version.length > 0 ? version : "available (version output empty)";
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
57
78
|
function getTomlSection(raw, sectionName) {
|
|
58
79
|
const lines = raw.split(/\r?\n/);
|
|
59
80
|
let inSection = false;
|
|
@@ -365,6 +386,15 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
365
386
|
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
366
387
|
validateHooks(_pluginRoot) {
|
|
367
388
|
const results = [];
|
|
389
|
+
const codexCliVersion = probeCodexCliVersion();
|
|
390
|
+
results.push({
|
|
391
|
+
check: "Codex CLI binary",
|
|
392
|
+
status: codexCliVersion ? "pass" : "warn",
|
|
393
|
+
message: codexCliVersion
|
|
394
|
+
? `codex --version resolved to ${codexCliVersion}`
|
|
395
|
+
: "Could not run codex --version; hooks need the Codex CLI available on PATH",
|
|
396
|
+
...(codexCliVersion ? {} : { fix: "Install Codex CLI or make codex available on PATH" }),
|
|
397
|
+
});
|
|
368
398
|
try {
|
|
369
399
|
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
370
400
|
const enabled = hasCodexHooksFeature(raw);
|
|
@@ -498,8 +528,9 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
498
528
|
}
|
|
499
529
|
}
|
|
500
530
|
getInstalledVersion() {
|
|
501
|
-
// Codex
|
|
502
|
-
|
|
531
|
+
// Codex uses standalone MCP registration; there is no platform-owned
|
|
532
|
+
// plugin version to compare against the context-mode npm package.
|
|
533
|
+
return "standalone";
|
|
503
534
|
}
|
|
504
535
|
// ── Upgrade ────────────────────────────────────────────
|
|
505
536
|
configureAllHooks(pluginRoot) {
|
|
@@ -229,7 +229,10 @@ export interface HookAdapter {
|
|
|
229
229
|
getHealthChecks?(pluginRoot: string): readonly HealthCheck[];
|
|
230
230
|
/** Check if the plugin is registered/enabled on this platform. */
|
|
231
231
|
checkPluginRegistration(): DiagnosticResult;
|
|
232
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Get the installed version from this platform's registry/marketplace, or
|
|
234
|
+
* "standalone" when no platform-owned plugin version exists.
|
|
235
|
+
*/
|
|
233
236
|
getInstalledVersion(): string;
|
|
234
237
|
/** Configure all hooks for this platform. Returns change descriptions. */
|
|
235
238
|
configureAllHooks(pluginRoot: string): string[];
|
package/build/cli.js
CHANGED
|
@@ -763,7 +763,11 @@ async function doctor() {
|
|
|
763
763
|
` — local v${localVersion}, latest v${latestVersion}` +
|
|
764
764
|
color.dim("\n Run: /context-mode:ctx-upgrade"));
|
|
765
765
|
}
|
|
766
|
-
if (installedVersion === "
|
|
766
|
+
if (installedVersion === "standalone") {
|
|
767
|
+
p.log.info(color.dim(`${adapter.name}: standalone MCP mode`) +
|
|
768
|
+
" — no platform plugin version to compare");
|
|
769
|
+
}
|
|
770
|
+
else if (installedVersion === "not installed") {
|
|
767
771
|
p.log.info(color.dim(`${adapter.name}: not installed`) +
|
|
768
772
|
" — using standalone MCP mode");
|
|
769
773
|
}
|
package/build/executor.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ interface ExecuteOptions {
|
|
|
19
19
|
timeout?: number;
|
|
20
20
|
/** Keep process running after timeout instead of killing it. */
|
|
21
21
|
background?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Issue #45 — per-call cwd override for the shell language. When set,
|
|
24
|
+
* the shell script runs in this directory instead of `#projectRoot`.
|
|
25
|
+
* Non-shell languages keep their tmpDir sandbox cwd regardless (the
|
|
26
|
+
* script file lives there). Used by Codex MCP handlers to pin shell
|
|
27
|
+
* commands to a resolved project root when the spawning host inherited
|
|
28
|
+
* a non-project cwd (e.g. $HOME).
|
|
29
|
+
*/
|
|
30
|
+
cwd?: string;
|
|
22
31
|
}
|
|
23
32
|
interface ExecuteFileOptions extends ExecuteOptions {
|
|
24
33
|
path: string;
|
package/build/executor.js
CHANGED
|
@@ -130,7 +130,7 @@ export class PolyglotExecutor {
|
|
|
130
130
|
this.#backgroundedPids.clear();
|
|
131
131
|
}
|
|
132
132
|
async execute(opts) {
|
|
133
|
-
const { language, code, timeout, background = false } = opts;
|
|
133
|
+
const { language, code, timeout, background = false, cwd: cwdOverride } = opts;
|
|
134
134
|
const tmpDir = mkdtempSync(join(OS_TMPDIR, ".ctx-mode-"));
|
|
135
135
|
try {
|
|
136
136
|
const filePath = this.#writeScript(tmpDir, code, language);
|
|
@@ -142,7 +142,11 @@ export class PolyglotExecutor {
|
|
|
142
142
|
// Shell commands run in the project directory so git, relative paths,
|
|
143
143
|
// and other project-aware tools work naturally. Non-shell languages
|
|
144
144
|
// run in the temp directory where their script file is written.
|
|
145
|
-
|
|
145
|
+
// Issue #45 — `cwdOverride` lets per-call sites (Codex MCP handlers)
|
|
146
|
+
// pin shell cwd without mutating process-wide state.
|
|
147
|
+
const cwd = language === "shell"
|
|
148
|
+
? (cwdOverride ?? this.#projectRoot)
|
|
149
|
+
: tmpDir;
|
|
146
150
|
const result = await this.#spawn(cmd, cwd, tmpDir, timeout, background);
|
|
147
151
|
// Skip tmpDir cleanup if process was backgrounded — it may still need files
|
|
148
152
|
if (!result.backgrounded) {
|
package/build/server.d.ts
CHANGED
|
@@ -78,6 +78,18 @@ export declare function resolveSessionIdFromSessionDB(opts?: {
|
|
|
78
78
|
sessionsDir?: string;
|
|
79
79
|
bypassCache?: boolean;
|
|
80
80
|
}): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Project directory detection across supported platforms.
|
|
83
|
+
*
|
|
84
|
+
* Priority:
|
|
85
|
+
* 1. Platform-specific env var (set by host IDE before MCP server spawn)
|
|
86
|
+
* 2. CONTEXT_MODE_PROJECT_DIR (set by start.mjs for ALL platforms — universal)
|
|
87
|
+
* 3. process.cwd() (last resort)
|
|
88
|
+
*
|
|
89
|
+
* CONTEXT_MODE_PROJECT_DIR guarantees correct projectDir even for platforms
|
|
90
|
+
* that don't set their own env var (Cursor, OpenClaw, Codex, Kiro, Zed).
|
|
91
|
+
*/
|
|
92
|
+
export declare function getProjectDir(): string;
|
|
81
93
|
/**
|
|
82
94
|
* Parse FTS5 highlight markers to find match positions in the
|
|
83
95
|
* original (marker-free) text. Returns character offsets into the
|
package/build/server.js
CHANGED
|
@@ -465,7 +465,7 @@ function getSessionDir() {
|
|
|
465
465
|
* CONTEXT_MODE_PROJECT_DIR guarantees correct projectDir even for platforms
|
|
466
466
|
* that don't set their own env var (Cursor, OpenClaw, Codex, Kiro, Zed).
|
|
467
467
|
*/
|
|
468
|
-
function getProjectDir() {
|
|
468
|
+
export function getProjectDir() {
|
|
469
469
|
const override = projectDirOverride.getStore();
|
|
470
470
|
if (override)
|
|
471
471
|
return override.projectDir;
|
|
@@ -500,14 +500,22 @@ function getProjectDir() {
|
|
|
500
500
|
// and the legacy literal cascade is preserved there for semver safety.
|
|
501
501
|
let transcriptsRoot;
|
|
502
502
|
let strictPlatform;
|
|
503
|
+
let codexHome;
|
|
503
504
|
try {
|
|
504
505
|
const detected = detectPlatform().platform;
|
|
505
506
|
strictPlatform = detected;
|
|
506
507
|
if (detected === "claude-code") {
|
|
507
508
|
transcriptsRoot = join(homedir(), ".claude", "projects");
|
|
508
509
|
}
|
|
510
|
+
// Issue #45 — Codex publishes no workspace env var, so the resolver
|
|
511
|
+
// reads `meta.cwd` from the most-recently-modified session.jsonl under
|
|
512
|
+
// `${codexHome}/sessions/`. Wire codexHome at the call site so the
|
|
513
|
+
// resolver can be exercised under test without process-level mutation.
|
|
514
|
+
if (detected === "codex") {
|
|
515
|
+
codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
516
|
+
}
|
|
509
517
|
}
|
|
510
|
-
catch { /* detection failure — leave
|
|
518
|
+
catch { /* detection failure — leave undefined, resolver uses legacy cascade */ }
|
|
511
519
|
return resolveProjectDir({
|
|
512
520
|
env: process.env,
|
|
513
521
|
cwd: process.cwd(),
|
|
@@ -515,6 +523,7 @@ function getProjectDir() {
|
|
|
515
523
|
transcriptsRoot,
|
|
516
524
|
transcriptMaxAgeMs: 5 * 60 * 1000,
|
|
517
525
|
strictPlatform,
|
|
526
|
+
codexHome,
|
|
518
527
|
});
|
|
519
528
|
}
|
|
520
529
|
/**
|
|
@@ -1731,13 +1740,20 @@ EXAMPLE: ctx_index(path: "/path/to/large-spec.md", source: "openapi-v2-spec")`,
|
|
|
1731
1740
|
path: z
|
|
1732
1741
|
.string()
|
|
1733
1742
|
.optional()
|
|
1734
|
-
.describe("File path to read and index (content never enters context). Provide this OR content."),
|
|
1743
|
+
.describe("File OR directory path to read and index (content never enters context). Provide this OR content. Directory paths trigger a bounded recursive walk (#687)."),
|
|
1735
1744
|
source: z
|
|
1736
1745
|
.string()
|
|
1737
1746
|
.optional()
|
|
1738
1747
|
.describe("Label for the indexed content (e.g., 'Context7: React useEffect', 'Skill: frontend-design')"),
|
|
1748
|
+
include: z.array(z.string()).optional().describe("Directory-only: glob patterns to include (default: all matching extensions)."),
|
|
1749
|
+
exclude: z.array(z.string()).optional().describe("Directory-only: glob patterns to exclude. Merged with defaults (node_modules, .git, dist, build, .next, coverage, .venv, __pycache__, .DS_Store)."),
|
|
1750
|
+
maxDepth: z.number().int().min(0).optional().describe("Directory-only: max recursion depth from root (default: 5)."),
|
|
1751
|
+
maxFiles: z.number().int().min(1).optional().describe("Directory-only: hard cap on files indexed (default: 200) — FTS5 blow-up guard."),
|
|
1752
|
+
extensions: z.array(z.string()).optional().describe("Directory-only: allowed file extensions (default: .md .mdx .txt .json .yaml .yml .ts .tsx .js .jsx .py .rs .go .sh)."),
|
|
1753
|
+
respectGitignore: z.boolean().optional().describe("Directory-only: apply nearest .gitignore (default: true)."),
|
|
1754
|
+
followSymlinks: z.boolean().optional().describe("Directory-only: follow directory symlinks (default: false — cycle hazard + escape risk)."),
|
|
1739
1755
|
}),
|
|
1740
|
-
}, async ({ content, path, source }) => {
|
|
1756
|
+
}, async ({ content, path, source, include, exclude, maxDepth, maxFiles, extensions, respectGitignore, followSymlinks }) => {
|
|
1741
1757
|
if (!content && !path) {
|
|
1742
1758
|
return trackResponse("ctx_index", {
|
|
1743
1759
|
content: [
|
|
@@ -1760,6 +1776,54 @@ EXAMPLE: ctx_index(path: "/path/to/large-spec.md", source: "openapi-v2-spec")`,
|
|
|
1760
1776
|
}
|
|
1761
1777
|
try {
|
|
1762
1778
|
const resolvedPath = path ? resolveProjectPath(path) : undefined;
|
|
1779
|
+
// Directory dispatch (#687, reported by @matiasduartee). When the
|
|
1780
|
+
// resolved path is a directory, walk it bounded and re-enter `index()`
|
|
1781
|
+
// per-file so the security gate at store.ts:845 (TOCTOU defense from
|
|
1782
|
+
// #442 round-3) keeps running for every file.
|
|
1783
|
+
if (resolvedPath && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1784
|
+
const store = getStore();
|
|
1785
|
+
const projectDir = getProjectDir();
|
|
1786
|
+
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
1787
|
+
const isWin32 = process.platform === "win32";
|
|
1788
|
+
const perFileDeny = (absPath) => {
|
|
1789
|
+
try {
|
|
1790
|
+
return evaluateFilePath(absPath, denyGlobs, isWin32, projectDir).denied;
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
return false; // fail-open consistent with checkFilePathDenyPolicy
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
const dirResult = store.indexDirectory({
|
|
1797
|
+
path: resolvedPath,
|
|
1798
|
+
source: source ?? resolvedPath,
|
|
1799
|
+
attribution: currentAttribution(),
|
|
1800
|
+
perFileDeny,
|
|
1801
|
+
include,
|
|
1802
|
+
exclude,
|
|
1803
|
+
maxDepth,
|
|
1804
|
+
maxFiles,
|
|
1805
|
+
extensions,
|
|
1806
|
+
respectGitignore,
|
|
1807
|
+
followSymlinks,
|
|
1808
|
+
});
|
|
1809
|
+
const capNote = dirResult.capped
|
|
1810
|
+
? ` (cap reached — only first ${dirResult.filesIndexed} of ${dirResult.totalSeen}+ files; raise maxFiles to index more)`
|
|
1811
|
+
: "";
|
|
1812
|
+
const denyNote = dirResult.denied > 0
|
|
1813
|
+
? ` (${dirResult.denied} file${dirResult.denied === 1 ? "" : "s"} blocked by Read deny policy)`
|
|
1814
|
+
: "";
|
|
1815
|
+
const failNote = dirResult.failed > 0
|
|
1816
|
+
? ` (${dirResult.failed} file${dirResult.failed === 1 ? "" : "s"} failed to read)`
|
|
1817
|
+
: "";
|
|
1818
|
+
return trackResponse("ctx_index", {
|
|
1819
|
+
content: [
|
|
1820
|
+
{
|
|
1821
|
+
type: "text",
|
|
1822
|
+
text: `Indexed ${dirResult.filesIndexed} file${dirResult.filesIndexed === 1 ? "" : "s"} (${dirResult.totalChunks} sections) from directory: ${dirResult.label}${capNote}${denyNote}${failNote}\nUse ctx_search(queries: ["..."]) to query this content.`,
|
|
1823
|
+
},
|
|
1824
|
+
],
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1763
1827
|
// Track the raw bytes being indexed (content or file)
|
|
1764
1828
|
if (content)
|
|
1765
1829
|
trackIndexed(Buffer.byteLength(content));
|
|
@@ -3106,7 +3170,47 @@ server.registerTool("ctx_stats", {
|
|
|
3106
3170
|
// 49 MB of indexed content sitting in the content DB.
|
|
3107
3171
|
// Render-time read-only — no DB mutation, no backfill.
|
|
3108
3172
|
const contentDbPath = getStorePath();
|
|
3109
|
-
|
|
3173
|
+
// v1.0.148 Bug E+F: a conversation typically spans many
|
|
3174
|
+
// session_ids (resume cycles, /compact rebirths, PID
|
|
3175
|
+
// sub-process sessions launched by ctx_execute). Scoping
|
|
3176
|
+
// per-session loses sandbox-burst bytes_avoided that the
|
|
3177
|
+
// PID-sessions own. Look up THIS session's project_dir
|
|
3178
|
+
// from META and aggregate via META subquery so all
|
|
3179
|
+
// sibling sessions in the same cwd attribute together.
|
|
3180
|
+
// Fallback to sessionId scope if the META lookup fails
|
|
3181
|
+
// (best-effort — the original metric is still defensible).
|
|
3182
|
+
let convReal;
|
|
3183
|
+
try {
|
|
3184
|
+
const Database = loadDatabase();
|
|
3185
|
+
const dbFiles = (await import("node:fs"))
|
|
3186
|
+
.readdirSync(getSessionDir())
|
|
3187
|
+
.filter((f) => f.endsWith(".db") && (!dbHash || f.startsWith(dbHash)));
|
|
3188
|
+
let projectDirForSid;
|
|
3189
|
+
for (const file of dbFiles) {
|
|
3190
|
+
try {
|
|
3191
|
+
const sdb = new Database((await import("node:path")).join(getSessionDir(), file), { readonly: true });
|
|
3192
|
+
try {
|
|
3193
|
+
const r = sdb
|
|
3194
|
+
.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?")
|
|
3195
|
+
.get(sid);
|
|
3196
|
+
if (r?.project_dir) {
|
|
3197
|
+
projectDirForSid = r.project_dir;
|
|
3198
|
+
break;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
finally {
|
|
3202
|
+
sdb.close();
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
catch { /* skip unreadable DB */ }
|
|
3206
|
+
}
|
|
3207
|
+
convReal = projectDirForSid
|
|
3208
|
+
? getRealBytesStats({ projectDir: projectDirForSid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath })
|
|
3209
|
+
: getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
|
|
3210
|
+
}
|
|
3211
|
+
catch {
|
|
3212
|
+
convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
|
|
3213
|
+
}
|
|
3110
3214
|
const lifeRealBase = getRealBytesStats({ sessionsDir: getSessionDir() });
|
|
3111
3215
|
// v1.0.134 SLICE C: lifetime tier sums ALL chunks (no
|
|
3112
3216
|
// session_id filter). Without this fold, lifetime "kept out"
|
|
@@ -427,6 +427,25 @@ export declare function getRealBytesStats(opts: {
|
|
|
427
427
|
sessionId?: string;
|
|
428
428
|
sessionsDir?: string;
|
|
429
429
|
worktreeHash?: string;
|
|
430
|
+
/**
|
|
431
|
+
* v1.0.148 follow-up (Bug E+F): when set, the function aggregates across
|
|
432
|
+
* EVERY session whose `session_meta.project_dir` matches this value, not
|
|
433
|
+
* just one session_id. Resolves the per-conversation under-attribution:
|
|
434
|
+
* one Claude Code conversation typically spans many session_ids (resume
|
|
435
|
+
* cycles, /compact rebirths, PID sub-process sessions spawned by
|
|
436
|
+
* ctx_execute), so a single-session_id filter loses the sandbox-burst
|
|
437
|
+
* bytes_avoided that all live under the conversation's cwd.
|
|
438
|
+
*
|
|
439
|
+
* Uses a META subquery (`session_id IN (SELECT session_id FROM
|
|
440
|
+
* session_meta WHERE project_dir = ?)`), then sums ALL events for
|
|
441
|
+
* matching sessions regardless of their event-level project_dir
|
|
442
|
+
* (sandbox-burst events write `project_dir = ''` even when the
|
|
443
|
+
* META row carries the parent cwd — see Bug F).
|
|
444
|
+
*
|
|
445
|
+
* Mutually exclusive with `sessionId`. When both are set, `sessionId`
|
|
446
|
+
* wins for back-compat.
|
|
447
|
+
*/
|
|
448
|
+
projectDir?: string;
|
|
430
449
|
/**
|
|
431
450
|
* v1.0.133 Slice 3: when set alongside `sessionId`, the function joins
|
|
432
451
|
* the FTS5 content DB at this path and folds chunk bytes into
|
|
@@ -13,6 +13,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { join, sep } from "node:path";
|
|
15
15
|
import { loadDatabase as loadDatabaseImpl } from "../db-base.js";
|
|
16
|
+
import { ensureSessionEventsSchema } from "./db.js";
|
|
16
17
|
import { resolveClaudeConfigDir } from "../util/claude-config.js";
|
|
17
18
|
function semverNewer(a, b) {
|
|
18
19
|
const pa = a.split(".").map(Number);
|
|
@@ -857,6 +858,15 @@ export function getRealBytesStats(opts) {
|
|
|
857
858
|
// don't need to type-narrow per row.
|
|
858
859
|
for (const file of dbFiles) {
|
|
859
860
|
const dbPath = join(sessionsDir, file);
|
|
861
|
+
// v1.0.148 hotfix: historical DBs were created with pre-v1.0.130
|
|
862
|
+
// schema (no bytes_avoided / bytes_returned / project_dir columns).
|
|
863
|
+
// The SELECT below references those columns, so without an in-place
|
|
864
|
+
// migration the prepare() throws and the surrounding catch silently
|
|
865
|
+
// skips the WHOLE DB — losing even the LENGTH(data) signal. Run the
|
|
866
|
+
// shared migration helper before opening readonly. Idempotent: a
|
|
867
|
+
// PRAGMA check inside the helper short-circuits when the DB is
|
|
868
|
+
// already current, so post-first-read calls are cheap.
|
|
869
|
+
ensureSessionEventsSchema(dbPath, DatabaseCtor);
|
|
860
870
|
try {
|
|
861
871
|
const sdb = new DatabaseCtor(dbPath, { readonly: true });
|
|
862
872
|
try {
|
|
@@ -878,6 +888,36 @@ export function getRealBytesStats(opts) {
|
|
|
878
888
|
}
|
|
879
889
|
catch { /* old schema */ }
|
|
880
890
|
}
|
|
891
|
+
else if (opts.projectDir) {
|
|
892
|
+
// Bug E+F: META-scoped aggregation. Take every session_id whose
|
|
893
|
+
// session_meta.project_dir matches, then sum ALL of those
|
|
894
|
+
// sessions' events regardless of the events' own project_dir
|
|
895
|
+
// (sandbox-burst PID sessions write empty event-level project_dir
|
|
896
|
+
// even when their META carries the parent cwd).
|
|
897
|
+
const row = sdb.prepare(`SELECT
|
|
898
|
+
COALESCE(SUM(LENGTH(data)), 0) AS data_bytes,
|
|
899
|
+
COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
|
|
900
|
+
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
901
|
+
FROM session_events
|
|
902
|
+
WHERE session_id IN (
|
|
903
|
+
SELECT session_id FROM session_meta WHERE project_dir = ?
|
|
904
|
+
)`).get(opts.projectDir);
|
|
905
|
+
if (row) {
|
|
906
|
+
eventDataBytes += Number(row.data_bytes ?? 0);
|
|
907
|
+
bytesAvoided += Number(row.bytes_avoided ?? 0);
|
|
908
|
+
bytesReturned += Number(row.bytes_returned ?? 0);
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const snap = sdb.prepare(`SELECT COALESCE(SUM(LENGTH(snapshot)), 0) AS bytes
|
|
912
|
+
FROM session_resume
|
|
913
|
+
WHERE session_id IN (
|
|
914
|
+
SELECT session_id FROM session_meta WHERE project_dir = ?
|
|
915
|
+
)`).get(opts.projectDir);
|
|
916
|
+
if (snap?.bytes)
|
|
917
|
+
snapshotBytes += Number(snap.bytes);
|
|
918
|
+
}
|
|
919
|
+
catch { /* old schema */ }
|
|
920
|
+
}
|
|
881
921
|
else {
|
|
882
922
|
const row = sdb.prepare(`SELECT
|
|
883
923
|
COALESCE(SUM(LENGTH(data)), 0) AS data_bytes,
|
|
@@ -1504,34 +1544,44 @@ function renderNarrative5Section(args) {
|
|
|
1504
1544
|
out.push(` Without that, you'd be re-explaining everything to a blank model right now.`);
|
|
1505
1545
|
}
|
|
1506
1546
|
out.push("");
|
|
1507
|
-
// Without/With bars —
|
|
1547
|
+
// Without/With bars — strict compression (v1.0.148, Bug G / ADR-0004).
|
|
1508
1548
|
//
|
|
1509
|
-
// Honest definitions
|
|
1510
|
-
// Without = bytes the model WOULD have re-seen
|
|
1511
|
-
//
|
|
1549
|
+
// Honest definitions:
|
|
1550
|
+
// Without = bytes the model WOULD have re-seen if context-mode
|
|
1551
|
+
// had not diverted them
|
|
1552
|
+
// = bytesAvoided + bytesReturned
|
|
1512
1553
|
// With = bytes the model ACTUALLY re-saw after context-mode
|
|
1513
|
-
// =
|
|
1554
|
+
// = max(1, bytesReturned)
|
|
1514
1555
|
//
|
|
1515
|
-
// Why eventDataBytes
|
|
1516
|
-
// `eventDataBytes` is the raw payload
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1556
|
+
// Why eventDataBytes is excluded from this ratio:
|
|
1557
|
+
// `eventDataBytes` is the raw hook payload (tool args, prompt
|
|
1558
|
+
// body) we captured for the knowledge base. Those bytes are
|
|
1559
|
+
// analytics infrastructure — they NEVER enter the model context
|
|
1560
|
+
// window. Including them on either side (as v1.0.134 SLICE B did
|
|
1561
|
+
// to dodge a degenerate 100% bar) misrepresents context cost.
|
|
1562
|
+
// SLICE B was an incidental fix that crushed the displayed
|
|
1563
|
+
// percentage from ~95% (the true compression ratio) to ~56% on
|
|
1564
|
+
// live conversations. eventDataBytes is rendered in Section 2
|
|
1565
|
+
// (captures count), not in this Section 1 Without/With bar.
|
|
1524
1566
|
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1567
|
+
// Empty-state branch:
|
|
1568
|
+
// If neither bytesAvoided nor bytesReturned has been measured yet
|
|
1569
|
+
// (early in a session, schema-migration recovery in progress, or
|
|
1570
|
+
// tool-heavy work that hasn't re-hit the index), we do NOT draw
|
|
1571
|
+
// a degenerate 0% / 100% bar. We emit one honest hint line and
|
|
1572
|
+
// skip the bar — honesty over decoration.
|
|
1528
1573
|
const realConv = realBytes?.conversation;
|
|
1529
1574
|
const measuredAvoided = realConv?.bytesAvoided ?? 0;
|
|
1530
1575
|
const measuredReturned = realConv?.bytesReturned ?? 0;
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1576
|
+
if (measuredAvoided + measuredReturned === 0) {
|
|
1577
|
+
// No measurable redirect activity yet — captures may exist, but
|
|
1578
|
+
// nothing has been diverted from the model context window.
|
|
1579
|
+
out.push(" No measurable redirect activity captured yet — bars will appear once context-mode diverts its first payload.");
|
|
1580
|
+
out.push("");
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
const convBytesWithout = measuredAvoided + measuredReturned;
|
|
1584
|
+
const convBytesWith = Math.max(1, measuredReturned);
|
|
1535
1585
|
const convTokensWithout = Math.max(1, Math.floor(convBytesWithout / 4));
|
|
1536
1586
|
const convTokensWith = Math.max(1, Math.floor(convBytesWith / 4));
|
|
1537
1587
|
const withoutBar = dataBar(convTokensWithout, convTokensWithout, 32);
|
package/build/session/db.d.ts
CHANGED
|
@@ -179,6 +179,50 @@ export interface ToolCallStats {
|
|
|
179
179
|
bytesReturned: number;
|
|
180
180
|
}>;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Apply any missing post-v1.0.130 `session_events` columns to an already-
|
|
184
|
+
* open writable database handle. Idempotent — each ALTER is guarded by a
|
|
185
|
+
* PRAGMA table_xinfo check, and the project_dir index is created only
|
|
186
|
+
* when a migration actually ran. Returns true if any column was added.
|
|
187
|
+
*
|
|
188
|
+
* Used by both the SessionDB constructor (for the active DB) and the
|
|
189
|
+
* analytics aggregator (for the 100+ historical DBs that never get
|
|
190
|
+
* opened through SessionDB). ADR-0001 compatible: no EXCLUSIVE pragma,
|
|
191
|
+
* no acquireDbLock — relies on the SQLite busy_timeout + WAL semantics
|
|
192
|
+
* already provided by SQLiteBase.
|
|
193
|
+
*/
|
|
194
|
+
export declare function applyMissingSessionEventsColumns(db: {
|
|
195
|
+
pragma: (q: string) => Array<{
|
|
196
|
+
name: string;
|
|
197
|
+
}>;
|
|
198
|
+
exec: (sql: string) => void;
|
|
199
|
+
}): boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Open a session DB file briefly, run any missing schema migrations,
|
|
202
|
+
* and close. Best-effort: missing tables, file-locks, corrupt files,
|
|
203
|
+
* and any DatabaseCtor error are swallowed silently — the caller
|
|
204
|
+
* (analytics aggregator) handles the readonly query that follows and
|
|
205
|
+
* will skip the DB if it remains unreadable.
|
|
206
|
+
*
|
|
207
|
+
* Lazy migration entry point for the analytics aggregator, which would
|
|
208
|
+
* otherwise read 100+ historical DBs with the old (pre-v1.0.130) schema
|
|
209
|
+
* and lose every signal (not just bytes_avoided) because the SELECT
|
|
210
|
+
* statement references columns that don't exist on legacy schemas.
|
|
211
|
+
*
|
|
212
|
+
* Two open/close cycles in the worst case (one readonly probe to detect
|
|
213
|
+
* legacy schema, one writable to migrate). For already-migrated DBs
|
|
214
|
+
* (the common case after first read), this opens writable once and
|
|
215
|
+
* exits without writing — cheaper than always-writable.
|
|
216
|
+
*/
|
|
217
|
+
export declare function ensureSessionEventsSchema(dbPath: string, DatabaseCtor: new (path: string, opts?: {
|
|
218
|
+
readonly?: boolean;
|
|
219
|
+
}) => {
|
|
220
|
+
pragma: (q: string) => Array<{
|
|
221
|
+
name: string;
|
|
222
|
+
}>;
|
|
223
|
+
exec: (sql: string) => void;
|
|
224
|
+
close: () => void;
|
|
225
|
+
}): void;
|
|
182
226
|
export declare class SessionDB extends SQLiteBase {
|
|
183
227
|
/**
|
|
184
228
|
* Cached prepared statements. Stored in a Map to avoid the JS private-field
|