context-mode 1.0.111 → 1.0.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/index.ts +3 -2
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +152 -34
- package/bin/statusline.mjs +144 -127
- package/build/adapters/base.d.ts +8 -5
- package/build/adapters/base.js +8 -18
- package/build/adapters/claude-code/index.d.ts +24 -3
- package/build/adapters/claude-code/index.js +44 -11
- package/build/adapters/codex/hooks.d.ts +10 -5
- package/build/adapters/codex/hooks.js +10 -5
- package/build/adapters/codex/index.d.ts +17 -5
- package/build/adapters/codex/index.js +337 -37
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/cursor/index.d.ts +6 -0
- package/build/adapters/cursor/index.js +83 -2
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +29 -6
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +75 -0
- package/build/adapters/omp/plugin.js +220 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +198 -0
- package/build/adapters/openclaw/plugin.d.ts +130 -0
- package/build/adapters/openclaw/plugin.js +629 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/plugin.d.ts +145 -0
- package/build/adapters/opencode/plugin.js +457 -0
- package/build/adapters/pi/extension.d.ts +26 -0
- package/build/adapters/pi/extension.js +552 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +113 -0
- package/build/adapters/pi/mcp-bridge.js +251 -0
- package/build/adapters/types.d.ts +11 -6
- package/build/cli.js +186 -170
- package/build/db-base.d.ts +15 -2
- package/build/db-base.js +50 -5
- package/build/executor.d.ts +2 -0
- package/build/executor.js +15 -2
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.js +64 -5
- package/build/search/auto-memory.js +6 -4
- package/build/security.js +30 -10
- package/build/server.d.ts +23 -1
- package/build/server.js +652 -174
- package/build/session/analytics.d.ts +404 -1
- package/build/session/analytics.js +1347 -42
- package/build/session/db.d.ts +114 -5
- package/build/session/db.js +275 -27
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +1 -0
- package/build/session/extract.js +79 -12
- package/build/session/purge.d.ts +111 -0
- package/build/session/purge.js +138 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +69 -6
- package/build/util/claude-config.d.ts +26 -0
- package/build/util/claude-config.js +91 -0
- package/build/util/hook-config.d.ts +4 -0
- package/build/util/hook-config.js +39 -0
- package/cli.bundle.mjs +411 -208
- package/configs/antigravity/GEMINI.md +0 -3
- package/configs/claude-code/CLAUDE.md +1 -4
- package/configs/codex/AGENTS.md +1 -4
- package/configs/codex/config.toml +3 -0
- package/configs/codex/hooks.json +8 -0
- package/configs/cursor/context-mode.mdc +0 -3
- package/configs/gemini-cli/GEMINI.md +0 -3
- package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
- package/configs/kilo/AGENTS.md +0 -3
- package/configs/kiro/KIRO.md +0 -3
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +0 -3
- package/configs/opencode/AGENTS.md +0 -3
- package/configs/pi/AGENTS.md +0 -3
- package/configs/qwen-code/QWEN.md +1 -4
- package/configs/vscode-copilot/copilot-instructions.md +0 -3
- package/configs/zed/AGENTS.md +0 -3
- package/hooks/codex/posttooluse.mjs +9 -2
- package/hooks/codex/precompact.mjs +69 -0
- package/hooks/codex/sessionstart.mjs +13 -9
- package/hooks/codex/stop.mjs +1 -2
- package/hooks/codex/userpromptsubmit.mjs +1 -2
- package/hooks/core/routing.mjs +237 -18
- package/hooks/cursor/afteragentresponse.mjs +1 -1
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +1 -1
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +1 -1
- package/hooks/ensure-deps.mjs +12 -13
- package/hooks/gemini-cli/aftertool.mjs +1 -1
- package/hooks/gemini-cli/beforeagent.mjs +1 -1
- package/hooks/gemini-cli/precompress.mjs +3 -2
- package/hooks/gemini-cli/sessionstart.mjs +9 -9
- package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
- package/hooks/jetbrains-copilot/precompact.mjs +3 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
- package/hooks/kiro/agentspawn.mjs +5 -5
- package/hooks/kiro/posttooluse.mjs +2 -2
- package/hooks/kiro/userpromptsubmit.mjs +1 -1
- package/hooks/posttooluse.mjs +45 -0
- package/hooks/precompact.mjs +17 -0
- package/hooks/pretooluse.mjs +23 -0
- package/hooks/routing-block.mjs +0 -12
- package/hooks/run-hook.mjs +16 -3
- package/hooks/session-db.bundle.mjs +27 -18
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +101 -64
- package/hooks/sessionstart.mjs +51 -2
- package/hooks/vscode-copilot/posttooluse.mjs +1 -1
- package/hooks/vscode-copilot/precompact.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +14 -8
- package/server.bundle.mjs +349 -147
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/context-mode-ops/SKILL.md +0 -299
- package/skills/context-mode-ops/agent-teams.md +0 -198
- package/skills/context-mode-ops/communication.md +0 -224
- package/skills/context-mode-ops/marketing.md +0 -124
- package/skills/context-mode-ops/release.md +0 -214
- package/skills/context-mode-ops/review-pr.md +0 -269
- package/skills/context-mode-ops/tdd.md +0 -329
- package/skills/context-mode-ops/triage-issue.md +0 -266
- package/skills/context-mode-ops/validation.md +0 -307
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- package/skills/tdd/tests.md +0 -61
package/build/session/db.d.ts
CHANGED
|
@@ -8,8 +8,93 @@
|
|
|
8
8
|
import { SQLiteBase } from "../db-base.js";
|
|
9
9
|
import type { SessionEvent } from "../types.js";
|
|
10
10
|
import type { ProjectAttribution } from "./project-attribution.js";
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function normalizeWorktreePath(path: string): string;
|
|
12
|
+
export declare function getWorktreeSuffix(projectDir?: string): string;
|
|
12
13
|
export declare function _resetWorktreeSuffixCacheForTests(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Hash a project directory the way the deployed code (≤ v1.0.111) did:
|
|
16
|
+
* normalize slashes only, preserve raw casing. Kept exported so the
|
|
17
|
+
* migration helper can locate pre-fix DB files for one-shot rename.
|
|
18
|
+
*
|
|
19
|
+
* Do NOT call this for new code paths — use {@link hashProjectDirCanonical}.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hashProjectDirLegacy(projectDir: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Hash a project directory case-stably. On case-insensitive filesystems
|
|
24
|
+
* (macOS HFS+/APFS, Windows NTFS) the path is lowercased so that
|
|
25
|
+
* `/Users/Mert/proj` and `/users/mert/proj` resolve to the same DB file.
|
|
26
|
+
* On Linux (case-sensitive) casing is preserved.
|
|
27
|
+
*
|
|
28
|
+
* Used as the base half of the SessionDB filename:
|
|
29
|
+
* <baseHash><worktreeSuffix>.db
|
|
30
|
+
*/
|
|
31
|
+
export declare function hashProjectDirCanonical(projectDir: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the per-project FTS5 content store DB path, performing a one-shot
|
|
34
|
+
* migration from a legacy raw-casing filename to the canonical one when only
|
|
35
|
+
* the legacy file (with optional `-wal` / `-shm` SQLite sidecars) exists.
|
|
36
|
+
*
|
|
37
|
+
* Same dual-hash safety contract as {@link resolveSessionDbPath}:
|
|
38
|
+
* - Linux: canonical hash equals legacy hash → no migration attempted.
|
|
39
|
+
* - Mac/Win: rename legacy → canonical when canonical missing.
|
|
40
|
+
* - Both exist: leave legacy alone (data-loss safety). Caller picks
|
|
41
|
+
* canonical; reconciliation is a manual operation.
|
|
42
|
+
*
|
|
43
|
+
* Differs from `resolveSessionDbPath` in two ways:
|
|
44
|
+
* 1. No worktree suffix — the FTS5 store is per-project, not per-worktree.
|
|
45
|
+
* 2. The `-wal` / `-shm` sidecars travel with the main `.db` during
|
|
46
|
+
* migration so an active SQLite WAL checkpoint is not stranded behind.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveContentStorePath(opts: {
|
|
49
|
+
projectDir: string;
|
|
50
|
+
contentDir: string;
|
|
51
|
+
}): string;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the SessionDB file path for a project, performing a one-shot
|
|
54
|
+
* migration from legacy raw-casing filenames to canonical ones when only
|
|
55
|
+
* the legacy file exists.
|
|
56
|
+
*
|
|
57
|
+
* Migration rules:
|
|
58
|
+
* - Linux: `legacyHash === canonicalHash` so the resolver short-circuits;
|
|
59
|
+
* no migration ever runs (case-sensitive FS, never any drift).
|
|
60
|
+
* - macOS / Windows: if the canonical path does not exist but a legacy
|
|
61
|
+
* path does, rename in place. This preserves the user's session
|
|
62
|
+
* history across the casing-fix upgrade.
|
|
63
|
+
* - When BOTH paths exist (rare — usually only if the user previously
|
|
64
|
+
* ran two terminals with different casing) the legacy file is left
|
|
65
|
+
* UNTOUCHED. The canonical path wins; manual reconciliation needed.
|
|
66
|
+
* Avoiding the rename here is the data-loss safety guarantee.
|
|
67
|
+
*
|
|
68
|
+
* Worktree separation is preserved: each call only ever migrates the ONE
|
|
69
|
+
* legacy file matching THIS projectDir's hash. Different worktrees have
|
|
70
|
+
* different physical paths → different hashes → different DB files; the
|
|
71
|
+
* migration cannot collapse worktrees.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveSessionDbPath(opts: {
|
|
74
|
+
projectDir: string;
|
|
75
|
+
sessionsDir: string;
|
|
76
|
+
}): string;
|
|
77
|
+
/**
|
|
78
|
+
* Generalized resolver: same case-fold + one-shot legacy-rename semantics
|
|
79
|
+
* as {@link resolveSessionDbPath}, parameterised on the file extension so
|
|
80
|
+
* the SAME logic powers `.db`, `-events.md`, and `.cleanup` paths.
|
|
81
|
+
*
|
|
82
|
+
* Source of truth for hooks: `hooks/session-helpers.mjs` imports this
|
|
83
|
+
* function from the bundled output (`hooks/session-db.bundle.mjs`) so the
|
|
84
|
+
* JS hooks and the TS server can never drift again on hash, suffix, or
|
|
85
|
+
* migration policy.
|
|
86
|
+
*
|
|
87
|
+
* Optional `suffix` lets the hook layer inject its cross-process cached
|
|
88
|
+
* worktree suffix (the marker-file optimisation that amortises the
|
|
89
|
+
* `git worktree list` cost across hook forks). When omitted, falls back
|
|
90
|
+
* to {@link getWorktreeSuffix} which uses an in-process cache only.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveSessionPath(opts: {
|
|
93
|
+
projectDir: string;
|
|
94
|
+
sessionsDir: string;
|
|
95
|
+
ext: string;
|
|
96
|
+
suffix?: string;
|
|
97
|
+
}): string;
|
|
13
98
|
/** A stored event row from the session_events table. */
|
|
14
99
|
export interface StoredEvent {
|
|
15
100
|
id: number;
|
|
@@ -21,10 +106,19 @@ export interface StoredEvent {
|
|
|
21
106
|
project_dir: string;
|
|
22
107
|
attribution_source: string;
|
|
23
108
|
attribution_confidence: number;
|
|
109
|
+
bytes_avoided: number;
|
|
110
|
+
bytes_returned: number;
|
|
24
111
|
source_hook: string;
|
|
25
112
|
created_at: string;
|
|
26
113
|
data_hash: string;
|
|
27
114
|
}
|
|
115
|
+
/** Optional per-event byte accounting passed to {@link SessionDB.insertEvent}. */
|
|
116
|
+
export interface EventBytes {
|
|
117
|
+
/** Bytes context-mode prevented from entering the model context window. */
|
|
118
|
+
bytesAvoided?: number;
|
|
119
|
+
/** Bytes context-mode actually returned to the model. */
|
|
120
|
+
bytesReturned?: number;
|
|
121
|
+
}
|
|
28
122
|
/** Session metadata row from the session_meta table. */
|
|
29
123
|
export interface SessionMeta {
|
|
30
124
|
session_id: string;
|
|
@@ -79,7 +173,7 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
79
173
|
*/
|
|
80
174
|
insertEvent(sessionId: string, event: Omit<SessionEvent, "data_hash"> & {
|
|
81
175
|
data_hash?: string;
|
|
82
|
-
}, sourceHook?: string, attribution?: Partial<ProjectAttribution
|
|
176
|
+
}, sourceHook?: string, attribution?: Partial<ProjectAttribution>, bytes?: EventBytes): void;
|
|
83
177
|
/**
|
|
84
178
|
* Bulk-insert N events in a SINGLE transaction.
|
|
85
179
|
*
|
|
@@ -91,7 +185,7 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
91
185
|
* Cross-platform: uses the same WAL-mode transaction primitive as
|
|
92
186
|
* insertEvent — behavior identical on macOS / Linux / Windows.
|
|
93
187
|
*/
|
|
94
|
-
bulkInsertEvents(sessionId: string, events: SessionEvent[], sourceHook?: string, attributions?: Array<Partial<ProjectAttribution> | undefined>): void;
|
|
188
|
+
bulkInsertEvents(sessionId: string, events: SessionEvent[], sourceHook?: string, attributions?: Array<Partial<ProjectAttribution> | undefined>, bytesList?: Array<EventBytes | undefined>): void;
|
|
95
189
|
/**
|
|
96
190
|
* Retrieve events for a session with optional filtering.
|
|
97
191
|
*/
|
|
@@ -104,6 +198,20 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
104
198
|
* Get the total event count for a session.
|
|
105
199
|
*/
|
|
106
200
|
getEventCount(sessionId: string): number;
|
|
201
|
+
/**
|
|
202
|
+
* Aggregate per-event byte accounting for a session.
|
|
203
|
+
*
|
|
204
|
+
* Returns the total bytes context-mode kept OUT of the model context
|
|
205
|
+
* window (`bytesAvoided`) and the total it actually returned to the
|
|
206
|
+
* model (`bytesReturned`). Both default to 0 for unknown sessions.
|
|
207
|
+
*
|
|
208
|
+
* Used by the Insight dashboard to render the "saved vs returned"
|
|
209
|
+
* panel without scanning every event row in JS.
|
|
210
|
+
*/
|
|
211
|
+
getEventBytesSummary(sessionId: string): {
|
|
212
|
+
bytesAvoided: number;
|
|
213
|
+
bytesReturned: number;
|
|
214
|
+
};
|
|
107
215
|
/**
|
|
108
216
|
* Return the most recently attributed project dir for a session.
|
|
109
217
|
*/
|
|
@@ -154,8 +262,9 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
154
262
|
* Atomically claim the most recent unconsumed resume snapshot in this DB,
|
|
155
263
|
* EXCLUDING any row that belongs to `currentSessionId`.
|
|
156
264
|
*
|
|
157
|
-
* `SessionDB` is sharded per project (see `
|
|
158
|
-
* project dir), so "this DB" already implies "this project".
|
|
265
|
+
* `SessionDB` is sharded per project (see `resolveSessionDbPath` — SHA-256
|
|
266
|
+
* of canonical project dir), so "this DB" already implies "this project".
|
|
267
|
+
* The atomic
|
|
159
268
|
* `UPDATE … RETURNING` ensures concurrent processes for the same project
|
|
160
269
|
* cannot both inject the same snapshot (Mickey / PR #376 race).
|
|
161
270
|
*
|
package/build/session/db.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { SQLiteBase, defaultDBPath } from "../db-base.js";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, realpathSync, renameSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
11
13
|
// ─────────────────────────────────────────────────────────
|
|
12
14
|
// Worktree isolation
|
|
13
15
|
// ─────────────────────────────────────────────────────────
|
|
@@ -19,15 +21,61 @@ import { execFileSync } from "node:child_process";
|
|
|
19
21
|
* (useful in CI environments or when git is unavailable).
|
|
20
22
|
* Set to empty string to disable isolation entirely.
|
|
21
23
|
*/
|
|
22
|
-
// Memoized per (
|
|
24
|
+
// Memoized per (projectDir, env override) — recomputing on every tool call cost
|
|
23
25
|
// ~12ms (git worktree list subprocess fork) on macOS, 50ms+ on Windows.
|
|
24
|
-
// Key by
|
|
25
|
-
//
|
|
26
|
+
// Key by projectDir so callers can pass the actual workspace even when the
|
|
27
|
+
// MCP server has chdir'd into the installed package directory.
|
|
26
28
|
let _wtCache;
|
|
27
|
-
export function
|
|
29
|
+
export function normalizeWorktreePath(path) {
|
|
30
|
+
const normalized = path.replace(/\\/g, "/");
|
|
31
|
+
if (/^\/+$/.test(normalized))
|
|
32
|
+
return "/";
|
|
33
|
+
if (/^[A-Za-z]:\/+$/.test(normalized))
|
|
34
|
+
return `${normalized.slice(0, 2)}/`;
|
|
35
|
+
return normalized.replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
// Case-insensitive filesystems (macOS HFS+/APFS default, Windows NTFS default)
|
|
38
|
+
// can report `currentRoot` and `mainRoot` with different casing for the same
|
|
39
|
+
// physical directory — git itself sometimes preserves the on-disk casing while
|
|
40
|
+
// user-supplied paths use a different casing. Compare canonically by resolving
|
|
41
|
+
// symlinks via realpath and case-folding on these platforms. POSIX/Linux is
|
|
42
|
+
// strictly case-sensitive so this is a no-op there.
|
|
43
|
+
function canonicalizeForCompare(root) {
|
|
44
|
+
let resolved = root;
|
|
45
|
+
try {
|
|
46
|
+
resolved = realpathSync.native(root);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Path may not exist (test fixtures, deleted dirs); fall back to as-given.
|
|
50
|
+
}
|
|
51
|
+
const normalized = normalizeWorktreePath(resolved);
|
|
52
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
53
|
+
return normalized.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
function gitOutput(projectDir, args) {
|
|
58
|
+
return execFileSync("git", ["-C", projectDir, ...args], {
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
timeout: 2000,
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
}).trim();
|
|
63
|
+
}
|
|
64
|
+
function getCurrentWorktreeRoot(projectDir) {
|
|
65
|
+
const root = gitOutput(projectDir, ["rev-parse", "--show-toplevel"]);
|
|
66
|
+
return root.length > 0 ? normalizeWorktreePath(root) : null;
|
|
67
|
+
}
|
|
68
|
+
function getMainWorktreeRoot(projectDir) {
|
|
69
|
+
const root = gitOutput(projectDir, ["worktree", "list", "--porcelain"])
|
|
70
|
+
.split(/\r?\n/)
|
|
71
|
+
.find((line) => line.startsWith("worktree "))
|
|
72
|
+
?.replace("worktree ", "")
|
|
73
|
+
?.trim();
|
|
74
|
+
return root ? normalizeWorktreePath(root) : null;
|
|
75
|
+
}
|
|
76
|
+
export function getWorktreeSuffix(projectDir = process.cwd()) {
|
|
28
77
|
const envSuffix = process.env.CONTEXT_MODE_SESSION_SUFFIX;
|
|
29
|
-
|
|
30
|
-
if (_wtCache && _wtCache.cwd === cwd && _wtCache.envSuffix === envSuffix) {
|
|
78
|
+
if (_wtCache && _wtCache.projectDir === projectDir && _wtCache.envSuffix === envSuffix) {
|
|
31
79
|
return _wtCache.suffix;
|
|
32
80
|
}
|
|
33
81
|
let suffix = "";
|
|
@@ -36,24 +84,27 @@ export function getWorktreeSuffix() {
|
|
|
36
84
|
}
|
|
37
85
|
else {
|
|
38
86
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
const currentRoot = getCurrentWorktreeRoot(projectDir);
|
|
88
|
+
const mainRoot = getMainWorktreeRoot(projectDir);
|
|
89
|
+
if (currentRoot && mainRoot) {
|
|
90
|
+
// Use the canonicalized currentRoot for BOTH the comparison and the
|
|
91
|
+
// hash so the suffix DB filename stays stable across casing-variant
|
|
92
|
+
// calls on the same machine (round-5 finding). Previously the hash
|
|
93
|
+
// ate raw casing, so the same linked worktree could land at two
|
|
94
|
+
// different `__<8-hex>` files depending on which casing the caller
|
|
95
|
+
// passed in.
|
|
96
|
+
const canonicalCurrent = canonicalizeForCompare(currentRoot);
|
|
97
|
+
const canonicalMain = canonicalizeForCompare(mainRoot);
|
|
98
|
+
if (canonicalCurrent !== canonicalMain) {
|
|
99
|
+
suffix = `__${createHash("sha256").update(canonicalCurrent).digest("hex").slice(0, 8)}`;
|
|
100
|
+
}
|
|
50
101
|
}
|
|
51
102
|
}
|
|
52
103
|
catch {
|
|
53
104
|
// git not available or not a git repo — no suffix
|
|
54
105
|
}
|
|
55
106
|
}
|
|
56
|
-
_wtCache = {
|
|
107
|
+
_wtCache = { projectDir, envSuffix, suffix };
|
|
57
108
|
return suffix;
|
|
58
109
|
}
|
|
59
110
|
// Test-only helper: clear the memoization between cases.
|
|
@@ -61,12 +112,160 @@ export function _resetWorktreeSuffixCacheForTests() {
|
|
|
61
112
|
_wtCache = undefined;
|
|
62
113
|
}
|
|
63
114
|
// ─────────────────────────────────────────────────────────
|
|
115
|
+
// SessionDB path resolution + case-fold migration
|
|
116
|
+
// ─────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Hash a project directory the way the deployed code (≤ v1.0.111) did:
|
|
119
|
+
* normalize slashes only, preserve raw casing. Kept exported so the
|
|
120
|
+
* migration helper can locate pre-fix DB files for one-shot rename.
|
|
121
|
+
*
|
|
122
|
+
* Do NOT call this for new code paths — use {@link hashProjectDirCanonical}.
|
|
123
|
+
*/
|
|
124
|
+
export function hashProjectDirLegacy(projectDir) {
|
|
125
|
+
return createHash("sha256")
|
|
126
|
+
.update(normalizeWorktreePath(projectDir))
|
|
127
|
+
.digest("hex")
|
|
128
|
+
.slice(0, 16);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Hash a project directory case-stably. On case-insensitive filesystems
|
|
132
|
+
* (macOS HFS+/APFS, Windows NTFS) the path is lowercased so that
|
|
133
|
+
* `/Users/Mert/proj` and `/users/mert/proj` resolve to the same DB file.
|
|
134
|
+
* On Linux (case-sensitive) casing is preserved.
|
|
135
|
+
*
|
|
136
|
+
* Used as the base half of the SessionDB filename:
|
|
137
|
+
* <baseHash><worktreeSuffix>.db
|
|
138
|
+
*/
|
|
139
|
+
export function hashProjectDirCanonical(projectDir) {
|
|
140
|
+
const normalized = normalizeWorktreePath(projectDir);
|
|
141
|
+
const folded = (process.platform === "darwin" || process.platform === "win32")
|
|
142
|
+
? normalized.toLowerCase()
|
|
143
|
+
: normalized;
|
|
144
|
+
return createHash("sha256").update(folded).digest("hex").slice(0, 16);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the per-project FTS5 content store DB path, performing a one-shot
|
|
148
|
+
* migration from a legacy raw-casing filename to the canonical one when only
|
|
149
|
+
* the legacy file (with optional `-wal` / `-shm` SQLite sidecars) exists.
|
|
150
|
+
*
|
|
151
|
+
* Same dual-hash safety contract as {@link resolveSessionDbPath}:
|
|
152
|
+
* - Linux: canonical hash equals legacy hash → no migration attempted.
|
|
153
|
+
* - Mac/Win: rename legacy → canonical when canonical missing.
|
|
154
|
+
* - Both exist: leave legacy alone (data-loss safety). Caller picks
|
|
155
|
+
* canonical; reconciliation is a manual operation.
|
|
156
|
+
*
|
|
157
|
+
* Differs from `resolveSessionDbPath` in two ways:
|
|
158
|
+
* 1. No worktree suffix — the FTS5 store is per-project, not per-worktree.
|
|
159
|
+
* 2. The `-wal` / `-shm` sidecars travel with the main `.db` during
|
|
160
|
+
* migration so an active SQLite WAL checkpoint is not stranded behind.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveContentStorePath(opts) {
|
|
163
|
+
const { projectDir, contentDir } = opts;
|
|
164
|
+
const canonicalHash = hashProjectDirCanonical(projectDir);
|
|
165
|
+
const canonicalPath = join(contentDir, `${canonicalHash}.db`);
|
|
166
|
+
if (existsSync(canonicalPath))
|
|
167
|
+
return canonicalPath;
|
|
168
|
+
const legacyHash = hashProjectDirLegacy(projectDir);
|
|
169
|
+
if (legacyHash === canonicalHash)
|
|
170
|
+
return canonicalPath; // Linux short-circuit
|
|
171
|
+
const legacyPath = join(contentDir, `${legacyHash}.db`);
|
|
172
|
+
if (existsSync(legacyPath)) {
|
|
173
|
+
try {
|
|
174
|
+
renameSync(legacyPath, canonicalPath);
|
|
175
|
+
// Travel the SQLite sidecars too so an active WAL is not orphaned.
|
|
176
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
177
|
+
try {
|
|
178
|
+
renameSync(legacyPath + suffix, canonicalPath + suffix);
|
|
179
|
+
}
|
|
180
|
+
catch { /* sidecar may not exist */ }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Race or permission issue — caller will create canonicalPath fresh.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return canonicalPath;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the SessionDB file path for a project, performing a one-shot
|
|
191
|
+
* migration from legacy raw-casing filenames to canonical ones when only
|
|
192
|
+
* the legacy file exists.
|
|
193
|
+
*
|
|
194
|
+
* Migration rules:
|
|
195
|
+
* - Linux: `legacyHash === canonicalHash` so the resolver short-circuits;
|
|
196
|
+
* no migration ever runs (case-sensitive FS, never any drift).
|
|
197
|
+
* - macOS / Windows: if the canonical path does not exist but a legacy
|
|
198
|
+
* path does, rename in place. This preserves the user's session
|
|
199
|
+
* history across the casing-fix upgrade.
|
|
200
|
+
* - When BOTH paths exist (rare — usually only if the user previously
|
|
201
|
+
* ran two terminals with different casing) the legacy file is left
|
|
202
|
+
* UNTOUCHED. The canonical path wins; manual reconciliation needed.
|
|
203
|
+
* Avoiding the rename here is the data-loss safety guarantee.
|
|
204
|
+
*
|
|
205
|
+
* Worktree separation is preserved: each call only ever migrates the ONE
|
|
206
|
+
* legacy file matching THIS projectDir's hash. Different worktrees have
|
|
207
|
+
* different physical paths → different hashes → different DB files; the
|
|
208
|
+
* migration cannot collapse worktrees.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveSessionDbPath(opts) {
|
|
211
|
+
return resolveSessionPath({ ...opts, ext: ".db" });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Generalized resolver: same case-fold + one-shot legacy-rename semantics
|
|
215
|
+
* as {@link resolveSessionDbPath}, parameterised on the file extension so
|
|
216
|
+
* the SAME logic powers `.db`, `-events.md`, and `.cleanup` paths.
|
|
217
|
+
*
|
|
218
|
+
* Source of truth for hooks: `hooks/session-helpers.mjs` imports this
|
|
219
|
+
* function from the bundled output (`hooks/session-db.bundle.mjs`) so the
|
|
220
|
+
* JS hooks and the TS server can never drift again on hash, suffix, or
|
|
221
|
+
* migration policy.
|
|
222
|
+
*
|
|
223
|
+
* Optional `suffix` lets the hook layer inject its cross-process cached
|
|
224
|
+
* worktree suffix (the marker-file optimisation that amortises the
|
|
225
|
+
* `git worktree list` cost across hook forks). When omitted, falls back
|
|
226
|
+
* to {@link getWorktreeSuffix} which uses an in-process cache only.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveSessionPath(opts) {
|
|
229
|
+
const { projectDir, sessionsDir, ext } = opts;
|
|
230
|
+
const suffix = opts.suffix ?? getWorktreeSuffix(projectDir);
|
|
231
|
+
const canonicalHash = hashProjectDirCanonical(projectDir);
|
|
232
|
+
const canonicalPath = join(sessionsDir, `${canonicalHash}${suffix}${ext}`);
|
|
233
|
+
if (existsSync(canonicalPath))
|
|
234
|
+
return canonicalPath;
|
|
235
|
+
const legacyHash = hashProjectDirLegacy(projectDir);
|
|
236
|
+
if (legacyHash === canonicalHash)
|
|
237
|
+
return canonicalPath; // Linux or already canonical
|
|
238
|
+
const legacyPath = join(sessionsDir, `${legacyHash}${suffix}${ext}`);
|
|
239
|
+
if (existsSync(legacyPath)) {
|
|
240
|
+
try {
|
|
241
|
+
renameSync(legacyPath, canonicalPath);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Race or permission issue — caller will create canonicalPath on first
|
|
245
|
+
// write. Better to lose this rename than to throw and break ctx_stats.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return canonicalPath;
|
|
249
|
+
}
|
|
250
|
+
// ─────────────────────────────────────────────────────────
|
|
64
251
|
// Constants
|
|
65
252
|
// ─────────────────────────────────────────────────────────
|
|
66
253
|
/** Maximum events per session before FIFO eviction kicks in. */
|
|
67
254
|
const MAX_EVENTS_PER_SESSION = 1000;
|
|
68
255
|
/** Number of recent events to check for deduplication. */
|
|
69
256
|
const DEDUP_WINDOW = 5;
|
|
257
|
+
/**
|
|
258
|
+
* Coerce an arbitrary input to a non-negative integer suitable for
|
|
259
|
+
* SQLite's INTEGER column. Accepts undefined / null / NaN / floats
|
|
260
|
+
* and returns 0 for invalid inputs so the column never violates its
|
|
261
|
+
* NOT NULL DEFAULT 0 contract.
|
|
262
|
+
*/
|
|
263
|
+
function clampNonNegativeInt(value) {
|
|
264
|
+
const n = Number(value);
|
|
265
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
266
|
+
return 0;
|
|
267
|
+
return Math.floor(n);
|
|
268
|
+
}
|
|
70
269
|
// ─────────────────────────────────────────────────────────
|
|
71
270
|
// Statement keys (typed enum to avoid string typos)
|
|
72
271
|
// ─────────────────────────────────────────────────────────
|
|
@@ -96,6 +295,7 @@ const S = {
|
|
|
96
295
|
incrementToolCall: "incrementToolCall",
|
|
97
296
|
getToolCallTotals: "getToolCallTotals",
|
|
98
297
|
getToolCallByTool: "getToolCallByTool",
|
|
298
|
+
getEventBytesSummary: "getEventBytesSummary",
|
|
99
299
|
};
|
|
100
300
|
// ─────────────────────────────────────────────────────────
|
|
101
301
|
// SessionDB
|
|
@@ -133,6 +333,8 @@ export class SessionDB extends SQLiteBase {
|
|
|
133
333
|
project_dir TEXT NOT NULL DEFAULT '',
|
|
134
334
|
attribution_source TEXT NOT NULL DEFAULT 'unknown',
|
|
135
335
|
attribution_confidence REAL NOT NULL DEFAULT 0,
|
|
336
|
+
bytes_avoided INTEGER NOT NULL DEFAULT 0,
|
|
337
|
+
bytes_returned INTEGER NOT NULL DEFAULT 0,
|
|
136
338
|
source_hook TEXT NOT NULL,
|
|
137
339
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
138
340
|
data_hash TEXT NOT NULL DEFAULT ''
|
|
@@ -184,6 +386,12 @@ export class SessionDB extends SQLiteBase {
|
|
|
184
386
|
if (!cols.has("attribution_confidence")) {
|
|
185
387
|
this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_confidence REAL NOT NULL DEFAULT 0");
|
|
186
388
|
}
|
|
389
|
+
if (!cols.has("bytes_avoided")) {
|
|
390
|
+
this.db.exec("ALTER TABLE session_events ADD COLUMN bytes_avoided INTEGER NOT NULL DEFAULT 0");
|
|
391
|
+
}
|
|
392
|
+
if (!cols.has("bytes_returned")) {
|
|
393
|
+
this.db.exec("ALTER TABLE session_events ADD COLUMN bytes_returned INTEGER NOT NULL DEFAULT 0");
|
|
394
|
+
}
|
|
187
395
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)");
|
|
188
396
|
}
|
|
189
397
|
catch {
|
|
@@ -199,23 +407,28 @@ export class SessionDB extends SQLiteBase {
|
|
|
199
407
|
p(S.insertEvent, `INSERT INTO session_events (
|
|
200
408
|
session_id, type, category, priority, data,
|
|
201
409
|
project_dir, attribution_source, attribution_confidence,
|
|
410
|
+
bytes_avoided, bytes_returned,
|
|
202
411
|
source_hook, data_hash
|
|
203
412
|
)
|
|
204
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
205
414
|
p(S.getEvents, `SELECT id, session_id, type, category, priority, data,
|
|
206
415
|
project_dir, attribution_source, attribution_confidence,
|
|
416
|
+
bytes_avoided, bytes_returned,
|
|
207
417
|
source_hook, created_at, data_hash
|
|
208
418
|
FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`);
|
|
209
419
|
p(S.getEventsByType, `SELECT id, session_id, type, category, priority, data,
|
|
210
420
|
project_dir, attribution_source, attribution_confidence,
|
|
421
|
+
bytes_avoided, bytes_returned,
|
|
211
422
|
source_hook, created_at, data_hash
|
|
212
423
|
FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`);
|
|
213
424
|
p(S.getEventsByPriority, `SELECT id, session_id, type, category, priority, data,
|
|
214
425
|
project_dir, attribution_source, attribution_confidence,
|
|
426
|
+
bytes_avoided, bytes_returned,
|
|
215
427
|
source_hook, created_at, data_hash
|
|
216
428
|
FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`);
|
|
217
429
|
p(S.getEventsByTypeAndPriority, `SELECT id, session_id, type, category, priority, data,
|
|
218
430
|
project_dir, attribution_source, attribution_confidence,
|
|
431
|
+
bytes_avoided, bytes_returned,
|
|
219
432
|
source_hook, created_at, data_hash
|
|
220
433
|
FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`);
|
|
221
434
|
p(S.getEventCount, `SELECT COUNT(*) AS cnt FROM session_events WHERE session_id = ?`);
|
|
@@ -297,6 +510,10 @@ export class SessionDB extends SQLiteBase {
|
|
|
297
510
|
FROM tool_calls WHERE session_id = ?`);
|
|
298
511
|
p(S.getToolCallByTool, `SELECT tool, calls, bytes_returned
|
|
299
512
|
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`);
|
|
513
|
+
// ── Event-level byte accounting (D2 PRD Phase 2) ──
|
|
514
|
+
p(S.getEventBytesSummary, `SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
|
|
515
|
+
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
516
|
+
FROM session_events WHERE session_id = ?`);
|
|
300
517
|
}
|
|
301
518
|
// ═══════════════════════════════════════════
|
|
302
519
|
// Events
|
|
@@ -310,7 +527,7 @@ export class SessionDB extends SQLiteBase {
|
|
|
310
527
|
* Eviction: if session exceeds MAX_EVENTS_PER_SESSION, evicts the
|
|
311
528
|
* lowest-priority (then oldest) event.
|
|
312
529
|
*/
|
|
313
|
-
insertEvent(sessionId, event, sourceHook = "PostToolUse", attribution) {
|
|
530
|
+
insertEvent(sessionId, event, sourceHook = "PostToolUse", attribution, bytes) {
|
|
314
531
|
// SHA256-based dedup hash (first 16 hex chars = 8 bytes of entropy)
|
|
315
532
|
const dataHash = createHash("sha256")
|
|
316
533
|
.update(event.data)
|
|
@@ -329,6 +546,8 @@ export class SessionDB extends SQLiteBase {
|
|
|
329
546
|
const attributionConfidence = Number.isFinite(rawConfidence)
|
|
330
547
|
? Math.max(0, Math.min(1, rawConfidence))
|
|
331
548
|
: 0;
|
|
549
|
+
const bytesAvoided = clampNonNegativeInt(bytes?.bytesAvoided);
|
|
550
|
+
const bytesReturned = clampNonNegativeInt(bytes?.bytesReturned);
|
|
332
551
|
// Atomic: dedup check + eviction + insert in a single transaction
|
|
333
552
|
// to prevent race conditions from concurrent hook calls.
|
|
334
553
|
const transaction = this.db.transaction(() => {
|
|
@@ -342,7 +561,7 @@ export class SessionDB extends SQLiteBase {
|
|
|
342
561
|
this.stmt(S.evictLowestPriority).run(sessionId);
|
|
343
562
|
}
|
|
344
563
|
// Insert the event
|
|
345
|
-
this.stmt(S.insertEvent).run(sessionId, event.type, event.category, event.priority, event.data, projectDir, attributionSource, attributionConfidence, sourceHook, dataHash);
|
|
564
|
+
this.stmt(S.insertEvent).run(sessionId, event.type, event.category, event.priority, event.data, projectDir, attributionSource, attributionConfidence, bytesAvoided, bytesReturned, sourceHook, dataHash);
|
|
346
565
|
// Update meta if session exists
|
|
347
566
|
this.stmt(S.updateMetaLastEvent).run(sessionId);
|
|
348
567
|
});
|
|
@@ -359,12 +578,12 @@ export class SessionDB extends SQLiteBase {
|
|
|
359
578
|
* Cross-platform: uses the same WAL-mode transaction primitive as
|
|
360
579
|
* insertEvent — behavior identical on macOS / Linux / Windows.
|
|
361
580
|
*/
|
|
362
|
-
bulkInsertEvents(sessionId, events, sourceHook = "PostToolUse", attributions) {
|
|
581
|
+
bulkInsertEvents(sessionId, events, sourceHook = "PostToolUse", attributions, bytesList) {
|
|
363
582
|
if (!events || events.length === 0)
|
|
364
583
|
return;
|
|
365
584
|
if (events.length === 1) {
|
|
366
585
|
// Cheaper to fall through to insertEvent (its own dedicated transaction).
|
|
367
|
-
this.insertEvent(sessionId, events[0], sourceHook, attributions?.[0]);
|
|
586
|
+
this.insertEvent(sessionId, events[0], sourceHook, attributions?.[0], bytesList?.[0]);
|
|
368
587
|
return;
|
|
369
588
|
}
|
|
370
589
|
// Pre-compute hashes + normalized attribution outside the transaction
|
|
@@ -382,7 +601,18 @@ export class SessionDB extends SQLiteBase {
|
|
|
382
601
|
const attributionConfidence = Number.isFinite(rawConfidence)
|
|
383
602
|
? Math.max(0, Math.min(1, rawConfidence))
|
|
384
603
|
: 0;
|
|
385
|
-
|
|
604
|
+
const eventBytes = bytesList?.[i];
|
|
605
|
+
const bytesAvoided = clampNonNegativeInt(eventBytes?.bytesAvoided);
|
|
606
|
+
const bytesReturned = clampNonNegativeInt(eventBytes?.bytesReturned);
|
|
607
|
+
return {
|
|
608
|
+
event,
|
|
609
|
+
dataHash,
|
|
610
|
+
projectDir,
|
|
611
|
+
attributionSource,
|
|
612
|
+
attributionConfidence,
|
|
613
|
+
bytesAvoided,
|
|
614
|
+
bytesReturned,
|
|
615
|
+
};
|
|
386
616
|
});
|
|
387
617
|
const transaction = this.db.transaction(() => {
|
|
388
618
|
let cnt = this.stmt(S.getEventCount).get(sessionId).cnt;
|
|
@@ -396,7 +626,7 @@ export class SessionDB extends SQLiteBase {
|
|
|
396
626
|
else {
|
|
397
627
|
cnt++;
|
|
398
628
|
}
|
|
399
|
-
this.stmt(S.insertEvent).run(sessionId, row.event.type, row.event.category, row.event.priority, row.event.data, row.projectDir, row.attributionSource, row.attributionConfidence, sourceHook, row.dataHash);
|
|
629
|
+
this.stmt(S.insertEvent).run(sessionId, row.event.type, row.event.category, row.event.priority, row.event.data, row.projectDir, row.attributionSource, row.attributionConfidence, row.bytesAvoided, row.bytesReturned, sourceHook, row.dataHash);
|
|
400
630
|
}
|
|
401
631
|
this.stmt(S.updateMetaLastEvent).run(sessionId);
|
|
402
632
|
});
|
|
@@ -427,6 +657,23 @@ export class SessionDB extends SQLiteBase {
|
|
|
427
657
|
const row = this.stmt(S.getEventCount).get(sessionId);
|
|
428
658
|
return row.cnt;
|
|
429
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Aggregate per-event byte accounting for a session.
|
|
662
|
+
*
|
|
663
|
+
* Returns the total bytes context-mode kept OUT of the model context
|
|
664
|
+
* window (`bytesAvoided`) and the total it actually returned to the
|
|
665
|
+
* model (`bytesReturned`). Both default to 0 for unknown sessions.
|
|
666
|
+
*
|
|
667
|
+
* Used by the Insight dashboard to render the "saved vs returned"
|
|
668
|
+
* panel without scanning every event row in JS.
|
|
669
|
+
*/
|
|
670
|
+
getEventBytesSummary(sessionId) {
|
|
671
|
+
const row = this.stmt(S.getEventBytesSummary).get(sessionId);
|
|
672
|
+
return {
|
|
673
|
+
bytesAvoided: Number(row?.bytes_avoided ?? 0),
|
|
674
|
+
bytesReturned: Number(row?.bytes_returned ?? 0),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
430
677
|
/**
|
|
431
678
|
* Return the most recently attributed project dir for a session.
|
|
432
679
|
*/
|
|
@@ -502,8 +749,9 @@ export class SessionDB extends SQLiteBase {
|
|
|
502
749
|
* Atomically claim the most recent unconsumed resume snapshot in this DB,
|
|
503
750
|
* EXCLUDING any row that belongs to `currentSessionId`.
|
|
504
751
|
*
|
|
505
|
-
* `SessionDB` is sharded per project (see `
|
|
506
|
-
* project dir), so "this DB" already implies "this project".
|
|
752
|
+
* `SessionDB` is sharded per project (see `resolveSessionDbPath` — SHA-256
|
|
753
|
+
* of canonical project dir), so "this DB" already implies "this project".
|
|
754
|
+
* The atomic
|
|
507
755
|
* `UPDATE … RETURNING` ensures concurrent processes for the same project
|
|
508
756
|
* cannot both inject the same snapshot (Mickey / PR #376 race).
|
|
509
757
|
*
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* event-emit — Phase 5+7 of D2 PRD (stats-event-driven-architecture)
|
|
3
|
+
*
|
|
4
|
+
* Server-side helpers that record sandbox / index / cache work into
|
|
5
|
+
* `session_events` with the new `bytes_avoided` / `bytes_returned`
|
|
6
|
+
* columns so the renderer can compute the real $ saved instead of the
|
|
7
|
+
* conservative `events × 256` token estimate.
|
|
8
|
+
*
|
|
9
|
+
* Design notes
|
|
10
|
+
* ────────────
|
|
11
|
+
* - Uses the public `SessionDB.insertEvent(... , bytes)` API the schema
|
|
12
|
+
* engineer extended in this branch — same dedup + FIFO eviction +
|
|
13
|
+
* transaction wrapping you'd get from any other event source.
|
|
14
|
+
* - Best-effort error swallowing matches `persistToolCallCounter` in
|
|
15
|
+
* `persist-tool-calls.ts`. A stats-side failure must NEVER break the
|
|
16
|
+
* parent MCP tool call.
|
|
17
|
+
* - Resolves the latest `session_id` from `session_meta` so the wiring
|
|
18
|
+
* in `server.ts` is `setImmediate(() => emit*({...}))` — no need to
|
|
19
|
+
* plumb session ids through every handler.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Record a `ctx_execute` / `ctx_execute_file` / `ctx_batch_execute` run.
|
|
23
|
+
* `bytesReturned` is the size of the stdout text the user actually saw —
|
|
24
|
+
* the rest of the sandbox output stayed out of context.
|
|
25
|
+
*/
|
|
26
|
+
export declare function emitSandboxExecuteEvent(opts: {
|
|
27
|
+
sessionDbPath: string;
|
|
28
|
+
toolName: string;
|
|
29
|
+
bytesReturned: number;
|
|
30
|
+
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Record a `ctx_index` / `trackIndexed` write — content kept out of
|
|
33
|
+
* context by being chunked into FTS5 instead of returned inline.
|
|
34
|
+
*/
|
|
35
|
+
export declare function emitIndexWriteEvent(opts: {
|
|
36
|
+
sessionDbPath: string;
|
|
37
|
+
source: string;
|
|
38
|
+
bytesAvoided: number;
|
|
39
|
+
}): void;
|
|
40
|
+
/**
|
|
41
|
+
* Record a `ctx_fetch_and_index` TTL cache hit — bytes the user would
|
|
42
|
+
* have spent re-fetching the same URL within the 24h cache window.
|
|
43
|
+
*/
|
|
44
|
+
export declare function emitCacheHitEvent(opts: {
|
|
45
|
+
sessionDbPath: string;
|
|
46
|
+
source: string;
|
|
47
|
+
bytesAvoided: number;
|
|
48
|
+
}): void;
|