pi-mega-compact 0.11.7 → 0.11.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/dist/extensions/dashboard-server/gather-debug-keys.js +18 -0
- package/dist/extensions/dashboard-server/gather-debug.js +129 -0
- package/dist/extensions/dashboard-server/routes-maintenance.js +6 -0
- package/dist/extensions/dashboard-server/routes-memory-map.js +8 -15
- package/dist/extensions/mega-commands.js +65 -5
- package/dist/extensions/mega-config.js +7 -0
- package/dist/extensions/mega-events/agent-handlers.js +74 -5
- package/dist/extensions/mega-events/context-handler.js +132 -3
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-trim.js +10 -0
- package/dist/src/config/turns.js +2 -1
- package/dist/src/memoryGraph/embedding.js +50 -0
- package/dist/src/memoryGraph/gates.js +379 -0
- package/dist/src/memoryGraph/sources.js +390 -0
- package/dist/src/memoryGraph.js +133 -259
- package/dist/src/store/sqlite/embedding-cache.js +71 -0
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/topics/cluster.js +10 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-DFitrb4Y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-DFitrb4Y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-PJrmq8pa.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-PJrmq8pa.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-C1WE7dEx.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-Dx-RpO8C.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-Dx-RpO8C.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-C63BEWEs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-C63BEWEs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-DHpJE6km.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-DHpJE6km.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-BSv0GfBb.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-Cg1fgEx_.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-Cg1fgEx_.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-DCahxNOQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-DCahxNOQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-RYvO1SZL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-RYvO1SZL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-BDGr_UXb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-BDGr_UXb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-ByrTnHaB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-ByrTnHaB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-CELafKis.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-CELafKis.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-BLWtmfr_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-BLWtmfr_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-8Fsh_Igx.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-8Fsh_Igx.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-D7mgXDe2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-D7mgXDe2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-h3jgEs6G.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-h3jgEs6G.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-C5lmipBF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-C5lmipBF.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/client.ts +5 -0
- package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
- package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +29 -2
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +101 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
- package/extensions/dashboard-server/api-contracts/index.ts +3 -1
- package/extensions/dashboard-server/api-contracts/maintenance.ts +13 -0
- package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
- package/extensions/dashboard-server/gather-debug-keys.ts +18 -0
- package/extensions/dashboard-server/gather-debug.ts +136 -0
- package/extensions/dashboard-server/routes-maintenance.ts +7 -0
- package/extensions/dashboard-server/routes-memory-map.ts +21 -29
- package/extensions/mega-commands.ts +63 -6
- package/extensions/mega-config.ts +22 -0
- package/extensions/mega-events/agent-handlers.ts +88 -5
- package/extensions/mega-events/context-handler.ts +152 -3
- package/extensions/mega-runtime/snapshot.ts +1 -1
- package/extensions/mega-trim.ts +15 -0
- package/package.json +1 -1
- package/src/config/turns.ts +5 -2
- package/src/memoryGraph/embedding.ts +63 -0
- package/src/memoryGraph/gates.ts +451 -0
- package/src/memoryGraph/sources.ts +484 -0
- package/src/memoryGraph.ts +188 -350
- package/src/store/sqlite/embedding-cache.ts +81 -0
- package/src/store/sqlite.ts +1 -0
- package/src/topics/cluster.ts +11 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-P4SUsvMf.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-P4SUsvMf.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
|
@@ -7,11 +7,26 @@ import { stampTurnsEpochFor } from "../mega-turn-store.js";
|
|
|
7
7
|
import { TurnsConfig } from "../../src/config/turns.js";
|
|
8
8
|
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
9
9
|
import { buildTopicModel, createTopicStore, bumpWikiCompactCounter, } from "../../src/topics/index.js";
|
|
10
|
+
import { TrigramEmbedder } from "../../src/embedder.js";
|
|
10
11
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
|
|
11
12
|
import { pressureFromPct, pressureRatio, } from "../mega-config.js";
|
|
12
13
|
import { appendMirrorMessages } from "./mirror-append.js";
|
|
13
14
|
import { stagedForTail, withRecallTail } from "./recall-tail.js";
|
|
14
|
-
import { buildSeparatedPrompt } from "./separated-prompt.js";
|
|
15
|
+
import { buildSeparatedPrompt, buildCacheOptimizedPrompt } from "./separated-prompt.js";
|
|
16
|
+
/** Best-effort text extraction from an AgentMessage for analytics/logging.
|
|
17
|
+
* AgentMessage is a discriminated union; .content exists only on some
|
|
18
|
+
* variants (user/assistant/toolResult/custom). Narrow by role, never assume. */
|
|
19
|
+
function messageContentText(m) {
|
|
20
|
+
const c = m.content;
|
|
21
|
+
if (typeof c === "string")
|
|
22
|
+
return c;
|
|
23
|
+
if (Array.isArray(c)) {
|
|
24
|
+
return c
|
|
25
|
+
.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text ?? "") : ""))
|
|
26
|
+
.join(" ");
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
15
30
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
16
31
|
export function registerContextHandler(pi, runtime, config) {
|
|
17
32
|
// ---- Auto-trigger: live trim (compact and continue) + native durable ----
|
|
@@ -40,14 +55,32 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
40
55
|
// reaches raw_transcript (PREVENT-PI: append-only, view-only).
|
|
41
56
|
const tailResult = (msgs) => {
|
|
42
57
|
const base = msgs ?? messages;
|
|
43
|
-
if (!stagedForTail(runtime, config) && !config.messageSeparation)
|
|
58
|
+
if (!stagedForTail(runtime, config) && !config.messageSeparation && !config.cacheStriping)
|
|
44
59
|
return undefined;
|
|
45
60
|
let result = stagedForTail(runtime, config)
|
|
46
61
|
? withRecallTail(base, runtime, config)
|
|
47
62
|
: base;
|
|
48
|
-
if (config.
|
|
63
|
+
if (config.cacheStriping) {
|
|
64
|
+
result = buildCacheOptimizedPrompt(result);
|
|
65
|
+
}
|
|
66
|
+
else if (config.messageSeparation) {
|
|
49
67
|
result = buildSeparatedPrompt(result);
|
|
50
68
|
}
|
|
69
|
+
// P2.5: log prefix stability (fire-and-forget, non-fatal).
|
|
70
|
+
// tailResult is sync, so use .then().catch() on the dynamic import.
|
|
71
|
+
if (result.length > 1) {
|
|
72
|
+
import("../../src/cache-stripe-impl.js").then(({ computeStabilityScore }) => {
|
|
73
|
+
const stableScore = computeStabilityScore({ content: messageContentText(result[0] ?? result[0]), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 }, result.slice(0, 2).map((m) => ({ content: messageContentText(m), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 })));
|
|
74
|
+
runtime.logger.info("prefix_stability", {
|
|
75
|
+
stableScore: Number.isFinite(stableScore) ? stableScore : 0,
|
|
76
|
+
prefixMessages: result.length,
|
|
77
|
+
separation: config.messageSeparation ? "v2" : "off",
|
|
78
|
+
striping: config.cacheStriping ? "v3" : "off",
|
|
79
|
+
});
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
// Non-fatal: stability logging is best-effort.
|
|
82
|
+
});
|
|
83
|
+
}
|
|
51
84
|
return { messages: result };
|
|
52
85
|
};
|
|
53
86
|
// Always track context for the dashboard/widget, even when auto is off.
|
|
@@ -88,6 +121,30 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
88
121
|
try {
|
|
89
122
|
const db = openStore(runtime.currentStateDir);
|
|
90
123
|
appendMirrorMessages(db, messages, runtime.rt.sessionId, epochIdFor(runtime.rt.sessionId), runtime.currentTurn);
|
|
124
|
+
// P2.2: populate conversation_thread + tool_results tables for
|
|
125
|
+
// prompt-cache analytics and durable separation. The live-array
|
|
126
|
+
// separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
|
|
127
|
+
// tailResult above) is sufficient for the prompt-construction path;
|
|
128
|
+
// these DB writes persist the split for post-hoc analysis, dashboard
|
|
129
|
+
// queries, and future readers. Non-fatal — failure here never breaks
|
|
130
|
+
// the agent loop (PREVENT-PI-004: zero network, local SQLite only).
|
|
131
|
+
{
|
|
132
|
+
const sid = runtime.rt.sessionId;
|
|
133
|
+
const turn = runtime.currentTurn;
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const threadStmt = db.prepare("INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
|
|
136
|
+
const toolStmt = db.prepare("INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)");
|
|
137
|
+
for (const m of messages) {
|
|
138
|
+
const role = m.role;
|
|
139
|
+
const content = messageContentText(m);
|
|
140
|
+
if (role === "user" || role === "assistant") {
|
|
141
|
+
threadStmt.run(sid, role, content, turn, now);
|
|
142
|
+
}
|
|
143
|
+
else if (role === "toolResult" || role === "bashExecution") {
|
|
144
|
+
toolStmt.run(sid, role, content, turn, now);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
91
148
|
}
|
|
92
149
|
catch (e) {
|
|
93
150
|
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
@@ -250,6 +307,70 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
250
307
|
error: String(wikiErr),
|
|
251
308
|
});
|
|
252
309
|
}
|
|
310
|
+
// D1: seed the topic model from raw_transcript when context_chunks is
|
|
311
|
+
// thin (pre-compaction). Gated on WIKI_SEED_FROM_TURNS; non-fatal.
|
|
312
|
+
// Seeds buildTopicModel with on-the-fly trigram embeddings from
|
|
313
|
+
// recent raw_transcript rows for the current session.
|
|
314
|
+
try {
|
|
315
|
+
if (config.autoWikiEnabled &&
|
|
316
|
+
config.turnsDbEnabled &&
|
|
317
|
+
TurnsConfig.WIKI_SEED_FROM_TURNS) {
|
|
318
|
+
const floor = 50;
|
|
319
|
+
const countRow = db
|
|
320
|
+
.prepare(`SELECT COUNT(*) AS cnt FROM context_chunks WHERE session_id = ?`)
|
|
321
|
+
.get(runtime.rt.sessionId);
|
|
322
|
+
const chunkCount = countRow?.cnt ?? 0;
|
|
323
|
+
if (chunkCount < floor) {
|
|
324
|
+
const transcriptRows = db
|
|
325
|
+
.prepare(`SELECT DISTINCT content_bytes
|
|
326
|
+
FROM raw_transcript
|
|
327
|
+
WHERE session_id = ?
|
|
328
|
+
AND length(content_bytes) > 10
|
|
329
|
+
ORDER BY seq ASC
|
|
330
|
+
LIMIT 200`)
|
|
331
|
+
.all(runtime.rt.sessionId);
|
|
332
|
+
if (transcriptRows.length > 0) {
|
|
333
|
+
const embedder = new TrigramEmbedder();
|
|
334
|
+
const seedChunks = [];
|
|
335
|
+
for (let i = 0; i < transcriptRows.length; i++) {
|
|
336
|
+
const text = transcriptRows[i].content_bytes.trim();
|
|
337
|
+
if (text.length === 0)
|
|
338
|
+
continue;
|
|
339
|
+
const vec = embedder.embed(text);
|
|
340
|
+
seedChunks.push({
|
|
341
|
+
chunkId: `seed_transcript_${i}`,
|
|
342
|
+
sessionId: runtime.rt.sessionId,
|
|
343
|
+
vec,
|
|
344
|
+
text,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (seedChunks.length > 0) {
|
|
348
|
+
const model = buildTopicModel(db, {
|
|
349
|
+
kRange: [
|
|
350
|
+
TurnsConfig.WIKI_K_MIN,
|
|
351
|
+
TurnsConfig.WIKI_K_MAX,
|
|
352
|
+
],
|
|
353
|
+
labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
|
|
354
|
+
restarts: 5,
|
|
355
|
+
seed: 0x9e3779b9,
|
|
356
|
+
}, seedChunks);
|
|
357
|
+
createTopicStore(runtime.currentStateDir).replaceTopicModel(model);
|
|
358
|
+
runtime.logger.info("wiki_seed", {
|
|
359
|
+
clusterCount: model.k,
|
|
360
|
+
sourceChunks: seedChunks.length,
|
|
361
|
+
totalChunks: model.totalChunks,
|
|
362
|
+
method: "kmeans+tfidf",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (seedErr) {
|
|
370
|
+
runtime.logger.warn("wiki_seed_failed", {
|
|
371
|
+
error: String(seedErr),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
253
374
|
// S27 Task 6: Fire-and-forget dedup pipeline.
|
|
254
375
|
// Deduplicates raw_transcript rows for the compacted range.
|
|
255
376
|
try {
|
|
@@ -336,6 +457,13 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
336
457
|
compactedFrom: ran.result.compactedFrom,
|
|
337
458
|
summary: ran.result.summary,
|
|
338
459
|
anchorUserMessages,
|
|
460
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
|
|
461
|
+
// window, relief takes priority over the anchor floor. Without this,
|
|
462
|
+
// computeLiveTrimCut bails to null (can't satisfy the floor) and the
|
|
463
|
+
// model is fed a raw overflow that errors every turn — the
|
|
464
|
+
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
465
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
466
|
+
criticalOver: (pct ?? 0) >= 90,
|
|
339
467
|
});
|
|
340
468
|
if (cut === null) {
|
|
341
469
|
runtime.diagCtxCutNull++;
|
|
@@ -344,6 +472,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
344
472
|
compactedFrom: ran.result.compactedFrom,
|
|
345
473
|
viewLen: view.length,
|
|
346
474
|
anchorUserMessages,
|
|
475
|
+
criticalOver: (pct ?? 0) >= 90,
|
|
347
476
|
});
|
|
348
477
|
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
349
478
|
}
|
|
@@ -131,7 +131,7 @@ export function computeMegaSnapshot(p) {
|
|
|
131
131
|
cachePct,
|
|
132
132
|
megaCacheFlare: p.megaCacheFlare,
|
|
133
133
|
megaCacheFlarePct: p.megaCacheFlarePct,
|
|
134
|
-
perTurnCacheHitPct: p.perTurnCacheHitPct
|
|
134
|
+
perTurnCacheHitPct: p.perTurnCacheHitPct,
|
|
135
135
|
levelUpFlare: p.levelUpFlare,
|
|
136
136
|
achievementFlare: p.achievementFlare,
|
|
137
137
|
achievementFlareTitles: p.achievementFlareTitles,
|
|
@@ -48,10 +48,20 @@ export function computeLiveTrimCut(view, opts) {
|
|
|
48
48
|
if (cut > 1) {
|
|
49
49
|
const finalRecent = view.slice(cut);
|
|
50
50
|
if (finalRecent.filter((m) => m.role === "user").length < opts.anchorUserMessages) {
|
|
51
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is critically over the window,
|
|
52
|
+
// return the boundary-safe cut even though it's below the anchor floor.
|
|
53
|
+
// Bailing to null here would feed the model the raw overflow and trap the
|
|
54
|
+
// session in an unrecoverable error loop ("Already compacted" + overflow).
|
|
55
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
56
|
+
if (opts.criticalOver && cut > 0)
|
|
57
|
+
return cut;
|
|
51
58
|
return null; // cannot satisfy the floor without dropping too much — retry next call
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
else {
|
|
62
|
+
// Same escape hatch for the cut > 1 false branch.
|
|
63
|
+
if (opts.criticalOver && cut > 0)
|
|
64
|
+
return cut;
|
|
55
65
|
return null;
|
|
56
66
|
}
|
|
57
67
|
}
|
package/dist/src/config/turns.js
CHANGED
|
@@ -42,7 +42,8 @@ export function loadTurnsConfig() {
|
|
|
42
42
|
WIKI_K_MIN: kMin,
|
|
43
43
|
WIKI_K_MAX: kMax,
|
|
44
44
|
WIKI_LABEL_TOP_TERMS: envNum("MEGACOMPACT_WIKI_LABEL_TOP_TERMS", 5),
|
|
45
|
-
WIKI_REBUILD_EVERY_N_COMPACTS: envNum("MEGACOMPACT_WIKI_REBUILD_EVERY",
|
|
45
|
+
WIKI_REBUILD_EVERY_N_COMPACTS: envNum("MEGACOMPACT_WIKI_REBUILD_EVERY", 3),
|
|
46
|
+
WIKI_SEED_FROM_TURNS: envBool("MEGACOMPACT_WIKI_SEED_FROM_TURNS", true),
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
export const TurnsConfig = loadTurnsConfig();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TrigramEmbedder } from "../embedder.js";
|
|
2
|
+
let _trigramEmbedder = null;
|
|
3
|
+
function lazyEmbedder() {
|
|
4
|
+
if (!_trigramEmbedder) {
|
|
5
|
+
_trigramEmbedder = new TrigramEmbedder();
|
|
6
|
+
}
|
|
7
|
+
return _trigramEmbedder;
|
|
8
|
+
}
|
|
9
|
+
function simpleHash(s) {
|
|
10
|
+
let h = 0x811c9dc5;
|
|
11
|
+
for (let i = 0; i < s.length; i++) {
|
|
12
|
+
h ^= s.charCodeAt(i);
|
|
13
|
+
h = Math.imul(h, 0x01000193);
|
|
14
|
+
}
|
|
15
|
+
return h.toString(36);
|
|
16
|
+
}
|
|
17
|
+
/** Compute or retrieve a cached embedding vector for a text string.
|
|
18
|
+
* Uses the embedding_cache table (created by the A2 migration) for
|
|
19
|
+
* persistence. Falls back to uncached TrigramEmbedder on table error. */
|
|
20
|
+
export function getOrComputeEmbedding(db, content) {
|
|
21
|
+
try {
|
|
22
|
+
const hash = simpleHash(content);
|
|
23
|
+
const cached = db
|
|
24
|
+
.prepare("SELECT embedding FROM embedding_cache WHERE content_hash = ?")
|
|
25
|
+
.get(hash);
|
|
26
|
+
if (cached && typeof cached.embedding === "string") {
|
|
27
|
+
return JSON.parse(cached.embedding);
|
|
28
|
+
}
|
|
29
|
+
const vec = lazyEmbedder().embed(content);
|
|
30
|
+
db.prepare("INSERT OR REPLACE INTO embedding_cache (content_hash, content, embedding) VALUES (?, ?, ?)").run(hash, content, JSON.stringify(vec));
|
|
31
|
+
return vec;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return lazyEmbedder().embed(content);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Cosine similarity between two equal-length vectors. */
|
|
38
|
+
export function cosineSimilarity(a, b) {
|
|
39
|
+
let dot = 0;
|
|
40
|
+
let na = 0;
|
|
41
|
+
let nb = 0;
|
|
42
|
+
const len = Math.min(a.length, b.length);
|
|
43
|
+
for (let i = 0; i < len; i++) {
|
|
44
|
+
dot += a[i] * b[i];
|
|
45
|
+
na += a[i] * a[i];
|
|
46
|
+
nb += b[i] * b[i];
|
|
47
|
+
}
|
|
48
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb);
|
|
49
|
+
return denom > 0 ? dot / denom : 0;
|
|
50
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Logger } from "../log.js";
|
|
2
|
+
const log = new Logger();
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Gate 1 — Source availability guard (informational, no drop)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function gateSourceAvailable(ws) {
|
|
7
|
+
if (ws.nodes.length === 0) {
|
|
8
|
+
log.warn("graph_source_unavailable", {
|
|
9
|
+
detail: "No nodes from any source — graph will be empty",
|
|
10
|
+
});
|
|
11
|
+
return { ok: false, dropped: 0, reason: "No nodes from any source" };
|
|
12
|
+
}
|
|
13
|
+
return { ok: true, dropped: 0 };
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Gate 2 — Identity merge: duplicate nodes with same id → richest wins
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export function gateIdentityMerge(ws) {
|
|
19
|
+
const { nodes } = ws;
|
|
20
|
+
const seen = new Map();
|
|
21
|
+
let dropped = 0;
|
|
22
|
+
for (const n of nodes) {
|
|
23
|
+
const existing = seen.get(n.id);
|
|
24
|
+
if (!existing) {
|
|
25
|
+
seen.set(n.id, n);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const existingScore = fieldScore(existing);
|
|
29
|
+
const currentScore = fieldScore(n);
|
|
30
|
+
if (currentScore > existingScore) {
|
|
31
|
+
seen.set(n.id, n);
|
|
32
|
+
}
|
|
33
|
+
dropped++;
|
|
34
|
+
log.warn("graph_identity_leak", {
|
|
35
|
+
nodeId: n.id,
|
|
36
|
+
dropped: currentScore > existingScore ? existing.id : n.id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
ws.nodes = [...seen.values()];
|
|
40
|
+
return { ok: dropped === 0, dropped, reason: dropped > 0 ? `${dropped} duplicate node ids merged` : undefined };
|
|
41
|
+
}
|
|
42
|
+
function fieldScore(n) {
|
|
43
|
+
// Base score by nodeType — richer sources win ties (spec: "richest available
|
|
44
|
+
// source wins"). checkpoint > turn-content > memory > turn-structural.
|
|
45
|
+
let s = 0;
|
|
46
|
+
switch (n.nodeType) {
|
|
47
|
+
case "checkpoint":
|
|
48
|
+
s += 4;
|
|
49
|
+
break;
|
|
50
|
+
case "turn-content":
|
|
51
|
+
s += 3;
|
|
52
|
+
break;
|
|
53
|
+
case "memory":
|
|
54
|
+
s += 2;
|
|
55
|
+
break;
|
|
56
|
+
case "turn":
|
|
57
|
+
s += 1;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (n.summaryTruncated)
|
|
61
|
+
s++;
|
|
62
|
+
if (n.textSnippet)
|
|
63
|
+
s++;
|
|
64
|
+
if (n.topicSummary)
|
|
65
|
+
s++;
|
|
66
|
+
if (n.tokenEstimate > 0)
|
|
67
|
+
s++;
|
|
68
|
+
if (n.decisionCount > 0)
|
|
69
|
+
s++;
|
|
70
|
+
if (n.dedupStatus)
|
|
71
|
+
s++;
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Gate 3 — Promotion guard: suppress turns with epoch_id if checkpoint exists
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function gatePromotionGuard(ws) {
|
|
78
|
+
const checkpointIds = new Set();
|
|
79
|
+
for (const n of ws.nodes) {
|
|
80
|
+
if (n.nodeType === "checkpoint")
|
|
81
|
+
checkpointIds.add(n.id);
|
|
82
|
+
}
|
|
83
|
+
const keep = [];
|
|
84
|
+
const droppedIds = new Set();
|
|
85
|
+
let dropped = 0;
|
|
86
|
+
for (const n of ws.nodes) {
|
|
87
|
+
if (n.nodeType !== "turn" && n.nodeType !== "turn-content") {
|
|
88
|
+
keep.push(n);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const epochId = n.epochId;
|
|
92
|
+
if (epochId && checkpointIds.has(epochId)) {
|
|
93
|
+
droppedIds.add(n.id);
|
|
94
|
+
dropped++;
|
|
95
|
+
log.warn("graph_orphaned_epoch", {
|
|
96
|
+
nodeId: n.id,
|
|
97
|
+
epochId,
|
|
98
|
+
detail: "Turn suppressed because its checkpoint exists in the graph",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (epochId && !checkpointIds.has(epochId)) {
|
|
103
|
+
log.warn("graph_orphaned_epoch", {
|
|
104
|
+
nodeId: n.id,
|
|
105
|
+
epochId,
|
|
106
|
+
detail: "Orphaned epoch — no matching checkpoint; keeping turn",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
keep.push(n);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
ws.nodes = keep;
|
|
113
|
+
ws.edges = ws.edges.filter((e) => !droppedIds.has(e.source) && !droppedIds.has(e.target));
|
|
114
|
+
return {
|
|
115
|
+
ok: dropped === 0,
|
|
116
|
+
dropped,
|
|
117
|
+
reason: dropped > 0 ? `${dropped} turn nodes suppressed (checkpoint exists)` : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Gate 4 — Node type / field completeness
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
export function gateNodeCompleteness(ws) {
|
|
124
|
+
const validNodes = [];
|
|
125
|
+
let dropped = 0;
|
|
126
|
+
for (const n of ws.nodes) {
|
|
127
|
+
if (!n.nodeType || !n.sessionId) {
|
|
128
|
+
dropped++;
|
|
129
|
+
log.warn("graph_node_double", {
|
|
130
|
+
nodeId: n.id,
|
|
131
|
+
detail: "Missing nodeType or sessionId",
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
validNodes.push(n);
|
|
136
|
+
}
|
|
137
|
+
ws.nodes = validNodes;
|
|
138
|
+
return {
|
|
139
|
+
ok: dropped === 0,
|
|
140
|
+
dropped,
|
|
141
|
+
reason: dropped > 0 ? `${dropped} nodes dropped for missing fields` : undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Gate 5 — Dangling edge guard
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
export function gateDanglingEdges(ws) {
|
|
148
|
+
const validIds = new Set(ws.nodes.map((n) => n.id));
|
|
149
|
+
const validEdges = [];
|
|
150
|
+
let dropped = 0;
|
|
151
|
+
for (const e of ws.edges) {
|
|
152
|
+
if (validIds.has(e.source) && validIds.has(e.target)) {
|
|
153
|
+
validEdges.push(e);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
dropped++;
|
|
157
|
+
log.warn("graph_dangling_edge", {
|
|
158
|
+
source: e.source,
|
|
159
|
+
target: e.target,
|
|
160
|
+
detail: "Edge references non-existent node(s)",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
ws.edges = validEdges;
|
|
165
|
+
return {
|
|
166
|
+
ok: dropped === 0,
|
|
167
|
+
dropped,
|
|
168
|
+
reason: dropped > 0 ? `${dropped} dangling edges removed` : undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Gate 6 — Edge type / threshold enforcement
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
function envThreshold(name, def) {
|
|
175
|
+
const v = process.env[name];
|
|
176
|
+
if (v === undefined)
|
|
177
|
+
return def;
|
|
178
|
+
const n = Number(v);
|
|
179
|
+
return Number.isFinite(n) ? n : def;
|
|
180
|
+
}
|
|
181
|
+
export function gateEdgeThresholds(ws, options) {
|
|
182
|
+
const withinThreshold = options?.withinTypeThreshold ?? envThreshold("MEGACOMPACT_MEMORY_GRAPH_WITHIN_TYPE_THRESHOLD", 0.7);
|
|
183
|
+
const crossThreshold = options?.crossTypeThreshold ?? envThreshold("MEGACOMPACT_MEMORY_GRAPH_CROSS_TYPE_THRESHOLD", 0.85);
|
|
184
|
+
const nodeTypeMap = new Map(ws.nodes.map((n) => [n.id, n.nodeType]));
|
|
185
|
+
const validEdges = [];
|
|
186
|
+
let dropped = 0;
|
|
187
|
+
for (const e of ws.edges) {
|
|
188
|
+
if (e.type !== "semantic") {
|
|
189
|
+
validEdges.push(e);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const srcType = nodeTypeMap.get(e.source);
|
|
193
|
+
const tgtType = nodeTypeMap.get(e.target);
|
|
194
|
+
if (srcType === "turn" || tgtType === "turn") {
|
|
195
|
+
dropped++;
|
|
196
|
+
log.warn("graph_structural_semantic_edge", {
|
|
197
|
+
nodeId: srcType === "turn" ? e.source : e.target,
|
|
198
|
+
source: e.source,
|
|
199
|
+
target: e.target,
|
|
200
|
+
score: e.weight,
|
|
201
|
+
detail: "Semantic edge on structural (turn) node",
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const sameType = srcType != null && srcType === tgtType;
|
|
206
|
+
const threshold = sameType ? withinThreshold : crossThreshold;
|
|
207
|
+
if (e.weight < threshold) {
|
|
208
|
+
dropped++;
|
|
209
|
+
log.warn("graph_edge_below_threshold", {
|
|
210
|
+
source: e.source,
|
|
211
|
+
target: e.target,
|
|
212
|
+
score: e.weight,
|
|
213
|
+
typePair: `${String(srcType)}↔${String(tgtType)}`,
|
|
214
|
+
threshold,
|
|
215
|
+
detail: `Edge below ${sameType ? "within-type" : "cross-type"} threshold`,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
validEdges.push(e);
|
|
220
|
+
}
|
|
221
|
+
ws.edges = validEdges;
|
|
222
|
+
return {
|
|
223
|
+
ok: dropped === 0,
|
|
224
|
+
dropped,
|
|
225
|
+
reason: dropped > 0 ? `${dropped} edges below threshold removed` : undefined,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Gate 7 — Dedup mirror cross-check
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
export function gateDedupRedundant(ws, options) {
|
|
232
|
+
const redundant = options?.redundantIds ?? new Set();
|
|
233
|
+
if (redundant.size === 0)
|
|
234
|
+
return { ok: true, dropped: 0 };
|
|
235
|
+
const keep = [];
|
|
236
|
+
const droppedIds = new Set();
|
|
237
|
+
let dropped = 0;
|
|
238
|
+
for (const n of ws.nodes) {
|
|
239
|
+
if (n.nodeType === "turn-content" && redundant.has(n.id)) {
|
|
240
|
+
dropped++;
|
|
241
|
+
droppedIds.add(n.id);
|
|
242
|
+
log.warn("graph_dedup_redundant", {
|
|
243
|
+
nodeId: n.id,
|
|
244
|
+
contentHash: n.id,
|
|
245
|
+
detail: "Turn content already collapsed into checkpoint",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
keep.push(n);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (droppedIds.size > 0) {
|
|
253
|
+
ws.edges = ws.edges.filter((e) => !droppedIds.has(e.source) && !droppedIds.has(e.target));
|
|
254
|
+
}
|
|
255
|
+
ws.nodes = keep;
|
|
256
|
+
return {
|
|
257
|
+
ok: dropped === 0,
|
|
258
|
+
dropped,
|
|
259
|
+
reason: dropped > 0 ? `${dropped} turn-content nodes redundant with checkpoints` : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Gate 8 — Structural vs semantic edge consistency
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
export function gateStructuralSemanticEdges(ws) {
|
|
266
|
+
const structuralTypes = new Set(["checkpoint", "turn"]);
|
|
267
|
+
const keep = [];
|
|
268
|
+
let dropped = 0;
|
|
269
|
+
for (const e of ws.edges) {
|
|
270
|
+
if (e.type !== "semantic") {
|
|
271
|
+
keep.push(e);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const srcNode = ws.nodes.find((n) => n.id === e.source);
|
|
275
|
+
const tgtNode = ws.nodes.find((n) => n.id === e.target);
|
|
276
|
+
if (srcNode && structuralTypes.has(srcNode.nodeType)) {
|
|
277
|
+
dropped++;
|
|
278
|
+
log.warn("graph_structural_semantic_edge", {
|
|
279
|
+
nodeId: srcNode.id,
|
|
280
|
+
nodeType: srcNode.nodeType,
|
|
281
|
+
detail: "Structural node has semantic edge",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else if (tgtNode && structuralTypes.has(tgtNode.nodeType)) {
|
|
285
|
+
dropped++;
|
|
286
|
+
log.warn("graph_structural_semantic_edge", {
|
|
287
|
+
nodeId: tgtNode.id,
|
|
288
|
+
nodeType: tgtNode.nodeType,
|
|
289
|
+
detail: "Structural node has semantic edge",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
keep.push(e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
ws.edges = keep;
|
|
297
|
+
return {
|
|
298
|
+
ok: dropped === 0,
|
|
299
|
+
dropped,
|
|
300
|
+
reason: dropped > 0 ? `${dropped} semantic edges on structural nodes removed` : undefined,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Gate 9 — Same-type edge deduplication
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
export function gateDedupEdges(ws) {
|
|
307
|
+
const best = new Map();
|
|
308
|
+
let dropped = 0;
|
|
309
|
+
for (const e of ws.edges) {
|
|
310
|
+
const key = `${e.source}|${e.target}|${e.type}`;
|
|
311
|
+
const existing = best.get(key);
|
|
312
|
+
if (!existing || e.weight > existing.weight) {
|
|
313
|
+
if (existing)
|
|
314
|
+
dropped++;
|
|
315
|
+
best.set(key, e);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
dropped++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (dropped > 0) {
|
|
322
|
+
log.warn("graph_dedup_redundant", {
|
|
323
|
+
count: dropped,
|
|
324
|
+
detail: "Redundant edges deduplicated",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
ws.edges = [...best.values()];
|
|
328
|
+
return {
|
|
329
|
+
ok: dropped === 0,
|
|
330
|
+
dropped,
|
|
331
|
+
reason: dropped > 0 ? `${dropped} redundant edges deduplicated` : undefined,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Run all 9 gates in order over a working set. Returns validated set + stats.
|
|
335
|
+
* Gate 7 uses optional redundantIds from the caller. */
|
|
336
|
+
export function runValidationPipeline(ws, options) {
|
|
337
|
+
const gatesRun = [];
|
|
338
|
+
const gatesPassed = [];
|
|
339
|
+
const warnings = [];
|
|
340
|
+
let totalDroppedNodes = 0;
|
|
341
|
+
let totalDroppedEdges = 0;
|
|
342
|
+
const gates = [
|
|
343
|
+
{ name: "source_available", fn: () => gateSourceAvailable(ws) },
|
|
344
|
+
{ name: "identity_merge", fn: () => gateIdentityMerge(ws) },
|
|
345
|
+
{ name: "promotion_guard", fn: () => gatePromotionGuard(ws) },
|
|
346
|
+
{ name: "node_completeness", fn: () => gateNodeCompleteness(ws) },
|
|
347
|
+
{ name: "dangling_edges", fn: () => gateDanglingEdges(ws) },
|
|
348
|
+
{ name: "edge_thresholds", fn: () => gateEdgeThresholds(ws, { withinTypeThreshold: options?.withinTypeThreshold, crossTypeThreshold: options?.crossTypeThreshold }) },
|
|
349
|
+
{ name: "dedup_redundant", fn: () => gateDedupRedundant(ws, { redundantIds: options?.redundantIds }) },
|
|
350
|
+
{ name: "structural_semantic", fn: () => gateStructuralSemanticEdges(ws) },
|
|
351
|
+
{ name: "dedup_edges", fn: () => gateDedupEdges(ws) },
|
|
352
|
+
];
|
|
353
|
+
for (const gate of gates) {
|
|
354
|
+
gatesRun.push(gate.name);
|
|
355
|
+
const r = gate.fn();
|
|
356
|
+
if (r.ok) {
|
|
357
|
+
gatesPassed.push(gate.name);
|
|
358
|
+
}
|
|
359
|
+
if (r.reason) {
|
|
360
|
+
warnings.push({ gate: gate.name, code: "gate_warning", count: r.dropped || 1 });
|
|
361
|
+
}
|
|
362
|
+
// Track by category
|
|
363
|
+
if (["identity_merge", "promotion_guard", "node_completeness"].includes(gate.name)) {
|
|
364
|
+
totalDroppedNodes += r.dropped;
|
|
365
|
+
}
|
|
366
|
+
if (["dangling_edges", "edge_thresholds", "dedup_redundant", "structural_semantic", "dedup_edges"].includes(gate.name)) {
|
|
367
|
+
totalDroppedEdges += r.dropped;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
ws,
|
|
372
|
+
stats: {
|
|
373
|
+
gatesRun,
|
|
374
|
+
gatesPassed,
|
|
375
|
+
dropped: { nodes: totalDroppedNodes, edges: totalDroppedEdges },
|
|
376
|
+
warnings,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|