@animalabs/connectome-host 0.7.0 → 0.7.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.1 — 2026-07-26
6
+
7
+ ### Changed
8
+
9
+ - **Health call stats are now per-call, not cumulative.** The previous version
10
+ rolled everything into two totals (main / compression), which hid exactly what
11
+ you want to see — how an individual turn behaved. Now one row per call, newest
12
+ first: time, origin, messages, fresh input, cached tokens, cached share, cache
13
+ write, output, breakpoints, duration and verdict, with refusals and errors
14
+ highlighted. Cumulative totals for the session remain in the Usage panel.
15
+
16
+ ## Unreleased
17
+
5
18
  ## 0.7.0 — 2026-07-26
6
19
 
7
20
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/connectome-host",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "General-purpose agent TUI host with recipe-based configuration",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -142,132 +142,107 @@ export function OpsAlertStrip(props: {
142
142
  }
143
143
 
144
144
 
145
- /**
146
- * Aggregate the call ledger by origin.
147
- *
148
- * `originEstimate` is the main-vs-compression split, and the `~` is honest: it
149
- * is derived from stream-vs-complete (agent turns stream; compression and
150
- * summarizer calls use complete()), NOT from a definitive origin tag. It is a
151
- * reliable proxy in practice, but it is an inference.
152
- */
153
- interface CallAgg {
154
- calls: number;
155
- input: number;
156
- output: number;
157
- cacheRead: number;
158
- cacheWrite: number;
159
- cost: number;
160
- hits: number;
161
- errors: number;
162
- refusals: number;
163
- breakpoints: number;
164
- lastInputs: number[];
165
- }
166
-
167
- const EMPTY_AGG = (): CallAgg => ({
168
- calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0,
169
- cost: 0, hits: 0, errors: 0, refusals: 0, breakpoints: 0, lastInputs: [],
170
- });
171
-
172
- /** Verdicts that mean the prefix was actually reused. */
173
- const HIT_VERDICTS = new Set(['HIT', 'hit+extend']);
174
-
175
- function aggregate(rows: LedgerRow[], origin: 'turn~' | 'aux~'): CallAgg {
176
- const a = EMPTY_AGG();
177
- for (const r of rows) {
178
- if (r.originEstimate !== origin) continue;
179
- a.calls++;
180
- a.input += r.tokens.input;
181
- a.output += r.tokens.output;
182
- a.cacheRead += r.tokens.cacheRead;
183
- a.cacheWrite += r.tokens.cacheWrite;
184
- a.cost += r.cost?.total ?? 0;
185
- if (HIT_VERDICTS.has(r.verdict)) a.hits++;
186
- if (r.error) a.errors++;
187
- if (r.stopReason === 'refusal') a.refusals++;
188
- a.breakpoints += r.cache.breakpoints ?? 0;
189
- a.lastInputs.push(r.tokens.input);
190
- }
191
- a.lastInputs = a.lastInputs.slice(-8);
192
- return a;
193
- }
194
-
195
145
  const n0 = (v: number) => v.toLocaleString();
196
- const pct = (num: number, den: number) => (den > 0 ? `${Math.round((100 * num) / den)}%` : '—');
197
-
198
- /** Cached share of what was sent — the number that tells you whether the prefix
199
- * is being reused or re-read. */
200
- const cachedShare = (a: CallAgg) => {
201
- const sent = a.input + a.cacheRead;
202
- return sent > 0 ? `${Math.round((100 * a.cacheRead) / sent)}%` : '—';
203
- };
204
146
 
205
147
  function CallStats(props: { rows: LedgerRow[] }) {
206
- const main = () => aggregate(props.rows, 'turn~');
207
- const aux = () => aggregate(props.rows, 'aux~');
148
+ /** Newest first the interesting call is the one that just happened. */
149
+ const recent = () => [...props.rows].reverse().slice(0, 24);
208
150
 
209
- const col = (label: string, a: () => CallAgg, tone: string) => (
210
- <div class="flex-1 min-w-[9rem]">
211
- <div class={`text-[10px] uppercase tracking-wider font-semibold mb-1 ${tone}`}>
212
- {label} <span class="text-neutral-600">({a().calls})</span>
213
- </div>
214
- <table class="w-full font-mono text-[10px]">
215
- <tbody>
216
- <For each={[
217
- ['fresh input', n0(a().input)],
218
- ['cache read', n0(a().cacheRead)],
219
- ['cache write', n0(a().cacheWrite)],
220
- ['cached share', cachedShare(a())],
221
- ['prefix reused', pct(a().hits, a().calls)],
222
- ['output', n0(a().output)],
223
- ['avg breakpoints', a().calls ? (a().breakpoints / a().calls).toFixed(1) : '—'],
224
- ['cost', a().cost ? `$${a().cost.toFixed(4)}` : '—'],
225
- ] as Array<[string, string]>}>
226
- {([k, v]) => (
227
- <tr>
228
- <td class="text-neutral-500 pr-2">{k}</td>
229
- <td class="text-neutral-200 text-right tabular-nums">{v}</td>
230
- </tr>
231
- )}
232
- </For>
233
- <Show when={a().errors > 0 || a().refusals > 0}>
234
- <tr>
235
- <td class="text-neutral-500 pr-2">errors / refusals</td>
236
- <td class="text-right tabular-nums text-red-300">
237
- {a().errors} / {a().refusals}
238
- </td>
239
- </tr>
240
- </Show>
241
- </tbody>
242
- </table>
243
- <Show when={a().lastInputs.length > 1}>
244
- <div class="text-[9px] text-neutral-600 mt-1 font-mono break-all"
245
- title="fresh input tokens, oldest → newest — a descent should trend down">
246
- {a().lastInputs.map(n0).join(' → ')}
247
- </div>
248
- </Show>
249
- </div>
250
- );
151
+ const clock = (ts: string) => {
152
+ const d = new Date(ts);
153
+ return Number.isNaN(d.getTime()) ? '—' : d.toISOString().slice(11, 19);
154
+ };
155
+ /** Fraction of the prompt that was reused rather than re-read. */
156
+ const share = (r: LedgerRow) => {
157
+ const sent = r.tokens.input + r.tokens.cacheRead;
158
+ return sent > 0 ? Math.round((100 * r.tokens.cacheRead) / sent) : null;
159
+ };
251
160
 
252
161
  return (
253
162
  <div>
254
163
  <div class="text-[10px] uppercase tracking-wider text-neutral-600 mb-1">
255
- recent llm calls ({props.rows.length})
164
+ llm calls — newest first ({props.rows.length} held)
256
165
  </div>
257
- <div class="flex gap-4 flex-wrap">
258
- {col('main turns', main, 'text-cyan-400')}
259
- {col('compression', aux, 'text-orange-400')}
166
+ <div class="overflow-x-auto">
167
+ <table class="w-full font-mono text-[10px] whitespace-nowrap">
168
+ <thead>
169
+ <tr class="text-neutral-600 border-b border-neutral-800">
170
+ <th class="text-left pr-2 font-normal">time</th>
171
+ <th class="text-left pr-2 font-normal">origin</th>
172
+ <th class="text-right pr-2 font-normal">msgs</th>
173
+ <th class="text-right pr-2 font-normal">fresh</th>
174
+ <th class="text-right pr-2 font-normal">cached</th>
175
+ <th class="text-right pr-2 font-normal">%c</th>
176
+ <th class="text-right pr-2 font-normal">write</th>
177
+ <th class="text-right pr-2 font-normal">out</th>
178
+ <th class="text-right pr-2 font-normal">bp</th>
179
+ <th class="text-right pr-2 font-normal">ms</th>
180
+ <th class="text-left font-normal">verdict</th>
181
+ </tr>
182
+ </thead>
183
+ <tbody>
184
+ <For each={recent()}>
185
+ {(r) => {
186
+ const main = r.originEstimate === 'turn~';
187
+ return (
188
+ <tr class={`border-b border-neutral-900 ${
189
+ r.error || r.stopReason === 'refusal' ? 'bg-red-950/30' : ''
190
+ }`}>
191
+ <td class="pr-2 text-neutral-500">{clock(r.timestamp)}</td>
192
+ <td class={`pr-2 ${main ? 'text-cyan-400' : 'text-orange-400'}`}>
193
+ {main ? 'turn' : 'compr'}
194
+ </td>
195
+ <td class="pr-2 text-right text-neutral-400">{r.messages}</td>
196
+ <td class="pr-2 text-right text-neutral-100">{n0(r.tokens.input)}</td>
197
+ <td class="pr-2 text-right text-sky-300">{n0(r.tokens.cacheRead)}</td>
198
+ <td class="pr-2 text-right text-neutral-400">
199
+ {share(r) === null ? '—' : `${share(r)}%`}
200
+ </td>
201
+ <td class="pr-2 text-right text-violet-300">
202
+ {r.tokens.cacheWrite ? n0(r.tokens.cacheWrite) : '·'}
203
+ </td>
204
+ <td class="pr-2 text-right text-neutral-400">{n0(r.tokens.output)}</td>
205
+ <td class="pr-2 text-right text-neutral-600">{r.cache.breakpoints ?? '·'}</td>
206
+ <td class="pr-2 text-right text-neutral-600">{n0(r.durationMs)}</td>
207
+ <td class={verdictTone(r.verdict)} title={r.cause}>
208
+ {r.verdict}
209
+ <Show when={r.stopReason === 'refusal'}>
210
+ <span class="text-red-300"> refusal</span>
211
+ </Show>
212
+ <Show when={r.error}>
213
+ <span class="text-red-300"> err</span>
214
+ </Show>
215
+ </td>
216
+ </tr>
217
+ );
218
+ }}
219
+ </For>
220
+ </tbody>
221
+ </table>
260
222
  </div>
261
223
  <div class="text-[9px] text-neutral-600 mt-1 leading-relaxed">
262
- Origin is inferred from stream-vs-complete (turns stream; compression uses
263
- complete) — a reliable proxy, not a definitive tag, hence <span class="font-mono">~</span>.
264
- “cached share” is cacheRead ÷ (input+cacheRead) what fraction of the prompt
265
- was reused rather than re-read.
224
+ <span class="text-cyan-400">turn</span> = main inference,
225
+ <span class="text-orange-400"> compr</span> = compression/summarizer — inferred from
226
+ stream-vs-complete, not a definitive tag (hence <span class="font-mono">~</span> upstream).
227
+ One turn may appear as a turn row plus several compr rows.
228
+ <span class="font-mono"> fresh</span> = tokens billed as new input;
229
+ <span class="font-mono"> cached</span> = read from cache;
230
+ <span class="font-mono"> %c</span> = cached ÷ (fresh+cached);
231
+ <span class="font-mono"> bp</span> = cache breakpoints.
266
232
  </div>
267
233
  </div>
268
234
  );
269
235
  }
270
236
 
237
+ /** Verdict colouring: reuse is good, rewrites cost a full re-read. */
238
+ function verdictTone(v: string): string {
239
+ if (v === 'HIT' || v === 'hit+extend') return 'text-emerald-300';
240
+ if (v === 'first-write') return 'text-violet-300';
241
+ if (v === 'uncached') return 'text-neutral-400';
242
+ if (v === 'ERROR' || v === 'empty') return 'text-red-300';
243
+ return 'text-amber-300';
244
+ }
245
+
271
246
  function CompositionBlock(props: { c: ContextComposition }) {
272
247
  const rows = () => {
273
248
  const c = props.c;