pi-mega-compact 0.4.1 → 0.4.2
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/sqlite.ts +106 -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
|
}
|
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,
|
|
@@ -235,6 +272,65 @@ export function bumpDedupStats(deduped: boolean, stateDir: string = getStateDir(
|
|
|
235
272
|
if (deduped) incMeta("deduped", 1, stateDir);
|
|
236
273
|
}
|
|
237
274
|
|
|
275
|
+
// --- Future-feature foundation (resume sessions / daily log / lessons) -------
|
|
276
|
+
// Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
277
|
+
// day one. Full UI/recall for these lands in later sprints.
|
|
278
|
+
|
|
279
|
+
/** Upsert a `sessions` row (resume + per-repo session history). */
|
|
280
|
+
export function touchSession(
|
|
281
|
+
sessionId: string,
|
|
282
|
+
repo: string | undefined,
|
|
283
|
+
stateDir: string = getStateDir(),
|
|
284
|
+
): void {
|
|
285
|
+
const db = openStore(stateDir);
|
|
286
|
+
const sid = normalizeSessionId(sessionId);
|
|
287
|
+
const existing = db
|
|
288
|
+
.prepare("SELECT started_at FROM sessions WHERE session_id = ?")
|
|
289
|
+
.get(sid) as { started_at: number | null } | undefined;
|
|
290
|
+
const now = Math.floor(Date.now() / 1000);
|
|
291
|
+
if (!existing) {
|
|
292
|
+
db.prepare(
|
|
293
|
+
`INSERT INTO sessions(session_id, repo, started_at, last_compacted_at, status)
|
|
294
|
+
VALUES(?, ?, ?, ?, 'active')`,
|
|
295
|
+
).run(sid, repo ?? null, now, now);
|
|
296
|
+
} else {
|
|
297
|
+
db.prepare(
|
|
298
|
+
"UPDATE sessions SET last_compacted_at = ?, repo = COALESCE(?, repo), status = 'active' WHERE session_id = ?",
|
|
299
|
+
).run(now, repo ?? null, sid);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Append a `daily_log` entry (day = YYYY-MM-DD, local-naive from Date). */
|
|
304
|
+
export function logDaily(
|
|
305
|
+
sessionId: string,
|
|
306
|
+
event: string,
|
|
307
|
+
detail: string | undefined,
|
|
308
|
+
tokensSaved: number,
|
|
309
|
+
stateDir: string = getStateDir(),
|
|
310
|
+
): void {
|
|
311
|
+
const db = openStore(stateDir);
|
|
312
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
313
|
+
const now = Math.floor(Date.now() / 1000);
|
|
314
|
+
db.prepare(
|
|
315
|
+
`INSERT INTO daily_log(day, session_id, event, detail, tokens_saved, ts)
|
|
316
|
+
VALUES(?, ?, ?, ?, ?, ?)`,
|
|
317
|
+
).run(day, normalizeSessionId(sessionId), event, detail ?? null, tokensSaved, now);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Append a `lessons` entry (future lessons-learned browse/recall). */
|
|
321
|
+
export function addLesson(
|
|
322
|
+
sessionId: string,
|
|
323
|
+
repo: string | undefined,
|
|
324
|
+
lesson: string,
|
|
325
|
+
stateDir: string = getStateDir(),
|
|
326
|
+
): void {
|
|
327
|
+
const db = openStore(stateDir);
|
|
328
|
+
const now = Math.floor(Date.now() / 1000);
|
|
329
|
+
db.prepare(
|
|
330
|
+
`INSERT INTO lessons(session_id, repo, lesson, ts) VALUES(?, ?, ?, ?)`,
|
|
331
|
+
).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
|
|
332
|
+
}
|
|
333
|
+
|
|
238
334
|
/** Map a DB row to the public StoredCheckpoint shape. */
|
|
239
335
|
function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
240
336
|
return {
|
|
@@ -247,6 +343,7 @@ function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
|
247
343
|
nextSteps: row.next_steps ? JSON.parse(row.next_steps) : [],
|
|
248
344
|
filesModified: row.files_modified ? JSON.parse(row.files_modified) : [],
|
|
249
345
|
tokenEstimate: row.token_estimate ?? 0,
|
|
346
|
+
originalTokenEstimate: row.original_token_estimate ?? undefined,
|
|
250
347
|
regionHash: row.region_hash ?? "",
|
|
251
348
|
contentHash: row.content_hash ?? undefined,
|
|
252
349
|
contentHash2: row.content_hash2 ?? undefined,
|
|
@@ -269,11 +366,11 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
269
366
|
(id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
|
|
270
367
|
normalized_text, summary, topic_summary, summary_hash,
|
|
271
368
|
key_decisions, next_steps, files_modified, embedding_blob,
|
|
272
|
-
token_estimate, timestamp, dedup_status, compressed_original)
|
|
369
|
+
token_estimate, original_token_estimate, timestamp, dedup_status, compressed_original)
|
|
273
370
|
VALUES (@id, @sid, @region_hash, @content_hash, @content_hash2, @content_hash_version,
|
|
274
371
|
@normalized_text, @summary, @topic_summary, @summary_hash,
|
|
275
372
|
@key_decisions, @next_steps, @files_modified, @embedding_blob,
|
|
276
|
-
@token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
373
|
+
@token_estimate, @original_token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
277
374
|
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
278
375
|
summary=excluded.summary,
|
|
279
376
|
topic_summary=excluded.topic_summary,
|
|
@@ -283,6 +380,7 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
283
380
|
files_modified=excluded.files_modified,
|
|
284
381
|
embedding_blob=excluded.embedding_blob,
|
|
285
382
|
token_estimate=excluded.token_estimate,
|
|
383
|
+
original_token_estimate=excluded.original_token_estimate,
|
|
286
384
|
timestamp=excluded.timestamp,
|
|
287
385
|
dedup_status=excluded.dedup_status,
|
|
288
386
|
compressed_original=excluded.compressed_original`,
|
|
@@ -302,6 +400,7 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
302
400
|
files_modified: jsonText(cp.filesModified),
|
|
303
401
|
embedding_blob: encodeEmbedding(cp.embedding ?? []),
|
|
304
402
|
token_estimate: cp.tokenEstimate ?? 0,
|
|
403
|
+
original_token_estimate: cp.originalTokenEstimate ?? null,
|
|
305
404
|
timestamp: cp.timestamp ?? 0,
|
|
306
405
|
dedup_status: "active",
|
|
307
406
|
compressed_original: cp.compressedOriginal ?? null,
|
|
@@ -508,6 +607,8 @@ export interface RepoStats {
|
|
|
508
607
|
sessionCount: number;
|
|
509
608
|
/** Cumulative stored-summary tokens saved (Σ stored summaries). */
|
|
510
609
|
tokensSaved: number;
|
|
610
|
+
/** Sum of original dropped-region token estimates (repo-wide). */
|
|
611
|
+
originalTokens: number;
|
|
511
612
|
/** Cumulative dedup add() attempts (store-wide). */
|
|
512
613
|
dedupAttempts: number;
|
|
513
614
|
/** Cumulative deduped collapses (store-wide). */
|
|
@@ -521,14 +622,16 @@ export function repoStats(stateDir: string = getStateDir()): RepoStats {
|
|
|
521
622
|
const row = db
|
|
522
623
|
.prepare(
|
|
523
624
|
`SELECT COUNT(*) AS c, COALESCE(SUM(token_estimate),0) AS tok,
|
|
625
|
+
COALESCE(SUM(original_token_estimate),0) AS orig,
|
|
524
626
|
COUNT(DISTINCT session_id) AS sessions
|
|
525
627
|
FROM context_chunks WHERE dedup_status != 'removed'`,
|
|
526
628
|
)
|
|
527
|
-
.get() as { c: number; tok: number; sessions: number };
|
|
629
|
+
.get() as { c: number; tok: number; orig: number; sessions: number };
|
|
528
630
|
const ds = getDedupStats(stateDir);
|
|
529
631
|
return {
|
|
530
632
|
checkpointCount: row.c,
|
|
531
633
|
totalTokenEstimate: row.tok,
|
|
634
|
+
originalTokens: row.orig,
|
|
532
635
|
sessionCount: row.sessions,
|
|
533
636
|
tokensSaved: getMetaNumber("tokens_saved", stateDir),
|
|
534
637
|
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
|
};
|