railmux 0.3.6.dev5__tar.gz → 0.3.6.dev6__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 (162) hide show
  1. {railmux-0.3.6.dev5/src/railmux.egg-info → railmux-0.3.6.dev6}/PKG-INFO +1 -1
  2. railmux-0.3.6.dev6/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/codex_index.py +13 -2
  4. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/models.py +9 -3
  5. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/tmux_ctl.py +10 -7
  6. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/app.py +56 -79
  7. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6/src/railmux.egg-info}/PKG-INFO +1 -1
  8. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_codex_index.py +1 -0
  9. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_codex_lineage.py +26 -22
  10. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_fast_full_window.py +6 -5
  11. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_soft_quit.py +5 -9
  12. railmux-0.3.6.dev5/src/railmux/__init__.py +0 -1
  13. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/LICENSE +0 -0
  14. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/MANIFEST.in +0 -0
  15. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/README.md +0 -0
  16. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/pyproject.toml +0 -0
  17. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/setup.cfg +0 -0
  18. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/__main__.py +0 -0
  19. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/atomic_file.py +0 -0
  20. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/background_index.py +0 -0
  21. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/cli.py +0 -0
  22. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/config.py +0 -0
  23. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/config_cli.py +0 -0
  24. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/diagnostics.py +0 -0
  25. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/discovery.py +0 -0
  26. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/display_transport.py +0 -0
  27. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fast_display_client.py +0 -0
  28. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fast_display_history.py +0 -0
  29. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fast_display_input.py +0 -0
  30. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fast_display_protocol.py +0 -0
  31. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fast_display_server.py +0 -0
  32. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/favorites.py +0 -0
  33. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/fuzzy.py +0 -0
  34. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/help_workspace.py +0 -0
  35. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/launcher.py +0 -0
  36. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/legacy_sessions.py +0 -0
  37. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/local_clipboard.py +0 -0
  38. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/local_open.py +0 -0
  39. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/modes.py +0 -0
  40. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/remote_config.py +0 -0
  45. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/renames.py +0 -0
  46. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/restart_state.py +0 -0
  47. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/runtime_config.py +0 -0
  48. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/scroll_agent.py +0 -0
  49. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/scroll_manager.py +0 -0
  50. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/selection_isolation.py +0 -0
  51. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/self_update.py +0 -0
  52. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/session_cache.py +0 -0
  53. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/session_index.py +0 -0
  54. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/session_lease.py +0 -0
  55. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/setting_contracts.py +0 -0
  56. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/settings.py +0 -0
  57. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ssh_args.py +0 -0
  58. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ssh_compat.py +0 -0
  59. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
  60. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ssh_doctor.py +0 -0
  61. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/system_deps.py +0 -0
  62. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/terminal_status.py +0 -0
  63. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/tmux_binding_manager.py +0 -0
  64. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/tmux_health.py +0 -0
  65. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/tmux_server.py +0 -0
  66. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/tool_panes.py +0 -0
  67. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/transcript.py +0 -0
  68. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/__init__.py +0 -0
  69. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/_widgets.py +0 -0
  70. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/keymap.py +0 -0
  71. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/modals.py +0 -0
  72. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/projects_pane.py +0 -0
  73. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/running_pane.py +0 -0
  74. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/sessions_pane.py +0 -0
  75. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/sidebar.py +0 -0
  76. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/statusbar.py +0 -0
  77. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux/ui/workspace.py +0 -0
  78. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
  79. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
  80. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
  81. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux.egg-info/requires.txt +0 -0
  82. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/src/railmux.egg-info/top_level.txt +0 -0
  83. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_agent_workspace.py +0 -0
  84. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_atomic_file.py +0 -0
  85. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_background_index.py +0 -0
  86. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_cli.py +0 -0
  87. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_codex_delete.py +0 -0
  88. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_codex_transcript.py +0 -0
  89. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_config.py +0 -0
  90. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_config_cli.py +0 -0
  91. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_denested_transport_benchmark.py +0 -0
  92. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_diagnostics.py +0 -0
  93. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_discovery.py +0 -0
  94. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_display_transport.py +0 -0
  95. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_focus_highlight.py +0 -0
  96. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_fullscreen_binding.py +0 -0
  97. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_fuzzy.py +0 -0
  98. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_help_agent.py +0 -0
  99. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_help_workspace.py +0 -0
  100. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_history_cleanup.py +0 -0
  101. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_keymap.py +0 -0
  102. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_launcher.py +0 -0
  103. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_layout_profile.py +0 -0
  104. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_legacy_sessions.py +0 -0
  105. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_local_clipboard.py +0 -0
  106. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_local_open.py +0 -0
  107. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_modals.py +0 -0
  108. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_mode_selection.py +0 -0
  109. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_modes.py +0 -0
  110. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_mouse_manager.py +0 -0
  111. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_new_project.py +0 -0
  112. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_options.py +0 -0
  113. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_orphan_recovery.py +0 -0
  114. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_pane_surface.py +0 -0
  115. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_path_codec.py +0 -0
  116. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_projects_pane.py +0 -0
  117. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_release_notes.py +0 -0
  118. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_release_workflow.py +0 -0
  119. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_remote_config.py +0 -0
  120. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_renames.py +0 -0
  121. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_restart_state.py +0 -0
  122. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_running_pane.py +0 -0
  123. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_running_sort.py +0 -0
  124. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_runtime_config.py +0 -0
  125. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_scroll_agent.py +0 -0
  126. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_scroll_manager.py +0 -0
  127. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_selection_isolation.py +0 -0
  128. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_self_update.py +0 -0
  129. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_session_cache.py +0 -0
  130. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_session_index.py +0 -0
  131. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_session_lease.py +0 -0
  132. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_sessions_pane.py +0 -0
  133. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_setting_contracts.py +0 -0
  134. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_settings.py +0 -0
  135. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_sidebar.py +0 -0
  136. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_ssh_args.py +0 -0
  137. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_ssh_compat.py +0 -0
  138. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_ssh_display_benchmark.py +0 -0
  139. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
  140. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_ssh_doctor.py +0 -0
  141. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_status.py +0 -0
  142. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_statusbar.py +0 -0
  143. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_system_deps.py +0 -0
  144. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_terminal_size.py +0 -0
  145. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_terminal_status.py +0 -0
  146. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_termux_local_touch.py +0 -0
  147. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_binding_manager.py +0 -0
  148. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_ctl.py +0 -0
  149. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_integration.py +0 -0
  151. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_server.py +0 -0
  152. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tmux_status.py +0 -0
  153. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_tool_panes.py +0 -0
  154. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_transcript.py +0 -0
  155. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_utf8_state.py +0 -0
  156. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_web_demo_recorder.py +0 -0
  157. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_wheel_smoke.py +0 -0
  158. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_widgets.py +0 -0
  159. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tests/test_yolo_prompt.py +0 -0
  160. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tools/denested_transport_benchmark.py +0 -0
  161. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tools/release_notes.py +0 -0
  162. {railmux-0.3.6.dev5 → railmux-0.3.6.dev6}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.6.dev5
3
+ Version: 0.3.6.dev6
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.3.6.dev6"
@@ -36,7 +36,7 @@ _TOOL_BLOCK_AGE_S = 120
36
36
 
37
37
 
38
38
  FileSignature = tuple[int, int] # (mtime_ns, size)
39
- _PERSISTENT_CACHE_SCHEMA = 1
39
+ _PERSISTENT_CACHE_SCHEMA = 2
40
40
  _PERSISTENT_CACHE_MAX_BYTES = 16 * 1024 * 1024
41
41
  _PERSISTENT_CACHE_MAX_RECORDS = 100_000
42
42
 
@@ -175,6 +175,7 @@ def _cache_session(meta: SessionMeta) -> dict:
175
175
  "pending_tool": meta.pending_tool,
176
176
  "attention": _cache_attention(meta.attention),
177
177
  "forked_from_id": meta.forked_from_id,
178
+ "codex_rollback_count": meta.codex_rollback_count,
178
179
  }
179
180
 
180
181
 
@@ -183,6 +184,7 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
183
184
  "session_id", "cwd", "project_key", "title", "message_count", "token_total",
184
185
  "last_mtime", "size_bytes", "git_branch", "last_user_message",
185
186
  "status", "pending_tool", "attention", "forked_from_id",
187
+ "codex_rollback_count",
186
188
  })
187
189
  if not isinstance(raw, dict) or frozenset(raw) != keys:
188
190
  raise ValueError("invalid cached session")
@@ -211,7 +213,12 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
211
213
  for value, limit, nullable in strings
212
214
  ):
213
215
  raise ValueError("invalid cached session string")
214
- counts = (raw["message_count"], raw["token_total"], raw["size_bytes"])
216
+ counts = (
217
+ raw["message_count"],
218
+ raw["token_total"],
219
+ raw["size_bytes"],
220
+ raw["codex_rollback_count"],
221
+ )
215
222
  if any(
216
223
  not isinstance(value, int)
217
224
  or isinstance(value, bool)
@@ -260,6 +267,7 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
260
267
  session_type="codex",
261
268
  attention=_decode_cache_attention(raw["attention"]),
262
269
  forked_from_id=forked_from_id,
270
+ codex_rollback_count=raw["codex_rollback_count"],
263
271
  )
264
272
 
265
273
 
@@ -1047,6 +1055,7 @@ def _parse_codex_session(
1047
1055
  # -- scan remaining lines for messages, events -------------------
1048
1056
  title: str | None = None
1049
1057
  message_count = 0
1058
+ rollback_count = 0
1050
1059
  token_total = 0
1051
1060
  first_user_message: str | None = None
1052
1061
  last_user_message: str | None = None
@@ -1164,6 +1173,7 @@ def _parse_codex_session(
1164
1173
  attention = _codex_abort_attention(rec, event_order)
1165
1174
  current_turn_had_error = True
1166
1175
  elif et == "thread_rolled_back":
1176
+ rollback_count += 1
1167
1177
  # task_complete / turn_aborted / thread_rolled_back: the turn is
1168
1178
  # over and any dangling tool calls are dead — clear them so an
1169
1179
  # aborted/rolled-back session never reads as busy/blocked.
@@ -1241,6 +1251,7 @@ def _parse_codex_session(
1241
1251
  session_type="codex",
1242
1252
  attention=attention,
1243
1253
  forked_from_id=forked_from_id,
1254
+ codex_rollback_count=rollback_count,
1244
1255
  )
1245
1256
  if hidden_subagent:
1246
1257
  return HiddenCodexStatus(
@@ -70,11 +70,17 @@ class SessionMeta:
70
70
  # intentionally independent from ``status`` (conversation activity) and
71
71
  # from tmux/process liveness, which is owned by App's running registry.
72
72
  attention: AttentionState | None = None
73
- # Codex creates a new rollout UUID when a conversation is rewound/forked.
74
- # The parent link lets the Codex index present that chain as one logical
75
- # conversation without changing the provider-owned UUID used for resume.
73
+ # Codex creates a new rollout UUID when a conversation is rewound/forked,
74
+ # resumed, continued, or compacted. The parent link lets the Codex index
75
+ # present that chain as one logical conversation without treating the link
76
+ # itself as proof of why the provider created it.
76
77
  # Always None for Claude sessions and older Codex rollouts.
77
78
  forked_from_id: str | None = None
79
+ # Number of provider-emitted ``thread_rolled_back`` lifecycle records in
80
+ # this Codex rollout. Child rollouts can be ordinary continuation or
81
+ # compaction checkpoints, so the parent link alone is not rewind evidence.
82
+ # Always zero for Claude sessions and Codex versions without this signal.
83
+ codex_rollback_count: int = 0
78
84
  # A Railmux lease observed in the same shared provider history root. This
79
85
  # is a read-time UI overlay, never provider transcript metadata.
80
86
  remote_owner: str | None = None
@@ -1839,13 +1839,16 @@ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
1839
1839
  # A confirmed branch transition prefixes the opaque SSH history cache epoch.
1840
1840
  # Plain generations use native pane history; only this explicit marker permits
1841
1841
  # the canonical transcript fallback that removes an abandoned rewind suffix.
1842
- # Only the evidence-gated v2 marker authorizes transcript-backed Codex
1843
- # scrolling. The released v1 marker treated every live direct child rollout
1844
- # as a rewind, but Codex can also create that shape while bootstrapping an
1845
- # ordinary resume. Keeping the prefix versioned makes existing over-broad
1846
- # markers fail back to raw pane history on upgrade.
1847
- RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v2:"
1848
- RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX = "canonical:"
1842
+ # Only the provider-rollback-gated v3 marker authorizes transcript-backed Codex
1843
+ # scrolling. Released v1/v2 markers used rollout-shape heuristics, but Codex
1844
+ # also creates identical parent/child rollouts for ordinary continuation and
1845
+ # compaction. Versioning makes both over-broad markers fail back to raw pane
1846
+ # history on upgrade without touching provider files.
1847
+ RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v3:"
1848
+ RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIXES = (
1849
+ "canonical:",
1850
+ "canonical-v2:",
1851
+ )
1849
1852
  # tmux names function keys only through F12. Private restore routes use
1850
1853
  # canonical xterm sequences in literal mode, so they cannot be confused with
1851
1854
  # user input forwarded to an agent pane.
@@ -387,11 +387,10 @@ class _Running:
387
387
  # provider generation lets the refresh path discard only that stale
388
388
  # presentation cache when the exact live writer advances to its child.
389
389
  codex_canonical_session_id: str | None = None
390
- # A direct rollout child is a rewind only after its parent was proved to
391
- # belong to this provider process generation. An explicit Codex resume
392
- # may create a child rollout during bootstrap without rewinding anything.
393
- codex_rollout_proven_in_pane: bool = False
394
- codex_baseline_message_count: int = 0
390
+ # Provider-emitted rollback count when the current rollout was adopted.
391
+ # Parent/child rollout shape is not sufficient: Codex also forks ordinary
392
+ # continuation and compaction checkpoints without a user rewind.
393
+ codex_baseline_rollback_count: int = 0
395
394
  codex_history_generation_stamped: bool = False
396
395
  lease_session_ids: frozenset[str] = frozenset()
397
396
  lease_warning: str | None = None
@@ -6671,8 +6670,7 @@ class App:
6671
6670
  )
6672
6671
 
6673
6672
  canonical_id = None
6674
- rollout_proven = False
6675
- baseline_count = 0
6673
+ baseline_rollback_count = 0
6676
6674
  if session_type == "codex":
6677
6675
  canonical_id = (
6678
6676
  raw.get("codex_canonical_session_id")
@@ -6691,24 +6689,19 @@ class App:
6691
6689
  canonical_uuid = None
6692
6690
  if canonical_uuid != canonical_id.lower():
6693
6691
  canonical_id = None
6694
- rollout_proven = (
6695
- raw.get("codex_rollout_proven_in_pane", False)
6696
- if trust_codex_history_state else False
6697
- )
6698
- baseline_count = (
6699
- raw.get("codex_baseline_message_count", 0)
6700
- if trust_codex_history_state else 0
6692
+ raw_rollback_count = (
6693
+ raw.get("codex_baseline_rollback_count")
6694
+ if trust_codex_history_state else None
6701
6695
  )
6702
- if (
6703
- not isinstance(rollout_proven, bool)
6704
- or not isinstance(baseline_count, int)
6705
- or isinstance(baseline_count, bool)
6706
- or baseline_count < 0
6696
+ if raw_rollback_count is not None and (
6697
+ not isinstance(raw_rollback_count, int)
6698
+ or isinstance(raw_rollback_count, bool)
6699
+ or raw_rollback_count < 0
6707
6700
  ):
6708
6701
  return None
6702
+ baseline_rollback_count = raw_rollback_count or 0
6709
6703
  if canonical_id is None:
6710
- rollout_proven = False
6711
- baseline_count = 0
6704
+ baseline_rollback_count = 0
6712
6705
  meta = self._codex_index.get(key, refresh=False)
6713
6706
  if (
6714
6707
  canonical_id is not None
@@ -6716,8 +6709,16 @@ class App:
6716
6709
  and canonical_id not in self._codex_lineage_ids(key)
6717
6710
  ):
6718
6711
  canonical_id = None
6719
- rollout_proven = False
6720
- baseline_count = 0
6712
+ baseline_rollback_count = 0
6713
+ if canonical_id is not None and raw_rollback_count is None:
6714
+ # Released bindings predate the explicit rollback baseline.
6715
+ # Seed it from that exact indexed generation so inherited old
6716
+ # rollback records cannot authorize a later ordinary child.
6717
+ canonical_meta = self._codex_index.get(
6718
+ canonical_id, refresh=False)
6719
+ if canonical_meta is not None:
6720
+ baseline_rollback_count = (
6721
+ canonical_meta.codex_rollback_count)
6721
6722
  if (meta is not None
6722
6723
  and self._path_key(meta.project.real_path)
6723
6724
  != self._path_key(cwd)):
@@ -6774,8 +6775,7 @@ class App:
6774
6775
  status="busy",
6775
6776
  session_type=session_type,
6776
6777
  codex_canonical_session_id=canonical_id,
6777
- codex_rollout_proven_in_pane=rollout_proven,
6778
- codex_baseline_message_count=baseline_count,
6778
+ codex_baseline_rollback_count=baseline_rollback_count,
6779
6779
  )
6780
6780
  meta = self._codex_representative(key) or meta
6781
6781
  else:
@@ -6792,10 +6792,8 @@ class App:
6792
6792
  session_type=session_type,
6793
6793
  codex_canonical_session_id=(
6794
6794
  canonical_id if session_type == "codex" else None),
6795
- codex_rollout_proven_in_pane=(
6796
- rollout_proven if session_type == "codex" else False),
6797
- codex_baseline_message_count=(
6798
- baseline_count if session_type == "codex" else 0),
6795
+ codex_baseline_rollback_count=(
6796
+ baseline_rollback_count if session_type == "codex" else 0),
6799
6797
  )
6800
6798
 
6801
6799
  def _running_binding_data(
@@ -6836,10 +6834,8 @@ class App:
6836
6834
  if running.codex_canonical_session_id is not None:
6837
6835
  data["codex_canonical_session_id"] = (
6838
6836
  running.codex_canonical_session_id)
6839
- data["codex_rollout_proven_in_pane"] = (
6840
- running.codex_rollout_proven_in_pane)
6841
- data["codex_baseline_message_count"] = (
6842
- running.codex_baseline_message_count)
6837
+ data["codex_baseline_rollback_count"] = (
6838
+ running.codex_baseline_rollback_count)
6843
6839
  return data
6844
6840
 
6845
6841
  def _stamp_running(self, running: _Running) -> bool:
@@ -7318,9 +7314,8 @@ class App:
7318
7314
  ):
7319
7315
  exact = self._codex_exact_meta(marker.session_id)
7320
7316
  running.codex_canonical_session_id = marker.session_id
7321
- running.codex_rollout_proven_in_pane = True
7322
- running.codex_baseline_message_count = (
7323
- exact.message_count if exact is not None else 0)
7317
+ running.codex_baseline_rollback_count = (
7318
+ exact.codex_rollback_count if exact is not None else 0)
7324
7319
  if running is None:
7325
7320
  pre_launch_ids: frozenset[str] = frozenset()
7326
7321
  pre_launch_complete = False
@@ -9006,14 +9001,11 @@ class App:
9006
9001
  the same terminal pane after a rewind. Its abandoned suffix can remain
9007
9002
  in raw tmux scrollback even though the canonical JSONL is correct.
9008
9003
 
9009
- A direct child is not sufficient evidence: Codex may also create one
9010
- while bootstrapping an ordinary ``resume``. Explicit-resume generations
9011
- therefore begin unproved. A child becomes a confirmed rewind only when
9012
- its parent was born/adopted in this pane generation or gained real
9013
- conversation messages after Railmux baselined it. An unproved first
9014
- child is adopted as the resume bootstrap generation without resetting
9015
- the pane or changing raw history format; descendants of that exact open
9016
- child can subsequently be confirmed normally.
9004
+ Parent/child shape is not rewind evidence: Codex uses the same shape
9005
+ for ordinary continuation, context compaction, and resume bootstrap.
9006
+ A child becomes canonical only when the provider's explicit
9007
+ ``thread_rolled_back`` count advanced after Railmux baselined its
9008
+ parent. Missing or ambiguous evidence keeps raw pane history.
9017
9009
  """
9018
9010
  current_id = meta.session_id
9019
9011
  previous_id = running.codex_canonical_session_id
@@ -9026,25 +9018,17 @@ class App:
9026
9018
  ):
9027
9019
  # The first coherent index generation may arrive after Codex
9028
9020
  # has already published its ordinary resume-bootstrap child.
9029
- # Adopt that exact descendant directly; it is now the proved
9030
- # generation for any later branch transition.
9021
+ # Adopt only that exact open descendant as a raw baseline.
9031
9022
  if not self._codex_rollout_is_exact_open(
9032
9023
  running, meta, server, codex_rollout_probes
9033
9024
  ):
9034
9025
  return
9035
- running.codex_rollout_proven_in_pane = True
9036
9026
  running.codex_canonical_session_id = current_id
9037
- running.codex_baseline_message_count = meta.message_count
9027
+ running.codex_baseline_rollback_count = meta.codex_rollback_count
9038
9028
  self._stamp_codex_history_state(running, meta)
9039
9029
  self._stamp_running(running)
9040
9030
  return
9041
9031
  if current_id == previous_id:
9042
- if (
9043
- not running.codex_rollout_proven_in_pane
9044
- and meta.message_count > running.codex_baseline_message_count
9045
- ):
9046
- running.codex_rollout_proven_in_pane = True
9047
- self._stamp_running(running)
9048
9032
  if not running.codex_history_generation_stamped:
9049
9033
  self._stamp_codex_history_state(running, meta)
9050
9034
  return
@@ -9058,22 +9042,19 @@ class App:
9058
9042
  return
9059
9043
 
9060
9044
  previous_meta = self._codex_exact_meta(previous_id)
9061
- parent_gained_conversation = bool(
9062
- previous_meta is not None
9063
- and previous_meta.message_count
9064
- > running.codex_baseline_message_count
9065
- )
9066
- if not (
9067
- running.codex_rollout_proven_in_pane
9068
- or parent_gained_conversation
9069
- ):
9070
- # Ordinary Codex resume bootstrap: the direct child is exact and
9071
- # open in this pane, but the dormant parent never participated in
9072
- # this process generation. Adopt it as the new raw-history baseline
9073
- # and deliberately overwrite any released v1 canonical marker.
9045
+ rollback_count = max(
9046
+ meta.codex_rollback_count,
9047
+ (
9048
+ previous_meta.codex_rollback_count
9049
+ if previous_meta is not None else 0
9050
+ ),
9051
+ )
9052
+ if rollback_count <= running.codex_baseline_rollback_count:
9053
+ # Ordinary continuation/compaction/resume: adopt the exact open
9054
+ # child as a new raw-history baseline. This also overwrites any
9055
+ # released v1/v2 marker that could have misclassified the pane.
9074
9056
  running.codex_canonical_session_id = current_id
9075
- running.codex_baseline_message_count = meta.message_count
9076
- running.codex_rollout_proven_in_pane = True
9057
+ running.codex_baseline_rollback_count = rollback_count
9077
9058
  self._stamp_codex_history_state(running, meta)
9078
9059
  self._stamp_running(running)
9079
9060
  return
@@ -9086,8 +9067,7 @@ class App:
9086
9067
  if self._codex_real_pane_identity(running) is None:
9087
9068
  return
9088
9069
  running.codex_canonical_session_id = current_id
9089
- running.codex_baseline_message_count = meta.message_count
9090
- running.codex_rollout_proven_in_pane = True
9070
+ running.codex_baseline_rollback_count = rollback_count
9091
9071
  self._stamp_codex_history_state(running, meta, canonical=True)
9092
9072
  self._stamp_running(running)
9093
9073
 
@@ -9131,10 +9111,10 @@ class App:
9131
9111
  """Publish Preview source plus the SSH history-source cache epoch.
9132
9112
 
9133
9113
  A plain UUID keeps managed SSH history on raw pane capture. Only a
9134
- confirmed rewind transition writes ``canonical-v2:<uuid>`` and permits
9135
- a transcript projection. Preserve that exact v2 marker across app
9136
- restarts; the released over-broad ``canonical:<uuid>`` marker is
9137
- intentionally replaced with a plain UUID.
9114
+ provider-confirmed rewind writes ``canonical-v3:<uuid>`` and permits a
9115
+ transcript projection. Preserve only that v3 marker across app
9116
+ restarts; released heuristic v1/v2 markers are intentionally replaced
9117
+ with a plain UUID.
9138
9118
  """
9139
9119
  if running.is_legacy:
9140
9120
  return
@@ -9172,8 +9152,6 @@ class App:
9172
9152
  marker == canonical_generation
9173
9153
  for marker in existing_generations.values()
9174
9154
  )
9175
- if effective_canonical:
9176
- running.codex_rollout_proven_in_pane = True
9177
9155
  desired_generation = (
9178
9156
  canonical_generation if effective_canonical else meta.session_id)
9179
9157
  generation_results = [
@@ -9387,8 +9365,7 @@ class App:
9387
9365
  # Placeholder exclusion plus exact rollout correlation proves
9388
9366
  # this generation was born in the pane we launched.
9389
9367
  r.codex_canonical_session_id = candidate.session_id
9390
- r.codex_rollout_proven_in_pane = True
9391
- r.codex_baseline_message_count = candidate.message_count
9368
+ r.codex_baseline_rollback_count = candidate.codex_rollback_count
9392
9369
  self._running[candidate.session_id] = r
9393
9370
  self._stamp_running(r)
9394
9371
  # A new provider session has no UUID to reserve before launch.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.6.dev5
3
+ Version: 0.3.6.dev6
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -1037,6 +1037,7 @@ def test_scan_codex_thread_rolled_back_not_busy(tmp_path):
1037
1037
  assert meta is not None
1038
1038
  assert meta.status == "idle"
1039
1039
  assert meta.attention is None
1040
+ assert meta.codex_rollback_count == 1
1040
1041
 
1041
1042
 
1042
1043
  def test_scan_codex_task_started_without_complete_is_busy(tmp_path):
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
  from pathlib import Path
5
5
  from unittest.mock import MagicMock
6
6
 
7
+ import pytest
8
+
7
9
  from railmux import tmux_ctl, tmux_server
8
10
  from railmux.favorites import Favorites
9
11
  from railmux.models import Project, SessionMeta
@@ -80,8 +82,7 @@ def test_live_rewind_preserves_view_and_stamps_canonical_transcript(
80
82
  project=project,
81
83
  session_type="codex",
82
84
  codex_canonical_session_id=root_id,
83
- codex_rollout_proven_in_pane=True,
84
- codex_baseline_message_count=1,
85
+ codex_baseline_rollback_count=0,
85
86
  )
86
87
  meta = SessionMeta(
87
88
  project=project,
@@ -94,6 +95,7 @@ def test_live_rewind_preserves_view_and_stamps_canonical_transcript(
94
95
  last_mtime=2.0,
95
96
  session_type="codex",
96
97
  forked_from_id=root_id,
98
+ codex_rollback_count=1,
97
99
  )
98
100
  identity = tmux_ctl.PaneIdentity(
99
101
  "%9", 123, "railmux", "$4", "@5", False, 80, 24)
@@ -168,7 +170,7 @@ def test_first_codex_generation_only_establishes_scrollback_baseline(
168
170
  app._stamp_running.assert_called_once_with(running)
169
171
 
170
172
 
171
- def test_codex_conversation_proof_is_persisted_only_once(tmp_path) -> None:
173
+ def test_same_codex_generation_does_not_manufacture_rollback(tmp_path) -> None:
172
174
  session_id = "019fc605-5188-7212-bc48-ea023fe8b73c"
173
175
  project = Project(tmp_path, "-project", Path(), 1, 0.0)
174
176
  running = _Running(
@@ -178,7 +180,7 @@ def test_codex_conversation_proof_is_persisted_only_once(tmp_path) -> None:
178
180
  project=project,
179
181
  session_type="codex",
180
182
  codex_canonical_session_id=session_id,
181
- codex_baseline_message_count=1,
183
+ codex_baseline_rollback_count=3,
182
184
  codex_history_generation_stamped=True,
183
185
  )
184
186
  meta = SessionMeta(
@@ -190,6 +192,7 @@ def test_codex_conversation_proof_is_persisted_only_once(tmp_path) -> None:
190
192
  token_total=0,
191
193
  last_mtime=2.0,
192
194
  session_type="codex",
195
+ codex_rollback_count=3,
193
196
  )
194
197
  app = App.__new__(App)
195
198
  app._stamp_running = MagicMock()
@@ -197,8 +200,8 @@ def test_codex_conversation_proof_is_persisted_only_once(tmp_path) -> None:
197
200
  app._sync_codex_rewind_scrollback(running, meta, None, {})
198
201
  app._sync_codex_rewind_scrollback(running, meta, None, {})
199
202
 
200
- assert running.codex_rollout_proven_in_pane
201
- app._stamp_running.assert_called_once_with(running)
203
+ assert running.codex_baseline_rollback_count == 3
204
+ app._stamp_running.assert_not_called()
202
205
 
203
206
 
204
207
  def test_codex_resume_bootstrap_child_keeps_raw_history(
@@ -214,7 +217,7 @@ def test_codex_resume_bootstrap_child_keeps_raw_history(
214
217
  project=project,
215
218
  session_type="codex",
216
219
  codex_canonical_session_id=parent_id,
217
- codex_baseline_message_count=8,
220
+ codex_baseline_rollback_count=4,
218
221
  )
219
222
  parent = SessionMeta(
220
223
  project=project,
@@ -225,6 +228,7 @@ def test_codex_resume_bootstrap_child_keeps_raw_history(
225
228
  token_total=0,
226
229
  last_mtime=1.0,
227
230
  session_type="codex",
231
+ codex_rollback_count=4,
228
232
  )
229
233
  child = SessionMeta(
230
234
  project=project,
@@ -236,6 +240,7 @@ def test_codex_resume_bootstrap_child_keeps_raw_history(
236
240
  last_mtime=2.0,
237
241
  session_type="codex",
238
242
  forked_from_id=parent_id,
243
+ codex_rollback_count=4,
239
244
  )
240
245
  app = App.__new__(App)
241
246
  app._codex_lineage_ids = MagicMock(return_value={parent_id, child_id})
@@ -247,13 +252,12 @@ def test_codex_resume_bootstrap_child_keeps_raw_history(
247
252
  app._sync_codex_rewind_scrollback(running, child, None, {})
248
253
 
249
254
  assert running.codex_canonical_session_id == child_id
250
- assert running.codex_rollout_proven_in_pane
251
- assert running.codex_baseline_message_count == child.message_count
255
+ assert running.codex_baseline_rollback_count == 4
252
256
  app._stamp_codex_history_state.assert_called_once_with(running, child)
253
257
  app._stamp_running.assert_called_once_with(running)
254
258
 
255
259
 
256
- def test_first_observed_resume_child_is_adopted_as_proven_raw_generation(
260
+ def test_first_observed_resume_child_is_adopted_as_raw_generation(
257
261
  tmp_path,
258
262
  ) -> None:
259
263
  parent_id = "019fc605-5188-7212-bc48-ea023fe8b73c"
@@ -286,8 +290,7 @@ def test_first_observed_resume_child_is_adopted_as_proven_raw_generation(
286
290
  app._sync_codex_rewind_scrollback(running, child, None, {})
287
291
 
288
292
  assert running.codex_canonical_session_id == child_id
289
- assert running.codex_rollout_proven_in_pane
290
- assert running.codex_baseline_message_count == child.message_count
293
+ assert running.codex_baseline_rollback_count == 0
291
294
  app._stamp_codex_history_state.assert_called_once_with(running, child)
292
295
 
293
296
 
@@ -324,12 +327,12 @@ def test_first_observed_resume_child_waits_for_exact_live_writer(
324
327
  app._sync_codex_rewind_scrollback(running, child, None, {})
325
328
 
326
329
  assert running.codex_canonical_session_id is None
327
- assert not running.codex_rollout_proven_in_pane
330
+ assert running.codex_baseline_rollback_count == 0
328
331
  app._stamp_codex_history_state.assert_not_called()
329
332
  app._stamp_running.assert_not_called()
330
333
 
331
334
 
332
- def test_codex_parent_activity_confirms_same_poll_rewind(
335
+ def test_codex_provider_rollback_confirms_same_poll_rewind(
333
336
  monkeypatch, tmp_path,
334
337
  ) -> None:
335
338
  parent_id = "019fc605-5188-7212-bc48-ea023fe8b73c"
@@ -342,7 +345,7 @@ def test_codex_parent_activity_confirms_same_poll_rewind(
342
345
  project=project,
343
346
  session_type="codex",
344
347
  codex_canonical_session_id=parent_id,
345
- codex_baseline_message_count=8,
348
+ codex_baseline_rollback_count=7,
346
349
  )
347
350
  parent = SessionMeta(
348
351
  project=project,
@@ -353,6 +356,7 @@ def test_codex_parent_activity_confirms_same_poll_rewind(
353
356
  token_total=0,
354
357
  last_mtime=2.0,
355
358
  session_type="codex",
359
+ codex_rollback_count=8,
356
360
  )
357
361
  child = SessionMeta(
358
362
  project=project,
@@ -364,6 +368,7 @@ def test_codex_parent_activity_confirms_same_poll_rewind(
364
368
  last_mtime=3.0,
365
369
  session_type="codex",
366
370
  forked_from_id=parent_id,
371
+ codex_rollback_count=8,
367
372
  )
368
373
  identity = tmux_ctl.PaneIdentity(
369
374
  "%9", 123, "railmux", "$4", "@5", False, 80, 24)
@@ -466,8 +471,11 @@ def test_codex_history_state_preserves_confirmed_canonical_marker_on_restart(
466
471
  "%9", tmux_server.TRANSCRIPT_SOURCE_OPTION)
467
472
 
468
473
 
469
- def test_codex_history_state_downgrades_released_v1_marker_to_raw(
470
- monkeypatch, tmp_path,
474
+ @pytest.mark.parametrize(
475
+ "legacy_prefix", tmux_ctl.RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIXES
476
+ )
477
+ def test_codex_history_state_downgrades_released_marker_to_raw(
478
+ monkeypatch, tmp_path, legacy_prefix,
471
479
  ) -> None:
472
480
  session_id = "019fc7c1-a27c-7ae0-9937-7570552a112a"
473
481
  project = Project(tmp_path, "-project", Path(), 1, 0.0)
@@ -497,9 +505,7 @@ def test_codex_history_state_downgrades_released_v1_marker_to_raw(
497
505
  monkeypatch.setattr(
498
506
  tmux_ctl,
499
507
  "pane_user_option",
500
- lambda *_args: (
501
- f"{tmux_ctl.RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX}{session_id}"
502
- ),
508
+ lambda *_args: f"{legacy_prefix}{session_id}",
503
509
  )
504
510
 
505
511
  app._stamp_codex_history_state(running, meta)
@@ -507,7 +513,6 @@ def test_codex_history_state_downgrades_released_v1_marker_to_raw(
507
513
  assert stamped.call_count == 2
508
514
  assert stamped.call_args_list[-1].args == (
509
515
  "%9", tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION, session_id)
510
- assert not running.codex_rollout_proven_in_pane
511
516
 
512
517
 
513
518
  def test_codex_history_state_unifies_wrapper_with_existing_canonical_real_pane(
@@ -557,5 +562,4 @@ def test_codex_history_state_unifies_wrapper_with_existing_canonical_real_pane(
557
562
  ]
558
563
  assert [call.args for call in generation_calls] == [(
559
564
  "%10", tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION, canonical)]
560
- assert running.codex_rollout_proven_in_pane
561
565
  assert running.codex_history_generation_stamped
@@ -6818,8 +6818,11 @@ def test_server_accepts_canonical_history_only_for_matching_transcript(
6818
6818
  assert not mismatched[0].canonical_history
6819
6819
 
6820
6820
 
6821
- def test_server_released_v1_canonical_marker_fails_back_to_raw(
6822
- monkeypatch, tmp_path,
6821
+ @pytest.mark.parametrize(
6822
+ "legacy_prefix", tmux_ctl.RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIXES
6823
+ )
6824
+ def test_server_released_canonical_marker_fails_back_to_raw(
6825
+ monkeypatch, tmp_path, legacy_prefix,
6823
6826
  ):
6824
6827
  session_id = "019fc7c1-a27c-7ae0-9937-7570552a112a"
6825
6828
  path = tmp_path / f"rollout-2026-08-04T02-49-33-{session_id}.jsonl"
@@ -6827,9 +6830,7 @@ def test_server_released_v1_canonical_marker_fails_back_to_raw(
6827
6830
  transcript = fast_display_server.tmux_server.encode_transcript_source(
6828
6831
  "codex", session_id, path)
6829
6832
  assert transcript is not None
6830
- generation = (
6831
- f"{tmux_ctl.RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX}{session_id}"
6832
- )
6833
+ generation = f"{legacy_prefix}{session_id}"
6833
6834
  monkeypatch.setattr(
6834
6835
  fast_display_server, "_live_controller", lambda _session: "%1")
6835
6836
  monkeypatch.setattr(