cctally 1.37.0 → 1.37.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 +5 -0
- package/bin/_lib_conversation.py +48 -24
- package/bin/_lib_conversation_query.py +37 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.37.1] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Conversation viewer: the `TaskCreate`/`TaskUpdate`/`TaskList` checklist card rendered an empty "0 / 0" for tasks created inside subagents. Subagent Task tools record their result as a plain string (`Task #N created successfully: …`) rather than the structured shape main-session tools use, so the reader couldn't recover the task ids; it now parses both shapes. The running checklist is also reconstructed per-subagent, so parallel subagents no longer bleed their tasks into one another's cards, and a Task run whose results can't be parsed now falls back to plain tool chips instead of a misleading empty card. Existing transcripts pick this up on the next `cache-sync --rebuild`.
|
|
12
|
+
|
|
8
13
|
## [1.37.0] - 2026-06-11
|
|
9
14
|
|
|
10
15
|
### Added
|
package/bin/_lib_conversation.py
CHANGED
|
@@ -8,6 +8,7 @@ mid-write tail line. Spec §1, §2.
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
import json
|
|
11
|
+
import re
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
|
|
13
14
|
HUMAN = "human"
|
|
@@ -288,45 +289,68 @@ def _attach_ask_answers(blocks, obj):
|
|
|
288
289
|
results[0]["ask_annotations"] = bounded_anno
|
|
289
290
|
|
|
290
291
|
|
|
292
|
+
# Subagent Task tools record toolUseResult=null and put the identity in the
|
|
293
|
+
# human-readable result text instead; the id is the only thing the fold needs
|
|
294
|
+
# from the result (subject/status come from the call input). Anchored to the
|
|
295
|
+
# line start so unrelated output mentioning a task id mid-sentence never matches.
|
|
296
|
+
# Shapes verified against real subagent transcripts (Claude Code 2.1.173).
|
|
297
|
+
_TASK_CREATE_RESULT_RE = re.compile(r"^Task #(\d+) created\b")
|
|
298
|
+
_TASK_UPDATE_RESULT_RE = re.compile(r"^Updated task #(\d+)\b")
|
|
299
|
+
|
|
300
|
+
|
|
291
301
|
def _attach_task_meta(blocks, obj):
|
|
292
302
|
"""Stash a Task* tool's record-level identity onto its single tool_result
|
|
293
|
-
block so the query-kernel fold has a robust id
|
|
294
|
-
|
|
295
|
-
|
|
303
|
+
block so the query-kernel fold has a robust id. Task ids are monotonic +
|
|
304
|
+
never reused, so the explicit id is the only stable fold key. Two result
|
|
305
|
+
shapes, both self-identifying off the single tool_result block:
|
|
306
|
+
|
|
307
|
+
Structured (MAIN-session Task tools) — toolUseResult carries the identity:
|
|
296
308
|
TaskCreate -> {"task": {"id": ...}} -> block["task_id"]
|
|
297
309
|
TaskUpdate -> {"taskId": ...} -> block["task_id"]
|
|
298
310
|
TaskList -> {"tasks": [{id,subject,status}]} -> block["task_list"]
|
|
311
|
+
|
|
312
|
+
String-content (SUBAGENT Task tools) — toolUseResult is null and the id
|
|
313
|
+
lives in the result text ("Task #7 created successfully: ..." / "Updated
|
|
314
|
+
task #3 status"); we recover the id from block["text"]. Subagent-driven
|
|
315
|
+
workflows make this the dominant shape, so missing it left every subagent
|
|
316
|
+
Task run rendering as an empty "0 / 0" card.
|
|
317
|
+
|
|
299
318
|
Same exactly-one-result-block guard as _attach_subagent_result. Subjects
|
|
300
319
|
bounded through _bound_input.
|
|
301
320
|
|
|
302
321
|
The ``task.id`` (not ``task.task_id``) gate deliberately ignores the
|
|
303
322
|
look-alike local_bash spawn result {"task": {"task_id": ..., ...}}, which is
|
|
304
323
|
a different tool family and carries no checklist id."""
|
|
305
|
-
tur = obj.get("toolUseResult")
|
|
306
|
-
if not isinstance(tur, dict):
|
|
307
|
-
return
|
|
308
324
|
results = [b for b in blocks if b.get("kind") == "tool_result"]
|
|
309
325
|
if len(results) != 1:
|
|
310
326
|
return
|
|
311
327
|
block = results[0]
|
|
312
|
-
|
|
313
|
-
if isinstance(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
tur = obj.get("toolUseResult")
|
|
329
|
+
if isinstance(tur, dict):
|
|
330
|
+
task = tur.get("task")
|
|
331
|
+
if isinstance(task, dict) and task.get("id") is not None:
|
|
332
|
+
block["task_id"] = str(task["id"])
|
|
333
|
+
return
|
|
334
|
+
if tur.get("taskId") is not None:
|
|
335
|
+
block["task_id"] = str(tur["taskId"])
|
|
336
|
+
return
|
|
337
|
+
tasks = tur.get("tasks")
|
|
338
|
+
if isinstance(tasks, list):
|
|
339
|
+
snap = []
|
|
340
|
+
for t in tasks:
|
|
341
|
+
if not isinstance(t, dict) or t.get("id") is None:
|
|
342
|
+
continue
|
|
343
|
+
bounded, _ = _bound_input({"subject": t.get("subject") or ""})
|
|
344
|
+
snap.append({"id": str(t["id"]),
|
|
345
|
+
"subject": bounded.get("subject", ""),
|
|
346
|
+
"status": t.get("status") or "pending"})
|
|
347
|
+
block["task_list"] = snap
|
|
348
|
+
return
|
|
349
|
+
# String-content fallback (subagent Task tools): no structured identity.
|
|
350
|
+
m = (_TASK_CREATE_RESULT_RE.match(block.get("text") or "")
|
|
351
|
+
or _TASK_UPDATE_RESULT_RE.match(block.get("text") or ""))
|
|
352
|
+
if m:
|
|
353
|
+
block["task_id"] = m.group(1)
|
|
330
354
|
|
|
331
355
|
|
|
332
356
|
def _stringify(c):
|
|
@@ -711,24 +711,37 @@ _TASK_TRIO = ("TaskCreate", "TaskUpdate", "TaskList")
|
|
|
711
711
|
def _fold_task_runs(items, task_link):
|
|
712
712
|
"""Reconstruct the running to-do list from the chronological Task* op stream
|
|
713
713
|
and stamp the resulting todos[] snapshot onto the FIRST tool_call of each
|
|
714
|
-
Task* run.
|
|
715
|
-
|
|
714
|
+
Task* run. Key on the explicit task id (never reused). `deleted` drops a
|
|
715
|
+
task; a TaskList result reseeds the whole snapshot.
|
|
716
|
+
|
|
717
|
+
Scoped PER subagent thread (``subagent_key``): the main session (key None)
|
|
718
|
+
and each subagent keep INDEPENDENT running checklists, so parallel subagents
|
|
719
|
+
with disjoint task-id ranges never bleed into one another's cards. Within a
|
|
720
|
+
thread, state still spans the whole session.
|
|
721
|
+
|
|
722
|
+
Degradation guard: a thread's run is stamped only once that thread has
|
|
723
|
+
recognized a real create/list (``seen``). A Task* run with no recognizable
|
|
724
|
+
create — a future result shape we don't parse, or pre-fix legacy rows —
|
|
725
|
+
leaves ``task_snapshot`` ABSENT, so the frontend falls back to generic chips
|
|
726
|
+
instead of a misleading empty "0 / 0" card.
|
|
716
727
|
|
|
717
728
|
Mirrors the ask_answers join: the parser stashed the record-level identity
|
|
718
729
|
onto the tool_result block, the Phase-1 sweep popped it into ``task_link``
|
|
719
730
|
keyed by tool_use_id, and this fold joins it back. The frontend stays a pure
|
|
720
731
|
todos[] renderer — all running-list state lives here."""
|
|
721
|
-
|
|
722
|
-
state = {}
|
|
732
|
+
threads = {} # subagent_key -> {"order": [...], "state": {...}, "seen": bool}
|
|
723
733
|
|
|
724
|
-
def snapshot():
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
734
|
+
def snapshot(th):
|
|
735
|
+
st, order = th["state"], th["order"]
|
|
736
|
+
return [dict(content=st[i]["content"], status=st[i]["status"],
|
|
737
|
+
**({"activeForm": st[i]["activeForm"]} if st[i].get("activeForm") else {}))
|
|
738
|
+
for i in order if i in st]
|
|
728
739
|
|
|
729
740
|
for it in items:
|
|
730
741
|
if it.get("kind") != "assistant":
|
|
731
742
|
continue
|
|
743
|
+
th = threads.setdefault(it.get("subagent_key"),
|
|
744
|
+
{"order": [], "state": {}, "seen": False})
|
|
732
745
|
first_task_call = None
|
|
733
746
|
for b in it["blocks"]:
|
|
734
747
|
if b.get("kind") != "tool_call" or b.get("name") not in _TASK_TRIO:
|
|
@@ -741,30 +754,32 @@ def _fold_task_runs(items, task_link):
|
|
|
741
754
|
if name == "TaskCreate":
|
|
742
755
|
tid = link.get("task_id")
|
|
743
756
|
if tid is not None:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
757
|
+
th["seen"] = True
|
|
758
|
+
if tid not in th["state"]:
|
|
759
|
+
th["order"].append(tid)
|
|
760
|
+
th["state"][tid] = {"content": inp.get("subject") or "", "status": "pending",
|
|
761
|
+
"activeForm": inp.get("activeForm") or ""}
|
|
748
762
|
elif name == "TaskUpdate":
|
|
749
763
|
tid = str(inp.get("taskId")) if inp.get("taskId") is not None else link.get("task_id")
|
|
750
764
|
status = inp.get("status")
|
|
751
765
|
if tid is not None:
|
|
752
766
|
if status == "deleted":
|
|
753
|
-
state.pop(tid, None)
|
|
754
|
-
elif tid in state and status:
|
|
755
|
-
state[tid]["status"] = status
|
|
767
|
+
th["state"].pop(tid, None)
|
|
768
|
+
elif tid in th["state"] and status:
|
|
769
|
+
th["state"][tid]["status"] = status
|
|
756
770
|
elif name == "TaskList":
|
|
757
771
|
snap = link.get("task_list")
|
|
758
772
|
if snap is not None:
|
|
759
|
-
|
|
760
|
-
|
|
773
|
+
th["seen"] = True
|
|
774
|
+
th["order"] = []
|
|
775
|
+
th["state"] = {}
|
|
761
776
|
for t in snap:
|
|
762
777
|
tid = t["id"]
|
|
763
|
-
order.append(tid)
|
|
764
|
-
state[tid] = {"content": t.get("subject") or "",
|
|
765
|
-
|
|
766
|
-
if first_task_call is not None:
|
|
767
|
-
first_task_call["task_snapshot"] = snapshot()
|
|
778
|
+
th["order"].append(tid)
|
|
779
|
+
th["state"][tid] = {"content": t.get("subject") or "",
|
|
780
|
+
"status": t.get("status") or "pending", "activeForm": ""}
|
|
781
|
+
if first_task_call is not None and th["seen"]:
|
|
782
|
+
first_task_call["task_snapshot"] = snapshot(th)
|
|
768
783
|
|
|
769
784
|
|
|
770
785
|
def _latest(logical, col):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.37.
|
|
3
|
+
"version": "1.37.1",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|