cctally 1.42.0 → 1.43.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 +13 -0
- package/bin/_cctally_cache.py +81 -0
- package/bin/_cctally_db.py +25 -1
- package/bin/_lib_conversation.py +135 -0
- package/bin/_lib_conversation_query.py +45 -1
- package/dashboard/static/assets/{index-3yVZt8cV.css → index-mPwkGfE9.css} +1 -1
- package/dashboard/static/assets/{index-DDuPO7EI.js → index-qxISkiGC.js} +5 -5
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.43.1] - 2026-06-13
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Conversation viewer: harness-injected user-role lines — compaction summaries, background task/Bash completion notifications, remote-control "Message sent at …" stamps, and `!`-mode bash echoes — are no longer rendered as your own messages; they now show as labeled system/notification pills (compaction expandable), and a remote-control reply shows just your text. Already-recorded history is corrected at read time with no migration. (#191)
|
|
12
|
+
|
|
13
|
+
## [1.43.0] - 2026-06-13
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Conversation viewer: a slash command you typed with a real prompt in its arguments (e.g. `/frontend-design <your task>`) is no longer hidden as a "System marker" and dropped from the outline — its arguments now render as your "You" bubble with a small command badge, drive the conversation title, and are searchable; bare control commands (`/clear`, `/compact`, `/model`, the `local-command-*` echoes) and empty-argument invocations still fold into hidden system-marker pills. Recognized at ingest plus a read-time fallback that fixes already-recorded history, with a cache migration (`011`) that backfills the search index for past commands (#188).
|
|
17
|
+
- Conversation viewer: clicking an outline entry now selects and highlights exactly that entry instead of one a turn or two above it, and the forward jump-to-next chips/keys (`u`/`e`/`b`/`p`) reliably advance to the next landmark instead of re-selecting the same one — the outline tracks an explicit selection you set by clicking or jumping, which takes precedence over the scroll position until you next scroll (#188, #187).
|
|
18
|
+
- Conversation viewer: clicking a subagent in the outline now flashes its collapsed card in place and marks the subagent as the current selection, instead of silently force-expanding the thread, flashing a buried inner message, and highlighting the most-recent prompt above it (#188).
|
|
19
|
+
- Conversation viewer: the floating "↓ N new" live-update pill no longer counts messages streamed into a collapsed subagent thread you have not expanded — it counts only turns that actually become visible (top-level messages, a newly appearing subagent card, or messages inside an expanded thread) (#188).
|
|
20
|
+
|
|
8
21
|
## [1.42.0] - 2026-06-13
|
|
9
22
|
|
|
10
23
|
### Fixed
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -681,6 +681,16 @@ def sync_cache(
|
|
|
681
681
|
"DELETE FROM cache_meta WHERE key IN "
|
|
682
682
|
"('conversation_search_split_pending',"
|
|
683
683
|
" 'conversation_search_split_cursor')")
|
|
684
|
+
# #188 bug 4: a rebuild repopulates conversation_messages via the
|
|
685
|
+
# offset-0 walk through the parser, which now classifies a
|
|
686
|
+
# command-args invocation as entry_type='human' at INGEST (A2) — so
|
|
687
|
+
# the migration-011 backfill is redundant. Drop its flag + cursor so
|
|
688
|
+
# the post-rebuild sync runs no redundant promotion pass. MISSING
|
|
689
|
+
# this site re-arms the flag on every cache-sync --rebuild.
|
|
690
|
+
conn.execute(
|
|
691
|
+
"DELETE FROM cache_meta WHERE key IN "
|
|
692
|
+
"('conversation_promote_command_args_pending',"
|
|
693
|
+
" 'conversation_promote_command_args_cursor')")
|
|
684
694
|
conn.commit()
|
|
685
695
|
eprint("[cache-sync] rebuild: cleared Claude cached entries")
|
|
686
696
|
|
|
@@ -780,6 +790,15 @@ def sync_cache(
|
|
|
780
790
|
# old search keeps working until the final swap.
|
|
781
791
|
_consume_search_split(conn)
|
|
782
792
|
|
|
793
|
+
# #188 bug 4: consume the migration-011 command-args promotion under
|
|
794
|
+
# the SAME held flock, AFTER the search split so a row flipped to
|
|
795
|
+
# entry_type='human' here keeps the fresh search_tool/search_thinking
|
|
796
|
+
# the split just wrote (the consumer recomputes them anyway, but
|
|
797
|
+
# ordering keeps the two passes independent + idempotent). Flips
|
|
798
|
+
# legacy META command rows carrying a real <command-args> prompt to
|
|
799
|
+
# HUMAN(text=args); the split-FTS UPDATE triggers re-index the args.
|
|
800
|
+
_consume_promote_command_args(conn)
|
|
801
|
+
|
|
783
802
|
paths: list[pathlib.Path] = list(_iter_claude_jsonl_files())
|
|
784
803
|
stats.files_total = len(paths)
|
|
785
804
|
|
|
@@ -1357,6 +1376,68 @@ def _consume_search_split(conn) -> None:
|
|
|
1357
1376
|
conn.commit()
|
|
1358
1377
|
|
|
1359
1378
|
|
|
1379
|
+
def _consume_promote_command_args(conn) -> None:
|
|
1380
|
+
"""#188 bug 4: flock-held consumer for ``conversation_promote_command_args_pending``
|
|
1381
|
+
(set by cache migration 011). Cursor-resumable walk of
|
|
1382
|
+
``conversation_messages WHERE entry_type='meta'``: a row whose ``blocks_json``
|
|
1383
|
+
is a pure slash-command marker with a NON-EMPTY ``<command-args>`` is a real
|
|
1384
|
+
user turn, so flip it to ``entry_type='human'`` with ``text=args`` and
|
|
1385
|
+
recompute ``search_tool``/``search_thinking`` via the SHARED
|
|
1386
|
+
``_lib_conversation._derive_search_columns`` chokepoint (byte-identical to
|
|
1387
|
+
live ingest). ``/clear`` and stdout-only markers (``_extract_command_invocation``
|
|
1388
|
+
returns None) stay META untouched.
|
|
1389
|
+
|
|
1390
|
+
The split-FTS ``AFTER UPDATE OF text, search_tool, search_thinking`` triggers
|
|
1391
|
+
keep the external-content index in sync, so we never hand-write FTS rows.
|
|
1392
|
+
FTS5-unavailable (``fts5_unavailable`` set): no triggers exist, so the
|
|
1393
|
+
base-column UPDATE alone is correct (the index lands later via the
|
|
1394
|
+
rebuild-on-availability path). Checkpoints
|
|
1395
|
+
``conversation_promote_command_args_cursor`` per 500-row batch; clears both
|
|
1396
|
+
keys when the cursor is exhausted. Interrupted ⇒ resumes from the cursor on
|
|
1397
|
+
the next locked sync; a fresh install never sets the flag → cheap no-op."""
|
|
1398
|
+
if conn.execute(
|
|
1399
|
+
"SELECT 1 FROM cache_meta "
|
|
1400
|
+
"WHERE key='conversation_promote_command_args_pending'"
|
|
1401
|
+
).fetchone() is None:
|
|
1402
|
+
return
|
|
1403
|
+
row = conn.execute(
|
|
1404
|
+
"SELECT value FROM cache_meta "
|
|
1405
|
+
"WHERE key='conversation_promote_command_args_cursor'").fetchone()
|
|
1406
|
+
last_id = int(row[0]) if row else 0
|
|
1407
|
+
while True:
|
|
1408
|
+
batch = conn.execute(
|
|
1409
|
+
"SELECT id, blocks_json FROM conversation_messages "
|
|
1410
|
+
"WHERE id > ? AND entry_type='meta' ORDER BY id LIMIT 500",
|
|
1411
|
+
(last_id,)).fetchall()
|
|
1412
|
+
if not batch:
|
|
1413
|
+
break
|
|
1414
|
+
ups = []
|
|
1415
|
+
for rid, bj in batch:
|
|
1416
|
+
last_id = rid
|
|
1417
|
+
try:
|
|
1418
|
+
blocks = json.loads(bj) if bj else []
|
|
1419
|
+
except (TypeError, ValueError):
|
|
1420
|
+
blocks = []
|
|
1421
|
+
inv = _lib_conversation._extract_command_invocation(
|
|
1422
|
+
blocks, _lib_conversation._join_text_blocks(blocks))
|
|
1423
|
+
if inv is None:
|
|
1424
|
+
continue
|
|
1425
|
+
st, sth = _lib_conversation._derive_search_columns(blocks)
|
|
1426
|
+
ups.append((inv["args"], st, sth, rid))
|
|
1427
|
+
if ups:
|
|
1428
|
+
conn.executemany(
|
|
1429
|
+
"UPDATE conversation_messages SET entry_type='human', text=?, "
|
|
1430
|
+
"search_tool=?, search_thinking=? WHERE id=?", ups)
|
|
1431
|
+
_cctally_db_sib._set_cache_meta(
|
|
1432
|
+
conn, "conversation_promote_command_args_cursor", str(last_id))
|
|
1433
|
+
conn.commit()
|
|
1434
|
+
conn.execute(
|
|
1435
|
+
"DELETE FROM cache_meta WHERE key IN "
|
|
1436
|
+
"('conversation_promote_command_args_pending',"
|
|
1437
|
+
" 'conversation_promote_command_args_cursor')")
|
|
1438
|
+
conn.commit()
|
|
1439
|
+
|
|
1440
|
+
|
|
1360
1441
|
def iter_entries(
|
|
1361
1442
|
conn: sqlite3.Connection,
|
|
1362
1443
|
range_start: dt.datetime,
|
package/bin/_cctally_db.py
CHANGED
|
@@ -821,7 +821,12 @@ def _run_pending_migrations(
|
|
|
821
821
|
"projected_milestones",
|
|
822
822
|
"codex_budget_milestones",
|
|
823
823
|
),
|
|
824
|
-
|
|
824
|
+
# conversation_messages joins session_entries as a fresh-install
|
|
825
|
+
# signal (#188): a transcript-populated cache whose cost rows are
|
|
826
|
+
# absent must still be treated as non-fresh so a flag-only
|
|
827
|
+
# conversation migration's consumer actually runs (e.g. 011's
|
|
828
|
+
# command-args promotion) instead of being stamped without it.
|
|
829
|
+
"cache.db": ("session_entries", "conversation_messages"),
|
|
825
830
|
}.get(db_label, ())
|
|
826
831
|
for probe_table in probe_tables:
|
|
827
832
|
# _probe_table_nonempty centralizes the "is there data here?"
|
|
@@ -3359,6 +3364,25 @@ def conversation_search_depth(conn: sqlite3.Connection) -> str:
|
|
|
3359
3364
|
return "prose-only" if pending else "full"
|
|
3360
3365
|
|
|
3361
3366
|
|
|
3367
|
+
@cache_migration("011_conversation_promote_command_args")
|
|
3368
|
+
def _011_conversation_promote_command_args(conn: sqlite3.Connection) -> None:
|
|
3369
|
+
"""Flag-only arm for #188 bug 4. Sets
|
|
3370
|
+
``conversation_promote_command_args_pending``; sync_cache consumes it under
|
|
3371
|
+
the cache.db.lock flock (cursor-resumable: flip legacy ``entry_type='META'``
|
|
3372
|
+
command-marker rows whose ``<command-args>`` carry a real user prompt to
|
|
3373
|
+
``entry_type='human'`` with ``text=args``, recomputing the split search
|
|
3374
|
+
columns so the args enter the entry_type='human' list-title/prompts facet and
|
|
3375
|
+
the FTS index — see _cctally_cache._consume_promote_command_args). The
|
|
3376
|
+
handler does NO data work so the dispatcher's central stamp (#140) marks a
|
|
3377
|
+
genuinely-complete handler; a fresh install stamps it WITHOUT running (its
|
|
3378
|
+
rows are already promoted at ingest, so the consumer finds nothing to flip).
|
|
3379
|
+
DISPLAY is already fixed read-time (no migration needed); the migration's
|
|
3380
|
+
sole job is FTS-searchability + the list-title facet on legacy data. Mirrors
|
|
3381
|
+
the flag-only pattern of 002/003/007/009/010."""
|
|
3382
|
+
_set_cache_meta(conn, "conversation_promote_command_args_pending", "1")
|
|
3383
|
+
conn.commit()
|
|
3384
|
+
|
|
3385
|
+
|
|
3362
3386
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
3363
3387
|
|
|
3364
3388
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|
package/bin/_lib_conversation.py
CHANGED
|
@@ -41,6 +41,53 @@ def _is_system_marker(text) -> bool:
|
|
|
41
41
|
return bool(text) and _MARKER_RE.fullmatch(text) is not None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# #188: a slash-command invocation carries the user's real prompt inside
|
|
45
|
+
# <command-args>. The <command-name>/<command-message> wrappers are plumbing.
|
|
46
|
+
# Anchored mid-string is fine — _is_system_marker already proved the whole text
|
|
47
|
+
# is ONLY markers before these run, so the first match is the lone occurrence.
|
|
48
|
+
_CMD_NAME_RE = re.compile(r"<command-name>([\s\S]*?)</command-name>")
|
|
49
|
+
_CMD_ARGS_RE = re.compile(r"<command-args>([\s\S]*?)</command-args>")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _join_text_blocks(blocks):
|
|
53
|
+
"""'\\n'-join the text-block bodies of a normalized blocks list (mirrors
|
|
54
|
+
_blocks_and_text's prose join + the query kernel's _join_text_blocks). The
|
|
55
|
+
migration-011 consumer rebuilds the marker text from blocks_json to feed
|
|
56
|
+
_extract_command_invocation, so this lives in the parser kernel (the one
|
|
57
|
+
_cctally_cache already imports) to keep the two derivations identical."""
|
|
58
|
+
if not blocks:
|
|
59
|
+
return ""
|
|
60
|
+
return "\n".join(b.get("text", "") or ""
|
|
61
|
+
for b in blocks if b.get("kind") == "text")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_command_invocation(blocks, text):
|
|
65
|
+
"""If a row is a pure slash-command marker whose <command-args> is non-empty
|
|
66
|
+
(after strip), return ``{"name": <command-name or "">, "args": <args>}``;
|
|
67
|
+
else ``None`` (#188 bug 4).
|
|
68
|
+
|
|
69
|
+
Block-aware: promotes ONLY when every block is text (mirrors the all-text
|
|
70
|
+
guard in ``_normalize`` / ``_meta_classify`` so a marker text block PLUS an
|
|
71
|
+
attachment — e.g. an image — is never promoted) AND ``text`` is a pure
|
|
72
|
+
command-marker block (``_is_system_marker``) AND ``<command-args>`` holds a
|
|
73
|
+
real prompt. ``/clear``, ``/exit``, ``/compact``, ``/model`` (empty args) and
|
|
74
|
+
stdout-only markers all return ``None`` so they stay hidden as system
|
|
75
|
+
markers. ``name`` is taken from ``<command-name>`` for the reader's command
|
|
76
|
+
badge; it is purely cosmetic (``""`` when the marker omits the name tag)."""
|
|
77
|
+
if not blocks or not all(b.get("kind") == "text" for b in blocks):
|
|
78
|
+
return None
|
|
79
|
+
if not _is_system_marker(text):
|
|
80
|
+
return None
|
|
81
|
+
am = _CMD_ARGS_RE.search(text)
|
|
82
|
+
if am is None:
|
|
83
|
+
return None
|
|
84
|
+
args = am.group(1).strip()
|
|
85
|
+
if not args:
|
|
86
|
+
return None
|
|
87
|
+
nm = _CMD_NAME_RE.search(text)
|
|
88
|
+
return {"name": (nm.group(1).strip() if nm else ""), "args": args}
|
|
89
|
+
|
|
90
|
+
|
|
44
91
|
# #186: strip ANSI terminal control sequences from captured prose/thinking so a
|
|
45
92
|
# slash-command stdout echo (which keeps its SGR styling, e.g. `\x1b[1mFable
|
|
46
93
|
# 5\x1b[22m`) never leaks literal `^[[1m` control codes into a title, an outline
|
|
@@ -65,6 +112,64 @@ def _strip_ansi(text):
|
|
|
65
112
|
return _ANSI_RE.sub("", text) if text else text
|
|
66
113
|
|
|
67
114
|
|
|
115
|
+
# ---- #191: harness-injected user-line discriminators (shared with the query
|
|
116
|
+
# kernel via re-export). All key on a SELF-IDENTIFYING body shape so the
|
|
117
|
+
# read-time recovery half can rescue already-ingested `human` rows. ----
|
|
118
|
+
|
|
119
|
+
# Compaction summary preamble. The ingest authority is the `isCompactSummary`
|
|
120
|
+
# JSONL flag (see _normalize); this body match is the read-time meta_kind label
|
|
121
|
+
# authority (and rescues the rare legacy sentinel-without-flag row).
|
|
122
|
+
_COMPACT_SENTINEL = "This session is being continued from a previous conversation"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_compaction_body(text) -> bool:
|
|
126
|
+
"""True iff `text` is a compaction-summary body."""
|
|
127
|
+
return bool(text) and text.lstrip().startswith(_COMPACT_SENTINEL)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Full open+close-tag wrapper (NOT a bare startswith — Codex P1a): a real prompt
|
|
131
|
+
# that merely begins with the literal tag but isn't a well-formed closed block
|
|
132
|
+
# stays human. Unrolled-lazy body = linear time; \1 backref forces the close tag
|
|
133
|
+
# to match the open. A trailing harness line after the close tag is allowed.
|
|
134
|
+
_NOTIFICATION_RE = re.compile(
|
|
135
|
+
r"\s*<(task|bash)-notification>(?:(?!</\1-notification>)[\s\S])*</\1-notification>")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _is_notification_body(text) -> bool:
|
|
139
|
+
"""True iff `text` opens with a complete <task-notification>…</task-notification>
|
|
140
|
+
or <bash-notification>…</bash-notification> background-completion wrapper."""
|
|
141
|
+
return bool(text) and _NOTIFICATION_RE.match(text) is not None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# `!`-mode local shell echoes. A DEDICATED detector, NOT a `_MARKER_TAGS` entry
|
|
145
|
+
# (Codex P1b) — so a bash echo containing a literal <command-args> can never
|
|
146
|
+
# reach the #188 `_extract_command_invocation` promotion path.
|
|
147
|
+
_BASH_ECHO_RE = re.compile(
|
|
148
|
+
r"\s*<(bash-input|bash-stdout|bash-stderr)>(?:(?!</\1>)[\s\S])*</\1>")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_bash_echo_body(text) -> bool:
|
|
152
|
+
"""True iff `text` opens with a complete <bash-input>/<bash-stdout>/
|
|
153
|
+
<bash-stderr> echo wrapper."""
|
|
154
|
+
return bool(text) and _BASH_ECHO_RE.match(text) is not None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Remote-control replies (sent from the Claude mobile/remote app) arrive as a
|
|
158
|
+
# real user turn prefixed with a `Message sent at <ts> UTC.` system-reminder
|
|
159
|
+
# stamp. NARROW (Codex / approved decision): only this exact leading shape is
|
|
160
|
+
# stripped; no other <system-reminder> is touched.
|
|
161
|
+
_REMOTE_CONTROL_RE = re.compile(
|
|
162
|
+
r"\A\s*<system-reminder>Message sent at [^<]*UTC\.</system-reminder>\s*")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _strip_remote_control_prefix(text):
|
|
166
|
+
"""Remove a leading remote-control `Message sent at … UTC.` system-reminder
|
|
167
|
+
block, returning the real user reply. No-op when absent; None/'' pass through."""
|
|
168
|
+
if not text:
|
|
169
|
+
return text
|
|
170
|
+
return _REMOTE_CONTROL_RE.sub("", text, count=1)
|
|
171
|
+
|
|
172
|
+
|
|
68
173
|
_TOOL_RESULT_CAP = 16000 # was 4000; full text always re-derivable from JSONL
|
|
69
174
|
_INPUT_LEAF_CAP = 8000 # max chars per string leaf in a bounded tool input
|
|
70
175
|
_INPUT_TOTAL_CAP = 32000 # honesty backstop on the serialized bounded input
|
|
@@ -192,6 +297,33 @@ def _normalize(obj, t, offset):
|
|
|
192
297
|
# tool_result block still folds as a result.
|
|
193
298
|
entry_type = META
|
|
194
299
|
text = ""
|
|
300
|
+
elif obj.get("isCompactSummary"):
|
|
301
|
+
# #191: compaction summary injected as a user line — authoritative flag.
|
|
302
|
+
entry_type = META
|
|
303
|
+
text = ""
|
|
304
|
+
elif _is_notification_body(text):
|
|
305
|
+
# #191: <task-notification>/<bash-notification> background completion.
|
|
306
|
+
entry_type = META
|
|
307
|
+
text = ""
|
|
308
|
+
elif _is_bash_echo_body(text):
|
|
309
|
+
# #191: <bash-input>/<bash-stdout>/<bash-stderr> `!`-mode echo. Dedicated
|
|
310
|
+
# branch (NOT _MARKER_TAGS) so it can't reach the #188 promotion path.
|
|
311
|
+
entry_type = META
|
|
312
|
+
text = ""
|
|
313
|
+
elif (blocks and all(b["kind"] == "text" for b in blocks)
|
|
314
|
+
and (_inv := _extract_command_invocation(blocks, text)) is not None):
|
|
315
|
+
# #188: a slash-command invocation carrying a real user prompt in
|
|
316
|
+
# <command-args> IS a user turn — the wrapper is plumbing but the args
|
|
317
|
+
# are what the user typed. Promote it (entry_type=HUMAN, text=args) so it
|
|
318
|
+
# enters FTS / title derivation / the entry_type='human' prompts facet.
|
|
319
|
+
# blocks_json is UNTOUCHED (still the raw <command-name>…), so the read
|
|
320
|
+
# path derives the command-name badge from the blocks. Ordered BEFORE the
|
|
321
|
+
# empty-args/stdout-only system-marker fold below — /clear, /exit,
|
|
322
|
+
# /compact and friends (empty args) fall through to META there. The
|
|
323
|
+
# all-text guard mirrors the marker fold so a marker+attachment row is
|
|
324
|
+
# never folded NOR promoted (it stays a plain HUMAN turn in the else).
|
|
325
|
+
entry_type = HUMAN
|
|
326
|
+
text = _inv["args"]
|
|
195
327
|
elif blocks and all(b["kind"] == "text" for b in blocks) and _is_system_marker(text):
|
|
196
328
|
# A slash-command echo carried as a plain user line (NOT isMeta): the
|
|
197
329
|
# user did not type it. `<local-command-stdout>…</local-command-stdout>`
|
|
@@ -206,7 +338,10 @@ def _normalize(obj, t, offset):
|
|
|
206
338
|
entry_type = META
|
|
207
339
|
text = ""
|
|
208
340
|
else:
|
|
341
|
+
# #191: a leading remote-control `Message sent at … UTC.` system-reminder
|
|
342
|
+
# stamp is stripped from the real user reply (narrow). No-op when absent.
|
|
209
343
|
entry_type = HUMAN
|
|
344
|
+
text = _strip_remote_control_prefix(text)
|
|
210
345
|
is_asst = t == "assistant"
|
|
211
346
|
# #177 S6: derive the split search columns on the FINAL post-augment blocks
|
|
212
347
|
# (every _attach_* pass above has already merged bash_stderr / answers /
|
|
@@ -34,10 +34,21 @@ from _lib_conversation import iter_media_items
|
|
|
34
34
|
# here for back-compat so existing `from _lib_conversation_query import
|
|
35
35
|
# _is_system_marker` importers (and the title-skip path below) keep resolving.
|
|
36
36
|
from _lib_conversation import _MARKER_TAGS, _MARKER_RE, _is_system_marker
|
|
37
|
+
# #188: the slash-command-with-args promotion helper. Used at read time to
|
|
38
|
+
# present a legacy/ingested command-marker row carrying a real <command-args>
|
|
39
|
+
# prompt as a "You" turn (text=args, command_name badge). Re-exported for the
|
|
40
|
+
# back-compat import surface + the consumer in _cctally_cache.py.
|
|
41
|
+
from _lib_conversation import _extract_command_invocation
|
|
37
42
|
# #186: read-time ANSI strip for rows already indexed with raw SGR (no forced
|
|
38
43
|
# re-ingest). Scoped to prose/thinking/title/label — NEVER tool_result (Bash
|
|
39
44
|
# AnsiText boundary). Shares the parser's regex so ingest and read-time agree.
|
|
40
45
|
from _lib_conversation import _strip_ansi
|
|
46
|
+
# #191: harness-injected user-line discriminators — defined in the parser,
|
|
47
|
+
# re-exported here so ingest and read-time recovery share one classification.
|
|
48
|
+
from _lib_conversation import (
|
|
49
|
+
_is_compaction_body, _is_notification_body, _is_bash_echo_body,
|
|
50
|
+
_strip_remote_control_prefix,
|
|
51
|
+
)
|
|
41
52
|
|
|
42
53
|
|
|
43
54
|
_TITLE_MAX = 120
|
|
@@ -139,6 +150,12 @@ def _meta_classify(item, allow_human_fallback):
|
|
|
139
150
|
all_text = all(b.get("kind") == "text" for b in (item.get("blocks") or []))
|
|
140
151
|
if _is_system_marker(body) and (is_meta or all_text):
|
|
141
152
|
return ("command", None, body)
|
|
153
|
+
if all_text and _is_compaction_body(body):
|
|
154
|
+
return ("compaction", None, body) # #191
|
|
155
|
+
if all_text and _is_notification_body(body):
|
|
156
|
+
return ("notification", None, body) # #191
|
|
157
|
+
if all_text and _is_bash_echo_body(body):
|
|
158
|
+
return ("command", None, body) # #191 (#5 -> System-marker pill)
|
|
142
159
|
if not is_meta:
|
|
143
160
|
return None
|
|
144
161
|
return ("context", None, body)
|
|
@@ -204,9 +221,12 @@ def _session_titles_map(conn, session_ids):
|
|
|
204
221
|
continue # already resolved to the first non-marker
|
|
205
222
|
if _is_system_marker(text) or _looks_like_command_plumbing(text):
|
|
206
223
|
continue
|
|
224
|
+
if (_is_compaction_body(text) or _is_notification_body(text)
|
|
225
|
+
or _is_bash_echo_body(text)): # #191: never a title
|
|
226
|
+
continue
|
|
207
227
|
if skip_skill_titles and _first_nonblank_line(text).startswith(_SKILL_PREAMBLE):
|
|
208
228
|
continue
|
|
209
|
-
t = _title_from_text(text)
|
|
229
|
+
t = _title_from_text(_strip_remote_control_prefix(text)) # #191: strip the stamp
|
|
210
230
|
if t:
|
|
211
231
|
titles[sid] = t
|
|
212
232
|
return titles
|
|
@@ -646,6 +666,23 @@ def _assemble_session(conn, session_id):
|
|
|
646
666
|
allow_human_fallback = _reingest_pending(conn)
|
|
647
667
|
for it in items:
|
|
648
668
|
if it["kind"] in ("meta", "human"):
|
|
669
|
+
# #188: a slash-command invocation carrying a real prompt in
|
|
670
|
+
# <command-args> is a USER turn. Promote it BEFORE _meta_classify so
|
|
671
|
+
# it never folds into a command-marker pill. Run on the BLOCK-joined
|
|
672
|
+
# text (NOT it["text"]: '' for a legacy META row, args post-migration
|
|
673
|
+
# — neither is the raw marker the command_name parses from). When it
|
|
674
|
+
# yields non-empty args, present kind='human', text=args, and attach
|
|
675
|
+
# the command_name badge (derived from the blocks). Idempotent: a
|
|
676
|
+
# migrated entry_type='human' row re-derives the same badge. /clear &
|
|
677
|
+
# empty-args/stdout markers yield None and fall through to
|
|
678
|
+
# _meta_classify's command/skill/context fold unchanged.
|
|
679
|
+
inv = _extract_command_invocation(
|
|
680
|
+
it.get("blocks"), _join_text_blocks(it.get("blocks")))
|
|
681
|
+
if inv is not None:
|
|
682
|
+
it["kind"] = "human"
|
|
683
|
+
it["text"] = inv["args"]
|
|
684
|
+
it["command_name"] = inv["name"] or None
|
|
685
|
+
continue
|
|
649
686
|
cls = _meta_classify(it, allow_human_fallback)
|
|
650
687
|
if cls is not None:
|
|
651
688
|
meta_kind, skill_name, body = cls
|
|
@@ -654,6 +691,13 @@ def _assemble_session(conn, session_id):
|
|
|
654
691
|
it["skill_name"] = skill_name
|
|
655
692
|
it["text"] = body
|
|
656
693
|
|
|
694
|
+
# #191: strip a leading remote-control `Message sent at … UTC.` stamp from any
|
|
695
|
+
# turn that remains a genuine human reply. Shared assembly -> covers BOTH the
|
|
696
|
+
# reader and the outline. No-op on #188-promoted command turns (no stamp).
|
|
697
|
+
for it in items:
|
|
698
|
+
if it["kind"] == "human" and it.get("text"):
|
|
699
|
+
it["text"] = _strip_remote_control_prefix(it["text"])
|
|
700
|
+
|
|
657
701
|
# ---- Phase 4b: fold a Skill-invoked skill body into its Skill tool chip ----
|
|
658
702
|
# A Skill invocation's injected body (now meta_kind='skill') links to its
|
|
659
703
|
# Skill tool_use via source_tool_use_id (threaded as the internal
|