pi-mega-compact 0.12.0 → 0.12.2

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 (75) hide show
  1. package/dist/extensions/dashboard-server/gather-debug.js +63 -0
  2. package/dist/extensions/dashboard-server/routes-health.js +44 -0
  3. package/dist/extensions/dashboard-server/routes-setup.js +30 -4
  4. package/dist/extensions/dashboard-server/routes.js +1 -1
  5. package/dist/extensions/dashboard-server/server.js +3 -1
  6. package/dist/extensions/mega-events/agent-handlers.js +27 -1
  7. package/dist/extensions/mega-events/health-handler.js +19 -2
  8. package/dist/src/memoryGraph/sources.js +42 -15
  9. package/dist/src/memoryGraph.js +17 -5
  10. package/dist/src/store/sqlite/meta.js +11 -0
  11. package/dist/src/store/turns/schema.js +18 -2
  12. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DeQ0qnBX.js → AchievementTiles-Cntu89Xw.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DeQ0qnBX.js.map → AchievementTiles-Cntu89Xw.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dtk_RJfs.js → AchievementsTab-4ckY9z4n.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementsTab-Dtk_RJfs.js.map → AchievementsTab-4ckY9z4n.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{CacheTab-kWcQt3PI.js → CacheTab-BfGJkiaf.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{CacheTab-kWcQt3PI.js.map → CacheTab-BfGJkiaf.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{ConfigTab-UFlo73fR.js → ConfigTab-DV08rHoV.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{ConfigTab-UFlo73fR.js.map → ConfigTab-DV08rHoV.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{EventsTab-BENO2N0y.js → EventsTab-DHytZ0xs.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{EventsTab-BENO2N0y.js.map → EventsTab-DHytZ0xs.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{GameTab-BCJavOML.js → GameTab-Ofpoz-6s.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{GameTab-BCJavOML.js.map → GameTab-Ofpoz-6s.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/HealthTab-B0klAxD8.js +2 -0
  25. package/extensions/dashboard-client/dist/assets/HealthTab-B0klAxD8.js.map +1 -0
  26. package/extensions/dashboard-client/dist/assets/MaintenanceTab-CHKh3KPI.js +2 -0
  27. package/extensions/dashboard-client/dist/assets/MaintenanceTab-CHKh3KPI.js.map +1 -0
  28. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-w5BZP3i-.js → MemoryMapTab-B6FLQ-32.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-w5BZP3i-.js.map → MemoryMapTab-B6FLQ-32.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-BBCXa3xS.js → MetricsTab-DvOjTDkW.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-BBCXa3xS.js.map → MetricsTab-DvOjTDkW.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-CzABMSAQ.js → OverviewTab-CqbONEwi.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-CzABMSAQ.js.map → OverviewTab-CqbONEwi.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DFiM_Ubg.js → ReposTab-sc1GLn7i.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DFiM_Ubg.js.map → ReposTab-sc1GLn7i.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-BtW4bees.js → SessionsTab-C5gB45oc.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-BtW4bees.js.map → SessionsTab-C5gB45oc.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/SetupTab-BEEX4OF5.js +2 -0
  39. package/extensions/dashboard-client/dist/assets/SetupTab-BEEX4OF5.js.map +1 -0
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CC13EXij.js → TimeSavedCard-BBwvb4cC.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CC13EXij.js.map → TimeSavedCard-BBwvb4cC.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{TopicsTab-BrTB8uEK.js → TopicsTab-BsCf512_.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{TopicsTab-BrTB8uEK.js.map → TopicsTab-BsCf512_.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-BJK7FIHu.js → TurnsTab-C3ti7NB5.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-BJK7FIHu.js.map → TurnsTab-C3ti7NB5.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js +2 -0
  47. package/extensions/dashboard-client/dist/assets/health-lSeDFTz7.js.map +1 -0
  48. package/extensions/dashboard-client/dist/assets/{index-B8k19B9g.js → index-BEeaI9uo.js} +3 -3
  49. package/extensions/dashboard-client/dist/assets/{index-B8k19B9g.js.map → index-BEeaI9uo.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{useSSE-CWBB4Me7.js → useSSE-BMSSO9wB.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{useSSE-CWBB4Me7.js.map → useSSE-BMSSO9wB.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/index.html +1 -1
  53. package/extensions/dashboard-client/src/api/health.ts +16 -0
  54. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +91 -1
  55. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +19 -0
  56. package/extensions/dashboard-server/api-contracts/maintenance.ts +1 -0
  57. package/extensions/dashboard-server/api-contracts/setup.ts +7 -1
  58. package/extensions/dashboard-server/gather-debug.ts +63 -0
  59. package/extensions/dashboard-server/routes-health.ts +51 -0
  60. package/extensions/dashboard-server/routes-setup.ts +28 -4
  61. package/extensions/dashboard-server/routes.ts +1 -1
  62. package/extensions/dashboard-server/server.ts +2 -0
  63. package/extensions/mega-events/agent-handlers.ts +26 -1
  64. package/extensions/mega-events/health-handler.ts +31 -3
  65. package/package.json +1 -1
  66. package/src/memoryGraph/sources.ts +49 -15
  67. package/src/memoryGraph.ts +17 -5
  68. package/src/store/sqlite/meta.ts +16 -0
  69. package/src/store/turns/schema.ts +18 -2
  70. package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js +0 -2
  71. package/extensions/dashboard-client/dist/assets/HealthTab-C7iw7rhx.js.map +0 -1
  72. package/extensions/dashboard-client/dist/assets/MaintenanceTab-C0rdAMWc.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MaintenanceTab-C0rdAMWc.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/SetupTab-Duy1LJhg.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/SetupTab-Duy1LJhg.js.map +0 -1
@@ -35,6 +35,7 @@ export function sendGatherDebug(res, ctx) {
35
35
  dashboardSnapshot: { error: "not fetched" },
36
36
  recentEvents: [],
37
37
  criticalEvents: [],
38
+ dataSourceAudit: {},
38
39
  };
39
40
  // 1. Config flags (no secrets — MEGACOMPACT_* only)
40
41
  for (const key of MEGACOMPACT_ENV_KEYS) {
@@ -115,6 +116,68 @@ export function sendGatherDebug(res, ctx) {
115
116
  catch (e) {
116
117
  result.recentEvents = [{ error: e instanceof Error ? e.message : String(e) }];
117
118
  }
119
+ // 6. Data source audit — per-tab data population check
120
+ try {
121
+ const audit = {};
122
+ const db = openStoreDb(ctx);
123
+ // Main db tables
124
+ for (const table of ["context_chunks", "memories", "raw_transcript", "checkpoint_epochs", "dedup_mirror"]) {
125
+ try {
126
+ const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get();
127
+ audit[`main.${table}`] = row.c;
128
+ }
129
+ catch {
130
+ audit[`main.${table}`] = "table missing";
131
+ }
132
+ }
133
+ // context_health + cache_poison_events
134
+ for (const table of ["context_health", "cache_poison_events"]) {
135
+ try {
136
+ const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get();
137
+ audit[`main.${table}`] = row.c;
138
+ }
139
+ catch {
140
+ audit[`main.${table}`] = "table missing";
141
+ }
142
+ }
143
+ // turns.db tables
144
+ try {
145
+ const turnMod = require("../../src/store/turns/connection.js");
146
+ const tdb = turnMod.openTurnDb(ctx.stateDir);
147
+ for (const table of ["turns", "turn_recall", "session_conversations", "topics", "memory_topics"]) {
148
+ try {
149
+ const row = tdb.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get();
150
+ audit[`turns.${table}`] = row.c;
151
+ }
152
+ catch {
153
+ audit[`turns.${table}`] = "table missing";
154
+ }
155
+ }
156
+ }
157
+ catch (e) {
158
+ audit["turns.db"] = `error: ${e instanceof Error ? e.message : String(e)}`;
159
+ }
160
+ // .mega-compact.env
161
+ try {
162
+ const envPath = ctx.stateDir + "/.mega-compact.env";
163
+ audit["mega-compact.env"] = existsSync(envPath) ? "exists" : "not found";
164
+ }
165
+ catch {
166
+ audit["mega-compact.env"] = "check failed";
167
+ }
168
+ // dashboard.json
169
+ try {
170
+ const metricsPath = defaultMetricsPath(ctx.stateDir);
171
+ audit["dashboard.json"] = existsSync(metricsPath) ? "exists" : "not found";
172
+ }
173
+ catch {
174
+ audit["dashboard.json"] = "check failed";
175
+ }
176
+ result.dataSourceAudit = audit;
177
+ }
178
+ catch (e) {
179
+ result.dataSourceAudit = { error: e instanceof Error ? e.message : String(e) };
180
+ }
118
181
  // Send (local helper to avoid importing sendJson from a non-exporting module)
119
182
  try {
120
183
  const body = JSON.stringify(result);
@@ -79,3 +79,47 @@ export function handleCachePoison(req, res, ctx) {
79
79
  }
80
80
  return true;
81
81
  }
82
+ /** GET/PUT /api/context-health/settings — read or toggle mitigation. */
83
+ export function handleHealthSettings(req, res, ctx) {
84
+ if (!req.url?.startsWith("/api/context-health/settings"))
85
+ return false;
86
+ try {
87
+ const pfReq = createRequire(import.meta.url);
88
+ const { getHealthMitigate, setHealthMitigate } = pfReq("../../src/store/sqlite.js");
89
+ if (req.method === "GET") {
90
+ const mitigate = getHealthMitigate(ctx.stateDir);
91
+ res.writeHead(200, { "Content-Type": "application/json" });
92
+ res.end(JSON.stringify({ mitigate }));
93
+ return true;
94
+ }
95
+ if (req.method === "PUT") {
96
+ let body = "";
97
+ req.on("data", (c) => (body += c));
98
+ req.on("end", () => {
99
+ try {
100
+ const parsed = JSON.parse(body);
101
+ if (typeof parsed.mitigate !== "boolean") {
102
+ res.writeHead(400, { "Content-Type": "application/json" });
103
+ res.end(JSON.stringify({ error: "mitigate must be boolean" }));
104
+ return;
105
+ }
106
+ setHealthMitigate(parsed.mitigate, ctx.stateDir);
107
+ res.writeHead(200, { "Content-Type": "application/json" });
108
+ res.end(JSON.stringify({ mitigate: parsed.mitigate }));
109
+ }
110
+ catch {
111
+ res.writeHead(400, { "Content-Type": "application/json" });
112
+ res.end(JSON.stringify({ error: "invalid_json" }));
113
+ }
114
+ });
115
+ return true;
116
+ }
117
+ res.writeHead(405, { "Content-Type": "application/json" });
118
+ res.end(JSON.stringify({ error: "method_not_allowed" }));
119
+ }
120
+ catch (e) {
121
+ res.writeHead(500, { "Content-Type": "application/json" });
122
+ res.end(JSON.stringify({ error: String(e) }));
123
+ }
124
+ return true;
125
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: local subprocess detection only
12
12
  import { createRequire } from "node:module";
13
- import { writeFileSync, mkdirSync } from "node:fs";
13
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  // ---------------------------------------------------------------------------
16
16
  // handleSetupStatus — "/api/setup-status"
@@ -24,7 +24,28 @@ function detectCurrentEmbedder() {
24
24
  return "minilm";
25
25
  return "trigram";
26
26
  }
27
- export function handleSetupStatus(req, res, _ctx) {
27
+ /** Read the configured embedder from the .mega-compact.env file (written by
28
+ * the configure endpoint). This shows what the user SELECTED even before pi
29
+ * restarts and loads the env file into process.env. */
30
+ function detectConfiguredEmbedder(stateDir) {
31
+ try {
32
+ const envPath = join(stateDir, ".mega-compact.env");
33
+ if (!existsSync(envPath))
34
+ return { embedder: "trigram", url: null };
35
+ const content = readFileSync(envPath, "utf-8");
36
+ const urlMatch = content.match(/^export\s+MEGACOMPACT_EMBEDDING_URL="([^"]+)"/m);
37
+ if (urlMatch)
38
+ return { embedder: "http", url: urlMatch[1] };
39
+ const minilmMatch = content.match(/^export\s+MEGACOMPACT_MINILM="?(.+?)"?\s*$/m);
40
+ if (minilmMatch && ["1", "true", "yes"].includes(minilmMatch[1].trim().toLowerCase()))
41
+ return { embedder: "minilm", url: null };
42
+ return { embedder: "trigram", url: null };
43
+ }
44
+ catch {
45
+ return { embedder: "trigram", url: null };
46
+ }
47
+ }
48
+ export function handleSetupStatus(req, res, ctx) {
28
49
  if (req.url !== "/api/setup-status")
29
50
  return false;
30
51
  if (req.method !== "GET") {
@@ -33,9 +54,14 @@ export function handleSetupStatus(req, res, _ctx) {
33
54
  res.end(JSON.stringify({ error: "method_not_allowed" }));
34
55
  return true;
35
56
  }
57
+ const configured = detectConfiguredEmbedder(ctx.stateDir);
58
+ const active = detectCurrentEmbedder();
36
59
  const body = {
37
- currentEmbedder: detectCurrentEmbedder(),
38
- embeddingUrl: process.env["MEGACOMPACT_EMBEDDING_URL"] ?? null,
60
+ currentEmbedder: active,
61
+ configuredEmbedder: configured.embedder,
62
+ configuredUrl: configured.url,
63
+ restartRequired: active !== configured.embedder,
64
+ embeddingUrl: process.env["MEGACOMPACT_EMBEDDING_URL"] ?? configured.url,
39
65
  embedCache: process.env["MEGACOMPACT_EMBED_CACHE"] ?? null,
40
66
  minilm: ["1", "true", "yes"].includes((process.env["MEGACOMPACT_MINILM"] ?? "").trim().toLowerCase()),
41
67
  };
@@ -16,4 +16,4 @@ export { handleMemoryStatus } from "./routes-memory.js";
16
16
  export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
17
17
  export { handleMemoryMap } from "./routes-memory-map.js";
18
18
  export { handleCacheStripes } from "./routes-cache.js";
19
- export { handleContextHealth, handleCachePoison } from "./routes-health.js";
19
+ export { handleContextHealth, handleCachePoison, handleHealthSettings } from "./routes-health.js";
@@ -12,7 +12,7 @@ import { join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { createRequire } from "node:module";
14
14
  import { log, setLogPath, setDashboardServerVersion } from "./state.js";
15
- import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleContextHealth, handleCachePoison, handleStatic, } from "./routes.js";
15
+ import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleContextHealth, handleCachePoison, handleHealthSettings, handleStatic, } from "./routes.js";
16
16
  export async function launchDashboardServer(stateDir) {
17
17
  // Our own package version — exposed at /api/version so the launcher can
18
18
  // detect a stale server (started by an older build) and replace it on
@@ -211,6 +211,8 @@ export async function launchDashboardServer(stateDir) {
211
211
  return;
212
212
  if (handleCachePoison(req, res, ctx))
213
213
  return;
214
+ if (handleHealthSettings(req, res, ctx))
215
+ return;
214
216
  handleStatic(req, res, ctx);
215
217
  });
216
218
  // Bind base + range are env-configurable so tests can use a private,
@@ -785,6 +785,32 @@ export function registerAgentHandlers(pi, runtime, config) {
785
785
  }
786
786
  }
787
787
  // v0.12: Context Health — compute + persist health score. Non-fatal.
788
- handleTurnEndHealth(event, runtime, config);
788
+ // Mitigation signals are acted on here (ctx is in scope at turn_end).
789
+ const healthSignal = handleTurnEndHealth(event, runtime, config);
790
+ if (healthSignal.forceCompact) {
791
+ runtime.logger?.info("health_mitigate_compact", {
792
+ composite: healthSignal.composite,
793
+ turnIndex: event.turnIndex,
794
+ });
795
+ try {
796
+ // Force a compaction to flush degraded context. Same race-guarded
797
+ // deferred path as the critical-over escape hatch above.
798
+ if (!piCompactWouldNoop(ctx)) {
799
+ ctx.compact();
800
+ }
801
+ }
802
+ catch {
803
+ /* non-fatal: mitigation never breaks the agent loop */
804
+ }
805
+ }
806
+ if (healthSignal.breakPrefix) {
807
+ runtime.logger?.info("health_mitigate_prefix_break", {
808
+ turnIndex: event.turnIndex,
809
+ });
810
+ // Invalidate the cached trim so the next context event regenerates
811
+ // from scratch — the prefix change forces a cache miss, bypassing
812
+ // the corrupted KV state.
813
+ runtime.trimCache = null;
814
+ }
789
815
  });
790
816
  }
@@ -14,6 +14,7 @@ import { computeTopicDrift, computeErrorEscalation, computePrefixInstability, co
14
14
  import { computePrefixHash, checkPrefixHash, evaluateCachePoison, } from "../../src/contextHealth/cachePoison.js";
15
15
  import { computeHealthScore, recordContextHealth, } from "../../src/contextHealth.js";
16
16
  import { recordCachePoisonEvent, } from "../../src/store/sqlite/context-health.js";
17
+ import { getHealthMitigate } from "../../src/store/sqlite/meta.js";
17
18
  const RING_MAX = 5;
18
19
  /** Extract assistant text from the turn_end event message. */
19
20
  function extractAssistantText(event) {
@@ -40,10 +41,12 @@ function extractMessageTexts(event) {
40
41
  }
41
42
  return [];
42
43
  }
43
- /** Handle turn_end: compute + persist context health. Non-fatal. */
44
+ /** Handle turn_end: compute + persist context health. Non-fatal.
45
+ * Returns mitigation signals for the caller to act on (agent-handlers has ctx). */
44
46
  export function handleTurnEndHealth(event, runtime, config) {
47
+ const noSignal = { forceCompact: false, breakPrefix: false, composite: 1 };
45
48
  if (!config.contextHealth)
46
- return;
49
+ return noSignal;
47
50
  try {
48
51
  const text = extractAssistantText(event);
49
52
  const embedder = defaultEmbedder();
@@ -138,6 +141,10 @@ export function handleTurnEndHealth(event, runtime, config) {
138
141
  cachePoison,
139
142
  turnIndex: event.turnIndex,
140
143
  });
144
+ // Mitigation: check the runtime-toggleable flag (meta table), not just
145
+ // the env var. The dashboard Maintenance tab toggles this at runtime.
146
+ const mitigate = config.contextHealthMitigate ||
147
+ getHealthMitigate(runtime.currentStateDir);
141
148
  // Update ring buffers
142
149
  runtime.recentTurnEmbeddings.push(emb);
143
150
  if (runtime.recentTurnEmbeddings.length > RING_MAX) {
@@ -147,10 +154,20 @@ export function handleTurnEndHealth(event, runtime, config) {
147
154
  if (runtime.recentErrorCategories.length > RING_MAX) {
148
155
  runtime.recentErrorCategories.shift();
149
156
  }
157
+ // Return mitigation signals so agent-handlers.ts (which has ctx) can act.
158
+ if (mitigate) {
159
+ return {
160
+ forceCompact: composite < 0.4,
161
+ breakPrefix: cachePoison < 0.3,
162
+ composite,
163
+ };
164
+ }
165
+ return { forceCompact: false, breakPrefix: false, composite };
150
166
  }
151
167
  catch (e) {
152
168
  runtime.logger?.error("context_health_failed", {
153
169
  error: String(e instanceof Error ? e.message : e),
154
170
  });
171
+ return noSignal;
155
172
  }
156
173
  }
@@ -155,26 +155,53 @@ export function buildTurnNodes(db, sessionId, nodes, edges) {
155
155
  // ---------------------------------------------------------------------------
156
156
  // Source B: turn-content nodes (from turns JOIN raw_transcript)
157
157
  // ---------------------------------------------------------------------------
158
- export function buildTurnContentNodes(db, sessionId, nodes, edges) {
158
+ export function buildTurnContentNodes(turnDb, mainDb, sessionId, nodes, edges) {
159
159
  // Local dedup only — DON'T skip ids from Source A (turn-structural). Source A
160
160
  // and Source B share the same node id (turn:<session>:<idx>); Gate 2
161
161
  // (identity_merge) merges them with the richest nodeType winning. Using the
162
162
  // global nodes array here would skip every node (Source A already added them).
163
163
  const seen = new Set();
164
- const rows = sessionId
165
- ? db
166
- .prepare(`SELECT t.turn_index, t.role, t.ended_at, r.content_bytes
167
- FROM turns t
168
- JOIN raw_transcript r ON r.session_id = t.session_id AND r.turn_index = t.turn_index
169
- WHERE t.session_id = ?
170
- ORDER BY t.turn_index ASC`)
164
+ // turns live in turns.db, raw_transcript in the main db — can't JOIN across
165
+ // databases, so query separately and merge in JS.
166
+ const turnRows = sessionId
167
+ ? turnDb
168
+ .prepare(`SELECT turn_index, role, ended_at
169
+ FROM turns
170
+ WHERE session_id = ?
171
+ ORDER BY turn_index ASC`)
171
172
  .all(sessionId)
172
- : db
173
- .prepare(`SELECT t.turn_index, t.role, t.ended_at, r.content_bytes
174
- FROM turns t
175
- JOIN raw_transcript r ON r.session_id = t.session_id AND r.turn_index = t.turn_index
176
- ORDER BY t.turn_index ASC`)
173
+ : turnDb
174
+ .prepare(`SELECT turn_index, role, ended_at
175
+ FROM turns
176
+ ORDER BY turn_index ASC`)
177
177
  .all();
178
+ // Build a map of (session_id, turn_index) → content from raw_transcript.
179
+ const contentMap = new Map();
180
+ try {
181
+ const transcriptRows = sessionId
182
+ ? mainDb
183
+ .prepare(`SELECT turn_index, content_bytes FROM raw_transcript WHERE session_id = ?`)
184
+ .all(sessionId)
185
+ : mainDb
186
+ .prepare(`SELECT turn_index, content_bytes FROM raw_transcript`)
187
+ .all();
188
+ for (const r of transcriptRows) {
189
+ contentMap.set(String(r.turn_index), String(r.content_bytes ?? ""));
190
+ }
191
+ }
192
+ catch {
193
+ // raw_transcript may not exist in the main db (dbMirror off) — return empty.
194
+ return;
195
+ }
196
+ // Merge: only produce nodes where both a turn row AND transcript content exist.
197
+ const rows = turnRows
198
+ .filter((t) => contentMap.has(String(t.turn_index)))
199
+ .map((t) => ({
200
+ turn_index: t.turn_index,
201
+ role: t.role,
202
+ ended_at: t.ended_at,
203
+ content_bytes: contentMap.get(String(t.turn_index)) ?? "",
204
+ }));
178
205
  if (rows.length === 0)
179
206
  return;
180
207
  const turnContentNodes = [];
@@ -206,7 +233,7 @@ export function buildTurnContentNodes(db, sessionId, nodes, edges) {
206
233
  if (turnContentNodes.length > 1) {
207
234
  const embeddings = turnContentNodes.map((n) => {
208
235
  try {
209
- return { id: n.id, vec: getOrComputeEmbedding(db, n.textSnippet) };
236
+ return { id: n.id, vec: getOrComputeEmbedding(mainDb, n.textSnippet) };
210
237
  }
211
238
  catch {
212
239
  return { id: n.id, vec: null };
@@ -229,7 +256,7 @@ export function buildTurnContentNodes(db, sessionId, nodes, edges) {
229
256
  }
230
257
  }
231
258
  }
232
- linkTurnToCheckpointEdges(db, sessionId, turnContentNodes, edges);
259
+ linkTurnToCheckpointEdges(mainDb, sessionId, turnContentNodes, edges);
233
260
  }
234
261
  nodes.push(...turnContentNodes);
235
262
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { getStateDir } from "./store.js";
16
16
  import { openStore } from "./store/sqlite/utils.js";
17
+ import { openTurnStore } from "./store/turns/connection.js";
17
18
  import { Logger } from "./log.js";
18
19
  import { runValidationPipeline } from "./memoryGraph/gates.js";
19
20
  import { buildCheckpointNodes, buildTurnNodes, buildTurnContentNodes, buildMemoryNodes, addRaptorAnnotations, deduplicateEdges, areTurnsEnabled, isTurnContentEnabled, isTurnContentFlaggedOn, areMemoriesEnabled, } from "./memoryGraph/sources.js";
@@ -83,13 +84,22 @@ function buildUnvalidatedGraph(sessionId, stateDir) {
83
84
  log.warn("graph_source_unavailable", { detail: "Cannot open store" });
84
85
  return { nodes: [], edges: [] };
85
86
  }
87
+ // Turns live in the isolated turns.db, not the main sqlite.db. Open it
88
+ // separately so buildTurnNodes/buildTurnContentNodes can query it.
89
+ let turnDb = null;
90
+ try {
91
+ turnDb = openTurnStore(stateDir);
92
+ }
93
+ catch {
94
+ log.warn("graph_source_unavailable", { detail: "Cannot open turns.db" });
95
+ }
86
96
  try {
87
97
  // Source: checkpoints (always on)
88
98
  buildCheckpointNodes(db, sessionId, nodes, edges);
89
- // Source A: turns (structural, metadata-only)
90
- if (areTurnsEnabled()) {
99
+ // Source A: turns (structural, metadata-only) — from turns.db, not main db
100
+ if (areTurnsEnabled() && turnDb) {
91
101
  try {
92
- buildTurnNodes(db, sessionId, nodes, edges);
102
+ buildTurnNodes(turnDb, sessionId, nodes, edges);
93
103
  }
94
104
  catch (err) {
95
105
  log.warn("graph_source_unavailable", {
@@ -99,9 +109,11 @@ function buildUnvalidatedGraph(sessionId, stateDir) {
99
109
  }
100
110
  }
101
111
  // Source B: turn content (raw_transcript join, embedding-based)
102
- if (isTurnContentEnabled()) {
112
+ // raw_transcript is in the main db, but turns are in turns.db — the JOIN
113
+ // can't span databases, so we pass both and merge in JS.
114
+ if (isTurnContentEnabled() && turnDb) {
103
115
  try {
104
- buildTurnContentNodes(db, sessionId, nodes, edges);
116
+ buildTurnContentNodes(turnDb, db, sessionId, nodes, edges);
105
117
  }
106
118
  catch (err) {
107
119
  log.warn("graph_source_unavailable", {
@@ -57,6 +57,17 @@ export function bumpDedupStats(deduped, stateDir = getStateDir()) {
57
57
  if (deduped)
58
58
  incMeta("deduped", 1, stateDir);
59
59
  }
60
+ // --- Context health mitigation (runtime-toggleable via dashboard) ----------
61
+ /** Read the context_health_mitigate flag from meta. Returns false if unset. */
62
+ export function getHealthMitigate(stateDir = getStateDir()) {
63
+ return getMeta("context_health_mitigate", stateDir) === "true";
64
+ }
65
+ /** Set the context_health_mitigate flag in meta (runtime-toggleable). */
66
+ export function setHealthMitigate(enabled, stateDir = getStateDir()) {
67
+ const db = openStore(stateDir);
68
+ db.prepare(`INSERT INTO meta(key, value) VALUES('context_health_mitigate', ?)
69
+ ON CONFLICT(key) DO UPDATE SET value = ?`).run(enabled ? "true" : "false", enabled ? "true" : "false");
70
+ }
60
71
  // --- Live dashboard counters (schemaless meta key/value — NO migration) -----
61
72
  // These reuse the private `incMeta` atomically-incrementing integer counter so
62
73
  // all cumulative tallies live in the same `meta` table as tokens_saved etc.
@@ -1,7 +1,23 @@
1
1
  /** Schema version stamp (written once into turns_meta). */
2
- const SCHEMA_VERSION = 1;
2
+ const SCHEMA_VERSION = 2;
3
3
  /** Create the unified turn-store schema if absent. Idempotent. */
4
4
  export function initTurnSchema(db) {
5
+ // Migration: v1 had a pressure_band CHECK constraint that only allowed
6
+ // 'green','yellow','red' — but the runtime sends 'low','medium','high',
7
+ // 'ultra','mega'. Every INSERT failed silently. Since the constraint
8
+ // blocked ALL writes, the turns table is guaranteed empty — safe to
9
+ // drop and recreate with the widened constraint.
10
+ try {
11
+ const tableSql = db
12
+ .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='turns'")
13
+ .get();
14
+ if (tableSql?.sql && tableSql.sql.includes("'green'") && !tableSql.sql.includes("'low'")) {
15
+ db.exec("DROP TABLE IF EXISTS turns");
16
+ }
17
+ }
18
+ catch {
19
+ /* non-fatal: table may not exist yet */
20
+ }
5
21
  // ── Contract tables (master's SqliteTurnStore shape) ──────────────────
6
22
  db.exec(`
7
23
  CREATE TABLE IF NOT EXISTS turns_meta (
@@ -22,7 +38,7 @@ export function initTurnSchema(db) {
22
38
  ended_at INTEGER NOT NULL,
23
39
  ctx_tokens INTEGER,
24
40
  ctx_percent REAL,
25
- pressure_band TEXT CHECK(pressure_band IS NULL OR pressure_band IN ('green','yellow','red')),
41
+ pressure_band TEXT CHECK(pressure_band IS NULL OR pressure_band IN ('low','medium','high','ultra','mega','green','yellow','red')),
26
42
  model TEXT,
27
43
  epoch_id TEXT,
28
44
  UNIQUE(conversation_id, turn_index)
@@ -1,2 +1,2 @@
1
- import{r as o,j as n}from"./index-B8k19B9g.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
- //# sourceMappingURL=AchievementTiles-DeQ0qnBX.js.map
1
+ import{r as o,j as n}from"./index-BEeaI9uo.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
+ //# sourceMappingURL=AchievementTiles-Cntu89Xw.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementTiles-DeQ0qnBX.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
1
+ {"version":3,"file":"AchievementTiles-Cntu89Xw.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
@@ -1,2 +1,2 @@
1
- import{r as t,n as m,j as s}from"./index-B8k19B9g.js";import{A as v}from"./AchievementTiles-DeQ0qnBX.js";function x(){const[r,o]=t.useState([]),[l,u]=t.useState(!0),[a,n]=t.useState(null),c=t.useCallback(async()=>{try{const e=await m();o(e),n(null)}catch(e){n(e instanceof Error?e:new Error(String(e)))}finally{u(!1)}},[]);return t.useEffect(()=>{let e=!0;const i=async()=>{e&&await c()};i();const h=setInterval(()=>void i(),15e3);return()=>{e=!1,clearInterval(h)}},[c]),l&&r.length===0?s.jsx("div",{className:"tab-stub",children:"Loading achievements…"}):a&&r.length===0?s.jsxs("div",{className:"tab-stub",children:["Error: ",a.message]}):s.jsxs("div",{className:"achievements-tab",children:[s.jsx("h2",{children:"Achievements"}),s.jsx(v,{achievements:r})]})}export{x as default};
2
- //# sourceMappingURL=AchievementsTab-Dtk_RJfs.js.map
1
+ import{r as t,n as m,j as s}from"./index-BEeaI9uo.js";import{A as v}from"./AchievementTiles-Cntu89Xw.js";function x(){const[r,o]=t.useState([]),[l,u]=t.useState(!0),[a,n]=t.useState(null),c=t.useCallback(async()=>{try{const e=await m();o(e),n(null)}catch(e){n(e instanceof Error?e:new Error(String(e)))}finally{u(!1)}},[]);return t.useEffect(()=>{let e=!0;const i=async()=>{e&&await c()};i();const h=setInterval(()=>void i(),15e3);return()=>{e=!1,clearInterval(h)}},[c]),l&&r.length===0?s.jsx("div",{className:"tab-stub",children:"Loading achievements…"}):a&&r.length===0?s.jsxs("div",{className:"tab-stub",children:["Error: ",a.message]}):s.jsxs("div",{className:"achievements-tab",children:[s.jsx("h2",{children:"Achievements"}),s.jsx(v,{achievements:r})]})}export{x as default};
2
+ //# sourceMappingURL=AchievementsTab-4ckY9z4n.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementsTab-Dtk_RJfs.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
1
+ {"version":3,"file":"AchievementsTab-4ckY9z4n.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
@@ -1,2 +1,2 @@
1
- import{j as e,r as h,u as j,k as R,l as P,i as E,c as $}from"./index-B8k19B9g.js";import{C as L,T as A}from"./TimeSavedCard-CC13EXij.js";import{b as v,c as u,d as H,e as M,f as o}from"./format-COjDIVGj.js";import{l as I}from"./pricing-CvE4NllQ.js";function N(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:s,className:t,title:l}){return e.jsxs("div",{className:"ov-stat-row",children:[e.jsx("span",{className:"ov-stat-label",title:l,children:a}),e.jsx("span",{className:`ov-stat-value${t?` ${t}`:""}`,children:s})]})}function w(a){return e.jsxs("div",{className:"card provider-cache-card",children:[e.jsx(r,{label:"Cache Hit Rate",value:`${a.hitPct.toFixed(1)}%`,className:N(a.hitPct)}),e.jsx(r,{label:"Turns Recorded",value:String(a.turnCount)}),e.jsx(r,{label:"Cache Read Tokens",value:v(a.totalCacheRead)}),e.jsx(r,{label:"Cache Write Tokens",value:v(a.totalCacheWrite)}),e.jsx(r,{label:"Input Tokens",value:v(a.totalInput)}),e.jsx(r,{label:"$ Saved (reads)",value:u(a.cacheReadSaved)}),e.jsx(r,{label:"Write Investment",value:u(a.cacheWriteCost)}),e.jsx(r,{label:"Net Saved",value:u(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(s=>{const t=I(s.model),l=t!=null?s.totalCacheRead/1e6*t:null,n=t!=null?s.totalCacheWrite/1e6*t:null;return e.jsxs("div",{className:"ov-stat-group",children:[e.jsx(r,{label:"Model",value:s.model}),e.jsx(r,{label:"Hit Rate",value:`${s.hitPct.toFixed(1)}%`,className:N(s.hitPct)}),e.jsx(r,{label:"Cache Read",value:v(s.totalCacheRead)}),e.jsx(r,{label:"Cache Write",value:v(s.totalCacheWrite)}),e.jsx(r,{label:"Samples",value:String(s.sampleCount)}),e.jsx(r,{label:"$ Saved (reads)",value:l!=null?u(l):"—"}),e.jsx(r,{label:"Write Investment",value:n!=null?u(n):"—"})]},s.model)})]})]})}const W=["ov-bar-green","ov-bar-blue","ov-bar-teal","ov-bar-yellow","ov-bar-gray"],D=["Permanent","Epoch","Topic","Thread","Volatile"],_={good:"ov-text-green",fair:"ov-text-yellow",degraded:"ov-text-orange",poor:"ov-text-red"};function F({data:a}){const s=Math.max(...a.buckets.map(l=>l.count),1),t=_[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 ${t}`,children:a.health.label}),e.jsx("span",{className:"ov-stripe-health-score",children:o(a.health.score)}),e.jsxs("span",{className:"ov-stripe-churn",children:["Churn: ",o(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(l=>{const n=s>0?Math.max(0,Math.min(100,l.count/s*100)):0,c=W[l.stripe]??"ov-bar-gray",i=l.label||(D[l.stripe]??`Stripe ${l.stripe}`);return e.jsxs("div",{className:"ov-stripe-row",children:[e.jsx("span",{className:"ov-stripe-label",title:i,children:i}),e.jsx("div",{className:"ov-bar-track",children:e.jsx("div",{className:`ov-bar-fill ${c}`,style:{width:`${n}%`}})}),e.jsx("span",{className:"ov-stripe-count",children:l.count}),e.jsx("span",{className:"ov-stripe-stability",title:`min ${o(l.minStability)} / max ${o(l.maxStability)}`,children:o(l.avgStability)})]},l.stripe)})}),e.jsxs("div",{className:"ov-stripe-footer",children:["Epoch: ",a.epochId??"—","· Dominant tier: ",o(a.health.dominantTier)]})]})}function m(a){return`${a.toFixed(1)}%`}function O(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 G=({perf:a})=>{const{cache_hit_pct:s}=a,t=s.samples&&s.samples.length>0?s.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:m(s.avg)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Latest: ",e.jsx("strong",{children:m(s.latest)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Samples: ",e.jsx("strong",{children:s.n})]})]}),t.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-sparkline",role:"img","aria-label":`Cache hit rate trend: ${s.n} samples, average ${m(s.avg)}, latest ${m(s.latest)}`,children:t.map((l,n)=>{const c=Math.max(2,Math.round(l.pct*.6)),i=O(l.pct),x=n===t.length-1;return e.jsx("div",{className:`ov-sparkline-bar ${i}${x?" ov-bar-latest":""}`,style:{height:`${c}px`},title:`${m(l.pct)} at ${new Date(l.ts).toLocaleTimeString()}`},n)})}),e.jsxs("div",{className:"ov-card-footer",children:[t.length," sample",t.length!==1?"s":""," over"," ",t.length>1?`${Math.round((t[t.length-1].ts-t[0].ts)/1e3)}s`:"latest turn"]})]}):e.jsx("div",{className:"ov-empty-state",children:"No cache-hit samples yet"})]})};function q(){const[a,s]=h.useState(!1),{data:t,loading:l,error:n}=j(h.useCallback(()=>R(),[]),{pollInterval:5e3}),{data:c,loading:i,error:x}=j(h.useCallback(()=>P(),[]),{pollInterval:15e3}),{data:g,loading:T,error:C}=j(h.useCallback(()=>E({minutes:30}),[]),{pollInterval:15e3}),{data:d,loading:y,error:S}=j(h.useCallback(()=>$(),[]),{pollInterval:5e3});return e.jsxs("div",{className:"cache-tab",children:[e.jsxs("div",{className:"card-section",children:[e.jsx("button",{type:"button",className:"info-toggle",onClick:()=>s(!a),children:a?"Hide Guidance":"Show Cache-Friendly Prompt Ordering Guidance"}),a&&e.jsxs("div",{className:"info-panel",children:[e.jsx("h3",{children:"Cache-Friendly Prompt Ordering"}),e.jsxs("ul",{children:[e.jsx("li",{children:"Keep system prompts and stable context at the top of your conversation — the provider caches the leading prefix."}),e.jsxs("li",{children:["Tool results and volatile content are automatically moved to the tail by message separation (",e.jsx("code",{children:"MEGACOMPACT_MESSAGE_SEPARATION"}),") so they don't invalidate the cache prefix."]}),e.jsxs("li",{children:["Cache striping (",e.jsx("code",{children:"MEGACOMPACT_CACHE_STRIPING"}),") further orders stable context by a stability score so the most durable chunks lead."]}),e.jsx("li",{children:"Avoid inserting new instructions mid-conversation — prepend them instead."})]}),d&&e.jsxs("div",{className:"cache-status-indicator",children:[e.jsx("strong",{children:"Status:"})," ",d.config.messageSeparation?"Separation Enabled":"Separation Disabled (Check env)"," |"," ",d.config.cacheStriping?"Striping Enabled":"Striping Disabled (Check env)"]})]})]}),e.jsx("h2",{children:"Provider Prompt Cache"}),l?e.jsx("p",{className:"tab-stub",children:"Loading..."}):n?e.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",n.message]}):t?e.jsx(w,{hitPct:t.cache.avgHitPct,turnCount:t.cache.turnCount,totalCacheRead:t.cache.totalCacheRead,totalCacheWrite:t.cache.totalCacheWrite,totalInput:t.cache.totalInput,cacheReadSaved:t.savings?.cacheReadSaved??null,cacheWriteCost:t.savings?.cacheWriteCost??null,netSaved:t.savings?.netSaved??null,modelLabel:t.savings?.model??null,firstTurnAt:t.cache.firstTurnAt,latestTurnAt:t.cache.latestTurnAt,byModel:t.cache.byModel??[]}):null,e.jsx("h2",{children:"Cache Stripe Distribution"}),i?e.jsx("p",{className:"tab-stub",children:"Loading..."}):x?e.jsxs("p",{className:"tab-stub",children:["Cache stripe data unavailable: ",x.message]}):c?e.jsx(F,{data:c}):null,e.jsx("h2",{children:"Cache Hit-Rate Trend"}),T?e.jsx("p",{className:"tab-stub",children:"Loading..."}):C?e.jsxs("p",{className:"tab-stub",children:["Hit-rate trend unavailable: ",C.message]}):g?e.jsx(G,{perf:g}):null,e.jsx("h2",{children:"Mega-Compact Dedup Cache"}),y?e.jsx("p",{className:"tab-stub",children:"Loading..."}):S?e.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",S.message]}):d&&(()=>{const{cacheHits:p,compacts:f,timeSaved:b}=d;return e.jsxs("div",{className:"card-grid overview-card-grid",children:[e.jsx(L,{cacheHitsSession:p.session,cacheHitsTotal:p.total,tokensSavedSession:p.sessionTokensSaved,tokensSavedTotal:p.totalTokensSaved,compactionsSession:f.session,compactionsTotal:f.total}),e.jsx(A,{compactSessionSec:b.compact.sessionSec,compactTotalSec:b.compact.totalSec,cacheHitSessionSec:b.cacheHit.sessionSec,cacheHitTotalSec:b.cacheHit.totalSec})]})})()]})}export{q as default};
2
- //# sourceMappingURL=CacheTab-kWcQt3PI.js.map
1
+ import{j as e,r as h,u as j,k as R,l as P,i as E,c as $}from"./index-BEeaI9uo.js";import{C as L,T as A}from"./TimeSavedCard-BBwvb4cC.js";import{b as v,c as u,d as H,e as M,f as o}from"./format-COjDIVGj.js";import{l as I}from"./pricing-CvE4NllQ.js";function N(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:s,className:t,title:l}){return e.jsxs("div",{className:"ov-stat-row",children:[e.jsx("span",{className:"ov-stat-label",title:l,children:a}),e.jsx("span",{className:`ov-stat-value${t?` ${t}`:""}`,children:s})]})}function w(a){return e.jsxs("div",{className:"card provider-cache-card",children:[e.jsx(r,{label:"Cache Hit Rate",value:`${a.hitPct.toFixed(1)}%`,className:N(a.hitPct)}),e.jsx(r,{label:"Turns Recorded",value:String(a.turnCount)}),e.jsx(r,{label:"Cache Read Tokens",value:v(a.totalCacheRead)}),e.jsx(r,{label:"Cache Write Tokens",value:v(a.totalCacheWrite)}),e.jsx(r,{label:"Input Tokens",value:v(a.totalInput)}),e.jsx(r,{label:"$ Saved (reads)",value:u(a.cacheReadSaved)}),e.jsx(r,{label:"Write Investment",value:u(a.cacheWriteCost)}),e.jsx(r,{label:"Net Saved",value:u(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(s=>{const t=I(s.model),l=t!=null?s.totalCacheRead/1e6*t:null,n=t!=null?s.totalCacheWrite/1e6*t:null;return e.jsxs("div",{className:"ov-stat-group",children:[e.jsx(r,{label:"Model",value:s.model}),e.jsx(r,{label:"Hit Rate",value:`${s.hitPct.toFixed(1)}%`,className:N(s.hitPct)}),e.jsx(r,{label:"Cache Read",value:v(s.totalCacheRead)}),e.jsx(r,{label:"Cache Write",value:v(s.totalCacheWrite)}),e.jsx(r,{label:"Samples",value:String(s.sampleCount)}),e.jsx(r,{label:"$ Saved (reads)",value:l!=null?u(l):"—"}),e.jsx(r,{label:"Write Investment",value:n!=null?u(n):"—"})]},s.model)})]})]})}const W=["ov-bar-green","ov-bar-blue","ov-bar-teal","ov-bar-yellow","ov-bar-gray"],D=["Permanent","Epoch","Topic","Thread","Volatile"],_={good:"ov-text-green",fair:"ov-text-yellow",degraded:"ov-text-orange",poor:"ov-text-red"};function F({data:a}){const s=Math.max(...a.buckets.map(l=>l.count),1),t=_[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 ${t}`,children:a.health.label}),e.jsx("span",{className:"ov-stripe-health-score",children:o(a.health.score)}),e.jsxs("span",{className:"ov-stripe-churn",children:["Churn: ",o(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(l=>{const n=s>0?Math.max(0,Math.min(100,l.count/s*100)):0,c=W[l.stripe]??"ov-bar-gray",i=l.label||(D[l.stripe]??`Stripe ${l.stripe}`);return e.jsxs("div",{className:"ov-stripe-row",children:[e.jsx("span",{className:"ov-stripe-label",title:i,children:i}),e.jsx("div",{className:"ov-bar-track",children:e.jsx("div",{className:`ov-bar-fill ${c}`,style:{width:`${n}%`}})}),e.jsx("span",{className:"ov-stripe-count",children:l.count}),e.jsx("span",{className:"ov-stripe-stability",title:`min ${o(l.minStability)} / max ${o(l.maxStability)}`,children:o(l.avgStability)})]},l.stripe)})}),e.jsxs("div",{className:"ov-stripe-footer",children:["Epoch: ",a.epochId??"—","· Dominant tier: ",o(a.health.dominantTier)]})]})}function m(a){return`${a.toFixed(1)}%`}function O(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 G=({perf:a})=>{const{cache_hit_pct:s}=a,t=s.samples&&s.samples.length>0?s.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:m(s.avg)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Latest: ",e.jsx("strong",{children:m(s.latest)})]}),e.jsxs("div",{className:"ov-perf-stat",children:["Samples: ",e.jsx("strong",{children:s.n})]})]}),t.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"ov-sparkline",role:"img","aria-label":`Cache hit rate trend: ${s.n} samples, average ${m(s.avg)}, latest ${m(s.latest)}`,children:t.map((l,n)=>{const c=Math.max(2,Math.round(l.pct*.6)),i=O(l.pct),x=n===t.length-1;return e.jsx("div",{className:`ov-sparkline-bar ${i}${x?" ov-bar-latest":""}`,style:{height:`${c}px`},title:`${m(l.pct)} at ${new Date(l.ts).toLocaleTimeString()}`},n)})}),e.jsxs("div",{className:"ov-card-footer",children:[t.length," sample",t.length!==1?"s":""," over"," ",t.length>1?`${Math.round((t[t.length-1].ts-t[0].ts)/1e3)}s`:"latest turn"]})]}):e.jsx("div",{className:"ov-empty-state",children:"No cache-hit samples yet"})]})};function q(){const[a,s]=h.useState(!1),{data:t,loading:l,error:n}=j(h.useCallback(()=>R(),[]),{pollInterval:5e3}),{data:c,loading:i,error:x}=j(h.useCallback(()=>P(),[]),{pollInterval:15e3}),{data:g,loading:T,error:C}=j(h.useCallback(()=>E({minutes:30}),[]),{pollInterval:15e3}),{data:d,loading:y,error:S}=j(h.useCallback(()=>$(),[]),{pollInterval:5e3});return e.jsxs("div",{className:"cache-tab",children:[e.jsxs("div",{className:"card-section",children:[e.jsx("button",{type:"button",className:"info-toggle",onClick:()=>s(!a),children:a?"Hide Guidance":"Show Cache-Friendly Prompt Ordering Guidance"}),a&&e.jsxs("div",{className:"info-panel",children:[e.jsx("h3",{children:"Cache-Friendly Prompt Ordering"}),e.jsxs("ul",{children:[e.jsx("li",{children:"Keep system prompts and stable context at the top of your conversation — the provider caches the leading prefix."}),e.jsxs("li",{children:["Tool results and volatile content are automatically moved to the tail by message separation (",e.jsx("code",{children:"MEGACOMPACT_MESSAGE_SEPARATION"}),") so they don't invalidate the cache prefix."]}),e.jsxs("li",{children:["Cache striping (",e.jsx("code",{children:"MEGACOMPACT_CACHE_STRIPING"}),") further orders stable context by a stability score so the most durable chunks lead."]}),e.jsx("li",{children:"Avoid inserting new instructions mid-conversation — prepend them instead."})]}),d&&e.jsxs("div",{className:"cache-status-indicator",children:[e.jsx("strong",{children:"Status:"})," ",d.config.messageSeparation?"Separation Enabled":"Separation Disabled (Check env)"," |"," ",d.config.cacheStriping?"Striping Enabled":"Striping Disabled (Check env)"]})]})]}),e.jsx("h2",{children:"Provider Prompt Cache"}),l?e.jsx("p",{className:"tab-stub",children:"Loading..."}):n?e.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",n.message]}):t?e.jsx(w,{hitPct:t.cache.avgHitPct,turnCount:t.cache.turnCount,totalCacheRead:t.cache.totalCacheRead,totalCacheWrite:t.cache.totalCacheWrite,totalInput:t.cache.totalInput,cacheReadSaved:t.savings?.cacheReadSaved??null,cacheWriteCost:t.savings?.cacheWriteCost??null,netSaved:t.savings?.netSaved??null,modelLabel:t.savings?.model??null,firstTurnAt:t.cache.firstTurnAt,latestTurnAt:t.cache.latestTurnAt,byModel:t.cache.byModel??[]}):null,e.jsx("h2",{children:"Cache Stripe Distribution"}),i?e.jsx("p",{className:"tab-stub",children:"Loading..."}):x?e.jsxs("p",{className:"tab-stub",children:["Cache stripe data unavailable: ",x.message]}):c?e.jsx(F,{data:c}):null,e.jsx("h2",{children:"Cache Hit-Rate Trend"}),T?e.jsx("p",{className:"tab-stub",children:"Loading..."}):C?e.jsxs("p",{className:"tab-stub",children:["Hit-rate trend unavailable: ",C.message]}):g?e.jsx(G,{perf:g}):null,e.jsx("h2",{children:"Mega-Compact Dedup Cache"}),y?e.jsx("p",{className:"tab-stub",children:"Loading..."}):S?e.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",S.message]}):d&&(()=>{const{cacheHits:p,compacts:f,timeSaved:b}=d;return e.jsxs("div",{className:"card-grid overview-card-grid",children:[e.jsx(L,{cacheHitsSession:p.session,cacheHitsTotal:p.total,tokensSavedSession:p.sessionTokensSaved,tokensSavedTotal:p.totalTokensSaved,compactionsSession:f.session,compactionsTotal:f.total}),e.jsx(A,{compactSessionSec:b.compact.sessionSec,compactTotalSec:b.compact.totalSec,cacheHitSessionSec:b.cacheHit.sessionSec,cacheHitTotalSec:b.cacheHit.totalSec})]})})()]})}export{q as default};
2
+ //# sourceMappingURL=CacheTab-BfGJkiaf.js.map