pi-mega-compact 0.13.7 → 0.14.1

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.
Files changed (86) hide show
  1. package/README.md +13 -6
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +6 -0
  3. package/dist/extensions/dashboard-server/index-reader.js +49 -26
  4. package/dist/extensions/dashboard-server/routes-cache.js +17 -5
  5. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +7 -0
  6. package/dist/extensions/dashboard-server/routes-raptor.js +60 -0
  7. package/dist/extensions/dashboard-server/routes.js +1 -1
  8. package/dist/extensions/dashboard-server/server.js +3 -1
  9. package/dist/extensions/mega-pipeline/recall.js +24 -16
  10. package/dist/extensions/mega-runtime/capture-model.js +15 -2
  11. package/dist/extensions/mega-runtime/runtime-snapshot.js +26 -0
  12. package/dist/extensions/mega-runtime/runtime.js +1 -0
  13. package/dist/src/config/dedup.js +2 -1
  14. package/dist/src/costApi.js +113 -0
  15. package/dist/src/dedup/raptor/incremental.js +31 -3
  16. package/dist/src/dedup/raptor/index.js +10 -6
  17. package/dist/src/vector-search.js +3 -3
  18. package/extensions/dashboard-client/dist/assets/{AreaChart-j2VOSRp6.js → AreaChart-BUUketyS.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{AreaChart-j2VOSRp6.js.map → AreaChart-BUUketyS.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{CacheTab-Dr1HeMfc.js → CacheTab-VNFZSdRr.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{CacheTab-Dr1HeMfc.js.map → CacheTab-VNFZSdRr.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-DRG2k7FS.js → EventsTab-Dr7RDBy2.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-DRG2k7FS.js.map → EventsTab-Dr7RDBy2.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{HealthTab-DGE9PpW3.js → HealthTab-B8VZ8gbb.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{HealthTab-DGE9PpW3.js.map → HealthTab-B8VZ8gbb.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DeYRhF4q.js → MaintenanceTab-DMtrXRcn.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DeYRhF4q.js.map → MaintenanceTab-DMtrXRcn.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/MemoryMapTab-PFrMdLOm.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DSnSO3nG.js.map → MemoryMapTab-PFrMdLOm.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-RIzNc1Uj.js → MetricsTab-Dd1kXuvg.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-RIzNc1Uj.js.map → MetricsTab-Dd1kXuvg.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-_1BJ4Bmh.js → OverviewTab-DOpObVA9.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-_1BJ4Bmh.js.map → OverviewTab-DOpObVA9.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DkU7ywZB.js → ReposTab-D6pEUtEJ.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DkU7ywZB.js.map → ReposTab-D6pEUtEJ.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-Bq_F_aOk.js → SessionsTab-CcFzDJ7W.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-Bq_F_aOk.js.map → SessionsTab-CcFzDJ7W.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-BkBD2Y97.js → SetupTab-YCt5NGB5.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-BkBD2Y97.js.map → SetupTab-YCt5NGB5.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BXzFKU7C.js → TimeSavedCard-BsLNhOOj.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BXzFKU7C.js.map → TimeSavedCard-BsLNhOOj.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{TurnsTab-Bjpp3vBH.js → TurnsTab-C1jpVZrH.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{TurnsTab-Bjpp3vBH.js.map → TurnsTab-C1jpVZrH.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{WikiTab-dBN2EA5G.js → WikiTab-RHoVt95g.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{WikiTab-dBN2EA5G.js.map → WikiTab-RHoVt95g.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{button-Bt0b5QVp.js → button-DSZI4aZ5.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{button-Bt0b5QVp.js.map → button-DSZI4aZ5.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{card-D5CafXtg.js → card-CsZzIyPW.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{card-D5CafXtg.js.map → card-CsZzIyPW.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{index-GPHRnEN7.js → index-B1_J_Y0b.js} +11 -11
  51. package/extensions/dashboard-client/dist/assets/index-B1_J_Y0b.js.map +1 -0
  52. package/extensions/dashboard-client/dist/assets/index-C5TGTOd6.css +1 -0
  53. package/extensions/dashboard-client/dist/assets/{switch-CY5Gwj1o.js → switch-DOIA1Nxu.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{switch-CY5Gwj1o.js.map → switch-DOIA1Nxu.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{toggle-C9HpkIfH.js → toggle-CyS6PHCO.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{toggle-C9HpkIfH.js.map → toggle-CyS6PHCO.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{useSSE-2JFrLDlE.js → useSSE-CY7UIUzm.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{useSSE-2JFrLDlE.js.map → useSSE-CY7UIUzm.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/index.html +2 -2
  60. package/extensions/dashboard-client/src/api/client.ts +10 -0
  61. package/extensions/dashboard-client/src/components/layout/AppShell.tsx +1 -1
  62. package/extensions/dashboard-client/src/styles/base.css +2 -4
  63. package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +58 -4
  64. package/extensions/dashboard-client/tailwind.config.js +1 -1
  65. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +9 -1
  66. package/extensions/dashboard-server/api-contracts/index.ts +2 -2
  67. package/extensions/dashboard-server/api-contracts/raptor.ts +19 -0
  68. package/extensions/dashboard-server/index-reader.ts +28 -5
  69. package/extensions/dashboard-server/routes-cache.ts +17 -5
  70. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +17 -0
  71. package/extensions/dashboard-server/routes-raptor.ts +88 -1
  72. package/extensions/dashboard-server/routes.ts +1 -1
  73. package/extensions/dashboard-server/server.ts +2 -0
  74. package/extensions/mega-pipeline/recall.ts +30 -21
  75. package/extensions/mega-runtime/capture-model.ts +14 -2
  76. package/extensions/mega-runtime/runtime-snapshot.ts +40 -0
  77. package/extensions/mega-runtime/runtime.ts +1 -0
  78. package/package.json +1 -1
  79. package/src/config/dedup.ts +2 -1
  80. package/src/costApi.ts +131 -0
  81. package/src/dedup/raptor/incremental.ts +48 -3
  82. package/src/dedup/raptor/index.ts +10 -6
  83. package/src/vector-search.ts +3 -3
  84. package/extensions/dashboard-client/dist/assets/MemoryMapTab-DSnSO3nG.js +0 -2
  85. package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +0 -1
  86. package/extensions/dashboard-client/dist/assets/index-GPHRnEN7.js.map +0 -1
@@ -30,7 +30,8 @@ body {
30
30
  }
31
31
 
32
32
  .dashboard-app {
33
- max-width: min(1600px, 96vw);
33
+ width: 100%;
34
+ max-width: 100vw;
34
35
  margin: 0 auto;
35
36
  padding: clamp(8px, 2vw, 24px);
36
37
  }
@@ -210,9 +211,6 @@ body {
210
211
  }
211
212
 
212
213
  @media (min-width: 1601px) {
213
- .dashboard-app {
214
- max-width: min(1800px, 96vw);
215
- }
216
214
  .card-grid {
217
215
  grid-template-columns: repeat(4, 1fr);
218
216
  }
@@ -7,8 +7,12 @@
7
7
  */
8
8
  import type React from "react";
9
9
  import { useEffect, useState } from "react";
10
- import { fetchRaptorTree } from "../../api/client";
11
- import type { RaptorTreeResponse, RaptorNodeDTO } from "@contracts";
10
+ import { fetchRaptorTree, fetchRaptorBuildHistory } from "../../api/client";
11
+ import type {
12
+ RaptorTreeResponse,
13
+ RaptorNodeDTO,
14
+ RaptorBuildHistoryResponse,
15
+ } from "@contracts";
12
16
 
13
17
  /** Per-level border color for node cards. */
14
18
  function levelColor(level: number): string {
@@ -68,6 +72,7 @@ function NodeCard({ node }: { node: RaptorNodeDTO }): React.ReactElement {
68
72
 
69
73
  export default function RaptorTreeView(): React.ReactElement {
70
74
  const [data, setData] = useState<RaptorTreeResponse | null>(null);
75
+ const [history, setHistory] = useState<RaptorBuildHistoryResponse | null>(null);
71
76
  const [loading, setLoading] = useState<boolean>(true);
72
77
  const [error, setError] = useState<string | null>(null);
73
78
 
@@ -75,10 +80,11 @@ export default function RaptorTreeView(): React.ReactElement {
75
80
  let cancelled = false;
76
81
  setLoading(true);
77
82
  setError(null);
78
- fetchRaptorTree()
79
- .then((d) => {
83
+ Promise.all([fetchRaptorTree(), fetchRaptorBuildHistory()])
84
+ .then(([d, h]) => {
80
85
  if (!cancelled) {
81
86
  setData(d);
87
+ setHistory(h);
82
88
  setLoading(false);
83
89
  }
84
90
  })
@@ -120,6 +126,11 @@ export default function RaptorTreeView(): React.ReactElement {
120
126
  );
121
127
  }
122
128
 
129
+ const latestBuild = history && !history.empty ? history.builds[0] : null;
130
+ const coherencePct = latestBuild?.coherenceScore != null
131
+ ? Math.round(latestBuild.coherenceScore * 100)
132
+ : null;
133
+
123
134
  return (
124
135
  <div className="w-full p-4">
125
136
  <div className="mb-4 flex items-center gap-6 text-sm text-muted-foreground">
@@ -130,7 +141,50 @@ export default function RaptorTreeView(): React.ReactElement {
130
141
  <strong>{data.levels + 1}</strong> levels
131
142
  </span>
132
143
  <span>Built {formatDate(data.builtAt ?? 0)}</span>
144
+ {latestBuild && (
145
+ <>
146
+ <span>
147
+ Coherence:{" "}
148
+ <strong style={{ color: coherencePct != null && coherencePct >= 70 ? "#22c55e" : "#f59e0b" }}>
149
+ {coherencePct != null ? `${coherencePct}%` : "—"}
150
+ </strong>
151
+ </span>
152
+ <span>
153
+ Leaves: <strong>{latestBuild.leafCount}</strong>
154
+ </span>
155
+ <span>
156
+ Depth: <strong>{latestBuild.depth}</strong>
157
+ </span>
158
+ {latestBuild.timedOut && (
159
+ <span style={{ color: "#ef4444" }}>budget timeout</span>
160
+ )}
161
+ </>
162
+ )}
133
163
  </div>
164
+ {history && !history.empty && history.builds.length > 1 && (
165
+ <details className="mb-4">
166
+ <summary className="cursor-pointer text-xs text-muted-foreground">
167
+ Build history ({history.builds.length} builds)
168
+ </summary>
169
+ <div className="mt-2 space-y-1">
170
+ {history.builds.slice(0, 10).map((b) => (
171
+ <div key={b.buildId} className="flex items-center gap-4 text-xs text-muted-foreground">
172
+ <span>{formatDate(b.completedAt)}</span>
173
+ <span>{b.nodeCount} nodes</span>
174
+ <span>{b.leafCount} leaves</span>
175
+ <span>depth {b.depth}</span>
176
+ <span>
177
+ coherence{" "}
178
+ {b.coherenceScore != null
179
+ ? `${Math.round(b.coherenceScore * 100)}%`
180
+ : "—"}
181
+ </span>
182
+ {b.timedOut && <span style={{ color: "#ef4444" }}>timeout</span>}
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </details>
187
+ )}
134
188
  {data.nodes.map((node) => (
135
189
  <NodeCard key={node.id} node={node} />
136
190
  ))}
@@ -6,7 +6,7 @@ export default {
6
6
  container: {
7
7
  center: true,
8
8
  padding: "1.5rem",
9
- screens: { "2xl": "1600px" },
9
+ screens: { "2xl": "100%" },
10
10
  },
11
11
  extend: {
12
12
  colors: {
@@ -15,7 +15,7 @@ import type { IndexesSummaryResponse } from "../multi-repo.js";
15
15
  import type { GameStateResponse } from "../game.js";
16
16
  import type { SseEvent } from "../index.js";
17
17
  import type { ProviderCacheResponse } from "../provider-cache.js";
18
- import type { RaptorTreeResponse } from "../raptor.js";
18
+ import type { RaptorTreeResponse, RaptorBuildHistoryResponse } from "../raptor.js";
19
19
  import type { MemoryStatusResponse } from "../memory.js";
20
20
  import type { SetupStatusResponse, SetupDetectResponse, SetupConfigureRequest, SetupConfigureResponse } from "../setup.js";
21
21
  import type { EmbedderHealthResponse } from "../embedder-health.js";
@@ -424,6 +424,14 @@ export const ENDPOINTS = {
424
424
  "Hierarchical RAPTOR tree (summary nodes by level) for a session; defaults to the latest session with nodes.",
425
425
  } as const satisfies EndpointDef<"GET", undefined, RaptorTreeResponse>,
426
426
 
427
+ /** GET /api/raptor-build-history — Build history (coherence, depth, timeout) for a session. */
428
+ raptorBuildHistory: {
429
+ method: "GET",
430
+ path: "/api/raptor-build-history",
431
+ description:
432
+ "RAPTOR build history rows (node/leaf count, depth, coherence score, timed_out) for a session; defaults to the latest session with builds.",
433
+ } as const satisfies EndpointDef<"GET", undefined, RaptorBuildHistoryResponse>,
434
+
427
435
  /** GET /api/embedder-health — Round-trip a test embed through the active embedder. */
428
436
  embedderHealth: {
429
437
  method: "GET",
@@ -241,8 +241,8 @@ export type {
241
241
  GraphValidationReport,
242
242
  } from "./memory-map.js";
243
243
 
244
- // Part B — RAPTOR tree
245
- export type { RaptorNodeDTO, RaptorTreeResponse } from "./raptor.js";
244
+ // Part B — RAPTOR tree + build history
245
+ export type { RaptorNodeDTO, RaptorTreeResponse, RaptorBuildHistoryDTO, RaptorBuildHistoryResponse } from "./raptor.js";
246
246
 
247
247
  // A3 — cache stripe distribution + health
248
248
  export type {
@@ -47,3 +47,22 @@ export interface RaptorTreeResponse {
47
47
  /** True when there are no RAPTOR nodes for the resolved session. */
48
48
  readonly empty: boolean;
49
49
  }
50
+
51
+ /** A single build-history row (client-safe). */
52
+ export interface RaptorBuildHistoryDTO {
53
+ readonly buildId: string;
54
+ readonly sessionId: string;
55
+ readonly startedAt: number;
56
+ readonly completedAt: number;
57
+ readonly nodeCount: number;
58
+ readonly leafCount: number;
59
+ readonly depth: number;
60
+ readonly coherenceScore: number | null;
61
+ readonly timedOut: boolean;
62
+ }
63
+
64
+ /** Response body for GET /api/raptor-build-history. */
65
+ export interface RaptorBuildHistoryResponse {
66
+ readonly builds: RaptorBuildHistoryDTO[];
67
+ readonly empty: boolean;
68
+ }
@@ -18,6 +18,7 @@ import {
18
18
  readProviderCacheForRepo,
19
19
  } from "../../src/store/sqlite/perf-samples.js";
20
20
  import { lookupModelInputRate } from "../../src/pricing.js";
21
+ import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
21
22
 
22
23
  export function getIndexDir(): string {
23
24
  const override = process.env.MEGACOMPACT_INDEX_DIR;
@@ -41,7 +42,29 @@ export function readIndex(): IndexIndex | null {
41
42
  const rows = db
42
43
  .prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
43
44
  .all() as Record<string, unknown>[];
44
- const mapped: IndexRepo[] = rows.map((r) => ({
45
+ // Lazily fetch cost-API pricing once per readIndex call (cached for 1 hour
46
+ // inside costApi.ts). Avoids per-row network calls.
47
+ const costApi = costApiConfig();
48
+ const mapped: IndexRepo[] = rows.map((r) => {
49
+ const modelName = String(r.model_name ?? "");
50
+ const localRate = (r.input_rate as number | null) ||
51
+ lookupModelInputRate(modelName) ||
52
+ null;
53
+ let inputRate = localRate;
54
+ let outputRate = (r.output_rate as number | null) ?? null;
55
+ // Fall back to the cost API when the local table has no data.
56
+ if (costApi.enabled && (inputRate == null || outputRate == null) && modelName) {
57
+ const apiPricing = lookupCostApiPricing(modelName);
58
+ if (apiPricing) {
59
+ if (inputRate == null && apiPricing.inputRate != null) {
60
+ inputRate = apiPricing.inputRate;
61
+ }
62
+ if (outputRate == null && apiPricing.outputRate != null) {
63
+ outputRate = apiPricing.outputRate;
64
+ }
65
+ }
66
+ }
67
+ return {
45
68
  repoRoot: String(r.repo_root ?? ""),
46
69
  displayName: String(r.display_name ?? ""),
47
70
  stateDir: String(r.state_dir ?? ""),
@@ -52,9 +75,8 @@ export function readIndex(): IndexIndex | null {
52
75
  provider: (r.provider as string | null) ?? null,
53
76
  providerName: (r.provider_name as string | null) ?? null,
54
77
  modelName: (r.model_name as string | null) ?? null,
55
- inputRate: ((r.input_rate as number | null) ||
56
- lookupModelInputRate(String(r.model_name ?? ""))) ?? null,
57
- outputRate: (r.output_rate as number | null) ?? null,
78
+ inputRate,
79
+ outputRate,
58
80
  lastSeen: Number(r.last_seen ?? 0),
59
81
  // Defaults — enriched below from each repo's own store.
60
82
  tokensKept: 0,
@@ -66,7 +88,8 @@ export function readIndex(): IndexIndex | null {
66
88
  providerCachePct: null,
67
89
  providerCacheRead: null,
68
90
  providerCacheWrite: null,
69
- }));
91
+ };
92
+ });
70
93
  // Enrich each repo with per-store token + model detail read directly via
71
94
  // node:sqlite (same zero-dependency invariant as readIndex; no store graph
72
95
  // import). Best-effort: a missing/corrupt store degrades to the defaults
@@ -13,7 +13,8 @@ import { createRequire } from "node:module";
13
13
  import type { IncomingMessage, ServerResponse } from "node:http";
14
14
 
15
15
  import type { RouteContext } from "./routes-core.js";
16
- import { computeCacheSavings } from "../../src/pricing.js";
16
+ import { computeCacheSavings, lookupModelInputRate } from "../../src/pricing.js";
17
+ import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // handleProviderCache — "/api/provider-cache"
@@ -118,18 +119,29 @@ export function handleProviderCache(
118
119
  } | null = null;
119
120
  try {
120
121
  const snap = latestModelSnapshot(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
121
- if (snap && snap.inputRate > 0) {
122
+ let rate = snap?.inputRate ?? 0;
123
+ let modelName = snap?.modelName ?? snap?.modelId ?? "unknown";
124
+ // Fallback chain: SQLite snapshot → local pricing table → cost API
125
+ if (rate <= 0 && modelName !== "unknown") {
126
+ rate = lookupModelInputRate(modelName) ?? 0;
127
+ }
128
+ if (rate <= 0 && costApiConfig().enabled && modelName !== "unknown") {
129
+ // guardrails-allow PREVENT-PI-004: cost API pricing lookup (user-opted-in)
130
+ const apiPricing = lookupCostApiPricing(modelName);
131
+ rate = apiPricing?.inputRate ?? 0;
132
+ }
133
+ if (rate > 0) {
122
134
  const s = computeCacheSavings(
123
135
  lifetime.totalCacheRead,
124
136
  lifetime.totalCacheWrite,
125
- snap.inputRate,
137
+ rate,
126
138
  );
127
139
  savings = {
128
140
  cacheReadSaved: s.cacheReadSaved,
129
141
  cacheWriteCost: s.cacheWriteCost,
130
142
  netSaved: s.netSaved,
131
- model: snap.modelName ?? snap.modelId ?? "unknown",
132
- inputRate: snap.inputRate,
143
+ model: modelName,
144
+ inputRate: rate,
133
145
  };
134
146
  }
135
147
  } catch {
@@ -272,6 +272,23 @@ export const SETTINGS: ReadonlyArray<{
272
272
  num("MEGACOMPACT_EMBED_CACHE", "Embed Cache Size", "Embedding cache entries (0 = disabled)", 256, 0, 10000),
273
273
  ],
274
274
  },
275
+ {
276
+ name: "Cost API",
277
+ settings: [
278
+ boolDirect(
279
+ "MEGACOMPACT_COST_API_ENABLED",
280
+ "Cost API Lookup",
281
+ "Fetch model pricing from an external API (PREVENT-PI-004 network exception — opt-in). Enriches the dashboard Cache/Repos tabs with real $/token rates for models not in the local pricing table.",
282
+ false,
283
+ ),
284
+ str(
285
+ "MEGACOMPACT_COST_API_URL",
286
+ "Cost API URL",
287
+ "OpenRouter-compatible model pricing endpoint (e.g. https://openrouter.ai/api/v1/models). Only contacted when Cost API Lookup is ON.",
288
+ "",
289
+ ),
290
+ ],
291
+ },
275
292
  ];
276
293
 
277
294
  /** Flat index of every setting by key for O(1) lookup + validation. */
@@ -10,7 +10,7 @@ import { createRequire } from "node:module";
10
10
  import type { ServerResponse, IncomingMessage } from "node:http";
11
11
  import { parse as parseUrl } from "node:url";
12
12
  import type { RouteContext } from "./routes-core.js";
13
- import type { RaptorTreeResponse, RaptorNodeDTO } from "./api-contracts/raptor.js";
13
+ import type { RaptorTreeResponse, RaptorNodeDTO, RaptorBuildHistoryResponse, RaptorBuildHistoryDTO } from "./api-contracts/raptor.js";
14
14
  import { openStore } from "../../src/store/sqlite.js";
15
15
 
16
16
  const _req = createRequire(import.meta.url);
@@ -126,3 +126,90 @@ export function handleRaptorTree(
126
126
  return true;
127
127
  }
128
128
  }
129
+
130
+ /** Find the most recent session that has build history rows (else null). */
131
+ function latestBuildSession(stateDir: string): string | null {
132
+ try {
133
+ const db = openStore(stateDir);
134
+ const row = db
135
+ .prepare(
136
+ "SELECT DISTINCT session_id FROM raptor_build_history ORDER BY completed_at DESC LIMIT 1",
137
+ )
138
+ .get() as { session_id: string } | undefined;
139
+ return row?.session_id ?? null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ export function handleRaptorBuildHistory(
146
+ req: IncomingMessage,
147
+ res: ServerResponse,
148
+ ctx: RouteContext,
149
+ ): boolean {
150
+ if (req.method !== "GET") return false;
151
+ const parsed = parseUrl(req.url ?? "", true);
152
+ if (parsed.pathname !== "/api/raptor-build-history") return false;
153
+
154
+ try {
155
+ const { listBuildHistory } = _req(
156
+ "../../src/dedup/raptor/buildHistory.js",
157
+ ) as {
158
+ listBuildHistory: (
159
+ sessionId: string,
160
+ stateDir: string,
161
+ ) => Array<{
162
+ buildId: string;
163
+ sessionId: string;
164
+ startedAt: number;
165
+ completedAt: number;
166
+ nodeCount: number;
167
+ leafCount: number;
168
+ depth: number;
169
+ coherenceScore: number | null;
170
+ timedOut: boolean;
171
+ }>;
172
+ };
173
+
174
+ let sessionId =
175
+ typeof parsed.query.sessionId === "string" ? parsed.query.sessionId : "";
176
+
177
+ if (!sessionId) {
178
+ sessionId = latestBuildSession(ctx.stateDir) ?? "";
179
+ if (!sessionId) {
180
+ const body: RaptorBuildHistoryResponse = { builds: [], empty: true };
181
+ res.writeHead(200, { "Content-Type": "application/json" });
182
+ res.end(JSON.stringify(body));
183
+ return true;
184
+ }
185
+ }
186
+
187
+ const rows = listBuildHistory(sessionId, ctx.stateDir);
188
+ if (rows.length === 0) {
189
+ const body: RaptorBuildHistoryResponse = { builds: [], empty: true };
190
+ res.writeHead(200, { "Content-Type": "application/json" });
191
+ res.end(JSON.stringify(body));
192
+ return true;
193
+ }
194
+
195
+ const dtos: RaptorBuildHistoryDTO[] = rows.map((r) => ({
196
+ buildId: r.buildId,
197
+ sessionId: r.sessionId,
198
+ startedAt: r.startedAt,
199
+ completedAt: r.completedAt,
200
+ nodeCount: r.nodeCount,
201
+ leafCount: r.leafCount,
202
+ depth: r.depth,
203
+ coherenceScore: r.coherenceScore,
204
+ timedOut: r.timedOut,
205
+ }));
206
+ const body: RaptorBuildHistoryResponse = { builds: dtos, empty: false };
207
+ res.writeHead(200, { "Content-Type": "application/json" });
208
+ res.end(JSON.stringify(body));
209
+ return true;
210
+ } catch (e) {
211
+ res.writeHead(500, { "Content-Type": "application/json" });
212
+ res.end(JSON.stringify({ error: String(e) }));
213
+ return true;
214
+ }
215
+ }
@@ -23,7 +23,7 @@ export { handleProviderCache } from "./routes-cache.js";
23
23
  export { handleMemoryStatus } from "./routes-memory.js";
24
24
  export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
25
25
  export { handleMemoryMap } from "./routes-memory-map.js";
26
- export { handleRaptorTree } from "./routes-raptor.js";
26
+ export { handleRaptorTree, handleRaptorBuildHistory } from "./routes-raptor.js";
27
27
  export { handleCacheStripes } from "./routes-cache.js";
28
28
  export { handleContextHealth, handleCachePoison, handleHealthSettings } from "./routes-health.js";
29
29
  export { handleEmbedderHealth } from "./routes-embedder-health.js";
@@ -46,6 +46,7 @@ import {
46
46
  handleSetupConfigure,
47
47
  handleMemoryMap,
48
48
  handleRaptorTree,
49
+ handleRaptorBuildHistory,
49
50
  handleContextHealth,
50
51
  handleCachePoison,
51
52
  handleHealthSettings,
@@ -260,6 +261,7 @@ export async function launchDashboardServer(
260
261
  if (handleSetupConfigure(req, res, ctx)) return;
261
262
  if (handleMemoryMap(req, res, ctx)) return;
262
263
  if (handleRaptorTree(req, res, ctx)) return;
264
+ if (handleRaptorBuildHistory(req, res, ctx)) return;
263
265
  if (handleContextHealth(req, res, ctx)) return;
264
266
  if (handleCachePoison(req, res, ctx)) return;
265
267
  if (handleHealthSettings(req, res, ctx)) return;
@@ -107,16 +107,23 @@ export function doRecall(
107
107
  );
108
108
  runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
109
109
  }
110
+ let sumTokens = 0;
111
+ for (const h of result.toInject) sumTokens += h.checkpoint.tokenEstimate;
110
112
  if (result.toInject.length > 0) {
111
- let sumTokens = 0;
112
- for (const h of result.toInject) sumTokens += h.checkpoint.tokenEstimate;
113
113
  runtime.rt.recallInjections += result.toInject.length;
114
114
  runtime.rt.cacheHitTokens += sumTokens;
115
115
  incRecallInjected(result.toInject.length, runtime.currentStateDir);
116
116
  incCacheHitTokens(sumTokens, runtime.currentStateDir);
117
- // S43: record recall provenance — which checkpoints/summaries served this
118
- // turn, their score + source path. Linked to the turn row written at
119
- // turn_end via the conversation+turnIndex. Best-effort + non-fatal.
117
+ }
118
+ // S43: record recall provenance which checkpoints/summaries served this
119
+ // turn, their score + source path. Linked to the turn row written at
120
+ // turn_end via the conversation+turnIndex. Best-effort + non-fatal.
121
+ // Persists telemetry (HyDE + recall metrics) even when recall returned
122
+ // no hits, so empty-recall HyDE invocations are still visible in the
123
+ // dashboard Turns/Metrics tabs.
124
+ const hasTelemetry =
125
+ result.hydeInfo != null || result.recallMetrics != null;
126
+ if (result.toInject.length > 0 || hasTelemetry) {
120
127
  try {
121
128
  const convId = ensureConversationIdFor(
122
129
  config,
@@ -136,22 +143,24 @@ export function doRecall(
136
143
  },
137
144
  runtime.currentStateDir,
138
145
  );
139
- recordRecallWrite(
140
- config,
141
- turnId,
142
- result.toInject.map((h) => ({
143
- checkpointId: h.checkpoint.checkpointId,
144
- score: h.score,
145
- source:
146
- h.raptorLevel !== undefined
147
- ? "raptor"
148
- : h.repoId
149
- ? "cross-repo"
150
- : "flat",
151
- raptorLevel: h.raptorLevel,
152
- })),
153
- runtime.currentStateDir,
154
- );
146
+ if (result.toInject.length > 0) {
147
+ recordRecallWrite(
148
+ config,
149
+ turnId,
150
+ result.toInject.map((h) => ({
151
+ checkpointId: h.checkpoint.checkpointId,
152
+ score: h.score,
153
+ source:
154
+ h.raptorLevel !== undefined
155
+ ? "raptor"
156
+ : h.repoId
157
+ ? "cross-repo"
158
+ : "flat",
159
+ raptorLevel: h.raptorLevel,
160
+ })),
161
+ runtime.currentStateDir,
162
+ );
163
+ }
155
164
  } catch {
156
165
  /* non-fatal: recall provenance never breaks the recall path */
157
166
  }
@@ -11,6 +11,7 @@ import { resolveRepoRoot } from "../mega-config.js";
11
11
  import { recordModelSnapshot, recordRepoModel } from "../../src/store/sqlite.js";
12
12
  import type { ModelSnapshot } from "../../src/store/sqlite.js";
13
13
  import { lookupModelInputRate } from "../../src/pricing.js";
14
+ import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
14
15
 
15
16
  // ---------------------------------------------------------------------- types
16
17
 
@@ -45,13 +46,24 @@ export function captureModelImpl(ctx: CaptureModelContext, ectx: ExtensionContex
45
46
  }
46
47
  const modelId = m.id;
47
48
  const fallbackInput = lookupModelInputRate(modelId);
49
+ let inputRate = m.cost?.input || fallbackInput || 0;
50
+ let outputRate = m.cost?.output ?? 0;
51
+ // Cost API last-resort fallback for models not in the local pricing table
52
+ if (inputRate <= 0 && costApiConfig().enabled) {
53
+ // guardrails-allow PREVENT-PI-004: cost API pricing lookup (user-opted-in)
54
+ const apiPricing = lookupCostApiPricing(modelId);
55
+ if (apiPricing) {
56
+ inputRate = apiPricing.inputRate ?? 0;
57
+ if (outputRate <= 0) outputRate = apiPricing.outputRate ?? 0;
58
+ }
59
+ }
48
60
  const snap: Omit<ModelSnapshot, "capturedAt"> = {
49
61
  provider: m.provider,
50
62
  providerName,
51
63
  modelId,
52
64
  modelName: m.name ?? null,
53
- inputRate: m.cost?.input || fallbackInput || 0,
54
- outputRate: m.cost?.output ?? 0,
65
+ inputRate,
66
+ outputRate,
55
67
  contextWindow: m.contextWindow ?? 0,
56
68
  maxTokens: m.maxTokens ?? 0,
57
69
  reasoning: !!m.reasoning,
@@ -72,6 +72,9 @@ export interface RuntimeSnapshotContext extends RuntimeHelpersContext {
72
72
  lastSnapshotSig: string | null;
73
73
  lastWidgetCtx?: ExtensionContext;
74
74
  lastActivityAt: number;
75
+ /** Timestamp (ms) of the last session heartbeat write. Used by the
76
+ * material-change gate fast path to refresh heartbeats on a cadence. */
77
+ lastHeartbeatAt: number;
75
78
  lastLevel: number;
76
79
  diagCtxFastGate: number;
77
80
  diagLiveTrimFires: number;
@@ -117,6 +120,12 @@ export function snapshotImpl(
117
120
  const sig = materialSigImpl(self);
118
121
  if (ctx && self.widgetData && self.lastSnapshotSig === sig) {
119
122
  self.renderWidget(ctx);
123
+ // Refresh heartbeat on a time cadence even when material state hasn't
124
+ // changed, so the session doesn't appear stale and get pruned by the
125
+ // dashboard's 30-min cutoff. The full snapshot recompute is still gated
126
+ // (expensive SQLite opens + writeFileSync), but a heartbeat row is
127
+ // cheap (single INSERT OR REPLACE on the index DB).
128
+ refreshHeartbeatIfStale(self, ctx, 10_000);
120
129
  return;
121
130
  }
122
131
  const perfT0 = performance.now();
@@ -313,4 +322,35 @@ export function snapshotImpl(
313
322
  /* non-fatal: perf instrumentation never blocks the agent */
314
323
  }
315
324
  self.lastSnapshotSig = sig;
325
+ self.lastHeartbeatAt = Date.now();
326
+ }
327
+
328
+ // ----------------------------------------------------------- heartbeat refresh
329
+
330
+ /** Refresh the session heartbeat if enough time has elapsed since the last
331
+ * write. Called from the material-change gate fast path (where the full
332
+ * snapshot recompute is skipped) so the session stays visible in the
333
+ * dashboard during idle/typing periods. Non-fatal — best-effort like the
334
+ * main heartbeat write. */
335
+ function refreshHeartbeatIfStale(
336
+ self: RuntimeSnapshotContext,
337
+ ctx: ExtensionContext | undefined,
338
+ intervalMs: number,
339
+ ): void {
340
+ const now = Date.now();
341
+ if (now - self.lastHeartbeatAt < intervalMs) return;
342
+ try {
343
+ const repo =
344
+ resolveRepoRoot(ctx?.cwd ?? self.currentStateDir) ?? self.currentStateDir;
345
+ recordSessionHeartbeat(
346
+ process.pid,
347
+ self.rt.sessionId,
348
+ repo,
349
+ self.currentStateDir,
350
+ self.lastCtxWindow || 0,
351
+ );
352
+ self.lastHeartbeatAt = now;
353
+ } catch {
354
+ /* non-fatal: heartbeat refresh must never block the snapshot path */
355
+ }
316
356
  }
@@ -209,6 +209,7 @@ export class MegaRuntime {
209
209
  // (already-registered) widget factory is refreshed. Kills the per-event
210
210
  // main-thread block during typing/idle streaming with no material change.
211
211
  lastSnapshotSig: string | null = null;
212
+ lastHeartbeatAt: number = 0;
212
213
  // v0.8.5: bumped whenever the cached game-state memo is evicted (bumpGameState
213
214
  // for in-process /mega-game writes, the fs.watch callback for cross-process
214
215
  // dashboard-server writes, and bindRepo on repo switch) so the snapshot gate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.13.7",
3
+ "version": "0.14.1",
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",
@@ -89,7 +89,8 @@ export function loadDedupConfig(): DedupConfigShape {
89
89
  L2_ENABLED: envBool("MEGACOMPACT_L2_ENABLED", true),
90
90
  // Fix D: RAPTOR promoted to live recall. Default ON; canary.ts sequences it
91
91
  // last (L0→L1→L2→RAPTOR) and auto-disables on p95 breach, so promotion is
92
- // safe. `RAPTOR_SHADOW_MODE=false` still gates serving during transition.
92
+ // safe. Shadow mode (RAPTOR_SHADOW_MODE=true) opts out of serving while
93
+ // still building + persisting the tree (transition/eval use).
93
94
  RAPTOR_ENABLED: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
94
95
  MARK_ONLY_L0: envBool("MEGACOMPACT_MARK_ONLY_L0", false),
95
96
  MARK_ONLY_L1: envBool("MEGACOMPACT_MARK_ONLY_L1", false),