pi-mega-compact 0.21.1 → 0.21.3
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/config.js +13 -2
- package/dist/dedup/l1-minhash.js +5 -4
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/mega-compact-child.js +126 -0
- package/dist/extensions/mega-compact.js +10 -0
- package/dist/extensions/mega-config.js +11 -3
- package/dist/src/bridge/factory.js +181 -0
- package/dist/src/bridge/types.js +10 -0
- package/dist/src/bridge.js +1 -0
- package/dist/src/config.js +13 -2
- package/dist/src/dedup/l1-minhash.js +5 -4
- package/dist/src/recall/validator.js +8 -1
- package/dist/src/recall/vote.js +34 -9
- package/dist/src/store/sqlite/fts5-search.js +6 -1
- package/dist/src/vector-read.js +6 -2
- package/dist/src/vectorStore/add.js +5 -1
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/mega-compact-child.ts +129 -0
- package/extensions/mega-compact.ts +10 -0
- package/extensions/mega-config-types.ts +5 -0
- package/extensions/mega-config.ts +11 -3
- package/package.json +1 -1
- package/src/bridge/factory.ts +231 -0
- package/src/bridge/types.ts +138 -0
- package/src/bridge.ts +24 -0
- package/src/config.ts +12 -3
- package/src/dedup/l1-minhash.ts +5 -4
- package/src/recall/validator.ts +8 -1
- package/src/recall/vote.ts +29 -8
- package/src/store/sqlite/fts5-search.ts +7 -1
- package/src/vector-read.ts +9 -2
- package/src/vectorStore/add.ts +7 -1
package/dist/config.js
CHANGED
|
@@ -116,8 +116,19 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
116
116
|
// default (0.12) keeps recall permissive within a repo while still rejecting
|
|
117
117
|
// effectively-unrelated hits. Call-time read so tests can set the env per-test.
|
|
118
118
|
// ---------------------------------------------------------------------------
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Same-repo recall cosine floor: top winner must be >= this to be injected.
|
|
121
|
+
*
|
|
122
|
+
* E1 follow-up (PR #18 review): NaN-safe + clamped to [0,1]. A typo'd env var
|
|
123
|
+
* yielded NaN before; `cosine < NaN` is false, which disabled gate 1 entirely
|
|
124
|
+
* (every candidate passed). Non-finite falls back to 0.12; out-of-range clamps.
|
|
125
|
+
*/
|
|
126
|
+
export const RECALL_MIN_COSINE = () => {
|
|
127
|
+
const n = Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12");
|
|
128
|
+
if (!Number.isFinite(n))
|
|
129
|
+
return 0.12;
|
|
130
|
+
return Math.min(1, Math.max(0, n));
|
|
131
|
+
};
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
122
133
|
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
|
123
134
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
package/dist/dedup/l1-minhash.js
CHANGED
|
@@ -19,6 +19,7 @@ export const SHINGLE_SIZE = 5; // char 5-grams
|
|
|
19
19
|
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
20
20
|
const SEED = 0xdeadbeef;
|
|
21
21
|
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
22
|
+
const PBigInt = 2147483647n; // BigInt twin for overflow-safe modular reduction
|
|
22
23
|
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
23
24
|
function coeffA(i) {
|
|
24
25
|
return (SEED + i * 2 + 1) % P;
|
|
@@ -67,10 +68,10 @@ export function minhashSignature(text) {
|
|
|
67
68
|
const b = coeffB(i);
|
|
68
69
|
let min = P;
|
|
69
70
|
for (const x of grams) {
|
|
70
|
-
// (a*x + b) mod p
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const h = (
|
|
71
|
+
// (a*x + b) mod p. a, x < 2^31 so a*x < 2^62 — EXCEEDS 2^53. The naive
|
|
72
|
+
// (a*(x%P))%P loses precision (verified: a=x=p-1 → lossy 2147483644 vs
|
|
73
|
+
// exact 1). BigInt is correct + cheap (~5ms per signature).
|
|
74
|
+
const h = Number((BigInt(a) * BigInt(x % P) + BigInt(b)) % PBigInt);
|
|
74
75
|
if (h < min)
|
|
75
76
|
min = h;
|
|
76
77
|
}
|
|
@@ -138,6 +138,7 @@ export const SETTINGS = [
|
|
|
138
138
|
name: "Three-Way Failback",
|
|
139
139
|
settings: [
|
|
140
140
|
boolDirect("MEGACOMPACT_THREE_WAY_FAILBACK", "Three-Way Failback", "Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
141
|
+
boolDirect("MEGACOMPACT_ITHACUS_BRIDGE", "ithacus Bridge", "Mega↔ithacus bridge: gate the child extension + bridge usage that tie this extension to ithacus's durable compaction. Default ON; OFF (=0/`=false`) is byte-identical to pre-bridge behavior — the bridge is only consulted when this is ON. Positive sprint flag. Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
141
142
|
boolDirect("MEGACOMPACT_RECALL_TAIL_INJECT", "Recall Tail Inject", "Compose the staged recall block as a trailing user message on the context event (tail inject) instead of the legacy system-prompt prepend. Tail mode keeps the cache prefix stable and is the mode InjectionConfirm verifies against ContextEvent.messages; OFF falls back to the legacy prepend path (verified by string-contains). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
142
143
|
],
|
|
143
144
|
},
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-compact-child.ts — minimal extension loaded ONLY into dispatched child
|
|
3
|
+
* pi subprocesses (spawned by ithacus with a second `-e` flag).
|
|
4
|
+
*
|
|
5
|
+
* Design: a child is a FRESH pi process started with `--no-extensions -e <this
|
|
6
|
+
* file>` (see ithacus-spawn.ts). It does NOT receive the parent's MegaConfig and
|
|
7
|
+
* is a separate process, so it reads its two control env vars directly and owns
|
|
8
|
+
* its own bridge. It gives children recall-at-start + compaction-on-shutdown via
|
|
9
|
+
* the mega-compact bridge, with NO tools and NO console output, so it never
|
|
10
|
+
* pollutes the child's `--mode json` JSONL stdout that ithacus-spawn parses.
|
|
11
|
+
*
|
|
12
|
+
* Per the teammate brief this mirrors ithacus-child-mailbox.ts (default export,
|
|
13
|
+
* no console, dispose on session_shutdown) but registers ZERO tools — registering
|
|
14
|
+
* any tool risks a pi duplicate-tool-name hard-fail and children need none.
|
|
15
|
+
*
|
|
16
|
+
* PREVENT-PI-004: no network. The bridge is a same-repo relative import over a
|
|
17
|
+
* local sqlite store; the only I/O is the read-only `git rev-parse` inside
|
|
18
|
+
* repoStateDir. Nothing to flag.
|
|
19
|
+
*/
|
|
20
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { createMegaBridge } from "../src/bridge.js";
|
|
22
|
+
import { repoStateDir } from "./mega-config.js";
|
|
23
|
+
import { STATE_DIR_DEFAULT } from "../src/config.js";
|
|
24
|
+
/** Default-ON env bool: only `=false`/`=0` disables (matches mega-config envBool). */
|
|
25
|
+
function envBool(name, fallback) {
|
|
26
|
+
const v = process.env[name];
|
|
27
|
+
if (v == null || v === "")
|
|
28
|
+
return fallback;
|
|
29
|
+
return v === "true" || v === "1";
|
|
30
|
+
}
|
|
31
|
+
/** Extract a recall query from a single AgentMessage (string or content blocks). */
|
|
32
|
+
function messageToText(m) {
|
|
33
|
+
const c = m.content;
|
|
34
|
+
if (typeof c === "string")
|
|
35
|
+
return c;
|
|
36
|
+
if (Array.isArray(c))
|
|
37
|
+
return c.map((b) => b.text ?? "").join(" ");
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
/** Convert a session's AgentMessages into the bridge's lightweight shape. */
|
|
41
|
+
function toBridgeMessages(ctx) {
|
|
42
|
+
const out = [];
|
|
43
|
+
try {
|
|
44
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
45
|
+
for (const m of sessionEntryToContextMessages(entry)) {
|
|
46
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
47
|
+
out.push({ role: m.role, text: messageToText(m) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* non-fatal: a child without a session manager yields no messages */
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export default function (pi) {
|
|
58
|
+
// Flag read at LOAD time: a flag-OFF child registers nothing and a flag-ON
|
|
59
|
+
// child that never fires a hook pays zero cost (bridge is built lazily).
|
|
60
|
+
if (!envBool("MEGACOMPACT_ITHACUS_BRIDGE", true))
|
|
61
|
+
return;
|
|
62
|
+
let bridge;
|
|
63
|
+
// Build the bridge lazily on first hook fire so cost is opt-in by usage.
|
|
64
|
+
const getBridge = () => {
|
|
65
|
+
if (!bridge) {
|
|
66
|
+
bridge = createMegaBridge({
|
|
67
|
+
stateDir: repoStateDir(process.cwd(), STATE_DIR_DEFAULT),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return bridge;
|
|
71
|
+
};
|
|
72
|
+
// S52-style recall injection: prepend staged checkpoints + durable memories
|
|
73
|
+
// to the system prompt, mirroring the main entry's before_agent_start path.
|
|
74
|
+
// 4th-layer stability guard: an unset/empty sessionId makes recall silently
|
|
75
|
+
// useless (the openclaw Date.now() gotcha), so skip outright.
|
|
76
|
+
pi.on("before_agent_start", async (event) => {
|
|
77
|
+
try {
|
|
78
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
79
|
+
if (!sessionId || sessionId === "")
|
|
80
|
+
return undefined;
|
|
81
|
+
// Prefer the event's raw prompt; fall back to a generic query.
|
|
82
|
+
const query = event.prompt && event.prompt.trim() ? event.prompt.trim() : "";
|
|
83
|
+
if (query === "")
|
|
84
|
+
return undefined;
|
|
85
|
+
const b = getBridge();
|
|
86
|
+
const cp = b.recallCheckpoints({ sessionId, query, limit: 3 });
|
|
87
|
+
const mem = await b.recallMemories({ query, limit: 5 });
|
|
88
|
+
const blocks = [];
|
|
89
|
+
if (!cp.empty && cp.block)
|
|
90
|
+
blocks.push(cp.block);
|
|
91
|
+
if (!mem.empty && mem.block)
|
|
92
|
+
blocks.push(mem.block);
|
|
93
|
+
if (blocks.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${blocks.join("\n\n")}` };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// layer b: non-fatal — never break the agent loop. No injection.
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Compaction on shutdown: persist the session's messages as a checkpoint.
|
|
103
|
+
// Best-effort; non-fatal. Releases the sqlite handle via close().
|
|
104
|
+
// The bridge is constructed lazily here too: a child that only compacts (no
|
|
105
|
+
// recall fired) still persists its session. Best-effort; non-fatal.
|
|
106
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
107
|
+
try {
|
|
108
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
109
|
+
if (!sessionId || sessionId === "")
|
|
110
|
+
return;
|
|
111
|
+
const messages = toBridgeMessages(ctx);
|
|
112
|
+
if (messages.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
await getBridge().compact({ sessionId, messages });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* non-fatal */
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (bridge) {
|
|
121
|
+
bridge.close();
|
|
122
|
+
bridge = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -83,6 +83,16 @@ export default function (pi) {
|
|
|
83
83
|
console.warn("[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3");
|
|
84
84
|
config.poisonedContextRepeatThreshold = 3;
|
|
85
85
|
}
|
|
86
|
+
// E1: validate similarity thresholds — NaN or out of range silently disables
|
|
87
|
+
// recall dedup (anything >= NaN is false). envFlag guards NaN; clamp the rest.
|
|
88
|
+
if (!(config.dedupSim > 0 && config.dedupSim <= 1)) {
|
|
89
|
+
console.warn("[mega-compact] MEGACOMPACT_DEDUP_SIM must be in (0,1]; using default 0.9");
|
|
90
|
+
config.dedupSim = 0.9;
|
|
91
|
+
}
|
|
92
|
+
if (!(config.crossRepoCosine >= 0 && config.crossRepoCosine <= 1)) {
|
|
93
|
+
console.warn("[mega-compact] MEGACOMPACT_CROSSREPO_COSINE must be in [0,1]; using default 0.9");
|
|
94
|
+
config.crossRepoCosine = 0.9;
|
|
95
|
+
}
|
|
86
96
|
const runtime = new MegaRuntime(config);
|
|
87
97
|
registerEventHandlers(pi, runtime, config);
|
|
88
98
|
registerCommands(pi, runtime, config);
|
|
@@ -171,7 +171,7 @@ export function loadConfig() {
|
|
|
171
171
|
advisoryChannel: envBool("MEGACOMPACT_ADVISORY_CHANNEL", true),
|
|
172
172
|
autoPctTrigger,
|
|
173
173
|
autoInlineK: envFlag("MEGACOMPACT_AUTO_INLINE_K", 3),
|
|
174
|
-
dedupSim:
|
|
174
|
+
dedupSim: envFlag("MEGACOMPACT_DEDUP_SIM", 0.9),
|
|
175
175
|
raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
176
176
|
legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
|
|
177
177
|
dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
|
|
@@ -180,13 +180,16 @@ export function loadConfig() {
|
|
|
180
180
|
turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
181
181
|
autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
182
182
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
183
|
-
crossRepoCosine:
|
|
183
|
+
crossRepoCosine: envFlag("MEGACOMPACT_CROSSREPO_COSINE", 0.9),
|
|
184
184
|
// 3WF-3: SAME-repo recall cosine floor applied by the 3-source validator to
|
|
185
185
|
// the top winner. SEPARATE from crossRepoCosine (S17, default 0.90, stricter
|
|
186
186
|
// and cross-repo only). This same-repo floor is permissive by default (0.12)
|
|
187
187
|
// so recall still surfaces loosely-relevant within-repo context while
|
|
188
188
|
// rejecting effectively-unrelated hits. Mirrors src/config.ts RECALL_MIN_COSINE.
|
|
189
|
-
|
|
189
|
+
// E1 follow-up (PR #18 review): envFlag (Number.isFinite-guarded) like the
|
|
190
|
+
// dedupSim/crossRepoCosine fix in PR #18 — a typo'd env var must fall back
|
|
191
|
+
// to 0.12, not yield NaN.
|
|
192
|
+
recallMinCosine: envFlag("MEGACOMPACT_RECALL_MIN_COSINE", 0.12),
|
|
190
193
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
191
194
|
memoryReviewInterval: envFlag("MEGACOMPACT_MEMORY_REVIEW_INTERVAL", 10),
|
|
192
195
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
|
@@ -195,6 +198,11 @@ export function loadConfig() {
|
|
|
195
198
|
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
196
199
|
// event even when session_start never fires. Default ON; OFF = byte-identical.
|
|
197
200
|
threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
|
|
201
|
+
// Sprint A: Mega↔ithacus bridge — gate the child extension + bridge usage.
|
|
202
|
+
// Default ON; OFF (=0/`=false`) = byte-identical pre-bridge behavior.
|
|
203
|
+
// Runtime reads envBool(plain key), mirroring threeWayFailback (plain-write
|
|
204
|
+
// convention, not _DISABLED).
|
|
205
|
+
ithacusBridge: envBool("MEGACOMPACT_ITHACUS_BRIDGE", true),
|
|
198
206
|
// 3WF-2: ThrashGuard re-arm budget as a fraction of effectiveThreshold.
|
|
199
207
|
// 0.10 default (10% of the effective threshold) — see mega-config-types.
|
|
200
208
|
// Clamped to [0.01, 0.5]: below 1% the guard is almost never armed (any
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/factory.ts — `createMegaBridge(opts)` implementation.
|
|
3
|
+
*
|
|
4
|
+
* A thin, pi-agnostic wrapper over the engine's compaction / recall / memory /
|
|
5
|
+
* fork / vector APIs. Stores are constructed lazily on first use (a consumer
|
|
6
|
+
* that only calls recallMemories pays no VectorStore cost). Exceptions
|
|
7
|
+
* propagate from every method except `fork` (catches ForkError by design) and
|
|
8
|
+
* `close` (swallows best-effort cleanup), so failures surface in tests.
|
|
9
|
+
*/
|
|
10
|
+
import { compactSession } from "../engine.js";
|
|
11
|
+
import { recallAndInline, recallAndInlineAsync, recallMemoriesAndInline, } from "../recall.js";
|
|
12
|
+
import { forkFromConversation, ForkError } from "../fork.js";
|
|
13
|
+
import { createTurnStore } from "../store/turns/index.js";
|
|
14
|
+
import { addMemory } from "../store/sqlite/memories.js";
|
|
15
|
+
import { VectorStore, vectorSearch } from "../vectorStore.js";
|
|
16
|
+
import { repoKey } from "../store/repoKey.js";
|
|
17
|
+
/** Map a RecallInjectResult to the bridge's slimmer result contract. */
|
|
18
|
+
function mapRecallResult(r) {
|
|
19
|
+
return {
|
|
20
|
+
block: r.block,
|
|
21
|
+
report: r.report,
|
|
22
|
+
hitCount: r.toInject.length,
|
|
23
|
+
empty: r.empty,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Map the memoryRecallAndInline tuple result to the bridge contract. */
|
|
27
|
+
function mapMemoryResult(r) {
|
|
28
|
+
return {
|
|
29
|
+
block: r.block,
|
|
30
|
+
report: r.report,
|
|
31
|
+
hitCount: r.report.length,
|
|
32
|
+
empty: r.empty,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Map vectorSearch hits to the cortex result contract. */
|
|
36
|
+
function mapCortexHits(hits, limit) {
|
|
37
|
+
const top = hits.slice(0, limit);
|
|
38
|
+
return {
|
|
39
|
+
results: top.map((h) => ({
|
|
40
|
+
checkpointId: h.checkpoint.checkpointId,
|
|
41
|
+
score: h.score,
|
|
42
|
+
summary: h.checkpoint.summary,
|
|
43
|
+
})),
|
|
44
|
+
hitCount: top.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a MegaBridge over a single stateDir.
|
|
49
|
+
*
|
|
50
|
+
* The VectorStore and TurnStore are lazy: constructed on first use and cached
|
|
51
|
+
* in closures. The stateDir is retained for memory recall, which needs it
|
|
52
|
+
* directly.
|
|
53
|
+
*/
|
|
54
|
+
export function createMegaBridge(opts) {
|
|
55
|
+
const stateDir = opts.stateDir;
|
|
56
|
+
let vectorStore;
|
|
57
|
+
let turnStore;
|
|
58
|
+
const getVectorStore = () => {
|
|
59
|
+
if (!vectorStore)
|
|
60
|
+
vectorStore = new VectorStore({ stateDir });
|
|
61
|
+
return vectorStore;
|
|
62
|
+
};
|
|
63
|
+
const getTurnStore = () => {
|
|
64
|
+
if (!turnStore)
|
|
65
|
+
turnStore = createTurnStore({ stateDir });
|
|
66
|
+
return turnStore;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
compact(input) {
|
|
70
|
+
const result = compactSession({
|
|
71
|
+
sessionId: input.sessionId,
|
|
72
|
+
messages: input.messages,
|
|
73
|
+
keepFrom: input.keepFrom,
|
|
74
|
+
summary: input.summary,
|
|
75
|
+
keyDecisions: input.keyDecisions,
|
|
76
|
+
nextSteps: input.nextSteps,
|
|
77
|
+
filesModified: input.filesModified,
|
|
78
|
+
compressionPressure: input.compressionPressure,
|
|
79
|
+
}, getVectorStore());
|
|
80
|
+
return {
|
|
81
|
+
skipped: result.skipped,
|
|
82
|
+
deduped: result.deduped,
|
|
83
|
+
summary: result.summary,
|
|
84
|
+
checkpointId: result.checkpointId,
|
|
85
|
+
tokenEstimate: result.tokenEstimate,
|
|
86
|
+
originalTokenEstimate: result.originalTokenEstimate,
|
|
87
|
+
compactedFrom: result.compactedFrom,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
recallCheckpoints(opts) {
|
|
91
|
+
const recallOpts = {
|
|
92
|
+
sessionId: opts.sessionId,
|
|
93
|
+
query: opts.query,
|
|
94
|
+
limit: opts.limit ?? 3,
|
|
95
|
+
source: "command",
|
|
96
|
+
skipInjected: opts.skipInjected,
|
|
97
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
98
|
+
};
|
|
99
|
+
return mapRecallResult(recallAndInline(recallOpts, getVectorStore()));
|
|
100
|
+
},
|
|
101
|
+
async recallMemories(opts) {
|
|
102
|
+
const memOpts = {
|
|
103
|
+
query: opts.query,
|
|
104
|
+
stateDir,
|
|
105
|
+
limit: opts.limit,
|
|
106
|
+
minSimilarity: opts.minSimilarity,
|
|
107
|
+
crossRepo: opts.crossRepo,
|
|
108
|
+
crossRepoCosine: opts.crossRepoCosine,
|
|
109
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
110
|
+
};
|
|
111
|
+
const r = await recallMemoriesAndInline(memOpts);
|
|
112
|
+
return mapMemoryResult(r);
|
|
113
|
+
},
|
|
114
|
+
async recallAndInlineAsync(opts) {
|
|
115
|
+
const recallOpts = {
|
|
116
|
+
sessionId: opts.sessionId,
|
|
117
|
+
query: opts.query,
|
|
118
|
+
limit: opts.limit ?? 3,
|
|
119
|
+
source: "command",
|
|
120
|
+
skipInjected: opts.skipInjected,
|
|
121
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
122
|
+
};
|
|
123
|
+
const r = await recallAndInlineAsync(recallOpts, getVectorStore());
|
|
124
|
+
return mapRecallResult(r);
|
|
125
|
+
},
|
|
126
|
+
fork(opts) {
|
|
127
|
+
try {
|
|
128
|
+
const outcome = forkFromConversation(getTurnStore(), opts.parentConversationId, opts.turnIndex);
|
|
129
|
+
return {
|
|
130
|
+
childConversationId: outcome.childConversationId,
|
|
131
|
+
checkpointIds: outcome.checkpointIds,
|
|
132
|
+
forkTurnIndex: opts.turnIndex,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
if (e instanceof ForkError) {
|
|
137
|
+
return { error: e.code };
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
cortexQuery(opts) {
|
|
143
|
+
const limit = opts.limit ?? 3;
|
|
144
|
+
const scope = opts.repo ?? repoKey(stateDir);
|
|
145
|
+
const hits = vectorSearch(getVectorStore(), scope, opts.query, limit);
|
|
146
|
+
return mapCortexHits(hits, limit);
|
|
147
|
+
},
|
|
148
|
+
addMemory(input) {
|
|
149
|
+
// repo === null ⇒ stateDir-scoped durable memory (matches recallMemories).
|
|
150
|
+
return addMemory({
|
|
151
|
+
kind: input.kind,
|
|
152
|
+
content: input.content,
|
|
153
|
+
tags: input.tags,
|
|
154
|
+
category: input.category,
|
|
155
|
+
}, null, stateDir);
|
|
156
|
+
},
|
|
157
|
+
recordTurn(input) {
|
|
158
|
+
const turn = {
|
|
159
|
+
conversationId: input.conversationId,
|
|
160
|
+
sessionId: input.sessionId,
|
|
161
|
+
turnIndex: input.turnIndex,
|
|
162
|
+
role: input.role ?? "assistant",
|
|
163
|
+
endedAt: input.endedAt ?? Date.now(),
|
|
164
|
+
ctxTokens: input.ctxTokens,
|
|
165
|
+
ctxPercent: input.ctxPercent,
|
|
166
|
+
model: input.model,
|
|
167
|
+
};
|
|
168
|
+
getTurnStore().asWriter().appendTurn(turn);
|
|
169
|
+
},
|
|
170
|
+
close() {
|
|
171
|
+
if (turnStore) {
|
|
172
|
+
try {
|
|
173
|
+
turnStore.close();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* best-effort */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/types.ts — contract types for the bidirectional mega-compact bridge.
|
|
3
|
+
*
|
|
4
|
+
* A pi-agnostic, unit-testable adapter surface that wraps the engine's
|
|
5
|
+
* compaction / recall / memory / fork / vector APIs behind one factory so an
|
|
6
|
+
* external host (ithacus) can drive them without importing pi-runtime types.
|
|
7
|
+
* Every type here mirrors a real engine signature (see factory.ts for the
|
|
8
|
+
* wiring).
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMegaBridge } from "./bridge/factory.js";
|
package/dist/src/config.js
CHANGED
|
@@ -116,8 +116,19 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
116
116
|
// default (0.12) keeps recall permissive within a repo while still rejecting
|
|
117
117
|
// effectively-unrelated hits. Call-time read so tests can set the env per-test.
|
|
118
118
|
// ---------------------------------------------------------------------------
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Same-repo recall cosine floor: top winner must be >= this to be injected.
|
|
121
|
+
*
|
|
122
|
+
* E1 follow-up (PR #18 review): NaN-safe + clamped to [0,1]. A typo'd env var
|
|
123
|
+
* yielded NaN before; `cosine < NaN` is false, which disabled gate 1 entirely
|
|
124
|
+
* (every candidate passed). Non-finite falls back to 0.12; out-of-range clamps.
|
|
125
|
+
*/
|
|
126
|
+
export const RECALL_MIN_COSINE = () => {
|
|
127
|
+
const n = Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12");
|
|
128
|
+
if (!Number.isFinite(n))
|
|
129
|
+
return 0.12;
|
|
130
|
+
return Math.min(1, Math.max(0, n));
|
|
131
|
+
};
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
122
133
|
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
|
123
134
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
@@ -19,6 +19,7 @@ export const SHINGLE_SIZE = 5; // char 5-grams
|
|
|
19
19
|
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
20
20
|
const SEED = 0xdeadbeef;
|
|
21
21
|
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
22
|
+
const PBigInt = 2147483647n; // BigInt twin for overflow-safe modular reduction
|
|
22
23
|
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
23
24
|
function coeffA(i) {
|
|
24
25
|
return (SEED + i * 2 + 1) % P;
|
|
@@ -67,10 +68,10 @@ export function minhashSignature(text) {
|
|
|
67
68
|
const b = coeffB(i);
|
|
68
69
|
let min = P;
|
|
69
70
|
for (const x of grams) {
|
|
70
|
-
// (a*x + b) mod p
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const h = (
|
|
71
|
+
// (a*x + b) mod p. a, x < 2^31 so a*x < 2^62 — EXCEEDS 2^53. The naive
|
|
72
|
+
// (a*(x%P))%P loses precision (verified: a=x=p-1 → lossy 2147483644 vs
|
|
73
|
+
// exact 1). BigInt is correct + cheap (~5ms per signature).
|
|
74
|
+
const h = Number((BigInt(a) * BigInt(x % P) + BigInt(b)) % PBigInt);
|
|
74
75
|
if (h < min)
|
|
75
76
|
min = h;
|
|
76
77
|
}
|
|
@@ -76,7 +76,14 @@ export function validateRecall(winners, opts, store) {
|
|
|
76
76
|
// No comparable cosine available => cannot clear a cosine gate.
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// E1 follow-up (PR #18 review): NaN/Infinity must NEVER clear the floor.
|
|
80
|
+
// `NaN < floor` is false, so an unguarded comparison lets a NaN cosine
|
|
81
|
+
// PASS gate 1 and inject — one NaN source poisons the whole 3WF-3
|
|
82
|
+
// quorum. Reject non-finite scores explicitly; the candidate is skipped
|
|
83
|
+
// and, if all fail, the provenance floor ("no recall") is returned —
|
|
84
|
+
// never a zero-score injection. The default TrigramEmbedder cannot
|
|
85
|
+
// produce NaN (zero-norm guard), but a BYO localhost embedder can.
|
|
86
|
+
if (!Number.isFinite(cosine) || cosine < floor)
|
|
80
87
|
continue;
|
|
81
88
|
// Gate 2: not already resident in the live window.
|
|
82
89
|
if (liveVecs.length > 0) {
|
package/dist/src/recall/vote.js
CHANGED
|
@@ -31,17 +31,36 @@ import { listCheckpoints } from "../store/sqlite.js";
|
|
|
31
31
|
import { computeContentDigest } from "../dedup/digest.js";
|
|
32
32
|
import { recallRawHits } from "./readonly.js";
|
|
33
33
|
import { Logger } from "../log.js";
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Per-source normalization: map raw scores to 0..1 via min-max within source.
|
|
36
|
+
*
|
|
37
|
+
* E1 follow-up (PR #18 review): non-finite scores (NaN/±Infinity) are DROPPED
|
|
38
|
+
* before the min/max fold — a single NaN silently propagates through Math.min/
|
|
39
|
+
* max and turns EVERY normalized score of that source into NaN (verified),
|
|
40
|
+
* poisoning the whole 3-source quorum. Dropping the bad entry degrades that
|
|
41
|
+
* source gracefully instead. Exported so the guard is unit-testable directly.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeScores(scores) {
|
|
36
44
|
const map = new Map();
|
|
37
|
-
|
|
38
|
-
return map;
|
|
39
|
-
const min = Math.min(...scores);
|
|
40
|
-
const max = Math.max(...scores);
|
|
41
|
-
const span = max - min;
|
|
45
|
+
const finite = [];
|
|
42
46
|
scores.forEach((s, i) => {
|
|
43
|
-
|
|
47
|
+
if (Number.isFinite(s))
|
|
48
|
+
finite.push({ idx: i, s });
|
|
44
49
|
});
|
|
50
|
+
if (finite.length === 0)
|
|
51
|
+
return map;
|
|
52
|
+
let min = Infinity;
|
|
53
|
+
let max = -Infinity;
|
|
54
|
+
for (const { s } of finite) {
|
|
55
|
+
if (s < min)
|
|
56
|
+
min = s;
|
|
57
|
+
if (s > max)
|
|
58
|
+
max = s;
|
|
59
|
+
}
|
|
60
|
+
const span = max - min;
|
|
61
|
+
for (const { idx, s } of finite) {
|
|
62
|
+
map.set(idx, span === 0 ? 1 : (s - min) / span);
|
|
63
|
+
}
|
|
45
64
|
return map;
|
|
46
65
|
}
|
|
47
66
|
/**
|
|
@@ -130,7 +149,13 @@ export function voteRecall(opts, store) {
|
|
|
130
149
|
const norm = perSource.find((p) => p.name === src.name).norm;
|
|
131
150
|
const bestByCp = new Map();
|
|
132
151
|
src.cands.forEach((c, i) => {
|
|
133
|
-
|
|
152
|
+
// E1 follow-up: normalizeScores dropped non-finite scores; such a hit is
|
|
153
|
+
// NOT a valid nomination — skip it entirely instead of defaulting it to
|
|
154
|
+
// 0 (which would still name the checkpoint and let it rank last into the
|
|
155
|
+
// fallback ranking).
|
|
156
|
+
const n = norm.get(i);
|
|
157
|
+
if (n === undefined)
|
|
158
|
+
return;
|
|
134
159
|
const prev = bestByCp.get(c.checkpointId);
|
|
135
160
|
if (prev === undefined || n > prev)
|
|
136
161
|
bestByCp.set(c.checkpointId, n);
|
|
@@ -87,7 +87,12 @@ export function hydrateFts5Hits(hits, sessionId, stateDir) {
|
|
|
87
87
|
const out = [];
|
|
88
88
|
for (const h of hits) {
|
|
89
89
|
const cp = cpMap.get(h.id);
|
|
90
|
-
|
|
90
|
+
// H1 follow-up (PR #18 review): exclude SemDeDup-'removed' rows. The FTS5
|
|
91
|
+
// index is NOT pruned by vectorSemDedup, so a removed row can still MATCH;
|
|
92
|
+
// hydrating it would let the fts5 voter NAME a dead checkpoint (a removed
|
|
93
|
+
// row plus a recency vote is a 2/3 agreement that passes the validator).
|
|
94
|
+
// Mirrors vectorSearch's read-time filter.
|
|
95
|
+
if (cp && cp.dedupStatus !== "removed")
|
|
91
96
|
out.push({ checkpointId: h.id, score: h.score, summary: cp.summary });
|
|
92
97
|
}
|
|
93
98
|
return out;
|
package/dist/src/vector-read.js
CHANGED
|
@@ -72,7 +72,10 @@ export function vectorDedupe(store, sessionId, regionHashOrText, isText = false)
|
|
|
72
72
|
const state = loadSessionState(sid, stateDir);
|
|
73
73
|
if (state.storedRegionHashes.includes(hash))
|
|
74
74
|
return true;
|
|
75
|
-
|
|
75
|
+
// H1 follow-up (PR #18 review): a SemDeDup-'removed' row's regionHash must not
|
|
76
|
+
// report "already represented" — its content is excluded from recall, so the
|
|
77
|
+
// incoming region is NOT deduplicated in any retrievable sense.
|
|
78
|
+
return listCheckpoints(sid, stateDir).some((c) => c.dedupStatus !== "removed" && c.regionHash === hash);
|
|
76
79
|
}
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
78
81
|
// Injection tracking
|
|
@@ -115,7 +118,8 @@ export function vectorTopSimilar(store, sessionId, n) {
|
|
|
115
118
|
const ordered = [...checkpoints].sort((a, b) => a.checkpointId.localeCompare(b.checkpointId));
|
|
116
119
|
const current = ordered[ordered.length - 1];
|
|
117
120
|
const scored = ordered
|
|
118
|
-
.filter((cp) => cp.checkpointId !== current.checkpointId
|
|
121
|
+
.filter((cp) => cp.checkpointId !== current.checkpointId &&
|
|
122
|
+
cp.dedupStatus !== "removed")
|
|
119
123
|
.map((cp) => ({
|
|
120
124
|
checkpoint: cp,
|
|
121
125
|
score: cosineSimilarity(current.embedding, cp.embedding),
|
|
@@ -40,7 +40,11 @@ export function addCheckpoint(store, input) {
|
|
|
40
40
|
const t0 = Date.now();
|
|
41
41
|
const sessionId = normalizeSessionId(input.sessionId);
|
|
42
42
|
const regionHash = computeRegionHash(input.regionText);
|
|
43
|
-
|
|
43
|
+
// H1: exclude SemDeDup-'removed' rows from dedup matching. search() already
|
|
44
|
+
// filters these, but add() did NOT — so an L0/L1/L2 match against a previously
|
|
45
|
+
// removed duplicate would upsertCheckpoint it back to active, resurrecting it
|
|
46
|
+
// into recall and defeating SemDeDup.
|
|
47
|
+
const all = listCheckpoints(sessionId, store.stateDir).filter((cp) => cp.dedupStatus !== "removed");
|
|
44
48
|
// Honest "tokens saved" base for this region. For a deduped add the whole
|
|
45
49
|
// original region is discarded (nothing new stored); for a new checkpoint
|
|
46
50
|
// we persist (orig − stored). Falls back to stored when orig is unknown.
|
|
@@ -305,6 +305,12 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
|
|
|
305
305
|
"Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
|
|
306
306
|
true,
|
|
307
307
|
),
|
|
308
|
+
boolDirect(
|
|
309
|
+
"MEGACOMPACT_ITHACUS_BRIDGE",
|
|
310
|
+
"ithacus Bridge",
|
|
311
|
+
"Mega↔ithacus bridge: gate the child extension + bridge usage that tie this extension to ithacus's durable compaction. Default ON; OFF (=0/`=false`) is byte-identical to pre-bridge behavior — the bridge is only consulted when this is ON. Positive sprint flag. Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
|
|
312
|
+
true,
|
|
313
|
+
),
|
|
308
314
|
boolDirect(
|
|
309
315
|
"MEGACOMPACT_RECALL_TAIL_INJECT",
|
|
310
316
|
"Recall Tail Inject",
|