context-mode 1.0.166 → 1.0.168

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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +6 -4
  7. package/build/adapters/codex/usage.d.ts +107 -0
  8. package/build/adapters/codex/usage.js +227 -0
  9. package/build/adapters/gemini-cli/hooks.d.ts +7 -1
  10. package/build/adapters/gemini-cli/hooks.js +9 -1
  11. package/build/adapters/gemini-cli/index.js +11 -0
  12. package/build/adapters/kimi/paths.d.ts +20 -0
  13. package/build/adapters/kimi/paths.js +41 -1
  14. package/build/adapters/kimi/usage.d.ts +82 -0
  15. package/build/adapters/kimi/usage.js +217 -0
  16. package/build/adapters/omp/plugin.d.ts +6 -0
  17. package/build/adapters/omp/plugin.js +87 -2
  18. package/build/adapters/omp/usage.d.ts +49 -0
  19. package/build/adapters/omp/usage.js +110 -0
  20. package/build/adapters/openclaw/plugin.d.ts +10 -0
  21. package/build/adapters/openclaw/plugin.js +57 -0
  22. package/build/adapters/openclaw/usage.d.ts +34 -0
  23. package/build/adapters/openclaw/usage.js +52 -0
  24. package/build/adapters/opencode/plugin.d.ts +17 -0
  25. package/build/adapters/opencode/plugin.js +40 -1
  26. package/build/adapters/pi/extension.js +34 -1
  27. package/build/adapters/qwen-code/index.js +23 -1
  28. package/build/adapters/qwen-code/usage.d.ts +90 -0
  29. package/build/adapters/qwen-code/usage.js +222 -0
  30. package/build/session/analytics.js +30 -0
  31. package/build/session/db.d.ts +11 -0
  32. package/build/session/db.js +33 -0
  33. package/build/session/extract.d.ts +224 -0
  34. package/build/session/extract.js +705 -62
  35. package/build/session/model-prices.json +429 -0
  36. package/build/session/pricing.d.ts +64 -0
  37. package/build/session/pricing.js +151 -0
  38. package/cli.bundle.mjs +177 -170
  39. package/configs/antigravity-cli/plugin.json +1 -1
  40. package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
  41. package/configs/gemini-cli/settings.json +11 -0
  42. package/hooks/codex/stop.mjs +91 -4
  43. package/hooks/gemini-cli/aftermodel.mjs +70 -0
  44. package/hooks/kimi/stop.mjs +74 -3
  45. package/hooks/qwen-code/platform.mjs +1 -0
  46. package/hooks/qwen-code/stop.mjs +168 -0
  47. package/hooks/session-db.bundle.mjs +7 -7
  48. package/hooks/session-extract.bundle.mjs +3 -2
  49. package/hooks/session-loaders.mjs +16 -1
  50. package/hooks/stop.mjs +35 -2
  51. package/openclaw.plugin.json +1 -1
  52. package/package.json +1 -1
  53. package/server.bundle.mjs +108 -101
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.166",
3
+ "version": "1.0.168",
4
4
  "description": "context-mode for Antigravity CLI (agy): sandboxed code execution, FTS5 knowledge base, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context-mode",
3
3
  "description": "context-mode for GitHub Copilot CLI: sandboxed code execution in 11 languages, an FTS5 knowledge base with BM25 ranking, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
4
- "version": "1.0.166",
4
+ "version": "1.0.168",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "context-window",
@@ -23,6 +23,17 @@
23
23
  ]
24
24
  }
25
25
  ],
26
+ "AfterModel": [
27
+ {
28
+ "matcher": "",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "context-mode hook gemini-cli aftermodel"
33
+ }
34
+ ]
35
+ }
36
+ ],
26
37
  "PreCompress": [
27
38
  {
28
39
  "matcher": "",
@@ -11,14 +11,70 @@ import "../ensure-deps.mjs";
11
11
  */
12
12
 
13
13
  import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, CODEX_OPTS } from "../session-helpers.mjs";
14
- import { createSessionLoaders } from "../session-loaders.mjs";
15
- import { dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
14
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
18
+ import { homedir } from "node:os";
17
19
 
18
20
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
19
- const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
21
+ const { loadSessionDB, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
20
22
  const OPTS = CODEX_OPTS;
21
23
 
24
+ /**
25
+ * Locate the codex rollout JSONL for this session. Codex persists turns at
26
+ * $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<session_id>.jsonl (default
27
+ * $CODEX_HOME = ~/.codex). The hook stdin does NOT carry the path, so we walk
28
+ * the sessions tree and match the filename suffix on the session id. Pure
29
+ * directory walk + string suffix test — NO regex. Best-effort: returns null on
30
+ * any failure so cost capture never blocks the turn.
31
+ */
32
+ function findCodexRollout(sessionId) {
33
+ try {
34
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
35
+ const home = process.env.CODEX_HOME || join(homedir(), ".codex");
36
+ const root = join(home, "sessions");
37
+ if (!existsSync(root)) return null;
38
+ const suffix = `${sessionId}.jsonl`;
39
+ const stack = [root];
40
+ while (stack.length > 0) {
41
+ const dir = stack.pop();
42
+ let entries;
43
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
44
+ for (const e of entries) {
45
+ const full = join(dir, e.name);
46
+ if (e.isDirectory()) { stack.push(full); continue; }
47
+ if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix)) {
48
+ return full;
49
+ }
50
+ }
51
+ }
52
+ } catch {
53
+ // ignore — best-effort
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Load the codex usage extractor (build/adapters/codex/usage.js), mirroring the
60
+ * loadModule build-path fallback in session-loaders. Returns null if the module
61
+ * is absent (e.g. pre-build dev tree) so the hook degrades gracefully.
62
+ */
63
+ async function loadCodexUsage() {
64
+ try {
65
+ const pluginRoot = join(HOOK_DIR, "..", "..");
66
+ const candidates = [
67
+ join(pluginRoot, "build", "adapters", "codex", "usage.js"),
68
+ ];
69
+ for (const p of candidates) {
70
+ if (existsSync(p)) return await import(pathToFileURL(p).href);
71
+ }
72
+ } catch {
73
+ // ignore
74
+ }
75
+ return null;
76
+ }
77
+
22
78
  try {
23
79
  const raw = await readStdin();
24
80
  const input = parseStdin(raw);
@@ -43,6 +99,37 @@ try {
43
99
  priority: 1,
44
100
  }, "Stop");
45
101
 
102
+ // ─── codex MAIN-turn cost capture (cursor-aware, no double-count) ─────────
103
+ // Codex carries no tokens on the hook payload (model only); per-turn usage is
104
+ // persisted to the session rollout JSONL as `event_msg`/`token_count`
105
+ // records. We tail that file, cursor-gated by rollout LINE INDEX, summing
106
+ // ONLY the completed turns NEW since the last Stop. Each step is best-effort
107
+ // — a hook must never block the session, so any read/extract failure here is
108
+ // swallowed without aborting the turn_end write above.
109
+ try {
110
+ const rolloutPath = findCodexRollout(sessionId);
111
+ if (rolloutPath) {
112
+ let rollout = null;
113
+ try { rollout = readFileSync(rolloutPath, "utf-8"); } catch { /* unreadable — skip */ }
114
+ if (rollout) {
115
+ const usageMod = await loadCodexUsage();
116
+ if (usageMod && typeof usageMod.extractCodexUsageSince === "function") {
117
+ const { resolveProjectAttributions } = await loadProjectAttribution();
118
+ const cursor = db.getUsageCursor(sessionId);
119
+ const { events, cursor: next } = usageMod.extractCodexUsageSince(rollout, cursor);
120
+ if (events.length > 0) {
121
+ // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
122
+ // platform (gated on ~/.context-mode/platform.json).
123
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
124
+ }
125
+ if (next) db.setUsageCursor(sessionId, next);
126
+ }
127
+ }
128
+ }
129
+ } catch {
130
+ // Best-effort cost capture — never block the session on failure.
131
+ }
132
+
46
133
  db.close();
47
134
  } catch {
48
135
  // Codex hooks must not block the session.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import "../suppress-stderr.mjs";
3
+ import "../ensure-deps.mjs";
4
+ /**
5
+ * Gemini CLI AfterModel hook — per-turn token / cost capture.
6
+ *
7
+ * AfterModel fires per model call inside gemini-cli's stream loop
8
+ * (packages/core/src/core/geminiChat.ts:1213). The hook payload carries
9
+ * `llm_request` + `llm_response` (hooks/types.ts:692-695); the real Gemini
10
+ * `usageMetadata` (promptTokenCount, candidatesTokenCount, +cachedContent/
11
+ * thoughts when present) and resolved model live on `llm_response`
12
+ * (hookTranslator.ts:60-64, loggingContentGenerator.ts:405,553).
13
+ *
14
+ * parseGeminiUsage maps that into a builder `agent_usage` event; cost_usd is
15
+ * derived from the pricing catalog (gemini exposes no native cost). The event
16
+ * is forwarded via attributeAndInsertEvents — same path as hooks/stop.mjs.
17
+ *
18
+ * One AfterModel call = one billed model round-trip = one priced event.
19
+ * Per-userPromptId summation into a single per-turn total is deferred; emitting
20
+ * per-call never double-counts (each call's usageMetadata is authoritative).
21
+ *
22
+ * Must be fast (<20ms). No network, no LLM, just a parse + SQLite write.
23
+ * AfterModel is non-blocking — no stdout output.
24
+ */
25
+
26
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, GEMINI_OPTS } from "../session-helpers.mjs";
27
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
28
+ import { appendFileSync } from "node:fs";
29
+ import { join, dirname } from "node:path";
30
+ import { homedir } from "node:os";
31
+ import { fileURLToPath } from "node:url";
32
+
33
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
34
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
35
+ const OPTS = GEMINI_OPTS;
36
+ const DEBUG_LOG = join(homedir(), ".gemini", "context-mode", "aftermodel-debug.log");
37
+
38
+ try {
39
+ const raw = await readStdin();
40
+ const input = parseStdin(raw);
41
+ const projectDir = getInputProjectDir(input, OPTS);
42
+
43
+ const { parseGeminiUsage } = await loadExtract();
44
+ const event = parseGeminiUsage(input);
45
+
46
+ if (event) {
47
+ const { resolveProjectAttributions } = await loadProjectAttribution();
48
+ const { SessionDB } = await loadSessionDB();
49
+
50
+ const dbPath = getSessionDBPath(OPTS, projectDir);
51
+ const db = new SessionDB({ dbPath });
52
+ const sessionId = getSessionId(input, OPTS);
53
+ db.ensureSession(sessionId, projectDir);
54
+
55
+ // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
56
+ // platform (gated on ~/.context-mode/platform.json) — same as stop.mjs.
57
+ attributeAndInsertEvents(db, sessionId, [event], input, projectDir, "AfterModel", resolveProjectAttributions);
58
+ db.close();
59
+
60
+ try {
61
+ appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] OK: ${event.model_id ?? "?"} in:${event.input_tokens ?? 0} out:${event.output_tokens ?? 0}\n`);
62
+ } catch { /* silent */ }
63
+ }
64
+ } catch (err) {
65
+ try {
66
+ appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ERR: ${err?.message || err}\n`);
67
+ } catch { /* silent */ }
68
+ }
69
+
70
+ // AfterModel is non-blocking — no stdout output
@@ -16,14 +16,53 @@ import "../ensure-deps.mjs";
16
16
  */
17
17
 
18
18
  import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
19
- import { createSessionLoaders } from "../session-loaders.mjs";
20
- import { dirname } from "node:path";
19
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
20
+ import { dirname, join, resolve } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
+ import { homedir } from "node:os";
23
+ import { existsSync, readFileSync } from "node:fs";
22
24
 
23
25
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
24
- const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
26
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
25
27
  const OPTS = KIMI_OPTS;
26
28
 
29
+ /**
30
+ * Resolve <kimiConfigDir> (mirrors src/adapters/kimi/paths.ts:resolveKimiConfigDir
31
+ * — hooks cannot import the TS module, so the logic is duplicated here).
32
+ */
33
+ function resolveKimiConfigDir() {
34
+ const envVal = process.env.KIMI_CODE_HOME;
35
+ if (envVal) {
36
+ if (envVal.startsWith("~")) return resolve(homedir(), envVal.replace(/^~[/\\]?/, ""));
37
+ return resolve(envVal);
38
+ }
39
+ return resolve(homedir(), ".kimi-code");
40
+ }
41
+
42
+ /**
43
+ * Best-effort <sessionDir>/wire.jsonl resolution keyed by session id.
44
+ *
45
+ * NOTE / WIRE GAP: adapter-matrix/kimi.md confirms usage lands at
46
+ * <sessionDir>/wire.jsonl (agent/index.ts:142), but the exact session_id ->
47
+ * sessionDir directory layout is not carried in the hook stdin payload and the
48
+ * kimi-code refs are not checked out in this worktree to pin it. We probe the
49
+ * documented candidate layouts and return the first whose wire.jsonl exists,
50
+ * else null -> the cost block degrades to a no-op rather than guessing wrong.
51
+ */
52
+ function resolveKimiWireJsonlPath(sessionId) {
53
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
54
+ const configDir = resolveKimiConfigDir();
55
+ const candidates = [
56
+ join(configDir, "sessions", sessionId, "wire.jsonl"),
57
+ join(configDir, "agents", sessionId, "wire.jsonl"),
58
+ join(configDir, sessionId, "wire.jsonl"),
59
+ ];
60
+ for (const candidate of candidates) {
61
+ try { if (existsSync(candidate)) return candidate; } catch { /* try next */ }
62
+ }
63
+ return null;
64
+ }
65
+
27
66
  try {
28
67
  const raw = await readStdin();
29
68
  const input = parseStdin(raw);
@@ -53,6 +92,38 @@ try {
53
92
  priority: 1,
54
93
  }, "Stop");
55
94
 
95
+ // ─── kimi-code per-turn cost capture (cursor-gated, no double-count) ───
96
+ // Usage lives ONLY on <sessionDir>/wire.jsonl `usage.record` lines, never in
97
+ // hook stdin (adapter-matrix/kimi.md). Tail the file, sum NEW delta lines
98
+ // since a per-session high-water cursor, and forward. Best-effort — a missing
99
+ // wire file or read/extract failure must never block the turn_end write or
100
+ // the session, so the whole block is wrapped and swallowed.
101
+ try {
102
+ const wirePath = resolveKimiWireJsonlPath(sessionId);
103
+ if (wirePath) {
104
+ let wireText = null;
105
+ try {
106
+ wireText = readFileSync(wirePath, "utf-8");
107
+ } catch {
108
+ // unreadable/missing wire.jsonl — skip capture this turn.
109
+ }
110
+ if (wireText) {
111
+ const { extractKimiUsageSince } = await loadExtract();
112
+ const { resolveProjectAttributions } = await loadProjectAttribution();
113
+ const cursor = db.getUsageCursor(sessionId);
114
+ const { events, cursor: next } = extractKimiUsageSince(wireText, cursor);
115
+ if (events.length > 0) {
116
+ // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
117
+ // platform (gated on ~/.context-mode/platform.json).
118
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
119
+ }
120
+ if (next) db.setUsageCursor(sessionId, next);
121
+ }
122
+ }
123
+ } catch {
124
+ // Best-effort cost capture — never block the session on failure.
125
+ }
126
+
56
127
  db.close();
57
128
  } catch {
58
129
  // Kimi Code hooks must not block the session.
@@ -0,0 +1 @@
1
+ process.env.CONTEXT_MODE_PLATFORM = "qwen-code";
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Qwen Code Stop hook — record turn-end state + capture per-turn token cost.
7
+ *
8
+ * Stop fires at the end of an assistant turn, not at true session shutdown, so
9
+ * we record a `turn_end` marker (session_end stays reserved for a genuine
10
+ * terminal lifecycle event) and tail the session JSONL for new usage.
11
+ *
12
+ * COST CAPTURE: qwen-code's hook stdin carries tool I/O ONLY — token usage is
13
+ * unreachable through the hook stream (matrix §4). The single live capture path
14
+ * is a cursor-gated tail of the session record file at
15
+ * ~/.qwen/tmp/<project_id>/chats/<sessionId>.jsonl
16
+ * where <project_id> = sha256(projectRoot) hex
17
+ * (refs chatRecordingService.ts:451 + storage.ts:316-320 getProjectTempDir;
18
+ * paths.ts:262 getProjectHash). The extractor lives in
19
+ * src/adapters/qwen-code/usage.ts and is re-exported through the
20
+ * session-extract bundle (src/session/extract.ts), reachable here via
21
+ * loadExtract() — exactly the kimi wire.jsonl pattern.
22
+ */
23
+
24
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "../session-helpers.mjs";
25
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
26
+ import { dirname, join, resolve } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { homedir, platform } from "node:os";
29
+ import { createHash } from "node:crypto";
30
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
31
+
32
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
33
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
34
+
35
+ // Qwen Code session options. Mirrors the QwenCodeAdapter config (index.ts):
36
+ // config dir ~/.qwen, project dir via QWEN_PROJECT_DIR (no config-dir env var,
37
+ // no session-id env var — session_id arrives on the hook stdin). Kept local
38
+ // because session-helpers.mjs ships no QWEN_OPTS export.
39
+ const QWEN_OPTS = {
40
+ configDir: ".qwen",
41
+ configDirEnv: undefined,
42
+ projectDirEnv: "QWEN_PROJECT_DIR",
43
+ sessionIdEnv: undefined,
44
+ };
45
+
46
+ /**
47
+ * Resolve the qwen config dir (~/.qwen). Qwen Code exposes no documented
48
+ * config-dir override env var (unlike CODEX_HOME / KIMI_CODE_HOME), so this is
49
+ * a fixed home-rooted path. NO regex.
50
+ */
51
+ function resolveQwenHome() {
52
+ return resolve(homedir(), ".qwen");
53
+ }
54
+
55
+ /**
56
+ * EXACT port of qwen's getProjectHash (paths.ts:262): sha256 hex of the project
57
+ * root, lowercased first on win32 for case-insensitive FS parity. The TS twin
58
+ * is `qwenProjectHash` in src/adapters/qwen-code/usage.ts (unit-tested); the
59
+ * hook cannot import that TS at runtime, so the logic is duplicated here — same
60
+ * precedent as kimi's resolveKimiConfigDir. NO regex.
61
+ */
62
+ function qwenProjectHash(projectRoot) {
63
+ const normalized = platform() === "win32" ? String(projectRoot).toLowerCase() : String(projectRoot);
64
+ return createHash("sha256").update(normalized).digest("hex");
65
+ }
66
+
67
+ /**
68
+ * Resolve <qwenHome>/tmp/<sha256(projectRoot)>/chats/<sessionId>.jsonl.
69
+ *
70
+ * Primary path is fully deterministic from the hashing scheme above. Should the
71
+ * hash diverge (e.g. an upstream qwen normalization we did not pin), we fall
72
+ * back to a glob of every <qwenHome>/tmp/<*>/chats dir for <sessionId>.jsonl —
73
+ * a pure directory walk + filename equality test, NO regex. Best-effort:
74
+ * returns null on any failure so cost capture never blocks the turn.
75
+ */
76
+ function resolveQwenChatJsonlPath(projectDir, sessionId) {
77
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
78
+ const qwenHome = resolveQwenHome();
79
+ const fileName = `${sessionId}.jsonl`;
80
+
81
+ // 1) Canonical hashed path.
82
+ try {
83
+ if (typeof projectDir === "string" && projectDir.length > 0) {
84
+ const direct = join(qwenHome, "tmp", qwenProjectHash(projectDir), "chats", fileName);
85
+ if (existsSync(direct)) return direct;
86
+ }
87
+ } catch { /* fall through to glob */ }
88
+
89
+ // 2) Glob fallback — scan every tmp/<hash>/chats for this session file.
90
+ try {
91
+ const tmpRoot = join(qwenHome, "tmp");
92
+ if (!existsSync(tmpRoot)) return null;
93
+ let projectDirs;
94
+ try { projectDirs = readdirSync(tmpRoot, { withFileTypes: true }); } catch { return null; }
95
+ for (const entry of projectDirs) {
96
+ if (!entry.isDirectory()) continue;
97
+ const candidate = join(tmpRoot, entry.name, "chats", fileName);
98
+ try { if (existsSync(candidate)) return candidate; } catch { /* try next */ }
99
+ }
100
+ } catch { /* best-effort */ }
101
+
102
+ return null;
103
+ }
104
+
105
+ try {
106
+ const raw = await readStdin();
107
+ const input = parseStdin(raw);
108
+ const projectDir = getInputProjectDir(input, QWEN_OPTS);
109
+
110
+ const { SessionDB } = await loadSessionDB();
111
+ const dbPath = getSessionDBPath(QWEN_OPTS, projectDir);
112
+ const db = new SessionDB({ dbPath });
113
+ const sessionId = getSessionId(input, QWEN_OPTS);
114
+
115
+ db.ensureSession(sessionId, projectDir);
116
+ // SessionEvent contract requires {type, category, data, priority}. insertEvent
117
+ // hashes `data` for the dedup key, so encode the turn snapshot into `data`.
118
+ const payload = {
119
+ stop_hook_active: input.stop_hook_active ?? false,
120
+ last_assistant_message: typeof input.last_assistant_message === "string"
121
+ ? input.last_assistant_message.slice(0, 2000)
122
+ : null,
123
+ };
124
+ db.insertEvent(sessionId, {
125
+ type: "turn_end",
126
+ category: "session",
127
+ data: JSON.stringify(payload),
128
+ priority: 1,
129
+ }, "Stop");
130
+
131
+ // ─── qwen-code per-turn cost capture (cursor-gated, no double-count) ───
132
+ // Usage lives ONLY on the session chats JSONL ChatRecords, never in hook
133
+ // stdin. Tail the file, sum NEW usage records since a per-session high-water
134
+ // cursor, and forward. Best-effort — a missing file or read/extract failure
135
+ // must never block the turn_end write or the session, so the whole block is
136
+ // wrapped and swallowed.
137
+ try {
138
+ const jsonlPath = resolveQwenChatJsonlPath(projectDir, sessionId);
139
+ if (jsonlPath) {
140
+ let jsonlText = null;
141
+ try {
142
+ jsonlText = readFileSync(jsonlPath, "utf-8");
143
+ } catch {
144
+ // unreadable/missing chats JSONL — skip capture this turn.
145
+ }
146
+ if (jsonlText) {
147
+ const { extractQwenUsageSince } = await loadExtract();
148
+ const { resolveProjectAttributions } = await loadProjectAttribution();
149
+ const cursor = db.getUsageCursor(sessionId);
150
+ const { events, cursor: next } = extractQwenUsageSince(jsonlText, cursor);
151
+ if (events.length > 0) {
152
+ // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
153
+ // platform (gated on ~/.context-mode/platform.json).
154
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
155
+ }
156
+ if (next) db.setUsageCursor(sessionId, next);
157
+ }
158
+ }
159
+ } catch {
160
+ // Best-effort cost capture — never block the session on failure.
161
+ }
162
+
163
+ db.close();
164
+ } catch {
165
+ // Qwen Code hooks must not block the session.
166
+ }
167
+
168
+ process.stdout.write("{}\n");
@@ -1,6 +1,6 @@
1
- import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync as P,renameSync as ce}from"node:fs";import{tmpdir as ue}from"node:os";import{join as de}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},m=null;function le(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function Ee(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,a]=(r.node??"0.0.0").split("."),i=Number(s),c=Number(a);return!Number.isFinite(i)||!Number.isFinite(c)?!1:i>22||i===22&&c>=5}function ge(){if(!m){let n=ie(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;m=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(Ee()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&le(e)?m=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:m=n("better-sqlite3")}else m=n("better-sqlite3")}return m}function F(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function k(n){if(!ae(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function me(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function x(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function B(n="context-mode"){return de(ue(),`${n}-${process.pid}.db`)}function _e(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function pe(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function ye(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ce(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var S=Symbol.for("__context_mode_live_dbs_v3__"),O=(()=>{let n=globalThis;return n[S]||(n[S]=new Set,process.on("exit",()=>{for(let e of n[S])x(e);n[S].clear()})),n[S]})(),R=class{#e;#t;constructor(e){let t=ge();this.#e=e,k(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(pe(a)){ye(e),k(e);try{r=new t(e,{timeout:3e4}),F(r)}catch(i){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${i instanceof Error?i.message:String(i)}`)}}else throw s}this.#t=r,O.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){O.delete(this.#t),x(this.#t)}withRetry(e){return _e(e)}cleanup(){O.delete(this.#t),x(this.#t),me(this.#e)}};import{createHash as f}from"node:crypto";import{execFileSync as Se}from"node:child_process";import{accessSync as fe,constants as he,existsSync as L,mkdirSync as Te,realpathSync as ve,renameSync as I}from"node:fs";import{homedir as q}from"node:os";import{dirname as Re,isAbsolute as G,join as E,resolve as p}from"node:path";var l="CONTEXT_MODE_DIR",Y="sessions",j="content",h=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Ce(e,t,i),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=i.ignoredEnvVar,this.ignoredReason=i.ignoredReason}},D=new Map;function Ge(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):E(be(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function be(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,q())}function V(n,e){return n.startsWith("~")?p(q(),n.replace(/^~[/\\]?/,"")):G(n)?p(n):e?p(e,n):p(n)}function De(n,e,t){return new h(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
2
- `))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw De(n,t,`${l} must be an absolute path.`);return{kind:"override",root:p(t)}}function Le(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:E(t.root,e),envVar:l,source:"override"}}function Ne(n,e,t){return{kind:n,path:p(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:E(e.root,Y),envVar:l,source:"override"}:Ne("session",n,Le(e))}function Ye(n){let e=z("content",j);if(e)return e;let t=Q(n);return{kind:"content",path:E(Re(t.path),j),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){let e=z("stats",Y);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function ze(n){return n.message}function Qe(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Je(){D.clear()}function Ze(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=D.get(e);if(t instanceof h)throw t;if(t===n.path)return t;try{return Te(n.path,{recursive:!0}),fe(n.path,he.W_OK),D.set(e,n.path),n.path}catch(r){let s=new h(n.kind,Ae(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw D.set(e,s),s}}function Ce(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
3
- `)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ae(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var _;function g(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function H(n){let e=n;try{e=ve.native(n)}catch{}let t=g(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return Se("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function we(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?g(e):null}function xe(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?g(e):null}function Ie(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(_&&_.projectDir===n&&_.envSuffix===e)return _.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=we(n),s=xe(n);if(r&&s){let a=H(r),i=H(s);a!==i&&(t=`__${f("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return _={projectDir:n,envSuffix:e,suffix:t},t}function et(){_=void 0}function ee(n){return f("sha256").update(g(n)).digest("hex").slice(0,16)}function te(n){let e=g(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return f("sha256").update(t).digest("hex").slice(0,16)}function tt(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=E(t,`${r}.db`);if(L(s))return s;let a=ee(e);if(a===r)return s;let i=E(t,`${a}.db`);if(L(i))try{I(i,s);for(let c of["-wal","-shm"])try{I(i+c,s+c)}catch{}}catch{}return s}function nt(n){return Me({...n,ext:".db"})}function Me(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??Ie(e),a=te(e),i=E(t,`${a}${s}${r}`);if(L(i))return i;let c=ee(e);if(c===a)return i;let d=E(t,`${c}${s}${r}`);if(L(d))try{I(d,i)}catch{}return i}var W=1e3,X=5;function b(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",getLatestCommitMessage:"getLatestCommitMessage",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool",getEventBytesSummary:"getEventBytesSummary"},Ue=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Ue)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${a}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function rt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends R{constructor(e){super(e?.dbPath??B("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=this.db.pragma("table_xinfo(session_events)").find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
1
+ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync as P,renameSync as ce}from"node:fs";import{tmpdir as ue}from"node:os";import{join as de}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},m=null;function le(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function ge(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,a]=(r.node??"0.0.0").split("."),i=Number(s),c=Number(a);return!Number.isFinite(i)||!Number.isFinite(c)?!1:i>22||i===22&&c>=5}function Ee(){if(!m){let n=ie(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;m=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(ge()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&le(e)?m=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:m=n("better-sqlite3")}else m=n("better-sqlite3")}return m}function F(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function k(n){if(!ae(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function me(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function x(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function B(n="context-mode"){return de(ue(),`${n}-${process.pid}.db`)}function _e(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function pe(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function ye(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ce(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var S=Symbol.for("__context_mode_live_dbs_v3__"),O=(()=>{let n=globalThis;return n[S]||(n[S]=new Set,process.on("exit",()=>{for(let e of n[S])x(e);n[S].clear()})),n[S]})(),R=class{#e;#t;constructor(e){let t=Ee();this.#e=e,k(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(pe(a)){ye(e),k(e);try{r=new t(e,{timeout:3e4}),F(r)}catch(i){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${i instanceof Error?i.message:String(i)}`)}}else throw s}this.#t=r,O.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){O.delete(this.#t),x(this.#t)}withRetry(e){return _e(e)}cleanup(){O.delete(this.#t),x(this.#t),me(this.#e)}};import{createHash as f}from"node:crypto";import{execFileSync as Se}from"node:child_process";import{accessSync as fe,constants as Te,existsSync as L,mkdirSync as he,realpathSync as ve,renameSync as I}from"node:fs";import{homedir as q}from"node:os";import{dirname as Re,isAbsolute as G,join as g,resolve as p}from"node:path";var l="CONTEXT_MODE_DIR",Y="sessions",j="content",T=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Ce(e,t,i),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=i.ignoredEnvVar,this.ignoredReason=i.ignoredReason}},D=new Map;function Ge(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):g(be(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function be(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,q())}function V(n,e){return n.startsWith("~")?p(q(),n.replace(/^~[/\\]?/,"")):G(n)?p(n):e?p(e,n):p(n)}function De(n,e,t){return new T(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
2
+ `))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw De(n,t,`${l} must be an absolute path.`);return{kind:"override",root:p(t)}}function Le(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:g(t.root,e),envVar:l,source:"override"}}function Ne(n,e,t){return{kind:n,path:p(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:g(e.root,Y),envVar:l,source:"override"}:Ne("session",n,Le(e))}function Ye(n){let e=z("content",j);if(e)return e;let t=Q(n);return{kind:"content",path:g(Re(t.path),j),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){let e=z("stats",Y);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function ze(n){return n.message}function Qe(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Je(){D.clear()}function Ze(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=D.get(e);if(t instanceof T)throw t;if(t===n.path)return t;try{return he(n.path,{recursive:!0}),fe(n.path,Te.W_OK),D.set(e,n.path),n.path}catch(r){let s=new T(n.kind,Ae(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw D.set(e,s),s}}function Ce(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
3
+ `)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ae(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var _;function E(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function H(n){let e=n;try{e=ve.native(n)}catch{}let t=E(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return Se("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function we(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?E(e):null}function xe(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?E(e):null}function Ie(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(_&&_.projectDir===n&&_.envSuffix===e)return _.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=we(n),s=xe(n);if(r&&s){let a=H(r),i=H(s);a!==i&&(t=`__${f("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return _={projectDir:n,envSuffix:e,suffix:t},t}function et(){_=void 0}function ee(n){return f("sha256").update(E(n)).digest("hex").slice(0,16)}function te(n){let e=E(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return f("sha256").update(t).digest("hex").slice(0,16)}function tt(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=g(t,`${r}.db`);if(L(s))return s;let a=ee(e);if(a===r)return s;let i=g(t,`${a}.db`);if(L(i))try{I(i,s);for(let c of["-wal","-shm"])try{I(i+c,s+c)}catch{}}catch{}return s}function nt(n){return Ue({...n,ext:".db"})}function Ue(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??Ie(e),a=te(e),i=g(t,`${a}${s}${r}`);if(L(i))return i;let c=ee(e);if(c===a)return i;let d=g(t,`${c}${s}${r}`);if(L(d))try{I(d,i)}catch{}return i}var W=1e3,X=5;function b(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",getLatestCommitMessage:"getLatestCommitMessage",incrementCompactCount:"incrementCompactCount",getUsageCursor:"getUsageCursor",setUsageCursor:"setUsageCursor",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool",getEventBytesSummary:"getEventBytesSummary"},Me=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Me)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${a}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function rt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends R{constructor(e){super(e?.dbPath??B("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=this.db.pragma("table_xinfo(session_events)").find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
4
4
  CREATE TABLE IF NOT EXISTS session_events (
5
5
  id INTEGER PRIMARY KEY AUTOINCREMENT,
6
6
  session_id TEXT NOT NULL,
@@ -50,7 +50,7 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
50
50
  );
51
51
 
52
52
  CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
53
- `);try{ne(this.db)}catch{}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
53
+ `);try{ne(this.db)}catch{}try{this.db.pragma("table_xinfo(session_meta)").some(t=>t.name==="usage_cursor")||this.db.exec("ALTER TABLE session_meta ADD COLUMN usage_cursor TEXT")}catch{}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
54
54
  session_id, type, category, priority, data,
55
55
  project_dir, attribution_source, attribution_confidence,
56
56
  bytes_avoided, bytes_returned,
@@ -108,7 +108,7 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
108
108
  FROM session_events
109
109
  WHERE session_id = ? AND type = 'git_commit'
110
110
  ORDER BY id DESC
111
- LIMIT 1`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
111
+ LIMIT 1`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.getUsageCursor,"SELECT usage_cursor FROM session_meta WHERE session_id = ?"),e(o.setUsageCursor,"UPDATE session_meta SET usage_cursor = ? WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
112
112
  VALUES (?, ?, ?)
113
113
  ON CONFLICT(session_id) DO UPDATE SET
114
114
  snapshot = excluded.snapshot,
@@ -139,6 +139,6 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
139
139
  FROM tool_calls WHERE session_id = ?`),e(o.getToolCallByTool,`SELECT tool, calls, bytes_returned
140
140
  FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),e(o.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
141
141
  COALESCE(SUM(bytes_returned), 0) AS bytes_returned
142
- FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=f("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),T=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,y=b(a?.bytesAvoided),v=b(a?.bytesReturned),N=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=W&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,T,y,v,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>N())}bulkInsertEvents(e,t,r="PostToolUse",s,a){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],a?.[0]);return}let i=t.map((d,u)=>{let T=f("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),y=s?.[u],v=String(y?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),N=v===""?"":g(v),M=String(y?.source??d.attribution_source??"unknown"),C=Number(y?.confidence??d.attribution_confidence??0),re=Number.isFinite(C)?Math.max(0,Math.min(1,C)):0,U=a?.[u],se=b(U?.bytesAvoided),oe=b(U?.bytesReturned);return{event:d,dataHash:T,projectDir:N,attributionSource:M,attributionConfidence:re,bytesAvoided:se,bytesReturned:oe}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=W?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,r,u.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>c())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,a=t?.minPriority;return s&&a!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,a,r):s?this.stmt(o.getEventsByType).all(e,s,r):a!==void 0?this.stmt(o.getEventsByPriority).all(e,a,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch{return""}}searchEvents(e,t,r,s){try{let a=e.replace(/[%_]/g,c=>"\\"+c),i=s??null;return this.stmt(o.searchEvents).all(r,a,a,i,i,t)}catch{return[]}}getSessionIdsForProject(e){try{let t=g(e);return this.db.prepare(`SELECT DISTINCT session_id
142
+ FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=f("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),h=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,y=b(a?.bytesAvoided),v=b(a?.bytesReturned),N=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=W&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,h,y,v,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>N())}bulkInsertEvents(e,t,r="PostToolUse",s,a){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],a?.[0]);return}let i=t.map((d,u)=>{let h=f("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),y=s?.[u],v=String(y?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),N=v===""?"":E(v),U=String(y?.source??d.attribution_source??"unknown"),C=Number(y?.confidence??d.attribution_confidence??0),re=Number.isFinite(C)?Math.max(0,Math.min(1,C)):0,M=a?.[u],se=b(M?.bytesAvoided),oe=b(M?.bytesReturned);return{event:d,dataHash:h,projectDir:N,attributionSource:U,attributionConfidence:re,bytesAvoided:se,bytesReturned:oe}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=W?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,r,u.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>c())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,a=t?.minPriority;return s&&a!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,a,r):s?this.stmt(o.getEventsByType).all(e,s,r):a!==void 0?this.stmt(o.getEventsByPriority).all(e,a,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch{return""}}searchEvents(e,t,r,s){try{let a=e.replace(/[%_]/g,c=>"\\"+c),i=s??null;return this.stmt(o.searchEvents).all(r,a,a,i,i,t)}catch{return[]}}getSessionIdsForProject(e){try{let t=E(e);return this.db.prepare(`SELECT DISTINCT session_id
143
143
  FROM session_events
144
- WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),a=this.getSessionStats(e),i=(t?.tool_calls??0)>0?t?.unique_files??0:0,c=t?.errors??0,d=Math.min(i,c);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:a?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,h as StorageDirectoryError,et as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Je as clearStorageDirectoryCheckCacheForTests,Qe as describeStorageDirectorySource,rt as ensureSessionEventsSchema,Ze as ensureWritableStorageDir,ze as formatStorageDirectoryError,Ie as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,g as normalizeWorktreePath,Ye as resolveContentStorageDir,tt as resolveContentStorePath,Ge as resolveDefaultSessionDir,nt as resolveSessionDbPath,Me as resolveSessionPath,Q as resolveSessionStorageDir,Ke as resolveStatsStorageDir};
144
+ WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),a=this.getSessionStats(e),i=(t?.tool_calls??0)>0?t?.unique_files??0:0,c=t?.errors??0,d=Math.min(i,c);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:a?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}getUsageCursor(e){return this.stmt(o.getUsageCursor).get(e)?.usage_cursor??null}setUsageCursor(e,t){this.stmt(o.setUsageCursor).run(t,e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,T as StorageDirectoryError,et as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Je as clearStorageDirectoryCheckCacheForTests,Qe as describeStorageDirectorySource,rt as ensureSessionEventsSchema,Ze as ensureWritableStorageDir,ze as formatStorageDirectoryError,Ie as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,E as normalizeWorktreePath,Ye as resolveContentStorageDir,tt as resolveContentStorePath,Ge as resolveDefaultSessionDir,nt as resolveSessionDbPath,Ue as resolveSessionPath,Q as resolveSessionStorageDir,Ke as resolveStatsStorageDir};