pi-mega-compact 0.20.58 → 0.20.60

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 (91) hide show
  1. package/dist/config/vector-cortex-dash-0b.js +21 -0
  2. package/dist/config/vector-cortex-dash-0c.js +23 -0
  3. package/dist/config/vector-cortex.js +4 -0
  4. package/dist/config.js +1 -1
  5. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
  6. package/dist/src/config/vector-cortex-dash-0b.js +21 -0
  7. package/dist/src/config/vector-cortex-dash-0c.js +23 -0
  8. package/dist/src/config/vector-cortex.js +4 -0
  9. package/dist/src/config.js +1 -1
  10. package/extensions/dashboard-client/dist/assets/{AreaChart-CtJ6dSMT.js → AreaChart-B5Is2_EN.js} +2 -2
  11. package/extensions/dashboard-client/dist/assets/{AreaChart-CtJ6dSMT.js.map → AreaChart-B5Is2_EN.js.map} +1 -1
  12. package/extensions/dashboard-client/dist/assets/{BarChart-DJ3j3xDz.js → BarChart-Bj2oT871.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{BarChart-DJ3j3xDz.js.map → BarChart-Bj2oT871.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/CacheTab-DuhLeKHW.js +2 -0
  15. package/extensions/dashboard-client/dist/assets/CacheTab-DuhLeKHW.js.map +1 -0
  16. package/extensions/dashboard-client/dist/assets/{EventsTab-DF-Q3o7x.js → EventsTab-BG6L8uWh.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{EventsTab-DF-Q3o7x.js.map → EventsTab-BG6L8uWh.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{HealthTab-Dr1HGOkl.js → HealthTab-CZmGCUJh.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{HealthTab-Dr1HGOkl.js.map → HealthTab-CZmGCUJh.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/HydeDetailPanel-B-P1jTtk.js +5 -0
  21. package/extensions/dashboard-client/dist/assets/HydeDetailPanel-B-P1jTtk.js.map +1 -0
  22. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-zKhR18_s.js → MaintenanceTab-CoZ1mSjU.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-zKhR18_s.js.map → MaintenanceTab-CoZ1mSjU.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DekIzfn2.js → MemoryMapTab-BwVaWniY.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DekIzfn2.js.map → MemoryMapTab-BwVaWniY.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MetricsTab-CEKajY38.js → MetricsCards-BCJoHe_n.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/MetricsCards-BCJoHe_n.js.map +1 -0
  28. package/extensions/dashboard-client/dist/assets/MetricsTab-UQSqUGXe.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/MetricsTab-UQSqUGXe.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/{OverviewTab-24sG8xiE.js → OverviewTab-qi12IYvs.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{OverviewTab-24sG8xiE.js.map → OverviewTab-qi12IYvs.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{ReposTab-70gkV8l9.js → ReposTab-CcHZPzOO.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{ReposTab-70gkV8l9.js.map → ReposTab-CcHZPzOO.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/SessionsTab-nzd1408Z.js +2 -0
  35. package/extensions/dashboard-client/dist/assets/SessionsTab-nzd1408Z.js.map +1 -0
  36. package/extensions/dashboard-client/dist/assets/SetupTab-DPrG3s9V.js +3 -0
  37. package/extensions/dashboard-client/dist/assets/{SetupTab-tbPrrfYx.js.map → SetupTab-DPrG3s9V.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B6mdtCWE.js → TimeSavedCard-DhfdRM3_.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B6mdtCWE.js.map → TimeSavedCard-DhfdRM3_.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/TurnsTab-oalBNLVr.js +2 -0
  41. package/extensions/dashboard-client/dist/assets/TurnsTab-oalBNLVr.js.map +1 -0
  42. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-SDThp0R0.js → VcStatusBadge-XGMW6u62.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-SDThp0R0.js.map → VcStatusBadge-XGMW6u62.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BACHKLjU.js +2 -0
  45. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BACHKLjU.js.map +1 -0
  46. package/extensions/dashboard-client/dist/assets/{WikiTab-Cki48_8N.js → WikiTab-CGQWHuRq.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{WikiTab-Cki48_8N.js.map → WikiTab-CGQWHuRq.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{button-CojFYd7q.js → button-SyOWCGkp.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{button-CojFYd7q.js.map → button-SyOWCGkp.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{card-Cxeeje9o.js → card-69DCTMel.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{card-Cxeeje9o.js.map → card-69DCTMel.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{client-extra-DLxyGxW8.js → client-extra-CgeDg1Md.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{client-extra-DLxyGxW8.js.map → client-extra-CgeDg1Md.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRlqBks2.js → generateCategoricalChart-BwTtrqm5.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRlqBks2.js.map → generateCategoricalChart-BwTtrqm5.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{index-CkbdlPBm.js → index-BZcYzeCr.js} +3 -3
  57. package/extensions/dashboard-client/dist/assets/{index-CkbdlPBm.js.map → index-BZcYzeCr.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{switch-Cx_nl-Ba.js → switch-DhQfmSFD.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{switch-Cx_nl-Ba.js.map → switch-DhQfmSFD.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{toggle-CfIFzp6P.js → toggle-B3gNAVog.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{toggle-CfIFzp6P.js.map → toggle-B3gNAVog.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{useSSE-B9_N7ccG.js → useSSE-BkkjZ4bO.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{useSSE-B9_N7ccG.js.map → useSSE-BkkjZ4bO.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/index.html +1 -1
  65. package/extensions/dashboard-client/src/tabs/AdminTab.tsx +81 -0
  66. package/extensions/dashboard-client/src/tabs/CacheTab/MetricsCards.tsx +66 -0
  67. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +46 -0
  68. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +10 -51
  69. package/extensions/dashboard-client/src/tabs/SessionsTab/TurnMemoryView.tsx +383 -0
  70. package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +77 -6
  71. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +33 -4
  72. package/extensions/dashboard-client/src/tabs/VectorCortexTab/sections.tsx +201 -0
  73. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +168 -122
  74. package/extensions/dashboard-client/src/tabs/index.ts +31 -0
  75. package/extensions/dashboard-client/src/tabs/registry.ts +34 -0
  76. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
  77. package/package.json +1 -1
  78. package/src/config/vector-cortex-dash-0b.ts +23 -0
  79. package/src/config/vector-cortex-dash-0c.ts +25 -0
  80. package/src/config/vector-cortex.ts +4 -0
  81. package/src/config.ts +2 -0
  82. package/extensions/dashboard-client/dist/assets/CacheTab-CXU1_9lR.js +0 -2
  83. package/extensions/dashboard-client/dist/assets/CacheTab-CXU1_9lR.js.map +0 -1
  84. package/extensions/dashboard-client/dist/assets/MetricsTab-CEKajY38.js.map +0 -1
  85. package/extensions/dashboard-client/dist/assets/SessionsTab-B3quvYPZ.js +0 -2
  86. package/extensions/dashboard-client/dist/assets/SessionsTab-B3quvYPZ.js.map +0 -1
  87. package/extensions/dashboard-client/dist/assets/SetupTab-tbPrrfYx.js +0 -3
  88. package/extensions/dashboard-client/dist/assets/TurnsTab-CmhRJJqr.js +0 -5
  89. package/extensions/dashboard-client/dist/assets/TurnsTab-CmhRJJqr.js.map +0 -1
  90. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DDjCkTjk.js +0 -2
  91. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DDjCkTjk.js.map +0 -1
@@ -18,6 +18,7 @@ import type {
18
18
  CacheStripesResponse,
19
19
  PerfResponse,
20
20
  PrefixStabilityResponse,
21
+ SettingsResponse,
21
22
  } from "@contracts";
22
23
  import { useApi } from "../hooks/useApi";
23
24
  import {
@@ -26,6 +27,7 @@ import {
26
27
  fetchCacheStripes,
27
28
  fetchPrefixStability,
28
29
  fetchPerf,
30
+ fetchSettings,
29
31
  } from "../api/client";
30
32
  import { CacheHitsCard } from "../components/CacheHitsCard";
31
33
  import { TimeSavedCard } from "../components/TimeSavedCard";
@@ -35,6 +37,25 @@ import { CacheHitRateTrendCard } from "../components/CacheHitRateTrendCard";
35
37
  import { PrefixStabilityCard } from "../components/PrefixStabilityCard";
36
38
  import { Button } from "../components/ui/button";
37
39
  import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
40
+ import { MetricsCards } from "./CacheTab/MetricsCards";
41
+
42
+ const DASH_0C_KEY = "MEGACOMPACT_DASH_0C";
43
+
44
+ /** Resolve the DASH-0c consolidation flag from the server settings state.
45
+ * The dashboard client is a browser bundle with NO `process` global, so the
46
+ * positive sprint flag is read from the server-authoritative /api/rag-settings
47
+ * state (the server resolves MEGACOMPACT_DASH_0C into a SettingState boolean).
48
+ * Absent/not-yet-loaded => false (flag-off posture), so flag-off users never
49
+ * see the Performance section flash; it mounts only once settings confirm ON. */
50
+ function dash0cEnabled(settings: SettingsResponse | null): boolean {
51
+ if (!settings) return false;
52
+ for (const cat of settings.categories) {
53
+ for (const s of cat.settings) {
54
+ if (s.key === DASH_0C_KEY && s.type === "boolean") return s.value === true;
55
+ }
56
+ }
57
+ return false;
58
+ }
38
59
 
39
60
  export default function CacheTab(): React.ReactElement {
40
61
  const [infoExpanded, setInfoExpanded] = useState(false);
@@ -87,6 +108,13 @@ export default function CacheTab(): React.ReactElement {
87
108
  { pollInterval: 5000 },
88
109
  );
89
110
 
111
+ /* --- DASH-0c: server-resolved settings state for the flag gate --- */
112
+ const { data: settingsData } = useApi<SettingsResponse>(
113
+ useCallback(() => fetchSettings(), []),
114
+ { pollInterval: 0, maxRetries: 0 },
115
+ );
116
+ const dash0cOn = dash0cEnabled(settingsData);
117
+
90
118
  return (
91
119
  <div className="flex flex-col gap-4">
92
120
  <div>
@@ -223,6 +251,24 @@ export default function CacheTab(): React.ReactElement {
223
251
  );
224
252
  })()
225
253
  )}
254
+ {/* ==============================================================
255
+ Section 5 -- Performance (DASH-0c)
256
+ The metrics body (ModelBadge + PerfChart + PerfCards + RagDashboard)
257
+ absorbed from MetricsTab as a Performance section. Moved VERBATIM to
258
+ ./CacheTab/MetricsCards.tsx. Omitted entirely when flag-off so the
259
+ cache-only body is byte-identical to the predecessor.
260
+ ============================================================== */}
261
+ {dash0cOn && (
262
+ <section aria-labelledby="cache-perf-cards">
263
+ <h2
264
+ id="cache-perf-cards"
265
+ className="font-heading text-lg font-semibold"
266
+ >
267
+ Performance
268
+ </h2>
269
+ <MetricsCards />
270
+ </section>
271
+ )}
226
272
  </div>
227
273
  );
228
274
  }
@@ -1,58 +1,17 @@
1
1
  /**
2
- * dashboard-client/src/tabs/MetricsTab.tsx — Metrics tab (C2).
2
+ * dashboard-client/src/tabs/MetricsTab.tsx — Metrics tab (C2) shell.
3
3
  *
4
- * Fetches /api/perf (rolling-window aggregates) + /api/snapshot (for model).
5
- * Renders ModelBadge + PerfChart + 5 perf cards (latency, throughput, process,
6
- * snapshot cost, TUI lag proxy). Polls every 10s.
4
+ * DASH-0c: the Cache+Performance surface absorbs the metrics body as a
5
+ * Performance section; this file is reduced to a shell that re-exports
6
+ * `MetricsCards` (the byte-preserved body moved to ./CacheTab/MetricsCards.tsx).
7
+ * Kept as a deep-link anchor for #metrics and for rollback symmetry (flag-off
8
+ * renders MetricsTab as a standalone top-level surface). A DASH-0d cleanup deletes
9
+ * this standalone copy after a deep-link audit proves no live consumer points
10
+ * at it. The render body is otherwise identical to the pre-DASH-0c MetricsTab.
7
11
  */
8
12
 
9
- import type React from "react";
10
- import { useCallback } from "react";
11
- import { useApi } from "../hooks/useApi";
12
- import { fetchPerf, fetchSnapshot } from "../api/client";
13
- import type { PerfResponse, SnapshotResponse } from "@contracts";
14
- import { NEW_UI } from "../config";
15
- import { PerfChart } from "../components/PerfChart";
16
- import { PerfCards } from "../components/PerfCards";
17
- import { ModelBadge } from "../components/ModelBadge";
18
- import { RagDashboard } from "../components/RagDashboard";
13
+ import { MetricsCards } from "./CacheTab/MetricsCards";
19
14
 
20
15
  export default function MetricsTab(): React.ReactElement {
21
- const { data: perf, error: perfErr } = useApi<PerfResponse>(
22
- useCallback(() => fetchPerf({ minutes: 30 }), []),
23
- { pollInterval: 10_000 },
24
- );
25
- const { data: snapshot } = useApi<SnapshotResponse>(
26
- useCallback(() => fetchSnapshot(), []),
27
- { pollInterval: 10_000 },
28
- );
29
-
30
- if (perfErr && !perf) {
31
- return (
32
- <div className="tab-stub">Error loading perf: {perfErr.message}</div>
33
- );
34
- }
35
- if (!perf) {
36
- return <div className="tab-stub">Loading perf…</div>;
37
- }
38
-
39
- return (
40
- <div className="flex flex-col gap-4">
41
- {snapshot?.model && (
42
- <ModelBadge
43
- name={snapshot.model.name}
44
- providerName={snapshot.model.providerName}
45
- provider={snapshot.model.provider}
46
- inputRate={snapshot.model.inputRate}
47
- outputRate={snapshot.model.outputRate}
48
- />
49
- )}
50
- <PerfChart perf={perf} />
51
- <PerfCards perf={perf} />
52
- <div className="text-xs text-muted-foreground">
53
- {perf.sampleCount} samples · updated {perf.updatedAt}
54
- </div>
55
- {NEW_UI() && <RagDashboard />}
56
- </div>
57
- );
16
+ return <MetricsCards />;
58
17
  }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * dashboard-client/src/tabs/SessionsTab/TurnMemoryView.tsx — Turn-by-turn memory
3
+ * tracking + recall (S52), moved VERBATIM from the top-level TurnsTab.tsx.
4
+ *
5
+ * DASH-0b: the Sessions surface absorbs the existent TurnsTab as a drill-down.
6
+ * This file holds the byte-preserved turns body (the `TurnMemoryView`
7
+ * component). The standalone `tabs/TurnsTab.tsx` is LEFT UNTOUCHED this sprint —
8
+ * its copy stays live for flag-off / pre-DASH parity. A DASH-0d cleanup deletes
9
+ * the standalone copy after a deep-link audit proves no live consumer points
10
+ * at it. Only the export name changed (TurnsTab → TurnMemoryView); the render
11
+ * body is otherwise identical.
12
+ *
13
+ * Fetches /api/turns (conversation list), expands the active conversation into
14
+ * per-turn detail (/api/turns/conversation/:id), and renders:
15
+ * - Conversation summary list (turn count, recall total, epochs, last turn).
16
+ * - Per-turn rows: turn index, role, ctx tokens/percent + pressure band,
17
+ * epoch id, and the injected-checkpoint recall set (the memory recalled
18
+ * at each turn, with score + source).
19
+ * - Pending rewind intents (S52A) + Fork action (S50C primitive).
20
+ *
21
+ * Read-mostly via /api/turns; fork/intent/prune via POST. No absolute URLs
22
+ * (PREVENT-PI-004: loopback-only, same-origin static bundle).
23
+ */
24
+
25
+ import type React from "react";
26
+ import { useCallback, useState } from "react";
27
+ import { useApi } from "../../hooks/useApi";
28
+ import {
29
+ fetchTurns,
30
+ fetchConversationTurns,
31
+ fetchTurnIntents,
32
+ postFork,
33
+ postTurnIntent,
34
+ } from "../../api/client";
35
+ import type {
36
+ TurnsResponse,
37
+ ConversationTurnsResponse,
38
+ RewindIntentsResponse,
39
+ TurnRow,
40
+ ConversationSummary,
41
+ } from "@contracts";
42
+ import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
43
+ import { Badge } from "../../components/ui/badge";
44
+ import { Button } from "../../components/ui/button";
45
+ import { HydeDetailPanel } from "../../components/HydeDetailPanel";
46
+
47
+ function fmtTs(ms: number): string {
48
+ if (!ms) return "—";
49
+ return new Date(ms).toLocaleString();
50
+ }
51
+
52
+ function bandVariant(band: TurnRow["pressureBand"]): "success" | "warning" | "danger" | "default" {
53
+ if (band === "green") return "success";
54
+ if (band === "yellow") return "warning";
55
+ if (band === "red") return "danger";
56
+ return "default";
57
+ }
58
+
59
+ function sourceLabel(s: TurnRow["recall"][number]["source"]): string {
60
+ if (s === "checkpoint") return "ckpt";
61
+ if (s === "cluster_summary") return "raptor";
62
+ return "mem";
63
+ }
64
+
65
+ export function TurnMemoryView(): React.ReactElement {
66
+ const {
67
+ data: turns,
68
+ loading,
69
+ error,
70
+ } = useApi<TurnsResponse>(
71
+ useCallback(() => fetchTurns(), []),
72
+ {
73
+ pollInterval: 10_000,
74
+ },
75
+ );
76
+ const { data: intents } = useApi<RewindIntentsResponse>(
77
+ useCallback(() => fetchTurnIntents(), []),
78
+ { pollInterval: 5_000 },
79
+ );
80
+ const [expanded, setExpanded] = useState<string | null>(null);
81
+ const [detail, setDetail] = useState<ConversationTurnsResponse | null>(null);
82
+ const [detailLoading, setDetailLoading] = useState(false);
83
+ const [busy, setBusy] = useState<string | null>(null);
84
+ const [notice, setNotice] = useState<string | null>(null);
85
+
86
+ const expand = useCallback(
87
+ async (conversationId: string) => {
88
+ if (expanded === conversationId) {
89
+ setExpanded(null);
90
+ setDetail(null);
91
+ return;
92
+ }
93
+ setExpanded(conversationId);
94
+ setDetailLoading(true);
95
+ setDetail(null);
96
+ try {
97
+ const d = await fetchConversationTurns(conversationId);
98
+ setDetail(d);
99
+ } finally {
100
+ setDetailLoading(false);
101
+ }
102
+ },
103
+ [expanded],
104
+ );
105
+
106
+ const onFork = useCallback(
107
+ async (conversationId: string, turnIndex: number) => {
108
+ setBusy(`fork:${conversationId}:${turnIndex}`);
109
+ setNotice(null);
110
+ try {
111
+ const out = await postFork(conversationId, turnIndex);
112
+ setNotice(
113
+ `Forked → ${out.childConversationId} (${out.recalledCount} recalled)`,
114
+ );
115
+ } catch (e) {
116
+ setNotice(`Fork failed: ${(e as Error).message}`);
117
+ } finally {
118
+ setBusy(null);
119
+ }
120
+ },
121
+ [],
122
+ );
123
+
124
+ const onRewind = useCallback(
125
+ async (conversationId: string, turnIndex: number) => {
126
+ setBusy(`rewind:${conversationId}:${turnIndex}`);
127
+ setNotice(null);
128
+ try {
129
+ await postTurnIntent(conversationId, turnIndex);
130
+ setNotice(`Rewind intent queued for turn ${turnIndex}`);
131
+ } catch (e) {
132
+ setNotice(`Rewind failed: ${(e as Error).message}`);
133
+ } finally {
134
+ setBusy(null);
135
+ }
136
+ },
137
+ [],
138
+ );
139
+
140
+ if (error && !turns) {
141
+ return <div className="tab-stub">Error loading turns: {error.message}</div>;
142
+ }
143
+ if (loading && !turns) {
144
+ return <div className="tab-stub">Loading turns…</div>;
145
+ }
146
+ if (!turns || turns.conversations.length === 0) {
147
+ return (
148
+ <div className="tab-stub">
149
+ <h3>Turn-by-turn memory</h3>
150
+ <p>
151
+ No turns recorded yet. Per-turn tracking starts when{" "}
152
+ <code>MEGACOMPACT_TURNS_DB=1</code> and the session runs through a
153
+ compaction. Each turn records its context metrics + the checkpoints
154
+ recalled into it.
155
+ </p>
156
+ </div>
157
+ );
158
+ }
159
+
160
+ return (
161
+ <div className="flex flex-col gap-4">
162
+ <h3 className="font-heading text-lg font-semibold">
163
+ Turn-by-turn memory tracking + recall
164
+ </h3>
165
+ {notice && <Badge variant="warning">{notice}</Badge>}
166
+
167
+ {intents && intents.intents.length > 0 && (
168
+ <Card>
169
+ <CardHeader>
170
+ <CardTitle>
171
+ Pending rewind intents ({intents.intents.length})
172
+ </CardTitle>
173
+ </CardHeader>
174
+ <CardContent>
175
+ <ul className="list-disc space-y-1 pl-5">
176
+ {intents.intents.map((i) => (
177
+ <li key={i.id}>
178
+ {fmtTs(i.createdAt)} — rewind{" "}
179
+ <code>{i.conversationId}</code> to turn {i.targetTurnIndex}
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ </CardContent>
184
+ </Card>
185
+ )}
186
+
187
+ <Card>
188
+ <CardHeader>
189
+ <CardTitle>
190
+ Conversations ({turns.conversations.length})
191
+ </CardTitle>
192
+ </CardHeader>
193
+ <CardContent>
194
+ <table className="w-full border-collapse text-sm">
195
+ <thead>
196
+ <tr>
197
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Conversation</th>
198
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Turns</th>
199
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Recall</th>
200
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Epochs</th>
201
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Avg ctx%</th>
202
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Last turn</th>
203
+ <th className="border-b border-border px-3 py-2"></th>
204
+ </tr>
205
+ </thead>
206
+ <tbody>
207
+ {turns.conversations.map((c: ConversationSummary) => (
208
+ <ConversationRow
209
+ key={c.conversationId}
210
+ c={c}
211
+ active={turns.activeConversationId === c.conversationId}
212
+ expanded={expanded === c.conversationId}
213
+ onExpand={() => expand(c.conversationId)}
214
+ />
215
+ ))}
216
+ </tbody>
217
+ </table>
218
+ </CardContent>
219
+ </Card>
220
+
221
+ {expanded && (
222
+ <Card>
223
+ <CardHeader>
224
+ <CardTitle>
225
+ Turns in <code>{expanded}</code>
226
+ </CardTitle>
227
+ </CardHeader>
228
+ <CardContent>
229
+ {detailLoading ? (
230
+ <div className="text-sm text-muted-foreground">Loading turns…</div>
231
+ ) : detail ? (
232
+ <TurnDetail
233
+ detail={detail}
234
+ busy={busy}
235
+ onFork={onFork}
236
+ onRewind={onRewind}
237
+ />
238
+ ) : (
239
+ <div className="text-sm text-muted-foreground">No detail.</div>
240
+ )}
241
+ </CardContent>
242
+ </Card>
243
+ )}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ function ConversationRow({
249
+ c,
250
+ active,
251
+ expanded,
252
+ onExpand,
253
+ }: {
254
+ c: ConversationSummary;
255
+ active: boolean;
256
+ expanded: boolean;
257
+ onExpand: () => void;
258
+ }): React.ReactElement {
259
+ return (
260
+ <>
261
+ <tr
262
+ className={`border-b border-border/50 hover:bg-bg-elevated/40 ${
263
+ active ? "bg-primary/10" : ""
264
+ }`}
265
+ onClick={onExpand}
266
+ style={{ cursor: "pointer" }}
267
+ >
268
+ <td className="px-3 py-2">
269
+ {expanded ? "▾" : "▸"} <code>{c.conversationId}</code>
270
+ {active && <Badge variant="accent" className="ml-2">active</Badge>}
271
+ </td>
272
+ <td className="px-3 py-2 text-right">{c.turnCount}</td>
273
+ <td className="px-3 py-2 text-right">{c.totalRecall}</td>
274
+ <td className="px-3 py-2 text-right">{c.epochCount}</td>
275
+ <td className="px-3 py-2 text-right">{c.avgCtxPercent.toFixed(0)}%</td>
276
+ <td className="px-3 py-2">{fmtTs(c.lastTurnAt)}</td>
277
+ <td className="px-3 py-2" />
278
+ </tr>
279
+ </>
280
+ );
281
+ }
282
+
283
+ function TurnDetail({
284
+ detail,
285
+ busy,
286
+ onFork,
287
+ onRewind,
288
+ }: {
289
+ detail: ConversationTurnsResponse;
290
+ busy: string | null;
291
+ onFork: (conversationId: string, turnIndex: number) => void;
292
+ onRewind: (conversationId: string, turnIndex: number) => void;
293
+ }): React.ReactElement {
294
+ return (
295
+ <table className="w-full border-collapse text-sm">
296
+ <thead>
297
+ <tr>
298
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">#</th>
299
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Role</th>
300
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Ctx</th>
301
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Band</th>
302
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Epoch</th>
303
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Recalled checkpoints (memory recall)</th>
304
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">HyDE</th>
305
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Ended</th>
306
+ <th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Actions</th>
307
+ </tr>
308
+ </thead>
309
+ <tbody>
310
+ {detail.turns.map((t) => (
311
+ <tr
312
+ key={`${t.conversationId}:${t.turnIndex}`}
313
+ className="border-b border-border/50 hover:bg-bg-elevated/40"
314
+ >
315
+ <td className="px-3 py-2 text-right">{t.turnIndex}</td>
316
+ <td className="px-3 py-2">{t.role}</td>
317
+ <td className="px-3 py-2">
318
+ {t.ctxTokens ?? "—"}
319
+ {t.ctxPercent != null && (
320
+ <span className="text-muted-foreground">
321
+ {" "}
322
+ · {t.ctxPercent.toFixed(0)}%
323
+ </span>
324
+ )}
325
+ </td>
326
+ <td className="px-3 py-2">
327
+ <Badge variant={bandVariant(t.pressureBand)}>
328
+ {t.pressureBand ?? "—"}
329
+ </Badge>
330
+ </td>
331
+ <td className="px-3 py-2">{t.epochId ? <code>{t.epochId.slice(0, 8)}</code> : "—"}</td>
332
+ <td className="px-3 py-2">
333
+ {t.recall.length === 0 ? (
334
+ <span className="text-muted-foreground">—</span>
335
+ ) : (
336
+ <ul className="space-y-1">
337
+ {t.recall.map((r) => (
338
+ <li key={r.checkpointId}>
339
+ <code>{r.checkpointId.slice(0, 12)}</code>{" "}
340
+ <span className="text-muted-foreground">
341
+ {sourceLabel(r.source)} · {r.score.toFixed(2)}
342
+ {r.raptorLevel != null ? ` · L${r.raptorLevel}` : ""}
343
+ </span>
344
+ </li>
345
+ ))}
346
+ </ul>
347
+ )}
348
+ </td>
349
+ <td className="px-3 py-2 align-top">
350
+ <HydeDetailPanel
351
+ conversationId={t.conversationId}
352
+ turnIndex={t.turnIndex}
353
+ />
354
+ </td>
355
+ <td className="px-3 py-2">{fmtTs(t.endedAt)}</td>
356
+ <td className="px-3 py-2">
357
+ <div className="flex gap-2">
358
+ <Button
359
+ type="button"
360
+ variant="outline"
361
+ size="sm"
362
+ disabled={busy !== null}
363
+ onClick={() => onFork(t.conversationId, t.turnIndex)}
364
+ >
365
+ fork
366
+ </Button>
367
+ <Button
368
+ type="button"
369
+ variant="outline"
370
+ size="sm"
371
+ disabled={busy !== null}
372
+ onClick={() => onRewind(t.conversationId, t.turnIndex)}
373
+ >
374
+ rewind
375
+ </Button>
376
+ </div>
377
+ </td>
378
+ </tr>
379
+ ))}
380
+ </tbody>
381
+ </table>
382
+ );
383
+ }
@@ -1,9 +1,22 @@
1
1
  /**
2
- * dashboard-client/src/tabs/SessionsTab.tsx — Sessions tab (S39).
2
+ * dashboard-client/src/tabs/SessionsTab.tsx — Sessions tab (S39, DASH-0b).
3
3
  *
4
- * Renders: window selector (5/15/30/60 min), sessions summary tiles,
5
- * the stacked-memory graph (SessionsMemoryChart), and the active sessions
6
- * table (ActiveSessionsTable).
4
+ * DASH-0b: the Sessions surface absorbs the existent TurnsTab as a drill-down.
5
+ * A `SessionsViews` toggle ("Active Sessions" / "Turn Memory") switches between
6
+ * the existent sessions summary/chart/table body and the turns view (rendered by
7
+ * `SessionsTab/TurnMemoryView.tsx` — the turns body moved VERBATIM from the
8
+ * standalone TurnsTab.tsx, which is left untouched this sprint for flag-off
9
+ * parity).
10
+ *
11
+ * Flag: `MEGACOMPACT_DASH_0B` default ON. The dashboard client is a browser
12
+ * bundle with NO `process` global, so the positive sprint flag cannot be read
13
+ * server-side-style (`process.env`) inside the client. Instead the flag is read
14
+ * from the server-authoritative `/api/rag-settings` state: the server resolves
15
+ * `process.env["MEGACOMPACT_DASH_0B"] ?? default` into the `SettingState.value`
16
+ * boolean. When the flag is OFF (settings confirmed value=false), this surface
17
+ * renders ONLY the existent sessions body — no toggle, no TurnMemoryView branch
18
+ * (byte-identical predecessor). The standalone TurnsTab.tsx top-level surface is
19
+ * untouched and remains reachable for flag-off parity.
7
20
  *
8
21
  * Data sources: fetchSessions() + fetchSessionTimeseries(minutes). Both poll
9
22
  * every 2s for near-real-time updates. useSSE() listens for session_sample
@@ -18,12 +31,19 @@ import type React from "react";
18
31
  import { useCallback, useEffect, useMemo, useState } from "react";
19
32
  import { useApi } from "../hooks/useApi";
20
33
  import { useSSE } from "../hooks/useSSE";
21
- import { fetchSessions, fetchSessionTimeseries } from "../api/client";
22
- import type { SseEvent, SseSessionSample, SessionsResponse, SessionTimeseriesResponse } from "@contracts";
34
+ import { fetchSessions, fetchSessionTimeseries, fetchSettings } from "../api/client";
35
+ import type {
36
+ SseEvent,
37
+ SseSessionSample,
38
+ SessionsResponse,
39
+ SessionTimeseriesResponse,
40
+ SettingsResponse,
41
+ } from "@contracts";
23
42
  import { SessionsMemoryChart } from "../components/SessionsMemoryChart";
24
43
  import { ActiveSessionsTable } from "../components/ActiveSessionsTable";
25
44
  import { Toggle } from "../components/ui/toggle";
26
45
  import { Card, CardContent } from "../components/ui/card";
46
+ import { TurnMemoryView } from "./SessionsTab/TurnMemoryView";
27
47
 
28
48
  /**
29
49
  * Shape of the {@link SseSessionSample} SSE event appended to events.log by
@@ -40,6 +60,21 @@ function isSessionSample(
40
60
  return e.type === "session_sample";
41
61
  }
42
62
 
63
+ const DASH_0B_KEY = "MEGACOMPACT_DASH_0B";
64
+
65
+ /** Resolve the DASH-0b consolidation flag from the server settings state.
66
+ * Absent/not-yet-loaded => false (flag-off posture), so flag-off users never
67
+ * see a toggle flash; the toggle appears only once settings confirm it is ON. */
68
+ function dash0bEnabled(settings: SettingsResponse | null): boolean {
69
+ if (!settings) return false;
70
+ for (const cat of settings.categories) {
71
+ for (const s of cat.settings) {
72
+ if (s.key === DASH_0B_KEY && s.type === "boolean") return s.value === true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+
43
78
  const WINDOWS: ReadonlyArray<{ value: number; label: string }> = [
44
79
  { value: 5, label: "5m" },
45
80
  { value: 15, label: "15m" },
@@ -98,8 +133,19 @@ function SummaryTiles({ sessions }: SummaryTilesProps): React.ReactElement {
98
133
  );
99
134
  }
100
135
 
136
+ type SessionsView = "active" | "turns";
137
+
101
138
  export default function SessionsTab(): React.ReactElement {
102
139
  const [minutes, setMinutes] = useState(30);
140
+ const [view, setView] = useState<SessionsView>("active");
141
+
142
+ const {
143
+ data: settingsData,
144
+ } = useApi<SettingsResponse>(
145
+ useCallback(() => fetchSettings(), []),
146
+ { pollInterval: 0, maxRetries: 0 },
147
+ );
148
+ const dash0bOn = dash0bEnabled(settingsData);
103
149
 
104
150
  const {
105
151
  data: sessionsData,
@@ -142,6 +188,10 @@ export default function SessionsTab(): React.ReactElement {
142
188
  refetchTimeseries();
143
189
  }, [lastSampleTs, refetchSessions, refetchTimeseries]);
144
190
 
191
+ if (dash0bOn && view === "turns") {
192
+ return <TurnMemoryView />;
193
+ }
194
+
145
195
  if (sessionsErr && !sessionsData) {
146
196
  return (
147
197
  <div className="tab-stub">
@@ -185,6 +235,27 @@ export default function SessionsTab(): React.ReactElement {
185
235
  </div>
186
236
  </div>
187
237
 
238
+ {dash0bOn && (
239
+ <div
240
+ className="flex items-center gap-2"
241
+ role="tablist"
242
+ aria-label="Sessions views"
243
+ >
244
+ <Toggle
245
+ pressed={view === "active"}
246
+ onClick={() => setView("active")}
247
+ >
248
+ Active Sessions
249
+ </Toggle>
250
+ <Toggle
251
+ pressed={view === "turns"}
252
+ onClick={() => setView("turns")}
253
+ >
254
+ Turn Memory
255
+ </Toggle>
256
+ </div>
257
+ )}
258
+
188
259
  {sessions.length === 0 ? (
189
260
  <div className="text-sm text-muted-foreground">No active sessions.</div>
190
261
  ) : (