pi-mega-compact 0.7.8 → 0.7.9
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/README.md +11 -12
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/html.js +756 -0
- package/dist/extensions/dashboard-server/index-reader.js +133 -0
- package/dist/extensions/dashboard-server/server.js +370 -0
- package/dist/extensions/dashboard-server/snapshot.js +43 -0
- package/dist/extensions/dashboard-server/state.js +30 -0
- package/dist/extensions/dashboard-server/types.js +5 -0
- package/dist/extensions/dashboard-server.js +7 -1315
- package/dist/extensions/mega-commands.js +162 -134
- package/dist/extensions/mega-compact.test.js +90 -21
- package/dist/extensions/mega-conflict-cmds.js +5 -1
- package/dist/extensions/mega-dashboard-cmds.js +29 -22
- package/dist/extensions/mega-db-cmds.js +11 -2
- package/dist/extensions/mega-events/agent-handlers.js +173 -0
- package/dist/extensions/mega-events/compact-handlers.js +133 -0
- package/dist/extensions/mega-events/context-handler.js +249 -0
- package/dist/extensions/mega-events/register.js +21 -0
- package/dist/extensions/mega-events/session-handlers.js +142 -0
- package/dist/extensions/mega-events.js +15 -699
- package/dist/extensions/mega-pipeline/compact.js +324 -0
- package/dist/extensions/mega-pipeline/memory-review.js +38 -0
- package/dist/extensions/mega-pipeline/recall.js +147 -0
- package/dist/extensions/mega-pipeline.js +9 -480
- package/dist/extensions/mega-runtime/helpers.js +40 -0
- package/dist/extensions/mega-runtime/query.js +29 -0
- package/dist/extensions/mega-runtime/state.js +711 -0
- package/dist/extensions/mega-runtime/widget.js +197 -0
- package/dist/extensions/mega-runtime.js +15 -947
- package/dist/src/store/sqlite/checkpoints.js +145 -0
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/dedup-mirror.js +64 -0
- package/dist/src/store/sqlite/foundation.js +38 -0
- package/dist/src/store/sqlite/global-index.js +224 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/maintenance.js +235 -0
- package/dist/src/store/sqlite/memories.js +164 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/meta.js +82 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/model-snapshots.js +47 -0
- package/dist/src/store/sqlite/raptor.js +57 -0
- package/dist/src/store/sqlite/raw-transcript.js +134 -0
- package/dist/src/store/sqlite/schema.js +250 -0
- package/dist/src/store/sqlite/session-state.js +28 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/stats.js +66 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/store/sqlite/utils.js +120 -0
- package/dist/src/store/sqlite.js +20 -1607
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-server/html.ts +758 -0
- package/extensions/dashboard-server/index-reader.ts +130 -0
- package/extensions/dashboard-server/server.ts +358 -0
- package/extensions/dashboard-server/snapshot.ts +44 -0
- package/extensions/dashboard-server/state.ts +33 -0
- package/extensions/dashboard-server/types.ts +134 -0
- package/extensions/dashboard-server.ts +7 -1431
- package/extensions/mega-commands.ts +33 -10
- package/extensions/mega-compact.test.ts +198 -43
- package/extensions/mega-conflict-cmds.ts +6 -2
- package/extensions/mega-dashboard-cmds.ts +30 -23
- package/extensions/mega-db-cmds.ts +11 -3
- package/extensions/mega-events/agent-handlers.ts +214 -0
- package/extensions/mega-events/compact-handlers.ts +164 -0
- package/extensions/mega-events/context-handler.ts +290 -0
- package/extensions/mega-events/register.ts +37 -0
- package/extensions/mega-events/session-handlers.ts +165 -0
- package/extensions/mega-events.ts +15 -780
- package/extensions/mega-pipeline/compact.ts +366 -0
- package/extensions/mega-pipeline/memory-review.ts +46 -0
- package/extensions/mega-pipeline/recall.ts +165 -0
- package/extensions/mega-pipeline.ts +9 -537
- package/extensions/mega-runtime/helpers.ts +68 -0
- package/extensions/mega-runtime/query.ts +29 -0
- package/extensions/mega-runtime/state.ts +797 -0
- package/extensions/mega-runtime/widget.ts +258 -0
- package/extensions/mega-runtime.ts +15 -1093
- package/package.json +4 -3
- package/src/store/sqlite/checkpoints.ts +204 -0
- package/src/store/sqlite/dedup-mirror.ts +114 -0
- package/src/store/sqlite/foundation.ts +63 -0
- package/src/store/sqlite/global-index.ts +305 -0
- package/src/store/sqlite/maintenance.ts +294 -0
- package/src/store/sqlite/memories.ts +217 -0
- package/src/store/sqlite/meta.ts +108 -0
- package/src/store/sqlite/model-snapshots.ts +83 -0
- package/src/store/sqlite/raptor.ts +107 -0
- package/src/store/sqlite/raw-transcript.ts +221 -0
- package/src/store/sqlite/schema.ts +258 -0
- package/src/store/sqlite/session-state.ts +38 -0
- package/src/store/sqlite/stats.ts +127 -0
- package/src/store/sqlite/utils.ts +125 -0
- package/src/store/sqlite.ts +20 -2204
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memories.ts — durable "save to memory" store (taken over from memory extensions).
|
|
3
|
+
*
|
|
4
|
+
* One SQLite store for user-saved memories, scoped by repo. Mirrors the
|
|
5
|
+
* lessons/sessions pattern: all state lives in SQLite from day one.
|
|
6
|
+
*/
|
|
7
|
+
import { getStateDir } from "../../store.js";
|
|
8
|
+
import { openStore } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
// S24 storage hardening: keep each memory row bounded so the durable store can
|
|
11
|
+
// never blow a downstream consumer's per-entry buffer (e.g. pi's native
|
|
12
|
+
// file-backed memory caps a single entry at ~5k chars). We truncate content at
|
|
13
|
+
// MEMORY_MAX_CHARS and evict the least-recently-referenced rows past
|
|
14
|
+
// MEMORY_MAX_ROWS per repo via LRU. Both are SQLite-only (PREVENT-PI-004): no
|
|
15
|
+
// file-backed memory is written anywhere. Defaults are overridable via env
|
|
16
|
+
// (MEGACOMPACT_MEMORY_MAX_CHARS / MEGACOMPACT_MEMORY_MAX_ROWS).
|
|
17
|
+
export const MEMORY_MAX_CHARS = 4000;
|
|
18
|
+
export const MEMORY_MAX_ROWS = 500;
|
|
19
|
+
|
|
20
|
+
/** Read an env override as a positive int, falling back to `fallback`. */
|
|
21
|
+
function envInt(name: string, fallback: number): number {
|
|
22
|
+
const v = process.env[name];
|
|
23
|
+
if (v == null || v === "") return fallback;
|
|
24
|
+
const n = Number(v);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Effective per-entry char cap (env-overridable, default MEMORY_MAX_CHARS). */
|
|
29
|
+
export function memoryMaxChars(): number {
|
|
30
|
+
return envInt("MEGACOMPACT_MEMORY_MAX_CHARS", MEMORY_MAX_CHARS);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Effective per-repo row cap (env-overridable, default MEMORY_MAX_ROWS). */
|
|
34
|
+
export function memoryMaxRows(): number {
|
|
35
|
+
return envInt("MEGACOMPACT_MEMORY_MAX_ROWS", MEMORY_MAX_ROWS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Truncate memory content to the per-entry cap, preserving a trailing marker. */
|
|
39
|
+
function capMemoryContent(content: string): string {
|
|
40
|
+
const cap = memoryMaxChars();
|
|
41
|
+
if (content.length <= cap) return content;
|
|
42
|
+
return content.slice(0, cap) + "…[truncated]";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Evict the least-recently-referenced rows for a repo past MEMORY_MAX_ROWS.
|
|
47
|
+
* LRU key = COALESCE(last_referenced, last_recalled_at, created_at) so a memory
|
|
48
|
+
* that is recalled/referenced survives over a stale one. Best-effort: any error
|
|
49
|
+
* is swallowed by the caller. Repo-scoped so one noisy repo can't evict another.
|
|
50
|
+
*/
|
|
51
|
+
function evictMemoryLru(repo: string | null, stateDir: string): void {
|
|
52
|
+
const db = openStore(stateDir);
|
|
53
|
+
const maxRows = memoryMaxRows();
|
|
54
|
+
// SQLite `= NULL` is never true, so the null-repo scope (memories are
|
|
55
|
+
// stateDir-scoped when repo is null — the applyMemoryOps path) needs `IS NULL`.
|
|
56
|
+
const where = repo == null ? "repo IS NULL" : "repo = ?";
|
|
57
|
+
const countRow = repo == null
|
|
58
|
+
? db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE ${where}`).get()
|
|
59
|
+
: db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE ${where}`).get(repo);
|
|
60
|
+
const count = (countRow as { n: number }).n;
|
|
61
|
+
const over = count - maxRows;
|
|
62
|
+
if (over <= 0) return;
|
|
63
|
+
// Delete the `over` least-recently-used rows. ORDER BY the LRU key ASC, id ASC
|
|
64
|
+
// (id ASC breaks ties deterministically — oldest created first). The `where`
|
|
65
|
+
// clause is a code-controlled constant (never user input) → PREVENT-002 OK.
|
|
66
|
+
const sql =
|
|
67
|
+
`DELETE FROM memories WHERE ${where} AND id IN (
|
|
68
|
+
SELECT id FROM memories WHERE ${where}
|
|
69
|
+
ORDER BY COALESCE(last_referenced, last_recalled_at, created_at) ASC, id ASC
|
|
70
|
+
LIMIT ?
|
|
71
|
+
)`;
|
|
72
|
+
if (repo == null) db.prepare(sql).run(over);
|
|
73
|
+
else db.prepare(sql).run(repo, repo, over);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface MemoryRecord {
|
|
77
|
+
id: number;
|
|
78
|
+
repo: string | null;
|
|
79
|
+
kind: string;
|
|
80
|
+
content: string;
|
|
81
|
+
tags: string[];
|
|
82
|
+
createdAt: number;
|
|
83
|
+
lastRecalledAt: number | null;
|
|
84
|
+
category: string | null;
|
|
85
|
+
target: string | null;
|
|
86
|
+
lastReferenced: number | null;
|
|
87
|
+
sourceTurn: number | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Save a memory to the current repo's store. Returns the new row id.
|
|
91
|
+
* S24 hardening: content is truncated to MEMORY_MAX_CHARS and, once the per-repo
|
|
92
|
+
* row count exceeds MEMORY_MAX_ROWS, the least-recently-used rows are evicted
|
|
93
|
+
* (LRU) so the store stays bounded. */
|
|
94
|
+
export function addMemory(
|
|
95
|
+
memory: { kind?: string; content: string; tags?: string[]; category?: string; target?: string; sourceTurn?: number },
|
|
96
|
+
repo: string | null,
|
|
97
|
+
stateDir: string = getStateDir(),
|
|
98
|
+
): number {
|
|
99
|
+
const db = openStore(stateDir);
|
|
100
|
+
const now = Math.floor(Date.now() / 1000);
|
|
101
|
+
const res = db
|
|
102
|
+
.prepare(
|
|
103
|
+
`INSERT INTO memories(repo, kind, content, tags, created_at, last_recalled_at, category, target, source_turn)
|
|
104
|
+
VALUES(?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
|
|
105
|
+
)
|
|
106
|
+
.run(
|
|
107
|
+
repo ?? null,
|
|
108
|
+
memory.kind ?? "note",
|
|
109
|
+
capMemoryContent(memory.content),
|
|
110
|
+
JSON.stringify(memory.tags ?? []),
|
|
111
|
+
now,
|
|
112
|
+
memory.category ?? null,
|
|
113
|
+
memory.target ?? null,
|
|
114
|
+
memory.sourceTurn ?? null,
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
evictMemoryLru(repo, stateDir);
|
|
118
|
+
} catch {
|
|
119
|
+
/* non-fatal: eviction must never fail an add */
|
|
120
|
+
}
|
|
121
|
+
return Number(res.lastInsertRowid);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** List recent memories for a repo (or all repos when repo is null). */
|
|
125
|
+
export function listMemories(repo: string | null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
|
|
126
|
+
const db = openStore(stateDir);
|
|
127
|
+
const rows = repo
|
|
128
|
+
? db.prepare("SELECT * FROM memories WHERE repo = ? ORDER BY created_at DESC LIMIT ?").all(repo, limit)
|
|
129
|
+
: db.prepare("SELECT * FROM memories ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
130
|
+
return (rows as any[]).map(mapMemoryRow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Substring search across content + tags. */
|
|
134
|
+
export function searchMemories(query: string, repo: string | null = null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
|
|
135
|
+
const db = openStore(stateDir);
|
|
136
|
+
const like = `%${query}%`;
|
|
137
|
+
const rows = repo
|
|
138
|
+
? db.prepare("SELECT * FROM memories WHERE repo = ? AND (content LIKE ? OR tags LIKE ?) ORDER BY created_at DESC LIMIT ?").all(repo, like, like, limit)
|
|
139
|
+
: db.prepare("SELECT * FROM memories WHERE content LIKE ? OR tags LIKE ? ORDER BY created_at DESC LIMIT ?").all(like, like, limit);
|
|
140
|
+
return (rows as any[]).map(mapMemoryRow);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Mark a memory as recalled (updates last_recalled_at). Returns true if found. */
|
|
144
|
+
export function recallMemory(id: number, stateDir: string = getStateDir()): boolean {
|
|
145
|
+
const db = openStore(stateDir);
|
|
146
|
+
const now = Math.floor(Date.now() / 1000);
|
|
147
|
+
const res = db.prepare("UPDATE memories SET last_recalled_at = ? WHERE id = ?").run(now, id);
|
|
148
|
+
return res.changes > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Mark a memory as referenced (updates last_referenced). Returns true if found. */
|
|
152
|
+
export function referenceMemory(id: number, stateDir: string = getStateDir()): boolean {
|
|
153
|
+
const db = openStore(stateDir);
|
|
154
|
+
const now = Math.floor(Date.now() / 1000);
|
|
155
|
+
const res = db.prepare("UPDATE memories SET last_referenced = ? WHERE id = ?").run(now, id);
|
|
156
|
+
return res.changes > 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Replace a memory's mutable fields by id. Returns true if a row was updated. */
|
|
160
|
+
export function replaceMemory(
|
|
161
|
+
id: number,
|
|
162
|
+
patch: { kind?: string; content?: string; tags?: string[]; category?: string; target?: string; sourceTurn?: number },
|
|
163
|
+
stateDir: string = getStateDir(),
|
|
164
|
+
): boolean {
|
|
165
|
+
const db = openStore(stateDir);
|
|
166
|
+
const res = db
|
|
167
|
+
.prepare(
|
|
168
|
+
`UPDATE memories
|
|
169
|
+
SET kind = COALESCE(?, kind),
|
|
170
|
+
content = COALESCE(?, content),
|
|
171
|
+
tags = COALESCE(?, tags),
|
|
172
|
+
category = COALESCE(?, category),
|
|
173
|
+
target = COALESCE(?, target),
|
|
174
|
+
source_turn = COALESCE(?, source_turn)
|
|
175
|
+
WHERE id = ?`,
|
|
176
|
+
)
|
|
177
|
+
.run(
|
|
178
|
+
patch.kind ?? null,
|
|
179
|
+
patch.content != null ? capMemoryContent(patch.content) : null,
|
|
180
|
+
patch.tags ? JSON.stringify(patch.tags) : null,
|
|
181
|
+
"category" in patch ? (patch.category ?? null) : null,
|
|
182
|
+
"target" in patch ? (patch.target ?? null) : null,
|
|
183
|
+
"sourceTurn" in patch ? (patch.sourceTurn ?? null) : null,
|
|
184
|
+
id,
|
|
185
|
+
);
|
|
186
|
+
return res.changes > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Remove a memory by id. Returns true if a row was deleted. */
|
|
190
|
+
export function removeMemory(id: number, stateDir: string = getStateDir()): boolean {
|
|
191
|
+
const db = openStore(stateDir);
|
|
192
|
+
const res = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
193
|
+
return res.changes > 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Look up a single memory by id (or undefined). */
|
|
197
|
+
export function getMemory(id: number, stateDir: string = getStateDir()): MemoryRecord | undefined {
|
|
198
|
+
const db = openStore(stateDir);
|
|
199
|
+
const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
200
|
+
return row ? mapMemoryRow(row) : undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function mapMemoryRow(row: any): MemoryRecord {
|
|
204
|
+
return {
|
|
205
|
+
id: row.id,
|
|
206
|
+
repo: row.repo ?? null,
|
|
207
|
+
kind: row.kind ?? "note",
|
|
208
|
+
content: row.content ?? "",
|
|
209
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
210
|
+
createdAt: row.created_at ?? 0,
|
|
211
|
+
lastRecalledAt: row.last_recalled_at ?? null,
|
|
212
|
+
category: row.category ?? null,
|
|
213
|
+
target: row.target ?? null,
|
|
214
|
+
lastReferenced: row.last_referenced ?? null,
|
|
215
|
+
sourceTurn: row.source_turn ?? null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta.ts — `meta` table key/value helpers + cumulative counters
|
|
3
|
+
* (tokens_saved, dedup stats, compact count, recall injected, cache-hit tokens).
|
|
4
|
+
*/
|
|
5
|
+
import { getStateDir } from "../../store.js";
|
|
6
|
+
import { openStore } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
/** Read a string-valued meta key (or undefined). Used for cumulative counters. */
|
|
9
|
+
export function getMeta(key: string, stateDir: string = getStateDir()): string | undefined {
|
|
10
|
+
const db = openStore(stateDir);
|
|
11
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key) as
|
|
12
|
+
| { value: string }
|
|
13
|
+
| undefined;
|
|
14
|
+
return row?.value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cumulative "tokens saved" — the sum of stored checkpoint token estimates across
|
|
19
|
+
* all compactions in this store (one per repo). Persisted in the SQLite `meta`
|
|
20
|
+
* table so it survives session restarts and travels with the repo's state dir,
|
|
21
|
+
* mirroring how `storageDedupRate` is cumulative. Incremented in VectorStore.add()
|
|
22
|
+
* when a new (non-deduped) checkpoint is persisted.
|
|
23
|
+
*/
|
|
24
|
+
export function getTokensSaved(stateDir: string = getStateDir()): number {
|
|
25
|
+
const raw = getMeta("tokens_saved", stateDir);
|
|
26
|
+
const n = raw == null ? 0 : Number(raw);
|
|
27
|
+
return Number.isFinite(n) ? n : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Add `delta` (>=0) to the cumulative tokens-saved counter. */
|
|
31
|
+
export function addTokensSaved(delta: number, stateDir: string = getStateDir()): void {
|
|
32
|
+
if (!(delta > 0)) return;
|
|
33
|
+
const db = openStore(stateDir);
|
|
34
|
+
db.prepare(
|
|
35
|
+
`INSERT INTO meta(key, value) VALUES('tokens_saved', ?)
|
|
36
|
+
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + ? AS TEXT)`,
|
|
37
|
+
).run(String(delta), delta);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Cumulative store-wide dedup accounting (Sprint 9+). Persisted in the SQLite
|
|
41
|
+
* `meta` table so it survives session restarts and travels with the repo's
|
|
42
|
+
* state dir — mirroring `tokens_saved`. Replaces the legacy JSON
|
|
43
|
+
* `dedup-stats.json` file (all stats now live in the SQLite store). */
|
|
44
|
+
export interface DedupStats {
|
|
45
|
+
/** Total add() calls (new checkpoints + deduped collapses). */
|
|
46
|
+
attempts: number;
|
|
47
|
+
/** add() calls that collapsed onto an existing checkpoint. */
|
|
48
|
+
deduped: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Read a store-wide integer counter from the meta table (0 if absent). */
|
|
52
|
+
export function getMetaNumber(key: string, stateDir: string = getStateDir()): number {
|
|
53
|
+
const raw = getMeta(key, stateDir);
|
|
54
|
+
const n = raw == null ? 0 : Number(raw);
|
|
55
|
+
return Number.isFinite(n) ? n : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Atomically add `delta` to an integer meta counter. */
|
|
59
|
+
function incMeta(key: string, delta: number, stateDir: string = getStateDir()): void {
|
|
60
|
+
if (!(delta > 0)) return;
|
|
61
|
+
const db = openStore(stateDir);
|
|
62
|
+
db.prepare(
|
|
63
|
+
`INSERT INTO meta(key, value) VALUES(?, ?)
|
|
64
|
+
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + ? AS TEXT)`,
|
|
65
|
+
).run(key, String(delta), delta);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read the cumulative store-wide dedup counters. */
|
|
69
|
+
export function getDedupStats(stateDir: string = getStateDir()): DedupStats {
|
|
70
|
+
return {
|
|
71
|
+
attempts: getMetaNumber("dedup_attempts", stateDir),
|
|
72
|
+
deduped: getMetaNumber("deduped", stateDir),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Increment the store-wide dedup counters for one add() call. */
|
|
77
|
+
export function bumpDedupStats(deduped: boolean, stateDir: string = getStateDir()): void {
|
|
78
|
+
incMeta("dedup_attempts", 1, stateDir);
|
|
79
|
+
if (deduped) incMeta("deduped", 1, stateDir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Live dashboard counters (schemaless meta key/value — NO migration) -----
|
|
83
|
+
// These reuse the private `incMeta` atomically-incrementing integer counter so
|
|
84
|
+
// all cumulative tallies live in the same `meta` table as tokens_saved etc.
|
|
85
|
+
|
|
86
|
+
export function incCompactCount(stateDir: string = getStateDir()): void {
|
|
87
|
+
incMeta("compact_count", 1, stateDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getCompactCount(stateDir: string = getStateDir()): number {
|
|
91
|
+
return getMetaNumber("compact_count", stateDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function incRecallInjected(n: number, stateDir: string = getStateDir()): void {
|
|
95
|
+
if (n > 0) incMeta("recall_injected", n, stateDir);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getRecallInjected(stateDir: string = getStateDir()): number {
|
|
99
|
+
return getMetaNumber("recall_injected", stateDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function incCacheHitTokens(delta: number, stateDir: string = getStateDir()): void {
|
|
103
|
+
if (delta > 0) incMeta("cache_hit_tokens_saved", delta, stateDir);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getCacheHitTokensSaved(stateDir: string = getStateDir()): number {
|
|
107
|
+
return getMetaNumber("cache_hit_tokens_saved", stateDir);
|
|
108
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-snapshots.ts — `model_snapshots` table (active model/provider per repo).
|
|
3
|
+
*/
|
|
4
|
+
import { getStateDir } from "../../store.js";
|
|
5
|
+
import { openStore } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
/** A captured model/provider snapshot (for cost estimation + dashboard). */
|
|
8
|
+
export interface ModelSnapshot {
|
|
9
|
+
provider: string;
|
|
10
|
+
providerName: string | null;
|
|
11
|
+
modelId: string;
|
|
12
|
+
modelName: string | null;
|
|
13
|
+
inputRate: number; // USD per input token
|
|
14
|
+
outputRate: number; // USD per output token
|
|
15
|
+
contextWindow: number;
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
reasoning: boolean;
|
|
18
|
+
capturedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Persist the active model/provider for a repo (latest row wins per repo). */
|
|
22
|
+
export function recordModelSnapshot(
|
|
23
|
+
repoRoot: string,
|
|
24
|
+
snap: Omit<ModelSnapshot, "capturedAt">,
|
|
25
|
+
stateDir: string = getStateDir(),
|
|
26
|
+
): void {
|
|
27
|
+
const db = openStore(stateDir);
|
|
28
|
+
db.prepare(
|
|
29
|
+
`INSERT INTO model_snapshots
|
|
30
|
+
(repo_root, provider, provider_name, model_id, model_name, input_rate,
|
|
31
|
+
output_rate, context_window, max_tokens, reasoning, captured_at)
|
|
32
|
+
VALUES (@repo_root, @provider, @provider_name, @model_id, @model_name,
|
|
33
|
+
@input_rate, @output_rate, @context_window, @max_tokens, @reasoning, @captured_at)`,
|
|
34
|
+
).run({
|
|
35
|
+
repo_root: repoRoot,
|
|
36
|
+
provider: snap.provider,
|
|
37
|
+
provider_name: snap.providerName,
|
|
38
|
+
model_id: snap.modelId,
|
|
39
|
+
model_name: snap.modelName,
|
|
40
|
+
input_rate: snap.inputRate,
|
|
41
|
+
output_rate: snap.outputRate,
|
|
42
|
+
context_window: snap.contextWindow,
|
|
43
|
+
max_tokens: snap.maxTokens,
|
|
44
|
+
reasoning: snap.reasoning ? 1 : 0,
|
|
45
|
+
captured_at: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Most recent model/provider snapshot for a repo, or undefined. */
|
|
50
|
+
export function latestModelSnapshot(stateDir: string = getStateDir()): ModelSnapshot | undefined {
|
|
51
|
+
const db = openStore(stateDir);
|
|
52
|
+
const row = db
|
|
53
|
+
.prepare(
|
|
54
|
+
`SELECT * FROM model_snapshots ORDER BY captured_at DESC LIMIT 1`,
|
|
55
|
+
)
|
|
56
|
+
.get() as
|
|
57
|
+
| {
|
|
58
|
+
provider: string;
|
|
59
|
+
provider_name: string | null;
|
|
60
|
+
model_id: string;
|
|
61
|
+
model_name: string | null;
|
|
62
|
+
input_rate: number;
|
|
63
|
+
output_rate: number;
|
|
64
|
+
context_window: number;
|
|
65
|
+
max_tokens: number;
|
|
66
|
+
reasoning: number;
|
|
67
|
+
captured_at: number;
|
|
68
|
+
}
|
|
69
|
+
| undefined;
|
|
70
|
+
if (!row) return undefined;
|
|
71
|
+
return {
|
|
72
|
+
provider: row.provider,
|
|
73
|
+
providerName: row.provider_name,
|
|
74
|
+
modelId: row.model_id,
|
|
75
|
+
modelName: row.model_name,
|
|
76
|
+
inputRate: row.input_rate,
|
|
77
|
+
outputRate: row.output_rate,
|
|
78
|
+
contextWindow: row.context_window,
|
|
79
|
+
maxTokens: row.max_tokens,
|
|
80
|
+
reasoning: row.reasoning === 1,
|
|
81
|
+
capturedAt: row.captured_at,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raptor.ts — Sprint 13 RAPTOR node persistence.
|
|
3
|
+
*/
|
|
4
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
5
|
+
import { openStore, jsonText, encodeEmbedding, decodeEmbedding } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
export interface StoredRaptorNode {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
level: number;
|
|
11
|
+
parentId: string | null;
|
|
12
|
+
children: string[];
|
|
13
|
+
summary: string;
|
|
14
|
+
embedding: number[];
|
|
15
|
+
qualityMarker: string;
|
|
16
|
+
tokenEstimate: number;
|
|
17
|
+
/** S25: epoch ms when the tree containing this node was built. */
|
|
18
|
+
builtAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Persist a single RAPTOR node (upsert by (session_id, id)). */
|
|
22
|
+
export function upsertRaptorNode(node: StoredRaptorNode, stateDir: string = getStateDir()): void {
|
|
23
|
+
const db = openStore(stateDir);
|
|
24
|
+
db.prepare(
|
|
25
|
+
`INSERT INTO raptor_nodes(id, session_id, level, parent_id, children, summary, embedding_blob, quality_marker, token_estimate, built_at)
|
|
26
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
+
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
28
|
+
level=excluded.level, parent_id=excluded.parent_id, children=excluded.children,
|
|
29
|
+
summary=excluded.summary, embedding_blob=excluded.embedding_blob,
|
|
30
|
+
quality_marker=excluded.quality_marker, token_estimate=excluded.token_estimate,
|
|
31
|
+
built_at=excluded.built_at`,
|
|
32
|
+
).run(
|
|
33
|
+
node.id,
|
|
34
|
+
node.sessionId,
|
|
35
|
+
node.level,
|
|
36
|
+
node.parentId,
|
|
37
|
+
jsonText(node.children),
|
|
38
|
+
node.summary,
|
|
39
|
+
encodeEmbedding(node.embedding),
|
|
40
|
+
node.qualityMarker,
|
|
41
|
+
node.tokenEstimate,
|
|
42
|
+
node.builtAt,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Persist an entire built RAPTOR tree for a session (shadow or live). */
|
|
47
|
+
export function saveRaptorTree(
|
|
48
|
+
sessionId: string,
|
|
49
|
+
tree: {
|
|
50
|
+
nodes: Map<string, {
|
|
51
|
+
id: string;
|
|
52
|
+
level: number;
|
|
53
|
+
parentId: string | null;
|
|
54
|
+
children: string[];
|
|
55
|
+
summary: string;
|
|
56
|
+
embedding: number[];
|
|
57
|
+
qualityMarker: string;
|
|
58
|
+
tokenEstimate: number;
|
|
59
|
+
}>
|
|
60
|
+
},
|
|
61
|
+
builtAt: number,
|
|
62
|
+
stateDir: string = getStateDir(),
|
|
63
|
+
): void {
|
|
64
|
+
for (const node of tree.nodes.values()) {
|
|
65
|
+
upsertRaptorNode(
|
|
66
|
+
{
|
|
67
|
+
id: node.id,
|
|
68
|
+
sessionId,
|
|
69
|
+
level: node.level,
|
|
70
|
+
parentId: node.parentId,
|
|
71
|
+
children: node.children,
|
|
72
|
+
summary: node.summary,
|
|
73
|
+
embedding: node.embedding,
|
|
74
|
+
qualityMarker: node.qualityMarker,
|
|
75
|
+
tokenEstimate: node.tokenEstimate,
|
|
76
|
+
builtAt,
|
|
77
|
+
},
|
|
78
|
+
stateDir,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Load all RAPTOR nodes for a session. */
|
|
84
|
+
export function listRaptorNodes(sessionId: string, stateDir: string = getStateDir()): StoredRaptorNode[] {
|
|
85
|
+
const db = openStore(stateDir);
|
|
86
|
+
const rows = db
|
|
87
|
+
.prepare("SELECT * FROM raptor_nodes WHERE session_id = ? ORDER BY level ASC, id ASC")
|
|
88
|
+
.all(normalizeSessionId(sessionId)) as any[];
|
|
89
|
+
return rows.map((row) => ({
|
|
90
|
+
id: row.id,
|
|
91
|
+
sessionId: row.session_id,
|
|
92
|
+
level: row.level,
|
|
93
|
+
parentId: row.parent_id ?? null,
|
|
94
|
+
children: row.children ? JSON.parse(row.children) : [],
|
|
95
|
+
summary: row.summary ?? "",
|
|
96
|
+
embedding: decodeEmbedding(row.embedding_blob),
|
|
97
|
+
qualityMarker: row.quality_marker ?? "low",
|
|
98
|
+
tokenEstimate: row.token_estimate ?? 0,
|
|
99
|
+
builtAt: Number(row.built_at ?? 0),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Delete all RAPTOR nodes for a session (rollback/cleanup). */
|
|
104
|
+
export function clearRaptorNodes(sessionId: string, stateDir: string = getStateDir()): void {
|
|
105
|
+
const db = openStore(stateDir);
|
|
106
|
+
db.prepare("DELETE FROM raptor_nodes WHERE session_id = ?").run(normalizeSessionId(sessionId));
|
|
107
|
+
}
|