agent-session-bridge 0.2.0__tar.gz → 0.2.2__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.0/src/agent_session_bridge.egg-info → agent_session_bridge-0.2.2}/PKG-INFO +10 -9
  2. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/README.md +6 -5
  3. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/pyproject.toml +4 -4
  4. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2/src/agent_session_bridge.egg-info}/PKG-INFO +10 -9
  5. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/screens.py +21 -2
  6. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/codex.py +23 -0
  7. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/codex_db.py +51 -2
  8. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_codex_db.py +67 -0
  9. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_pilot.py +4 -0
  10. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/LICENSE +0 -0
  11. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/setup.cfg +0 -0
  12. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/agent_session_bridge.egg-info/SOURCES.txt +0 -0
  13. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/agent_session_bridge.egg-info/dependency_links.txt +0 -0
  14. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/agent_session_bridge.egg-info/entry_points.txt +0 -0
  15. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/agent_session_bridge.egg-info/requires.txt +0 -0
  16. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/agent_session_bridge.egg-info/top_level.txt +0 -0
  17. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/__init__.py +0 -0
  18. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/_ids.py +0 -0
  19. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/cli.py +0 -0
  20. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/convert.py +0 -0
  21. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/handshake.py +0 -0
  22. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/ir.py +0 -0
  23. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/place.py +0 -0
  24. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/__init__.py +0 -0
  25. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/_content.py +0 -0
  26. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/_jsonl.py +0 -0
  27. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/_pending.py +0 -0
  28. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/claude_code.py +0 -0
  29. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/codex.py +0 -0
  30. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/readers/hermes.py +0 -0
  31. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/skill_install.py +0 -0
  32. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/skills/session-handoff/SKILL.md +0 -0
  33. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/__init__.py +0 -0
  34. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/actions.py +0 -0
  35. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/app.py +0 -0
  36. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/discovery.py +0 -0
  37. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/options.py +0 -0
  38. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/register.py +0 -0
  39. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/tui/summary.py +0 -0
  40. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/__init__.py +0 -0
  41. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/_common.py +0 -0
  42. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/claude_code.py +0 -0
  43. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/hermes.py +0 -0
  44. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/src/session_bridge/writers/hermes_db.py +0 -0
  45. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_claude_reader.py +0 -0
  46. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_cli_tui.py +0 -0
  47. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_codex_reader.py +0 -0
  48. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_handshake_convert_cli.py +0 -0
  49. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_hermes_db.py +0 -0
  50. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_hermes_reader.py +0 -0
  51. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_ir.py +0 -0
  52. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_place.py +0 -0
  53. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_fixes.py +0 -0
  54. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round11_fixes.py +0 -0
  55. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round12_fixes.py +0 -0
  56. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round13_fixes.py +0 -0
  57. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round14_fixes.py +0 -0
  58. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round16_fixes.py +0 -0
  59. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round17_fixes.py +0 -0
  60. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round18_fixes.py +0 -0
  61. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round18b_fixes.py +0 -0
  62. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round1_fixes.py +0 -0
  63. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round21_fixes.py +0 -0
  64. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round22_fixes.py +0 -0
  65. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round25_fixes.py +0 -0
  66. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round27_fixes.py +0 -0
  67. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round28_fixes.py +0 -0
  68. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round2_fixes.py +0 -0
  69. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round3_fixes.py +0 -0
  70. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round4_fixes.py +0 -0
  71. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round5_fixes.py +0 -0
  72. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round6_fixes.py +0 -0
  73. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round7_fixes.py +0 -0
  74. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round8_fixes.py +0 -0
  75. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_review_round9_fixes.py +0 -0
  76. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_skill_install.py +0 -0
  77. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_stub_open_calls.py +0 -0
  78. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_actions.py +0 -0
  79. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_discovery.py +0 -0
  80. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_options.py +0 -0
  81. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_register.py +0 -0
  82. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_tui_summary.py +0 -0
  83. {agent_session_bridge-0.2.0 → agent_session_bridge-0.2.2}/tests/test_writers.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-session-bridge
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
- Project-URL: Homepage, https://github.com/connectwithprakash/session-bridge
8
- Project-URL: Repository, https://github.com/connectwithprakash/session-bridge
9
- Project-URL: Changelog, https://github.com/connectwithprakash/session-bridge/blob/main/CHANGELOG.md
7
+ Project-URL: Homepage, https://github.com/connectwithprakash/agent-session-bridge
8
+ Project-URL: Repository, https://github.com/connectwithprakash/agent-session-bridge
9
+ Project-URL: Changelog, https://github.com/connectwithprakash/agent-session-bridge/blob/main/CHANGELOG.md
10
10
  Project-URL: Project page, https://www.connectwithprakash.com/project/session-bridge
11
11
  Keywords: ai-agents,claude-code,codex,session,cli,tui
12
12
  Classifier: Development Status :: 4 - Beta
@@ -31,7 +31,7 @@ Dynamic: license-file
31
31
 
32
32
  # session-bridge
33
33
 
34
- [![test](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml)
34
+ [![test](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml)
35
35
  [![PyPI](https://img.shields.io/pypi/v/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
36
36
  [![Python](https://img.shields.io/pypi/pyversions/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
37
37
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -60,7 +60,7 @@ and fail closed (no write) when it doesn't match.
60
60
  | Harness | Last verified against | Verification |
61
61
  |---|---|---|
62
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 |
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
64
  | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
65
65
  | Python | 3.11 – 3.13 | CI test matrix |
66
66
 
@@ -98,9 +98,10 @@ source.jsonl ─▶ reader ─▶ IR (Session: messages, tools, pending) ─▶
98
98
  ## Install
99
99
 
100
100
  ```bash
101
- uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
102
- uvx agent-session-bridge --help # or: no-install run
103
- pip install 'agent-session-bridge[tui]' # or: plain pip
101
+ uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
102
+ brew install connectwithprakash/tap/session-bridge # or: Homebrew (TUI included)
103
+ uvx agent-session-bridge --help # or: no-install run
104
+ pip install 'agent-session-bridge[tui]' # or: plain pip
104
105
  ```
105
106
 
106
107
  Installs the `session-bridge` command (docs use that name); an
@@ -1,6 +1,6 @@
1
1
  # session-bridge
2
2
 
3
- [![test](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml)
3
+ [![test](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
5
5
  [![Python](https://img.shields.io/pypi/pyversions/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -29,7 +29,7 @@ and fail closed (no write) when it doesn't match.
29
29
  | Harness | Last verified against | Verification |
30
30
  |---|---|---|
31
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 |
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
33
  | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
34
34
  | Python | 3.11 – 3.13 | CI test matrix |
35
35
 
@@ -67,9 +67,10 @@ source.jsonl ─▶ reader ─▶ IR (Session: messages, tools, pending) ─▶
67
67
  ## Install
68
68
 
69
69
  ```bash
70
- uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
71
- uvx agent-session-bridge --help # or: no-install run
72
- pip install 'agent-session-bridge[tui]' # or: plain pip
70
+ uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
71
+ brew install connectwithprakash/tap/session-bridge # or: Homebrew (TUI included)
72
+ uvx agent-session-bridge --help # or: no-install run
73
+ pip install 'agent-session-bridge[tui]' # or: plain pip
73
74
  ```
74
75
 
75
76
  Installs the `session-bridge` command (docs use that name); an
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-session-bridge"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -27,9 +27,9 @@ classifiers = [
27
27
  dependencies = []
28
28
 
29
29
  [project.urls]
30
- Homepage = "https://github.com/connectwithprakash/session-bridge"
31
- Repository = "https://github.com/connectwithprakash/session-bridge"
32
- Changelog = "https://github.com/connectwithprakash/session-bridge/blob/main/CHANGELOG.md"
30
+ Homepage = "https://github.com/connectwithprakash/agent-session-bridge"
31
+ Repository = "https://github.com/connectwithprakash/agent-session-bridge"
32
+ Changelog = "https://github.com/connectwithprakash/agent-session-bridge/blob/main/CHANGELOG.md"
33
33
  "Project page" = "https://www.connectwithprakash.com/project/session-bridge"
34
34
 
35
35
  [project.optional-dependencies]
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-session-bridge
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
7
- Project-URL: Homepage, https://github.com/connectwithprakash/session-bridge
8
- Project-URL: Repository, https://github.com/connectwithprakash/session-bridge
9
- Project-URL: Changelog, https://github.com/connectwithprakash/session-bridge/blob/main/CHANGELOG.md
7
+ Project-URL: Homepage, https://github.com/connectwithprakash/agent-session-bridge
8
+ Project-URL: Repository, https://github.com/connectwithprakash/agent-session-bridge
9
+ Project-URL: Changelog, https://github.com/connectwithprakash/agent-session-bridge/blob/main/CHANGELOG.md
10
10
  Project-URL: Project page, https://www.connectwithprakash.com/project/session-bridge
11
11
  Keywords: ai-agents,claude-code,codex,session,cli,tui
12
12
  Classifier: Development Status :: 4 - Beta
@@ -31,7 +31,7 @@ Dynamic: license-file
31
31
 
32
32
  # session-bridge
33
33
 
34
- [![test](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml)
34
+ [![test](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/agent-session-bridge/actions/workflows/test.yml)
35
35
  [![PyPI](https://img.shields.io/pypi/v/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
36
36
  [![Python](https://img.shields.io/pypi/pyversions/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
37
37
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -60,7 +60,7 @@ and fail closed (no write) when it doesn't match.
60
60
  | Harness | Last verified against | Verification |
61
61
  |---|---|---|
62
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 |
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
64
  | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
65
65
  | Python | 3.11 – 3.13 | CI test matrix |
66
66
 
@@ -98,9 +98,10 @@ source.jsonl ─▶ reader ─▶ IR (Session: messages, tools, pending) ─▶
98
98
  ## Install
99
99
 
100
100
  ```bash
101
- uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
102
- uvx agent-session-bridge --help # or: no-install run
103
- pip install 'agent-session-bridge[tui]' # or: plain pip
101
+ uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
102
+ brew install connectwithprakash/tap/session-bridge # or: Homebrew (TUI included)
103
+ uvx agent-session-bridge --help # or: no-install run
104
+ pip install 'agent-session-bridge[tui]' # or: plain pip
104
105
  ```
105
106
 
106
107
  Installs the `session-bridge` command (docs use that name); an
@@ -399,7 +399,9 @@ class DryRunScreen(Screen):
399
399
  self.opts,
400
400
  claude_home=getattr(self.app, "claude_home", None),
401
401
  )
402
- self.app.call_from_thread(self.app.push_screen, ResultScreen(outcome))
402
+ self.app.call_from_thread(
403
+ self.app.push_screen, ResultScreen(outcome, target=self.opts.target)
404
+ )
403
405
 
404
406
 
405
407
  class ResultScreen(Screen):
@@ -410,9 +412,10 @@ class ResultScreen(Screen):
410
412
  ("q", "quit", "Quit"),
411
413
  ]
412
414
 
413
- def __init__(self, outcome) -> None:
415
+ def __init__(self, outcome, target: str | None = None) -> None:
414
416
  super().__init__()
415
417
  self.outcome = outcome
418
+ self.target = target
416
419
 
417
420
  def compose(self) -> ComposeResult:
418
421
  yield Header()
@@ -435,6 +438,22 @@ class ResultScreen(Screen):
435
438
  lines.append(f"\n[b]resume with:[/] {escape(o.resume_hint)}")
436
439
  if o.error:
437
440
  lines.append(f"\n[b red]error:[/] {escape(o.error)}")
441
+ # A converted file alone is not resumable in every harness; say so
442
+ # here, where the user is about to go looking for their session.
443
+ if not o.error and not o.placed_path:
444
+ if self.target in ("codex", "hermes"):
445
+ lines.append(
446
+ f"\n[yellow]heads up:[/] {self.target} lists sessions from its own "
447
+ f"store, so this file will NOT appear in its resume picker. "
448
+ f"To make it resumable, use Register ([b]g[/] on the summary "
449
+ f"screen) instead."
450
+ )
451
+ elif self.target == "claude-code":
452
+ lines.append(
453
+ "\n[yellow]heads up:[/] `claude --resume` only finds sessions "
454
+ "under ~/.claude/projects. Enable placement on the options "
455
+ "screen to make this resumable."
456
+ )
438
457
  lines.append("\n[dim]n for another conversion, q to quit[/]")
439
458
  return "\n".join(lines)
440
459
 
@@ -150,5 +150,28 @@ def write_codex(
150
150
  # Preserve an otherwise-empty message so message count survives the round trip.
151
151
  if not emitted and msg.role in (Role.USER, Role.ASSISTANT, Role.SYSTEM):
152
152
  add(_msg_payload(role, ""), ts)
153
+ # Codex writes an event_msg twin for every user turn, and its resume
154
+ # picker only lists sessions whose rollout contains at least one
155
+ # user_message event — without this the imported session resumes by
156
+ # id but never appears in the picker. The reader ignores event_msg
157
+ # as a response_item duplicate, so round-trips are unaffected.
158
+ if msg.role is Role.USER:
159
+ user_text = msg.text()
160
+ if user_text.strip():
161
+ records.append(
162
+ {
163
+ "timestamp": ts,
164
+ "type": "event_msg",
165
+ "payload": {
166
+ "type": "user_message",
167
+ "message": user_text,
168
+ "images": [],
169
+ "local_images": [],
170
+ "audio": [],
171
+ "local_audio": [],
172
+ "text_elements": [],
173
+ },
174
+ }
175
+ )
153
176
 
154
177
  return records, report
@@ -16,7 +16,9 @@ from __future__ import annotations
16
16
 
17
17
  import json
18
18
  import os
19
+ import shutil
19
20
  import sqlite3
21
+ import subprocess
20
22
  import tempfile
21
23
  import time
22
24
  import uuid
@@ -81,6 +83,43 @@ def validate_codex_store(codex_home: str | Path) -> Path:
81
83
  return db_path
82
84
 
83
85
 
86
+ def infer_codex_cli_version(codex_home: str | Path) -> str:
87
+ """The installed Codex CLI's version, for stamping imported sessions.
88
+
89
+ Asks the binary first (`codex --version` -> "codex-cli 0.145.0"): the
90
+ store's rows are not a trustworthy source because previously imported
91
+ sessions land there too, so inferring from rows can echo an earlier
92
+ import's value back. The newest store row is only the fallback when no
93
+ codex binary is on PATH; empty string for a fresh store, which Codex
94
+ tolerates (the column defaults to '' for pre-column rows).
95
+ """
96
+ exe = shutil.which("codex")
97
+ if exe:
98
+ try:
99
+ out = subprocess.run(
100
+ [exe, "--version"], capture_output=True, text=True, timeout=10
101
+ ).stdout.strip()
102
+ if out:
103
+ return out.split()[-1]
104
+ except (OSError, subprocess.SubprocessError):
105
+ pass
106
+ db_path = validate_codex_store(codex_home)
107
+ conn = sqlite3.connect(db_path.resolve().as_uri() + "?mode=ro", uri=True)
108
+ try:
109
+ row = conn.execute(
110
+ """
111
+ SELECT cli_version
112
+ FROM threads
113
+ WHERE cli_version IS NOT NULL AND cli_version != ''
114
+ ORDER BY recency_at_ms DESC, updated_at_ms DESC, updated_at DESC
115
+ LIMIT 1
116
+ """
117
+ ).fetchone()
118
+ finally:
119
+ conn.close()
120
+ return row[0] if row else ""
121
+
122
+
84
123
  def infer_codex_model(codex_home: str | Path, model_provider: str = "openai") -> str | None:
85
124
  """Return the most recently used model for a configured Codex provider."""
86
125
  db_path = validate_codex_store(codex_home)
@@ -161,6 +200,8 @@ def _thread_values(
161
200
  "tokens_used": 0,
162
201
  "has_user_event": int(bool(first_user_message)),
163
202
  "archived": 0,
203
+ # meta.version here is the TARGET Codex version (the registrar
204
+ # replaces the source harness's with the installed Codex's).
164
205
  "cli_version": session.meta.version or "",
165
206
  "first_user_message": first_user_message,
166
207
  "memory_mode": "enabled",
@@ -221,8 +262,13 @@ def register_codex_session(
221
262
 
222
263
  timestamp = started_at if started_at is not None else time.time()
223
264
  iso_timestamp = datetime.fromtimestamp(timestamp, UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z")
224
- day_path = datetime.fromtimestamp(timestamp, UTC).strftime("%Y/%m/%d")
225
- rollout_path = home / "sessions" / day_path / f"rollout-{iso_timestamp.replace(':', '-')}-{session_id}.jsonl"
265
+ # The resume picker parses rollout filenames strictly: local time,
266
+ # second precision, no timezone suffix matching what Codex itself
267
+ # writes. Record timestamps inside the file stay UTC.
268
+ local = datetime.fromtimestamp(timestamp)
269
+ day_path = local.strftime("%Y/%m/%d")
270
+ file_stamp = local.strftime("%Y-%m-%dT%H-%M-%S")
271
+ rollout_path = home / "sessions" / day_path / f"rollout-{file_stamp}-{session_id}.jsonl"
226
272
  if rollout_path.exists():
227
273
  raise CodexRegistrationError(f"Codex rollout already exists: {rollout_path}")
228
274
 
@@ -232,6 +278,9 @@ def register_codex_session(
232
278
  cwd=resolved_cwd,
233
279
  model=model,
234
280
  model_provider=model_provider,
281
+ # The source harness's version must not masquerade as Codex's;
282
+ # stamp the installed Codex's version observed from its own store.
283
+ version=infer_codex_cli_version(home),
235
284
  )
236
285
  target_session = replace(session, meta=target_meta)
237
286
  transcript_sandbox, index_sandbox = workspace_write_policies(resolved_cwd)
@@ -315,3 +315,70 @@ def test_cli_register_codex_backs_up_and_adds_handshake(tmp_path):
315
315
  ]
316
316
  ) == 0
317
317
  assert len(list(home.glob("state_5.sqlite.session-bridge-backup-*"))) == 2
318
+
319
+
320
+ def test_registration_matches_native_picker_requirements(tmp_path, monkeypatch):
321
+ """The resume picker only lists sessions whose rollout carries an
322
+ event_msg user_message and whose filename matches the native pattern."""
323
+ import re
324
+
325
+ monkeypatch.setattr('session_bridge.writers.codex_db.shutil.which', lambda _: None)
326
+
327
+ home = tmp_path / "codex"
328
+ _make_codex_home(home)
329
+ session = _sample_session()
330
+ rollout = register_codex_session(
331
+ session, home, "1e8400e2-9b1d-4716-a4f6-426655440000",
332
+ cwd=str(tmp_path), title="picker check", model="gpt-5", model_provider="openai",
333
+ )
334
+ # Native filename: local time, no milliseconds/Z suffix.
335
+ assert re.fullmatch(
336
+ r"rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-[0-9a-f-]{36}\.jsonl",
337
+ rollout.name,
338
+ ), rollout.name
339
+ records = [json.loads(line) for line in rollout.read_text().splitlines()]
340
+ user_events = [
341
+ r for r in records
342
+ if r.get("type") == "event_msg" and r["payload"].get("type") == "user_message"
343
+ ]
344
+ assert user_events, "picker requires an event_msg user_message in the rollout"
345
+ assert user_events[0]["payload"]["message"].strip()
346
+ conn = sqlite3.connect(home / "state_5.sqlite")
347
+ row = dict(zip([c[0] for c in conn.execute("SELECT model, first_user_message, preview, cli_version FROM threads").description],
348
+ conn.execute("SELECT model, first_user_message, preview, cli_version FROM threads").fetchone()))
349
+ conn.close()
350
+ assert row["model"] == "gpt-5"
351
+ assert row["first_user_message"].strip()
352
+ assert row["preview"].strip()
353
+ # The source harness's version must not masquerade as Codex's
354
+ # cli_version; a fresh isolated store has no native row to infer from.
355
+ assert row["cli_version"] == ""
356
+
357
+
358
+ def test_cli_version_inferred_from_native_rows(tmp_path, monkeypatch):
359
+ monkeypatch.setattr('session_bridge.writers.codex_db.shutil.which', lambda _: None)
360
+ home = tmp_path / "codex"
361
+ _make_codex_home(home)
362
+ conn = sqlite3.connect(home / "state_5.sqlite")
363
+ conn.execute(
364
+ "INSERT INTO threads (id, rollout_path, created_at, updated_at, source, "
365
+ "model_provider, cwd, title, sandbox_policy, approval_mode, cli_version, "
366
+ "recency_at_ms, updated_at_ms) "
367
+ "VALUES (?, ?, 1, 1, 'cli', 'openai', '/tmp', 'native', '{}', 'on-request', "
368
+ "'0.145.0', 1, 1)",
369
+ (str(uuid.uuid4()), "/tmp/native.jsonl"),
370
+ )
371
+ conn.commit()
372
+ conn.close()
373
+ rollout = register_codex_session(
374
+ _sample_session(), home, "2e8400e2-9b1d-4716-a4f6-426655440000",
375
+ cwd=str(tmp_path), title="version check", model="gpt-5", model_provider="openai",
376
+ )
377
+ conn = sqlite3.connect(home / "state_5.sqlite")
378
+ got = conn.execute(
379
+ "SELECT cli_version FROM threads WHERE id='2e8400e2-9b1d-4716-a4f6-426655440000'"
380
+ ).fetchone()[0]
381
+ conn.close()
382
+ assert got == "0.145.0"
383
+ meta = json.loads(rollout.read_text().splitlines()[0])
384
+ assert meta["payload"]["cli_version"] == "0.145.0"
@@ -138,6 +138,10 @@ def test_convert_flow_end_to_end(tmp_path):
138
138
  assert isinstance(app.screen, ResultScreen)
139
139
  assert app.screen.outcome.error is None
140
140
  assert (out_dir / "converted.jsonl").exists()
141
+ # This flow targets claude-code without placement, so the result
142
+ # screen must warn that the file alone is not resumable.
143
+ body = str(app.screen.query_one("#result-body", Static).render())
144
+ assert "heads up" in body and "Enable placement" in body
141
145
 
142
146
  await pilot.press("n")
143
147
  await pilot.pause(0.2)