pi-mega-compact 0.16.0 → 0.16.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 (80) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/model-thresholds.js +1 -0
  3. package/dist/extensions/dashboard-server/routes-model-thresholds.js +167 -0
  4. package/dist/extensions/dashboard-server/routes.js +3 -2
  5. package/dist/extensions/dashboard-server/server.js +6 -2
  6. package/dist/extensions/mega-events/context-handler.js +37 -11
  7. package/dist/src/store/sqlite/model-snapshots.js +21 -0
  8. package/dist/src/store/sqlite/model-thresholds.js +93 -0
  9. package/dist/src/store/sqlite/schema/core.js +10 -0
  10. package/dist/src/store/sqlite.js +1 -0
  11. package/dist/src/vector-search.js +1 -1
  12. package/extensions/dashboard-client/dist/assets/{AreaChart-CrmTcE7w.js → AreaChart-Y_X0Bbjd.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{AreaChart-CrmTcE7w.js.map → AreaChart-Y_X0Bbjd.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{BarChart-Cf8DTaXS.js → BarChart-B9XDb-Ob.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{BarChart-Cf8DTaXS.js.map → BarChart-B9XDb-Ob.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{CacheTab-Ce0MES6p.js → CacheTab-BUGVb1G1.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{CacheTab-Ce0MES6p.js.map → CacheTab-BUGVb1G1.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{EventsTab-DbaUuA58.js → EventsTab-dOqLYTsy.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{EventsTab-DbaUuA58.js.map → EventsTab-dOqLYTsy.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{HealthTab-BmEXkcfL.js → HealthTab-c2vezKg1.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{HealthTab-BmEXkcfL.js.map → HealthTab-c2vezKg1.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-n2JQrMy7.js → MaintenanceTab-bQYHIcdx.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-n2JQrMy7.js.map → MaintenanceTab-bQYHIcdx.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/MemoryMapTab-2L8pH6y2.js +2 -0
  25. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dvu7vkOm.js.map → MemoryMapTab-2L8pH6y2.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MetricsTab-Q0g32AmP.js → MetricsTab-CzUx1jpo.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MetricsTab-Q0g32AmP.js.map → MetricsTab-CzUx1jpo.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{OverviewTab-iKiEcOLL.js → OverviewTab-eCNoaclv.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{OverviewTab-iKiEcOLL.js.map → OverviewTab-eCNoaclv.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{ReposTab-DWpnv77y.js → ReposTab-tb0IW3NF.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{ReposTab-DWpnv77y.js.map → ReposTab-tb0IW3NF.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{SessionsTab-G0YxR1vG.js → SessionsTab-C-IoLw5h.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{SessionsTab-G0YxR1vG.js.map → SessionsTab-C-IoLw5h.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/SetupTab-BvmFi8gf.js +2 -0
  35. package/extensions/dashboard-client/dist/assets/SetupTab-BvmFi8gf.js.map +1 -0
  36. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-1_LCFiJp.js → TimeSavedCard-DBH-cOlA.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-1_LCFiJp.js.map → TimeSavedCard-DBH-cOlA.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{TurnsTab-pqOkjE4L.js → TurnsTab-DBMgmvnX.js} +3 -3
  39. package/extensions/dashboard-client/dist/assets/{TurnsTab-pqOkjE4L.js.map → TurnsTab-DBMgmvnX.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/WikiTab-1LcQrXk3.js +2 -0
  41. package/extensions/dashboard-client/dist/assets/{WikiTab-99sKE4nB.js.map → WikiTab-1LcQrXk3.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/button-DtEsZTUE.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/{button-YcV2VQEP.js.map → button-DtEsZTUE.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{card-Bwxzo50p.js → card-DcfTmw-4.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{card-Bwxzo50p.js.map → card-DcfTmw-4.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-C6hlfMYG.js → generateCategoricalChart-BsIxUDyw.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-C6hlfMYG.js.map → generateCategoricalChart-BsIxUDyw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{index-DRa2d4TF.js → index-B93cewiX.js} +14 -14
  49. package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +1 -0
  51. package/extensions/dashboard-client/dist/assets/{switch-rU-lor0v.js → switch-Bowd7jCD.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{switch-rU-lor0v.js.map → switch-Bowd7jCD.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{toggle-h-9B4w9X.js → toggle-Bo7kJZ4u.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{toggle-h-9B4w9X.js.map → toggle-Bo7kJZ4u.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{useSSE-BxJORWx8.js → useSSE-zNhb0c3u.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{useSSE-BxJORWx8.js.map → useSSE-zNhb0c3u.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/index.html +2 -2
  58. package/extensions/dashboard-client/src/api/client.ts +38 -13
  59. package/extensions/dashboard-client/src/tabs/SetupTab/ThresholdsPanel.tsx +334 -0
  60. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +9 -1
  61. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +50 -17
  62. package/extensions/dashboard-server/api-contracts/index.ts +28 -3
  63. package/extensions/dashboard-server/api-contracts/model-thresholds.ts +76 -0
  64. package/extensions/dashboard-server/routes-model-thresholds.ts +222 -0
  65. package/extensions/dashboard-server/routes.ts +11 -2
  66. package/extensions/dashboard-server/server.ts +5 -1
  67. package/extensions/mega-events/context-handler.ts +106 -69
  68. package/package.json +1 -1
  69. package/src/store/sqlite/model-snapshots.ts +99 -62
  70. package/src/store/sqlite/model-thresholds.ts +168 -0
  71. package/src/store/sqlite/schema/core.ts +10 -0
  72. package/src/store/sqlite.ts +1 -0
  73. package/src/vector-search.ts +1 -1
  74. package/extensions/dashboard-client/dist/assets/MemoryMapTab-Dvu7vkOm.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/SetupTab-C-y6H-F-.js +0 -2
  76. package/extensions/dashboard-client/dist/assets/SetupTab-C-y6H-F-.js.map +0 -1
  77. package/extensions/dashboard-client/dist/assets/WikiTab-99sKE4nB.js +0 -2
  78. package/extensions/dashboard-client/dist/assets/button-YcV2VQEP.js +0 -2
  79. package/extensions/dashboard-client/dist/assets/index-DRa2d4TF.js.map +0 -1
  80. package/extensions/dashboard-client/dist/assets/index-za_gmEmQ.css +0 -1
@@ -54,6 +54,9 @@ import type {
54
54
  SettingsUpdateRequest,
55
55
  SettingsResponsePost,
56
56
  RagMetricsResponse,
57
+ ModelThresholdsResponse,
58
+ ModelThresholdPutRequest,
59
+ ModelThresholdPutResponse,
57
60
  WikiIndexResponse,
58
61
  WikiPageResponse,
59
62
  CurationResult,
@@ -337,7 +340,9 @@ export function configureEmbedder(
337
340
  // ─── RAPTOR tree (Part B) ─────────────────────────────────────────────
338
341
 
339
342
  /** GET /api/raptor-tree — tree for a session (defaults to latest with nodes). */
340
- export function fetchRaptorTree(sessionId?: string): Promise<RaptorTreeResponse> {
343
+ export function fetchRaptorTree(
344
+ sessionId?: string,
345
+ ): Promise<RaptorTreeResponse> {
341
346
  return getJson<RaptorTreeResponse>(
342
347
  `${ENDPOINTS.raptorTree.path}${query({ sessionId })}`,
343
348
  );
@@ -398,10 +403,7 @@ export function fetchWikiIndex(): Promise<WikiIndexResponse> {
398
403
  /** GET /api/wiki/topic/:topicId — single topic page + provenance. */
399
404
  export function fetchWikiTopic(topicId: string): Promise<WikiPageResponse> {
400
405
  return getJson<WikiPageResponse>(
401
- ENDPOINTS.wikiTopic.path.replace(
402
- ":topicId",
403
- encodeURIComponent(topicId),
404
- ),
406
+ ENDPOINTS.wikiTopic.path.replace(":topicId", encodeURIComponent(topicId)),
405
407
  );
406
408
  }
407
409
 
@@ -411,10 +413,7 @@ export function renameTopic(
411
413
  label: string,
412
414
  ): Promise<CurationResult> {
413
415
  return putJson<CurationResult>(
414
- ENDPOINTS.renameTopic.path.replace(
415
- ":topicId",
416
- encodeURIComponent(topicId),
417
- ),
416
+ ENDPOINTS.renameTopic.path.replace(":topicId", encodeURIComponent(topicId)),
418
417
  { label } satisfies RenameTopicRequest,
419
418
  );
420
419
  }
@@ -436,10 +435,7 @@ export function splitTopic(
436
435
  memoryIds: string[],
437
436
  ): Promise<CurationResult> {
438
437
  return postJson<CurationResult>(
439
- ENDPOINTS.splitTopic.path.replace(
440
- ":topicId",
441
- encodeURIComponent(topicId),
442
- ),
438
+ ENDPOINTS.splitTopic.path.replace(":topicId", encodeURIComponent(topicId)),
443
439
  { memoryIds } satisfies SplitTopicRequest,
444
440
  );
445
441
  }
@@ -460,3 +456,32 @@ export function fetchTopicTimeline(
460
456
  export function fetchTopicEvolution(): Promise<TopicEvolutionResponse> {
461
457
  return getJson<TopicEvolutionResponse>(ENDPOINTS.topicEvolution.path);
462
458
  }
459
+
460
+ // ─── Per-model compaction thresholds (S52 / v0.16.1) ────────────────────
461
+
462
+ /** GET /api/model-thresholds — list known models with their thresholds. */
463
+ export function fetchModelThresholds(): Promise<ModelThresholdsResponse> {
464
+ return getJson<ModelThresholdsResponse>(ENDPOINTS.modelThresholds.path);
465
+ }
466
+
467
+ /** PUT /api/model-thresholds — upsert a per-model override. */
468
+ export function putModelThreshold(
469
+ body: ModelThresholdPutRequest,
470
+ ): Promise<ModelThresholdPutResponse> {
471
+ return putJson<ModelThresholdPutResponse>(
472
+ ENDPOINTS.modelThresholds.path,
473
+ body,
474
+ );
475
+ }
476
+
477
+ /** DELETE /api/model-thresholds/:modelId — delete an override (revert). */
478
+ export async function deleteModelThreshold(
479
+ modelId: string,
480
+ ): Promise<{ deleted: boolean }> {
481
+ const res = await fetch(
482
+ `${ENDPOINTS.modelThresholds.path}/${encodeURIComponent(modelId)}`,
483
+ { method: "DELETE" },
484
+ );
485
+ if (!res.ok) throw new Error(`deleteModelThreshold ${res.status}`);
486
+ return res.json() as Promise<{ deleted: boolean }>;
487
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * SetupTab/ThresholdsPanel.tsx — per-model compaction thresholds (S52 / v0.16.1).
3
+ *
4
+ * Lists every known model (from model_snapshots) with two sliders each:
5
+ * - Safety Margin (0-20%): reserved after summary + maxOutput. Lower =
6
+ * more usable tail but risks the "too long even after compaction" error.
7
+ * - Fire Point (10-90%): % of window where compaction fires. Lower = compact
8
+ * earlier (more headroom); higher = more context before compaction.
9
+ *
10
+ * Warnings fire on <5% safety (overflow risk) and >85% fire point (aggressive
11
+ * — may hit the cap before compaction triggers). Changes save immediately via
12
+ * PUT /api/model-thresholds; a "Reset to default" button DELETEs the override.
13
+ *
14
+ * PREVENT-PI-004: all data comes from localhost dashboard API endpoints.
15
+ */
16
+ import type React from "react";
17
+ import { useState, useEffect, useCallback } from "react";
18
+ import {
19
+ fetchModelThresholds,
20
+ putModelThreshold,
21
+ deleteModelThreshold,
22
+ } from "../../api/client";
23
+ import type { ModelThresholdsResponse } from "@contracts";
24
+
25
+ interface ModelRow {
26
+ readonly modelId: string;
27
+ readonly provider: string;
28
+ readonly modelName: string | null;
29
+ readonly contextWindow: number;
30
+ readonly maxTokens: number;
31
+ readonly hasOverride: boolean;
32
+ readonly threshold: {
33
+ readonly safetyMarginPct: number;
34
+ readonly firePointPct: number;
35
+ readonly isOverride: boolean;
36
+ };
37
+ }
38
+
39
+ function fmtWindow(n: number): string {
40
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
41
+ if (n >= 1000) return `${Math.round(n / 1000)}K`;
42
+ return String(n);
43
+ }
44
+
45
+ /** Live preview of the usable tail budget for a model + slider values. */
46
+ function usableTail(
47
+ ctxWindow: number,
48
+ maxTokens: number,
49
+ safetyPct: number,
50
+ ): string {
51
+ if (ctxWindow <= 0) return "—";
52
+ const maxOut = maxTokens > 0 ? maxTokens : Math.ceil(ctxWindow * 0.1);
53
+ const reserve = maxOut + Math.ceil(ctxWindow * (safetyPct / 100));
54
+ const usable = ctxWindow - reserve;
55
+ if (usable <= 0) return "0 (at risk)";
56
+ return `${fmtWindow(usable)} tokens`;
57
+ }
58
+
59
+ function ThresholdSlider({
60
+ label,
61
+ value,
62
+ min,
63
+ max,
64
+ step = 1,
65
+ onChange,
66
+ warning,
67
+ }: {
68
+ readonly label: string;
69
+ readonly value: number;
70
+ readonly min: number;
71
+ readonly max: number;
72
+ readonly step?: number;
73
+ readonly onChange: (v: number) => void;
74
+ readonly warning?: string;
75
+ }): React.ReactElement {
76
+ return (
77
+ <div className="flex flex-col gap-1">
78
+ <div className="flex items-center justify-between text-xs">
79
+ <span className="font-medium text-fg-muted">{label}</span>
80
+ <span
81
+ className={`font-mono ${warning ? "text-warning" : "text-fg"}`}
82
+ title={warning}
83
+ >
84
+ {value}%
85
+ </span>
86
+ </div>
87
+ <input
88
+ type="range"
89
+ min={min}
90
+ max={max}
91
+ step={step}
92
+ value={value}
93
+ onChange={(e) => onChange(Number(e.target.value))}
94
+ className="threshold-slider w-full"
95
+ />
96
+ {warning && <p className="text-[10px] text-warning">⚠ {warning}</p>}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ function ModelThresholdRow({
102
+ model,
103
+ defaults,
104
+ safetyRange,
105
+ fireRange,
106
+ onSave,
107
+ onReset,
108
+ saving,
109
+ }: {
110
+ readonly model: ModelRow;
111
+ readonly defaults: { safetyMarginPct: number; firePointPct: number };
112
+ readonly safetyRange: readonly [number, number];
113
+ readonly fireRange: readonly [number, number];
114
+ readonly onSave: (
115
+ modelId: string,
116
+ safetyMarginPct: number,
117
+ firePointPct: number,
118
+ ) => Promise<void>;
119
+ readonly onReset: (modelId: string) => Promise<void>;
120
+ readonly saving: boolean;
121
+ }): React.ReactElement {
122
+ const [safety, setSafety] = useState(model.threshold.safetyMarginPct);
123
+ const [fire, setFire] = useState(model.threshold.firePointPct);
124
+ const [dirty, setDirty] = useState(false);
125
+ const [err, setErr] = useState<string | null>(null);
126
+
127
+ // Re-sync when the server data changes (e.g. after reset).
128
+ useEffect(() => {
129
+ setSafety(model.threshold.safetyMarginPct);
130
+ setFire(model.threshold.firePointPct);
131
+ setDirty(false);
132
+ }, [model.threshold.safetyMarginPct, model.threshold.firePointPct]);
133
+
134
+ const safetyWarning =
135
+ safety < 5
136
+ ? `Low safety margin — risks "conversation too long" errors when a single turn injects a large tool output between gate fires.`
137
+ : undefined;
138
+ const fireWarning =
139
+ fire > 85
140
+ ? `High fire point — compaction may trigger close to the window cap; a single large turn can overshoot before the gate fires.`
141
+ : undefined;
142
+
143
+ const save = useCallback(async () => {
144
+ setErr(null);
145
+ try {
146
+ await onSave(model.modelId, safety, fire);
147
+ setDirty(false);
148
+ } catch (e) {
149
+ setErr(e instanceof Error ? e.message : String(e));
150
+ }
151
+ }, [model.modelId, safety, fire, onSave]);
152
+
153
+ return (
154
+ <div className="rounded-md border border-border bg-bg-elevated/30 p-3">
155
+ <div className="mb-2 flex items-start justify-between">
156
+ <div>
157
+ <div className="flex items-center gap-2">
158
+ <span className="text-sm font-semibold text-fg">
159
+ {model.modelName ?? model.modelId}
160
+ </span>
161
+ {model.threshold.isOverride && (
162
+ <span
163
+ className="rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-primary"
164
+ title="This model has a custom override (not the default)"
165
+ >
166
+ Override
167
+ </span>
168
+ )}
169
+ </div>
170
+ <div className="text-[11px] text-fg-muted">
171
+ {model.provider} · {fmtWindow(model.contextWindow)} window ·{" "}
172
+ {fmtWindow(model.maxTokens)} output
173
+ </div>
174
+ </div>
175
+ <div className="text-right text-[11px] text-fg-muted">
176
+ <div>
177
+ Usable tail:{" "}
178
+ <span className="font-mono text-fg">
179
+ {usableTail(model.contextWindow, model.maxTokens, safety)}
180
+ </span>
181
+ </div>
182
+ <div>
183
+ Compaction fires at:{" "}
184
+ <span className="font-mono text-fg">
185
+ {fmtWindow(Math.ceil(model.contextWindow * (fire / 100)))}
186
+ </span>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
191
+ <ThresholdSlider
192
+ label="Safety margin"
193
+ value={safety}
194
+ min={safetyRange[0]}
195
+ max={safetyRange[1]}
196
+ onChange={(v) => {
197
+ setSafety(v);
198
+ setDirty(true);
199
+ }}
200
+ warning={safetyWarning}
201
+ />
202
+ <ThresholdSlider
203
+ label="Fire point"
204
+ value={fire}
205
+ min={fireRange[0]}
206
+ max={fireRange[1]}
207
+ onChange={(v) => {
208
+ setFire(v);
209
+ setDirty(true);
210
+ }}
211
+ warning={fireWarning}
212
+ />
213
+ </div>
214
+ <div className="mt-2 flex items-center gap-2">
215
+ <button
216
+ type="button"
217
+ onClick={save}
218
+ disabled={!dirty || saving}
219
+ className="rounded-md bg-primary px-2.5 py-1 text-xs font-semibold text-primary-fg disabled:cursor-not-allowed disabled:opacity-50"
220
+ >
221
+ {dirty ? "Save override" : "Saved"}
222
+ </button>
223
+ {model.threshold.isOverride && (
224
+ <button
225
+ type="button"
226
+ onClick={() => onReset(model.modelId)}
227
+ disabled={saving}
228
+ className="rounded-md border border-border px-2.5 py-1 text-xs text-fg-muted disabled:cursor-not-allowed disabled:opacity-50"
229
+ >
230
+ Reset to default ({defaults.safetyMarginPct}% /{" "}
231
+ {defaults.firePointPct}%)
232
+ </button>
233
+ )}
234
+ {err && <span className="text-[11px] text-error">{err}</span>}
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ export default function ThresholdsPanel(): React.ReactElement {
241
+ const [data, setData] = useState<ModelThresholdsResponse | null>(null);
242
+ const [loading, setLoading] = useState(true);
243
+ const [error, setError] = useState<string | null>(null);
244
+ const [saving, setSaving] = useState(false);
245
+
246
+ const load = useCallback(async () => {
247
+ try {
248
+ setError(null);
249
+ setData(await fetchModelThresholds());
250
+ } catch (e) {
251
+ setError(e instanceof Error ? e.message : String(e));
252
+ } finally {
253
+ setLoading(false);
254
+ }
255
+ }, []);
256
+
257
+ useEffect(() => {
258
+ load();
259
+ }, [load]);
260
+
261
+ const onSave = useCallback(
262
+ async (modelId: string, safetyMarginPct: number, firePointPct: number) => {
263
+ setSaving(true);
264
+ try {
265
+ await putModelThreshold({ modelId, safetyMarginPct, firePointPct });
266
+ await load();
267
+ } finally {
268
+ setSaving(false);
269
+ }
270
+ },
271
+ [load],
272
+ );
273
+
274
+ const onReset = useCallback(
275
+ async (modelId: string) => {
276
+ setSaving(true);
277
+ try {
278
+ await deleteModelThreshold(modelId);
279
+ await load();
280
+ } finally {
281
+ setSaving(false);
282
+ }
283
+ },
284
+ [load],
285
+ );
286
+
287
+ if (loading)
288
+ return <div className="p-4 text-sm text-fg-muted">Loading thresholds…</div>;
289
+ if (error)
290
+ return <div className="p-4 text-sm text-error">Error: {error}</div>;
291
+ if (!data) return <div className="p-4 text-sm text-fg-muted">No data.</div>;
292
+
293
+ return (
294
+ <div className="flex flex-col gap-4">
295
+ <div className="rounded-md border border-border bg-bg-elevated/20 p-3">
296
+ <h3 className="text-sm font-semibold">
297
+ Per-Model Compaction Thresholds
298
+ </h3>
299
+ <p className="mt-1 text-[11px] text-fg-muted">
300
+ Tune when compaction fires + how much safety margin to reserve,
301
+ <strong> per model</strong>. Different providers' models range from 8K
302
+ to 1M+ context — one global % is wrong. Defaults:{" "}
303
+ <span className="font-mono">
304
+ {data.defaults.safetyMarginPct}% safety
305
+ </span>{" "}
306
+ /{" "}
307
+ <span className="font-mono">{data.defaults.firePointPct}% fire</span>.
308
+ Values outside the safe band show warnings.
309
+ </p>
310
+ </div>
311
+ {data.models.length === 0 ? (
312
+ <div className="rounded-md border border-border p-4 text-sm text-fg-muted">
313
+ No models captured yet. Start a pi session in any repo to populate
314
+ model snapshots, then return here to tune per-model thresholds.
315
+ </div>
316
+ ) : (
317
+ <div className="flex flex-col gap-3">
318
+ {data.models.map((m) => (
319
+ <ModelThresholdRow
320
+ key={m.modelId}
321
+ model={m}
322
+ defaults={data.defaults}
323
+ safetyRange={data.defaults.safetyMarginRange}
324
+ fireRange={data.defaults.firePointRange}
325
+ onSave={onSave}
326
+ onReset={onReset}
327
+ saving={saving}
328
+ />
329
+ ))}
330
+ </div>
331
+ )}
332
+ </div>
333
+ );
334
+ }
@@ -13,12 +13,19 @@ import GameTab from "./GameTab";
13
13
  import AchievementsTab from "./AchievementsTab";
14
14
  import ConfigTab from "./ConfigTab";
15
15
  import EmbedderSetup from "./SetupTab/EmbedderSetup";
16
+ import ThresholdsPanel from "./SetupTab/ThresholdsPanel";
16
17
  import { Toggle } from "../components/ui/toggle";
17
18
 
18
- type SetupSubTab = "embedder" | "config" | "game" | "achievements";
19
+ type SetupSubTab =
20
+ | "embedder"
21
+ | "thresholds"
22
+ | "config"
23
+ | "game"
24
+ | "achievements";
19
25
 
20
26
  const SUB_TABS: ReadonlyArray<{ id: SetupSubTab; label: string }> = [
21
27
  { id: "embedder", label: "Embedder" },
28
+ { id: "thresholds", label: "Thresholds" },
22
29
  { id: "config", label: "Config" },
23
30
  { id: "game", label: "Game Mode" },
24
31
  { id: "achievements", label: "Achievements" },
@@ -45,6 +52,7 @@ export default function SetupTab(): React.ReactElement {
45
52
  </nav>
46
53
 
47
54
  {subTab === "embedder" && <EmbedderSetup />}
55
+ {subTab === "thresholds" && <ThresholdsPanel />}
48
56
  {subTab === "config" && <ConfigTab />}
49
57
  {subTab === "game" && <GameTab />}
50
58
  {subTab === "achievements" && <AchievementsTab />}
@@ -15,9 +15,17 @@ import type { IndexesSummaryResponse } from "../multi-repo.js";
15
15
  import type { GameStateResponse } from "../game.js";
16
16
  import type { SseEvent } from "../index.js";
17
17
  import type { ProviderCacheResponse } from "../provider-cache.js";
18
- import type { RaptorTreeResponse, RaptorBuildHistoryResponse } from "../raptor.js";
18
+ import type {
19
+ RaptorTreeResponse,
20
+ RaptorBuildHistoryResponse,
21
+ } from "../raptor.js";
19
22
  import type { MemoryStatusResponse } from "../memory.js";
20
- import type { SetupStatusResponse, SetupDetectResponse, SetupConfigureRequest, SetupConfigureResponse } from "../setup.js";
23
+ import type {
24
+ SetupStatusResponse,
25
+ SetupDetectResponse,
26
+ SetupConfigureRequest,
27
+ SetupConfigureResponse,
28
+ } from "../setup.js";
21
29
  import type { EmbedderHealthResponse } from "../embedder-health.js";
22
30
  import type {
23
31
  RagSettingsResponse,
@@ -25,6 +33,7 @@ import type {
25
33
  RagSettingsResponsePost,
26
34
  } from "../rag-settings.js";
27
35
  import type { RagMetricsResponse } from "../rag-metrics.js";
36
+ import type { ModelThresholdsResponse } from "../model-thresholds.js";
28
37
  import type {
29
38
  TurnsResponse,
30
39
  ConversationTurnsResponse,
@@ -192,7 +201,11 @@ export const ENDPOINTS = {
192
201
  path: "/api/perf/samples",
193
202
  description:
194
203
  "Raw perf_samples for one kind (time-series drill-down across any perf metric).",
195
- } as const satisfies EndpointDef<"GET", PerfSamplesQuery, PerfSamplesResponse>,
204
+ } as const satisfies EndpointDef<
205
+ "GET",
206
+ PerfSamplesQuery,
207
+ PerfSamplesResponse
208
+ >,
196
209
 
197
210
  /** GET /api/achievements — Achievement tiles with unlock state. */
198
211
  achievements: {
@@ -329,8 +342,7 @@ export const ENDPOINTS = {
329
342
  topicTimeline: {
330
343
  method: "GET",
331
344
  path: "/api/wiki/topic/:topicId/timeline",
332
- description:
333
- "Per-topic memory-assignment timeline buckets for charts.",
345
+ description: "Per-topic memory-assignment timeline buckets for charts.",
334
346
  } as const satisfies EndpointDef<"GET", undefined, TopicTimelineResponse>,
335
347
 
336
348
  /** GET /api/wiki/evolution — node/edge D3 evolution graph. */
@@ -380,16 +392,15 @@ export const ENDPOINTS = {
380
392
  "Lifetime provider prompt cache hit-rate aggregates + dollar savings estimate.",
381
393
  } as const satisfies EndpointDef<"GET", undefined, ProviderCacheResponse>,
382
394
 
395
+ // ─── A3 Cache Stripes (PLAN_V2 Phase 4) ───────────────────────────
383
396
 
384
- // ─── A3 Cache Stripes (PLAN_V2 Phase 4) ───────────────────────────
385
-
386
- /** GET /api/cache-stripes — Per-stripe distribution + health score. */
387
- cacheStripes: {
388
- method: "GET",
389
- path: "/api/cache-stripes",
390
- description:
391
- "Per-stripe distribution counts, average stability scores, and composite cache health.",
392
- } as const satisfies EndpointDef<"GET", undefined, CacheStripesResponse>,
397
+ /** GET /api/cache-stripes Per-stripe distribution + health score. */
398
+ cacheStripes: {
399
+ method: "GET",
400
+ path: "/api/cache-stripes",
401
+ description:
402
+ "Per-stripe distribution counts, average stability scores, and composite cache health.",
403
+ } as const satisfies EndpointDef<"GET", undefined, CacheStripesResponse>,
393
404
  // ─── Memory Effectiveness (S53B) ───────────────────────────────────────
394
405
 
395
406
  /** GET /api/memory-status — Memory store aggregate statistics. */
@@ -422,7 +433,11 @@ export const ENDPOINTS = {
422
433
  path: "/api/setup-configure",
423
434
  description:
424
435
  "Write the chosen embedder configuration to .mega-compact.env (loaded at next startup). Returns whether a restart is required.",
425
- } as const satisfies EndpointDef<"POST", SetupConfigureRequest, SetupConfigureResponse>,
436
+ } as const satisfies EndpointDef<
437
+ "POST",
438
+ SetupConfigureRequest,
439
+ SetupConfigureResponse
440
+ >,
426
441
 
427
442
  // ─── RAPTOR tree (Part B) ───────────────────────────────────────
428
443
 
@@ -440,7 +455,11 @@ export const ENDPOINTS = {
440
455
  path: "/api/raptor-build-history",
441
456
  description:
442
457
  "RAPTOR build history rows (node/leaf count, depth, coherence score, timed_out) for a session; defaults to the latest session with builds.",
443
- } as const satisfies EndpointDef<"GET", undefined, RaptorBuildHistoryResponse>,
458
+ } as const satisfies EndpointDef<
459
+ "GET",
460
+ undefined,
461
+ RaptorBuildHistoryResponse
462
+ >,
444
463
 
445
464
  /** GET /api/embedder-health — Round-trip a test embed through the active embedder. */
446
465
  embedderHealth: {
@@ -466,7 +485,11 @@ export const ENDPOINTS = {
466
485
  path: "/api/rag-settings",
467
486
  description:
468
487
  "Update RAG feature flag states by writing _DISABLED env vars to .mega-compact.env.",
469
- } as const satisfies EndpointDef<"POST", RagSettingsRequest, RagSettingsResponsePost>,
488
+ } as const satisfies EndpointDef<
489
+ "POST",
490
+ RagSettingsRequest,
491
+ RagSettingsResponsePost
492
+ >,
470
493
 
471
494
  // ─── RAG Metrics (Sprint H2) ──────────────────────────────────────
472
495
 
@@ -477,4 +500,14 @@ export const ENDPOINTS = {
477
500
  description:
478
501
  "HyDE invocation + recall-quality telemetry: flags, totals, recent turns, and daily series.",
479
502
  } as const satisfies EndpointDef<"GET", undefined, RagMetricsResponse>,
503
+
504
+ // ─── Per-model compaction thresholds (S52 / v0.16.1) ───────────────
505
+
506
+ /** GET /api/model-thresholds — List known models + their thresholds. */
507
+ modelThresholds: {
508
+ method: "GET",
509
+ path: "/api/model-thresholds",
510
+ description:
511
+ "Every known model (from model_snapshots) with its per-model threshold override (or defaults).",
512
+ } as const satisfies EndpointDef<"GET", undefined, ModelThresholdsResponse>,
480
513
  } as const;
@@ -230,7 +230,14 @@ export type {
230
230
  export type { MemoryStatusResponse, MemoryStatsTopMemory } from "./memory.js";
231
231
 
232
232
  // Setup wizard — embedder configuration + backend detection
233
- export type { SetupStatusResponse, SetupDetectResponse, SetupConfigureRequest, SetupConfigureResponse, DetectResult, OllamaDetectResult } from "./setup.js";
233
+ export type {
234
+ SetupStatusResponse,
235
+ SetupDetectResponse,
236
+ SetupConfigureRequest,
237
+ SetupConfigureResponse,
238
+ DetectResult,
239
+ OllamaDetectResult,
240
+ } from "./setup.js";
234
241
 
235
242
  // Embedder health probe
236
243
  export type { EmbedderHealthResponse } from "./embedder-health.js";
@@ -245,7 +252,12 @@ export type {
245
252
  } from "./memory-map.js";
246
253
 
247
254
  // Part B — RAPTOR tree + build history
248
- export type { RaptorNodeDTO, RaptorTreeResponse, RaptorBuildHistoryDTO, RaptorBuildHistoryResponse } from "./raptor.js";
255
+ export type {
256
+ RaptorNodeDTO,
257
+ RaptorTreeResponse,
258
+ RaptorBuildHistoryDTO,
259
+ RaptorBuildHistoryResponse,
260
+ } from "./raptor.js";
249
261
 
250
262
  // A3 — cache stripe distribution + health
251
263
  export type {
@@ -288,4 +300,17 @@ export type {
288
300
  SseWikiTopicSplit,
289
301
  WikiSseEvent,
290
302
  } from "./wiki.js";
291
- export type { CurationResult, OverrideKind, WikiCurationStore } from "../../../src/wiki/curation.js";
303
+ export type {
304
+ CurationResult,
305
+ OverrideKind,
306
+ WikiCurationStore,
307
+ } from "../../../src/wiki/curation.js";
308
+ export type {
309
+ ModelThresholdDTO,
310
+ KnownModel,
311
+ ModelThresholdsResponse,
312
+ ModelThresholdPutRequest,
313
+ ModelThresholdPutResponse,
314
+ ModelThresholdDeleteResponse,
315
+ ModelThresholdsError,
316
+ } from "./model-thresholds.js";