pi-mega-compact 0.4.0
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/LICENSE +24 -0
- package/README.md +375 -0
- package/extensions/DASHBOARD.md +160 -0
- package/extensions/dashboard-server.test.ts +124 -0
- package/extensions/dashboard-server.ts +459 -0
- package/extensions/error-patterns.ts +175 -0
- package/extensions/mega-compact.test.ts +351 -0
- package/extensions/mega-compact.ts +846 -0
- package/extensions/openclaw-mega-compact.ts +370 -0
- package/package.json +61 -0
- package/src/adapt.ts +120 -0
- package/src/boundary.test.ts +61 -0
- package/src/boundary.ts +94 -0
- package/src/canary.ts +126 -0
- package/src/compact.test.ts +99 -0
- package/src/compact.ts +262 -0
- package/src/config/dedup.ts +120 -0
- package/src/config.ts +15 -0
- package/src/dedup/dedup.test.ts +46 -0
- package/src/dedup/digest.ts +40 -0
- package/src/dedup/l1-lsh.ts +67 -0
- package/src/dedup/l1-minhash.ts +90 -0
- package/src/dedup/l1-verify.ts +55 -0
- package/src/dedup/l1.test.ts +57 -0
- package/src/dedup/mmr.ts +54 -0
- package/src/dedup/normalize.ts +41 -0
- package/src/dedup/raptor/guardrails.ts +112 -0
- package/src/dedup/raptor/index.ts +118 -0
- package/src/dedup/raptor/kmeans.ts +156 -0
- package/src/dedup/raptor/raptor.test.ts +238 -0
- package/src/dedup/raptor/retrieval.ts +102 -0
- package/src/dedup/raptor/summarizer.ts +91 -0
- package/src/dedup/raptor/tree.ts +254 -0
- package/src/dedup/sprint12.test.ts +242 -0
- package/src/dedup/topk.ts +61 -0
- package/src/dedup-engine.test.ts +609 -0
- package/src/e2e.test.ts +843 -0
- package/src/embedder.ts +111 -0
- package/src/engine.test.ts +123 -0
- package/src/engine.ts +192 -0
- package/src/extractive.test.ts +156 -0
- package/src/extractive.ts +265 -0
- package/src/httpEmbedder.ts +154 -0
- package/src/log.test.ts +47 -0
- package/src/log.ts +60 -0
- package/src/monitoring.ts +171 -0
- package/src/ratio.bench.test.ts +1316 -0
- package/src/recall.integration.test.ts +96 -0
- package/src/recall.test.ts +59 -0
- package/src/recall.ts +100 -0
- package/src/sprint14.test.ts +245 -0
- package/src/store/backfill.ts +263 -0
- package/src/store/bloom.ts +122 -0
- package/src/store/compression.test.ts +83 -0
- package/src/store/compression.ts +203 -0
- package/src/store/integrity.ts +65 -0
- package/src/store/migrate.test.ts +158 -0
- package/src/store/migrate.ts +108 -0
- package/src/store/sprint10.test.ts +182 -0
- package/src/store/sqlite.ts +519 -0
- package/src/store.test.ts +169 -0
- package/src/store.ts +192 -0
- package/src/supersede.test.ts +42 -0
- package/src/supersede.ts +67 -0
- package/src/tokens.ts +35 -0
- package/src/types.test.ts +10 -0
- package/src/types.ts +49 -0
- package/src/vectorStore.test.ts +480 -0
- package/src/vectorStore.ts +544 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { VectorStore } from "./vectorStore.js";
|
|
7
|
+
import { compactSession } from "./engine.js";
|
|
8
|
+
import { recallAndInline } from "./recall.js";
|
|
9
|
+
import type { EngineMessage } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-resume-"));
|
|
12
|
+
let counter = 0;
|
|
13
|
+
/** Two instances, SAME disk dir — simulates a fresh process / resumed session. */
|
|
14
|
+
function storeForDir(dir: string) {
|
|
15
|
+
return new VectorStore({ dedupSim: 0.9, stateDir: dir });
|
|
16
|
+
}
|
|
17
|
+
function msg(role: EngineMessage["role"], text: string, toolName?: string): EngineMessage {
|
|
18
|
+
return toolName ? { role, text, toolName, input: text, output: text } : { role, text };
|
|
19
|
+
}
|
|
20
|
+
const SESS = "sess_resume";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Simulate the extension's `recentUserQuery`: the resume query is built from the
|
|
24
|
+
* newest user message in the (re-loaded) session. We model "newest user msg"
|
|
25
|
+
* directly rather than going through pi's session manager.
|
|
26
|
+
*/
|
|
27
|
+
function latestUserQuery(messages: EngineMessage[]): string {
|
|
28
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
29
|
+
if (messages[i].role === "user") return messages[i].text;
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("resume contract: compact in one process, recall in a fresh one from disk", () => {
|
|
35
|
+
const dir = join(baseTmp, `run-${counter++}`);
|
|
36
|
+
|
|
37
|
+
// --- Process 1: the original session compacts ---
|
|
38
|
+
const writer = storeForDir(dir);
|
|
39
|
+
const session: EngineMessage[] = [
|
|
40
|
+
msg("user", "investigated src/compact.ts and added a truncate helper"),
|
|
41
|
+
msg("assistant", "added truncate", "Edit"),
|
|
42
|
+
msg("user", "then wired it into the summary pipeline"),
|
|
43
|
+
msg("assistant", "wired it in", "Edit"),
|
|
44
|
+
];
|
|
45
|
+
const ran = compactSession(
|
|
46
|
+
{ sessionId: SESS, messages: session, keepFrom: session.length, timestamp: 1 },
|
|
47
|
+
writer,
|
|
48
|
+
);
|
|
49
|
+
assert.equal(ran.skipped, false);
|
|
50
|
+
assert.ok(ran.checkpointId, "a checkpoint was persisted");
|
|
51
|
+
|
|
52
|
+
// --- Process 2: pi restarts, session resumes from disk ---
|
|
53
|
+
const reader = storeForDir(dir); // brand new instance, same dir
|
|
54
|
+
const resumeQuery = latestUserQuery(session); // newest user msg
|
|
55
|
+
const r = recallAndInline(
|
|
56
|
+
{ sessionId: SESS, query: resumeQuery, limit: 3, source: "resume" },
|
|
57
|
+
reader,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
assert.equal(r.empty, false, "resume must re-surface the compacted context");
|
|
61
|
+
assert.equal(r.toInject.length, 1);
|
|
62
|
+
assert.equal(r.toInject[0].checkpoint.checkpointId, ran.checkpointId);
|
|
63
|
+
assert.ok(r.block.includes("Recalled context"), "block is model-visible system-prompt text");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("resume contract: nothing to recall for a brand-new session", () => {
|
|
67
|
+
const dir = join(baseTmp, `run-${counter++}`);
|
|
68
|
+
const fresh = storeForDir(dir);
|
|
69
|
+
const r = recallAndInline(
|
|
70
|
+
{ sessionId: "sess_never_seen", query: "anything at all", limit: 3, source: "resume" },
|
|
71
|
+
fresh,
|
|
72
|
+
);
|
|
73
|
+
assert.equal(r.empty, true);
|
|
74
|
+
assert.equal(r.block, "");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("resume contract: re-inject is deduped after the first recall", () => {
|
|
78
|
+
const dir = join(baseTmp, `run-${counter++}`);
|
|
79
|
+
const writer = storeForDir(dir);
|
|
80
|
+
const session: EngineMessage[] = [
|
|
81
|
+
msg("user", "built the trigram embedder for the vector store"),
|
|
82
|
+
msg("assistant", "built it", "Edit"),
|
|
83
|
+
];
|
|
84
|
+
compactSession({ sessionId: SESS, messages: session, keepFrom: 2, timestamp: 1 }, writer);
|
|
85
|
+
|
|
86
|
+
const reader = storeForDir(dir);
|
|
87
|
+
const q = latestUserQuery(session);
|
|
88
|
+
const first = recallAndInline({ sessionId: SESS, query: q, limit: 3, source: "resume" }, reader);
|
|
89
|
+
const second = recallAndInline({ sessionId: SESS, query: q, limit: 3, source: "resume" }, reader);
|
|
90
|
+
assert.equal(first.empty, false);
|
|
91
|
+
assert.equal(second.empty, true, "second resume does not re-inject (sentinel)");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("cleanup", () => {
|
|
95
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
96
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { VectorStore } from "./vectorStore.js";
|
|
7
|
+
import { compactSession } from "./engine.js";
|
|
8
|
+
import { recallAndInline, formatRecallBlock } from "./recall.js";
|
|
9
|
+
import type { EngineMessage } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-recall-"));
|
|
12
|
+
let counter = 0;
|
|
13
|
+
function store() {
|
|
14
|
+
return new VectorStore({ dedupSim: 0.9, stateDir: join(baseTmp, `run-${counter++}`) });
|
|
15
|
+
}
|
|
16
|
+
function msg(role: EngineMessage["role"], text: string, toolName?: string): EngineMessage {
|
|
17
|
+
return toolName ? { role, text, toolName, input: text, output: text } : { role, text };
|
|
18
|
+
}
|
|
19
|
+
const SESS = "sess_recall";
|
|
20
|
+
|
|
21
|
+
test("recallAndInline injects new hits and marks them injected", () => {
|
|
22
|
+
const s = store();
|
|
23
|
+
compactSession({ sessionId: SESS, messages: [msg("user", "investigated src/vectorStore.ts embedding"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
|
|
24
|
+
compactSession({ sessionId: SESS, messages: [msg("user", "fixed the dedupe race in store.ts"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
|
|
25
|
+
|
|
26
|
+
const r1 = recallAndInline({ sessionId: SESS, query: "vectorStore embedding", limit: 1, source: "command" }, s as any);
|
|
27
|
+
assert.equal(r1.empty, false);
|
|
28
|
+
assert.equal(r1.toInject.length, 1);
|
|
29
|
+
assert.ok(r1.block.includes("Recalled context"));
|
|
30
|
+
|
|
31
|
+
// Second call with the same query must NOT re-inject (shared dedup).
|
|
32
|
+
const r2 = recallAndInline({ sessionId: SESS, query: "vectorStore embedding", limit: 1, source: "command" }, s as any);
|
|
33
|
+
assert.equal(r2.empty, true);
|
|
34
|
+
assert.equal(r2.toInject.length, 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("recallAndInline skipInjected=false re-returns hits", () => {
|
|
38
|
+
const s = store();
|
|
39
|
+
compactSession({ sessionId: SESS, messages: [msg("user", "configured the fast gate threshold"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
|
|
40
|
+
const r1 = recallAndInline({ sessionId: SESS, query: "fast gate threshold", limit: 5, source: "resume" }, s as any);
|
|
41
|
+
const r2 = recallAndInline({ sessionId: SESS, query: "fast gate threshold", limit: 5, source: "resume", skipInjected: false }, s as any);
|
|
42
|
+
assert.equal(r1.toInject.length, 1);
|
|
43
|
+
assert.equal(r2.toInject.length, 1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("formatRecallBlock is empty for no hits", () => {
|
|
47
|
+
assert.equal(formatRecallBlock([]), "");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("recallAndInline empty when store has nothing for query", () => {
|
|
51
|
+
const s = store();
|
|
52
|
+
const r = recallAndInline({ sessionId: SESS, query: "no such topic exists here", limit: 5, source: "command" }, s as any);
|
|
53
|
+
assert.equal(r.empty, true);
|
|
54
|
+
assert.equal(r.block, "");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("cleanup", () => {
|
|
58
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
59
|
+
});
|
package/src/recall.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall.ts — Layer 5 (RECALL / INLINE): the unified injection path.
|
|
3
|
+
*
|
|
4
|
+
* ONE vector store, THREE entry points, ONE dedup engine. Every way context
|
|
5
|
+
* gets re-injected into the window (auto-inline on resume, on-demand
|
|
6
|
+
* /recall-context, and the dedup sentinel) goes through `recallAndInline`.
|
|
7
|
+
* It always does: search -> dedupe -> inject. The only thing that differs per
|
|
8
|
+
* entry point is *what triggers it* and *what query it uses*.
|
|
9
|
+
*
|
|
10
|
+
* Injection respects PREVENT-PI-003: pi has no `system` message role, so we
|
|
11
|
+
* prepend our recall block to the system prompt via the `before_agent_start`
|
|
12
|
+
* hook's `systemPrompt` result (the extension wires that). This module is
|
|
13
|
+
* pi-agnostic: it returns an injectable text block and records injections; the
|
|
14
|
+
* extension decides where it lands.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { recall as searchRecall } from "./engine.js";
|
|
18
|
+
import type { SearchHit, VectorStore } from "./vectorStore.js";
|
|
19
|
+
|
|
20
|
+
export type RecallSource = "resume" | "command" | "sentinel";
|
|
21
|
+
|
|
22
|
+
export interface RecallInjectOptions {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
query: string;
|
|
25
|
+
limit?: number;
|
|
26
|
+
source: RecallSource;
|
|
27
|
+
/** Skip checkpoints already injected this session (recall dedup). */
|
|
28
|
+
skipInjected?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RecallInjectResult {
|
|
32
|
+
/** Blocks that are ready to inline (already deduped against the window). */
|
|
33
|
+
toInject: SearchHit[];
|
|
34
|
+
/** Human-readable lines for status/notify reporting. */
|
|
35
|
+
report: string[];
|
|
36
|
+
/** The concatenated, model-visible recall block (empty when nothing new). */
|
|
37
|
+
block: string;
|
|
38
|
+
/** True when nothing new was inlined. */
|
|
39
|
+
empty: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Wrap a recall block so the model reads it as restored compacted context. */
|
|
43
|
+
export function formatRecallBlock(hits: SearchHit[]): string {
|
|
44
|
+
if (hits.length === 0) return "";
|
|
45
|
+
const parts = hits.map((h, i) => {
|
|
46
|
+
const score = (h.score * 100).toFixed(0);
|
|
47
|
+
return (
|
|
48
|
+
`### Recalled context [${i + 1}] (relevance ${score}%)\n` +
|
|
49
|
+
`${h.checkpoint.summary.trim()}\n` +
|
|
50
|
+
(h.checkpoint.filesModified.length
|
|
51
|
+
? `Key files: ${h.checkpoint.filesModified.join(", ")}.\n`
|
|
52
|
+
: "")
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
return (
|
|
56
|
+
"The following compacted context was recalled from earlier in this session " +
|
|
57
|
+
"and is relevant to the current request. Treat it as background you already know:\n\n" +
|
|
58
|
+
parts.join("\n")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run the unified recall+dudupe+prepare-inject pipeline. Does NOT touch pi;
|
|
64
|
+
* it records injections via `markInjected` so the next call dedupes. The
|
|
65
|
+
* `store` is passed by the extension (defaults to the engine's default store).
|
|
66
|
+
*/
|
|
67
|
+
export function recallAndInline(
|
|
68
|
+
opts: RecallInjectOptions,
|
|
69
|
+
store: Pick<VectorStore, "search" | "wasInjected" | "markInjected">,
|
|
70
|
+
): RecallInjectResult {
|
|
71
|
+
const limit = opts.limit ?? 3;
|
|
72
|
+
const skip = opts.skipInjected ?? true;
|
|
73
|
+
|
|
74
|
+
const { hits } = searchRecall(
|
|
75
|
+
{ sessionId: opts.sessionId, query: opts.query, limit, skipInjected: false },
|
|
76
|
+
store as VectorStore,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Shared dedup: drop checkpoints already injected this session, then mark the
|
|
80
|
+
// survivors so repeated triggers are free. (Cosine near-dup collapse already
|
|
81
|
+
// happened inside store.search.)
|
|
82
|
+
const toInject: SearchHit[] = [];
|
|
83
|
+
for (const h of hits) {
|
|
84
|
+
if (skip && store.wasInjected(opts.sessionId, h.checkpoint.checkpointId)) continue;
|
|
85
|
+
toInject.push(h);
|
|
86
|
+
store.markInjected(opts.sessionId, h.checkpoint.checkpointId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const block = formatRecallBlock(toInject);
|
|
90
|
+
const report = toInject.map(
|
|
91
|
+
(h) => ` • ${h.checkpoint.checkpointId} (${h.checkpoint.summary.slice(0, 60).replace(/\n/g, " ")}…)`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
toInject,
|
|
96
|
+
report,
|
|
97
|
+
block,
|
|
98
|
+
empty: toInject.length === 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sprint14.test.ts — Sprint 14 full-pipeline wiring (flags, backfill, monitoring, canary).
|
|
3
|
+
* Hermetic: isolated state dirs, no network, no remote.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { VectorStore } from "./vectorStore.js";
|
|
12
|
+
import { defaultEmbedder } from "./embedder.js";
|
|
13
|
+
import { loadDedupConfig, type DedupConfigShape } from "./config/dedup.js";
|
|
14
|
+
import {
|
|
15
|
+
loadMetrics,
|
|
16
|
+
saveMetrics,
|
|
17
|
+
recordDecision,
|
|
18
|
+
evaluateAlerts,
|
|
19
|
+
fpRate,
|
|
20
|
+
p95,
|
|
21
|
+
type DedupMetrics,
|
|
22
|
+
} from "./monitoring.js";
|
|
23
|
+
import { backfillPhase, backfillRaptor } from "./store/backfill.js";
|
|
24
|
+
import { listCheckpoints, closeStore } from "./store/sqlite.js";
|
|
25
|
+
import { CanaryController, runCanary } from "./canary.js";
|
|
26
|
+
|
|
27
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-s14-"));
|
|
28
|
+
|
|
29
|
+
function cfg(over: Partial<DedupConfigShape> = {}): DedupConfigShape {
|
|
30
|
+
return { ...loadDedupConfig(), ...over };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function store(over: Partial<DedupConfigShape> = {}, eventsPath?: string): VectorStore {
|
|
34
|
+
const dir = join(baseTmp, `run-${Math.floor(performance.now() * 1000)}-${Math.random()}`);
|
|
35
|
+
return new VectorStore({ stateDir: dir, config: cfg(over), eventsPath });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- 1. Flag matrix: 16 combos don't crash add()/search() -------------------
|
|
39
|
+
|
|
40
|
+
test("flag matrix: all 16 L0/L1/L2/RAPTOR enable combos are safe", () => {
|
|
41
|
+
const flags = [false, true];
|
|
42
|
+
let combos = 0;
|
|
43
|
+
for (const l0 of flags)
|
|
44
|
+
for (const l1 of flags)
|
|
45
|
+
for (const l2 of flags)
|
|
46
|
+
for (const raptor of flags) {
|
|
47
|
+
combos++;
|
|
48
|
+
const s = store({ L0_ENABLED: l0, L1_ENABLED: l1, L2_ENABLED: l2, RAPTOR_ENABLED: raptor });
|
|
49
|
+
s.add({ sessionId: "s", summary: "x", regionText: `region A for combo ${combos} about the cache`, timestamp: 1 });
|
|
50
|
+
const r2 = s.add({ sessionId: "s", summary: "x", regionText: `region B for combo ${combos} about the parser`, timestamp: 2 });
|
|
51
|
+
// search must not throw under any combination
|
|
52
|
+
const hits = s.search("s", "cache", 3);
|
|
53
|
+
assert.ok(Array.isArray(hits));
|
|
54
|
+
// With all tiers off, the second add is always "new" (no collapse).
|
|
55
|
+
if (!l0 && !l1 && !l2) assert.equal(r2.deduped, false);
|
|
56
|
+
}
|
|
57
|
+
assert.equal(combos, 16);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// --- 2. MARK_ONLY_L1 records but doesn't collapse ---------------------------
|
|
61
|
+
|
|
62
|
+
test("MARK_ONLY_L1: L1 match is recorded but not collapsed (new checkpoint)", () => {
|
|
63
|
+
// Disable L2 so we isolate L1 behavior (L2 cosine would otherwise catch the near-dup).
|
|
64
|
+
const s = store({ L1_ENABLED: true, MARK_ONLY_L1: true, L2_ENABLED: false });
|
|
65
|
+
const a = s.add({ sessionId: "s", summary: "x", regionText: "the parser optimized the hot loop", timestamp: 1 });
|
|
66
|
+
const b = s.add({ sessionId: "s", summary: "x", regionText: "the parser optimized the hot loops", timestamp: 2 });
|
|
67
|
+
assert.equal(a.deduped, false);
|
|
68
|
+
// MARK_ONLY → b is NOT collapsed into a; both stored as active.
|
|
69
|
+
assert.equal(b.deduped, false);
|
|
70
|
+
const all = listCheckpoints("s", (s as any).stateDir);
|
|
71
|
+
assert.equal(all.length, 2);
|
|
72
|
+
assert.ok(all.every((c) => c.dedupStatus === "active"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("MARK_ONLY_L1 off: L1 match IS collapsed", () => {
|
|
76
|
+
const s = store({ L1_ENABLED: true, MARK_ONLY_L1: false });
|
|
77
|
+
s.add({ sessionId: "s", summary: "x", regionText: "the parser optimized the hot loop", timestamp: 1 });
|
|
78
|
+
const b = s.add({ sessionId: "s", summary: "x", regionText: "the parser optimized the hot loops", timestamp: 2 });
|
|
79
|
+
assert.equal(b.deduped, true);
|
|
80
|
+
assert.equal(b.reason, "l1MinHash");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// --- 3. Backfill resumes after interrupt -------------------------------------
|
|
84
|
+
|
|
85
|
+
test("backfill L1 resumes after simulated interrupt", () => {
|
|
86
|
+
const dir = join(baseTmp, `bf-${Math.floor(performance.now())}`);
|
|
87
|
+
// Seed 12 checkpoints, 2 per batch of 5.
|
|
88
|
+
const s = new VectorStore({ stateDir: dir, config: cfg() });
|
|
89
|
+
const texts = [
|
|
90
|
+
"The walrus drifted past the lighthouse while the baker kneaded sourdough at dawn",
|
|
91
|
+
"Quantum entanglement linked the two photons across the lab in a cryogenic chamber",
|
|
92
|
+
"A medieval scribe copied the gospel by candlelight atop a windswept cliff",
|
|
93
|
+
"The rover sampled basalt from the crater and transmitted spectra to mission control",
|
|
94
|
+
"Jazz musicians improvised a syncopated triangle rhythm beneath the streetlamp",
|
|
95
|
+
"The glacier calved a towering iceberg into the fjord with a thunderous crack",
|
|
96
|
+
"A botanist cataloged the orchid species thriving in the cloud forest canopy",
|
|
97
|
+
"The blacksmith forged a horseshoe while sparks danced across the anvil",
|
|
98
|
+
"Astronomers imaged a distant nebula glowing with newborn stellar furnaces",
|
|
99
|
+
"The ferry crossed the strait as gulls wheeled above the churning wake",
|
|
100
|
+
"A weaver threaded crimson silk through the loom in the mountain village",
|
|
101
|
+
"The surgeon sutured the incision with steady hands under the theatre lights",
|
|
102
|
+
];
|
|
103
|
+
for (let i = 0; i < 12; i++) {
|
|
104
|
+
s.add({ sessionId: "sess_bf", summary: `n${i}`, regionText: texts[i], timestamp: i });
|
|
105
|
+
}
|
|
106
|
+
// Interrupt after batch 1 (5 rows).
|
|
107
|
+
const r1 = backfillPhase("L1", "sess_bf", dir, { batchSize: 5, interruptAfterBatches: 1 });
|
|
108
|
+
assert.equal(r1.interrupted, true);
|
|
109
|
+
assert.equal(r1.processed, 5);
|
|
110
|
+
assert.ok(r1.cursor === "chkpt_005" || r1.cursor === "chkpt_05");
|
|
111
|
+
// Resume: should continue from the cursor → process the remaining 7.
|
|
112
|
+
const r2 = backfillPhase("L1", "sess_bf", dir, { batchSize: 5 });
|
|
113
|
+
assert.equal(r2.interrupted, false);
|
|
114
|
+
assert.equal(r2.processed, 12); // total across both runs (cursor-based resume)
|
|
115
|
+
closeStore(dir);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// --- 4. Alert fires on injected FP spike -------------------------------------
|
|
119
|
+
|
|
120
|
+
test("alert: FP spike breaches threshold → MARK_ONLY flagged + warning", () => {
|
|
121
|
+
const config = cfg();
|
|
122
|
+
const m: DedupMetrics = loadMetrics("/dev/null");
|
|
123
|
+
// Inject 100 L1 decisions, 20 false positives → 20% > FP_RATE_L1L2 (5%).
|
|
124
|
+
for (let i = 0; i < 100; i++) {
|
|
125
|
+
recordDecision(m, "L1", i < 20 ? "deduped" : "new", 5, i < 20);
|
|
126
|
+
}
|
|
127
|
+
const res = evaluateAlerts(m, config);
|
|
128
|
+
assert.ok(res.breached.includes("L1"));
|
|
129
|
+
assert.ok(res.warnings.some((w) => w.includes("DEDUP FP BREACH tier=L1")));
|
|
130
|
+
assert.ok(fpRate(m, "L1") > config.FP_RATE_L1L2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("alert: clean run does NOT breach", () => {
|
|
134
|
+
const config = cfg();
|
|
135
|
+
const m = loadMetrics("/dev/null");
|
|
136
|
+
for (let i = 0; i < 100; i++) recordDecision(m, "L1", "new", 5, false);
|
|
137
|
+
const res = evaluateAlerts(m, config);
|
|
138
|
+
assert.equal(res.breached.length, 0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// --- 5. Canary auto-disables a tier whose p95 exceeds budget ---------------
|
|
142
|
+
|
|
143
|
+
test("canary: auto-disables a tier whose p95 exceeds budget", () => {
|
|
144
|
+
// Feed where L2 always has high latency (breaches P95_BUDGET_MS).
|
|
145
|
+
const feed = (_step: number, c: DedupConfigShape): DedupMetrics => {
|
|
146
|
+
const m = loadMetrics("/dev/null");
|
|
147
|
+
if (c.L0_ENABLED) recordDecision(m, "L0", "new", 1, false);
|
|
148
|
+
if (c.L1_ENABLED) recordDecision(m, "L1", "new", 1, false);
|
|
149
|
+
if (c.L2_ENABLED) recordDecision(m, "L2", "new", 500, false); // > 100ms budget
|
|
150
|
+
if (c.RAPTOR_ENABLED) recordDecision(m, "RAPTOR", "new", 1, false);
|
|
151
|
+
return m;
|
|
152
|
+
};
|
|
153
|
+
const { controller, disabled } = runCanary(feed, cfg({ P95_BUDGET_MS: 100 }));
|
|
154
|
+
assert.ok(disabled.includes("L2"), `expected L2 auto-disabled, got ${JSON.stringify(disabled)}`);
|
|
155
|
+
assert.equal(controller.config.L2_ENABLED, false);
|
|
156
|
+
// Lower tiers should still be enabled.
|
|
157
|
+
assert.equal(controller.config.L0_ENABLED, true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("canary: sequential enablement order L0→L1→L2→RAPTOR", () => {
|
|
161
|
+
const c = new CanaryController(cfg());
|
|
162
|
+
assert.deepEqual([...c.getState().enabled], ["L0"]);
|
|
163
|
+
const t1 = c.stepForward();
|
|
164
|
+
assert.equal(t1, "L1");
|
|
165
|
+
assert.equal(c.stepForward(), "L2");
|
|
166
|
+
assert.equal(c.stepForward(), "RAPTOR");
|
|
167
|
+
assert.equal(c.stepForward(), null); // all enabled
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// --- 6. Monitoring: structured decision events written ----------------------
|
|
171
|
+
|
|
172
|
+
test("monitoring: add() writes structured decision events to events.log", () => {
|
|
173
|
+
const dir = join(baseTmp, `mon-${Math.floor(performance.now())}`);
|
|
174
|
+
const eventsPath = join(dir, "events.log");
|
|
175
|
+
const s = new VectorStore({
|
|
176
|
+
stateDir: dir,
|
|
177
|
+
config: cfg(),
|
|
178
|
+
eventsPath,
|
|
179
|
+
});
|
|
180
|
+
s.add({ sessionId: "s", summary: "x", regionText: "unique region alpha one", timestamp: 1 });
|
|
181
|
+
s.add({ sessionId: "s", summary: "x", regionText: "unique region alpha one", timestamp: 2 }); // L0 content dup
|
|
182
|
+
assert.ok(existsSync(eventsPath));
|
|
183
|
+
const lines = readFileSync(eventsPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
184
|
+
assert.ok(lines.length >= 2);
|
|
185
|
+
const ev = JSON.parse(lines[0]);
|
|
186
|
+
assert.ok(["L0"].includes(ev.tier));
|
|
187
|
+
assert.ok(["new", "deduped", "mark_only"].includes(ev.result));
|
|
188
|
+
closeStore(dir);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- 7. RAPTOR backfill builds + persists a tree ---------------------------
|
|
192
|
+
|
|
193
|
+
test("backfill RAPTOR builds + persists a tree for a session", () => {
|
|
194
|
+
const dir = join(baseTmp, `raptorbf-${Math.floor(performance.now())}`);
|
|
195
|
+
const s = new VectorStore({ stateDir: dir, config: cfg() });
|
|
196
|
+
const texts = [
|
|
197
|
+
"The whale breached beside the research vessel near the polar ice shelf",
|
|
198
|
+
"A potter shaped the clay vessel on the spinning wheel at the riverside studio",
|
|
199
|
+
"The comet streaked across the pre dawn sky witnessed by the hilltop observatory",
|
|
200
|
+
"Lumberjacks felled the ancient cedar while the river carried the logs downstream",
|
|
201
|
+
"The chemist titrated the solution until the indicator turned faint violet",
|
|
202
|
+
"A flock of cranes migrated northward over the thawing wetland at first light",
|
|
203
|
+
"The locksmith picked the stubborn tumbler and opened the oak cabinet",
|
|
204
|
+
"Geologists hammered the schist sample from the canyon wall into the satchel",
|
|
205
|
+
"The chocolatier tempered the couverture until it snapped with a clean gloss",
|
|
206
|
+
"A fisher cast the line into the mist where the trout rose to the fly",
|
|
207
|
+
"The archivist unsealed the parchment scroll recovered from the coastal ruin",
|
|
208
|
+
"Beekeepers harvested the golden comb while the orchard blossoms drifted down",
|
|
209
|
+
"The pilot navigated the canyon winds using only the instrument panel glow",
|
|
210
|
+
"A tailor stitched the velvet cuff with silk thread by the window",
|
|
211
|
+
"The miner extracted the quartz crystal from the vein deep in the shaft",
|
|
212
|
+
"Cartographers plotted the uncharted island onto the worn leather map",
|
|
213
|
+
"The gardener pruned the rosebush and tied the canes to the cedar trellis",
|
|
214
|
+
"A violinist tuned the gut strings until the chamber rang pure and bright",
|
|
215
|
+
"The diver surfaced with the amphora lifted from the sunken galleon",
|
|
216
|
+
"Shepherds guided the flock across the high pasture toward the stone bothy",
|
|
217
|
+
];
|
|
218
|
+
for (let i = 0; i < 20; i++) {
|
|
219
|
+
s.add({ sessionId: "sess_rb", summary: `n${i}`, regionText: texts[i], timestamp: i });
|
|
220
|
+
}
|
|
221
|
+
const res = backfillRaptor("sess_rb", dir, defaultEmbedder());
|
|
222
|
+
assert.ok(res.processed > 0);
|
|
223
|
+
// Sanity: source checkpoints remain intact (backfill is additive).
|
|
224
|
+
const nodes = listCheckpoints("sess_rb", dir).length;
|
|
225
|
+
assert.ok(nodes >= 20);
|
|
226
|
+
closeStore(dir);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// --- 8. dashboard.json metrics round-trip -----------------------------------
|
|
230
|
+
|
|
231
|
+
test("metrics: dashboard.json persists + p95 computed", () => {
|
|
232
|
+
const path = join(baseTmp, `dash-${Math.floor(performance.now())}.json`);
|
|
233
|
+
const m = loadMetrics(path);
|
|
234
|
+
for (let i = 0; i < 10; i++) recordDecision(m, "L2", "new", i * 10, false);
|
|
235
|
+
saveMetrics(path, m);
|
|
236
|
+
const reloaded = loadMetrics(path);
|
|
237
|
+
assert.equal(reloaded.decisions.L2, 10);
|
|
238
|
+
assert.equal(p95(reloaded.latency.L2), 90); // 95th pct of [0,10,..,90]
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// --- cleanup ----------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
test("Sprint 14 cleanup", () => {
|
|
244
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
245
|
+
});
|