pi-mega-compact 0.4.1 → 0.4.3
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/extensions/dashboard-server.ts +9 -3
- package/extensions/mega-compact.test.ts +11 -2
- package/extensions/mega-compact.ts +27 -9
- package/package.json +1 -1
- package/src/engine.ts +21 -6
- package/src/store/sprint10.test.ts +32 -0
- package/src/store/sqlite.ts +125 -3
- package/src/store.ts +3 -0
- package/src/vectorStore.test.ts +28 -20
- package/src/vectorStore.ts +37 -8
|
@@ -53,6 +53,7 @@ interface Snapshot {
|
|
|
53
53
|
store: {
|
|
54
54
|
checkpointCount: number;
|
|
55
55
|
totalTokenEstimate: number;
|
|
56
|
+
originalTokens: number;
|
|
56
57
|
tokensSaved: number;
|
|
57
58
|
injectedCount: number;
|
|
58
59
|
dedupHitRate: number;
|
|
@@ -66,6 +67,7 @@ interface Snapshot {
|
|
|
66
67
|
repo: {
|
|
67
68
|
checkpointCount: number;
|
|
68
69
|
totalTokenEstimate: number;
|
|
70
|
+
originalTokens: number;
|
|
69
71
|
tokensSaved: number;
|
|
70
72
|
sessionCount: number;
|
|
71
73
|
dedupAttempts: number;
|
|
@@ -91,9 +93,9 @@ function readSnapshot(snapshotPath: string) {
|
|
|
91
93
|
session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
|
|
92
94
|
context: { tokens: null, percent: null, contextWindow: 0 },
|
|
93
95
|
trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80 },
|
|
94
|
-
store: { checkpointCount: 0, totalTokenEstimate: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
|
|
96
|
+
store: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
|
|
95
97
|
crew: { activeAgents: 0, currentTurn: 0 },
|
|
96
|
-
repo: { checkpointCount: 0, totalTokenEstimate: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
|
|
98
|
+
repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
|
|
97
99
|
} as Snapshot;
|
|
98
100
|
}
|
|
99
101
|
}
|
|
@@ -185,6 +187,7 @@ function dashboardHtml(tierName: string): string {
|
|
|
185
187
|
<div class="stat-grid">
|
|
186
188
|
<span class="label">Checkpoints</span><span class="value" id="st-count">0</span>
|
|
187
189
|
<span class="label">Tokens Stored</span><span class="value" id="st-tokens">0</span>
|
|
190
|
+
<span class="label">Original Tokens</span><span class="value" id="st-orig">0</span>
|
|
188
191
|
<span class="label">Tokens Saved</span><span class="value" id="st-saved">0</span>
|
|
189
192
|
<span class="label">Injected</span><span class="value" id="st-injected">0</span>
|
|
190
193
|
<span class="label">Dedup Rate</span><span class="value" id="st-dedup">0%</span>
|
|
@@ -198,6 +201,7 @@ function dashboardHtml(tierName: string): string {
|
|
|
198
201
|
<div class="stat-grid">
|
|
199
202
|
<span class="label">Checkpoints</span><span class="value" id="rp-count">0</span>
|
|
200
203
|
<span class="label">Tokens Stored</span><span class="value" id="rp-tokens">0</span>
|
|
204
|
+
<span class="label">Original Tokens</span><span class="value" id="rp-orig">0</span>
|
|
201
205
|
<span class="label">Tokens Saved</span><span class="value" id="rp-saved">0</span>
|
|
202
206
|
<span class="label">Sessions</span><span class="value" id="rp-sessions">0</span>
|
|
203
207
|
<span class="label">Collapsed</span><span class="value" id="rp-collapsed">0</span>
|
|
@@ -263,6 +267,7 @@ function dashboardHtml(tierName: string): string {
|
|
|
263
267
|
|
|
264
268
|
document.getElementById('st-count').textContent = d.store.checkpointCount;
|
|
265
269
|
document.getElementById('st-tokens').textContent = d.store.totalTokenEstimate.toLocaleString();
|
|
270
|
+
document.getElementById('st-orig').textContent = (d.store.originalTokens || 0).toLocaleString();
|
|
266
271
|
document.getElementById('st-saved').textContent = (d.store.tokensSaved || 0).toLocaleString();
|
|
267
272
|
document.getElementById('st-injected').textContent = d.store.injectedCount;
|
|
268
273
|
document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
|
|
@@ -272,9 +277,10 @@ function dashboardHtml(tierName: string): string {
|
|
|
272
277
|
document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
|
|
273
278
|
|
|
274
279
|
// Repo-wide (all sessions in this repo's SQLite store).
|
|
275
|
-
var repo = d.repo || { checkpointCount: 0, totalTokenEstimate: 0, tokensSaved: 0, sessionCount: 0, dedupCollapsed: 0, storageDedupRate: 0 };
|
|
280
|
+
var repo = d.repo || { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupCollapsed: 0, storageDedupRate: 0 };
|
|
276
281
|
document.getElementById('rp-count').textContent = repo.checkpointCount;
|
|
277
282
|
document.getElementById('rp-tokens').textContent = repo.totalTokenEstimate.toLocaleString();
|
|
283
|
+
document.getElementById('rp-orig').textContent = (repo.originalTokens || 0).toLocaleString();
|
|
278
284
|
document.getElementById('rp-saved').textContent = (repo.tokensSaved || 0).toLocaleString();
|
|
279
285
|
document.getElementById('rp-sessions').textContent = repo.sessionCount || 0;
|
|
280
286
|
document.getElementById('rp-collapsed').textContent = repo.dedupCollapsed || 0;
|
|
@@ -327,8 +327,17 @@ test("state snapshot writes dashboard.json after compaction", async () => {
|
|
|
327
327
|
const snapPath = j(h.stateDir, "dashboard.json");
|
|
328
328
|
assert.ok(ex(snapPath), "dashboard.json written after compaction");
|
|
329
329
|
const snap = JSON.parse(rf(snapPath, "utf-8"));
|
|
330
|
-
// Item B:
|
|
331
|
-
|
|
330
|
+
// Item B: the honest token model is wired — the original dropped region was
|
|
331
|
+
// captured (originalTokens > 0), and the saved amount never exceeds the
|
|
332
|
+
// original (saved = max(0, original − stored) ≤ original). For this tiny
|
|
333
|
+
// harness session the summary can be ≥ the region, so saved may be 0; the
|
|
334
|
+
// positive "saved > 0" case with a large region is covered by the
|
|
335
|
+
// vectorStore unit tests.
|
|
336
|
+
assert.ok(snap.store.originalTokens > 0, "snapshot.store.originalTokens captured after compaction");
|
|
337
|
+
assert.ok(
|
|
338
|
+
snap.store.originalTokens >= snap.store.tokensSaved,
|
|
339
|
+
"model invariant: original region >= tokens saved",
|
|
340
|
+
);
|
|
332
341
|
// Item A: crew (live agent) block is present in the dashboard snapshot.
|
|
333
342
|
assert.ok(snap.crew && typeof snap.crew.activeAgents === "number", "snapshot.crew.activeAgents present");
|
|
334
343
|
});
|
|
@@ -37,6 +37,7 @@ import { recallAndInline } from "../src/recall.js";
|
|
|
37
37
|
import { autoCompactCheck } from "../src/compact.js";
|
|
38
38
|
import { estimateSessionTokens } from "../src/tokens.js";
|
|
39
39
|
import { normalizeSessionId } from "../src/store.js";
|
|
40
|
+
import { touchSession, logDaily } from "../src/store/sqlite.js";
|
|
40
41
|
import { Logger } from "../src/log.js";
|
|
41
42
|
import type { EngineMessage } from "../src/types.js";
|
|
42
43
|
import { writeFileSync, appendFileSync, readFileSync } from "node:fs";
|
|
@@ -191,7 +192,8 @@ interface DashboardSnapshot {
|
|
|
191
192
|
store: {
|
|
192
193
|
checkpointCount: number;
|
|
193
194
|
totalTokenEstimate: number;
|
|
194
|
-
|
|
195
|
+
originalTokens: number; // Σ original dropped-region tokens (this session)
|
|
196
|
+
tokensSaved: number; // Σ(original − stored) for this session
|
|
195
197
|
injectedCount: number;
|
|
196
198
|
dedupHitRate: number;
|
|
197
199
|
storageDedupRate: number;
|
|
@@ -205,7 +207,8 @@ interface DashboardSnapshot {
|
|
|
205
207
|
repo: {
|
|
206
208
|
checkpointCount: number; // across all sessions in this repo's store
|
|
207
209
|
totalTokenEstimate: number; // repo-wide stored checkpoint tokens
|
|
208
|
-
|
|
210
|
+
originalTokens: number; // repo-wide Σ original dropped-region tokens
|
|
211
|
+
tokensSaved: number; // repo-wide cumulative (original − stored) + deduped orig
|
|
209
212
|
sessionCount: number; // distinct sessions with checkpoints
|
|
210
213
|
dedupAttempts: number; // cumulative add() calls (store-wide)
|
|
211
214
|
dedupCollapsed: number; // cumulative deduped collapses (store-wide)
|
|
@@ -304,10 +307,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
307
|
context: { tokens: lastCtxTokens, percent: lastCtxPercent, contextWindow: lastCtxWindow },
|
|
305
308
|
trigger: { armed, ready, currentTokens: lastCtxTokens, thresholdTokens: config.thresholdTokens, fastGatePct: config.fastGatePct },
|
|
306
309
|
crew: { activeAgents, currentTurn },
|
|
307
|
-
store: { checkpointCount: st.checkpointCount, totalTokenEstimate: st.totalTokenEstimate, tokensSaved: rt.tokensSaved, injectedCount: st.injectedCount, dedupHitRate: st.dedupHitRate, storageDedupRate: st.storageDedupRate, dedupAttempts: st.dedupAttempts, dedupCollapsed: st.dedupCollapsed },
|
|
310
|
+
store: { checkpointCount: st.checkpointCount, totalTokenEstimate: st.totalTokenEstimate, originalTokens: st.originalTokens, tokensSaved: rt.tokensSaved, injectedCount: st.injectedCount, dedupHitRate: st.dedupHitRate, storageDedupRate: st.storageDedupRate, dedupAttempts: st.dedupAttempts, dedupCollapsed: st.dedupCollapsed },
|
|
308
311
|
repo: {
|
|
309
312
|
checkpointCount: repo.checkpointCount,
|
|
310
313
|
totalTokenEstimate: repo.totalTokenEstimate,
|
|
314
|
+
originalTokens: repo.originalTokens,
|
|
311
315
|
tokensSaved: repo.tokensSaved,
|
|
312
316
|
sessionCount: repo.sessionCount,
|
|
313
317
|
dedupAttempts: repo.dedupAttempts,
|
|
@@ -425,13 +429,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
425
429
|
rt.lastCompactedFrom = result.compactedFrom;
|
|
426
430
|
rt.lastCompactedTokens = result.tokenEstimate;
|
|
427
431
|
rt.dedupAttempts++;
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
|
|
432
|
+
// Honest "tokens saved" for this session-instance only:
|
|
433
|
+
// new checkpoint → original − stored
|
|
434
|
+
// deduped onto existing → whole original region (nothing new stored)
|
|
435
|
+
// Resets to 0 on session_start (rt is rebuilt) — so a fresh session shows 0
|
|
436
|
+
// while the repo's cumulative saved (SQLite meta) keeps the running total.
|
|
437
|
+
const saved = result.deduped
|
|
438
|
+
? result.originalTokenEstimate
|
|
439
|
+
: Math.max(0, result.originalTokenEstimate - result.tokenEstimate);
|
|
440
|
+
rt.tokensSaved += saved;
|
|
433
441
|
if (result.deduped) rt.dedupSkips++;
|
|
434
442
|
|
|
443
|
+
// Record session activity + a daily-log entry in the per-repo SQLite store
|
|
444
|
+
// (foundation for resume-sessions / daily-log features). Best-effort — never
|
|
445
|
+
// block a compaction on bookkeeping.
|
|
446
|
+
try {
|
|
447
|
+
const repo = resolveRepoRoot(ctx.cwd);
|
|
448
|
+
touchSession(sid, repo, currentStateDir);
|
|
449
|
+
logDaily(sid, "compact", result.checkpointId, saved, currentStateDir);
|
|
450
|
+
} catch {
|
|
451
|
+
/* non-fatal: stats bookkeeping only */
|
|
452
|
+
}
|
|
453
|
+
|
|
435
454
|
// Sentinel marker: a non-LLM bookkeeping entry so subsequent triggers can
|
|
436
455
|
// skip re-vectorizing an already-compacted region (zero token cost).
|
|
437
456
|
pi.appendEntry(MARKER_TYPE, {
|
|
@@ -441,7 +460,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
441
460
|
deduped: result.deduped,
|
|
442
461
|
});
|
|
443
462
|
|
|
444
|
-
const saved = result.tokenEstimate;
|
|
445
463
|
setStatus(
|
|
446
464
|
ctx,
|
|
447
465
|
rt.persistedThisSession
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { findSuperseded, supersede } from "./supersede.js";
|
|
16
16
|
import { summarizeMessages, mergeCompactSummaries, formatCompactSummary } from "./compact.js";
|
|
17
17
|
import { extractiveSummarize } from "./extractive.js";
|
|
18
|
-
import { estimateSessionTokens } from "./tokens.js";
|
|
18
|
+
import { estimateSessionTokens, estimateBlockTokens } from "./tokens.js";
|
|
19
19
|
import { computeRegionHash, VectorStore, type SearchHit } from "./vectorStore.js";
|
|
20
20
|
import type { EngineMessage } from "./types.js";
|
|
21
21
|
|
|
@@ -51,6 +51,11 @@ export interface CompactResult {
|
|
|
51
51
|
summary: string;
|
|
52
52
|
regionHash: string;
|
|
53
53
|
tokenEstimate: number;
|
|
54
|
+
/** Token count of the original dropped region (before compaction). The honest
|
|
55
|
+
* "tokens saved" base = originalTokenEstimate − tokenEstimate (stored), or the
|
|
56
|
+
* full originalTokenEstimate when the region deduped onto an existing
|
|
57
|
+
* checkpoint (nothing new stored). */
|
|
58
|
+
originalTokenEstimate: number;
|
|
54
59
|
/** Index in `messages` where the compacted slice begins (for the caller to
|
|
55
60
|
* build a drop range). */
|
|
56
61
|
compactedFrom: number;
|
|
@@ -87,6 +92,7 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
87
92
|
summary: "",
|
|
88
93
|
regionHash: "",
|
|
89
94
|
tokenEstimate: 0,
|
|
95
|
+
originalTokenEstimate: 0,
|
|
90
96
|
compactedFrom,
|
|
91
97
|
};
|
|
92
98
|
}
|
|
@@ -105,7 +111,6 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
105
111
|
let keyDecisions: string[];
|
|
106
112
|
let nextSteps: string[];
|
|
107
113
|
let filesModified: string[];
|
|
108
|
-
let tokenEstimate: number;
|
|
109
114
|
|
|
110
115
|
if (useExtractive && !input.summary) {
|
|
111
116
|
const ext = extractiveSummarize(keep);
|
|
@@ -114,7 +119,6 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
114
119
|
keyDecisions = input.keyDecisions ?? ext.keyDecisions;
|
|
115
120
|
nextSteps = input.nextSteps ?? ext.nextSteps;
|
|
116
121
|
filesModified = input.filesModified ?? ext.filesModified;
|
|
117
|
-
tokenEstimate = input.tokenEstimate ?? ext.tokenEstimate;
|
|
118
122
|
} else {
|
|
119
123
|
const collapsed = input.summary ?? summarizeMessages(keep);
|
|
120
124
|
summary = formatCompactSummary(collapsed);
|
|
@@ -122,9 +126,18 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
122
126
|
keyDecisions = input.keyDecisions ?? [];
|
|
123
127
|
nextSteps = input.nextSteps ?? [];
|
|
124
128
|
filesModified = input.filesModified ?? [];
|
|
125
|
-
tokenEstimate = input.tokenEstimate ?? estimateSessionTokens(compactable);
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
// Honest "tokens saved" accounting:
|
|
132
|
+
// - originalTokenEstimate = the dropped region's token count (what context
|
|
133
|
+
// held before compaction) = the compacted slice's tokens.
|
|
134
|
+
// - storedTokens = the persisted summary's token count, computed from the
|
|
135
|
+
// actual summary string so it's honest for BOTH the extractive and legacy
|
|
136
|
+
// COLLAPSE paths (the legacy path's fallback estimateSessionTokens is the
|
|
137
|
+
// *original* size, not the stored size).
|
|
138
|
+
const originalTokenEstimate = estimateSessionTokens(compactable);
|
|
139
|
+
const storedTokens = estimateBlockTokens(summary);
|
|
140
|
+
|
|
128
141
|
// Region text = the compacted slice, used for dedup + embedding.
|
|
129
142
|
const regionText = input.regionText ?? keep.map((m) => m.text).join("\n");
|
|
130
143
|
const regionHash = computeRegionHash(regionText);
|
|
@@ -137,7 +150,8 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
137
150
|
nextSteps,
|
|
138
151
|
filesModified,
|
|
139
152
|
regionText,
|
|
140
|
-
tokenEstimate,
|
|
153
|
+
tokenEstimate: storedTokens,
|
|
154
|
+
originalTokenEstimate,
|
|
141
155
|
timestamp: input.timestamp ?? 0,
|
|
142
156
|
});
|
|
143
157
|
|
|
@@ -148,7 +162,8 @@ export function compactSession(input: CompactInput, store: VectorStore = getDefa
|
|
|
148
162
|
checkpointId: add.checkpoint.checkpointId,
|
|
149
163
|
summary,
|
|
150
164
|
regionHash,
|
|
151
|
-
tokenEstimate,
|
|
165
|
+
tokenEstimate: storedTokens,
|
|
166
|
+
originalTokenEstimate,
|
|
152
167
|
compactedFrom,
|
|
153
168
|
};
|
|
154
169
|
}
|
|
@@ -174,6 +174,38 @@ test("checkAllIntegrity covers every session", () => {
|
|
|
174
174
|
assert.ok(reports.every((r: { ok: boolean }) => r.ok));
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
+
// --- schema migration (v0.4.2: original_token_estimate) -------------------
|
|
178
|
+
|
|
179
|
+
test("Sprint 10 migration: pre-0.4.2 db gains original_token_estimate and repoStats works", () => {
|
|
180
|
+
const dir = join(baseTmp, `run-${counter++}`);
|
|
181
|
+
// Simulate a v0.4.1-era store: openStore() builds the full current schema,
|
|
182
|
+
// then drop the original_token_estimate column that 0.4.2 added. CREATE TABLE
|
|
183
|
+
// IF NOT EXISTS is a no-op on an existing table, which is exactly why the
|
|
184
|
+
// shipped v0.4.2 crashed at runtime on old repos ("no such column").
|
|
185
|
+
const db = openStore(dir);
|
|
186
|
+
db.exec("ALTER TABLE context_chunks DROP COLUMN original_token_estimate;");
|
|
187
|
+
closeStore(dir);
|
|
188
|
+
|
|
189
|
+
// Re-open through the real code path — must ALTER the column in, not crash.
|
|
190
|
+
const vs = new VectorStore({ dedupSim: 0.9, stateDir: dir });
|
|
191
|
+
vs.add({
|
|
192
|
+
sessionId: "sess_mig",
|
|
193
|
+
summary: "legacy region",
|
|
194
|
+
regionText: "some region text to compact",
|
|
195
|
+
tokenEstimate: 100,
|
|
196
|
+
originalTokenEstimate: 500,
|
|
197
|
+
timestamp: 1,
|
|
198
|
+
});
|
|
199
|
+
const repo = vs.repoStats();
|
|
200
|
+
// No "no such column" crash = migration succeeded; totals reflect the new col.
|
|
201
|
+
assert.equal(repo.checkpointCount, 1);
|
|
202
|
+
assert.equal(repo.originalTokens, 500, "originalTokens read from migrated column");
|
|
203
|
+
// Re-open a second time to prove idempotency (column already exists).
|
|
204
|
+
closeStore(dir);
|
|
205
|
+
const vs2 = new VectorStore({ dedupSim: 0.9, stateDir: dir });
|
|
206
|
+
assert.equal(vs2.repoStats().originalTokens, 500, "stable across re-open");
|
|
207
|
+
});
|
|
208
|
+
|
|
177
209
|
// --- cleanup ---------------------------------------------------------------
|
|
178
210
|
|
|
179
211
|
test("Sprint 10 cleanup", () => {
|
package/src/store/sqlite.ts
CHANGED
|
@@ -80,6 +80,7 @@ function initSchema(db: Database.Database): void {
|
|
|
80
80
|
files_modified TEXT, -- JSON array
|
|
81
81
|
embedding_blob BLOB, -- float32 vector
|
|
82
82
|
token_estimate INTEGER,
|
|
83
|
+
original_token_estimate INTEGER, -- dropped region size (tokens saved = orig − stored)
|
|
83
84
|
timestamp INTEGER,
|
|
84
85
|
dedup_status TEXT DEFAULT 'active',
|
|
85
86
|
compressed_original BLOB -- optional DR copy
|
|
@@ -146,6 +147,42 @@ function initSchema(db: Database.Database): void {
|
|
|
146
147
|
CREATE INDEX IF NOT EXISTS idx_raptor_session ON raptor_nodes(session_id);
|
|
147
148
|
CREATE INDEX IF NOT EXISTS idx_raptor_parent ON raptor_nodes(parent_id);
|
|
148
149
|
|
|
150
|
+
-- Foundation for future features (resume sessions, daily log, lessons
|
|
151
|
+
-- learned). Scaffolded now so all store data lives in SQLite from day one;
|
|
152
|
+
-- population is minimal (touchSession / logDaily on compact) and the full
|
|
153
|
+
-- UI/recall for these lands in later sprints.
|
|
154
|
+
|
|
155
|
+
-- Per-session registry (resume + per-repo session history).
|
|
156
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
157
|
+
session_id TEXT PRIMARY KEY,
|
|
158
|
+
repo TEXT,
|
|
159
|
+
started_at INTEGER,
|
|
160
|
+
ended_at INTEGER,
|
|
161
|
+
last_compacted_at INTEGER,
|
|
162
|
+
status TEXT DEFAULT 'active'
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
-- Append-only daily activity log (the "daily log" feature seed).
|
|
166
|
+
CREATE TABLE IF NOT EXISTS daily_log (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
day TEXT NOT NULL, -- YYYY-MM-DD
|
|
169
|
+
session_id TEXT,
|
|
170
|
+
event TEXT, -- e.g. 'compact'
|
|
171
|
+
detail TEXT,
|
|
172
|
+
tokens_saved INTEGER DEFAULT 0,
|
|
173
|
+
ts INTEGER
|
|
174
|
+
);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_daily_log_day ON daily_log(day);
|
|
176
|
+
|
|
177
|
+
-- Lessons learned (future recall/browse feature seed).
|
|
178
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
179
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
180
|
+
session_id TEXT,
|
|
181
|
+
repo TEXT,
|
|
182
|
+
lesson TEXT,
|
|
183
|
+
ts INTEGER
|
|
184
|
+
);
|
|
185
|
+
|
|
149
186
|
-- FTS5 trigram virtual table (Sprint 9+ pg_trgm-equivalent verification).
|
|
150
187
|
CREATE VIRTUAL TABLE IF NOT EXISTS context_chunks_trgm USING fts5(
|
|
151
188
|
id UNINDEXED,
|
|
@@ -153,6 +190,12 @@ function initSchema(db: Database.Database): void {
|
|
|
153
190
|
tokenize='trigram'
|
|
154
191
|
);
|
|
155
192
|
`);
|
|
193
|
+
// Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
|
|
194
|
+
// pre-existing table, so new columns added to context_chunks after a store was
|
|
195
|
+
// first created (e.g. original_token_estimate in v0.4.2) must be ALTERed in for
|
|
196
|
+
// databases created by an older version — otherwise repoStats()/upsert crash
|
|
197
|
+
// with "no such column" and the extension fails to load. Additive only.
|
|
198
|
+
ensureColumn(db, "context_chunks", "original_token_estimate", "INTEGER");
|
|
156
199
|
const v = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get() as
|
|
157
200
|
| { value: string }
|
|
158
201
|
| undefined;
|
|
@@ -161,6 +204,19 @@ function initSchema(db: Database.Database): void {
|
|
|
161
204
|
}
|
|
162
205
|
}
|
|
163
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Add `column` (with `decl`, e.g. "INTEGER") to `table` if it does not already
|
|
209
|
+
* exist. Idempotent: checks PRAGMA table_info first, so it is safe to run on
|
|
210
|
+
* every open. Table/column/decl are code-controlled constants (never user
|
|
211
|
+
* input), so the unavoidable identifier interpolation here does not violate
|
|
212
|
+
* PREVENT-002 (no external data reaches this SQL).
|
|
213
|
+
*/
|
|
214
|
+
function ensureColumn(db: Database.Database, table: string, column: string, decl: string): void {
|
|
215
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
216
|
+
if (cols.some((c) => c.name === column)) return;
|
|
217
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
164
220
|
/** Read a string-valued meta key (or undefined). Used for cumulative counters. */
|
|
165
221
|
export function getMeta(key: string, stateDir: string = getStateDir()): string | undefined {
|
|
166
222
|
const db = openStore(stateDir);
|
|
@@ -235,6 +291,65 @@ export function bumpDedupStats(deduped: boolean, stateDir: string = getStateDir(
|
|
|
235
291
|
if (deduped) incMeta("deduped", 1, stateDir);
|
|
236
292
|
}
|
|
237
293
|
|
|
294
|
+
// --- Future-feature foundation (resume sessions / daily log / lessons) -------
|
|
295
|
+
// Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
296
|
+
// day one. Full UI/recall for these lands in later sprints.
|
|
297
|
+
|
|
298
|
+
/** Upsert a `sessions` row (resume + per-repo session history). */
|
|
299
|
+
export function touchSession(
|
|
300
|
+
sessionId: string,
|
|
301
|
+
repo: string | undefined,
|
|
302
|
+
stateDir: string = getStateDir(),
|
|
303
|
+
): void {
|
|
304
|
+
const db = openStore(stateDir);
|
|
305
|
+
const sid = normalizeSessionId(sessionId);
|
|
306
|
+
const existing = db
|
|
307
|
+
.prepare("SELECT started_at FROM sessions WHERE session_id = ?")
|
|
308
|
+
.get(sid) as { started_at: number | null } | undefined;
|
|
309
|
+
const now = Math.floor(Date.now() / 1000);
|
|
310
|
+
if (!existing) {
|
|
311
|
+
db.prepare(
|
|
312
|
+
`INSERT INTO sessions(session_id, repo, started_at, last_compacted_at, status)
|
|
313
|
+
VALUES(?, ?, ?, ?, 'active')`,
|
|
314
|
+
).run(sid, repo ?? null, now, now);
|
|
315
|
+
} else {
|
|
316
|
+
db.prepare(
|
|
317
|
+
"UPDATE sessions SET last_compacted_at = ?, repo = COALESCE(?, repo), status = 'active' WHERE session_id = ?",
|
|
318
|
+
).run(now, repo ?? null, sid);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Append a `daily_log` entry (day = YYYY-MM-DD, local-naive from Date). */
|
|
323
|
+
export function logDaily(
|
|
324
|
+
sessionId: string,
|
|
325
|
+
event: string,
|
|
326
|
+
detail: string | undefined,
|
|
327
|
+
tokensSaved: number,
|
|
328
|
+
stateDir: string = getStateDir(),
|
|
329
|
+
): void {
|
|
330
|
+
const db = openStore(stateDir);
|
|
331
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
332
|
+
const now = Math.floor(Date.now() / 1000);
|
|
333
|
+
db.prepare(
|
|
334
|
+
`INSERT INTO daily_log(day, session_id, event, detail, tokens_saved, ts)
|
|
335
|
+
VALUES(?, ?, ?, ?, ?, ?)`,
|
|
336
|
+
).run(day, normalizeSessionId(sessionId), event, detail ?? null, tokensSaved, now);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Append a `lessons` entry (future lessons-learned browse/recall). */
|
|
340
|
+
export function addLesson(
|
|
341
|
+
sessionId: string,
|
|
342
|
+
repo: string | undefined,
|
|
343
|
+
lesson: string,
|
|
344
|
+
stateDir: string = getStateDir(),
|
|
345
|
+
): void {
|
|
346
|
+
const db = openStore(stateDir);
|
|
347
|
+
const now = Math.floor(Date.now() / 1000);
|
|
348
|
+
db.prepare(
|
|
349
|
+
`INSERT INTO lessons(session_id, repo, lesson, ts) VALUES(?, ?, ?, ?)`,
|
|
350
|
+
).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
|
|
351
|
+
}
|
|
352
|
+
|
|
238
353
|
/** Map a DB row to the public StoredCheckpoint shape. */
|
|
239
354
|
function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
240
355
|
return {
|
|
@@ -247,6 +362,7 @@ function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
|
247
362
|
nextSteps: row.next_steps ? JSON.parse(row.next_steps) : [],
|
|
248
363
|
filesModified: row.files_modified ? JSON.parse(row.files_modified) : [],
|
|
249
364
|
tokenEstimate: row.token_estimate ?? 0,
|
|
365
|
+
originalTokenEstimate: row.original_token_estimate ?? undefined,
|
|
250
366
|
regionHash: row.region_hash ?? "",
|
|
251
367
|
contentHash: row.content_hash ?? undefined,
|
|
252
368
|
contentHash2: row.content_hash2 ?? undefined,
|
|
@@ -269,11 +385,11 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
269
385
|
(id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
|
|
270
386
|
normalized_text, summary, topic_summary, summary_hash,
|
|
271
387
|
key_decisions, next_steps, files_modified, embedding_blob,
|
|
272
|
-
token_estimate, timestamp, dedup_status, compressed_original)
|
|
388
|
+
token_estimate, original_token_estimate, timestamp, dedup_status, compressed_original)
|
|
273
389
|
VALUES (@id, @sid, @region_hash, @content_hash, @content_hash2, @content_hash_version,
|
|
274
390
|
@normalized_text, @summary, @topic_summary, @summary_hash,
|
|
275
391
|
@key_decisions, @next_steps, @files_modified, @embedding_blob,
|
|
276
|
-
@token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
392
|
+
@token_estimate, @original_token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
277
393
|
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
278
394
|
summary=excluded.summary,
|
|
279
395
|
topic_summary=excluded.topic_summary,
|
|
@@ -283,6 +399,7 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
283
399
|
files_modified=excluded.files_modified,
|
|
284
400
|
embedding_blob=excluded.embedding_blob,
|
|
285
401
|
token_estimate=excluded.token_estimate,
|
|
402
|
+
original_token_estimate=excluded.original_token_estimate,
|
|
286
403
|
timestamp=excluded.timestamp,
|
|
287
404
|
dedup_status=excluded.dedup_status,
|
|
288
405
|
compressed_original=excluded.compressed_original`,
|
|
@@ -302,6 +419,7 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
302
419
|
files_modified: jsonText(cp.filesModified),
|
|
303
420
|
embedding_blob: encodeEmbedding(cp.embedding ?? []),
|
|
304
421
|
token_estimate: cp.tokenEstimate ?? 0,
|
|
422
|
+
original_token_estimate: cp.originalTokenEstimate ?? null,
|
|
305
423
|
timestamp: cp.timestamp ?? 0,
|
|
306
424
|
dedup_status: "active",
|
|
307
425
|
compressed_original: cp.compressedOriginal ?? null,
|
|
@@ -508,6 +626,8 @@ export interface RepoStats {
|
|
|
508
626
|
sessionCount: number;
|
|
509
627
|
/** Cumulative stored-summary tokens saved (Σ stored summaries). */
|
|
510
628
|
tokensSaved: number;
|
|
629
|
+
/** Sum of original dropped-region token estimates (repo-wide). */
|
|
630
|
+
originalTokens: number;
|
|
511
631
|
/** Cumulative dedup add() attempts (store-wide). */
|
|
512
632
|
dedupAttempts: number;
|
|
513
633
|
/** Cumulative deduped collapses (store-wide). */
|
|
@@ -521,14 +641,16 @@ export function repoStats(stateDir: string = getStateDir()): RepoStats {
|
|
|
521
641
|
const row = db
|
|
522
642
|
.prepare(
|
|
523
643
|
`SELECT COUNT(*) AS c, COALESCE(SUM(token_estimate),0) AS tok,
|
|
644
|
+
COALESCE(SUM(original_token_estimate),0) AS orig,
|
|
524
645
|
COUNT(DISTINCT session_id) AS sessions
|
|
525
646
|
FROM context_chunks WHERE dedup_status != 'removed'`,
|
|
526
647
|
)
|
|
527
|
-
.get() as { c: number; tok: number; sessions: number };
|
|
648
|
+
.get() as { c: number; tok: number; orig: number; sessions: number };
|
|
528
649
|
const ds = getDedupStats(stateDir);
|
|
529
650
|
return {
|
|
530
651
|
checkpointCount: row.c,
|
|
531
652
|
totalTokenEstimate: row.tok,
|
|
653
|
+
originalTokens: row.orig,
|
|
532
654
|
sessionCount: row.sessions,
|
|
533
655
|
tokensSaved: getMetaNumber("tokens_saved", stateDir),
|
|
534
656
|
dedupAttempts: ds.attempts,
|
package/src/store.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface StoredCheckpoint {
|
|
|
58
58
|
nextSteps: string[];
|
|
59
59
|
filesModified: string[];
|
|
60
60
|
tokenEstimate: number;
|
|
61
|
+
/** Token count of the ORIGINAL dropped region (before compaction). Drives the
|
|
62
|
+
* honest "tokens saved" = originalTokenEstimate − tokenEstimate. */
|
|
63
|
+
originalTokenEstimate?: number;
|
|
61
64
|
regionHash: string;
|
|
62
65
|
/** Primary content-addressable hash (full 64-hex SHA-256 of normalized text). */
|
|
63
66
|
contentHash?: string;
|
package/src/vectorStore.test.ts
CHANGED
|
@@ -250,47 +250,55 @@ test("stats reports counts, last checkpoint, and dedup rate", () => {
|
|
|
250
250
|
assert.ok(Math.abs(st2.dedupHitRate - 0.5) < 1e-9);
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
-
test("tokensSaved
|
|
253
|
+
test("tokensSaved = original − stored per session; deduped add saves the whole region", () => {
|
|
254
254
|
const s = store();
|
|
255
|
-
// Two genuinely new checkpoints
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
// Two genuinely new checkpoints. saved = original − stored.
|
|
256
|
+
// cp1: orig 2000, stored 500 → saved 1500
|
|
257
|
+
// cp2: orig 3000, stored 700 → saved 2300
|
|
258
|
+
s.add({ sessionId: "sess_saved", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, originalTokenEstimate: 2000, timestamp: 1 });
|
|
259
|
+
s.add({ sessionId: "sess_saved", summary: "beta", regionText: "region beta text", tokenEstimate: 700, originalTokenEstimate: 3000, timestamp: 2 });
|
|
258
260
|
const st = s.stats("sess_saved");
|
|
259
|
-
assert.equal(st.
|
|
261
|
+
assert.equal(st.totalTokenEstimate, 1200, "Σ stored summaries");
|
|
262
|
+
assert.equal(st.originalTokens, 5000, "Σ original region tokens");
|
|
263
|
+
assert.equal(st.tokensSaved, 3800, "per-session saved = Σ(original − stored) = 1500 + 2300");
|
|
260
264
|
assert.equal(st.dedupCollapsed, 0);
|
|
261
265
|
assert.equal(st.dedupAttempts, 2);
|
|
262
266
|
|
|
263
|
-
// A third add that dedups onto an existing region
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
const deduped = s.add({ sessionId: "sess_saved", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, timestamp: 3 });
|
|
267
|
+
// A third add that dedups onto an existing region: whole original region (2000)
|
|
268
|
+
// is discarded (nothing new stored) → repo saved grows by the full original,
|
|
269
|
+
// dedupCollapsed bumps, and no new checkpoint row is created.
|
|
270
|
+
const deduped = s.add({ sessionId: "sess_saved", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, originalTokenEstimate: 2000, timestamp: 3 });
|
|
267
271
|
assert.ok(deduped.deduped, "identical region should dedup");
|
|
268
272
|
const st3 = s.stats("sess_saved");
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
// Per-session DB sum only covers stored rows (deduped adds create no row), so
|
|
274
|
+
// the per-session figure is unchanged; the deduped save lands in the repo meta.
|
|
275
|
+
assert.equal(st3.tokensSaved, 3800, "per-session DB sum unchanged by deduped add");
|
|
276
|
+
assert.equal(st3.dedupCollapsed, 1, "deduped collapse counted");
|
|
271
277
|
assert.equal(st3.dedupAttempts, 3);
|
|
278
|
+
// Repo cumulative counter DID capture the deduped region's full original size.
|
|
279
|
+
assert.equal(s.repoStats().tokensSaved, 3800 + 2000, "repo saved includes deduped original");
|
|
272
280
|
});
|
|
273
281
|
|
|
274
|
-
test("repoStats aggregates every session
|
|
282
|
+
test("repoStats aggregates every session + counts deduped original tokens", () => {
|
|
275
283
|
const dir = join(baseTmp, `repo-${counter++}`);
|
|
276
284
|
const a = new VectorStore({ dedupSim: 0.9, stateDir: dir });
|
|
277
285
|
const b = new VectorStore({ dedupSim: 0.9, stateDir: dir }); // same disk store, diff instance
|
|
278
|
-
a.add({ sessionId: "sess_a", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, timestamp: 1 });
|
|
279
|
-
b.add({ sessionId: "sess_b", summary: "beta", regionText: "region beta text", tokenEstimate: 700, timestamp: 2 });
|
|
286
|
+
a.add({ sessionId: "sess_a", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, originalTokenEstimate: 2000, timestamp: 1 });
|
|
287
|
+
b.add({ sessionId: "sess_b", summary: "beta", regionText: "region beta text", tokenEstimate: 700, originalTokenEstimate: 3000, timestamp: 2 });
|
|
280
288
|
|
|
281
289
|
const repo = a.repoStats();
|
|
282
290
|
assert.equal(repo.checkpointCount, 2, "checkpoints across both sessions");
|
|
283
291
|
assert.equal(repo.sessionCount, 2, "two distinct sessions");
|
|
284
|
-
assert.equal(repo.totalTokenEstimate, 1200);
|
|
285
|
-
assert.equal(repo.
|
|
292
|
+
assert.equal(repo.totalTokenEstimate, 1200, "Σ stored");
|
|
293
|
+
assert.equal(repo.originalTokens, 5000, "Σ original");
|
|
294
|
+
assert.equal(repo.tokensSaved, 3800, "repo saved = Σ(original − stored) = 1500 + 2300");
|
|
286
295
|
assert.equal(repo.dedupCollapsed, 0);
|
|
287
296
|
|
|
288
|
-
// A deduped add into sess_a
|
|
289
|
-
|
|
290
|
-
const deduped = a.add({ sessionId: "sess_a", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, timestamp: 3 });
|
|
297
|
+
// A deduped add into sess_a: whole original region saved, no new row.
|
|
298
|
+
const deduped = a.add({ sessionId: "sess_a", summary: "alpha", regionText: "region alpha text", tokenEstimate: 500, originalTokenEstimate: 2000, timestamp: 3 });
|
|
291
299
|
assert.ok(deduped.deduped);
|
|
292
300
|
const repo2 = a.repoStats();
|
|
293
|
-
assert.equal(repo2.tokensSaved,
|
|
301
|
+
assert.equal(repo2.tokensSaved, 3800 + 2000, "deduped collapse adds full original region to repo saved");
|
|
294
302
|
assert.equal(repo2.dedupCollapsed, 1);
|
|
295
303
|
assert.equal(repo2.checkpointCount, 2, "still two stored checkpoints");
|
|
296
304
|
});
|
package/src/vectorStore.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface AddInput {
|
|
|
53
53
|
nextSteps?: string[];
|
|
54
54
|
filesModified?: string[];
|
|
55
55
|
tokenEstimate?: number;
|
|
56
|
+
/** Token count of the ORIGINAL dropped region (before compaction). Drives the
|
|
57
|
+
* honest "tokens saved" = originalTokenEstimate − tokenEstimate (stored), or
|
|
58
|
+
* the full originalTokenEstimate when the region dedups (nothing new stored).
|
|
59
|
+
* Optional for back-compat with direct add() callers; defaults to stored. */
|
|
60
|
+
originalTokenEstimate?: number;
|
|
56
61
|
/** Raw text of the compacted region — used to derive the regionHash + vector. */
|
|
57
62
|
regionText: string;
|
|
58
63
|
timestamp: number;
|
|
@@ -140,6 +145,10 @@ export class VectorStore {
|
|
|
140
145
|
const sessionId = normalizeSessionId(input.sessionId);
|
|
141
146
|
const regionHash = computeRegionHash(input.regionText);
|
|
142
147
|
const all = listCheckpoints(sessionId, this.stateDir);
|
|
148
|
+
// Honest "tokens saved" base for this region. For a deduped add the whole
|
|
149
|
+
// original region is discarded (nothing new stored); for a new checkpoint
|
|
150
|
+
// we persist (orig − stored). Falls back to stored when orig is unknown.
|
|
151
|
+
const origTokens = input.originalTokenEstimate ?? input.tokenEstimate ?? 0;
|
|
143
152
|
const cfg = this.cfg;
|
|
144
153
|
// Tracks whether a tier matched while in MARK_ONLY (record-but-don't-collapse),
|
|
145
154
|
// and which tier.
|
|
@@ -167,6 +176,8 @@ export class VectorStore {
|
|
|
167
176
|
contentMatch.timestamp = input.timestamp;
|
|
168
177
|
upsertCheckpoint(contentMatch, this.stateDir);
|
|
169
178
|
bumpDedupStats(true, this.stateDir);
|
|
179
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
180
|
+
addTokensSaved(origTokens, this.stateDir);
|
|
170
181
|
const r = { checkpoint: contentMatch, deduped: true, reason: "contentHash" };
|
|
171
182
|
this.record("L0", "deduped", "contentHash", Date.now() - t0);
|
|
172
183
|
return r;
|
|
@@ -182,6 +193,8 @@ export class VectorStore {
|
|
|
182
193
|
markOnly = "L0"; // fall through
|
|
183
194
|
} else {
|
|
184
195
|
bumpDedupStats(true, this.stateDir);
|
|
196
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
197
|
+
addTokensSaved(origTokens, this.stateDir);
|
|
185
198
|
const r = { checkpoint: regionMatch, deduped: true, reason: "regionHash" };
|
|
186
199
|
this.record("L0", "deduped", "regionHash", Date.now() - t0);
|
|
187
200
|
return r;
|
|
@@ -203,6 +216,8 @@ export class VectorStore {
|
|
|
203
216
|
summaryMatch.timestamp = input.timestamp;
|
|
204
217
|
upsertCheckpoint(summaryMatch, this.stateDir);
|
|
205
218
|
bumpDedupStats(true, this.stateDir);
|
|
219
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
220
|
+
addTokensSaved(origTokens, this.stateDir);
|
|
206
221
|
const r = { checkpoint: summaryMatch, deduped: true, reason: "summaryHash" };
|
|
207
222
|
this.record("L0", "deduped", "summaryHash", Date.now() - t0);
|
|
208
223
|
return r;
|
|
@@ -255,6 +270,8 @@ export class VectorStore {
|
|
|
255
270
|
nearest.checkpoint.timestamp = input.timestamp;
|
|
256
271
|
upsertCheckpoint(nearest.checkpoint, this.stateDir);
|
|
257
272
|
bumpDedupStats(true, this.stateDir);
|
|
273
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
274
|
+
addTokensSaved(origTokens, this.stateDir);
|
|
258
275
|
const r = { checkpoint: nearest.checkpoint, deduped: true, reason: "contentSimilarity" };
|
|
259
276
|
this.record("L2", "deduped", "contentSimilarity", Date.now() - t0);
|
|
260
277
|
return r;
|
|
@@ -275,6 +292,7 @@ export class VectorStore {
|
|
|
275
292
|
nextSteps: input.nextSteps ?? [],
|
|
276
293
|
filesModified: input.filesModified ?? [],
|
|
277
294
|
tokenEstimate: input.tokenEstimate ?? 0,
|
|
295
|
+
originalTokenEstimate: input.originalTokenEstimate,
|
|
278
296
|
regionHash,
|
|
279
297
|
contentHash: digest.contentHash,
|
|
280
298
|
contentHash2: digest.contentHash2,
|
|
@@ -287,9 +305,12 @@ export class VectorStore {
|
|
|
287
305
|
// Persistence is SQLite (store/sqlite.ts). upsertCheckpoint keeps the
|
|
288
306
|
// idempotent-by-id semantics the old JSON append implied.
|
|
289
307
|
upsertCheckpoint(checkpoint, this.stateDir);
|
|
290
|
-
// Cumulative "tokens saved" counter (per-repo SQLite meta).
|
|
291
|
-
//
|
|
292
|
-
|
|
308
|
+
// Cumulative "tokens saved" counter (per-repo SQLite meta). For a NEW
|
|
309
|
+
// checkpoint the saved amount is (original − stored); for a deduped add the
|
|
310
|
+
// whole original region is discarded (handled in the deduped return paths
|
|
311
|
+
// below). Survives sessions and travels with the repo.
|
|
312
|
+
const stored = input.tokenEstimate ?? 0;
|
|
313
|
+
addTokensSaved(Math.max(0, origTokens - stored), this.stateDir);
|
|
293
314
|
// L1: persist this checkpoint's MinHash signature + LSH buckets so future
|
|
294
315
|
// near-duplicate inserts can find it. Deterministic given the seed.
|
|
295
316
|
const sig = minhashSignature(input.regionText);
|
|
@@ -517,7 +538,8 @@ export class VectorStore {
|
|
|
517
538
|
injectedCount: number;
|
|
518
539
|
dedupHitRate: number; // injected / checkpoints, 0..1
|
|
519
540
|
storageDedupRate: number; // deduped adds / total adds, 0..1 (cumulative)
|
|
520
|
-
tokensSaved: number; //
|
|
541
|
+
tokensSaved: number; // Σ(original − stored) for this session's checkpoints
|
|
542
|
+
originalTokens: number; // Σ original region size for this session's checkpoints
|
|
521
543
|
dedupAttempts: number; // cumulative add() calls (store-wide)
|
|
522
544
|
dedupCollapsed: number; // cumulative deduped collapses (store-wide)
|
|
523
545
|
} {
|
|
@@ -530,10 +552,16 @@ export class VectorStore {
|
|
|
530
552
|
const last = ordered[ordered.length - 1];
|
|
531
553
|
const injected = state.injectedCheckpointIds.length;
|
|
532
554
|
const ds = getDedupStats(this.stateDir);
|
|
533
|
-
// Per-session "tokens saved" = this session's Σ stored summary token
|
|
534
|
-
// estimates (stored-sum definition). Equal to totalTokenEstimate by
|
|
535
|
-
// construction; repo-wide cumulative saved lives in repoStats().
|
|
536
555
|
const sessionTok = cps.reduce((s, c) => s + (c.tokenEstimate ?? 0), 0);
|
|
556
|
+
const sessionOrig = cps.reduce((s, c) => s + (c.originalTokenEstimate ?? 0), 0);
|
|
557
|
+
// Per-session "tokens saved" = Σ(original − stored) over this session's
|
|
558
|
+
// stored checkpoints. Deduped adds (whole region discarded, nothing stored)
|
|
559
|
+
// are counted in the repo-wide meta counter via repoStats(); the per-session
|
|
560
|
+
// DB sum here covers the rows that exist.
|
|
561
|
+
const sessionSaved = cps.reduce(
|
|
562
|
+
(s, c) => s + Math.max(0, (c.originalTokenEstimate ?? 0) - (c.tokenEstimate ?? 0)),
|
|
563
|
+
0,
|
|
564
|
+
);
|
|
537
565
|
return {
|
|
538
566
|
checkpointCount: cps.length,
|
|
539
567
|
totalTokenEstimate: sessionTok,
|
|
@@ -542,7 +570,8 @@ export class VectorStore {
|
|
|
542
570
|
injectedCount: injected,
|
|
543
571
|
dedupHitRate: cps.length === 0 ? 0 : injected / cps.length,
|
|
544
572
|
storageDedupRate: ds.attempts === 0 ? 0 : ds.deduped / ds.attempts,
|
|
545
|
-
tokensSaved:
|
|
573
|
+
tokensSaved: sessionSaved,
|
|
574
|
+
originalTokens: sessionOrig,
|
|
546
575
|
dedupAttempts: ds.attempts,
|
|
547
576
|
dedupCollapsed: ds.deduped,
|
|
548
577
|
};
|