cctally 1.28.0 → 1.29.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.
- package/CHANGELOG.md +18 -0
- package/bin/_cctally_cache.py +111 -59
- package/bin/_cctally_core.py +22 -49
- package/bin/_cctally_dashboard.py +239 -152
- package/bin/_cctally_db.py +193 -31
- package/bin/_cctally_milestones.py +126 -166
- package/bin/_cctally_record.py +161 -192
- package/bin/_lib_alert_axes.py +7 -4
- package/bin/_lib_conversation.py +21 -6
- package/bin/_lib_conversation_query.py +145 -49
- package/bin/_lib_jsonl.py +69 -50
- package/bin/cctally +5 -5
- package/dashboard/static/assets/index-BGaWg6ys.js +47 -0
- package/dashboard/static/assets/{index-Bj5ckRUE.css → index-BqQ5xdX0.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-Dw4G5FD9.js +0 -18
|
@@ -29,6 +29,25 @@ def _project_label(cwd) -> str:
|
|
|
29
29
|
return os.path.basename(cwd.rstrip("/")) or cwd
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def _subagent_key(source_path):
|
|
33
|
+
"""Privacy-safe subagent-thread identity for the reader. Each subagent (Task)
|
|
34
|
+
invocation writes its own ``agent-<hash>.jsonl``; the main session is
|
|
35
|
+
``<session_id>.jsonl``. Returns the agent hash (``agent-`` prefix + ``.jsonl``
|
|
36
|
+
suffix stripped; an ``acompact-`` middle is kept), or ``None`` for the main
|
|
37
|
+
file / a non-agent path. We expose ONLY this derived key — never the raw
|
|
38
|
+
absolute ``source_path`` (which leaks home dir / username / encoded project,
|
|
39
|
+
and the conversation routes are LAN-exposable via dashboard.expose_transcripts)."""
|
|
40
|
+
if not source_path:
|
|
41
|
+
return None
|
|
42
|
+
base = os.path.basename(source_path)
|
|
43
|
+
if not base.startswith("agent-"):
|
|
44
|
+
return None
|
|
45
|
+
stem = base[len("agent-"):]
|
|
46
|
+
if stem.endswith(".jsonl"):
|
|
47
|
+
stem = stem[: -len(".jsonl")]
|
|
48
|
+
return stem or None
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
def _entry_cost(model, inp, out, cc, cr, cost_usd_raw) -> float:
|
|
33
52
|
"""Cost for one session_entries row via the shared pricing helper. Tokens →
|
|
34
53
|
the helper's usage dict. cost_usd_raw is passed as the optional override the
|
|
@@ -198,7 +217,7 @@ def get_conversation(conn, session_id, *, after=None, limit=500):
|
|
|
198
217
|
# uuid, so the first occurrence in ascending order is canonical.
|
|
199
218
|
raw = conn.execute(
|
|
200
219
|
"SELECT id, uuid, timestamp_utc, entry_type, text, blocks_json, model, "
|
|
201
|
-
" msg_id, req_id, is_sidechain, cwd, git_branch "
|
|
220
|
+
" msg_id, req_id, is_sidechain, cwd, git_branch, source_path, parent_uuid "
|
|
202
221
|
"FROM conversation_messages WHERE session_id=? "
|
|
203
222
|
"ORDER BY timestamp_utc, id", (session_id,)).fetchall()
|
|
204
223
|
|
|
@@ -224,7 +243,7 @@ def get_conversation(conn, session_id, *, after=None, limit=500):
|
|
|
224
243
|
turn_index = {} # (msg_id, req_id) -> index into items
|
|
225
244
|
for row in logical:
|
|
226
245
|
(rid, u, ts, etype, text, blocks, model, msg_id, req_id,
|
|
227
|
-
is_sc, cwd, branch) = row
|
|
246
|
+
is_sc, cwd, branch, source_path, parent_uuid) = row
|
|
228
247
|
if etype == "assistant" and msg_id is not None:
|
|
229
248
|
key = (msg_id, req_id)
|
|
230
249
|
idx = turn_index.get(key)
|
|
@@ -327,6 +346,12 @@ def _build_turn(members):
|
|
|
327
346
|
"blocks": [],
|
|
328
347
|
"model": first[6],
|
|
329
348
|
"is_sidechain": bool(first[9]),
|
|
349
|
+
# subagent_key / parent_uuid are SEED-sourced (the first fragment, the
|
|
350
|
+
# turn's entry point) and NOT re-promoted in _fold_fragment — the prose
|
|
351
|
+
# anchor's parent_uuid is an intra-turn link, not the entry point (Codex
|
|
352
|
+
# P1). subagent_key is uniform across a turn's fragments (one file).
|
|
353
|
+
"subagent_key": _subagent_key(first[12]),
|
|
354
|
+
"parent_uuid": first[13],
|
|
330
355
|
"_msg_id": first[7],
|
|
331
356
|
"_req_id": first[8],
|
|
332
357
|
"_has_prose": False,
|
|
@@ -374,7 +399,8 @@ def _build_simple(row):
|
|
|
374
399
|
key → no session_entries join); it carries an explicit cost_usd of 0.0 and NO
|
|
375
400
|
internal _msg_id/_req_id keys, so the cost loop's KeyError path can never fire
|
|
376
401
|
(I2). The model is preserved for assistant rows."""
|
|
377
|
-
(rid, u, ts, etype, text, blocks, model, msg_id, req_id, is_sc, cwd, branch
|
|
402
|
+
(rid, u, ts, etype, text, blocks, model, msg_id, req_id, is_sc, cwd, branch,
|
|
403
|
+
source_path, parent_uuid) = row
|
|
378
404
|
try:
|
|
379
405
|
parsed = _json.loads(blocks or "[]")
|
|
380
406
|
except (ValueError, TypeError):
|
|
@@ -387,6 +413,8 @@ def _build_simple(row):
|
|
|
387
413
|
"text": text,
|
|
388
414
|
"blocks": parsed,
|
|
389
415
|
"is_sidechain": bool(is_sc),
|
|
416
|
+
"subagent_key": _subagent_key(source_path),
|
|
417
|
+
"parent_uuid": parent_uuid,
|
|
390
418
|
}
|
|
391
419
|
if etype == "assistant":
|
|
392
420
|
item["model"] = model
|
|
@@ -440,19 +468,6 @@ def _row_to_hit(uuid_, sid, ts, cwd, snippet, msg_id, req_id):
|
|
|
440
468
|
}
|
|
441
469
|
|
|
442
470
|
|
|
443
|
-
def _dedup_hits(hits, limit, offset):
|
|
444
|
-
seen = set()
|
|
445
|
-
out = []
|
|
446
|
-
for h in hits:
|
|
447
|
-
key = (h["session_id"], h["uuid"])
|
|
448
|
-
if key in seen:
|
|
449
|
-
continue
|
|
450
|
-
seen.add(key)
|
|
451
|
-
out.append(h)
|
|
452
|
-
total = len(out)
|
|
453
|
-
return out[offset:offset + limit], total
|
|
454
|
-
|
|
455
|
-
|
|
456
471
|
def _attach_costs(conn, page):
|
|
457
472
|
"""Compute turn cost for the FINAL page's hits in ONE _turn_cost_map call,
|
|
458
473
|
then map it onto each hit and drop the private `_turn_key`. Off-page and
|
|
@@ -465,45 +480,126 @@ def _attach_costs(conn, page):
|
|
|
465
480
|
return page
|
|
466
481
|
|
|
467
482
|
|
|
468
|
-
def
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
483
|
+
def _like_pattern(q):
|
|
484
|
+
"""Build the LIKE pattern for `q`. Escape the ESCAPE char (\\) FIRST, then
|
|
485
|
+
the wildcards — otherwise a query containing a backslash (incl. a trailing
|
|
486
|
+
one) mis-escapes the appended '%' and the LIKE silently matches nothing
|
|
487
|
+
(paired with ESCAPE '\\' in the queries below)."""
|
|
488
|
+
return ("%" + q.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_")
|
|
489
|
+
+ "%")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _fts_snippets(conn, fts_q, ids):
|
|
493
|
+
"""{rowid: snippet} for the page rowids ONLY (#149). snippet() needs an
|
|
494
|
+
active MATCH, so it can't be deferred to an outer query over the page CTE;
|
|
495
|
+
a second bounded MATCH restricted to the page rowids generates snippets for
|
|
496
|
+
at most one page of hits instead of every corpus match."""
|
|
497
|
+
if not ids:
|
|
498
|
+
return {}
|
|
499
|
+
ph = ",".join("?" for _ in ids)
|
|
500
|
+
rows = conn.execute(
|
|
501
|
+
"SELECT cm.id, snippet(conversation_fts, 0, '[', ']', ' … ', 12) "
|
|
473
502
|
"FROM conversation_fts "
|
|
474
503
|
"JOIN conversation_messages cm ON cm.id = conversation_fts.rowid "
|
|
475
|
-
"WHERE conversation_fts MATCH ? "
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
page
|
|
485
|
-
|
|
504
|
+
f"WHERE conversation_fts MATCH ? AND cm.id IN ({ph})",
|
|
505
|
+
(fts_q, *ids),
|
|
506
|
+
).fetchall()
|
|
507
|
+
return {r[0]: r[1] for r in rows}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _texts_for_ids(conn, ids):
|
|
511
|
+
"""{rowid: text} for the page rowids ONLY (#149) — the LIKE page query omits
|
|
512
|
+
`text` so we never pull every matched row's body into Python; this fetches
|
|
513
|
+
it for just the page so `_manual_snippet` runs at most `limit` times."""
|
|
514
|
+
if not ids:
|
|
515
|
+
return {}
|
|
516
|
+
ph = ",".join("?" for _ in ids)
|
|
517
|
+
rows = conn.execute(
|
|
518
|
+
f"SELECT id, text FROM conversation_messages WHERE id IN ({ph})",
|
|
519
|
+
tuple(ids),
|
|
520
|
+
).fetchall()
|
|
521
|
+
return {r[0]: r[1] for r in rows}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _search_fts(conn, q, limit, offset):
|
|
525
|
+
# All of dedup + paging + total live in SQL (#149) so Python never holds
|
|
526
|
+
# more than one page of hits/snippets, regardless of corpus match count.
|
|
527
|
+
fts_q = _fts_query(q)
|
|
528
|
+
# Exact post-dedup logical total — counted in C with no snippet generation
|
|
529
|
+
# and no Python row materialization.
|
|
530
|
+
total = conn.execute(
|
|
531
|
+
"SELECT COUNT(*) FROM ("
|
|
532
|
+
" SELECT DISTINCT cm.session_id, cm.uuid "
|
|
533
|
+
" FROM conversation_fts "
|
|
534
|
+
" JOIN conversation_messages cm ON cm.id = conversation_fts.rowid "
|
|
535
|
+
" WHERE conversation_fts MATCH ?)",
|
|
536
|
+
(fts_q,),
|
|
537
|
+
).fetchone()[0]
|
|
538
|
+
# One row per logical (session_id, uuid): ROW_NUMBER()=1 keeps the SAME row
|
|
539
|
+
# the old Python dedup kept as its FIRST occurrence (order: bm25, ts DESC,
|
|
540
|
+
# id DESC — cm.id is the final deterministic tiebreaker), so the surviving
|
|
541
|
+
# snippet/cost and the page boundary stay byte-stable. bm25 still ranks
|
|
542
|
+
# across all matches (inherent to relevance ordering).
|
|
543
|
+
#
|
|
544
|
+
# bm25 is materialized as a plain `rank` column in the inner `matched` CTE
|
|
545
|
+
# before the window function runs: FTS5 auxiliary functions (bm25/snippet)
|
|
546
|
+
# may only be used directly against the MATCH query, NOT inside a window
|
|
547
|
+
# ORDER BY ("unable to use function bm25 in the requested context").
|
|
548
|
+
page = conn.execute(
|
|
549
|
+
"WITH matched AS ("
|
|
550
|
+
" SELECT cm.id AS rid, cm.session_id AS sid, cm.uuid AS uuid, "
|
|
551
|
+
" cm.timestamp_utc AS ts, cm.cwd AS cwd, "
|
|
552
|
+
" cm.msg_id AS mid, cm.req_id AS rqd, "
|
|
553
|
+
" bm25(conversation_fts) AS rank "
|
|
554
|
+
" FROM conversation_fts "
|
|
555
|
+
" JOIN conversation_messages cm ON cm.id = conversation_fts.rowid "
|
|
556
|
+
" WHERE conversation_fts MATCH ?), "
|
|
557
|
+
"ranked AS ("
|
|
558
|
+
" SELECT *, ROW_NUMBER() OVER ("
|
|
559
|
+
" PARTITION BY sid, uuid ORDER BY rank, ts DESC, rid DESC"
|
|
560
|
+
" ) AS rn "
|
|
561
|
+
" FROM matched) "
|
|
562
|
+
"SELECT rid, sid, uuid, ts, cwd, mid, rqd FROM ranked WHERE rn = 1 "
|
|
563
|
+
"ORDER BY rank, ts DESC, rid DESC LIMIT ? OFFSET ?",
|
|
564
|
+
(fts_q, limit, offset),
|
|
565
|
+
).fetchall()
|
|
566
|
+
snips = _fts_snippets(conn, fts_q, [r[0] for r in page])
|
|
567
|
+
hits = [_row_to_hit(uuid, sid, ts, cwd, snips.get(rid, ""), mid, rqd)
|
|
568
|
+
for (rid, sid, uuid, ts, cwd, mid, rqd) in page]
|
|
569
|
+
return {"query": q, "mode": "fts", "hits": _attach_costs(conn, hits),
|
|
486
570
|
"total": total}
|
|
487
571
|
|
|
488
572
|
|
|
489
573
|
def _search_like(conn, q, limit, offset):
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
like = (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"SELECT session_id, uuid
|
|
497
|
-
"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
574
|
+
# SQL-bounded mirror of _search_fts for the no-FTS5 fallback (#149); the
|
|
575
|
+
# COUNT + page each scan the table once (the degraded path already lacks an
|
|
576
|
+
# index for the substring match).
|
|
577
|
+
like = _like_pattern(q)
|
|
578
|
+
total = conn.execute(
|
|
579
|
+
"SELECT COUNT(*) FROM ("
|
|
580
|
+
" SELECT DISTINCT session_id, uuid FROM conversation_messages "
|
|
581
|
+
" WHERE text LIKE ? ESCAPE '\\' AND text != '')",
|
|
582
|
+
(like,),
|
|
583
|
+
).fetchone()[0]
|
|
584
|
+
page = conn.execute(
|
|
585
|
+
"WITH ranked AS ("
|
|
586
|
+
" SELECT id AS rid, session_id AS sid, uuid AS uuid, "
|
|
587
|
+
" timestamp_utc AS ts, cwd AS cwd, msg_id AS mid, req_id AS rqd, "
|
|
588
|
+
" ROW_NUMBER() OVER ("
|
|
589
|
+
" PARTITION BY session_id, uuid "
|
|
590
|
+
" ORDER BY timestamp_utc DESC, id DESC"
|
|
591
|
+
" ) AS rn "
|
|
592
|
+
" FROM conversation_messages "
|
|
593
|
+
" WHERE text LIKE ? ESCAPE '\\' AND text != '') "
|
|
594
|
+
"SELECT rid, sid, uuid, ts, cwd, mid, rqd FROM ranked WHERE rn = 1 "
|
|
595
|
+
"ORDER BY ts DESC, rid DESC LIMIT ? OFFSET ?",
|
|
596
|
+
(like, limit, offset),
|
|
597
|
+
).fetchall()
|
|
598
|
+
texts = _texts_for_ids(conn, [r[0] for r in page])
|
|
599
|
+
hits = [_row_to_hit(uuid, sid, ts, cwd,
|
|
600
|
+
_manual_snippet(texts.get(rid, ""), q), mid, rqd)
|
|
601
|
+
for (rid, sid, uuid, ts, cwd, mid, rqd) in page]
|
|
602
|
+
return {"query": q, "mode": "like", "hits": _attach_costs(conn, hits),
|
|
507
603
|
"total": total}
|
|
508
604
|
|
|
509
605
|
|
package/bin/_lib_jsonl.py
CHANGED
|
@@ -201,6 +201,68 @@ def _parse_usage_entries(
|
|
|
201
201
|
return no_key_entries
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
def parse_cost_entry(obj, path_str: str):
|
|
205
|
+
"""Pure per-line cost parser: given a parsed JSONL object, return
|
|
206
|
+
``(UsageEntry, msg_id, req_id)`` when it is a billable assistant entry, or
|
|
207
|
+
``None`` otherwise (non-assistant, missing/invalid usage, model, or
|
|
208
|
+
timestamp, or a ``<synthetic>`` placeholder). No I/O, no byte offset — the
|
|
209
|
+
caller owns the readline()+tell() loop.
|
|
210
|
+
|
|
211
|
+
Extracted (#138) so the streaming ``_iter_jsonl_entries_with_offsets`` reader
|
|
212
|
+
and the fused single-pass sync walker (``_cctally_cache._iter_sync_entries``)
|
|
213
|
+
share ONE gating implementation — each JSONL line is ``json.loads``-parsed
|
|
214
|
+
once and classified once, never re-parsed for a separate second walk.
|
|
215
|
+
"""
|
|
216
|
+
if obj.get("type") != "assistant":
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
ts_raw = obj.get("timestamp")
|
|
220
|
+
if not isinstance(ts_raw, str) or not ts_raw.strip():
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
msg = obj.get("message")
|
|
224
|
+
if not isinstance(msg, dict):
|
|
225
|
+
msg = obj
|
|
226
|
+
|
|
227
|
+
usage = msg.get("usage")
|
|
228
|
+
if not isinstance(usage, dict):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
model = msg.get("model") or obj.get("model")
|
|
232
|
+
if not isinstance(model, str) or not model.strip():
|
|
233
|
+
return None
|
|
234
|
+
model = model.strip()
|
|
235
|
+
if model == "<synthetic>":
|
|
236
|
+
# Matches ccusage's claude_loader.rs:454. Filtered here so the cache
|
|
237
|
+
# ingest path can't accidentally store these rows even if a downstream
|
|
238
|
+
# loop forgets to double-check (see `sync_cache` in _cctally_cache.py).
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
ts = dt.datetime.fromisoformat(ts_raw.strip().replace("Z", "+00:00"))
|
|
243
|
+
if ts.tzinfo is None:
|
|
244
|
+
ts = ts.replace(tzinfo=dt.timezone.utc)
|
|
245
|
+
except ValueError:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
msg_id = msg.get("id")
|
|
249
|
+
req_id = obj.get("requestId")
|
|
250
|
+
cost_usd_raw = obj.get("costUSD")
|
|
251
|
+
cost_usd = float(cost_usd_raw) if cost_usd_raw is not None else None
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
UsageEntry(
|
|
255
|
+
timestamp=ts,
|
|
256
|
+
model=model,
|
|
257
|
+
usage=usage,
|
|
258
|
+
cost_usd=cost_usd,
|
|
259
|
+
source_path=path_str,
|
|
260
|
+
),
|
|
261
|
+
msg_id,
|
|
262
|
+
req_id,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
204
266
|
def _iter_jsonl_entries_with_offsets(fh, path_str: str):
|
|
205
267
|
"""Yield (byte_offset, UsageEntry, msg_id, req_id) for each assistant
|
|
206
268
|
entry starting from fh's current position.
|
|
@@ -209,7 +271,9 @@ def _iter_jsonl_entries_with_offsets(fh, path_str: str):
|
|
|
209
271
|
accurate for resume-from-offset after partial ingests. Malformed JSON
|
|
210
272
|
and non-assistant lines are skipped, but the offset still advances past
|
|
211
273
|
them so they are never re-read. Range filtering is intentionally NOT
|
|
212
|
-
done here — filters are applied at query time by iter_entries().
|
|
274
|
+
done here — filters are applied at query time by iter_entries(). The
|
|
275
|
+
per-line gating lives in ``parse_cost_entry`` (shared with the fused
|
|
276
|
+
single-pass sync walker, #138).
|
|
213
277
|
"""
|
|
214
278
|
while True:
|
|
215
279
|
offset = fh.tell()
|
|
@@ -230,56 +294,11 @@ def _iter_jsonl_entries_with_offsets(fh, path_str: str):
|
|
|
230
294
|
obj = json.loads(stripped)
|
|
231
295
|
except json.JSONDecodeError:
|
|
232
296
|
continue
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ts_raw = obj.get("timestamp")
|
|
237
|
-
if not isinstance(ts_raw, str) or not ts_raw.strip():
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
|
-
msg = obj.get("message")
|
|
241
|
-
if not isinstance(msg, dict):
|
|
242
|
-
msg = obj
|
|
243
|
-
|
|
244
|
-
usage = msg.get("usage")
|
|
245
|
-
if not isinstance(usage, dict):
|
|
246
|
-
continue
|
|
247
|
-
|
|
248
|
-
model = msg.get("model") or obj.get("model")
|
|
249
|
-
if not isinstance(model, str) or not model.strip():
|
|
297
|
+
parsed = parse_cost_entry(obj, path_str)
|
|
298
|
+
if parsed is None:
|
|
250
299
|
continue
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
# Matches ccusage's claude_loader.rs:454. Filtered at the
|
|
254
|
-
# iterator level so the cache ingest path can't accidentally
|
|
255
|
-
# store these rows even if a downstream loop forgets to
|
|
256
|
-
# double-check (see `sync_cache` in _cctally_cache.py).
|
|
257
|
-
continue
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
ts = dt.datetime.fromisoformat(ts_raw.strip().replace("Z", "+00:00"))
|
|
261
|
-
if ts.tzinfo is None:
|
|
262
|
-
ts = ts.replace(tzinfo=dt.timezone.utc)
|
|
263
|
-
except ValueError:
|
|
264
|
-
continue
|
|
265
|
-
|
|
266
|
-
msg_id = msg.get("id")
|
|
267
|
-
req_id = obj.get("requestId")
|
|
268
|
-
cost_usd_raw = obj.get("costUSD")
|
|
269
|
-
cost_usd = float(cost_usd_raw) if cost_usd_raw is not None else None
|
|
270
|
-
|
|
271
|
-
yield (
|
|
272
|
-
offset,
|
|
273
|
-
UsageEntry(
|
|
274
|
-
timestamp=ts,
|
|
275
|
-
model=model,
|
|
276
|
-
usage=usage,
|
|
277
|
-
cost_usd=cost_usd,
|
|
278
|
-
source_path=path_str,
|
|
279
|
-
),
|
|
280
|
-
msg_id,
|
|
281
|
-
req_id,
|
|
282
|
-
)
|
|
300
|
+
entry, msg_id, req_id = parsed
|
|
301
|
+
yield (offset, entry, msg_id, req_id)
|
|
283
302
|
|
|
284
303
|
|
|
285
304
|
_CODEX_FILENAME_UUID_RE = re.compile(
|
package/bin/cctally
CHANGED
|
@@ -2100,18 +2100,18 @@ get_max_milestone_for_week = _cctally_milestones.get_max_milestone_for_
|
|
|
2100
2100
|
get_milestone_cost_for_week = _cctally_milestones.get_milestone_cost_for_week # record shim
|
|
2101
2101
|
get_milestones_for_week = _cctally_milestones.get_milestones_for_week # forecast c.; tui shim; percent-breakdown c.
|
|
2102
2102
|
insert_percent_milestone = _cctally_milestones.insert_percent_milestone # record shim; idempotency-test mod.
|
|
2103
|
-
insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim
|
|
2103
|
+
insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim; test_budget_alerts / test_project_budget_dashboard ns[] (+ test_codex_budget_alerts / test_projected_alerts post-#143 vendor-param unification)
|
|
2104
2104
|
insert_project_budget_milestone = _cctally_milestones.insert_project_budget_milestone # record shim; project-budget-config-test ns[]
|
|
2105
|
-
|
|
2106
|
-
_codex_budget_crossings = _cctally_milestones._codex_budget_crossings # record shim (shared INSERT-and-arm core for the codex_budget axis)
|
|
2105
|
+
_budget_crossings = _cctally_milestones._budget_crossings # record shim (shared INSERT-and-arm core for the budget axis, both vendors, #143)
|
|
2107
2106
|
_resolve_codex_budget_period_window = _cctally_milestones._resolve_codex_budget_period_window # record shim; milestones c. (codex period window)
|
|
2108
|
-
|
|
2107
|
+
_resolve_budget_window = _cctally_milestones._resolve_budget_window # record shim; milestones c. (per-vendor cheap budget window dispatcher, #143)
|
|
2108
|
+
_budget_spend_for_vendor = _cctally_milestones._budget_spend_for_vendor # record shim; milestones c. (per-vendor budget spend dispatcher, #143)
|
|
2109
2109
|
_reconcile_codex_budget_on_config_write = _cctally_milestones._reconcile_codex_budget_on_config_write # forecast/config c. (forward-only codex-budget reconcile)
|
|
2110
2110
|
_resolve_claude_budget_window = _cctally_milestones._resolve_claude_budget_window # record shim; milestones c. (period-aware Claude budget window)
|
|
2111
2111
|
_project_crossings = _cctally_milestones._project_crossings # record shim; milestones c. (#130 firing/reconcile shared crossing arithmetic)
|
|
2112
2112
|
insert_projected_milestone = _cctally_milestones.insert_projected_milestone # record shim
|
|
2113
2113
|
_projected_levels_already_latched = _cctally_milestones._projected_levels_already_latched # record shim
|
|
2114
|
-
_reconcile_budget_milestones_on_set = _cctally_milestones._reconcile_budget_milestones_on_set # test_budget_alerts ns[]
|
|
2114
|
+
_reconcile_budget_milestones_on_set = _cctally_milestones._reconcile_budget_milestones_on_set # test_budget_alerts / test_codex_budget_alerts ns[] (vendor-param, #143)
|
|
2115
2115
|
_reconcile_budget_on_config_write = _cctally_milestones._reconcile_budget_on_config_write # forecast/config/dashboard c.; test_forecast_ns_patch mod. patch
|
|
2116
2116
|
_reconcile_project_budget_milestones_on_write = _cctally_milestones._reconcile_project_budget_milestones_on_write # forecast/config/dashboard c. (forward-only project-budget reconcile)
|
|
2117
2117
|
|