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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raw-transcript.ts — S27 durable raw-transcript mirror + checkpoint-epoch registry.
|
|
3
|
+
*
|
|
4
|
+
* ADDITIVE + flag-default-OFF (MEGACOMPACT_DB_MIRROR). No behavior change
|
|
5
|
+
* until the flag is flipped on; the tables are created IF NOT EXISTS on
|
|
6
|
+
* open so existing stores are untouched. These helpers are storage primitives
|
|
7
|
+
* only — the runtime hook (Task 5) wires them into the compaction flow. All
|
|
8
|
+
* queries use @-parameterized placeholders (PREVENT-002); no `any` types
|
|
9
|
+
* (PREVENT-011).
|
|
10
|
+
*/
|
|
11
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
12
|
+
import { withTx } from "./utils.js";
|
|
13
|
+
|
|
14
|
+
/** One appended raw-message row in the durable mirror. */
|
|
15
|
+
export interface RawTranscriptRow {
|
|
16
|
+
contentHash: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
seq: number;
|
|
19
|
+
role: string;
|
|
20
|
+
contentBytes: string;
|
|
21
|
+
toolName: string | null;
|
|
22
|
+
/** ORIGINAL message timestamp captured at append time (NOT a served ts). */
|
|
23
|
+
messageTimestamp: number | null;
|
|
24
|
+
checkpointEpoch: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One checkpoint-epoch bookkeeping row (informational registry). */
|
|
28
|
+
export interface CheckpointEpoch {
|
|
29
|
+
epochId: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
startedSeq: number;
|
|
32
|
+
committedSeq: number;
|
|
33
|
+
summaryMessageText: string;
|
|
34
|
+
cutIndex: number;
|
|
35
|
+
checkpointId: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** DB row shape for raw_transcript (snake_case column names). */
|
|
40
|
+
interface RawTranscriptDBRow {
|
|
41
|
+
content_hash: string;
|
|
42
|
+
session_id: string;
|
|
43
|
+
seq: number;
|
|
44
|
+
role: string;
|
|
45
|
+
content_bytes: string;
|
|
46
|
+
tool_name: string | null;
|
|
47
|
+
message_timestamp: number | null;
|
|
48
|
+
checkpoint_epoch: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** DB row shape for checkpoint_epochs (snake_case column names). */
|
|
52
|
+
interface CheckpointEpochDBRow {
|
|
53
|
+
epoch_id: string;
|
|
54
|
+
session_id: string;
|
|
55
|
+
started_seq: number;
|
|
56
|
+
committed_seq: number;
|
|
57
|
+
summary_message_text: string;
|
|
58
|
+
cut_index: number;
|
|
59
|
+
checkpoint_id: string;
|
|
60
|
+
created_at: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rowToRawTranscript(row: RawTranscriptDBRow): RawTranscriptRow {
|
|
64
|
+
return {
|
|
65
|
+
contentHash: row.content_hash,
|
|
66
|
+
sessionId: row.session_id,
|
|
67
|
+
seq: Number(row.seq),
|
|
68
|
+
role: row.role,
|
|
69
|
+
contentBytes: row.content_bytes,
|
|
70
|
+
toolName: row.tool_name ?? null,
|
|
71
|
+
messageTimestamp:
|
|
72
|
+
row.message_timestamp == null ? null : Number(row.message_timestamp),
|
|
73
|
+
checkpointEpoch: row.checkpoint_epoch,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rowToCheckpointEpoch(row: CheckpointEpochDBRow): CheckpointEpoch {
|
|
78
|
+
return {
|
|
79
|
+
epochId: row.epoch_id,
|
|
80
|
+
sessionId: row.session_id,
|
|
81
|
+
startedSeq: Number(row.started_seq),
|
|
82
|
+
committedSeq: Number(row.committed_seq),
|
|
83
|
+
summaryMessageText: row.summary_message_text,
|
|
84
|
+
cutIndex: Number(row.cut_index),
|
|
85
|
+
checkpointId: row.checkpoint_id,
|
|
86
|
+
createdAt: Number(row.created_at),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Append one raw-message row to the durable mirror. Idempotent by
|
|
92
|
+
* (content_hash, session_id) via INSERT OR IGNORE — re-appending the same
|
|
93
|
+
* content for the same session is a no-op. seq is assigned server-side as
|
|
94
|
+
* COALESCE(MAX(seq),0)+1 within the session, so callers never need to compute
|
|
95
|
+
* it. Pass an open store handle (openStore) — matches the other DatabaseSync
|
|
96
|
+
* helpers. Parameterized (PREVENT-002).
|
|
97
|
+
*/
|
|
98
|
+
export function appendRawTranscript(db: DatabaseSync, row: RawTranscriptRow): void {
|
|
99
|
+
withTx(db, () => {
|
|
100
|
+
db.prepare(
|
|
101
|
+
`INSERT OR IGNORE INTO raw_transcript
|
|
102
|
+
(content_hash, session_id, seq, role, content_bytes, tool_name, message_timestamp, checkpoint_epoch)
|
|
103
|
+
VALUES (
|
|
104
|
+
@content_hash, @session_id,
|
|
105
|
+
COALESCE((SELECT MAX(seq) FROM raw_transcript WHERE session_id = @session_id), 0) + 1,
|
|
106
|
+
@role, @content_bytes, @tool_name, @message_timestamp, @checkpoint_epoch
|
|
107
|
+
)`,
|
|
108
|
+
).run({
|
|
109
|
+
"@content_hash": row.contentHash,
|
|
110
|
+
"@session_id": row.sessionId,
|
|
111
|
+
"@role": row.role,
|
|
112
|
+
"@content_bytes": row.contentBytes,
|
|
113
|
+
"@tool_name": row.toolName,
|
|
114
|
+
"@message_timestamp": row.messageTimestamp,
|
|
115
|
+
"@checkpoint_epoch": row.checkpointEpoch,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* List raw-transcript rows for a session in [fromSeq, toSeq], ordered by seq
|
|
122
|
+
* ascending. Returns camel-cased RawTranscriptRow[]. Parameterized.
|
|
123
|
+
*/
|
|
124
|
+
export function listRawTranscriptRange(
|
|
125
|
+
db: DatabaseSync,
|
|
126
|
+
sessionId: string,
|
|
127
|
+
fromSeq: number,
|
|
128
|
+
toSeq: number,
|
|
129
|
+
): RawTranscriptRow[] {
|
|
130
|
+
const rows = db
|
|
131
|
+
.prepare(
|
|
132
|
+
`SELECT content_hash, session_id, seq, role, content_bytes, tool_name, message_timestamp, checkpoint_epoch
|
|
133
|
+
FROM raw_transcript
|
|
134
|
+
WHERE session_id = @session_id AND seq >= @from_seq AND seq <= @to_seq
|
|
135
|
+
ORDER BY seq ASC`,
|
|
136
|
+
)
|
|
137
|
+
.all({
|
|
138
|
+
"@session_id": sessionId,
|
|
139
|
+
"@from_seq": fromSeq,
|
|
140
|
+
"@to_seq": toSeq,
|
|
141
|
+
}) as unknown as RawTranscriptDBRow[];
|
|
142
|
+
return rows.map(rowToRawTranscript);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Insert (or refresh) a checkpoint-epoch row. ON CONFLICT(epoch_id) DO UPDATE
|
|
147
|
+
* so re-running the same compaction epoch is idempotent / refresh-safe.
|
|
148
|
+
* Parameterized (PREVENT-002).
|
|
149
|
+
*/
|
|
150
|
+
export function writeCheckpointEpoch(db: DatabaseSync, epoch: CheckpointEpoch): void {
|
|
151
|
+
withTx(db, () => {
|
|
152
|
+
db.prepare(
|
|
153
|
+
`INSERT INTO checkpoint_epochs
|
|
154
|
+
(epoch_id, session_id, started_seq, committed_seq, summary_message_text, cut_index, checkpoint_id, created_at)
|
|
155
|
+
VALUES (@epoch_id, @session_id, @started_seq, @committed_seq, @summary_message_text, @cut_index, @checkpoint_id, @created_at)
|
|
156
|
+
ON CONFLICT(epoch_id) DO UPDATE SET
|
|
157
|
+
session_id = excluded.session_id,
|
|
158
|
+
started_seq = excluded.started_seq,
|
|
159
|
+
committed_seq = excluded.committed_seq,
|
|
160
|
+
summary_message_text = excluded.summary_message_text,
|
|
161
|
+
cut_index = excluded.cut_index,
|
|
162
|
+
checkpoint_id = excluded.checkpoint_id,
|
|
163
|
+
created_at = excluded.created_at`,
|
|
164
|
+
).run({
|
|
165
|
+
"@epoch_id": epoch.epochId,
|
|
166
|
+
"@session_id": epoch.sessionId,
|
|
167
|
+
"@started_seq": epoch.startedSeq,
|
|
168
|
+
"@committed_seq": epoch.committedSeq,
|
|
169
|
+
"@summary_message_text": epoch.summaryMessageText,
|
|
170
|
+
"@cut_index": epoch.cutIndex,
|
|
171
|
+
"@checkpoint_id": epoch.checkpointId,
|
|
172
|
+
"@created_at": epoch.createdAt,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Read one checkpoint-epoch row by id (or null if absent). Parameterized. */
|
|
178
|
+
export function readCheckpointEpoch(db: DatabaseSync, epochId: string): CheckpointEpoch | null {
|
|
179
|
+
const row = db
|
|
180
|
+
.prepare(
|
|
181
|
+
`SELECT epoch_id, session_id, started_seq, committed_seq, summary_message_text, cut_index, checkpoint_id, created_at
|
|
182
|
+
FROM checkpoint_epochs WHERE epoch_id = @epoch_id`,
|
|
183
|
+
)
|
|
184
|
+
.get({ "@epoch_id": epochId }) as unknown as CheckpointEpochDBRow | undefined;
|
|
185
|
+
return row ? rowToCheckpointEpoch(row) : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Latest checkpoint-epoch row for a session (highest created_at), or null if
|
|
190
|
+
* none. Parameterized (PREVENT-002).
|
|
191
|
+
*/
|
|
192
|
+
export function getActiveEpochForSession(db: DatabaseSync, sessionId: string): CheckpointEpoch | null {
|
|
193
|
+
const row = db
|
|
194
|
+
.prepare(
|
|
195
|
+
`SELECT epoch_id, session_id, started_seq, committed_seq, summary_message_text, cut_index, checkpoint_id, created_at
|
|
196
|
+
FROM checkpoint_epochs
|
|
197
|
+
WHERE session_id = @session_id
|
|
198
|
+
ORDER BY created_at DESC
|
|
199
|
+
LIMIT 1`,
|
|
200
|
+
)
|
|
201
|
+
.get({ "@session_id": sessionId }) as unknown as CheckpointEpochDBRow | undefined;
|
|
202
|
+
return row ? rowToCheckpointEpoch(row) : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** List all checkpoint epochs (diagnostic / test helper). */
|
|
206
|
+
export function listCheckpointEpochs(db: DatabaseSync): CheckpointEpoch[] {
|
|
207
|
+
const rows = db
|
|
208
|
+
.prepare(
|
|
209
|
+
`SELECT epoch_id, session_id, started_seq, committed_seq, summary_message_text, cut_index, checkpoint_id, created_at
|
|
210
|
+
FROM checkpoint_epochs
|
|
211
|
+
ORDER BY created_at DESC`,
|
|
212
|
+
)
|
|
213
|
+
.all() as unknown as CheckpointEpochDBRow[];
|
|
214
|
+
return rows.map(rowToCheckpointEpoch);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Count raw transcript rows (diagnostic / test helper). */
|
|
218
|
+
export function countRawTranscript(db: DatabaseSync): number {
|
|
219
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM raw_transcript`).get() as { cnt: number };
|
|
220
|
+
return row.cnt;
|
|
221
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.ts — table creation, migrations, `ensureColumn`, PRAGMA setup.
|
|
3
|
+
*/
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
const SCHEMA_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Add `column` (with `decl`, e.g. "INTEGER") to `table` if it does not already
|
|
10
|
+
* exist. Idempotent: checks PRAGMA table_info first, so it is safe to run on
|
|
11
|
+
* every open. Table/column/decl are code-controlled constants (never user
|
|
12
|
+
* input), so the unavoidable identifier interpolation here does not violate
|
|
13
|
+
* PREVENT-002 (no external data reaches this SQL).
|
|
14
|
+
*/
|
|
15
|
+
export function ensureColumn(db: DatabaseSync, table: string, column: string, decl: string): void {
|
|
16
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
17
|
+
if (cols.some((c) => c.name === column)) return;
|
|
18
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function initSchema(db: DatabaseSync): void {
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS context_chunks (
|
|
24
|
+
id TEXT NOT NULL,
|
|
25
|
+
session_id TEXT NOT NULL,
|
|
26
|
+
region_hash TEXT,
|
|
27
|
+
content_hash TEXT,
|
|
28
|
+
content_hash2 TEXT,
|
|
29
|
+
content_hash_version INTEGER,
|
|
30
|
+
normalized_text TEXT,
|
|
31
|
+
summary TEXT,
|
|
32
|
+
topic_summary TEXT,
|
|
33
|
+
summary_hash TEXT,
|
|
34
|
+
key_decisions TEXT, -- JSON array
|
|
35
|
+
next_steps TEXT, -- JSON array
|
|
36
|
+
files_modified TEXT, -- JSON array
|
|
37
|
+
embedding_blob BLOB, -- float32 vector
|
|
38
|
+
token_estimate INTEGER,
|
|
39
|
+
original_token_estimate INTEGER, -- dropped region size (tokens saved = orig − stored)
|
|
40
|
+
timestamp INTEGER,
|
|
41
|
+
dedup_status TEXT DEFAULT 'active',
|
|
42
|
+
compressed_original BLOB -- optional DR copy
|
|
43
|
+
);
|
|
44
|
+
-- Primary key is (session_id, id): checkpoint ids are unique per session
|
|
45
|
+
-- (chkpt_001 per session), not globally, so a bare id PK would collide
|
|
46
|
+
-- across sessions on the nextCheckpointId sequence.
|
|
47
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_pk
|
|
48
|
+
ON context_chunks(session_id, id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_session ON context_chunks(session_id);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_region ON context_chunks(region_hash);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_content ON context_chunks(content_hash);
|
|
52
|
+
-- Partial UNIQUE (QA #1): null content_hash rows never violate the constraint;
|
|
53
|
+
-- ON CONFLICT DO NOTHING makes backfill + L0 inserts safe.
|
|
54
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_content_hash
|
|
55
|
+
ON context_chunks(session_id, content_hash) WHERE content_hash IS NOT NULL;
|
|
56
|
+
|
|
57
|
+
-- Sprint 11: MinHash signature + LSH bucket tables for L1 near-dup dedup.
|
|
58
|
+
CREATE TABLE IF NOT EXISTS minhash_signatures (
|
|
59
|
+
chunk_id TEXT NOT NULL,
|
|
60
|
+
session_id TEXT NOT NULL,
|
|
61
|
+
signature_version INTEGER NOT NULL,
|
|
62
|
+
signatures TEXT NOT NULL, -- JSON array of 256 uint32
|
|
63
|
+
PRIMARY KEY (chunk_id, signature_version)
|
|
64
|
+
);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_minhash_session ON minhash_signatures(session_id);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS dedup_lsh_buckets (
|
|
68
|
+
bucket_key TEXT NOT NULL,
|
|
69
|
+
chunk_id TEXT NOT NULL,
|
|
70
|
+
session_id TEXT NOT NULL,
|
|
71
|
+
signature_version INTEGER NOT NULL,
|
|
72
|
+
PRIMARY KEY (bucket_key, chunk_id)
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_lsh_bucket ON dedup_lsh_buckets(bucket_key, session_id);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS session_state (
|
|
77
|
+
session_id TEXT PRIMARY KEY,
|
|
78
|
+
injected_checkpoint_ids TEXT, -- JSON array
|
|
79
|
+
stored_region_hashes TEXT -- JSON array
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
83
|
+
key TEXT PRIMARY KEY,
|
|
84
|
+
value TEXT
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- Sprint 13 (RAPTOR): hierarchical summary tree nodes. children are a JSON
|
|
88
|
+
-- array of child node ids (or raw leaf ids at the bottom); embedding_blob
|
|
89
|
+
-- is the node centroid. Additive; retrieval ignores this table until
|
|
90
|
+
-- Sprint 14 promotes RAPTOR out of shadow mode.
|
|
91
|
+
CREATE TABLE IF NOT EXISTS raptor_nodes (
|
|
92
|
+
id TEXT NOT NULL,
|
|
93
|
+
session_id TEXT NOT NULL,
|
|
94
|
+
level INTEGER NOT NULL,
|
|
95
|
+
parent_id TEXT,
|
|
96
|
+
children TEXT, -- JSON array of child ids
|
|
97
|
+
summary TEXT,
|
|
98
|
+
embedding_blob BLOB, -- float32 centroid
|
|
99
|
+
quality_marker TEXT DEFAULT 'low',
|
|
100
|
+
token_estimate INTEGER,
|
|
101
|
+
built_at INTEGER, -- S25: epoch ms when the tree was built (freshness guard)
|
|
102
|
+
PRIMARY KEY (session_id, id)
|
|
103
|
+
);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_raptor_session ON raptor_nodes(session_id);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_raptor_parent ON raptor_nodes(parent_id);
|
|
106
|
+
|
|
107
|
+
-- Foundation for future features (resume sessions, daily log, lessons
|
|
108
|
+
-- learned). Scaffolded now so all store data lives in SQLite from day one;
|
|
109
|
+
-- population is minimal (touchSession / logDaily on compact) and the full
|
|
110
|
+
-- UI/recall for these lands in later sprints.
|
|
111
|
+
|
|
112
|
+
-- Per-session registry (resume + per-repo session history).
|
|
113
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
114
|
+
session_id TEXT PRIMARY KEY,
|
|
115
|
+
repo TEXT,
|
|
116
|
+
started_at INTEGER,
|
|
117
|
+
ended_at INTEGER,
|
|
118
|
+
last_compacted_at INTEGER,
|
|
119
|
+
status TEXT DEFAULT 'active'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
-- Append-only daily activity log (the "daily log" feature seed).
|
|
123
|
+
CREATE TABLE IF NOT EXISTS daily_log (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
day TEXT NOT NULL, -- YYYY-MM-DD
|
|
126
|
+
session_id TEXT,
|
|
127
|
+
event TEXT, -- e.g. 'compact'
|
|
128
|
+
detail TEXT,
|
|
129
|
+
tokens_saved INTEGER DEFAULT 0,
|
|
130
|
+
ts INTEGER
|
|
131
|
+
);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_daily_log_day ON daily_log(day);
|
|
133
|
+
|
|
134
|
+
-- Active model/provider for cost estimation + the future multi-repo
|
|
135
|
+
-- dashboard (Phase 5b). One row per (repo, model change); latest wins.
|
|
136
|
+
CREATE TABLE IF NOT EXISTS model_snapshots (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
repo_root TEXT NOT NULL,
|
|
139
|
+
provider TEXT NOT NULL,
|
|
140
|
+
provider_name TEXT,
|
|
141
|
+
model_id TEXT NOT NULL,
|
|
142
|
+
model_name TEXT,
|
|
143
|
+
input_rate REAL, -- USD per input token (Model.cost.input)
|
|
144
|
+
output_rate REAL, -- USD per output token (Model.cost.output)
|
|
145
|
+
context_window INTEGER,
|
|
146
|
+
max_tokens INTEGER,
|
|
147
|
+
reasoning INTEGER DEFAULT 0,
|
|
148
|
+
captured_at INTEGER
|
|
149
|
+
);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_model_repo ON model_snapshots(repo_root);
|
|
151
|
+
|
|
152
|
+
-- Lessons learned (future recall/browse feature seed).
|
|
153
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
154
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
|
+
session_id TEXT,
|
|
156
|
+
repo TEXT,
|
|
157
|
+
lesson TEXT,
|
|
158
|
+
ts INTEGER
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
-- Durable "save to memory" store (taken over from memory extensions).
|
|
162
|
+
-- One row per saved memory; scoped by repo so memory travels with the
|
|
163
|
+
-- clone. All params are parameterized (PREVENT-002).
|
|
164
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
repo TEXT,
|
|
167
|
+
kind TEXT DEFAULT 'note', -- note | fact | decision | preference
|
|
168
|
+
content TEXT NOT NULL,
|
|
169
|
+
tags TEXT, -- JSON array of strings
|
|
170
|
+
created_at INTEGER,
|
|
171
|
+
last_recalled_at INTEGER,
|
|
172
|
+
-- S20 memory-RAG extension (auto-review add/replace/remove ops).
|
|
173
|
+
category TEXT, -- typed bucket, e.g. decision | fact | preference
|
|
174
|
+
target TEXT, -- optional subject/scope this memory targets
|
|
175
|
+
last_referenced INTEGER, -- last time memory was referenced by recall (epoch s)
|
|
176
|
+
source_turn INTEGER -- conversation turn that produced this memory
|
|
177
|
+
);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo);
|
|
179
|
+
|
|
180
|
+
-- FTS5 trigram virtual table (Sprint 9+ pg_trgm-equivalent verification).
|
|
181
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS context_chunks_trgm USING fts5(
|
|
182
|
+
id UNINDEXED,
|
|
183
|
+
normalized_text,
|
|
184
|
+
tokenize='trigram'
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
-- S27: durable raw-transcript mirror (MEGACOMPACT_DB_MIRROR). Appended
|
|
188
|
+
-- RAW message bytes per session so a compacted window can be rehydrated
|
|
189
|
+
-- from the local store instead of the pi runtime transcript (which is
|
|
190
|
+
-- trimmed). PK is (content_hash, session_id) — NOT content_hash alone —
|
|
191
|
+
-- so identical content in different sessions never collides. Additive:
|
|
192
|
+
-- CREATE TABLE IF NOT EXISTS leaves existing DBs untouched on open until
|
|
193
|
+
-- the S27 mirror flag is flipped on. All queries parameterized (PREVENT-002).
|
|
194
|
+
CREATE TABLE IF NOT EXISTS raw_transcript (
|
|
195
|
+
content_hash TEXT NOT NULL,
|
|
196
|
+
session_id TEXT NOT NULL,
|
|
197
|
+
seq INTEGER NOT NULL,
|
|
198
|
+
role TEXT NOT NULL,
|
|
199
|
+
content_bytes TEXT NOT NULL,
|
|
200
|
+
tool_name TEXT,
|
|
201
|
+
message_timestamp INTEGER, -- ORIGINAL msg ts at append, NOT served
|
|
202
|
+
checkpoint_epoch TEXT NOT NULL,
|
|
203
|
+
PRIMARY KEY (content_hash, session_id)
|
|
204
|
+
);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_rt_session_seq ON raw_transcript(session_id, seq);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_rt_epoch ON raw_transcript(checkpoint_epoch);
|
|
207
|
+
|
|
208
|
+
-- S27: checkpoint-epoch registry. One row per compaction epoch; the
|
|
209
|
+
-- summary_message_text is the verbatim system message that replaced the
|
|
210
|
+
-- trimmed prefix. Informational bookkeeping (the raw_transcript rows are
|
|
211
|
+
-- authoritative); refresh-safe via ON CONFLICT(epoch_id) DO UPDATE.
|
|
212
|
+
CREATE TABLE IF NOT EXISTS checkpoint_epochs (
|
|
213
|
+
epoch_id TEXT PRIMARY KEY,
|
|
214
|
+
session_id TEXT NOT NULL,
|
|
215
|
+
started_seq INTEGER NOT NULL,
|
|
216
|
+
committed_seq INTEGER NOT NULL,
|
|
217
|
+
summary_message_text TEXT NOT NULL,
|
|
218
|
+
cut_index INTEGER NOT NULL,
|
|
219
|
+
checkpoint_id TEXT NOT NULL,
|
|
220
|
+
created_at INTEGER NOT NULL
|
|
221
|
+
);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_epoch_session ON checkpoint_epochs(session_id, created_at DESC);
|
|
223
|
+
|
|
224
|
+
-- S27 Task 6: dedup_mirror for space-efficient deduplicated storage.
|
|
225
|
+
-- Each unique content_hash stores its bytes ONCE; raw_transcript rows
|
|
226
|
+
-- reference this table via content_ref instead of storing duplicate content_bytes inline.
|
|
227
|
+
CREATE TABLE IF NOT EXISTS dedup_mirror (
|
|
228
|
+
content_hash TEXT PRIMARY KEY,
|
|
229
|
+
content_bytes TEXT NOT NULL,
|
|
230
|
+
ref_count INTEGER NOT NULL DEFAULT 1,
|
|
231
|
+
first_seen_seq INTEGER NOT NULL,
|
|
232
|
+
created_at INTEGER NOT NULL
|
|
233
|
+
);
|
|
234
|
+
`);
|
|
235
|
+
// Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
|
|
236
|
+
// pre-existing table, so new columns added to context_chunks after a store was
|
|
237
|
+
// first created (e.g. original_token_estimate in v0.4.2) must be ALTERed in for
|
|
238
|
+
// databases created by an older version — otherwise repoStats()/upsert crash
|
|
239
|
+
// with "no such column" and the extension fails to load. Additive only.
|
|
240
|
+
ensureColumn(db, "context_chunks", "original_token_estimate", "INTEGER");
|
|
241
|
+
// S27 Task 6: content_ref column in raw_transcript for dedup_mirror references.
|
|
242
|
+
ensureColumn(db, "raw_transcript", "content_ref", "TEXT");
|
|
243
|
+
// S20 memory-RAG extension: additive columns for auto-review ops. Idempotent —
|
|
244
|
+
// only alters DBs created by an older version that lack these columns.
|
|
245
|
+
ensureColumn(db, "memories", "category", "TEXT");
|
|
246
|
+
ensureColumn(db, "memories", "target", "TEXT");
|
|
247
|
+
ensureColumn(db, "memories", "last_referenced", "INTEGER");
|
|
248
|
+
ensureColumn(db, "memories", "source_turn", "INTEGER");
|
|
249
|
+
// S25: RAPTOR freshness-guard timestamp. Additive; old DBs have NULL → 0 →
|
|
250
|
+
// treated as stale → flat fallback (safe).
|
|
251
|
+
ensureColumn(db, "raptor_nodes", "built_at", "INTEGER");
|
|
252
|
+
const v = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get() as
|
|
253
|
+
| { value: string }
|
|
254
|
+
| undefined;
|
|
255
|
+
if (!v) {
|
|
256
|
+
db.prepare("INSERT INTO meta(key, value) VALUES(?, ?)").run("schema_version", String(SCHEMA_VERSION));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-state.ts — `session_state` table (injection tracking).
|
|
3
|
+
*/
|
|
4
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import type { SessionState } from "../../store.js";
|
|
6
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
7
|
+
import { openStore, jsonText } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
function loadSessionStateRow(sid: string, db: DatabaseSync): SessionState {
|
|
10
|
+
const row = db.prepare("SELECT * FROM session_state WHERE session_id = ?").get(sid) as any;
|
|
11
|
+
if (!row) {
|
|
12
|
+
return { injectedCheckpointIds: [], storedRegionHashes: [] };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
injectedCheckpointIds: row.injected_checkpoint_ids ? JSON.parse(row.injected_checkpoint_ids) : [],
|
|
16
|
+
storedRegionHashes: row.stored_region_hashes ? JSON.parse(row.stored_region_hashes) : [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadSessionState(sessionId: string, stateDir: string = getStateDir()): SessionState {
|
|
21
|
+
return loadSessionStateRow(normalizeSessionId(sessionId), openStore(stateDir));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveSessionState(sessionId: string, state: SessionState, stateDir: string = getStateDir()): void {
|
|
25
|
+
const db = openStore(stateDir);
|
|
26
|
+
const sid = normalizeSessionId(sessionId);
|
|
27
|
+
db.prepare(
|
|
28
|
+
`INSERT INTO session_state(session_id, injected_checkpoint_ids, stored_region_hashes)
|
|
29
|
+
VALUES(@sid, @inj, @reg)
|
|
30
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
31
|
+
injected_checkpoint_ids=excluded.injected_checkpoint_ids,
|
|
32
|
+
stored_region_hashes=excluded.stored_region_hashes`,
|
|
33
|
+
).run({
|
|
34
|
+
sid,
|
|
35
|
+
inj: jsonText(state.injectedCheckpointIds),
|
|
36
|
+
reg: jsonText(state.storedRegionHashes),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stats.ts — per-session, repo-wide, and data-invariant stats.
|
|
3
|
+
*/
|
|
4
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
5
|
+
import { openStore } from "./utils.js";
|
|
6
|
+
import { getMetaNumber, getDedupStats } from "./meta.js";
|
|
7
|
+
|
|
8
|
+
export interface StoreStats {
|
|
9
|
+
checkpointCount: number;
|
|
10
|
+
totalTokenEstimate: number;
|
|
11
|
+
lastCheckpointId: string | undefined;
|
|
12
|
+
lastSummary: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function storeStats(sessionId: string, stateDir: string = getStateDir()): StoreStats {
|
|
16
|
+
const db = openStore(stateDir);
|
|
17
|
+
const sid = normalizeSessionId(sessionId);
|
|
18
|
+
const row = db
|
|
19
|
+
.prepare(
|
|
20
|
+
`SELECT COUNT(*) AS c, COALESCE(SUM(token_estimate),0) AS tok,
|
|
21
|
+
MAX(id) AS lastId
|
|
22
|
+
FROM context_chunks WHERE session_id = ?`,
|
|
23
|
+
)
|
|
24
|
+
.get(sid) as { c: number; tok: number; lastId: string | null };
|
|
25
|
+
let lastSummary: string | undefined;
|
|
26
|
+
if (row.lastId) {
|
|
27
|
+
const s = db.prepare("SELECT summary FROM context_chunks WHERE id = ?").get(row.lastId) as
|
|
28
|
+
| { summary: string }
|
|
29
|
+
| undefined;
|
|
30
|
+
lastSummary = s?.summary;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
checkpointCount: row.c,
|
|
34
|
+
totalTokenEstimate: row.tok,
|
|
35
|
+
lastCheckpointId: row.lastId ?? undefined,
|
|
36
|
+
lastSummary,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Repo-wide stats — aggregates every session in this store (one per repo).
|
|
41
|
+
* Backed by the SQLite `meta` cumulative counters (`tokens_saved`,
|
|
42
|
+
* `dedup_attempts`, `deduped`) plus a SUM over all `context_chunks`. This is the
|
|
43
|
+
* cumulative, resumable, cross-device view the dashboard surfaces as "Repo …". */
|
|
44
|
+
export interface RepoStats {
|
|
45
|
+
/** Total checkpoints across all sessions (excludes SemDeDup-removed rows). */
|
|
46
|
+
checkpointCount: number;
|
|
47
|
+
/** Sum of all stored checkpoint token estimates (repo-wide). */
|
|
48
|
+
totalTokenEstimate: number;
|
|
49
|
+
/** Total active sessions with at least one checkpoint. */
|
|
50
|
+
sessionCount: number;
|
|
51
|
+
/** Cumulative stored-summary tokens saved (Σ stored summaries). */
|
|
52
|
+
tokensSaved: number;
|
|
53
|
+
/** Sum of original dropped-region token estimates (repo-wide). */
|
|
54
|
+
originalTokens: number;
|
|
55
|
+
/** Cumulative dedup add() attempts (store-wide). */
|
|
56
|
+
dedupAttempts: number;
|
|
57
|
+
/** Cumulative deduped collapses (store-wide). */
|
|
58
|
+
dedupCollapsed: number;
|
|
59
|
+
/** Storage dedup rate (deduped / attempts), 0..1. */
|
|
60
|
+
storageDedupRate: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Data-safety invariant metrics (Phase 0 — trust foundation). Proves that every
|
|
65
|
+
* compacted region is still recoverable: we retain a compressed_original blob for
|
|
66
|
+
* each checkpoint and permanently delete nothing. "removed" rows are SemDeDup
|
|
67
|
+
* duplicates whose ORIGINAL is still retained on the surviving checkpoint — they
|
|
68
|
+
* are not data loss, so they are reported separately, not as deletions.
|
|
69
|
+
*/
|
|
70
|
+
export interface DataInvariantStats {
|
|
71
|
+
/** Checkpoints with a recoverable compressed_original blob. */
|
|
72
|
+
regionsRetained: number;
|
|
73
|
+
/** Total bytes of compressed_original retained (recoverable verbatim). */
|
|
74
|
+
compressedOriginalBytes: number;
|
|
75
|
+
/** Checkpoints missing a compressed_original blob (pre-blob or direct add). */
|
|
76
|
+
regionsWithoutBlob: number;
|
|
77
|
+
/** Bytes permanently deleted by the extension. ALWAYS 0 — the invariant. */
|
|
78
|
+
bytesPermanentlyDeleted: number;
|
|
79
|
+
/** Duplicate rows collapsed by dedup (original retained on the survivor). */
|
|
80
|
+
duplicatesCollapsed: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function dataInvariantStats(stateDir: string = getStateDir()): DataInvariantStats {
|
|
84
|
+
const db = openStore(stateDir);
|
|
85
|
+
const row = db
|
|
86
|
+
.prepare(
|
|
87
|
+
`SELECT
|
|
88
|
+
COUNT(compressed_original) AS withBlob,
|
|
89
|
+
COALESCE(SUM(LENGTH(compressed_original)),0) AS blobBytes,
|
|
90
|
+
SUM(CASE WHEN compressed_original IS NULL THEN 1 ELSE 0 END) AS noBlob
|
|
91
|
+
FROM context_chunks WHERE dedup_status != 'removed'`,
|
|
92
|
+
)
|
|
93
|
+
.get() as { withBlob: number; blobBytes: number; noBlob: number };
|
|
94
|
+
const removed = db
|
|
95
|
+
.prepare(`SELECT COUNT(*) AS c FROM context_chunks WHERE dedup_status = 'removed'`)
|
|
96
|
+
.get() as { c: number };
|
|
97
|
+
return {
|
|
98
|
+
regionsRetained: row.withBlob,
|
|
99
|
+
compressedOriginalBytes: row.blobBytes,
|
|
100
|
+
regionsWithoutBlob: row.noBlob ?? 0,
|
|
101
|
+
bytesPermanentlyDeleted: 0,
|
|
102
|
+
duplicatesCollapsed: removed.c,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function repoStats(stateDir: string = getStateDir()): RepoStats {
|
|
107
|
+
const db = openStore(stateDir);
|
|
108
|
+
const row = db
|
|
109
|
+
.prepare(
|
|
110
|
+
`SELECT COUNT(*) AS c, COALESCE(SUM(token_estimate),0) AS tok,
|
|
111
|
+
COALESCE(SUM(original_token_estimate),0) AS orig,
|
|
112
|
+
COUNT(DISTINCT session_id) AS sessions
|
|
113
|
+
FROM context_chunks WHERE dedup_status != 'removed'`,
|
|
114
|
+
)
|
|
115
|
+
.get() as { c: number; tok: number; orig: number; sessions: number };
|
|
116
|
+
const ds = getDedupStats(stateDir);
|
|
117
|
+
return {
|
|
118
|
+
checkpointCount: row.c,
|
|
119
|
+
totalTokenEstimate: row.tok,
|
|
120
|
+
originalTokens: row.orig,
|
|
121
|
+
sessionCount: row.sessions,
|
|
122
|
+
tokensSaved: getMetaNumber("tokens_saved", stateDir),
|
|
123
|
+
dedupAttempts: ds.attempts,
|
|
124
|
+
dedupCollapsed: ds.deduped,
|
|
125
|
+
storageDedupRate: ds.attempts === 0 ? 0 : ds.deduped / ds.attempts,
|
|
126
|
+
};
|
|
127
|
+
}
|