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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-2-Clause",
|
|
@@ -41,15 +41,16 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.json",
|
|
44
|
-
"lint": "tsc --noEmit && node scripts/guardrails-scan.mjs",
|
|
44
|
+
"lint": "tsc --noEmit && node scripts/guardrails-scan.mjs && node scripts/semantic-scan.mjs",
|
|
45
45
|
"test": "npm run build && node scripts/run-tests.mjs",
|
|
46
|
-
"guardrails": "python3 scripts/regression_check.py --all || node scripts/guardrails-scan.mjs",
|
|
46
|
+
"guardrails": "python3 scripts/regression_check.py --all || node scripts/guardrails-scan.mjs || node scripts/semantic-scan.mjs",
|
|
47
47
|
"precommit": "bash .claude/hooks/pre-commit.sh",
|
|
48
48
|
"prepublishOnly": "npm run build",
|
|
49
49
|
"postinstall": "npm rebuild @mongodb-js/zstd || true"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@earendil-works/pi-coding-agent": "*",
|
|
53
|
+
"@earendil-works/pi-tui": ">=0.80",
|
|
53
54
|
"openclaw": ">=0.1.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoints.ts — `context_chunks` checkpoint CRUD + MinHash/LSH dedup helpers.
|
|
3
|
+
*/
|
|
4
|
+
import type { StoredCheckpoint } from "../../store.js";
|
|
5
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
6
|
+
import {
|
|
7
|
+
openStore,
|
|
8
|
+
withTx,
|
|
9
|
+
jsonText,
|
|
10
|
+
encodeEmbedding,
|
|
11
|
+
rowToCheckpoint,
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
|
|
14
|
+
/** Insert or replace a checkpoint (idempotent by id). */
|
|
15
|
+
export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getStateDir()): void {
|
|
16
|
+
const db = openStore(stateDir);
|
|
17
|
+
const sid = normalizeSessionId(cp.sessionId);
|
|
18
|
+
withTx(db, () => {
|
|
19
|
+
db.prepare(
|
|
20
|
+
`INSERT INTO context_chunks
|
|
21
|
+
(id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
|
|
22
|
+
normalized_text, summary, topic_summary, summary_hash,
|
|
23
|
+
key_decisions, next_steps, files_modified, embedding_blob,
|
|
24
|
+
token_estimate, original_token_estimate, timestamp, dedup_status, compressed_original)
|
|
25
|
+
VALUES (@id, @sid, @region_hash, @content_hash, @content_hash2, @content_hash_version,
|
|
26
|
+
@normalized_text, @summary, @topic_summary, @summary_hash,
|
|
27
|
+
@key_decisions, @next_steps, @files_modified, @embedding_blob,
|
|
28
|
+
@token_estimate, @original_token_estimate, @timestamp, @dedup_status, @compressed_original)
|
|
29
|
+
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
30
|
+
summary=excluded.summary,
|
|
31
|
+
topic_summary=excluded.topic_summary,
|
|
32
|
+
summary_hash=excluded.summary_hash,
|
|
33
|
+
key_decisions=excluded.key_decisions,
|
|
34
|
+
next_steps=excluded.next_steps,
|
|
35
|
+
files_modified=excluded.files_modified,
|
|
36
|
+
embedding_blob=excluded.embedding_blob,
|
|
37
|
+
token_estimate=excluded.token_estimate,
|
|
38
|
+
original_token_estimate=excluded.original_token_estimate,
|
|
39
|
+
timestamp=excluded.timestamp,
|
|
40
|
+
dedup_status=excluded.dedup_status,
|
|
41
|
+
compressed_original=excluded.compressed_original`,
|
|
42
|
+
).run({
|
|
43
|
+
"@id": cp.checkpointId,
|
|
44
|
+
"@sid": sid,
|
|
45
|
+
"@region_hash": cp.regionHash ?? null,
|
|
46
|
+
"@content_hash": cp.contentHash ?? null,
|
|
47
|
+
"@content_hash2": cp.contentHash2 ?? null,
|
|
48
|
+
"@content_hash_version": cp.contentHashVersion ?? null,
|
|
49
|
+
"@normalized_text": cp.normalizedText ?? null,
|
|
50
|
+
"@summary": cp.summary ?? "",
|
|
51
|
+
"@topic_summary": cp.topicSummary ?? null,
|
|
52
|
+
"@summary_hash": cp.summaryHash ?? null,
|
|
53
|
+
"@key_decisions": jsonText(cp.keyDecisions),
|
|
54
|
+
"@next_steps": jsonText(cp.nextSteps),
|
|
55
|
+
"@files_modified": jsonText(cp.filesModified),
|
|
56
|
+
"@embedding_blob": encodeEmbedding(cp.embedding ?? []),
|
|
57
|
+
"@token_estimate": cp.tokenEstimate ?? 0,
|
|
58
|
+
"@original_token_estimate": cp.originalTokenEstimate ?? null,
|
|
59
|
+
"@timestamp": cp.timestamp ?? 0,
|
|
60
|
+
"@dedup_status": "active",
|
|
61
|
+
"@compressed_original": cp.compressedOriginal ?? null,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// FTS5 virtual tables don't support UPSERT — delete any prior row, reinsert.
|
|
65
|
+
// Store normalized_text (the L1 verify key); fall back to summary for rows
|
|
66
|
+
// that predate normalized_text population.
|
|
67
|
+
db.prepare("DELETE FROM context_chunks_trgm WHERE id = ?").run(cp.checkpointId);
|
|
68
|
+
db.prepare(
|
|
69
|
+
"INSERT INTO context_chunks_trgm(id, normalized_text) VALUES(?, ?)",
|
|
70
|
+
).run(cp.checkpointId, cp.normalizedText ?? cp.summary ?? "");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Sprint 11: MinHash signatures + LSH buckets --------------------------
|
|
75
|
+
|
|
76
|
+
/** Persist a checkpoint's MinHash signature (idempotent by chunk_id + version). */
|
|
77
|
+
export function upsertMinhashSignature(
|
|
78
|
+
chunkId: string,
|
|
79
|
+
sessionId: string,
|
|
80
|
+
signatureVersion: number,
|
|
81
|
+
signatures: number[],
|
|
82
|
+
stateDir: string = getStateDir(),
|
|
83
|
+
): void {
|
|
84
|
+
const db = openStore(stateDir);
|
|
85
|
+
const sid = normalizeSessionId(sessionId);
|
|
86
|
+
db.prepare(
|
|
87
|
+
`INSERT INTO minhash_signatures(chunk_id, session_id, signature_version, signatures)
|
|
88
|
+
VALUES(?, ?, ?, ?)
|
|
89
|
+
ON CONFLICT(chunk_id, signature_version) DO UPDATE SET
|
|
90
|
+
session_id=excluded.session_id, signatures=excluded.signatures`,
|
|
91
|
+
).run(chunkId, sid, signatureVersion, JSON.stringify(signatures));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Persist LSH bucket memberships for a chunk (one row per bucket key). */
|
|
95
|
+
export function insertLshBuckets(
|
|
96
|
+
chunkId: string,
|
|
97
|
+
sessionId: string,
|
|
98
|
+
signatureVersion: number,
|
|
99
|
+
bucketKeys: string[],
|
|
100
|
+
stateDir: string = getStateDir(),
|
|
101
|
+
): void {
|
|
102
|
+
const db = openStore(stateDir);
|
|
103
|
+
const sid = normalizeSessionId(sessionId);
|
|
104
|
+
const del = db.prepare("DELETE FROM dedup_lsh_buckets WHERE chunk_id = ?");
|
|
105
|
+
const ins = db.prepare(
|
|
106
|
+
"INSERT OR IGNORE INTO dedup_lsh_buckets(bucket_key, chunk_id, session_id, signature_version) VALUES(?, ?, ?, ?)",
|
|
107
|
+
);
|
|
108
|
+
withTx(db, () => {
|
|
109
|
+
del.run(chunkId);
|
|
110
|
+
for (const key of bucketKeys) ins.run(key, chunkId, sid, signatureVersion);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Candidate chunk_ids sharing any LSH bucket with `bucketKeys`, scoped to the
|
|
116
|
+
* session, capped at `limit`. Single query (no N loops) — QA #15 amplification
|
|
117
|
+
* guard. Returns DISTINCT chunk_ids excluding `excludeChunkId` (the new row).
|
|
118
|
+
*/
|
|
119
|
+
export function lshCandidateChunks(
|
|
120
|
+
bucketKeys: string[],
|
|
121
|
+
sessionId: string,
|
|
122
|
+
excludeChunkId: string,
|
|
123
|
+
stateDir: string = getStateDir(),
|
|
124
|
+
limit = 100,
|
|
125
|
+
): string[] {
|
|
126
|
+
if (bucketKeys.length === 0) return [];
|
|
127
|
+
const db = openStore(stateDir);
|
|
128
|
+
const sid = normalizeSessionId(sessionId);
|
|
129
|
+
const placeholders = bucketKeys.map(() => "?").join(",");
|
|
130
|
+
const rows = db
|
|
131
|
+
.prepare(
|
|
132
|
+
`SELECT DISTINCT chunk_id FROM dedup_lsh_buckets
|
|
133
|
+
WHERE bucket_key IN (${placeholders}) AND session_id = ? AND chunk_id != ?
|
|
134
|
+
LIMIT ?`,
|
|
135
|
+
)
|
|
136
|
+
.all(...bucketKeys, sid, excludeChunkId, limit) as { chunk_id: string }[];
|
|
137
|
+
return rows.map((r) => r.chunk_id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** All checkpoints for a session, sorted by id. */
|
|
141
|
+
export function listCheckpoints(sessionId: string, stateDir: string = getStateDir()): StoredCheckpoint[] {
|
|
142
|
+
const db = openStore(stateDir);
|
|
143
|
+
const sid = normalizeSessionId(sessionId);
|
|
144
|
+
const rows = db
|
|
145
|
+
.prepare("SELECT * FROM context_chunks WHERE session_id = ? ORDER BY id ASC")
|
|
146
|
+
.all(sid) as any[];
|
|
147
|
+
return rows.map(rowToCheckpoint);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** S25: the newest checkpoint timestamp for a session, or 0 when none. Used by
|
|
151
|
+
* the RAPTOR freshness guard to reject a tree older than the live checkpoints. */
|
|
152
|
+
export function maxCheckpointTimestamp(sessionId: string, stateDir: string = getStateDir()): number {
|
|
153
|
+
const db = openStore(stateDir);
|
|
154
|
+
const row = db
|
|
155
|
+
.prepare("SELECT MAX(timestamp) AS mx FROM context_chunks WHERE session_id = ?")
|
|
156
|
+
.get(normalizeSessionId(sessionId)) as { mx: number | null } | undefined;
|
|
157
|
+
return Number(row?.mx ?? 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Next sequential checkpoint id (chkpt_001 …) for a session. */
|
|
161
|
+
export function nextCheckpointId(sessionId: string, stateDir: string = getStateDir()): string {
|
|
162
|
+
const db = openStore(stateDir);
|
|
163
|
+
const sid = normalizeSessionId(sessionId);
|
|
164
|
+
const row = db
|
|
165
|
+
.prepare("SELECT MAX(CAST(SUBSTR(id, 7) AS INTEGER)) AS n FROM context_chunks WHERE session_id = ?")
|
|
166
|
+
.get(sid) as { n: number | null };
|
|
167
|
+
const next = (row.n ?? 0) + 1;
|
|
168
|
+
return `chkpt_${String(next).padStart(3, "0")}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** True if a checkpoint id already exists for a session. */
|
|
172
|
+
export function hasCheckpoint(sessionId: string, checkpointId: string, stateDir: string = getStateDir()): boolean {
|
|
173
|
+
const db = openStore(stateDir);
|
|
174
|
+
const row = db
|
|
175
|
+
.prepare("SELECT 1 FROM context_chunks WHERE session_id = ? AND id = ? LIMIT 1")
|
|
176
|
+
.get(normalizeSessionId(sessionId), checkpointId);
|
|
177
|
+
return row !== undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Fetch a single checkpoint by (session, id), or undefined if absent. */
|
|
181
|
+
export function getCheckpoint(
|
|
182
|
+
sessionId: string,
|
|
183
|
+
checkpointId: string,
|
|
184
|
+
stateDir: string = getStateDir(),
|
|
185
|
+
): StoredCheckpoint | undefined {
|
|
186
|
+
const db = openStore(stateDir);
|
|
187
|
+
const row = db
|
|
188
|
+
.prepare("SELECT * FROM context_chunks WHERE session_id = ? AND id = ? LIMIT 1")
|
|
189
|
+
.get(normalizeSessionId(sessionId), checkpointId) as any;
|
|
190
|
+
return row ? rowToCheckpoint(row) : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Mark a checkpoint's dedup_status (e.g. 'removed' by SemDeDup). */
|
|
194
|
+
export function setDedupStatus(
|
|
195
|
+
checkpointId: string,
|
|
196
|
+
sessionId: string,
|
|
197
|
+
status: string,
|
|
198
|
+
stateDir: string = getStateDir(),
|
|
199
|
+
): void {
|
|
200
|
+
const db = openStore(stateDir);
|
|
201
|
+
db.prepare(
|
|
202
|
+
"UPDATE context_chunks SET dedup_status = ? WHERE id = ? AND session_id = ?",
|
|
203
|
+
).run(status, checkpointId, normalizeSessionId(sessionId));
|
|
204
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dedup-mirror.ts — S27 Task 6 dedup_mirror CRUD.
|
|
3
|
+
*
|
|
4
|
+
* Space-efficient deduplicated storage: each unique content_hash stores its
|
|
5
|
+
* bytes ONCE; raw_transcript rows reference this table via content_ref
|
|
6
|
+
* instead of storing duplicate content_bytes inline.
|
|
7
|
+
*/
|
|
8
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Dedup mirror row (DB representation).
|
|
12
|
+
*/
|
|
13
|
+
export interface DedupMirrorRowDB {
|
|
14
|
+
content_hash: string;
|
|
15
|
+
content_bytes: string;
|
|
16
|
+
ref_count: number;
|
|
17
|
+
first_seen_seq: number;
|
|
18
|
+
created_at: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Upsert a row into dedup_mirror. If the hash already exists, increment ref_count.
|
|
23
|
+
* Returns true if this was a NEW unique content (first insert), false if it was a duplicate.
|
|
24
|
+
*/
|
|
25
|
+
export function upsertDedupMirror(
|
|
26
|
+
db: DatabaseSync,
|
|
27
|
+
contentHash: string,
|
|
28
|
+
contentBytes: string,
|
|
29
|
+
seq: number,
|
|
30
|
+
): boolean {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const existing = db
|
|
33
|
+
.prepare(`SELECT content_hash FROM dedup_mirror WHERE content_hash = @hash`)
|
|
34
|
+
.get({ "@hash": contentHash }) as { content_hash: string } | undefined;
|
|
35
|
+
if (existing) {
|
|
36
|
+
db.prepare(`UPDATE dedup_mirror SET ref_count = ref_count + 1 WHERE content_hash = @hash`).run({
|
|
37
|
+
"@hash": contentHash,
|
|
38
|
+
});
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
db.prepare(
|
|
42
|
+
`INSERT INTO dedup_mirror (content_hash, content_bytes, ref_count, first_seen_seq, created_at)
|
|
43
|
+
VALUES (@hash, @bytes, 1, @seq, @now)`,
|
|
44
|
+
).run({
|
|
45
|
+
"@hash": contentHash,
|
|
46
|
+
"@bytes": contentBytes,
|
|
47
|
+
"@seq": seq,
|
|
48
|
+
"@now": now,
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get dedup ratio for a session: total bytes vs unique bytes.
|
|
55
|
+
*/
|
|
56
|
+
export function getDedupRatio(
|
|
57
|
+
db: DatabaseSync,
|
|
58
|
+
sessionId: string,
|
|
59
|
+
): { totalBytes: number; uniqueBytes: number; ratio: number } {
|
|
60
|
+
const totalRow = db
|
|
61
|
+
.prepare(
|
|
62
|
+
`SELECT COALESCE(SUM(LENGTH(content_bytes)), 0) AS total
|
|
63
|
+
FROM raw_transcript
|
|
64
|
+
WHERE session_id = @session_id`,
|
|
65
|
+
)
|
|
66
|
+
.get({ "@session_id": sessionId }) as { total: number };
|
|
67
|
+
const uniqueRow = db
|
|
68
|
+
.prepare(
|
|
69
|
+
`SELECT COALESCE(SUM(LENGTH(content_bytes)), 0) AS unique_bytes
|
|
70
|
+
FROM dedup_mirror`,
|
|
71
|
+
)
|
|
72
|
+
.get() as { unique_bytes: number };
|
|
73
|
+
const totalBytes = totalRow.total;
|
|
74
|
+
const uniqueBytes = uniqueRow.unique_bytes;
|
|
75
|
+
const ratio = uniqueBytes > 0 ? totalBytes / uniqueBytes : 1;
|
|
76
|
+
return { totalBytes, uniqueBytes, ratio };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get dedup mirror stats (diagnostic / test helper).
|
|
81
|
+
*/
|
|
82
|
+
export function getDedupMirrorStats(db: DatabaseSync): {
|
|
83
|
+
rowCount: number;
|
|
84
|
+
totalBytes: number;
|
|
85
|
+
avgRefCount: number;
|
|
86
|
+
} {
|
|
87
|
+
const row = db
|
|
88
|
+
.prepare(
|
|
89
|
+
`SELECT COUNT(*) AS cnt,
|
|
90
|
+
COALESCE(SUM(LENGTH(content_bytes)), 0) AS total_bytes,
|
|
91
|
+
COALESCE(AVG(ref_count), 0) AS avg_ref
|
|
92
|
+
FROM dedup_mirror`,
|
|
93
|
+
)
|
|
94
|
+
.get() as { cnt: number; total_bytes: number; avg_ref: number };
|
|
95
|
+
return { rowCount: row.cnt, totalBytes: row.total_bytes, avgRefCount: row.avg_ref };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update raw_transcript.content_ref to point to dedup_mirror.
|
|
100
|
+
*/
|
|
101
|
+
export function updateRawTranscriptRef(
|
|
102
|
+
db: DatabaseSync,
|
|
103
|
+
sessionId: string,
|
|
104
|
+
seq: number,
|
|
105
|
+
contentHash: string,
|
|
106
|
+
): void {
|
|
107
|
+
db.prepare(
|
|
108
|
+
`UPDATE raw_transcript SET content_ref = @ref WHERE session_id = @sid AND seq = @seq`,
|
|
109
|
+
).run({
|
|
110
|
+
"@ref": contentHash,
|
|
111
|
+
"@sid": sessionId,
|
|
112
|
+
"@seq": seq,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foundation.ts — future-feature foundation (resume sessions / daily log / lessons).
|
|
3
|
+
*
|
|
4
|
+
* Scaffolded tables + minimal helpers so all store data lives in SQLite from
|
|
5
|
+
* day one. Full UI/recall for these lands in later sprints.
|
|
6
|
+
*/
|
|
7
|
+
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
8
|
+
import { openStore } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
/** Upsert a `sessions` row (resume + per-repo session history). */
|
|
11
|
+
export function touchSession(
|
|
12
|
+
sessionId: string,
|
|
13
|
+
repo: string | undefined,
|
|
14
|
+
stateDir: string = getStateDir(),
|
|
15
|
+
): void {
|
|
16
|
+
const db = openStore(stateDir);
|
|
17
|
+
const sid = normalizeSessionId(sessionId);
|
|
18
|
+
const existing = db
|
|
19
|
+
.prepare("SELECT started_at FROM sessions WHERE session_id = ?")
|
|
20
|
+
.get(sid) as { started_at: number | null } | undefined;
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
if (!existing) {
|
|
23
|
+
db.prepare(
|
|
24
|
+
`INSERT INTO sessions(session_id, repo, started_at, last_compacted_at, status)
|
|
25
|
+
VALUES(?, ?, ?, ?, 'active')`,
|
|
26
|
+
).run(sid, repo ?? null, now, now);
|
|
27
|
+
} else {
|
|
28
|
+
db.prepare(
|
|
29
|
+
"UPDATE sessions SET last_compacted_at = ?, repo = COALESCE(?, repo), status = 'active' WHERE session_id = ?",
|
|
30
|
+
).run(now, repo ?? null, sid);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Append a `daily_log` entry (day = YYYY-MM-DD, local-naive from Date). */
|
|
35
|
+
export function logDaily(
|
|
36
|
+
sessionId: string,
|
|
37
|
+
event: string,
|
|
38
|
+
detail: string | undefined,
|
|
39
|
+
tokensSaved: number,
|
|
40
|
+
stateDir: string = getStateDir(),
|
|
41
|
+
): void {
|
|
42
|
+
const db = openStore(stateDir);
|
|
43
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
44
|
+
const now = Math.floor(Date.now() / 1000);
|
|
45
|
+
db.prepare(
|
|
46
|
+
`INSERT INTO daily_log(day, session_id, event, detail, tokens_saved, ts)
|
|
47
|
+
VALUES(?, ?, ?, ?, ?, ?)`,
|
|
48
|
+
).run(day, normalizeSessionId(sessionId), event, detail ?? null, tokensSaved, now);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Append a `lessons` entry (future lessons-learned browse/recall). */
|
|
52
|
+
export function addLesson(
|
|
53
|
+
sessionId: string,
|
|
54
|
+
repo: string | undefined,
|
|
55
|
+
lesson: string,
|
|
56
|
+
stateDir: string = getStateDir(),
|
|
57
|
+
): void {
|
|
58
|
+
const db = openStore(stateDir);
|
|
59
|
+
const now = Math.floor(Date.now() / 1000);
|
|
60
|
+
db.prepare(
|
|
61
|
+
`INSERT INTO lessons(session_id, repo, lesson, ts) VALUES(?, ?, ?, ?)`,
|
|
62
|
+
).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
|
|
63
|
+
}
|