Skip to content

Commit ddefa46

Browse files
refactor(jsonl): address parser-split review nits
1 parent b0b712b commit ddefa46

5 files changed

Lines changed: 31 additions & 23 deletions

File tree

docs/architecture.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
▼ ▼ ▼
1616
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
1717
│ session_path │ │ jsonl_parser │ │ exclusion_rules │
18-
│ list_projects │ │ parse_session │ │ load + match │
19-
│ list_sessions │ │ quick_session_info │ └────────┬─────────┘
20-
│ safe_join │ │ _parse_tool_result │ │
18+
│ list_projects │ │ session_peek │ │ load + match │
19+
│ list_sessions │ │ tool_dispatch │ └────────┬─────────┘
20+
│ safe_join │ │ jsonl_helpers │ │
2121
└────────┬────────┘ └──────────┬──────────┘ │
2222
│ │ │
2323
└────────────┬───────────┴────────────────────────┘
@@ -48,7 +48,7 @@
4848
| Layer | Responsibility | Key modules |
4949
|-------|----------------|-------------|
5050
| **Data discovery** | Resolve `~/.claude/projects/`, list projects and sessions, prevent path traversal | `utils/session_path.py` |
51-
| **Parsing** | JSONL → session dict (messages, metadata, tool rendering) | `utils/jsonl_parser.py` |
51+
| **Parsing** | JSONL → session dict (messages, metadata, tool rendering) | `utils/jsonl_parser.py`, `utils/tool_dispatch.py`, `utils/session_peek.py`, `utils/jsonl_helpers.py` |
5252
| **Filtering** | Exclude sensitive sessions via rules file | `utils/exclusion_rules.py` |
5353
| **Statistics** | Aggregates for API and exporters | `utils/session_stats.py` |
5454
| **Export — Markdown** | Session → YAML-frontmatter Markdown | `utils/md_exporter.py` |

utils/jsonl_helpers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def extract_images(content_parts: Any) -> list[dict[str, Any]]:
5151
"data": source["data"],
5252
})
5353
elif part.get("type") == "tool_result":
54+
# Nested content is usually a block list; string content is not normalized here.
5455
nested = part.get("content", [])
5556
if isinstance(nested, list):
5657
for sub in nested:
@@ -64,12 +65,16 @@ def extract_images(content_parts: Any) -> list[dict[str, Any]]:
6465
return images
6566

6667

68+
def first_title_line(text: str, max_chars: int = 100) -> str:
69+
"""First non-empty line after system-tag strip, truncated for session titles."""
70+
return strip_system_tags(text).strip().split("\n")[0][:max_chars]
71+
72+
6773
def infer_title(messages: list[MessageDict]) -> str:
6874
"""Use the first line of the first real user message as the session title."""
6975
for msg in messages:
7076
if msg["role"] == "user" and msg.get("text"):
71-
text = strip_system_tags(msg["text"]).strip()
72-
first_line = text.split("\n")[0][:100]
77+
first_line = first_title_line(msg["text"])
7378
if first_line:
7479
return first_line
7580
return "Untitled Session"

utils/jsonl_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"quick_session_info",
2525
"_parse_tool_result",
2626
"_TOOL_RESULT_DISPATCH",
27+
"_entry_message",
2728
"_process_user",
2829
"_process_assistant",
2930
"_process_system",

utils/session_peek.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55

66
from models.session import QuickSessionInfoDict
7-
from utils.jsonl_helpers import entry_message, extract_text, strip_system_tags
7+
from utils.jsonl_helpers import entry_message, extract_text, first_title_line
88

99
_TAIL_READ_MIN_BYTES = 10 * 1024
1010
_MAX_HEAD_LINES = 80
@@ -48,8 +48,7 @@ def quick_session_info(filepath: str) -> QuickSessionInfoDict:
4848
msg = entry_message(entry)
4949
text = extract_text(msg.get("content", []))
5050
if text:
51-
clean = strip_system_tags(text).strip()
52-
first_line = clean.split("\n")[0][:100]
51+
first_line = first_title_line(text)
5352
if first_line:
5453
title = first_line
5554

@@ -60,6 +59,7 @@ def quick_session_info(filepath: str) -> QuickSessionInfoDict:
6059
with open(filepath, "rb") as f:
6160
f.seek(file_size - chunk_size)
6261
tail = f.read().decode("utf-8", errors="replace")
62+
# First line in tail is often a partial record after seek; json.loads skips it.
6363
# Parse lines in reverse to find latest timestamp
6464
for line in reversed(tail.splitlines()):
6565
line = line.strip()

utils/tool_dispatch.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,25 @@ def _tool_result_pred_file_edit(tr: dict[str, Any]) -> bool:
3737

3838

3939
def _tool_result_build_file_edit(tr: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
40+
# Summary fields only; full blob (e.g. structuredPatch) stays on message tool_result.
4041
result = dict(base)
4142
result["result_type"] = "file_edit"
4243
result["file_path"] = tr.get("filePath", "")
4344
result["replace_all"] = tr.get("replaceAll", False)
4445
return result
4546

4647

48+
def _tool_result_pred_plan(tr: dict[str, Any]) -> bool:
49+
return "plan" in tr and "filePath" in tr
50+
51+
52+
def _tool_result_build_plan(tr: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
53+
result = dict(base)
54+
result["result_type"] = "plan"
55+
result["file_path"] = tr.get("filePath", "")
56+
return result
57+
58+
4759
def _tool_result_pred_file_write(tr: dict[str, Any]) -> bool:
4860
return "filePath" in tr and "content" in tr
4961

@@ -82,7 +94,7 @@ def _tool_result_build_grep(tr: dict[str, Any], base: dict[str, Any]) -> dict[st
8294
result["num_lines"] = tr.get("numLines", 0)
8395
result["duration_ms"] = tr.get("durationMs")
8496
content = tr.get("content", "")
85-
if content and isinstance(content, str):
97+
if isinstance(content, str):
8698
result["content"] = content
8799
return result
88100

@@ -98,7 +110,7 @@ def _tool_result_build_file_read(tr: dict[str, Any], base: dict[str, Any]) -> di
98110
result["file_path"] = file_obj.get("filePath", "")
99111
result["num_lines"] = file_obj.get("numLines")
100112
content = file_obj.get("content", "")
101-
if content and isinstance(content, str):
113+
if isinstance(content, str):
102114
result["content"] = content
103115
return result
104116

@@ -217,17 +229,6 @@ def _tool_result_build_user_input(tr: dict[str, Any], base: dict[str, Any]) -> d
217229
return result
218230

219231

220-
def _tool_result_pred_plan(tr: dict[str, Any]) -> bool:
221-
return "plan" in tr and "filePath" in tr
222-
223-
224-
def _tool_result_build_plan(tr: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
225-
result = dict(base)
226-
result["result_type"] = "plan"
227-
result["file_path"] = tr.get("filePath", "")
228-
return result
229-
230-
231232
# Dispatch registry: **first matching predicate wins** (same as legacy if/elif).
232233
# Order is load-bearing — do not sort alphabetically or “more specific first”
233234
# without replaying tests and real session fixtures.
@@ -241,6 +242,8 @@ def _tool_result_build_plan(tr: dict[str, Any], base: dict[str, Any]) -> dict[st
241242
_TOOL_RESULT_DISPATCH = (
242243
(_tool_result_pred_bash, _tool_result_build_bash),
243244
(_tool_result_pred_file_edit, _tool_result_build_file_edit),
245+
# plan before file_write: plan blobs may also carry filePath + content
246+
(_tool_result_pred_plan, _tool_result_build_plan),
244247
(_tool_result_pred_file_write, _tool_result_build_file_write),
245248
(_tool_result_pred_glob, _tool_result_build_glob),
246249
(_tool_result_pred_grep, _tool_result_build_grep),
@@ -253,7 +256,6 @@ def _tool_result_build_plan(tr: dict[str, Any], base: dict[str, Any]) -> dict[st
253256
(_tool_result_pred_task_async, _tool_result_build_task_async),
254257
(_tool_result_pred_todo_write, _tool_result_build_todo_write),
255258
(_tool_result_pred_user_input, _tool_result_build_user_input),
256-
(_tool_result_pred_plan, _tool_result_build_plan),
257259
)
258260

259261

0 commit comments

Comments
 (0)