@danypops/pi-jittor 0.2.0 → 0.3.0

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 (25) hide show
  1. package/README.md +19 -3
  2. package/docs/USAGE_PRIOR_ART.md +1 -1
  3. package/extension/src/index.ts +379 -118
  4. package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
  5. package/extension/src/observability/context-growth.ts +26 -0
  6. package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
  7. package/extension/src/observability/context-report.ts +92 -0
  8. package/extension/src/observability/context-view.ts +264 -0
  9. package/extension/src/{footer.ts → observability/footer.ts} +63 -26
  10. package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
  11. package/extension/src/observability/provider-context-snapshot.ts +246 -0
  12. package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
  13. package/extension/src/{tui.ts → observability/status.ts} +202 -72
  14. package/extension/src/observability/usage.ts +314 -0
  15. package/extension/src/optimization/model-selection-panel.ts +160 -0
  16. package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
  17. package/extension/src/service-client.ts +49 -2
  18. package/extension/src/settings-tui.ts +73 -33
  19. package/extension/src/settings.ts +40 -29
  20. package/package.json +11 -5
  21. package/extension/src/benchmark-tui.ts +0 -113
  22. package/extension/src/context-report.ts +0 -49
  23. package/extension/src/context-view.ts +0 -108
  24. package/extension/src/usage.ts +0 -324
  25. /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
@@ -0,0 +1,264 @@
1
+ import type { ContextDelta, ContextSegment } from "@danypops/jittor";
2
+ import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
3
+ import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
4
+ import {
5
+ type ContextBarTheme,
6
+ type ContextRow,
7
+ type ContextRowsTheme,
8
+ type ContextSegment as MalevichContextSegment,
9
+ renderContextRowLines,
10
+ renderContextUsageBar,
11
+ } from "malevich-tui-components";
12
+ import type { ContextBreakdown } from "./context-breakdown.ts";
13
+ import { buildContextReport, buildContextRowsIterative } from "./context-report.ts";
14
+
15
+ const VISIBLE_ROWS = 22;
16
+ const MIN_TOKEN_FILTERS = [0, 100, 1_000, 10_000] as const;
17
+
18
+ export type ContextRowScope = "all" | "active" | "historical";
19
+
20
+ function rowTokens(row: ContextRow): number {
21
+ const match = row.isHeader ? row.text.match(/—\s+([\d,]+)\s+tok/) : row.text.match(/^\s*([\d,]+)\s+tok/);
22
+ return match ? Number(match[1]!.replaceAll(",", "")) : 0;
23
+ }
24
+
25
+ /** Filters the flattened pre-order tree while retaining every ancestor needed to understand a match. */
26
+ export function filterContextRows(rows: readonly ContextRow[], query: string, scope: ContextRowScope, minimumTokens: number): ContextRow[] {
27
+ const terms = query.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
28
+ const included = rows.map(() => false);
29
+ const parentByIndex: Array<number | null> = [];
30
+ const ancestors: number[] = [];
31
+ const historicalByDepth: boolean[] = [];
32
+ for (let index = 0; index < rows.length; index++) {
33
+ const row = rows[index]!;
34
+ while (ancestors.length > row.depth) ancestors.pop();
35
+ parentByIndex[index] = row.depth > 0 ? (ancestors[row.depth - 1] ?? null) : null;
36
+ const lower = row.text.toLocaleLowerCase();
37
+ const inheritedHistorical = row.depth > 0 ? (historicalByDepth[row.depth - 1] ?? false) : false;
38
+ const historical = inheritedHistorical || lower.includes("(inactive branch)") || lower.includes("(compacted)");
39
+ const scopeMatches = scope === "all" || (scope === "historical" ? historical : !historical);
40
+ const queryMatches = terms.every((term) => lower.includes(term));
41
+ included[index] = scopeMatches && queryMatches && rowTokens(row) >= minimumTokens;
42
+ ancestors[row.depth] = index;
43
+ ancestors.length = row.depth + 1;
44
+ historicalByDepth[row.depth] = historical;
45
+ historicalByDepth.length = row.depth + 1;
46
+ }
47
+ // Children follow parents in this pre-order list, so one reverse pass propagates every
48
+ // match to its ancestors in O(rows), even for a 50k-entry linear session tree.
49
+ for (let index = rows.length - 1; index >= 0; index--) {
50
+ if (!included[index]) continue;
51
+ const parent = parentByIndex[index];
52
+ if (parent !== null && parent !== undefined) included[parent] = true;
53
+ }
54
+ return rows.filter((_row, index) => included[index]);
55
+ }
56
+
57
+ /**
58
+ * A dynamically-contributed segment set (any string key from any extension, not a fixed enum)
59
+ * can't use a hardcoded per-key color map the way Papyrus's own ContextViewport did for its
60
+ * fixed seven segments -- this cycles a small categorical palette keyed by a stable hash of the
61
+ * segment key, so the same key always renders the same color within one process without needing
62
+ * every possible contributor's key to be known in advance.
63
+ */
64
+ const PALETTE: ThemeColor[] = ["accent", "success", "syntaxFunction", "warning", "syntaxKeyword", "syntaxType", "muted"];
65
+
66
+ function paletteColor(key: string): ThemeColor {
67
+ let hash = 0;
68
+ for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
69
+ return PALETTE[hash % PALETTE.length]!;
70
+ }
71
+
72
+ function formatTokenCount(tokens: number): string {
73
+ return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
74
+ }
75
+
76
+ function percentOf(part: number, whole: number): string {
77
+ return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
78
+ }
79
+
80
+ function contextDeltaLines(delta: ContextDelta): string[] {
81
+ const lifecycle = new Map<string, number>();
82
+ for (const change of delta.changes) lifecycle.set(change.lifecycle, (lifecycle.get(change.lifecycle) ?? 0) + 1);
83
+ const lifecycleText = [...lifecycle.entries()].map(([name, count]) => `${name} ${count}`).join(" · ") || "none";
84
+ const growthText = delta.growthBySource
85
+ .filter((growth) => growth.deltaTokens !== 0)
86
+ .map((growth) => `${growth.source} ${growth.deltaTokens > 0 ? "+" : ""}${growth.deltaTokens.toLocaleString()} tok`)
87
+ .join(" · ");
88
+ const changed = delta.firstChangedSegment
89
+ ? `first change: ${delta.firstChangedSegment.source} @ request ${delta.firstChangedSegment.requestPosition ?? "historical"}`
90
+ : delta.resetReason
91
+ ? `comparison reset: ${delta.resetReason}`
92
+ : "request structure unchanged";
93
+ return [
94
+ `Stable prefix ${delta.stablePrefixTokens.toLocaleString()} tok · ${changed}`,
95
+ `Lifecycle ${lifecycleText} · growth ${growthText || "none"}${delta.truncated ? " · bounded snapshot (truncated)" : ""}`,
96
+ "Stable-prefix correlation is structural evidence, not provider cache proof.",
97
+ ];
98
+ }
99
+
100
+ /** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
101
+ function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
102
+ return {
103
+ key: segment.key,
104
+ label: `${segment.label} [${segment.confidence}]`,
105
+ estimatedTokens: segment.estimatedTokens,
106
+ items: segment.items,
107
+ unknown: segment.unknown,
108
+ };
109
+ }
110
+
111
+ class ContextViewport {
112
+ private offsetY = 0;
113
+ private readonly rows: ContextRow[];
114
+ private readonly segments: readonly MalevichContextSegment[];
115
+ private searchMode = false;
116
+ private query = "";
117
+ private scope: ContextRowScope = "all";
118
+ private minimumFilterIndex = 0;
119
+
120
+ constructor(
121
+ private readonly tui: TUI,
122
+ private readonly theme: Theme,
123
+ private readonly breakdown: ContextBreakdown,
124
+ private readonly delta: ContextDelta | null,
125
+ private readonly close: () => void,
126
+ ) {
127
+ // Heaviest-first: Malevich renders segments in the order given, so sorting by weight for a
128
+ // merged multi-producer view is this viewport's own policy, matching context-report.ts.
129
+ this.segments = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
130
+ this.rows = buildContextRowsIterative(this.segments, breakdown.totalTokens ?? undefined);
131
+ }
132
+
133
+ invalidate(): void {}
134
+
135
+ private visibleRows(): ContextRow[] {
136
+ return filterContextRows(this.rows, this.query, this.scope, MIN_TOKEN_FILTERS[this.minimumFilterIndex]!);
137
+ }
138
+
139
+ private clampOffset(rows: readonly ContextRow[]): void {
140
+ this.offsetY = Math.min(this.offsetY, Math.max(0, rows.length - VISIBLE_ROWS));
141
+ }
142
+
143
+ render(width: number): string[] {
144
+ const theme = this.theme;
145
+ const contentWidth = Math.max(1, width);
146
+ const border = theme.fg("borderMuted", "─".repeat(contentWidth));
147
+ const lines: string[] = [border, truncateToWidth(theme.fg("accent", theme.bold("Context")), contentWidth, "")];
148
+
149
+ const { totalTokens, effectiveBudget } = this.breakdown;
150
+ if (totalTokens !== null && effectiveBudget !== null) {
151
+ lines.push(
152
+ truncateToWidth(
153
+ `${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`,
154
+ contentWidth,
155
+ "",
156
+ ),
157
+ );
158
+ } else if (totalTokens !== null) {
159
+ lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
160
+ } else {
161
+ lines.push(theme.fg("dim", "No real usage reported yet — sizes below are estimates only"));
162
+ }
163
+
164
+ const colorFor = (key: string) => (s: string) => theme.fg(paletteColor(key), s);
165
+ const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
166
+ lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
167
+ if (this.breakdown.overshootTokens > 0) {
168
+ lines.push(
169
+ truncateToWidth(
170
+ theme.fg(
171
+ "warning",
172
+ `Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`,
173
+ ),
174
+ contentWidth,
175
+ "",
176
+ ),
177
+ );
178
+ }
179
+ lines.push(theme.fg("dim", "Exact-text items name the model tokenizer; ≈ uses char/4; provider request totals remain aggregate."));
180
+ if (this.delta) {
181
+ for (const line of contextDeltaLines(this.delta)) lines.push(truncateToWidth(theme.fg("muted", line), contentWidth, ""));
182
+ }
183
+ lines.push("");
184
+
185
+ const rowsTheme: ContextRowsTheme = { colorFor, header: (s) => theme.bold(s) };
186
+ const filteredRows = this.visibleRows();
187
+ this.clampOffset(filteredRows);
188
+ const visible = filteredRows.slice(this.offsetY, this.offsetY + VISIBLE_ROWS);
189
+ lines.push(...renderContextRowLines(visible, contentWidth, rowsTheme));
190
+ if (filteredRows.length === 0) lines.push(theme.fg("dim", " (no matching context items)"));
191
+ else lines.push(theme.fg("muted", ` ${Math.min(this.offsetY + VISIBLE_ROWS, filteredRows.length)}/${filteredRows.length}`));
192
+
193
+ lines.push("");
194
+ const minimum = MIN_TOKEN_FILTERS[this.minimumFilterIndex]!;
195
+ const filterState = `scope: ${this.scope} · min: ${minimum === 0 ? "any" : `${formatTokenCount(minimum)} tok`}`;
196
+ lines.push(
197
+ truncateToWidth(
198
+ this.searchMode
199
+ ? theme.fg("accent", `Search: ${this.query}▌ · ${filterState}`)
200
+ : theme.fg("muted", `${this.query ? `search: ${this.query} · ` : ""}${filterState}`),
201
+ contentWidth,
202
+ "",
203
+ ),
204
+ );
205
+ lines.push(
206
+ theme.fg(
207
+ "dim",
208
+ this.searchMode
209
+ ? "type to search · backspace edit · enter apply · esc clear"
210
+ : "/ search · f scope · m min tokens · g/G top/bottom · ↑↓ scroll · esc close",
211
+ ),
212
+ );
213
+ lines.push(border);
214
+ return lines;
215
+ }
216
+
217
+ handleInput(data: string): void {
218
+ if (this.searchMode) {
219
+ if (matchesKey(data, "escape")) {
220
+ if (this.query.length > 0) this.query = "";
221
+ else this.searchMode = false;
222
+ } else if (matchesKey(data, "enter")) this.searchMode = false;
223
+ else if (matchesKey(data, "backspace")) this.query = this.query.slice(0, -1);
224
+ else if (/^[\x20-\x7e]+$/.test(data)) this.query += data;
225
+ else return;
226
+ this.offsetY = 0;
227
+ this.tui.requestRender();
228
+ return;
229
+ }
230
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
231
+ this.close();
232
+ return;
233
+ }
234
+ const rows = this.visibleRows();
235
+ if (data === "/") {
236
+ this.searchMode = true;
237
+ this.offsetY = 0;
238
+ } else if (data === "f") {
239
+ this.scope = this.scope === "all" ? "active" : this.scope === "active" ? "historical" : "all";
240
+ this.offsetY = 0;
241
+ } else if (data === "m") {
242
+ this.minimumFilterIndex = (this.minimumFilterIndex + 1) % MIN_TOKEN_FILTERS.length;
243
+ this.offsetY = 0;
244
+ } else if (data === "g") this.offsetY = 0;
245
+ else if (data === "G") this.offsetY = Math.max(0, rows.length - VISIBLE_ROWS);
246
+ else if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
247
+ else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, rows.length - VISIBLE_ROWS), this.offsetY + 1);
248
+ else return;
249
+ this.tui.requestRender();
250
+ }
251
+ }
252
+
253
+ /** Interactive scrollable Context Hub view in TUI mode; the same plain-text report as /context's non-interactive path otherwise. */
254
+ export async function showContextView(
255
+ ctx: ExtensionCommandContext,
256
+ breakdown: ContextBreakdown,
257
+ delta: ContextDelta | null = null,
258
+ ): Promise<void> {
259
+ if (ctx.mode !== "tui") {
260
+ ctx.ui.notify([buildContextReport(breakdown), ...(delta ? ["", ...contextDeltaLines(delta)] : [])].join("\n"), "info");
261
+ return;
262
+ }
263
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new ContextViewport(tui, theme, breakdown, delta, done));
264
+ }
@@ -1,13 +1,10 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
- import { ProgressBar } from "malevich-tui-components";
5
2
  import {
6
3
  FOOTER_BAR_MAX_WIDTH,
7
4
  FOOTER_BAR_MIN_WIDTH,
5
+ FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
8
6
  FOOTER_CONTEXT_ACCENT_FRACTION,
9
7
  FOOTER_CONTEXT_ERROR_FRACTION,
10
- FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
11
8
  FOOTER_CONTEXT_WARNING_FRACTION,
12
9
  FOOTER_WIDE_TERMINAL_WIDTH,
13
10
  MILLISECONDS_PER_DAY,
@@ -15,6 +12,9 @@ import {
15
12
  MILLISECONDS_PER_MINUTE,
16
13
  TELEMETRY_STALE_AFTER_MS,
17
14
  } from "@danypops/jittor";
15
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
+ import { ProgressBar } from "malevich-tui-components";
18
18
 
19
19
  type FooterColor = "accent" | "dim" | "warning" | "error";
20
20
 
@@ -48,19 +48,26 @@ interface FooterContext {
48
48
  }
49
49
 
50
50
  /** A bounded quota is explicitly remaining; unbounded values never receive a fabricated bar. */
51
- export type ProviderBudget = {
52
- kind: "bounded";
53
- label: string;
54
- remainingFraction: number;
55
- observedAt?: number;
56
- resetsAt?: number;
57
- resetText?: string;
58
- } | {
59
- kind: "unbounded";
60
- label: string;
61
- valueText: string;
62
- observedAt?: number;
63
- };
51
+ export type ProviderBudget =
52
+ | {
53
+ kind: "bounded";
54
+ label: string;
55
+ remainingFraction: number;
56
+ observedAt?: number;
57
+ resetsAt?: number;
58
+ resetText?: string;
59
+ }
60
+ | {
61
+ kind: "unbounded";
62
+ label: string;
63
+ valueText: string;
64
+ observedAt?: number;
65
+ }
66
+ | {
67
+ kind: "unavailable";
68
+ label: string;
69
+ valueText: string;
70
+ };
64
71
 
65
72
  export interface CompactionProgress {
66
73
  startedAt: number;
@@ -92,17 +99,25 @@ function footerCwd(cwd: string, home: string | undefined): string {
92
99
  const resolvedCwd = resolve(cwd);
93
100
  const resolvedHome = resolve(home);
94
101
  const relativeToHome = relative(resolvedHome, resolvedCwd);
95
- const inside = relativeToHome === "" || (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
102
+ const inside =
103
+ relativeToHome === "" || (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
96
104
  if (!inside) return cwd;
97
105
  return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
98
106
  }
99
107
 
100
108
  function sanitize(value: string): string {
101
- return value.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
109
+ return value
110
+ .replace(/[\r\n\t]/g, " ")
111
+ .replace(/ +/g, " ")
112
+ .trim();
102
113
  }
103
114
 
104
115
  function usageTotals(context: FooterContext): UsageTotals {
105
- let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
116
+ let input = 0,
117
+ output = 0,
118
+ cacheRead = 0,
119
+ cacheWrite = 0,
120
+ cost = 0;
106
121
  for (const entry of context.sessionManager.getEntries()) {
107
122
  if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
108
123
  const usage = entry.message.usage;
@@ -113,7 +128,7 @@ function usageTotals(context: FooterContext): UsageTotals {
113
128
  cost += usage?.cost?.total ?? 0;
114
129
  }
115
130
  const prompt = input + cacheRead + cacheWrite;
116
- return { input, output, cacheRead, cacheWrite, cost, ...(prompt > 0 ? { cacheHit: cacheRead / prompt * 100 } : {}) };
131
+ return { input, output, cacheRead, cacheWrite, cost, ...(prompt > 0 ? { cacheHit: (cacheRead / prompt) * 100 } : {}) };
117
132
  }
118
133
 
119
134
  function barWidth(width: number): number {
@@ -201,17 +216,25 @@ function resetLabel(resetsAt: number | undefined, now: number): string | undefin
201
216
  * the segment is omitted entirely rather than showing a placeholder that could never resolve.
202
217
  * `null` means not known yet but might resolve, which still earns the `?` placeholder.
203
218
  */
204
- function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterTheme, width: number, compact: boolean, now: number): string | undefined {
219
+ function budgetSegment(
220
+ budget: ProviderBudget | null | undefined,
221
+ theme: FooterTheme,
222
+ width: number,
223
+ compact: boolean,
224
+ now: number,
225
+ ): string | undefined {
205
226
  if (budget === undefined) return undefined;
206
227
  const w = barWidth(width);
207
228
  if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
229
+ if (budget.kind === "unavailable") return `${budget.label} ${theme.fg("warning", budget.valueText)}`;
208
230
  const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
209
231
  const staleText = stale ? ` ${theme.fg("warning", "stale")}` : "";
210
232
  if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
233
+ if (budget.resetsAt !== undefined && budget.resetsAt <= now) return `${budget.label} ${theme.fg("warning", "reset pending")}`;
211
234
  const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
212
235
  const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
213
- const value = `${(compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1))}% left`;
214
- const reset = compact ? undefined : resetLabel(budget.resetsAt, now) ?? budget.resetText;
236
+ const value = `${compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1)}% left`;
237
+ const reset = compact ? undefined : (resetLabel(budget.resetsAt, now) ?? budget.resetText);
215
238
  return `${budget.label} ${bar} ${value}${reset ? ` · ${reset}` : ""}${staleText}`;
216
239
  }
217
240
 
@@ -238,7 +261,12 @@ function repositorySegment(context: FooterContext, footerData: FooterData, theme
238
261
  return theme.fg("dim", cwd);
239
262
  }
240
263
 
241
- function modelSegments(context: FooterContext, footerData: FooterData, theme: FooterTheme, thinkingLevel: string): { full: string; compact: string } {
264
+ function modelSegments(
265
+ context: FooterContext,
266
+ footerData: FooterData,
267
+ theme: FooterTheme,
268
+ thinkingLevel: string,
269
+ ): { full: string; compact: string } {
242
270
  const model = context.model;
243
271
  const modelName = theme.bold(model?.id ?? "no-model");
244
272
  const provider = model && footerData.getAvailableProviderCount() > 1 ? `(${model.provider}) ` : "";
@@ -312,7 +340,16 @@ export function installIntegratedFooter(ctx: ExtensionContext, state: Integrated
312
340
  return {
313
341
  invalidate() {},
314
342
  render(width: number): string[] {
315
- return renderFooterLines(ctx as unknown as FooterContext, footerData, theme, state.providerBudget, getThinkingLevel(), width, Date.now(), state.compaction);
343
+ return renderFooterLines(
344
+ ctx as unknown as FooterContext,
345
+ footerData,
346
+ theme,
347
+ state.providerBudget,
348
+ getThinkingLevel(),
349
+ width,
350
+ Date.now(),
351
+ state.compaction,
352
+ );
316
353
  },
317
354
  dispose() {
318
355
  unsubscribe?.();
@@ -1,4 +1,4 @@
1
- import { classifyTaskFromTools, modelRunMetrics, type MetricObservation, type ModelRunObservation } from "@danypops/jittor";
1
+ import { classifyTaskFromTools, type MetricObservation, type ModelRunObservation, modelRunMetrics } from "@danypops/jittor";
2
2
 
3
3
  export interface ActiveLocalModelRun {
4
4
  runId: string;
@@ -59,18 +59,22 @@ export class LocalRunTelemetry {
59
59
  this.active = undefined;
60
60
  if (!active || typeof message !== "object" || message === null || Array.isArray(message)) return [];
61
61
  const value = message as Record<string, unknown>;
62
- if (value["role"] !== "assistant" || typeof value["provider"] !== "string" || typeof value["model"] !== "string") return [];
63
- const usage = typeof value["usage"] === "object" && value["usage"] !== null ? value["usage"] as Record<string, unknown> : {};
64
- const amount = (name: string): number => typeof usage[name] === "number" && Number.isFinite(usage[name]) ? usage[name] as number : 0;
65
- const cost = typeof usage["cost"] === "object" && usage["cost"] !== null && typeof (usage["cost"] as Record<string, unknown>)["total"] === "number"
66
- ? (usage["cost"] as Record<string, number>)["total"] ?? 0 : 0;
67
- const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value["stopReason"]))
68
- ? value["stopReason"] as ModelRunObservation["stopReason"] : "unknown";
62
+ if (value.role !== "assistant" || typeof value.provider !== "string" || typeof value.model !== "string") return [];
63
+ const usage = typeof value.usage === "object" && value.usage !== null ? (value.usage as Record<string, unknown>) : {};
64
+ const amount = (name: string): number =>
65
+ typeof usage[name] === "number" && Number.isFinite(usage[name]) ? (usage[name] as number) : 0;
66
+ const cost =
67
+ typeof usage.cost === "object" && usage.cost !== null && typeof (usage.cost as Record<string, unknown>).total === "number"
68
+ ? ((usage.cost as Record<string, number>).total ?? 0)
69
+ : 0;
70
+ const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value.stopReason))
71
+ ? (value.stopReason as ModelRunObservation["stopReason"])
72
+ : "unknown";
69
73
  const completedAt = Math.max(Date.now(), active.firstTokenAt ?? active.startedAt, active.startedAt);
70
74
  this.lastCompleted = {
71
75
  runId: active.runId,
72
- provider: value["provider"],
73
- model: value["model"],
76
+ provider: value.provider,
77
+ model: value.model,
74
78
  thinking: thinkingLevel,
75
79
  ...classifyTaskFromTools(active.toolNames),
76
80
  startedAt: active.startedAt,