@arbiterforge/ca-pi 0.15.6 → 0.15.8

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.
@@ -205,16 +205,76 @@ def model_pill(model, effort=""):
205
205
 
206
206
 
207
207
  def usage_row(label, tin, tout, cost, trail=""):
208
- """One row of the Session/Today mini-table: label │ ↓in ↑out │ $cost. in/out are
209
- fresh (sent) tokens; `cost` is Claude Code's real session cost (cost.total_cost_usd),
210
- not an estimate. `trail` carries extras (session age, burn sparkline)."""
208
+ """One row of the Session/Today mini-table: label | down-in up-out | cost.
209
+ in/out are fresh (sent) tokens. `cost` is a pre-rendered cell from
210
+ cost_cell(), whose label carries the figure's provenance; a bare number is
211
+ still accepted for callers outside the usage block. `trail` carries extras
212
+ (the burn sparkline)."""
213
+ money = cost if isinstance(cost, str) else f"{OK}{usd_fine(cost)}{RESET}"
211
214
  base = (f"{GREY}{label:<7}{RESET} {V0}{_VBAR}{RESET} "
212
215
  f"{V2}{DN}{RESET} {WHITE}{fmt_tok(tin):>6}{RESET} "
213
216
  f"{V2}{UP}{RESET} {WHITE}{fmt_tok(tout):>6}{RESET} {V0}{_VBAR}{RESET} "
214
- f"{OK}{usd_fine(cost)}{RESET}")
217
+ f"{money}")
215
218
  return base + (f" {trail}" if trail else "")
216
219
 
217
220
 
221
+ # Visible columns of usage_row() before the cost cell: label(7) + separators and
222
+ # two right-aligned 6-column token counts.
223
+ USAGE_ROW_FIXED = 30
224
+ # Coverage reasons that mean observed usage could not be priced at all.
225
+ PRICING_REASONS = frozenset({"unknown_model", "unpriced_modifier", "unknown_cache_ttl",
226
+ "unknown_charge", "unknown_iteration", "model_conflict"})
227
+
228
+
229
+ def cost_label(scope, allow_host=False, compact=False):
230
+ """(text, tone) for one Session/Today dollar figure, from the ledger's
231
+ structured coverage (spec AC-01/02, D-19). The renderer never infers
232
+ completeness from a number:
233
+ api≈$N complete reconstruction at pinned list rates
234
+ api≥$N a true lower bound: partial coverage, or still catching up
235
+ api≈? no defensible dollar figure: grey when nothing is reconstructed
236
+ yet (neutral, not a warning), amber when usage exists but
237
+ none of it could be priced
238
+ host≈$N Session only: Claude Code's own estimate, when reconstruction
239
+ is unavailable (never Today, never mixed with api figures)
240
+ A trailing `*` marks a stale snapshot, shown because this render lost the
241
+ ledger lock. `compact` drops the api/host words and keeps the provenance
242
+ symbol, for narrow boxes."""
243
+ scope = scope if isinstance(scope, dict) else {}
244
+ state = scope.get("state") or "unavailable"
245
+ api = "" if compact else "api"
246
+ host = scope.get("host") if allow_host else None
247
+ if state == "unavailable":
248
+ if isinstance(host, (int, float)) and not isinstance(host, bool) and host >= 0:
249
+ text, tone = ("h" if compact else "host") + "≈" + usd_fine(host), "host"
250
+ else:
251
+ text, tone = api + "≈?", "none"
252
+ elif state == "complete":
253
+ text, tone = api + "≈" + usd_fine(scope.get("cost")), "ok"
254
+ else:
255
+ reasons = set(scope.get("reasons") or ())
256
+ used = num(scope.get("in")) + num(scope.get("out")) > 0
257
+ if num(scope.get("pd")) <= 0 and used and reasons & PRICING_REASONS:
258
+ text, tone = api + "≈?", "unknown"
259
+ else:
260
+ text, tone = api + "≥" + usd_fine(scope.get("cost")), "partial"
261
+ if scope.get("stale"):
262
+ text += "*"
263
+ return text, tone
264
+
265
+
266
+ def cost_cell(scope, width, allow_host=False):
267
+ """The colored cost cell: the full label when it fits `width` visible
268
+ columns, else the compact form, so a figure is never clipped mid-number."""
269
+ text, tone = cost_label(scope, allow_host)
270
+ if len(text) > width:
271
+ text, tone = cost_label(scope, allow_host, compact=True)
272
+ # Amber only where coverage genuinely needs attention; "none" (nothing
273
+ # reconstructed) stays neutral, since amber is a status signal.
274
+ color = {"ok": OK, "partial": WARN, "unknown": WARN, "host": GREY, "none": GREY}.get(tone, GREY)
275
+ return f"{color}{text}{RESET}"
276
+
277
+
218
278
  def seg_lines(data):
219
279
  add = get(data, "cost", "total_lines_added")
220
280
  rem = get(data, "cost", "total_lines_removed")
@@ -2,10 +2,12 @@
2
2
  # codeArbiter — statusline subagent-rows helpers (extracted from statusline.py,
3
3
  # architecture-004).
4
4
  #
5
- # Owns resolving the current session's subagents directory, reading its recent
6
- # `.jsonl` transcripts (deduped per requestId, same rule as the Session/Today
7
- # rows), and labeling each subagent from its first user message. Carries no
8
- # rendering concern — the statusline turns the returned dicts into rows.
5
+ # Owns resolving the current session's subagents directory and turning the
6
+ # accounting scanner's per-child presentation summaries (from the ledger record)
7
+ # into rows, plus labeling a subagent from its first user message (sub_label is
8
+ # called BY that scanner). It never parses a child transcript itself: the render
9
+ # path has one JSONL parser (_ledgerlib). No rendering concern — the statusline
10
+ # turns the returned dicts into rows.
9
11
  #
10
12
  # Design principles (mirroring _ledgerlib.py):
11
13
  # - Stdlib only; no third-party imports ever.
@@ -14,11 +16,10 @@
14
16
  #
15
17
  # Public API:
16
18
  # subagent_dir(data, root, sid) -> str|None
17
- # read_subagents(sdir) -> (active, recent, shown, (tot_in, tot_out))
19
+ # read_subagents(sdir, rec=None) -> (active, recent, shown, (tot_in, tot_out))
18
20
  # sub_label(content) -> str
19
21
  # display_model(model_id) -> str
20
22
 
21
- import json
22
23
  import os
23
24
  import re
24
25
  import time
@@ -35,8 +36,7 @@ except Exception: # pragma: no cover — never let an import break the statusli
35
36
  ACTIVE_WINDOW = 150 # secs: a subagent file touched this recently is "active"
36
37
  SHOW_WINDOW = 600 # secs: still display recently-finished subagents
37
38
  MAX_SUB_ROWS = 4
38
- MAX_SUB_FILES = 12 # hot-path bound: parse at most this many files / render
39
- MAX_SUB_LINES = 2500 # per-file line cap
39
+ MAX_SUB_FILES = 12 # rows considered per render (presentation bound, not accounting)
40
40
 
41
41
 
42
42
  def display_model(model_id):
@@ -88,17 +88,24 @@ def subagent_dir(data, root, sid):
88
88
  return None
89
89
 
90
90
 
91
- def read_subagents(sdir):
92
- """Return (active, recent, shown[{label,inp,out,age,active}], (tot_in, tot_out)).
91
+ def read_subagents(sdir, rec=None):
92
+ """Return (active, recent, shown[{label,model,inp,out,age,active}], (tot_in, tot_out)).
93
93
  `active` = files touched within ACTIVE_WINDOW (a liveness proxy); `recent` =
94
94
  all files within SHOW_WINDOW (active + recently finished).
95
95
 
96
+ Rows come from the accounting scanner's per-source PRESENTATION SUMMARIES in
97
+ the ledger record `rec` (spec D-7): label, observed models and owner-attributed
98
+ tokens, with fork-replayed parent requests excluded. This reader only lists
99
+ the directory and stats files for liveness. It never opens or parses a child
100
+ transcript, so the render path has exactly one JSONL parser. A child the
101
+ scanner has not reached yet shows its fallback name and zero tokens until it
102
+ has.
103
+
96
104
  ONE result shape on EVERY path (#413). The element types are stable — int,
97
105
  int, list, (int|float, int|float) — so a caller may unpack four names
98
106
  unconditionally. statusline.py does exactly that, OUTSIDE its safe()
99
- wrapper, so an error branch returning a shorter tuple was a hard ValueError
100
- that erased the entire subagent section on a routine directory race. An
101
- unreadable/absent directory is an EMPTY result, not a different result."""
107
+ wrapper, so an error branch returning a shorter tuple would be a hard
108
+ ValueError. An unreadable/absent directory is an EMPTY result."""
102
109
  empty = (0, 0, [], (0, 0))
103
110
  now = time.time()
104
111
  files = []
@@ -117,61 +124,20 @@ def read_subagents(sdir):
117
124
  return empty
118
125
  files.sort(reverse=True) # most-recently-touched first
119
126
 
127
+ sources = rec.get("src") if isinstance(rec, dict) else None
128
+ sources = sources if isinstance(sources, dict) else {}
120
129
  active = sum(1 for mtime, _, _, _ in files if now - mtime <= ACTIVE_WINDOW)
121
130
  shown, tot_in, tot_out = [], 0, 0
122
- for mtime, size, fp, nm in files[:MAX_SUB_FILES]:
131
+ for mtime, size, _fp, nm in files[:MAX_SUB_FILES]:
123
132
  if size > 16 * 1024 * 1024:
124
133
  continue
125
- reqs = {}
126
- models = []
127
- label = None
128
- try:
129
- with open(fp, encoding="utf-8", errors="replace") as f:
130
- for i, ln in enumerate(f):
131
- if i > MAX_SUB_LINES:
132
- break
133
- ln = ln.strip()
134
- if not ln:
135
- continue
136
- try:
137
- d = json.loads(ln)
138
- except ValueError:
139
- continue
140
- if not isinstance(d, dict):
141
- # A transcript line may be ANY valid JSON value; only an
142
- # object carries an event. `[]`/`null`/`3`/`"s"` used to
143
- # reach .get() below inside a try that caught OSError
144
- # only, so one such line raised AttributeError out of
145
- # read_subagents and blanked every subagent row (#413).
146
- continue
147
- msg = d.get("message")
148
- if not isinstance(msg, dict):
149
- continue
150
- if label is None and msg.get("role") == "user":
151
- label = sub_label(msg.get("content"))
152
- if msg.get("role") == "assistant":
153
- raw_model = msg.get("model")
154
- if isinstance(raw_model, str) and raw_model.strip():
155
- raw_model = raw_model.strip()
156
- if raw_model not in models:
157
- models.append(raw_model)
158
- u = msg.get("usage")
159
- if isinstance(u, dict):
160
- # dedupe by requestId; fresh input only (cache reads excluded)
161
- # — same definition as the Session/Today rows.
162
- reqs[d.get("requestId") or msg.get("id") or i] = (
163
- num(u.get("input_tokens")) + num(u.get("cache_creation_input_tokens")),
164
- num(u.get("output_tokens")))
165
- except Exception: # noqa: BLE001 — #413: the error boundary is PER FILE
166
- # One unreadable or structurally surprising transcript must cost
167
- # only its own row, never the rest of the directory. Widened from
168
- # OSError so a shape the reader has not anticipated degrades the
169
- # same way malformed JSON already does (the module's standing
170
- # "never raise on malformed input" contract) instead of escaping
171
- # into the statusline and blanking every subagent row.
172
- continue
173
- inp = sum(v[0] for v in reqs.values())
174
- out = sum(v[1] for v in reqs.values())
134
+ summary = sources.get("c:" + nm)
135
+ summary = summary if isinstance(summary, dict) else {}
136
+ tok = summary.get("tok")
137
+ inp, out = ((num(tok[0]), num(tok[1])) if isinstance(tok, list) and len(tok) == 2
138
+ else (0, 0))
139
+ models = [m for m in (summary.get("models") or []) if isinstance(m, str)]
140
+ label = summary.get("label") if isinstance(summary.get("label"), str) else None
175
141
  tot_in += inp
176
142
  tot_out += out
177
143
  if len(shown) < MAX_SUB_ROWS: