pi-mega-compact 0.20.58 → 0.20.59

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 (77) hide show
  1. package/dist/config/vector-cortex-dash-0b.js +21 -0
  2. package/dist/config/vector-cortex.js +2 -0
  3. package/dist/config.js +1 -1
  4. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  5. package/dist/src/config/vector-cortex-dash-0b.js +21 -0
  6. package/dist/src/config/vector-cortex.js +2 -0
  7. package/dist/src/config.js +1 -1
  8. package/extensions/dashboard-client/dist/assets/{AreaChart-CtJ6dSMT.js → AreaChart-GTwQxOAm.js} +2 -2
  9. package/extensions/dashboard-client/dist/assets/{AreaChart-CtJ6dSMT.js.map → AreaChart-GTwQxOAm.js.map} +1 -1
  10. package/extensions/dashboard-client/dist/assets/{BarChart-DJ3j3xDz.js → BarChart-BFqUD5L4.js} +2 -2
  11. package/extensions/dashboard-client/dist/assets/{BarChart-DJ3j3xDz.js.map → BarChart-BFqUD5L4.js.map} +1 -1
  12. package/extensions/dashboard-client/dist/assets/{CacheTab-CXU1_9lR.js → CacheTab-BLM6_PQQ.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{CacheTab-CXU1_9lR.js.map → CacheTab-BLM6_PQQ.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{EventsTab-DF-Q3o7x.js → EventsTab-BYLh6cud.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{EventsTab-DF-Q3o7x.js.map → EventsTab-BYLh6cud.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{HealthTab-Dr1HGOkl.js → HealthTab-CkJXEbjn.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{HealthTab-Dr1HGOkl.js.map → HealthTab-CkJXEbjn.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/HydeDetailPanel-Dt9T7kc5.js +5 -0
  19. package/extensions/dashboard-client/dist/assets/HydeDetailPanel-Dt9T7kc5.js.map +1 -0
  20. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-zKhR18_s.js → MaintenanceTab-Lm3v0nJO.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-zKhR18_s.js.map → MaintenanceTab-Lm3v0nJO.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DekIzfn2.js → MemoryMapTab-C8AeBIQi.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DekIzfn2.js.map → MemoryMapTab-C8AeBIQi.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{MetricsTab-CEKajY38.js → MetricsTab-m410T4vk.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{MetricsTab-CEKajY38.js.map → MetricsTab-m410T4vk.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{OverviewTab-24sG8xiE.js → OverviewTab-BfDfMFXx.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{OverviewTab-24sG8xiE.js.map → OverviewTab-BfDfMFXx.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{ReposTab-70gkV8l9.js → ReposTab-CsFxxqJ6.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{ReposTab-70gkV8l9.js.map → ReposTab-CsFxxqJ6.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/SessionsTab-DstDOzwu.js +2 -0
  31. package/extensions/dashboard-client/dist/assets/SessionsTab-DstDOzwu.js.map +1 -0
  32. package/extensions/dashboard-client/dist/assets/{SetupTab-tbPrrfYx.js → SetupTab-DVT4PrH0.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{SetupTab-tbPrrfYx.js.map → SetupTab-DVT4PrH0.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B6mdtCWE.js → TimeSavedCard-BkRzjyRm.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B6mdtCWE.js.map → TimeSavedCard-BkRzjyRm.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/TurnsTab-D5QTnv52.js +2 -0
  37. package/extensions/dashboard-client/dist/assets/TurnsTab-D5QTnv52.js.map +1 -0
  38. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-SDThp0R0.js → VcStatusBadge-B8B8RStV.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-SDThp0R0.js.map → VcStatusBadge-B8B8RStV.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/VectorCortexTab-CrGANNd_.js +2 -0
  41. package/extensions/dashboard-client/dist/assets/VectorCortexTab-CrGANNd_.js.map +1 -0
  42. package/extensions/dashboard-client/dist/assets/{WikiTab-Cki48_8N.js → WikiTab-DyD0yGzI.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{WikiTab-Cki48_8N.js.map → WikiTab-DyD0yGzI.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{button-CojFYd7q.js → button-Bynl3ONj.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{button-CojFYd7q.js.map → button-Bynl3ONj.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{card-Cxeeje9o.js → card-Daf3pQIu.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{card-Cxeeje9o.js.map → card-Daf3pQIu.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{client-extra-DLxyGxW8.js → client-extra-DqAYZBW1.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{client-extra-DLxyGxW8.js.map → client-extra-DqAYZBW1.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRlqBks2.js → generateCategoricalChart-B-fe5jjr.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRlqBks2.js.map → generateCategoricalChart-B-fe5jjr.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{index-CkbdlPBm.js → index-Bjvv23bp.js} +3 -3
  53. package/extensions/dashboard-client/dist/assets/{index-CkbdlPBm.js.map → index-Bjvv23bp.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{switch-Cx_nl-Ba.js → switch-CivyERPS.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{switch-Cx_nl-Ba.js.map → switch-CivyERPS.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{toggle-CfIFzp6P.js → toggle-qmdkrAgF.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{toggle-CfIFzp6P.js.map → toggle-qmdkrAgF.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{useSSE-B9_N7ccG.js → useSSE-uNuduyGe.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{useSSE-B9_N7ccG.js.map → useSSE-uNuduyGe.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/index.html +1 -1
  61. package/extensions/dashboard-client/src/tabs/SessionsTab/TurnMemoryView.tsx +383 -0
  62. package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +77 -6
  63. package/extensions/dashboard-client/src/tabs/VectorCortexTab/sections.tsx +201 -0
  64. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +168 -122
  65. package/extensions/dashboard-client/src/tabs/index.ts +30 -0
  66. package/extensions/dashboard-client/src/tabs/registry.ts +17 -0
  67. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  68. package/package.json +1 -1
  69. package/src/config/vector-cortex-dash-0b.ts +23 -0
  70. package/src/config/vector-cortex.ts +2 -0
  71. package/src/config.ts +1 -0
  72. package/extensions/dashboard-client/dist/assets/SessionsTab-B3quvYPZ.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/SessionsTab-B3quvYPZ.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/TurnsTab-CmhRJJqr.js +0 -5
  75. package/extensions/dashboard-client/dist/assets/TurnsTab-CmhRJJqr.js.map +0 -1
  76. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DDjCkTjk.js +0 -2
  77. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DDjCkTjk.js.map +0 -1
@@ -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
  ) : (