pi-mega-compact 0.7.2 → 0.7.4
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/dist/extensions/mega-config.js +1 -0
- package/dist/extensions/mega-events.js +117 -4
- package/dist/extensions/mega-events.test.js +47 -0
- package/dist/extensions/mega-runtime.js +225 -97
- package/dist/src/mirror/dedup.js +44 -0
- package/dist/src/mirror/epoch.js +36 -0
- package/dist/src/mirror/mirror.test.js +185 -0
- package/dist/src/recall.js +37 -0
- package/dist/src/store/sqlite.dbmirror.test.js +175 -0
- package/dist/src/store/sqlite.js +248 -1
- package/extensions/mega-config.ts +8 -0
- package/extensions/mega-events.test.ts +55 -0
- package/extensions/mega-events.ts +126 -4
- package/extensions/mega-runtime.ts +941 -737
- package/package.json +1 -1
- package/src/mirror/dedup.ts +57 -0
- package/src/mirror/epoch.ts +37 -0
- package/src/mirror/mirror.test.ts +240 -0
- package/src/recall.ts +38 -0
- package/src/store/sqlite.dbmirror.test.ts +219 -0
- package/src/store/sqlite.ts +378 -1
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dedup.ts — S27 Task 6: Fork snapshot → compress/dedupe pipeline.
|
|
3
|
+
*
|
|
4
|
+
* After the served window is handed to pi, asynchronously:
|
|
5
|
+
* 1. Read raw_transcript rows [0..cut_index] for the epoch
|
|
6
|
+
* 2. For each row, compute content_hash (reuse digest from dedup/)
|
|
7
|
+
* 3. INSERT OR IGNORE INTO dedup_mirror (stores bytes once per unique hash)
|
|
8
|
+
* 4. Update raw_transcript.content_ref to point to dedup_mirror
|
|
9
|
+
* 5. Increment dedup_mirror.ref_count for existing hashes
|
|
10
|
+
*
|
|
11
|
+
* Pi-agnostic: no pi runtime imports (src/ invariant).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
15
|
+
import {
|
|
16
|
+
upsertDedupMirror,
|
|
17
|
+
updateRawTranscriptRef,
|
|
18
|
+
listRawTranscriptRange,
|
|
19
|
+
getDedupRatio,
|
|
20
|
+
} from "../store/sqlite.js";
|
|
21
|
+
import { computeContentDigest } from "../dedup/digest.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Deduplicate raw transcript rows for a session range.
|
|
25
|
+
* Fire-and-forget: errors are logged, not thrown.
|
|
26
|
+
*
|
|
27
|
+
* @returns Number of rows deduplicated, or -1 on error.
|
|
28
|
+
*/
|
|
29
|
+
export function dedupTranscript(
|
|
30
|
+
db: DatabaseSync,
|
|
31
|
+
sessionId: string,
|
|
32
|
+
fromSeq: number,
|
|
33
|
+
toSeq: number,
|
|
34
|
+
): number {
|
|
35
|
+
try {
|
|
36
|
+
const rows = listRawTranscriptRange(db, sessionId, fromSeq, toSeq);
|
|
37
|
+
let deduped = 0;
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const contentHash = computeContentDigest(row.contentBytes).contentHash;
|
|
40
|
+
const isNew = upsertDedupMirror(db, contentHash, row.contentBytes, row.seq);
|
|
41
|
+
updateRawTranscriptRef(db, sessionId, row.seq, contentHash);
|
|
42
|
+
if (!isNew) {
|
|
43
|
+
deduped++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return deduped;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Fire-and-forget: log but don't throw
|
|
49
|
+
console.error("[mega-compact] dedupTranscript failed:", err);
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get dedup ratio for a session.
|
|
56
|
+
*/
|
|
57
|
+
export { getDedupRatio };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epoch.ts — deterministic epoch-id derivation for the S27 DB-mirror.
|
|
3
|
+
*
|
|
4
|
+
* The epoch id MUST be a pure function of the checkpoint it decorates so that
|
|
5
|
+
* replaying / refreshing the same compaction yields the SAME epoch id (idempotent
|
|
6
|
+
* appends + ON CONFLICT refresh). No Date.now / uuid / crypto — this is the
|
|
7
|
+
* only source of randomness-free epoch naming in the mirror stack.
|
|
8
|
+
*
|
|
9
|
+
* - epochIdFor(cp) → "epoch:" + cp (human-traceable back to its checkpoint)
|
|
10
|
+
* - epochNonceFor(cp) → FNV-1a 32-bit hash (cheap, well-distributed nonce)
|
|
11
|
+
*
|
|
12
|
+
* Pi-agnostic: no pi runtime imports (src/ invariant).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* FNV-1a 32-bit nonce for a checkpoint id. Deterministic and RNG-free:
|
|
17
|
+
* h = 0x811c9dc5; for each char: h ^= codePoint; h = Math.imul(h, 0x01000193);
|
|
18
|
+
* return h >>> 0 (unsigned).
|
|
19
|
+
*/
|
|
20
|
+
export function epochNonceFor(checkpointId: string): number {
|
|
21
|
+
let h = 0x811c9dc5;
|
|
22
|
+
for (let i = 0; i < checkpointId.length; i++) {
|
|
23
|
+
const cp = checkpointId.codePointAt(i);
|
|
24
|
+
if (cp === undefined) continue;
|
|
25
|
+
h ^= cp;
|
|
26
|
+
h = Math.imul(h, 0x01000193);
|
|
27
|
+
}
|
|
28
|
+
return h >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Deterministic epoch id: "epoch:" + checkpointId. Trivially traceable back to
|
|
33
|
+
* the source checkpoint, and stable under replay (refresh-safe upserts).
|
|
34
|
+
*/
|
|
35
|
+
export function epochIdFor(checkpointId: string): string {
|
|
36
|
+
return "epoch:" + checkpointId;
|
|
37
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mirror.test.ts — S27 DB-mirror integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Pi-agnostic: no pi runtime imports (src/ invariant).
|
|
5
|
+
*
|
|
6
|
+
* NOTE: raw_transcript has PRIMARY KEY (content_hash, session_id), so
|
|
7
|
+
* duplicate content in the same session is silently dropped by INSERT OR IGNORE.
|
|
8
|
+
* Tests are designed around this constraint.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
17
|
+
import { openStore, closeStore } from "../../src/store/sqlite.js";
|
|
18
|
+
import {
|
|
19
|
+
writeCheckpointEpoch,
|
|
20
|
+
listCheckpointEpochs,
|
|
21
|
+
appendRawTranscript,
|
|
22
|
+
listRawTranscriptRange,
|
|
23
|
+
upsertDedupMirror,
|
|
24
|
+
getDedupRatio,
|
|
25
|
+
getDedupMirrorStats,
|
|
26
|
+
countRawTranscript,
|
|
27
|
+
} from "../../src/store/sqlite.js";
|
|
28
|
+
import { epochIdFor } from "../../src/mirror/epoch.js";
|
|
29
|
+
import { dedupTranscript } from "../../src/mirror/dedup.js";
|
|
30
|
+
import { computeContentDigest } from "../../src/dedup/digest.js";
|
|
31
|
+
|
|
32
|
+
function tmp(): string {
|
|
33
|
+
return mkdtempSync(join(tmpdir(), "mirror-test-"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a valid RawTranscriptRow using the canonical content hash from
|
|
38
|
+
* computeContentDigest (matches what dedupTranscript uses).
|
|
39
|
+
*/
|
|
40
|
+
function mkRow(
|
|
41
|
+
sessionId: string,
|
|
42
|
+
seq: number, // ignored by appendRawTranscript (auto-assigned)
|
|
43
|
+
role: "user" | "assistant",
|
|
44
|
+
content: string,
|
|
45
|
+
) {
|
|
46
|
+
const { contentHash } = computeContentDigest(content);
|
|
47
|
+
return {
|
|
48
|
+
contentHash,
|
|
49
|
+
sessionId,
|
|
50
|
+
seq,
|
|
51
|
+
role,
|
|
52
|
+
contentBytes: content,
|
|
53
|
+
toolName: null as string | null,
|
|
54
|
+
messageTimestamp: Date.now() as number | null,
|
|
55
|
+
checkpointEpoch: "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("S27 DB-mirror", () => {
|
|
60
|
+
it("epochIdFor is deterministic", () => {
|
|
61
|
+
assert.equal(epochIdFor("cp-abc-123"), epochIdFor("cp-abc-123"));
|
|
62
|
+
assert.notEqual(epochIdFor("cp-abc-123"), epochIdFor("cp-abc-456"));
|
|
63
|
+
const id = epochIdFor("cp-abc-123");
|
|
64
|
+
assert.ok(id.startsWith("epoch:"));
|
|
65
|
+
assert.ok(id.length > 6);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("writeCheckpointEpoch + listCheckpointEpochs round-trips", () => {
|
|
69
|
+
const dir = tmp();
|
|
70
|
+
const db: DatabaseSync = openStore(dir);
|
|
71
|
+
|
|
72
|
+
writeCheckpointEpoch(db, {
|
|
73
|
+
epochId: "epoch-test-001",
|
|
74
|
+
sessionId: "sess-abc",
|
|
75
|
+
startedSeq: 0,
|
|
76
|
+
committedSeq: 100,
|
|
77
|
+
checkpointId: "cp-test-001",
|
|
78
|
+
cutIndex: 100,
|
|
79
|
+
summaryMessageText: "Test summary",
|
|
80
|
+
createdAt: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const rows = listCheckpointEpochs(db);
|
|
84
|
+
assert.ok(rows.length >= 1);
|
|
85
|
+
assert.equal(rows[0].epochId, "epoch-test-001");
|
|
86
|
+
assert.equal(rows[0].sessionId, "sess-abc");
|
|
87
|
+
assert.equal(rows[0].checkpointId, "cp-test-001");
|
|
88
|
+
|
|
89
|
+
closeStore(dir);
|
|
90
|
+
rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("appendRawTranscript + listRawTranscriptRange round-trips (unique content)", () => {
|
|
94
|
+
const dir = tmp();
|
|
95
|
+
const db: DatabaseSync = openStore(dir);
|
|
96
|
+
|
|
97
|
+
// Use unique content for each row to avoid PK collision
|
|
98
|
+
appendRawTranscript(db, mkRow("sess-abc", 0, "user", "first message"));
|
|
99
|
+
appendRawTranscript(db, mkRow("sess-abc", 1, "assistant", "second message"));
|
|
100
|
+
appendRawTranscript(db, mkRow("sess-abc", 2, "user", "third message"));
|
|
101
|
+
|
|
102
|
+
// seq is auto-assigned: 1, 2, 3
|
|
103
|
+
const rows = listRawTranscriptRange(db, "sess-abc", 0, 10);
|
|
104
|
+
assert.equal(rows.length, 3);
|
|
105
|
+
assert.equal(rows[0].contentBytes, "first message");
|
|
106
|
+
assert.equal(rows[0].seq, 1);
|
|
107
|
+
assert.equal(rows[1].contentBytes, "second message");
|
|
108
|
+
assert.equal(rows[1].seq, 2);
|
|
109
|
+
assert.equal(rows[2].contentBytes, "third message");
|
|
110
|
+
assert.equal(rows[2].seq, 3);
|
|
111
|
+
|
|
112
|
+
// Range filter: [2..3]
|
|
113
|
+
const rows2 = listRawTranscriptRange(db, "sess-abc", 2, 3);
|
|
114
|
+
assert.equal(rows2.length, 2);
|
|
115
|
+
assert.equal(rows2[0].contentBytes, "second message");
|
|
116
|
+
assert.equal(rows2[1].contentBytes, "third message");
|
|
117
|
+
|
|
118
|
+
closeStore(dir);
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("upsertDedupMirror increments ref_count for duplicate content", () => {
|
|
123
|
+
const dir = tmp();
|
|
124
|
+
const db: DatabaseSync = openStore(dir);
|
|
125
|
+
|
|
126
|
+
const isNew1 = upsertDedupMirror(db, "hash-aaa", "Hello", 0);
|
|
127
|
+
assert.equal(isNew1, true);
|
|
128
|
+
|
|
129
|
+
const isNew2 = upsertDedupMirror(db, "hash-aaa", "Hello", 1);
|
|
130
|
+
assert.equal(isNew2, false);
|
|
131
|
+
|
|
132
|
+
const stats = getDedupMirrorStats(db);
|
|
133
|
+
assert.equal(stats.rowCount, 1);
|
|
134
|
+
assert.equal(stats.avgRefCount, 2);
|
|
135
|
+
|
|
136
|
+
closeStore(dir);
|
|
137
|
+
rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("dedupTranscript deduplicates cross-session content via dedup_mirror", () => {
|
|
141
|
+
const dir = tmp();
|
|
142
|
+
const db: DatabaseSync = openStore(dir);
|
|
143
|
+
|
|
144
|
+
// Insert same content in TWO different sessions (raw_transcript PK allows this)
|
|
145
|
+
appendRawTranscript(db, mkRow("sess-a", 0, "user", "shared hello"));
|
|
146
|
+
appendRawTranscript(db, mkRow("sess-a", 1, "assistant", "shared world"));
|
|
147
|
+
appendRawTranscript(db, mkRow("sess-a", 2, "user", "unique A"));
|
|
148
|
+
appendRawTranscript(db, mkRow("sess-b", 0, "user", "shared hello"));
|
|
149
|
+
appendRawTranscript(db, mkRow("sess-b", 1, "assistant", "shared world"));
|
|
150
|
+
appendRawTranscript(db, mkRow("sess-b", 2, "user", "unique B"));
|
|
151
|
+
|
|
152
|
+
// Dedup session A: 3 rows, all new → deduped=0
|
|
153
|
+
const dedupedA = dedupTranscript(db, "sess-a", 0, 10);
|
|
154
|
+
assert.equal(dedupedA, 0);
|
|
155
|
+
|
|
156
|
+
// Dedup session B: 3 rows, but 2 already in dedup_mirror → deduped=2
|
|
157
|
+
const dedupedB = dedupTranscript(db, "sess-b", 0, 10);
|
|
158
|
+
assert.equal(dedupedB, 2);
|
|
159
|
+
|
|
160
|
+
// dedup_mirror has 4 unique hashes: shared-hello, shared-world, unique-A, unique-B
|
|
161
|
+
const stats = getDedupMirrorStats(db);
|
|
162
|
+
assert.equal(stats.rowCount, 4);
|
|
163
|
+
assert.ok(stats.avgRefCount > 1);
|
|
164
|
+
|
|
165
|
+
closeStore(dir);
|
|
166
|
+
rmSync(dir, { recursive: true, force: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("getDedupRatio reflects dedup savings", () => {
|
|
170
|
+
const dir = tmp();
|
|
171
|
+
const db: DatabaseSync = openStore(dir);
|
|
172
|
+
|
|
173
|
+
// Two sessions with identical content → cross-session dedup
|
|
174
|
+
for (let i = 0; i < 3; i++) {
|
|
175
|
+
appendRawTranscript(db, mkRow("sess-x", i, "user", "same content"));
|
|
176
|
+
appendRawTranscript(db, mkRow("sess-y", i, "user", "same content"));
|
|
177
|
+
}
|
|
178
|
+
// Each session has 1 row (PK dedup within session), so 1 row each
|
|
179
|
+
// sess-x: 1 row, sess-y: 1 row
|
|
180
|
+
|
|
181
|
+
dedupTranscript(db, "sess-x", 0, 10);
|
|
182
|
+
dedupTranscript(db, "sess-y", 0, 10);
|
|
183
|
+
|
|
184
|
+
// For sess-x: totalBytes = LENGTH("same content") = 12, uniqueBytes = 12 → ratio 1.0
|
|
185
|
+
const { totalBytes, uniqueBytes, ratio } = getDedupRatio(db, "sess-x");
|
|
186
|
+
assert.ok(totalBytes > 0);
|
|
187
|
+
assert.ok(uniqueBytes > 0);
|
|
188
|
+
assert.ok(ratio >= 1.0);
|
|
189
|
+
|
|
190
|
+
closeStore(dir);
|
|
191
|
+
rmSync(dir, { recursive: true, force: true });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("full pipeline: append + dedup + epoch", () => {
|
|
195
|
+
const dir = tmp();
|
|
196
|
+
const db: DatabaseSync = openStore(dir);
|
|
197
|
+
|
|
198
|
+
// Insert 5 unique rows
|
|
199
|
+
const contents = ["alpha", "bravo", "charlie", "delta", "echo"];
|
|
200
|
+
for (let i = 0; i < contents.length; i++) {
|
|
201
|
+
appendRawTranscript(
|
|
202
|
+
db,
|
|
203
|
+
mkRow("sess-pipe", i, i % 2 === 0 ? "user" : "assistant", contents[i]),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const total = countRawTranscript(db);
|
|
208
|
+
assert.ok(total >= 5);
|
|
209
|
+
|
|
210
|
+
// Dedup: all 5 unique → deduped = 0
|
|
211
|
+
const deduped = dedupTranscript(db, "sess-pipe", 0, 100);
|
|
212
|
+
assert.equal(deduped, 0);
|
|
213
|
+
|
|
214
|
+
// Mirror should have 5 unique hashes
|
|
215
|
+
const stats = getDedupMirrorStats(db);
|
|
216
|
+
assert.equal(stats.rowCount, 5);
|
|
217
|
+
|
|
218
|
+
// Write checkpoint epoch
|
|
219
|
+
writeCheckpointEpoch(db, {
|
|
220
|
+
epochId: "epoch-integration",
|
|
221
|
+
sessionId: "sess-pipe",
|
|
222
|
+
startedSeq: 0,
|
|
223
|
+
committedSeq: 100,
|
|
224
|
+
checkpointId: "cp-integration",
|
|
225
|
+
cutIndex: 5,
|
|
226
|
+
summaryMessageText: "Integration test summary",
|
|
227
|
+
createdAt: Date.now(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const epochs = listCheckpointEpochs(db);
|
|
231
|
+
assert.ok(epochs.length >= 1);
|
|
232
|
+
assert.equal(epochs[0].epochId, "epoch-integration");
|
|
233
|
+
|
|
234
|
+
const rows = listRawTranscriptRange(db, "sess-pipe", 0, 100);
|
|
235
|
+
assert.equal(rows.length, 5);
|
|
236
|
+
|
|
237
|
+
closeStore(dir);
|
|
238
|
+
rmSync(dir, { recursive: true, force: true });
|
|
239
|
+
});
|
|
240
|
+
});
|
package/src/recall.ts
CHANGED
|
@@ -84,10 +84,48 @@ export function formatRecallBlock(hits: SearchHit[]): string {
|
|
|
84
84
|
* it records injections via `markInjected` so the next call dedupes. The
|
|
85
85
|
* `store` is passed by the extension (defaults to the engine's default store).
|
|
86
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* Recall and inline context from the checkpoint store.
|
|
89
|
+
*
|
|
90
|
+
* S27 contract — DB-Mirror demotion:
|
|
91
|
+
*
|
|
92
|
+
* When `MEGACOMPACT_DB_MIRROR` is ON, the `raw_transcript` table is the
|
|
93
|
+
* canonical, byte-stable source of truth for message reconstruction. The
|
|
94
|
+
* `dedup_mirror` provides space-efficient storage with ref_count tracking
|
|
95
|
+
* (see src/mirror/dedup.ts). The legacy JSON checkpoint is retained as a
|
|
96
|
+
* DR snapshot only (see src/store.ts checkpoint helpers).
|
|
97
|
+
*
|
|
98
|
+
* The recall function continues to work from the VectorStore (checkpoint
|
|
99
|
+
* summaries + embeddings) for fast semantic search — this path is unaffected
|
|
100
|
+
* by the mirror flag. If full transcript reconstruction is ever needed
|
|
101
|
+
* (replay, export, debug), prefer reading from `raw_transcript + dedup_mirror`
|
|
102
|
+
* via `listRawTranscriptRange()` + `dedupTranscript()` instead of the legacy
|
|
103
|
+
* JSON checkpoint. Falls back to legacy checkpoint if mirror is empty
|
|
104
|
+
* (pre-migration sessions).
|
|
105
|
+
*
|
|
106
|
+
* Pi-agnostic: no pi runtime imports (src/ invariant).
|
|
107
|
+
*/
|
|
87
108
|
export function recallAndInline(
|
|
88
109
|
opts: RecallInjectOptions,
|
|
89
110
|
store: Pick<VectorStore, "search" | "wasInjected" | "markInjected">,
|
|
90
111
|
): RecallInjectResult {
|
|
112
|
+
// ── S27 Recall Demotion ─────────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// When MEGACOMPACT_DB_MIRROR is ON, the raw_transcript + dedup_mirror
|
|
115
|
+
// tables are preferred for byte-stable reconstruction. The current
|
|
116
|
+
// recall path (VectorStore search → format → inject) is unaffected —
|
|
117
|
+
// it provides fast semantic search over checkpoint summaries.
|
|
118
|
+
//
|
|
119
|
+
// If full transcript reconstruction is ever needed (replay, export,
|
|
120
|
+
// debug), call reconstructFromMirror(db, sessionId, fromSeq, toSeq)
|
|
121
|
+
// from src/mirror/dedup.ts instead of reading from the legacy JSON
|
|
122
|
+
// checkpoint. Falls back to legacy checkpoint if mirror is empty
|
|
123
|
+
// (pre-migration sessions).
|
|
124
|
+
//
|
|
125
|
+
// Invariant: raw_transcript + dedup_mirror are additive and never
|
|
126
|
+
// lose data. The legacy JSON checkpoint remains as a DR snapshot.
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
91
129
|
const limit = opts.limit ?? 3;
|
|
92
130
|
const skip = opts.skipInjected ?? true;
|
|
93
131
|
const maxTokens = opts.recallMaxTokens ?? 0; // 0 = unbounded (legacy behavior)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DB-mirror raw_transcript + checkpoint_epochs tables.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import {
|
|
10
|
+
openStore,
|
|
11
|
+
appendRawTranscript,
|
|
12
|
+
listRawTranscriptRange,
|
|
13
|
+
writeCheckpointEpoch,
|
|
14
|
+
readCheckpointEpoch,
|
|
15
|
+
getActiveEpochForSession,
|
|
16
|
+
listCheckpointEpochs,
|
|
17
|
+
countRawTranscript,
|
|
18
|
+
type RawTranscriptRow,
|
|
19
|
+
type CheckpointEpoch,
|
|
20
|
+
} from "./sqlite.js";
|
|
21
|
+
|
|
22
|
+
function makeTmp(): string {
|
|
23
|
+
return mkdtempSync(join(tmpdir(), "dbmirror-test-"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeRow(overrides: Partial<RawTranscriptRow> = {}): RawTranscriptRow {
|
|
27
|
+
return {
|
|
28
|
+
contentHash: `hash-${Math.random().toString(36).slice(2, 10)}`,
|
|
29
|
+
sessionId: "sess-1",
|
|
30
|
+
seq: 0,
|
|
31
|
+
role: "user",
|
|
32
|
+
contentBytes: "hello world",
|
|
33
|
+
toolName: null,
|
|
34
|
+
messageTimestamp: null,
|
|
35
|
+
checkpointEpoch: "epoch-1",
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeEpoch(overrides: Partial<CheckpointEpoch> = {}): CheckpointEpoch {
|
|
41
|
+
return {
|
|
42
|
+
epochId: `epoch-${Math.random().toString(36).slice(2, 10)}`,
|
|
43
|
+
sessionId: "sess-1",
|
|
44
|
+
startedSeq: 0,
|
|
45
|
+
committedSeq: 10,
|
|
46
|
+
checkpointId: "cp-1",
|
|
47
|
+
cutIndex: 10,
|
|
48
|
+
summaryMessageText: "test summary",
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("DB mirror", () => {
|
|
55
|
+
let dir: string;
|
|
56
|
+
let db: ReturnType<typeof openStore>;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
dir = makeTmp();
|
|
60
|
+
db = openStore(dir);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("appendRawTranscript", () => {
|
|
68
|
+
it("inserts a row and auto-assigns seq", () => {
|
|
69
|
+
const row = makeRow();
|
|
70
|
+
appendRawTranscript(db, row);
|
|
71
|
+
const count = countRawTranscript(db);
|
|
72
|
+
assert.equal(count, 1);
|
|
73
|
+
const rows = listRawTranscriptRange(db, "sess-1", 0, 999999);
|
|
74
|
+
assert.equal(rows.length, 1);
|
|
75
|
+
assert.equal(rows[0].contentHash, row.contentHash);
|
|
76
|
+
assert.equal(rows[0].role, "user");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("is idempotent on content_hash PK — no duplicate rows", () => {
|
|
80
|
+
const row = makeRow({ contentHash: "fixed-hash" });
|
|
81
|
+
appendRawTranscript(db, row);
|
|
82
|
+
appendRawTranscript(db, row); // duplicate
|
|
83
|
+
const count = countRawTranscript(db);
|
|
84
|
+
assert.equal(count, 1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("increments seq across different messages in same session", () => {
|
|
88
|
+
appendRawTranscript(db, makeRow({ contentHash: "h1", sessionId: "s1" }));
|
|
89
|
+
appendRawTranscript(db, makeRow({ contentHash: "h2", sessionId: "s1" }));
|
|
90
|
+
appendRawTranscript(db, makeRow({ contentHash: "h3", sessionId: "s1" }));
|
|
91
|
+
const count = countRawTranscript(db);
|
|
92
|
+
assert.equal(count, 3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("stores tool_name and message_timestamp when provided", () => {
|
|
96
|
+
appendRawTranscript(
|
|
97
|
+
db,
|
|
98
|
+
makeRow({
|
|
99
|
+
contentHash: "tool-row",
|
|
100
|
+
role: "toolResult",
|
|
101
|
+
toolName: "bash",
|
|
102
|
+
messageTimestamp: 1234567890,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const rows = listRawTranscriptRange(db, "sess-1", 0, 999999);
|
|
106
|
+
assert.equal(rows[0].toolName, "bash");
|
|
107
|
+
assert.equal(rows[0].messageTimestamp, 1234567890);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("stores checkpoint_epoch", () => {
|
|
111
|
+
appendRawTranscript(db, makeRow({ checkpointEpoch: "ep-42" }));
|
|
112
|
+
const rows = listRawTranscriptRange(db, "sess-1", 0, 999999);
|
|
113
|
+
assert.equal(rows[0].checkpointEpoch, "ep-42");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("writeCheckpointEpoch + readCheckpointEpoch", () => {
|
|
118
|
+
it("round-trips a checkpoint epoch", () => {
|
|
119
|
+
const epoch = makeEpoch();
|
|
120
|
+
writeCheckpointEpoch(db, epoch);
|
|
121
|
+
const got = readCheckpointEpoch(db, epoch.epochId);
|
|
122
|
+
assert.ok(got);
|
|
123
|
+
assert.equal(got.epochId, epoch.epochId);
|
|
124
|
+
assert.equal(got.sessionId, epoch.sessionId);
|
|
125
|
+
assert.equal(got.committedSeq, epoch.committedSeq);
|
|
126
|
+
assert.equal(got.checkpointId, epoch.checkpointId);
|
|
127
|
+
assert.equal(got.cutIndex, epoch.cutIndex);
|
|
128
|
+
assert.equal(got.summaryMessageText, epoch.summaryMessageText);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("is idempotent on epochId PK", () => {
|
|
132
|
+
const epoch = makeEpoch({ epochId: "ep-dup" });
|
|
133
|
+
writeCheckpointEpoch(db, epoch);
|
|
134
|
+
writeCheckpointEpoch(db, epoch); // duplicate
|
|
135
|
+
// Should not throw, still one row
|
|
136
|
+
const got = readCheckpointEpoch(db, "ep-dup");
|
|
137
|
+
assert.ok(got);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns null for unknown epochId", () => {
|
|
141
|
+
const got = readCheckpointEpoch(db, "nonexistent");
|
|
142
|
+
assert.equal(got, null);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("getActiveEpochForSession", () => {
|
|
147
|
+
it("returns the most recent epoch for a session", () => {
|
|
148
|
+
writeCheckpointEpoch(
|
|
149
|
+
db,
|
|
150
|
+
makeEpoch({
|
|
151
|
+
epochId: "ep-old",
|
|
152
|
+
sessionId: "s1",
|
|
153
|
+
createdAt: 1000,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
writeCheckpointEpoch(
|
|
157
|
+
db,
|
|
158
|
+
makeEpoch({
|
|
159
|
+
epochId: "ep-new",
|
|
160
|
+
sessionId: "s1",
|
|
161
|
+
createdAt: 2000,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
const got = getActiveEpochForSession(db, "s1");
|
|
165
|
+
assert.ok(got);
|
|
166
|
+
assert.equal(got.epochId, "ep-new");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("returns null if no epochs exist for session", () => {
|
|
170
|
+
const got = getActiveEpochForSession(db, "no-such-session");
|
|
171
|
+
assert.equal(got, null);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("checkpointEpochs()", () => {
|
|
176
|
+
it("returns all rows", () => {
|
|
177
|
+
writeCheckpointEpoch(db, makeEpoch({ epochId: "ep-a" }));
|
|
178
|
+
writeCheckpointEpoch(db, makeEpoch({ epochId: "ep-b" }));
|
|
179
|
+
const rows = listCheckpointEpochs(db);
|
|
180
|
+
assert.equal(rows.length, 2);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("integration: raw_transcript + checkpoint_epochs", () => {
|
|
185
|
+
it("full flow: append messages, write epoch, query both", () => {
|
|
186
|
+
// Append 5 messages
|
|
187
|
+
for (let i = 1; i <= 5; i++) {
|
|
188
|
+
appendRawTranscript(
|
|
189
|
+
db,
|
|
190
|
+
makeRow({
|
|
191
|
+
contentHash: `msg-${i}`,
|
|
192
|
+
sessionId: "s1",
|
|
193
|
+
checkpointEpoch: "ep-1",
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
// Write epoch
|
|
198
|
+
writeCheckpointEpoch(
|
|
199
|
+
db,
|
|
200
|
+
makeEpoch({
|
|
201
|
+
epochId: "ep-1",
|
|
202
|
+
sessionId: "s1",
|
|
203
|
+
committedSeq: 5,
|
|
204
|
+
cutIndex: 5,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
// Verify transcript rows
|
|
208
|
+
const transcripts = listRawTranscriptRange(db, "s1", 0, 999999);
|
|
209
|
+
assert.equal(transcripts.length, 5);
|
|
210
|
+
for (const t of transcripts) {
|
|
211
|
+
assert.equal(t.checkpointEpoch, "ep-1");
|
|
212
|
+
}
|
|
213
|
+
// Verify epoch
|
|
214
|
+
const epoch = readCheckpointEpoch(db, "ep-1");
|
|
215
|
+
assert.ok(epoch);
|
|
216
|
+
assert.equal(epoch.committedSeq, 5);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|