pi-mega-compact 0.11.8 → 0.11.10
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/mega-config.js +1 -0
- package/dist/extensions/mega-events/agent-handlers.js +74 -5
- package/dist/extensions/mega-events/context-handler.js +68 -4
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-trim.js +10 -0
- package/dist/src/config/dedup.js +1 -0
- package/dist/src/dedup/raptor/incremental.js +276 -0
- package/dist/src/dedup/raptor/index.js +32 -8
- package/dist/src/dedup/raptor/summarizer.js +1 -1
- package/dist/src/httpEmbedder.js +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js → AchievementTiles-DFitrb4Y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BESF1HT1.js.map → AchievementTiles-DFitrb4Y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.js → AchievementsTab-PJrmq8pa.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-8RzNnz8u.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-D85fB9HP.js → ConfigTab-Dx-RpO8C.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-D85fB9HP.js.map → ConfigTab-Dx-RpO8C.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js → EventsTab-C63BEWEs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-TCLR81G5.js.map → EventsTab-C63BEWEs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.js → GameTab-DHpJE6km.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-BqneNRYH.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-CpjMQwRr.js → MemoryMapTab-Cg1fgEx_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CpjMQwRr.js.map → MemoryMapTab-Cg1fgEx_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js → MetricsTab-DCahxNOQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-LxNtNzLQ.js.map → MetricsTab-DCahxNOQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js → OverviewTab-RYvO1SZL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BXKUZbT6.js.map → OverviewTab-RYvO1SZL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js → ReposTab-BDGr_UXb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DTooKeAi.js.map → ReposTab-BDGr_UXb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js → SessionsTab-ByrTnHaB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Devulgm9.js.map → SessionsTab-ByrTnHaB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js → SetupTab-CELafKis.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-By9eqUoE.js.map → SetupTab-CELafKis.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js → TimeSavedCard-BLWtmfr_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-z4M8TDLm.js.map → TimeSavedCard-BLWtmfr_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js → TopicsTab-8Fsh_Igx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-bCGZSfLN.js.map → TopicsTab-8Fsh_Igx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js → TurnsTab-D7mgXDe2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-m_XzE0H4.js.map → TurnsTab-D7mgXDe2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js → index-h3jgEs6G.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-PRkilPXw.js.map → index-h3jgEs6G.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.js → useSSE-C5lmipBF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-CGzgc2IK.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/tabs/CacheTab.tsx +29 -2
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +101 -0
- package/extensions/dashboard-server/api-contracts/index.ts +1 -0
- package/extensions/dashboard-server/api-contracts/maintenance.ts +13 -0
- 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/mega-config.ts +4 -0
- package/extensions/mega-events/agent-handlers.ts +88 -5
- package/extensions/mega-events/context-handler.ts +73 -4
- package/extensions/mega-runtime/snapshot.ts +1 -1
- package/extensions/mega-trim.ts +15 -0
- package/package.json +1 -1
- package/src/config/dedup.ts +2 -0
- package/src/dedup/raptor/incremental.ts +339 -0
- package/src/dedup/raptor/index.ts +33 -8
- package/src/dedup/raptor/summarizer.ts +1 -1
- package/src/httpEmbedder.ts +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-CUIiUgh3.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-LblMg3NB.js.map +0 -1
|
@@ -39,7 +39,21 @@ import {
|
|
|
39
39
|
} from "../mega-config.js";
|
|
40
40
|
import { appendMirrorMessages } from "./mirror-append.js";
|
|
41
41
|
import { stagedForTail, withRecallTail } from "./recall-tail.js";
|
|
42
|
-
import { buildSeparatedPrompt } from "./separated-prompt.js";
|
|
42
|
+
import { buildSeparatedPrompt, buildCacheOptimizedPrompt } from "./separated-prompt.js";
|
|
43
|
+
|
|
44
|
+
/** Best-effort text extraction from an AgentMessage for analytics/logging.
|
|
45
|
+
* AgentMessage is a discriminated union; .content exists only on some
|
|
46
|
+
* variants (user/assistant/toolResult/custom). Narrow by role, never assume. */
|
|
47
|
+
function messageContentText(m: AgentMessage): string {
|
|
48
|
+
const c = (m as { content?: unknown }).content;
|
|
49
|
+
if (typeof c === "string") return c;
|
|
50
|
+
if (Array.isArray(c)) {
|
|
51
|
+
return c
|
|
52
|
+
.map((b) => (b && typeof b === "object" && "text" in b ? String((b as { text?: string }).text ?? "") : ""))
|
|
53
|
+
.join(" ");
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
43
57
|
|
|
44
58
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
45
59
|
export function registerContextHandler(
|
|
@@ -73,13 +87,33 @@ export function registerContextHandler(
|
|
|
73
87
|
// reaches raw_transcript (PREVENT-PI: append-only, view-only).
|
|
74
88
|
const tailResult = (msgs?: readonly AgentMessage[]) => {
|
|
75
89
|
const base = msgs ?? messages;
|
|
76
|
-
if (!stagedForTail(runtime, config) && !config.messageSeparation) return undefined;
|
|
90
|
+
if (!stagedForTail(runtime, config) && !config.messageSeparation && !config.cacheStriping) return undefined;
|
|
77
91
|
let result: AgentMessage[] = stagedForTail(runtime, config)
|
|
78
92
|
? withRecallTail(base, runtime, config)
|
|
79
93
|
: (base as AgentMessage[]);
|
|
80
|
-
if (config.
|
|
94
|
+
if (config.cacheStriping) {
|
|
95
|
+
result = buildCacheOptimizedPrompt(result);
|
|
96
|
+
} else if (config.messageSeparation) {
|
|
81
97
|
result = buildSeparatedPrompt(result);
|
|
82
98
|
}
|
|
99
|
+
// P2.5: log prefix stability (fire-and-forget, non-fatal).
|
|
100
|
+
// tailResult is sync, so use .then().catch() on the dynamic import.
|
|
101
|
+
if (result.length > 1) {
|
|
102
|
+
import("../../src/cache-stripe-impl.js").then(({ computeStabilityScore }) => {
|
|
103
|
+
const stableScore = computeStabilityScore(
|
|
104
|
+
{ content: messageContentText(result[0] ?? result[0]), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 },
|
|
105
|
+
result.slice(0, 2).map((m) => ({ content: messageContentText(m), chunkId: "prefix", accessCount: 0, lastAccessedAt: 0 })),
|
|
106
|
+
);
|
|
107
|
+
runtime.logger.info("prefix_stability", {
|
|
108
|
+
stableScore: Number.isFinite(stableScore) ? stableScore : 0,
|
|
109
|
+
prefixMessages: result.length,
|
|
110
|
+
separation: config.messageSeparation ? "v2" : "off",
|
|
111
|
+
striping: config.cacheStriping ? "v3" : "off",
|
|
112
|
+
});
|
|
113
|
+
}).catch(() => {
|
|
114
|
+
// Non-fatal: stability logging is best-effort.
|
|
115
|
+
});
|
|
116
|
+
}
|
|
83
117
|
return { messages: result };
|
|
84
118
|
};
|
|
85
119
|
// Always track context for the dashboard/widget, even when auto is off.
|
|
@@ -129,6 +163,33 @@ export function registerContextHandler(
|
|
|
129
163
|
epochIdFor(runtime.rt.sessionId),
|
|
130
164
|
runtime.currentTurn,
|
|
131
165
|
);
|
|
166
|
+
// P2.2: populate conversation_thread + tool_results tables for
|
|
167
|
+
// prompt-cache analytics and durable separation. The live-array
|
|
168
|
+
// separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
|
|
169
|
+
// tailResult above) is sufficient for the prompt-construction path;
|
|
170
|
+
// these DB writes persist the split for post-hoc analysis, dashboard
|
|
171
|
+
// queries, and future readers. Non-fatal — failure here never breaks
|
|
172
|
+
// the agent loop (PREVENT-PI-004: zero network, local SQLite only).
|
|
173
|
+
{
|
|
174
|
+
const sid = runtime.rt.sessionId;
|
|
175
|
+
const turn = runtime.currentTurn;
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const threadStmt = db.prepare(
|
|
178
|
+
"INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
179
|
+
);
|
|
180
|
+
const toolStmt = db.prepare(
|
|
181
|
+
"INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
182
|
+
);
|
|
183
|
+
for (const m of messages) {
|
|
184
|
+
const role = m.role;
|
|
185
|
+
const content = messageContentText(m);
|
|
186
|
+
if (role === "user" || role === "assistant") {
|
|
187
|
+
threadStmt.run(sid, role, content, turn, now);
|
|
188
|
+
} else if (role === "toolResult" || role === "bashExecution") {
|
|
189
|
+
toolStmt.run(sid, role, content, turn, now);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
132
193
|
} catch (e) {
|
|
133
194
|
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
134
195
|
}
|
|
@@ -232,7 +293,7 @@ export function registerContextHandler(
|
|
|
232
293
|
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
233
294
|
runtime.trimCache.cut <= messages.length
|
|
234
295
|
) {
|
|
235
|
-
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002
|
|
296
|
+
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized by computeLiveTrimCut (src/boundary.ts); replayed verbatim, transcript only grows within an epoch.
|
|
236
297
|
runtime.diagLiveTrimFires++;
|
|
237
298
|
runtime.diagLiveTrimReplays++;
|
|
238
299
|
runtime.snapshot(ctx);
|
|
@@ -474,6 +535,13 @@ export function registerContextHandler(
|
|
|
474
535
|
compactedFrom: ran.result.compactedFrom,
|
|
475
536
|
summary: ran.result.summary,
|
|
476
537
|
anchorUserMessages,
|
|
538
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
|
|
539
|
+
// window, relief takes priority over the anchor floor. Without this,
|
|
540
|
+
// computeLiveTrimCut bails to null (can't satisfy the floor) and the
|
|
541
|
+
// model is fed a raw overflow that errors every turn — the
|
|
542
|
+
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
543
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
544
|
+
criticalOver: (pct ?? 0) >= 90,
|
|
477
545
|
});
|
|
478
546
|
if (cut === null) {
|
|
479
547
|
runtime.diagCtxCutNull++;
|
|
@@ -482,6 +550,7 @@ export function registerContextHandler(
|
|
|
482
550
|
compactedFrom: ran.result.compactedFrom,
|
|
483
551
|
viewLen: view.length,
|
|
484
552
|
anchorUserMessages,
|
|
553
|
+
criticalOver: (pct ?? 0) >= 90,
|
|
485
554
|
});
|
|
486
555
|
return tailResult() ?? undefined; // unsafe / below anchor floor — no trim this call
|
|
487
556
|
}
|
|
@@ -225,7 +225,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
|
|
|
225
225
|
cachePct,
|
|
226
226
|
megaCacheFlare: p.megaCacheFlare,
|
|
227
227
|
megaCacheFlarePct: p.megaCacheFlarePct,
|
|
228
|
-
perTurnCacheHitPct: p.perTurnCacheHitPct
|
|
228
|
+
perTurnCacheHitPct: p.perTurnCacheHitPct,
|
|
229
229
|
levelUpFlare: p.levelUpFlare,
|
|
230
230
|
achievementFlare: p.achievementFlare,
|
|
231
231
|
achievementFlareTitles: p.achievementFlareTitles,
|
package/extensions/mega-trim.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface BuildLiveTrimViewOpts {
|
|
|
25
25
|
summary: string;
|
|
26
26
|
/** Min recent user messages to keep as the anchor (PREVENT-PI-001). */
|
|
27
27
|
anchorUserMessages: number;
|
|
28
|
+
/** CRITICAL-OVER ESCAPE HATCH: when true, context is over ~90% of the window
|
|
29
|
+
* and relief takes priority over the anchor floor. computeLiveTrimCut returns
|
|
30
|
+
* the boundary-safe cut even if it keeps fewer than `anchorUserMessages` user
|
|
31
|
+
* messages, rather than bailing to null (which would feed the model a raw
|
|
32
|
+
* overflow). A compacted session with a thin anchor is recoverable; an
|
|
33
|
+
* overflowed session that errors every turn is not. Default false. */
|
|
34
|
+
criticalOver?: boolean;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
/**
|
|
@@ -68,9 +75,17 @@ export function computeLiveTrimCut(view: EngineMessage[], opts: BuildLiveTrimVie
|
|
|
68
75
|
if (cut > 1) {
|
|
69
76
|
const finalRecent = view.slice(cut);
|
|
70
77
|
if (finalRecent.filter((m) => m.role === "user").length < opts.anchorUserMessages) {
|
|
78
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is critically over the window,
|
|
79
|
+
// return the boundary-safe cut even though it's below the anchor floor.
|
|
80
|
+
// Bailing to null here would feed the model the raw overflow and trap the
|
|
81
|
+
// session in an unrecoverable error loop ("Already compacted" + overflow).
|
|
82
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
83
|
+
if (opts.criticalOver && cut > 0) return cut;
|
|
71
84
|
return null; // cannot satisfy the floor without dropping too much — retry next call
|
|
72
85
|
}
|
|
73
86
|
} else {
|
|
87
|
+
// Same escape hatch for the cut > 1 false branch.
|
|
88
|
+
if (opts.criticalOver && cut > 0) return cut;
|
|
74
89
|
return null;
|
|
75
90
|
}
|
|
76
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.10",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
package/src/config/dedup.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface DedupConfigShape {
|
|
|
66
66
|
RAPTOR_MULTILEVEL_ENABLED: boolean;
|
|
67
67
|
RAPTOR_LEVEL_WEIGHTS: number[]; // per-level weights, index 0 = leaves (uncalibrated)
|
|
68
68
|
RAPTOR_LEAF_EXPANSION: boolean;
|
|
69
|
+
RAPTOR_INCREMENTAL: boolean;
|
|
69
70
|
RAPTOR_MAX_LEAF_EXPANSION: number; // uncalibrated
|
|
70
71
|
RAPTOR_FRESHNESS_HOURS: number; // S42D: skip rebuild when tree is fresh (uncalibrated)
|
|
71
72
|
/** S25 Phase-2: inject top-level RAPTOR summary nodes (root + level-1
|
|
@@ -109,6 +110,7 @@ export function loadDedupConfig(): DedupConfigShape {
|
|
|
109
110
|
RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
|
|
110
111
|
RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
|
|
111
112
|
RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
|
|
113
|
+
RAPTOR_INCREMENTAL: envBool("MEGACOMPACT_RAPTOR_INCREMENTAL", true),
|
|
112
114
|
RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
|
|
113
115
|
RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
|
|
114
116
|
RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* incremental.ts — incremental RAPTOR tree update (Sprint 26, #7).
|
|
3
|
+
*
|
|
4
|
+
* Instead of rebuilding the full tree (~2s per compaction), insert only the new
|
|
5
|
+
* checkpoints since the last build and recompute only the affected cluster
|
|
6
|
+
* assignments up the tree. Falls back to a full rebuild when the tree is
|
|
7
|
+
* missing, corrupted, or >50% of nodes are new.
|
|
8
|
+
*
|
|
9
|
+
* Non-fatal: every failure logs and returns null (caller falls back to full
|
|
10
|
+
* rebuild). PREVENT-PI-004: zero network; all operations are local SQLite + CPU.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Embedder, Vector } from "../../embedder.js";
|
|
14
|
+
import { cosineSimilarity } from "../../embedder.js";
|
|
15
|
+
import { defaultEmbedder as getDefaultEmbedder } from "../../embedder.js";
|
|
16
|
+
import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
|
|
17
|
+
import type { QualityMarker } from "./guardrails.js";
|
|
18
|
+
import { meanVector } from "./kmeans.js";
|
|
19
|
+
import { summarizeCluster } from "./summarizer.js";
|
|
20
|
+
import type { Logger } from "../../log.js";
|
|
21
|
+
import {
|
|
22
|
+
saveRaptorTree,
|
|
23
|
+
listRaptorNodes,
|
|
24
|
+
type StoredRaptorNode,
|
|
25
|
+
} from "../../store/sqlite.js";
|
|
26
|
+
|
|
27
|
+
/** When >FRESH_THRESHOLD of existing leaves are new, a full rebuild is cheaper. */
|
|
28
|
+
const FRESH_THRESHOLD = 0.5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Incrementally update a persisted RAPTOR tree with new leaves.
|
|
32
|
+
*
|
|
33
|
+
* 1. Reads the existing tree from the store.
|
|
34
|
+
* 2. Filters `newLeaves` to those NOT already covered by the tree.
|
|
35
|
+
* 3. If no new leaves → early-return (existing tree).
|
|
36
|
+
* 4. If no existing tree or >50% new → full rebuild.
|
|
37
|
+
* 5. For each new leaf, finds the best-matching Level-0 cluster node,
|
|
38
|
+
* reassigns (inserts into its children), updates that node's summary
|
|
39
|
+
* and centroid embedding.
|
|
40
|
+
* 6. Propagates changes up through parent levels to the root.
|
|
41
|
+
* 7. Persists the updated tree (full delete + reinsert in a tx).
|
|
42
|
+
*
|
|
43
|
+
* Never throws. Returns the updated tree, or null on failure/fallback.
|
|
44
|
+
*/
|
|
45
|
+
export function incrementRaptorTree(
|
|
46
|
+
existingLeaves: Leaf[],
|
|
47
|
+
newLeaves: Leaf[],
|
|
48
|
+
opts: {
|
|
49
|
+
embedder?: Embedder;
|
|
50
|
+
stateDir: string;
|
|
51
|
+
sessionId: string;
|
|
52
|
+
budgetMs?: number;
|
|
53
|
+
clustersPerLevel?: number;
|
|
54
|
+
consistencyThreshold?: number;
|
|
55
|
+
logger?: Logger;
|
|
56
|
+
builtAt?: number;
|
|
57
|
+
},
|
|
58
|
+
): RaptorTree | null {
|
|
59
|
+
const logger = opts.logger;
|
|
60
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 1. Read existing tree.
|
|
64
|
+
const storedNodes = listRaptorNodes(opts.sessionId, opts.stateDir);
|
|
65
|
+
if (storedNodes.length === 0) {
|
|
66
|
+
logger?.info("raptor_incremental_no_tree", { sessionId: opts.sessionId });
|
|
67
|
+
return null; // no existing tree — caller falls back to full buildRaptorTree
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Find new leaf ids not already in the tree.
|
|
71
|
+
const coveredLeafIds = new Set<string>();
|
|
72
|
+
for (const n of storedNodes) {
|
|
73
|
+
for (const cid of n.children) coveredLeafIds.add(cid);
|
|
74
|
+
}
|
|
75
|
+
const reallyNewLeaves = newLeaves.filter(
|
|
76
|
+
(l) => !coveredLeafIds.has(l.id),
|
|
77
|
+
);
|
|
78
|
+
const totalCovered = coveredLeafIds.size;
|
|
79
|
+
|
|
80
|
+
if (reallyNewLeaves.length === 0) {
|
|
81
|
+
logger?.info("raptor_incremental_no_new", { sessionId: opts.sessionId });
|
|
82
|
+
return rehydrateFromStored(storedNodes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Fallback guard: >50% new → full rebuild.
|
|
86
|
+
if (
|
|
87
|
+
totalCovered > 0 &&
|
|
88
|
+
reallyNewLeaves.length / totalCovered > FRESH_THRESHOLD
|
|
89
|
+
) {
|
|
90
|
+
logger?.info("raptor_incremental_fallback_ratio", {
|
|
91
|
+
sessionId: opts.sessionId,
|
|
92
|
+
coveredNodes: totalCovered,
|
|
93
|
+
newLeaves: reallyNewLeaves.length,
|
|
94
|
+
threshold: FRESH_THRESHOLD,
|
|
95
|
+
});
|
|
96
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Build the in-memory tree from stored nodes.
|
|
100
|
+
const tree = rehydrateFromStored(storedNodes);
|
|
101
|
+
if (!tree || tree.nodes.size === 0) {
|
|
102
|
+
logger?.info("raptor_incremental_rehydrate_failed", {
|
|
103
|
+
sessionId: opts.sessionId,
|
|
104
|
+
});
|
|
105
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Group stored nodes by level.
|
|
109
|
+
const levelGroups = new Map<number, StoredRaptorNode[]>();
|
|
110
|
+
for (const n of storedNodes) {
|
|
111
|
+
const g = levelGroups.get(n.level);
|
|
112
|
+
if (g) g.push(n);
|
|
113
|
+
else levelGroups.set(n.level, [n]);
|
|
114
|
+
}
|
|
115
|
+
// Level-0 covers leaf ids directly.
|
|
116
|
+
const level0Nodes =
|
|
117
|
+
levelGroups.get(0) ??
|
|
118
|
+
storedNodes.filter((n) => {
|
|
119
|
+
// A node is level 0 if its level is the minimum.
|
|
120
|
+
const minLevel = Math.min(...storedNodes.map((x) => x.level));
|
|
121
|
+
return n.level === minLevel;
|
|
122
|
+
});
|
|
123
|
+
if (level0Nodes.length === 0) {
|
|
124
|
+
// Single-root tree with no hierarchy: cannot increment, fall back.
|
|
125
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// For each new leaf, assign to the nearest Level-0 cluster node.
|
|
129
|
+
const leafEmbeddings = new Map<string, Vector>();
|
|
130
|
+
for (const l of reallyNewLeaves) leafEmbeddings.set(l.id, l.embedding);
|
|
131
|
+
|
|
132
|
+
// Track which Level-0 nodes changed (by their stored id).
|
|
133
|
+
const affectedNodeIds = new Set<string>();
|
|
134
|
+
|
|
135
|
+
for (const leaf of reallyNewLeaves) {
|
|
136
|
+
const emb = leafEmbeddings.get(leaf.id);
|
|
137
|
+
if (!emb || emb.length === 0) continue;
|
|
138
|
+
|
|
139
|
+
// Find best matching Level-0 node.
|
|
140
|
+
let bestNode: StoredRaptorNode | null = null;
|
|
141
|
+
let bestSim = -Infinity;
|
|
142
|
+
for (const cn of level0Nodes) {
|
|
143
|
+
const sim = cosineSimilarity(emb, cn.embedding);
|
|
144
|
+
if (sim > bestSim) {
|
|
145
|
+
bestSim = sim;
|
|
146
|
+
bestNode = cn;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!bestNode) continue;
|
|
150
|
+
|
|
151
|
+
// Add the leaf id to this node's children.
|
|
152
|
+
const updated = tree.nodes.get(bestNode.id);
|
|
153
|
+
if (updated) {
|
|
154
|
+
if (!updated.children.includes(leaf.id)) {
|
|
155
|
+
updated.children.push(leaf.id);
|
|
156
|
+
}
|
|
157
|
+
affectedNodeIds.add(bestNode.id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (affectedNodeIds.size === 0) {
|
|
162
|
+
// No matches found — fall back to full rebuild.
|
|
163
|
+
logger?.info("raptor_incremental_no_matches", {
|
|
164
|
+
sessionId: opts.sessionId,
|
|
165
|
+
});
|
|
166
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 6. Recompute embeddings and summaries for affected nodes, bottom-up.
|
|
170
|
+
// Build a map: leafId → leaf info for summarization.
|
|
171
|
+
const leafMap = new Map<string, Leaf>();
|
|
172
|
+
for (const l of existingLeaves) leafMap.set(l.id, l);
|
|
173
|
+
for (const l of reallyNewLeaves) leafMap.set(l.id, l);
|
|
174
|
+
|
|
175
|
+
// Recompute affected Level-0 nodes.
|
|
176
|
+
for (const nid of affectedNodeIds) {
|
|
177
|
+
const node = tree.nodes.get(nid);
|
|
178
|
+
if (!node) continue;
|
|
179
|
+
const coveredLeaves = node.children
|
|
180
|
+
.map((cid) => leafMap.get(cid))
|
|
181
|
+
.filter((l): l is Leaf => !!l);
|
|
182
|
+
if (coveredLeaves.length > 0) {
|
|
183
|
+
node.embedding = meanVector(
|
|
184
|
+
coveredLeaves.map((l) => l.embedding),
|
|
185
|
+
);
|
|
186
|
+
// Summarize: flatten messages from all covered leaves.
|
|
187
|
+
const messages = coveredLeaves.flatMap((l) => l.messages);
|
|
188
|
+
if (messages.length > 0) {
|
|
189
|
+
const cs = summarizeCluster(messages);
|
|
190
|
+
node.summary = cs.summary;
|
|
191
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Propagate up: for each higher level, recompute nodes whose leaf set
|
|
197
|
+
// includes any newly inserted leaf. Since `children` stores flattened leaf
|
|
198
|
+
// ids (not node ids), we check intersection with the new leaf id set.
|
|
199
|
+
const newLeafIds = new Set(reallyNewLeaves.map((l) => l.id));
|
|
200
|
+
const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b);
|
|
201
|
+
for (const level of sortedLevels.slice(1)) {
|
|
202
|
+
const levelNodes = levelGroups.get(level) ?? [];
|
|
203
|
+
for (const stored of levelNodes) {
|
|
204
|
+
const node = tree.nodes.get(stored.id);
|
|
205
|
+
if (!node) continue;
|
|
206
|
+
|
|
207
|
+
if (node.children.some((cid) => newLeafIds.has(cid))) {
|
|
208
|
+
// Recompute embedding as centroid of covered leaves.
|
|
209
|
+
const covered = node.children
|
|
210
|
+
.map((cid) => leafMap.get(cid))
|
|
211
|
+
.filter((l): l is Leaf => !!l);
|
|
212
|
+
if (covered.length > 0) {
|
|
213
|
+
node.embedding = meanVector(
|
|
214
|
+
covered.map((l) => l.embedding),
|
|
215
|
+
);
|
|
216
|
+
const messages = covered.flatMap((l) => l.messages);
|
|
217
|
+
if (messages.length > 0) {
|
|
218
|
+
const cs = summarizeCluster(messages);
|
|
219
|
+
node.summary = cs.summary;
|
|
220
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 7. Persist the full updated tree (delete + reinsert in a tx).
|
|
228
|
+
tree.builtAt = builtAt;
|
|
229
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
230
|
+
|
|
231
|
+
logger?.info("raptor_incremental_success", {
|
|
232
|
+
sessionId: opts.sessionId,
|
|
233
|
+
nodeCount: tree.nodes.size,
|
|
234
|
+
newLeaves: reallyNewLeaves.length,
|
|
235
|
+
affectedNodes: affectedNodeIds.size,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return tree;
|
|
239
|
+
} catch (e) {
|
|
240
|
+
logger?.error("raptor_incremental_failed", {
|
|
241
|
+
sessionId: opts.sessionId,
|
|
242
|
+
error: String(e instanceof Error ? e.message : e),
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Helpers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Full rebuild fallback: merges existingLeaves + newLeaves, builds from scratch.
|
|
254
|
+
*/
|
|
255
|
+
function fullRebuild(
|
|
256
|
+
existingLeaves: Leaf[],
|
|
257
|
+
newLeaves: Leaf[],
|
|
258
|
+
opts: {
|
|
259
|
+
embedder?: Embedder;
|
|
260
|
+
budgetMs?: number;
|
|
261
|
+
clustersPerLevel?: number;
|
|
262
|
+
consistencyThreshold?: number;
|
|
263
|
+
stateDir: string;
|
|
264
|
+
sessionId: string;
|
|
265
|
+
logger?: Logger;
|
|
266
|
+
builtAt?: number;
|
|
267
|
+
},
|
|
268
|
+
logger?: Logger,
|
|
269
|
+
): RaptorTree | null {
|
|
270
|
+
logger?.info("raptor_incremental_full_rebuild", {
|
|
271
|
+
sessionId: opts.sessionId,
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
// Merge: deduplicate by leaf id.
|
|
275
|
+
const seen = new Set<string>();
|
|
276
|
+
const allLeaves: Leaf[] = [];
|
|
277
|
+
for (const l of existingLeaves) {
|
|
278
|
+
if (!seen.has(l.id)) {
|
|
279
|
+
seen.add(l.id);
|
|
280
|
+
allLeaves.push(l);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const l of newLeaves) {
|
|
284
|
+
if (!seen.has(l.id)) {
|
|
285
|
+
seen.add(l.id);
|
|
286
|
+
allLeaves.push(l);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const tree = buildRaptorTree(allLeaves, {
|
|
290
|
+
embedder: opts.embedder ?? getDefaultEmbedder(),
|
|
291
|
+
budgetMs: opts.budgetMs,
|
|
292
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
293
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
294
|
+
});
|
|
295
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
296
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
297
|
+
return tree;
|
|
298
|
+
} catch (e2) {
|
|
299
|
+
logger?.error("raptor_incremental_full_rebuild_failed", {
|
|
300
|
+
sessionId: opts.sessionId,
|
|
301
|
+
error: String(e2 instanceof Error ? e2.message : e2),
|
|
302
|
+
});
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Rebuild the in-memory RaptorTree from stored nodes. */
|
|
308
|
+
function rehydrateFromStored(
|
|
309
|
+
nodes: StoredRaptorNode[],
|
|
310
|
+
): RaptorTree | null {
|
|
311
|
+
if (nodes.length === 0) return null;
|
|
312
|
+
const root = nodes.reduce<StoredRaptorNode | null>(
|
|
313
|
+
(best, n) => (!best || n.level > (best?.level ?? -1) ? n : best),
|
|
314
|
+
null,
|
|
315
|
+
);
|
|
316
|
+
const tree: RaptorTree = {
|
|
317
|
+
nodes: new Map(
|
|
318
|
+
nodes.map((n) => [
|
|
319
|
+
n.id,
|
|
320
|
+
{
|
|
321
|
+
id: n.id,
|
|
322
|
+
level: n.level,
|
|
323
|
+
parentId: n.parentId,
|
|
324
|
+
children: n.children,
|
|
325
|
+
summary: n.summary,
|
|
326
|
+
embedding: n.embedding,
|
|
327
|
+
qualityMarker: n.qualityMarker as QualityMarker,
|
|
328
|
+
tokenEstimate: n.tokenEstimate,
|
|
329
|
+
},
|
|
330
|
+
]),
|
|
331
|
+
),
|
|
332
|
+
rootId: root?.id ?? null,
|
|
333
|
+
levels: Math.max(1, ...nodes.map((n) => n.level + 1)),
|
|
334
|
+
timedOut: root != null && root.level >= 99,
|
|
335
|
+
builtAt: nodes.reduce((max, n) => Math.max(max, n.builtAt), 0),
|
|
336
|
+
};
|
|
337
|
+
return tree;
|
|
338
|
+
}
|
|
339
|
+
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import type { Embedder } from "../../embedder.js";
|
|
14
14
|
import { defaultEmbedder } from "../../embedder.js";
|
|
15
15
|
import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
|
|
16
|
+
import { incrementRaptorTree } from "./incremental.js";
|
|
16
17
|
import { stagedExpansion } from "./retrieval.js";
|
|
17
18
|
import { Logger } from "../../log.js";
|
|
18
19
|
import { saveRaptorTree, listRaptorNodes } from "../../store/sqlite.js";
|
|
19
20
|
import { insertBuildHistory, computeCoherenceScore } from "./buildHistory.js";
|
|
21
|
+
import { loadDedupConfig } from "../../config/dedup.js";
|
|
20
22
|
|
|
21
23
|
/** Shadow mode is on by default; set RAPTOR_SHADOW_MODE=false to serve live. */
|
|
22
24
|
export function isShadowMode(): boolean {
|
|
@@ -51,15 +53,38 @@ export function runRaptor(
|
|
|
51
53
|
const embedder = opts.embedder ?? defaultEmbedder();
|
|
52
54
|
const logger = opts.logger;
|
|
53
55
|
const startedAt = opts.builtAt ?? Date.now();
|
|
56
|
+
const builtAt = startedAt;
|
|
54
57
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
// Try incremental update first when the flag is ON (Sprint 26, #7).
|
|
59
|
+
// The incremental path reads the existing tree, diffs by leaf id, and
|
|
60
|
+
// inserts only new leaves into the nearest clusters. Falls back to a full
|
|
61
|
+
// rebuild when the tree is missing, corrupted, or >50% of nodes are new.
|
|
62
|
+
const cfg = loadDedupConfig();
|
|
63
|
+
let tree: RaptorTree | null = null;
|
|
64
|
+
if (cfg.RAPTOR_INCREMENTAL) {
|
|
65
|
+
tree = incrementRaptorTree(leaves, leaves, {
|
|
66
|
+
embedder,
|
|
67
|
+
stateDir: opts.stateDir,
|
|
68
|
+
sessionId: opts.sessionId,
|
|
69
|
+
budgetMs: opts.budgetMs,
|
|
70
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
71
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
72
|
+
logger,
|
|
73
|
+
builtAt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fall back to full rebuild when incremental returned null (not attempted,
|
|
78
|
+
// tree missing, ratio exceeded, or internal error).
|
|
79
|
+
if (!tree) {
|
|
80
|
+
tree = buildRaptorTree(leaves, {
|
|
81
|
+
embedder,
|
|
82
|
+
budgetMs: opts.budgetMs,
|
|
83
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
84
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
85
|
+
});
|
|
86
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
87
|
+
}
|
|
63
88
|
logger?.info("raptor_build", {
|
|
64
89
|
sessionId: opts.sessionId,
|
|
65
90
|
nodes: tree.nodes.size,
|
|
@@ -24,7 +24,7 @@ export interface ClusterSummary {
|
|
|
24
24
|
function ollamaEndpoint(): { url: string; model: string } | null {
|
|
25
25
|
const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
|
|
26
26
|
if (!model) return null;
|
|
27
|
-
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434";
|
|
27
|
+
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434"; // guardrails-allow PREVENT-PI-004: loopback-only Ollama endpoint for local RAPTOR summarization
|
|
28
28
|
// Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
|
|
29
29
|
if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
|
|
30
30
|
throw new Error(
|
package/src/httpEmbedder.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import type { Embedder, Vector } from "./embedder.js";
|
|
29
29
|
import { l2Normalize } from "./embedder.js";
|
|
30
30
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
31
|
-
import { isIP } from "node:net";
|
|
31
|
+
import { isIP } from "node:net"; // guardrails-allow PREVENT-PI-004: localhost-only loopback address validation in BYO embedding server
|
|
32
32
|
|
|
33
33
|
export interface HttpEmbedderOptions {
|
|
34
34
|
url: string;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{j as e,u as x,r as p,k as T,l as R,i as $,c as y}from"./index-PRkilPXw.js";import{C as L,T as P}from"./TimeSavedCard-z4M8TDLm.js";import{b as d,c as h,d as H,e as M,f as i}from"./format-COjDIVGj.js";import{l as I}from"./pricing-CvE4NllQ.js";function f(a){return a>=80?"ov-stat-value-green":a>=50?"ov-stat-value-yellow":"ov-stat-value-red"}function k(a){return a==null?"":a>=0?"ov-stat-value-green":"ov-stat-value-red"}function r({label:a,value:t,className:l,title:s}){return e.jsxs("div",{className:"ov-stat-row",children:[e.jsx("span",{className:"ov-stat-label",title:s,children:a}),e.jsx("span",{className:`ov-stat-value${l?` ${l}`:""}`,children:t})]})}function E(a){return e.jsxs("div",{className:"card provider-cache-card",children:[e.jsx(r,{label:"Cache Hit Rate",value:`${a.hitPct.toFixed(1)}%`,className:f(a.hitPct)}),e.jsx(r,{label:"Turns Recorded",value:String(a.turnCount)}),e.jsx(r,{label:"Cache Read Tokens",value:d(a.totalCacheRead)}),e.jsx(r,{label:"Cache Write Tokens",value:d(a.totalCacheWrite)}),e.jsx(r,{label:"Input Tokens",value:d(a.totalInput)}),e.jsx(r,{label:"$ Saved (reads)",value:h(a.cacheReadSaved)}),e.jsx(r,{label:"Write Investment",value:h(a.cacheWriteCost)}),e.jsx(r,{label:"Net Saved",value:h(a.netSaved),className:k(a.netSaved)}),e.jsx(r,{label:"Model",value:a.modelLabel??"—"}),e.jsx(r,{label:"Tracked Since",value:H(a.firstTurnAt)}),e.jsx(r,{label:"Last Updated",value:M(a.latestTurnAt)}),a.byModel.length>0&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-stat-section-label",children:"Per Model"}),a.byModel.map(t=>{const l=I(t.model),s=l!=null?t.totalCacheRead/1e6*l:null,c=l!=null?t.totalCacheWrite/1e6*l:null;return e.jsxs("div",{className:"ov-stat-group",children:[e.jsx(r,{label:"Model",value:t.model}),e.jsx(r,{label:"Hit Rate",value:`${t.hitPct.toFixed(1)}%`,className:f(t.hitPct)}),e.jsx(r,{label:"Cache Read",value:d(t.totalCacheRead)}),e.jsx(r,{label:"Cache Write",value:d(t.totalCacheWrite)}),e.jsx(r,{label:"Samples",value:String(t.sampleCount)}),e.jsx(r,{label:"$ Saved (reads)",value:s!=null?h(s):"—"}),e.jsx(r,{label:"Write Investment",value:c!=null?h(c):"—"})]},t.model)})]})]})}const W=["ov-bar-green","ov-bar-blue","ov-bar-teal","ov-bar-yellow","ov-bar-gray"],w=["Permanent","Epoch","Topic","Thread","Volatile"],A={good:"ov-text-green",fair:"ov-text-yellow",degraded:"ov-text-orange",poor:"ov-text-red"};function D({data:a}){const t=Math.max(...a.buckets.map(s=>s.count),1),l=A[a.health.label]??"ov-text-gray";return e.jsxs("div",{className:"ov-card",children:[e.jsx("h3",{className:"ov-card-title",children:"Cache Stripe Distribution"}),e.jsxs("div",{className:"ov-stripe-health-summary",children:[e.jsx("span",{className:`ov-stripe-health-badge ${l}`,children:a.health.label}),e.jsx("span",{className:"ov-stripe-health-score",children:i(a.health.score)}),e.jsxs("span",{className:"ov-stripe-churn",children:["Churn: ",i(a.health.churnRate)]}),e.jsxs("span",{className:"ov-stripe-total",children:[a.totalChunks," total chunks"]})]}),e.jsx("div",{className:"ov-stripe-bars",children:a.buckets.map(s=>{const c=t>0?Math.max(0,Math.min(100,s.count/t*100)):0,o=W[s.stripe]??"ov-bar-gray",n=s.label||(w[s.stripe]??`Stripe ${s.stripe}`);return e.jsxs("div",{className:"ov-stripe-row",children:[e.jsx("span",{className:"ov-stripe-label",title:n,children:n}),e.jsx("div",{className:"ov-bar-track",children:e.jsx("div",{className:`ov-bar-fill ${o}`,style:{width:`${c}%`}})}),e.jsx("span",{className:"ov-stripe-count",children:s.count}),e.jsx("span",{className:"ov-stripe-stability",title:`min ${i(s.minStability)} / max ${i(s.maxStability)}`,children:i(s.avgStability)})]},s.stripe)})}),e.jsxs("div",{className:"ov-stripe-footer",children:["Epoch: ",a.epochId??"—","· Dominant tier: ",i(a.health.dominantTier)]})]})}function v(a){return`${a.toFixed(1)}%`}function F(a){return a>=80?"ov-bar-green":a>=60?"ov-bar-teal":a>=40?"ov-bar-yellow":a>=20?"ov-bar-orange":"ov-bar-red"}const _=({perf:a})=>{const{cache_hit_pct:t}=a,l=t.samples&&t.samples.length>0?t.samples:[];return e.jsxs("div",{className:"ov-card cache-trend-card",children:[e.jsx("h3",{className:"ov-card-title",children:"Cache Hit-Rate Trend"}),e.jsxs("div",{className:"ov-perf-summary",children:[e.jsxs("div",{className:"ov-perf-stat",children:["Avg: ",e.jsx("strong",{children:v(t.avg)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Latest: ",e.jsx("strong",{children:v(t.latest)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Samples: ",e.jsx("strong",{children:t.n})]})]}),l.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-sparkline",role:"img","aria-label":`Cache hit rate trend: ${t.n} samples, average ${v(t.avg)}, latest ${v(t.latest)}`,children:l.map((s,c)=>{const o=Math.max(2,Math.round(s.pct*.6)),n=F(s.pct),b=c===l.length-1;return e.jsx("div",{className:`ov-sparkline-bar ${n}${b?" ov-bar-latest":""}`,style:{height:`${o}px`},title:`${v(s.pct)} at ${new Date(s.ts).toLocaleTimeString()}`},c)})}),e.jsxs("div",{className:"ov-card-footer",children:[l.length," sample",l.length!==1?"s":""," over"," ",l.length>1?`${Math.round((l[l.length-1].ts-l[0].ts)/1e3)}s`:"latest turn"]})]}):e.jsx("div",{className:"ov-empty-state",children:"No cache-hit samples yet"})]})};function q(){const{data:a,loading:t,error:l}=x(p.useCallback(()=>T(),[]),{pollInterval:5e3}),{data:s,loading:c,error:o}=x(p.useCallback(()=>R(),[]),{pollInterval:15e3}),{data:n,loading:b,error:j}=x(p.useCallback(()=>$({minutes:30}),[]),{pollInterval:15e3}),{data:g,loading:N,error:C}=x(p.useCallback(()=>y(),[]),{pollInterval:5e3});return e.jsxs("div",{className:"cache-tab",children:[e.jsx("h2",{children:"Provider Prompt Cache"}),t?e.jsx("p",{className:"tab-stub",children:"Loading..."}):l?e.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",l.message]}):a?e.jsx(E,{hitPct:a.cache.avgHitPct,turnCount:a.cache.turnCount,totalCacheRead:a.cache.totalCacheRead,totalCacheWrite:a.cache.totalCacheWrite,totalInput:a.cache.totalInput,cacheReadSaved:a.savings?.cacheReadSaved??null,cacheWriteCost:a.savings?.cacheWriteCost??null,netSaved:a.savings?.netSaved??null,modelLabel:a.savings?.model??null,firstTurnAt:a.cache.firstTurnAt,latestTurnAt:a.cache.latestTurnAt,byModel:a.cache.byModel??[]}):null,e.jsx("h2",{children:"Cache Stripe Distribution"}),c?e.jsx("p",{className:"tab-stub",children:"Loading..."}):o?e.jsxs("p",{className:"tab-stub",children:["Cache stripe data unavailable: ",o.message]}):s?e.jsx(D,{data:s}):null,e.jsx("h2",{children:"Cache Hit-Rate Trend"}),b?e.jsx("p",{className:"tab-stub",children:"Loading..."}):j?e.jsxs("p",{className:"tab-stub",children:["Hit-rate trend unavailable: ",j.message]}):n?e.jsx(_,{perf:n}):null,e.jsx("h2",{children:"Mega-Compact Dedup Cache"}),N?e.jsx("p",{className:"tab-stub",children:"Loading..."}):C?e.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",C.message]}):g&&(()=>{const{cacheHits:u,compacts:S,timeSaved:m}=g;return e.jsxs("div",{className:"card-grid overview-card-grid",children:[e.jsx(L,{cacheHitsSession:u.session,cacheHitsTotal:u.total,tokensSavedSession:u.sessionTokensSaved,tokensSavedTotal:u.totalTokensSaved,compactionsSession:S.session,compactionsTotal:S.total}),e.jsx(P,{compactSessionSec:m.compact.sessionSec,compactTotalSec:m.compact.totalSec,cacheHitSessionSec:m.cacheHit.sessionSec,cacheHitTotalSec:m.cacheHit.totalSec})]})})()]})}export{q as default};
|
|
2
|
-
//# sourceMappingURL=CacheTab-CUIiUgh3.js.map
|