agent-session-bridge 0.2.2__tar.gz → 0.2.4__tar.gz

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.
Files changed (83) hide show
  1. {agent_session_bridge-0.2.2/src/agent_session_bridge.egg-info → agent_session_bridge-0.2.4}/PKG-INFO +8 -4
  2. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/README.md +7 -3
  3. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/pyproject.toml +1 -1
  4. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4/src/agent_session_bridge.egg-info}/PKG-INFO +8 -4
  5. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/claude_code.py +16 -1
  6. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/codex.py +28 -0
  7. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_codex_db.py +13 -0
  8. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round11_fixes.py +10 -2
  9. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_writers.py +31 -0
  10. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/LICENSE +0 -0
  11. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/setup.cfg +0 -0
  12. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/agent_session_bridge.egg-info/SOURCES.txt +0 -0
  13. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/agent_session_bridge.egg-info/dependency_links.txt +0 -0
  14. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/agent_session_bridge.egg-info/entry_points.txt +0 -0
  15. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/agent_session_bridge.egg-info/requires.txt +0 -0
  16. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/agent_session_bridge.egg-info/top_level.txt +0 -0
  17. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/__init__.py +0 -0
  18. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/_ids.py +0 -0
  19. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/cli.py +0 -0
  20. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/convert.py +0 -0
  21. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/handshake.py +0 -0
  22. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/ir.py +0 -0
  23. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/place.py +0 -0
  24. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/__init__.py +0 -0
  25. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/_content.py +0 -0
  26. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/_jsonl.py +0 -0
  27. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/_pending.py +0 -0
  28. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/claude_code.py +0 -0
  29. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/codex.py +0 -0
  30. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/readers/hermes.py +0 -0
  31. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/skill_install.py +0 -0
  32. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/skills/session-handoff/SKILL.md +0 -0
  33. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/__init__.py +0 -0
  34. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/actions.py +0 -0
  35. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/app.py +0 -0
  36. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/discovery.py +0 -0
  37. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/options.py +0 -0
  38. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/register.py +0 -0
  39. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/screens.py +0 -0
  40. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/tui/summary.py +0 -0
  41. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/__init__.py +0 -0
  42. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/_common.py +0 -0
  43. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/codex_db.py +0 -0
  44. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/hermes.py +0 -0
  45. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/src/session_bridge/writers/hermes_db.py +0 -0
  46. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_claude_reader.py +0 -0
  47. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_cli_tui.py +0 -0
  48. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_codex_reader.py +0 -0
  49. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_handshake_convert_cli.py +0 -0
  50. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_hermes_db.py +0 -0
  51. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_hermes_reader.py +0 -0
  52. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_ir.py +0 -0
  53. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_place.py +0 -0
  54. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_fixes.py +0 -0
  55. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round12_fixes.py +0 -0
  56. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round13_fixes.py +0 -0
  57. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round14_fixes.py +0 -0
  58. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round16_fixes.py +0 -0
  59. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round17_fixes.py +0 -0
  60. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round18_fixes.py +0 -0
  61. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round18b_fixes.py +0 -0
  62. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round1_fixes.py +0 -0
  63. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round21_fixes.py +0 -0
  64. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round22_fixes.py +0 -0
  65. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round25_fixes.py +0 -0
  66. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round27_fixes.py +0 -0
  67. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round28_fixes.py +0 -0
  68. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round2_fixes.py +0 -0
  69. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round3_fixes.py +0 -0
  70. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round4_fixes.py +0 -0
  71. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round5_fixes.py +0 -0
  72. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round6_fixes.py +0 -0
  73. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round7_fixes.py +0 -0
  74. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round8_fixes.py +0 -0
  75. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_review_round9_fixes.py +0 -0
  76. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_skill_install.py +0 -0
  77. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_stub_open_calls.py +0 -0
  78. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_actions.py +0 -0
  79. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_discovery.py +0 -0
  80. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_options.py +0 -0
  81. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_pilot.py +0 -0
  82. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_register.py +0 -0
  83. {agent_session_bridge-0.2.2 → agent_session_bridge-0.2.4}/tests/test_tui_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-session-bridge
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Local-first cross-harness agent-session portability: export a live coding-agent session from one harness and resume it in another.
5
5
  Author: Prakash Chaudhary
6
6
  License: MIT
@@ -59,11 +59,15 @@ and fail closed (no write) when it doesn't match.
59
59
 
60
60
  | Harness | Last verified against | Verification |
61
61
  |---|---|---|
62
- | Claude Code | 2.1.x transcripts | Round-trip + live `claude --resume` recall of a converted-only fact |
63
- | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of a converted-only sentinel, and the registered session listed in the resume picker |
64
- | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
62
+ | Claude Code | 2.1.212 | Live `claude --resume` recall of user AND assistant sentinels, from claude, codex, and hermes sources |
63
+ | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of both-role sentinels from claude and hermes sources, and registered sessions listed in the resume picker |
64
+ | Hermes | `state.db` schema as of 2026-07-28 | Live `hermes --resume` replay with both-role recall, from claude and codex sources |
65
65
  | Python | 3.11 – 3.13 | CI test matrix |
66
66
 
67
+ All six directed conversion pairs were live-verified on real installs on
68
+ 2026-07-28 (each source harness resumed in each other target, with the model
69
+ quoting both a user turn and one of its own prior replies).
70
+
67
71
  A newer harness version usually still works (formats drift rarely), but treat
68
72
  anything beyond this table as unverified: run `session-bridge inspect` first,
69
73
  and expect SQLite registration to refuse cleanly if the schema moved.
@@ -28,11 +28,15 @@ and fail closed (no write) when it doesn't match.
28
28
 
29
29
  | Harness | Last verified against | Verification |
30
30
  |---|---|---|
31
- | Claude Code | 2.1.x transcripts | Round-trip + live `claude --resume` recall of a converted-only fact |
32
- | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of a converted-only sentinel, and the registered session listed in the resume picker |
33
- | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
31
+ | Claude Code | 2.1.212 | Live `claude --resume` recall of user AND assistant sentinels, from claude, codex, and hermes sources |
32
+ | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of both-role sentinels from claude and hermes sources, and registered sessions listed in the resume picker |
33
+ | Hermes | `state.db` schema as of 2026-07-28 | Live `hermes --resume` replay with both-role recall, from claude and codex sources |
34
34
  | Python | 3.11 – 3.13 | CI test matrix |
35
35
 
36
+ All six directed conversion pairs were live-verified on real installs on
37
+ 2026-07-28 (each source harness resumed in each other target, with the model
38
+ quoting both a user turn and one of its own prior replies).
39
+
36
40
  A newer harness version usually still works (formats drift rarely), but treat
37
41
  anything beyond this table as unverified: run `session-bridge inspect` first,
38
42
  and expect SQLite registration to refuse cleanly if the schema moved.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-session-bridge"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Local-first cross-harness agent-session portability: export a live coding-agent session from one harness and resume it in another."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-session-bridge
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Local-first cross-harness agent-session portability: export a live coding-agent session from one harness and resume it in another.
5
5
  Author: Prakash Chaudhary
6
6
  License: MIT
@@ -59,11 +59,15 @@ and fail closed (no write) when it doesn't match.
59
59
 
60
60
  | Harness | Last verified against | Verification |
61
61
  |---|---|---|
62
- | Claude Code | 2.1.x transcripts | Round-trip + live `claude --resume` recall of a converted-only fact |
63
- | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of a converted-only sentinel, and the registered session listed in the resume picker |
64
- | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
62
+ | Claude Code | 2.1.212 | Live `claude --resume` recall of user AND assistant sentinels, from claude, codex, and hermes sources |
63
+ | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of both-role sentinels from claude and hermes sources, and registered sessions listed in the resume picker |
64
+ | Hermes | `state.db` schema as of 2026-07-28 | Live `hermes --resume` replay with both-role recall, from claude and codex sources |
65
65
  | Python | 3.11 – 3.13 | CI test matrix |
66
66
 
67
+ All six directed conversion pairs were live-verified on real installs on
68
+ 2026-07-28 (each source harness resumed in each other target, with the model
69
+ quoting both a user turn and one of its own prior replies).
70
+
67
71
  A newer harness version usually still works (formats drift rarely), but treat
68
72
  anything beyond this table as unverified: run `session-bridge inspect` first,
69
73
  and expect SQLite registration to refuse cleanly if the schema moved.
@@ -23,7 +23,12 @@ def _assistant_content(msg: Message) -> list[dict[str, Any]]:
23
23
  blocks: list[dict[str, Any]] = []
24
24
  for b in msg.content:
25
25
  if b.type is BlockType.REASONING:
26
- blocks.append({"type": "thinking", "thinking": b.text or "", "signature": ""})
26
+ # An empty reasoning block (a presence marker from sources that
27
+ # store reasoning opaquely, e.g. Hermes codex_reasoning_items) is
28
+ # rejected by the Anthropic API on resume ("each thinking block
29
+ # must contain thinking"), so it cannot be preserved here.
30
+ if (b.text or "").strip():
31
+ blocks.append({"type": "thinking", "thinking": b.text, "signature": ""})
27
32
  elif b.type is BlockType.TEXT:
28
33
  blocks.append({"type": "text", "text": b.text or ""})
29
34
  elif b.type is BlockType.TOOL_CALL:
@@ -78,6 +83,16 @@ def _user_content(msg: Message) -> Any:
78
83
 
79
84
  def write_claude_code(session: Session) -> tuple[list[dict[str, Any]], ConversionReport]:
80
85
  report = report_losses(session, "claude-code")
86
+ if any(
87
+ b.type is BlockType.REASONING and not (b.text or "").strip()
88
+ for m in session.messages
89
+ for b in m.content
90
+ ):
91
+ report.warn(
92
+ "reasoning blocks with no visible text (opaque-source presence "
93
+ "markers) are dropped: the Anthropic API rejects empty thinking "
94
+ "blocks when the transcript is resumed."
95
+ )
81
96
  records: list[dict[str, Any]] = []
82
97
 
83
98
  # Preserve original uids/links when present; else synthesize a linear chain.
@@ -23,6 +23,16 @@ def _codex_role(role: Role) -> str:
23
23
 
24
24
 
25
25
  def _msg_payload(role: str, text: str) -> dict[str, Any]:
26
+ if role == "assistant":
27
+ # Codex's history reconstruction keeps assistant messages by channel
28
+ # phase; without this the resumed model sees only the user turns.
29
+ block_type = "output_text"
30
+ return {
31
+ "type": "message",
32
+ "role": role,
33
+ "content": [{"type": block_type, "text": text}],
34
+ "phase": "final_answer",
35
+ }
26
36
  # Assistant emits output_text; user/system emit input_text.
27
37
  block_type = "output_text" if role == "assistant" else "input_text"
28
38
  return {"type": "message", "role": role, "content": [{"type": block_type, "text": text}]}
@@ -173,5 +183,23 @@ def write_codex(
173
183
  },
174
184
  }
175
185
  )
186
+ elif msg.role is Role.ASSISTANT:
187
+ # Same twin discipline for assistant turns: the transcript view
188
+ # renders from event_msg records, so without agent_message events
189
+ # a resumed session displays as user-messages-only.
190
+ assistant_text = msg.text()
191
+ if assistant_text.strip():
192
+ records.append(
193
+ {
194
+ "timestamp": ts,
195
+ "type": "event_msg",
196
+ "payload": {
197
+ "type": "agent_message",
198
+ "message": assistant_text,
199
+ "phase": "final_answer",
200
+ "memory_citation": None,
201
+ },
202
+ }
203
+ )
176
204
 
177
205
  return records, report
@@ -343,6 +343,19 @@ def test_registration_matches_native_picker_requirements(tmp_path, monkeypatch):
343
343
  ]
344
344
  assert user_events, "picker requires an event_msg user_message in the rollout"
345
345
  assert user_events[0]["payload"]["message"].strip()
346
+ agent_events = [
347
+ r for r in records
348
+ if r.get("type") == "event_msg" and r["payload"].get("type") == "agent_message"
349
+ ]
350
+ assert agent_events, "transcript rendering requires agent_message events"
351
+ assistant_items = [
352
+ r for r in records
353
+ if r.get("type") == "response_item" and r["payload"].get("type") == "message"
354
+ and r["payload"].get("role") == "assistant"
355
+ ]
356
+ assert assistant_items and all(
357
+ i["payload"].get("phase") == "final_answer" for i in assistant_items
358
+ ), "history reconstruction drops assistant messages without a phase"
346
359
  conn = sqlite3.connect(home / "state_5.sqlite")
347
360
  row = dict(zip([c[0] for c in conn.execute("SELECT model, first_user_message, preview, cli_version FROM threads").description],
348
361
  conn.execute("SELECT model, first_user_message, preview, cli_version FROM threads").fetchone()))
@@ -34,7 +34,11 @@ def test_codex_preserves_empty_text_reasoning_block(tmp_path):
34
34
  assert _reasoning_count(back) == 1
35
35
 
36
36
 
37
- def test_empty_reasoning_survives_claude_codex_claude(tmp_path):
37
+ def test_empty_reasoning_dropped_with_disclosure_on_claude_return(tmp_path):
38
+ """Empty reasoning survives INTO codex, but cannot return into a
39
+ claude-code transcript: the Anthropic API rejects empty thinking blocks
40
+ on resume, which would make the whole output unresumable. The drop must
41
+ be disclosed instead."""
38
42
  src = tmp_path / "in.jsonl"
39
43
  src.write_text(
40
44
  json.dumps({
@@ -49,11 +53,15 @@ def test_empty_reasoning_survives_claude_codex_claude(tmp_path):
49
53
  h1 = convert("claude-code", "codex", src, inject_handshake=False)
50
54
  f1 = tmp_path / "codex.jsonl"
51
55
  f1.write_text("\n".join(json.dumps(r) for r in h1.records) + "\n", encoding="utf-8")
56
+ # presence still crosses into codex
57
+ from session_bridge.readers.codex import read_codex
58
+ assert _reasoning_count(read_codex(f1)) == 1
52
59
  h2 = convert("codex", "claude-code", f1, inject_handshake=False)
53
60
  f2 = tmp_path / "back.jsonl"
54
61
  f2.write_text("\n".join(json.dumps(r) for r in h2.records) + "\n", encoding="utf-8")
55
62
  from session_bridge.readers.claude_code import read_claude_code
56
- assert _reasoning_count(read_claude_code(f2)) == 1
63
+ assert _reasoning_count(read_claude_code(f2)) == 0
64
+ assert any("empty thinking" in w for w in h2.report.warnings)
57
65
 
58
66
 
59
67
  def test_hermes_reports_empty_reasoning_loss():
@@ -120,3 +120,34 @@ def test_to_hermes_warns_when_source_has_open_tool_call(tmp_path):
120
120
  assert any("no matching result" in w.lower() or "open tool call" in w.lower()
121
121
  or "pending" in w.lower()
122
122
  for w in report.warnings)
123
+
124
+
125
+ def test_claude_writer_drops_empty_reasoning_and_discloses():
126
+ """Anthropic rejects empty thinking blocks on resume, so presence-marker
127
+ reasoning (e.g. Hermes codex_reasoning_items) cannot reach the output."""
128
+ from session_bridge.ir import ContentBlock, Message, Role, Session, SessionMeta
129
+ from session_bridge.writers.claude_code import write_claude_code
130
+
131
+ session = Session(
132
+ meta=SessionMeta(source_harness="hermes", session_id="s1"),
133
+ messages=(
134
+ Message(role=Role.USER, content=(ContentBlock.text_block("hi"),)),
135
+ Message(
136
+ role=Role.ASSISTANT,
137
+ content=(
138
+ ContentBlock.reasoning(""),
139
+ ContentBlock.text_block("hello"),
140
+ ),
141
+ ),
142
+ ),
143
+ )
144
+ records, report = write_claude_code(session)
145
+ thinking = [
146
+ b
147
+ for r in records
148
+ if isinstance(r.get("message", {}).get("content"), list)
149
+ for b in r["message"]["content"]
150
+ if b.get("type") == "thinking"
151
+ ]
152
+ assert not thinking
153
+ assert any("empty thinking" in w for w in report.warnings)