cctally 1.40.0 → 1.41.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 +5 -0
- package/bin/_cctally_cache.py +103 -6
- package/bin/_cctally_dashboard.py +60 -3
- package/bin/_cctally_db.py +179 -45
- package/bin/_lib_conversation.py +75 -21
- package/bin/_lib_conversation_query.py +385 -32
- package/dashboard/static/assets/{index-BXm_vBKA.css → index-BKlFTF9C.css} +1 -1
- package/dashboard/static/assets/{index-BtbVKnkm.js → index-DZ_higAv.js} +34 -34
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
package/bin/_lib_conversation.py
CHANGED
|
@@ -53,7 +53,13 @@ class MessageRow:
|
|
|
53
53
|
stop_reason: "str | None" = None
|
|
54
54
|
attribution_skill: "str | None" = None
|
|
55
55
|
attribution_plugin: "str | None" = None
|
|
56
|
-
|
|
56
|
+
# #177 S6: split non-prose search columns, derived by _derive_search_columns
|
|
57
|
+
# on the FINAL post-augment blocks (see _normalize). ``search_aux`` is kept
|
|
58
|
+
# physically (legacy column) but NEVER assigned a non-empty value — the
|
|
59
|
+
# consolidated multi-column FTS reads search_tool/search_thinking instead.
|
|
60
|
+
search_tool: str = ""
|
|
61
|
+
search_thinking: str = ""
|
|
62
|
+
search_aux: str = "" # documented-dead (#177 S6); always "" on new rows
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def iter_message_rows(fh, path_str):
|
|
@@ -107,7 +113,7 @@ def _normalize(obj, t, offset):
|
|
|
107
113
|
msg = obj.get("message")
|
|
108
114
|
if not isinstance(msg, dict):
|
|
109
115
|
msg = {}
|
|
110
|
-
blocks, text
|
|
116
|
+
blocks, text = _blocks_and_text(msg.get("content"))
|
|
111
117
|
if t == "assistant":
|
|
112
118
|
entry_type = ASSISTANT
|
|
113
119
|
elif any(b["kind"] == "tool_result" for b in blocks):
|
|
@@ -140,6 +146,14 @@ def _normalize(obj, t, offset):
|
|
|
140
146
|
else:
|
|
141
147
|
entry_type = HUMAN
|
|
142
148
|
is_asst = t == "assistant"
|
|
149
|
+
# #177 S6: derive the split search columns on the FINAL post-augment blocks
|
|
150
|
+
# (every _attach_* pass above has already merged bash_stderr / answers /
|
|
151
|
+
# annotations into the blocks). This is the SAME chokepoint the migration-010
|
|
152
|
+
# backfill runs on json.loads(blocks_json), so live ingest and backfill
|
|
153
|
+
# produce byte-identical values (the parity invariant). Computed immediately
|
|
154
|
+
# before json.dumps(blocks) so blocks_json and the columns agree by
|
|
155
|
+
# construction.
|
|
156
|
+
search_tool, search_thinking = _derive_search_columns(blocks)
|
|
143
157
|
return MessageRow(
|
|
144
158
|
byte_offset=offset,
|
|
145
159
|
session_id=obj.get("sessionId"),
|
|
@@ -163,26 +177,27 @@ def _normalize(obj, t, offset):
|
|
|
163
177
|
stop_reason=msg.get("stop_reason") if is_asst else None,
|
|
164
178
|
attribution_skill=obj.get("attributionSkill"),
|
|
165
179
|
attribution_plugin=obj.get("attributionPlugin"),
|
|
166
|
-
|
|
180
|
+
search_tool=search_tool,
|
|
181
|
+
search_thinking=search_thinking,
|
|
182
|
+
# search_aux stays "" (documented-dead, #177 S6) — the consolidated
|
|
183
|
+
# multi-column FTS reads search_tool/search_thinking instead.
|
|
167
184
|
)
|
|
168
185
|
|
|
169
186
|
|
|
170
187
|
def _blocks_and_text(content):
|
|
171
|
-
"""Return (normalized blocks list, indexed-prose string
|
|
188
|
+
"""Return (normalized blocks list, indexed-prose string).
|
|
172
189
|
|
|
173
190
|
Prose (``text``) = joined ``text`` blocks only (thinking / tool_use /
|
|
174
|
-
tool_result excluded — those go to the
|
|
175
|
-
``
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
searchable without polluting prose FTS. Prose is deliberately excluded from
|
|
182
|
-
aux (it is already in ``text``)."""
|
|
191
|
+
tool_result excluded — those go to the split search columns via
|
|
192
|
+
``_derive_search_columns``, which runs in ``_normalize`` on the FINAL
|
|
193
|
+
post-augment blocks). #177 S6 dropped the in-loop ``search_aux`` accumulation
|
|
194
|
+
that used to be the third return element: deriving the search columns here
|
|
195
|
+
(pre-augment) would miss bash_stderr / answers / annotations and break the
|
|
196
|
+
ingest==backfill parity invariant, so the derivation moved to the
|
|
197
|
+
post-augment chokepoint."""
|
|
183
198
|
if isinstance(content, str):
|
|
184
|
-
return (([{"kind": "text", "text": content}] if content else []), content
|
|
185
|
-
blocks, texts
|
|
199
|
+
return (([{"kind": "text", "text": content}] if content else []), content)
|
|
200
|
+
blocks, texts = [], []
|
|
186
201
|
if isinstance(content, list):
|
|
187
202
|
# #177 S4: ordinal among media items at THIS list level, keyed by the
|
|
188
203
|
# object identity of each image/document item, so the placeholder writers
|
|
@@ -199,7 +214,6 @@ def _blocks_and_text(content):
|
|
|
199
214
|
elif bt == "thinking":
|
|
200
215
|
think = b.get("thinking", "") or ""
|
|
201
216
|
blocks.append({"kind": "thinking", "text": think}) # FULL text for render
|
|
202
|
-
aux_parts.append(think[:_TOOL_RESULT_CAP]) # aux index capped (I2)
|
|
203
217
|
elif bt == "tool_use":
|
|
204
218
|
bounded, input_trunc = _bound_input(b.get("input"))
|
|
205
219
|
block = {"kind": "tool_use", "name": b.get("name"),
|
|
@@ -211,7 +225,6 @@ def _blocks_and_text(content):
|
|
|
211
225
|
st = inp.get("subagent_type") if isinstance(inp, dict) else None
|
|
212
226
|
if isinstance(st, str) and st: # #166: spawn kind (Agent/Task)
|
|
213
227
|
block["subagent_type"] = st
|
|
214
|
-
aux_parts.extend(_aux_strings(bounded))
|
|
215
228
|
blocks.append(block)
|
|
216
229
|
elif bt == "tool_result":
|
|
217
230
|
raw = _stringify(b.get("content"))
|
|
@@ -230,14 +243,12 @@ def _blocks_and_text(content):
|
|
|
230
243
|
if media: # omitted when empty (additive)
|
|
231
244
|
block["media"] = media
|
|
232
245
|
blocks.append(block)
|
|
233
|
-
aux_parts.append(clipped)
|
|
234
246
|
elif bt in _MEDIA_BLOCK_TYPES:
|
|
235
247
|
blocks.append({"kind": bt, **_media(b.get("source")),
|
|
236
248
|
"index": media_index[id(b)]})
|
|
237
249
|
elif bt == "tool_reference":
|
|
238
250
|
blocks.append({"kind": "tool_reference", "name": b.get("name")})
|
|
239
|
-
return (blocks, "\n".join(t for t in texts if t)
|
|
240
|
-
"\n".join(a for a in aux_parts if a))
|
|
251
|
+
return (blocks, "\n".join(t for t in texts if t))
|
|
241
252
|
|
|
242
253
|
|
|
243
254
|
_SUBAGENT_META_KEYS = (
|
|
@@ -558,8 +569,51 @@ def _bound_input(inp):
|
|
|
558
569
|
return (bounded, state["truncated"])
|
|
559
570
|
|
|
560
571
|
|
|
572
|
+
def _derive_search_columns(blocks):
|
|
573
|
+
"""(search_tool, search_thinking) from the FINAL normalized blocks list.
|
|
574
|
+
|
|
575
|
+
MUST run post-augmentation (bash_stderr / answers / annotations already
|
|
576
|
+
merged into the blocks) so live ingest and the migration-010 backfill from
|
|
577
|
+
blocks_json produce byte-identical values (#177 S6 parity invariant — the
|
|
578
|
+
chokepoint runs in _normalize right before json.dumps(blocks), and the
|
|
579
|
+
backfill runs it on json.loads(blocks_json)). Caps are PER-BLOCK
|
|
580
|
+
(_TOOL_RESULT_CAP each), matching the old search_aux semantics — NOT a
|
|
581
|
+
whole-column total. Prose is excluded (already in the ``text`` column).
|
|
582
|
+
|
|
583
|
+
``search_tool`` = bounded tool-input string leaves + clipped tool_result
|
|
584
|
+
``text`` + ``bash_stderr`` + bounded AskUserQuestion answers/annotations
|
|
585
|
+
(real ingest stamps these as ``ask_answers``/``ask_annotations``; the raw
|
|
586
|
+
``answers``/``annotations`` keys are also read for forward compatibility and
|
|
587
|
+
the synthetic-block unit tests). ``search_thinking`` = thinking text."""
|
|
588
|
+
tool_parts, think_parts = [], []
|
|
589
|
+
for b in blocks if isinstance(blocks, list) else []:
|
|
590
|
+
if not isinstance(b, dict):
|
|
591
|
+
continue
|
|
592
|
+
k = b.get("kind")
|
|
593
|
+
if k == "thinking":
|
|
594
|
+
t = b.get("text") or ""
|
|
595
|
+
if t:
|
|
596
|
+
think_parts.append(t[:_TOOL_RESULT_CAP])
|
|
597
|
+
elif k == "tool_use":
|
|
598
|
+
tool_parts.extend(
|
|
599
|
+
s[:_TOOL_RESULT_CAP] for s in _aux_strings(b.get("input")))
|
|
600
|
+
elif k == "tool_result":
|
|
601
|
+
t = b.get("text") or ""
|
|
602
|
+
if t:
|
|
603
|
+
tool_parts.append(t[:_TOOL_RESULT_CAP])
|
|
604
|
+
stderr = b.get("bash_stderr") or ""
|
|
605
|
+
if stderr:
|
|
606
|
+
tool_parts.append(stderr[:_TOOL_RESULT_CAP])
|
|
607
|
+
# answers/annotations: real ingest uses the ``ask_``-prefixed keys;
|
|
608
|
+
# the bare keys are read too (forward-compat + synthetic unit tests).
|
|
609
|
+
for key in ("ask_answers", "ask_annotations", "answers", "annotations"):
|
|
610
|
+
tool_parts.extend(
|
|
611
|
+
s[:_TOOL_RESULT_CAP] for s in _aux_strings(b.get(key)))
|
|
612
|
+
return ("\n".join(tool_parts), "\n".join(think_parts))
|
|
613
|
+
|
|
614
|
+
|
|
561
615
|
def _aux_strings(v):
|
|
562
|
-
"""Yield string leaves from a bounded input value (for the
|
|
616
|
+
"""Yield string leaves from a bounded input value (for the search columns)."""
|
|
563
617
|
if isinstance(v, str):
|
|
564
618
|
if v:
|
|
565
619
|
yield v
|