@arbiterforge/ca-pi 0.15.5 → 0.15.7
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 +14 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_ledgerlib.py +1022 -355
- package/plugins/ca-pi/hooks/_reconciliationlib.py +82 -14
- 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/artifact-reconcile.py +5 -1
- 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
|
@@ -23,8 +23,10 @@ import _replylib
|
|
|
23
23
|
|
|
24
24
|
ID_RE = re.compile(r"[A-Z][A-Z0-9_-]{0,127}")
|
|
25
25
|
TOKEN_RE = re.compile(r"[A-Za-z0-9_-]{12,128}")
|
|
26
|
-
PENDING_DIR = Path("
|
|
27
|
-
ATTEMPT_DIR = Path("
|
|
26
|
+
PENDING_DIR = Path("pending")
|
|
27
|
+
ATTEMPT_DIR = Path("attempts")
|
|
28
|
+
LEGACY_PENDING_DIR = Path(".codearbiter/.markers/reconciliations")
|
|
29
|
+
LEGACY_ATTEMPT_DIR = Path(".codearbiter/.markers/reconciliation-attempts")
|
|
28
30
|
SOURCE_DIR = Path(".codearbiter/.artifacts/authority-sources")
|
|
29
31
|
PROVEN_UNCOMMITTED = frozenset({"REVISION_CONFLICT", "OPERATION_ROLLED_BACK"})
|
|
30
32
|
|
|
@@ -53,6 +55,65 @@ def _attempt(pending: dict[str, Any]) -> Path:
|
|
|
53
55
|
return ATTEMPT_DIR / f"{_operation_id(pending)}.json"
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def _state_root(root: Path) -> Path:
|
|
59
|
+
"""Keep operational state outside every repository input root."""
|
|
60
|
+
try:
|
|
61
|
+
spool = _artifactauthoritylib._spool_root(root)
|
|
62
|
+
return _artifactauthoritylib._safe_directory(spool, Path("reconciliations"), create=True)
|
|
63
|
+
except _artifactauthoritylib.AuthorityError as exc:
|
|
64
|
+
raise ReconciliationError("Git admin reconciliation storage is unavailable or unsafe") from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _legacy_pending(root: Path, artifact_id: str) -> Path:
|
|
68
|
+
return _legacy_file(root, LEGACY_PENDING_DIR, f"{artifact_id}.json")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _legacy_file(root: Path, directory: Path, filename: str) -> Path:
|
|
72
|
+
try:
|
|
73
|
+
parent = _approvallib._safe_directory(root, directory, create=False)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
return root / directory / filename
|
|
76
|
+
except _approvallib.ApprovalError as exc:
|
|
77
|
+
raise ReconciliationError("legacy reconciliation directory is unsafe") from exc
|
|
78
|
+
return parent / filename
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _state_file(root: Path, directory: Path, filename: str) -> Path:
|
|
82
|
+
try:
|
|
83
|
+
parent = _artifactauthoritylib._safe_directory(_state_root(root), directory, create=True)
|
|
84
|
+
except _artifactauthoritylib.AuthorityError as exc:
|
|
85
|
+
raise ReconciliationError("Git admin reconciliation directory is unsafe") from exc
|
|
86
|
+
return parent / filename
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _present(path: Path) -> bool:
|
|
90
|
+
try:
|
|
91
|
+
path.lstat()
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _pending_path(root: Path, artifact_id: str) -> Path:
|
|
98
|
+
current = _state_file(root, PENDING_DIR, f"{artifact_id}.json")
|
|
99
|
+
legacy = _legacy_pending(root, artifact_id)
|
|
100
|
+
if _present(current) and _present(legacy):
|
|
101
|
+
raise ReconciliationError("ambiguous reconciliation state in repository and Git admin storage")
|
|
102
|
+
return legacy if _present(legacy) else current
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _attempt_path(root: Path, pending: dict[str, Any]) -> Path:
|
|
106
|
+
filename = f"{_operation_id(pending)}.json"
|
|
107
|
+
legacy = _legacy_file(root, LEGACY_ATTEMPT_DIR, filename)
|
|
108
|
+
current = _state_file(root, ATTEMPT_DIR, filename)
|
|
109
|
+
legacy_pending = _present(_legacy_pending(root, pending["artifact_id"]))
|
|
110
|
+
if (_present(legacy) and _present(current)) or (
|
|
111
|
+
legacy_pending and _present(current)
|
|
112
|
+
) or (not legacy_pending and _present(legacy)):
|
|
113
|
+
raise ReconciliationError("mixed reconciliation state requires explicit repair")
|
|
114
|
+
return legacy if legacy_pending else current
|
|
115
|
+
|
|
116
|
+
|
|
56
117
|
def _mutation_request(pending: dict[str, Any], receipt: str) -> dict[str, Any]:
|
|
57
118
|
identity = pending["identity"]
|
|
58
119
|
request = {
|
|
@@ -72,7 +133,7 @@ def _reconciliation_lock(root: Path, artifact_id: str):
|
|
|
72
133
|
"""Serialize all transitions for one artifact, including authority revocation."""
|
|
73
134
|
if ID_RE.fullmatch(artifact_id) is None:
|
|
74
135
|
raise ReconciliationError("reconciliation ID is invalid")
|
|
75
|
-
parent =
|
|
136
|
+
parent = _state_file(root, PENDING_DIR, artifact_id).parent
|
|
76
137
|
path = parent / f"{artifact_id}.lock"
|
|
77
138
|
flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
78
139
|
fd = os.open(path, flags, 0o600)
|
|
@@ -147,11 +208,11 @@ def _write_attempt(root: Path, pending: dict[str, Any], request: dict[str, Any])
|
|
|
147
208
|
"format": "codearbiter.reconciliation-attempt/0.1.0",
|
|
148
209
|
"pending_sha256": _digest(_canonical(pending)), "request": request,
|
|
149
210
|
}
|
|
150
|
-
_write_atomic_new(root, _attempt(pending), _canonical(record))
|
|
211
|
+
_write_atomic_new(_state_root(root), _attempt(pending), _canonical(record))
|
|
151
212
|
|
|
152
213
|
|
|
153
214
|
def _load_attempt(root: Path, pending: dict[str, Any]) -> dict[str, Any] | None:
|
|
154
|
-
path = root
|
|
215
|
+
path = _attempt_path(root, pending)
|
|
155
216
|
try:
|
|
156
217
|
info = path.lstat()
|
|
157
218
|
except FileNotFoundError:
|
|
@@ -225,6 +286,8 @@ def _arm_reconciliation_unlocked(
|
|
|
225
286
|
token = token or secrets.token_urlsafe(24)
|
|
226
287
|
if TOKEN_RE.fullmatch(token) is None:
|
|
227
288
|
raise ReconciliationError("reconciliation token is invalid")
|
|
289
|
+
if _present(_legacy_pending(root, artifact_id)):
|
|
290
|
+
raise ReconciliationError("PENDING_RECONCILIATION: legacy request must be cancelled or recovered")
|
|
228
291
|
identity, snapshot, record = _current(client, artifact_id, target_id)
|
|
229
292
|
reply = (
|
|
230
293
|
f"reconcile-task {artifact_id} {target_id} {target_state} {token}"
|
|
@@ -240,10 +303,11 @@ def _arm_reconciliation_unlocked(
|
|
|
240
303
|
"prompt_sha256": _digest(reply.encode()),
|
|
241
304
|
}
|
|
242
305
|
relative = _pending(artifact_id)
|
|
306
|
+
state = _state_root(root)
|
|
243
307
|
created = False
|
|
244
308
|
try:
|
|
245
309
|
pending_bytes = _canonical(pending)
|
|
246
|
-
_write_atomic_new(
|
|
310
|
+
_write_atomic_new(state, relative, pending_bytes)
|
|
247
311
|
created = True
|
|
248
312
|
_artifactpromptlib.register(
|
|
249
313
|
root, "reconciliation", artifact_id, reply,
|
|
@@ -254,7 +318,7 @@ def _arm_reconciliation_unlocked(
|
|
|
254
318
|
except Exception:
|
|
255
319
|
if created:
|
|
256
320
|
try:
|
|
257
|
-
(
|
|
321
|
+
(state / relative).unlink()
|
|
258
322
|
except OSError:
|
|
259
323
|
pass
|
|
260
324
|
raise
|
|
@@ -267,7 +331,7 @@ def _arm_reconciliation_unlocked(
|
|
|
267
331
|
|
|
268
332
|
def _load(root: Path, artifact_id: str) -> dict[str, Any]:
|
|
269
333
|
try:
|
|
270
|
-
path = root
|
|
334
|
+
path = _pending_path(root, artifact_id)
|
|
271
335
|
info = path.lstat()
|
|
272
336
|
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
273
337
|
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode) or info.st_size > 131072:
|
|
@@ -328,7 +392,7 @@ def _cancel_reconciliation_unlocked(root: Path, artifact_id: str, prompt: str) -
|
|
|
328
392
|
raise ReconciliationError("reconciliation authority binding is unavailable") from exc
|
|
329
393
|
if binding != _digest(_canonical(pending)):
|
|
330
394
|
raise ReconciliationError("pending reconciliation changed after arming")
|
|
331
|
-
(root
|
|
395
|
+
_pending_path(root, artifact_id).unlink()
|
|
332
396
|
_artifactpromptlib.unregister(root, "reconciliation", artifact_id)
|
|
333
397
|
return {"artifact_id": artifact_id, "cancelled": True}
|
|
334
398
|
|
|
@@ -349,7 +413,7 @@ def cancel_orphaned_reconciliation(root: str | Path, artifact_id: str) -> dict[s
|
|
|
349
413
|
raise ReconciliationError("reconciliation authority binding is unreadable") from exc
|
|
350
414
|
if binding == _digest(_canonical(pending)):
|
|
351
415
|
raise ReconciliationError("reconciliation route is active; cancel with its exact prompt")
|
|
352
|
-
(root
|
|
416
|
+
_pending_path(root, artifact_id).unlink()
|
|
353
417
|
if binding is not None:
|
|
354
418
|
_artifactpromptlib.unregister(root, "reconciliation", artifact_id)
|
|
355
419
|
return {"artifact_id": artifact_id, "cancelled": True, "orphaned": True}
|
|
@@ -379,9 +443,10 @@ def recover_reconciliation(root: str | Path, client: Any, artifact_id: str) -> d
|
|
|
379
443
|
raise
|
|
380
444
|
result = None
|
|
381
445
|
committed = False
|
|
382
|
-
(root
|
|
446
|
+
attempt_path = _attempt_path(root, pending)
|
|
447
|
+
_pending_path(root, artifact_id).unlink()
|
|
383
448
|
_artifactpromptlib.unregister(root, "reconciliation", artifact_id)
|
|
384
|
-
|
|
449
|
+
attempt_path.unlink()
|
|
385
450
|
return {
|
|
386
451
|
"artifact_id": artifact_id, "committed": committed,
|
|
387
452
|
"revision": result.get("revision") if committed else None,
|
|
@@ -407,6 +472,8 @@ def _consume_reconciliation_unlocked(
|
|
|
407
472
|
parts = prompt.split(" ")
|
|
408
473
|
artifact_id, target_id = parts[1], parts[2]
|
|
409
474
|
pending = _load(root, artifact_id)
|
|
475
|
+
if _present(_legacy_pending(root, artifact_id)):
|
|
476
|
+
raise ReconciliationError("legacy reconciliation must be cancelled or recovered before consumption")
|
|
410
477
|
if not isinstance(host, str) or not host or not isinstance(session_id, str) or not session_id:
|
|
411
478
|
raise ReconciliationError("reconciliation host context is invalid")
|
|
412
479
|
try:
|
|
@@ -456,9 +523,10 @@ def _consume_reconciliation_unlocked(
|
|
|
456
523
|
if request is None:
|
|
457
524
|
raise ReconciliationError("in-flight reconciliation record disappeared")
|
|
458
525
|
result = client.call(pending["operation"], request)
|
|
459
|
-
(root
|
|
526
|
+
attempt_path = _attempt_path(root, pending)
|
|
527
|
+
_pending_path(root, artifact_id).unlink()
|
|
460
528
|
_artifactpromptlib.unregister(root, "reconciliation", artifact_id)
|
|
461
|
-
|
|
529
|
+
attempt_path.unlink()
|
|
462
530
|
return {"matched": True, "reconciled": True, "artifact_id": artifact_id, "target_id": target_id, "receipt": request["receipt"], "revision": result.get("revision")}
|
|
463
531
|
|
|
464
532
|
|
|
@@ -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:
|