cctally 1.100.0 → 1.102.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 +73 -0
- package/README.md +8 -2
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +1140 -282
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +53 -61
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +329 -111
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +151 -71
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +8 -1
- package/bin/_cctally_tui.py +425 -234
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_blocks.py +12 -0
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +62 -4
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_readme_refresh.py +26 -5
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/cctally +103 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
|
@@ -43,7 +43,7 @@ from _lib_conversation import _MARKER_TAGS, _MARKER_RE, _is_system_marker
|
|
|
43
43
|
# present a legacy/ingested command-marker row carrying a real <command-args>
|
|
44
44
|
# prompt as a "You" turn (text=args, command_name badge). Re-exported for the
|
|
45
45
|
# back-compat import surface + the consumer in _cctally_cache.py.
|
|
46
|
-
from _lib_conversation import _extract_command_invocation
|
|
46
|
+
from _lib_conversation import _extract_command_invocation, _join_text_blocks
|
|
47
47
|
# #186: read-time ANSI strip for rows already indexed with raw SGR (no forced
|
|
48
48
|
# re-ingest). Scoped to prose/thinking/title/label — NEVER tool_result (Bash
|
|
49
49
|
# AnsiText boundary). Shares the parser's regex so ingest and read-time agree.
|
|
@@ -160,16 +160,6 @@ def _skill_name_from_preamble(first_line) -> "str | None":
|
|
|
160
160
|
return os.path.basename(path) or None if path else None
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def _join_text_blocks(blocks) -> str:
|
|
164
|
-
"""Rejoin a row's text-block bodies the way the parser's _blocks_and_text did
|
|
165
|
-
('\\n'-joined). A true meta row carries text='' (parser) with the body here in
|
|
166
|
-
blocks; a not-yet-reingested human row carries the body in its text column —
|
|
167
|
-
_meta_classify reads whichever is populated."""
|
|
168
|
-
if not blocks:
|
|
169
|
-
return ""
|
|
170
|
-
return "\n".join(b.get("text", "") or "" for b in blocks if b.get("kind") == "text")
|
|
171
|
-
|
|
172
|
-
|
|
173
163
|
def _reingest_pending(conn) -> bool:
|
|
174
164
|
"""True iff migration 005's ``conversation_reingest_pending`` flag is still
|
|
175
165
|
set — i.e. existing history has NOT yet been re-ingested under the meta-aware
|
|
@@ -767,8 +757,108 @@ def _stamp_cache_failures(items):
|
|
|
767
757
|
}
|
|
768
758
|
|
|
769
759
|
|
|
760
|
+
def parse_blocks_json(blocks_json):
|
|
761
|
+
"""A normalized blocks list, or `[]` for anything that is not one.
|
|
762
|
+
|
|
763
|
+
Ingest writes a JSON array of block objects, but nothing in the schema
|
|
764
|
+
enforces the element shape. The shared text join skips malformed elements;
|
|
765
|
+
stricter semantic consumers validate separately. A bare string can still
|
|
766
|
+
make command-invocation normalization raise `AttributeError`, which the
|
|
767
|
+
diagnosis contains as `calculation_failed` (#620 S3 R1).
|
|
768
|
+
"""
|
|
769
|
+
try:
|
|
770
|
+
parsed = _json.loads(blocks_json or "[]")
|
|
771
|
+
except (ValueError, TypeError):
|
|
772
|
+
return []
|
|
773
|
+
return parsed if isinstance(parsed, list) else []
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def is_compaction_blocks(text, parsed_blocks):
|
|
777
|
+
"""The ONE read-time compaction predicate, over parsed blocks.
|
|
778
|
+
|
|
779
|
+
Compaction leaves exactly one retained signal and it is not the flag:
|
|
780
|
+
`isCompactSummary` is consumed at ingest, sets `entry_type = META` and
|
|
781
|
+
blanks `text`, and is never persisted. What survives is the summary body
|
|
782
|
+
inside `blocks_json`, so detection is sentinel inference over the body
|
|
783
|
+
reconstructed from it — mirroring the assembly meta-classify, which
|
|
784
|
+
requires an all-text body that reads as a compaction summary and is not a
|
|
785
|
+
slash-command invocation.
|
|
786
|
+
|
|
787
|
+
The all-text guard is evaluated FIRST and rejects a malformed element,
|
|
788
|
+
which is what keeps a bare string in the array from raising through
|
|
789
|
+
`_join_text_blocks`.
|
|
790
|
+
"""
|
|
791
|
+
blocks = parsed_blocks or []
|
|
792
|
+
if not all(isinstance(b, dict) and b.get("kind") == "text" for b in blocks):
|
|
793
|
+
return False
|
|
794
|
+
body = text or _join_text_blocks(blocks)
|
|
795
|
+
return bool(_is_compaction_body(body)
|
|
796
|
+
and _extract_command_invocation(blocks, body) is None)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def is_compaction_row(text, blocks_json):
|
|
800
|
+
"""`is_compaction_blocks` over a raw `blocks_json` column value."""
|
|
801
|
+
return is_compaction_blocks(text, parse_blocks_json(blocks_json))
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _cache_failure_rows(conn, session_id):
|
|
805
|
+
"""The narrow physical read the cache-churn stream is built from.
|
|
806
|
+
|
|
807
|
+
Enough to dedup, group turns, derive the subagent_key, detect compaction
|
|
808
|
+
and join the token row. No ``blocks_json`` body parse beyond the all-text
|
|
809
|
+
compaction guard. Returns None for an unknown session.
|
|
810
|
+
"""
|
|
811
|
+
exists = conn.execute(
|
|
812
|
+
"SELECT 1 FROM conversation_messages WHERE session_id=? LIMIT 1",
|
|
813
|
+
(session_id,)).fetchone()
|
|
814
|
+
if exists is None:
|
|
815
|
+
return None
|
|
816
|
+
return conn.execute(
|
|
817
|
+
"SELECT id, uuid, entry_type, text, blocks_json, model, "
|
|
818
|
+
" msg_id, req_id, source_path "
|
|
819
|
+
"FROM conversation_messages WHERE session_id=? "
|
|
820
|
+
"ORDER BY timestamp_utc, id", (session_id,)).fetchall()
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _cache_failure_turn_keys(rows):
|
|
824
|
+
"""The deduped assistant turn keys this stream will need tokens for.
|
|
825
|
+
|
|
826
|
+
The same list ``fold_claude_cache_failure_events`` builds internally, in
|
|
827
|
+
the same first-occurrence order, derived here so the wrapper can prefetch
|
|
828
|
+
the usage map.
|
|
829
|
+
"""
|
|
830
|
+
seen_uuid = set()
|
|
831
|
+
seen_keys = set()
|
|
832
|
+
keys = []
|
|
833
|
+
for row in rows:
|
|
834
|
+
u = row[1]
|
|
835
|
+
if u in seen_uuid:
|
|
836
|
+
continue
|
|
837
|
+
seen_uuid.add(u)
|
|
838
|
+
if row[2] == "assistant" and row[6] is not None:
|
|
839
|
+
key = (row[6], row[7])
|
|
840
|
+
if key not in seen_keys:
|
|
841
|
+
seen_keys.add(key)
|
|
842
|
+
keys.append(key)
|
|
843
|
+
return keys
|
|
844
|
+
|
|
845
|
+
|
|
770
846
|
def _lightweight_rebuild_events(conn, session_id):
|
|
771
|
-
"""
|
|
847
|
+
"""The document-ordered ``_CFEvent`` stream for one session (U1, #217 S1).
|
|
848
|
+
|
|
849
|
+
Since #620 S3 this is a thin wrapper over the pure
|
|
850
|
+
``fold_claude_cache_failure_events``; it performs the two reads and
|
|
851
|
+
delegates. Its signature, return shape and every caller are unchanged.
|
|
852
|
+
"""
|
|
853
|
+
rows = _cache_failure_rows(conn, session_id)
|
|
854
|
+
if rows is None:
|
|
855
|
+
return []
|
|
856
|
+
return fold_claude_cache_failure_events(
|
|
857
|
+
rows, _turn_usage_map(conn, _cache_failure_turn_keys(rows)))
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def fold_claude_cache_failure_events(rows, usage_map, *, with_sources=False):
|
|
861
|
+
"""Build the document-ordered ``_CFEvent`` stream WITHOUT a
|
|
772
862
|
full ``_assemble_session`` (U1, #217 S1). Skips the expensive assembly work
|
|
773
863
|
(no ``blocks_json`` body parse, no tool-result folding, no meta-classify /
|
|
774
864
|
ANSI strip, no subagent correlation) but faithfully reproduces the canonical
|
|
@@ -794,19 +884,6 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
794
884
|
fragment, and each meta/human row at its own position). That preserves the
|
|
795
885
|
running-max walk the predicate requires.
|
|
796
886
|
"""
|
|
797
|
-
exists = conn.execute(
|
|
798
|
-
"SELECT 1 FROM conversation_messages WHERE session_id=? LIMIT 1",
|
|
799
|
-
(session_id,)).fetchone()
|
|
800
|
-
if exists is None:
|
|
801
|
-
return []
|
|
802
|
-
# Narrow column set: enough to dedup, group turns, derive the subagent_key,
|
|
803
|
-
# detect compaction, and join the token row. No blocks_json body parse beyond
|
|
804
|
-
# the all-text compaction guard (which needs text OR the text-block join).
|
|
805
|
-
rows = conn.execute(
|
|
806
|
-
"SELECT id, uuid, entry_type, text, blocks_json, model, "
|
|
807
|
-
" msg_id, req_id, source_path "
|
|
808
|
-
"FROM conversation_messages WHERE session_id=? "
|
|
809
|
-
"ORDER BY timestamp_utc, id", (session_id,)).fetchall()
|
|
810
887
|
seen_uuid = set()
|
|
811
888
|
logical = []
|
|
812
889
|
for row in rows:
|
|
@@ -824,7 +901,6 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
824
901
|
sources = [] # parallel: the turn key per assistant event
|
|
825
902
|
turn_index = {} # (msg_id, req_id) -> index into events
|
|
826
903
|
turn_has_prose = {} # (msg_id, req_id) -> bool (model promoted yet?)
|
|
827
|
-
turn_keys = [] # ordered list of grouped turn keys (for the token map)
|
|
828
904
|
for (rid, u, etype, text, blocks, model, msg_id, req_id,
|
|
829
905
|
source_path) in logical:
|
|
830
906
|
if etype == "assistant" and msg_id is not None:
|
|
@@ -834,7 +910,6 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
834
910
|
if ev_idx is None:
|
|
835
911
|
ev_idx = len(events)
|
|
836
912
|
turn_index[key] = ev_idx
|
|
837
|
-
turn_keys.append(key)
|
|
838
913
|
ev = _CFEvent(key=(_subagent_key(source_path), model),
|
|
839
914
|
model=model)
|
|
840
915
|
events.append(ev)
|
|
@@ -861,15 +936,7 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
861
936
|
# BEFORE meta-classify, so a body carrying <command-args> never reads
|
|
862
937
|
# as compaction; compaction bodies have no <command-args>, so in
|
|
863
938
|
# practice the invocation check is a faithful belt-and-suspenders.
|
|
864
|
-
|
|
865
|
-
parsed_blocks = _json.loads(blocks or "[]")
|
|
866
|
-
except (ValueError, TypeError):
|
|
867
|
-
parsed_blocks = []
|
|
868
|
-
body = text or _join_text_blocks(parsed_blocks)
|
|
869
|
-
all_text = all(b.get("kind") == "text"
|
|
870
|
-
for b in (parsed_blocks or []))
|
|
871
|
-
if (all_text and _is_compaction_body(body)
|
|
872
|
-
and _extract_command_invocation(parsed_blocks, body) is None):
|
|
939
|
+
if is_compaction_row(text, blocks):
|
|
873
940
|
events.append(_CFEvent(compaction=True))
|
|
874
941
|
sources.append(None)
|
|
875
942
|
# tool_result rows and null-msg_id assistant rows carry no token event.
|
|
@@ -879,12 +946,14 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
879
946
|
# turn key absent from session_entries carries NO tokens, so its event is
|
|
880
947
|
# dropped (neither flags nor moves the running-max) — parity with
|
|
881
948
|
# _cache_failure_events_from_items's `tokens` skip.
|
|
882
|
-
usage =
|
|
949
|
+
usage = usage_map
|
|
883
950
|
filtered = []
|
|
951
|
+
filtered_sources = [] # parallel: the turn key per emitted event
|
|
884
952
|
si = 0
|
|
885
953
|
for ev in events:
|
|
886
954
|
if ev.compaction:
|
|
887
955
|
filtered.append(ev)
|
|
956
|
+
filtered_sources.append(None)
|
|
888
957
|
si += 1
|
|
889
958
|
continue
|
|
890
959
|
key = sources[si]
|
|
@@ -895,6 +964,14 @@ def _lightweight_rebuild_events(conn, session_id):
|
|
|
895
964
|
ev.cc = tok.get("cache_creation", 0) or 0
|
|
896
965
|
ev.cr = tok.get("cache_read", 0) or 0
|
|
897
966
|
filtered.append(ev)
|
|
967
|
+
filtered_sources.append(key)
|
|
968
|
+
# `with_sources` mirrors `_cache_failure_events_from_items`, which has always
|
|
969
|
+
# returned `(events, sources)`. `_iter_cache_failures` yields the flagged
|
|
970
|
+
# event's INDEX, so without the parallel list a caller cannot say which turn
|
|
971
|
+
# was flagged — which is exactly what the diagnosis has to publish. Default
|
|
972
|
+
# off, so every existing caller keeps the bare list it always received.
|
|
973
|
+
if with_sources:
|
|
974
|
+
return filtered, filtered_sources
|
|
898
975
|
return filtered
|
|
899
976
|
|
|
900
977
|
|
|
@@ -1678,14 +1755,68 @@ def _turn_usage_map(conn, turn_keys):
|
|
|
1678
1755
|
_SPAWN_TOOL_NAMES = ("Agent", "Task")
|
|
1679
1756
|
|
|
1680
1757
|
|
|
1758
|
+
def _claude_canonical_rows(conn, session_id):
|
|
1759
|
+
"""The 18-column physical read a canonical assembly runs over.
|
|
1760
|
+
|
|
1761
|
+
Returns None for an unknown session. Split out of ``_assemble_session``
|
|
1762
|
+
(#620 S3 X1) so the assembly fold can be pure over prefetched rows while
|
|
1763
|
+
the wrapper keeps performing exactly the reads it always did.
|
|
1764
|
+
|
|
1765
|
+
#177: stop_reason / attribution_* are TAIL-APPENDED (indices 15/16/17)
|
|
1766
|
+
AFTER source_tool_use_id so the existing positional reads (incl.
|
|
1767
|
+
``_latest(logical, 10/11)`` for cwd/git_branch, ``r[6]`` for model, ``[2]``
|
|
1768
|
+
for ts) are all unchanged. Every unpacker downstream extends its tail in
|
|
1769
|
+
lockstep.
|
|
1770
|
+
"""
|
|
1771
|
+
exists = conn.execute(
|
|
1772
|
+
"SELECT 1 FROM conversation_messages WHERE session_id=? LIMIT 1",
|
|
1773
|
+
(session_id,)).fetchone()
|
|
1774
|
+
if exists is None:
|
|
1775
|
+
return None
|
|
1776
|
+
return conn.execute(
|
|
1777
|
+
"SELECT id, uuid, timestamp_utc, entry_type, text, blocks_json, model, "
|
|
1778
|
+
" msg_id, req_id, is_sidechain, cwd, git_branch, source_path, parent_uuid, "
|
|
1779
|
+
" source_tool_use_id, stop_reason, attribution_skill, attribution_plugin "
|
|
1780
|
+
"FROM conversation_messages WHERE session_id=? "
|
|
1781
|
+
"ORDER BY timestamp_utc, id", (session_id,)).fetchall()
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
def _claude_turn_keys(rows):
|
|
1785
|
+
"""The deduped assistant turn keys, in first-occurrence document order.
|
|
1786
|
+
|
|
1787
|
+
Exactly the list ``fold_claude_canonical`` builds internally as
|
|
1788
|
+
``turn_index``, derived here so the wrapper can prefetch the cost and usage
|
|
1789
|
+
maps the fold consumes. Dedup by uuid comes first, because a replay carries
|
|
1790
|
+
the original uuid and its later occurrence is not canonical.
|
|
1791
|
+
"""
|
|
1792
|
+
seen_uuid = set()
|
|
1793
|
+
seen_keys = set()
|
|
1794
|
+
keys = []
|
|
1795
|
+
for row in rows:
|
|
1796
|
+
u = row[1]
|
|
1797
|
+
if u in seen_uuid:
|
|
1798
|
+
continue
|
|
1799
|
+
seen_uuid.add(u)
|
|
1800
|
+
if row[3] == "assistant" and row[7] is not None:
|
|
1801
|
+
key = (row[7], row[8])
|
|
1802
|
+
if key not in seen_keys:
|
|
1803
|
+
seen_keys.add(key)
|
|
1804
|
+
keys.append(key)
|
|
1805
|
+
return keys
|
|
1806
|
+
|
|
1807
|
+
|
|
1681
1808
|
def _assemble_session(conn, session_id):
|
|
1682
1809
|
"""Shared assembly for get_conversation / get_conversation_outline (#177 S5).
|
|
1683
1810
|
|
|
1684
|
-
Runs the full dedup
|
|
1811
|
+
Runs the full dedup -> turn-grouping -> fold -> sweep -> meta-classify ->
|
|
1685
1812
|
cost/usage-stamp pipeline over the WHOLE session and returns the
|
|
1686
1813
|
pre-pagination state, so the outline's turns match the reader's items 1:1
|
|
1687
|
-
BY CONSTRUCTION (Codex F8
|
|
1814
|
+
BY CONSTRUCTION (Codex F8 - one grouping pass, never two implementations).
|
|
1688
1815
|
Returns None for an unknown session.
|
|
1816
|
+
|
|
1817
|
+
Since #620 S3 this is a thin wrapper: it performs the three reads and
|
|
1818
|
+
delegates to the pure ``fold_claude_canonical``. Its signature, return
|
|
1819
|
+
shape and every caller are unchanged.
|
|
1689
1820
|
"""
|
|
1690
1821
|
# Opt-in phase instrumentation (issue #276 M5): an outer `assemble` root
|
|
1691
1822
|
# wrapping eight structural child seams. Explicit __enter__/__exit__ (not a
|
|
@@ -1696,38 +1827,94 @@ def _assemble_session(conn, session_id):
|
|
|
1696
1827
|
sp = _perf()
|
|
1697
1828
|
_root = sp.phase("assemble"); _root.__enter__()
|
|
1698
1829
|
_ph = sp.phase("assemble.read"); _ph.__enter__()
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
(session_id,)).fetchone()
|
|
1702
|
-
if exists is None:
|
|
1830
|
+
rows = _claude_canonical_rows(conn, session_id)
|
|
1831
|
+
if rows is None:
|
|
1703
1832
|
_ph.__exit__(None, None, None)
|
|
1704
1833
|
_root.__exit__(None, None, None)
|
|
1705
1834
|
return None
|
|
1835
|
+
_ph.set_count(len(rows)); _ph.__exit__(None, None, None)
|
|
1836
|
+
|
|
1837
|
+
# The three reads the fold would otherwise perform, hoisted here.
|
|
1838
|
+
# `_turn_cost_map` and `_turn_usage_map` are SEPARATE maps over the same
|
|
1839
|
+
# deduped session_entries row: cost may be a vendor-provided override, so
|
|
1840
|
+
# the two are never arithmetically equal and neither can be derived from
|
|
1841
|
+
# the other.
|
|
1842
|
+
turn_keys = _claude_turn_keys(rows)
|
|
1843
|
+
# #620 S3 R5: the two chunked `session_entries` reads are the database work
|
|
1844
|
+
# `assemble.cost` was created to measure, and hoisting them out of the fold
|
|
1845
|
+
# left that time attributed to the untraced remainder of the root. They get
|
|
1846
|
+
# their own phase here, carrying the same chunking meta, so
|
|
1847
|
+
# `dashboard-perf --trace on` still sees them. A DISTINCT name, because the
|
|
1848
|
+
# fold keeps its own `assemble.cost` phase over the stamping it still does
|
|
1849
|
+
# and two siblings sharing one name would collide in every by-name view.
|
|
1850
|
+
_ph = sp.phase("assemble.cost_read"); _ph.__enter__()
|
|
1851
|
+
_chunks = (len(turn_keys) + 399) // 400 if turn_keys else 0
|
|
1852
|
+
_ph.set_meta(turn_keys=len(turn_keys), cost_chunks=_chunks,
|
|
1853
|
+
usage_chunks=_chunks)
|
|
1854
|
+
_usage = _turn_usage_map(conn, turn_keys)
|
|
1855
|
+
_costs = _turn_cost_map(conn, turn_keys)
|
|
1856
|
+
_ph.__exit__(None, None, None)
|
|
1857
|
+
result = fold_claude_canonical(
|
|
1858
|
+
rows, _usage, _costs,
|
|
1859
|
+
allow_human_fallback=_reingest_pending(conn),
|
|
1860
|
+
)
|
|
1861
|
+
_root.__exit__(None, None, None)
|
|
1862
|
+
return result
|
|
1706
1863
|
|
|
1707
|
-
# Pull the session ordered; dedup logical messages by (session_id, uuid),
|
|
1708
|
-
# canonical row = earliest (timestamp_utc, id). Replays carry the original
|
|
1709
|
-
# uuid, so the first occurrence in ascending order is canonical.
|
|
1710
|
-
# #177: stop_reason / attribution_* are TAIL-APPENDED (indices 15/16/17)
|
|
1711
|
-
# AFTER source_tool_use_id so the existing positional reads (incl.
|
|
1712
|
-
# _latest(logical, 10/11) for cwd/git_branch, r[6] for model, [2] for ts)
|
|
1713
|
-
# are all unchanged. Every unpacker below extends its tail in lockstep.
|
|
1714
|
-
raw = conn.execute(
|
|
1715
|
-
"SELECT id, uuid, timestamp_utc, entry_type, text, blocks_json, model, "
|
|
1716
|
-
" msg_id, req_id, is_sidechain, cwd, git_branch, source_path, parent_uuid, "
|
|
1717
|
-
" source_tool_use_id, stop_reason, attribution_skill, attribution_plugin "
|
|
1718
|
-
"FROM conversation_messages WHERE session_id=? "
|
|
1719
|
-
"ORDER BY timestamp_utc, id", (session_id,)).fetchall()
|
|
1720
|
-
_ph.set_count(len(raw)); _ph.__exit__(None, None, None)
|
|
1721
1864
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1865
|
+
def dedupe_claude_uuid_rows(rows, uuid_of=None):
|
|
1866
|
+
"""Keep the FIRST occurrence of each uuid, in the order given.
|
|
1867
|
+
|
|
1868
|
+
One statement of the rule, shared by ``fold_claude_canonical`` and by the
|
|
1869
|
+
#620 S3 fan-out join. ``conversation_messages`` is unique on
|
|
1870
|
+
``(source_path, byte_offset)`` only, the dedicated ``idx_conv_session_uuid``
|
|
1871
|
+
index exists precisely because the canonical reader dedupes on the logical
|
|
1872
|
+
identity, and a ``--resume``d session replays its rows into a second file
|
|
1873
|
+
carrying the ORIGINAL uuid. Callers pass ONE session's rows in
|
|
1874
|
+
``(timestamp_utc, id)`` order, which makes this the ``(session_id, uuid)``
|
|
1875
|
+
deduplication the S3 spec names.
|
|
1876
|
+
|
|
1877
|
+
``uuid_of`` reads the uuid out of a caller's row shape; it defaults to the
|
|
1878
|
+
18-column physical tuple's index 1.
|
|
1879
|
+
"""
|
|
1880
|
+
read = uuid_of if uuid_of is not None else (lambda row: row[1])
|
|
1881
|
+
seen = set()
|
|
1882
|
+
kept = []
|
|
1883
|
+
for row in rows:
|
|
1884
|
+
key = read(row)
|
|
1885
|
+
if key in seen:
|
|
1728
1886
|
continue
|
|
1729
|
-
|
|
1730
|
-
|
|
1887
|
+
seen.add(key)
|
|
1888
|
+
kept.append(row)
|
|
1889
|
+
return kept
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
def fold_claude_canonical(rows, token_map, cost_map, *,
|
|
1893
|
+
allow_human_fallback=False):
|
|
1894
|
+
"""The pure canonical assembly, over prefetched facts (#620 S3 X1).
|
|
1895
|
+
|
|
1896
|
+
THREE positional arguments, not two. ``_assemble_session`` consumes a
|
|
1897
|
+
separately priced cost map alongside the token map, so a fold taking only
|
|
1898
|
+
rows and tokens cannot reproduce it: cost may be the vendor-provided
|
|
1899
|
+
``cost_usd_raw`` override with the token math bypassed, and the contract
|
|
1900
|
+
between the two maps is "same source row", never ``cost == f(tokens)``.
|
|
1901
|
+
|
|
1902
|
+
``rows`` is the 18-column physical stream in (timestamp_utc, id) order;
|
|
1903
|
+
``token_map`` and ``cost_map`` are keyed by ``(msg_id, req_id)``;
|
|
1904
|
+
``allow_human_fallback`` is the read-time 005-reingest gate. This function
|
|
1905
|
+
opens no database and returns the same dict ``_assemble_session`` has
|
|
1906
|
+
always returned.
|
|
1907
|
+
"""
|
|
1908
|
+
sp = _perf()
|
|
1909
|
+
# #620 S3 R8: this fold opens CHILD phases, and `_assemble_session` is no
|
|
1910
|
+
# longer its only caller — the diagnosis calls it directly. With no root
|
|
1911
|
+
# open those children would be emitted into whatever phase happened to be
|
|
1912
|
+
# on the stack, so open the `assemble` root here when nothing else has.
|
|
1913
|
+
_own_root = None
|
|
1914
|
+
if not sp.in_phase():
|
|
1915
|
+
_own_root = sp.phase("assemble"); _own_root.__enter__()
|
|
1916
|
+
_ph = sp.phase("assemble.dedup"); _ph.__enter__()
|
|
1917
|
+
logical = dedupe_claude_uuid_rows(rows) # canonical physical rows, in order
|
|
1731
1918
|
_ph.set_count(len(logical)); _ph.__exit__(None, None, None)
|
|
1732
1919
|
|
|
1733
1920
|
_ph = sp.phase("assemble.build"); _ph.__enter__()
|
|
@@ -1993,7 +2180,6 @@ def _assemble_session(conn, session_id):
|
|
|
1993
2180
|
# + skill_name, so the client renders a collapsed skill/system-marker/context
|
|
1994
2181
|
# disclosure instead of a "YOU" prompt. `text` is set to the rendered body
|
|
1995
2182
|
# (the DB text column stays '' for FTS); genuine human turns are untouched.
|
|
1996
|
-
allow_human_fallback = _reingest_pending(conn)
|
|
1997
2183
|
for it in items:
|
|
1998
2184
|
if it["kind"] in ("meta", "human"):
|
|
1999
2185
|
# #188: a slash-command invocation carrying a real prompt in
|
|
@@ -2070,10 +2256,10 @@ def _assemble_session(conn, session_id):
|
|
|
2070
2256
|
_cost_chunks = (_turn_keys + 399) // 400 if _turn_keys else 0
|
|
2071
2257
|
_ph.set_meta(turn_keys=_turn_keys, cost_chunks=_cost_chunks,
|
|
2072
2258
|
usage_chunks=_cost_chunks)
|
|
2073
|
-
costs =
|
|
2259
|
+
costs = cost_map
|
|
2074
2260
|
# #177: per-turn token usage from the SAME deduped session_entries row cost
|
|
2075
2261
|
# uses (a separate map; _turn_cost_map is unchanged for the search path).
|
|
2076
|
-
usage =
|
|
2262
|
+
usage = token_map
|
|
2077
2263
|
# Stamp per-item cost first, then derive the header from the SUM of the
|
|
2078
2264
|
# ROUNDED per-item assistant costs (M2) — so the §6.5 invariant
|
|
2079
2265
|
# sum(items.cost_usd) == header cost_usd holds EXACTLY to 1e-9 by
|
|
@@ -2256,7 +2442,8 @@ def _assemble_session(conn, session_id):
|
|
|
2256
2442
|
for it in items:
|
|
2257
2443
|
it.pop("_source_tool_use_id", None)
|
|
2258
2444
|
|
|
2259
|
-
|
|
2445
|
+
if _own_root is not None:
|
|
2446
|
+
_own_root.__exit__(None, None, None)
|
|
2260
2447
|
return {"items": items, "logical": logical,
|
|
2261
2448
|
"subagent_meta": subagent_meta, "header_cost": header_cost}
|
|
2262
2449
|
|