@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +16 -0
- package/plugins/ca-pi/hooks/_gitlib.py +8 -2
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_ledgerlib.py +1022 -355
- package/plugins/ca-pi/hooks/_segmentslib.py +64 -4
- package/plugins/ca-pi/hooks/_subagentslib.py +30 -64
- package/plugins/ca-pi/hooks/_usagelib.py +484 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/statusline.py +40 -24
- package/plugins/ca-pi/package.json +1 -1
|
@@ -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
|
|
209
|
-
fresh (sent) tokens
|
|
210
|
-
|
|
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"{
|
|
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
|
|
6
|
-
#
|
|
7
|
-
# rows
|
|
8
|
-
#
|
|
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 #
|
|
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
|
|
100
|
-
|
|
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,
|
|
131
|
+
for mtime, size, _fp, nm in files[:MAX_SUB_FILES]:
|
|
123
132
|
if size > 16 * 1024 * 1024:
|
|
124
133
|
continue
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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:
|