railmux 0.4.0.dev21__tar.gz → 0.4.0.dev23__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 (178) hide show
  1. {railmux-0.4.0.dev21/src/railmux.egg-info → railmux-0.4.0.dev23}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev23/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/codex_index.py +13 -2
  4. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_client.py +142 -2
  5. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_server.py +5 -0
  6. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/models.py +9 -3
  7. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_ctl.py +10 -7
  8. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/app.py +56 -79
  9. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23/src/railmux.egg-info}/PKG-INFO +1 -1
  10. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_index.py +1 -0
  11. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_lineage.py +26 -22
  12. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fast_full_window.py +193 -6
  13. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_soft_quit.py +5 -9
  14. railmux-0.4.0.dev21/src/railmux/__init__.py +0 -1
  15. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/LICENSE +0 -0
  16. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/MANIFEST.in +0 -0
  17. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/README.md +0 -0
  18. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/pyproject.toml +0 -0
  19. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/setup.cfg +0 -0
  20. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/__main__.py +0 -0
  21. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/atomic_file.py +0 -0
  22. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/background_index.py +0 -0
  23. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/cli.py +0 -0
  24. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/config.py +0 -0
  25. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/config_cli.py +0 -0
  26. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/diagnostics.py +0 -0
  27. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/discovery.py +0 -0
  28. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/display_transport.py +0 -0
  29. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/entrypoint.py +0 -0
  30. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_history.py +0 -0
  31. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_input.py +0 -0
  32. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_protocol.py +0 -0
  33. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/favorites.py +0 -0
  34. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fuzzy.py +0 -0
  35. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/help_workspace.py +0 -0
  36. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/launcher.py +0 -0
  37. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/legacy_sessions.py +0 -0
  38. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/local_clipboard.py +0 -0
  39. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/local_open.py +0 -0
  40. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/modes.py +0 -0
  41. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/mouse_manager.py +0 -0
  42. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/orphan_marker.py +0 -0
  43. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/pane_surface.py +0 -0
  44. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/path_codec.py +0 -0
  45. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/provider_paths.py +0 -0
  46. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/remote_config.py +0 -0
  47. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/renames.py +0 -0
  48. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/restart_state.py +0 -0
  49. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/runtime_config.py +0 -0
  50. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/scroll_agent.py +0 -0
  51. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/scroll_manager.py +0 -0
  52. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/selection_isolation.py +0 -0
  53. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/self_update.py +0 -0
  54. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_cache.py +0 -0
  55. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_index.py +0 -0
  56. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_lease.py +0 -0
  57. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/setting_contracts.py +0 -0
  58. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/settings.py +0 -0
  59. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_args.py +0 -0
  60. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_compat.py +0 -0
  61. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_display_diagnostics.py +0 -0
  62. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_doctor.py +0 -0
  63. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/system_deps.py +0 -0
  64. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/terminal_status.py +0 -0
  65. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_binding_manager.py +0 -0
  66. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_health.py +0 -0
  67. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_server.py +0 -0
  68. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tool_panes.py +0 -0
  69. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/transcript.py +0 -0
  70. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/__init__.py +0 -0
  71. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/_widgets.py +0 -0
  72. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/keymap.py +0 -0
  73. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/modals.py +0 -0
  74. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/projects_pane.py +0 -0
  75. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/running_pane.py +0 -0
  76. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/sessions_pane.py +0 -0
  77. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/sidebar.py +0 -0
  78. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/statusbar.py +0 -0
  79. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/workspace.py +0 -0
  80. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_attach_relay.py +0 -0
  81. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_bootstrap.py +0 -0
  82. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_install_log.py +0 -0
  83. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_msys2.py +0 -0
  84. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_pacman.py +0 -0
  85. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_tmux_lifecycle.py +0 -0
  86. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/SOURCES.txt +0 -0
  87. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/dependency_links.txt +0 -0
  88. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/entry_points.txt +0 -0
  89. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/requires.txt +0 -0
  90. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/top_level.txt +0 -0
  91. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_agent_workspace.py +0 -0
  92. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_atomic_file.py +0 -0
  93. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_background_index.py +0 -0
  94. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_cli.py +0 -0
  95. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_delete.py +0 -0
  96. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_transcript.py +0 -0
  97. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_config.py +0 -0
  98. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_config_cli.py +0 -0
  99. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_denested_transport_benchmark.py +0 -0
  100. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_diagnostics.py +0 -0
  101. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_discovery.py +0 -0
  102. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_display_transport.py +0 -0
  103. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_focus_highlight.py +0 -0
  104. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fullscreen_binding.py +0 -0
  105. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fuzzy.py +0 -0
  106. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_help_agent.py +0 -0
  107. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_help_workspace.py +0 -0
  108. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_history_cleanup.py +0 -0
  109. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_keymap.py +0 -0
  110. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_launcher.py +0 -0
  111. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_layout_profile.py +0 -0
  112. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_legacy_sessions.py +0 -0
  113. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_local_clipboard.py +0 -0
  114. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_local_open.py +0 -0
  115. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_modals.py +0 -0
  116. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_mode_selection.py +0 -0
  117. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_modes.py +0 -0
  118. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_mouse_manager.py +0 -0
  119. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_new_project.py +0 -0
  120. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_options.py +0 -0
  121. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_orphan_recovery.py +0 -0
  122. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_pane_surface.py +0 -0
  123. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_path_codec.py +0 -0
  124. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_projects_pane.py +0 -0
  125. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_provider_paths.py +0 -0
  126. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_release_notes.py +0 -0
  127. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_release_workflow.py +0 -0
  128. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_remote_config.py +0 -0
  129. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_renames.py +0 -0
  130. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_restart_state.py +0 -0
  131. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_running_pane.py +0 -0
  132. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_running_sort.py +0 -0
  133. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_runtime_config.py +0 -0
  134. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_scroll_agent.py +0 -0
  135. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_scroll_manager.py +0 -0
  136. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_selection_isolation.py +0 -0
  137. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_self_update.py +0 -0
  138. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_cache.py +0 -0
  139. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_index.py +0 -0
  140. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_lease.py +0 -0
  141. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_sessions_pane.py +0 -0
  142. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_setting_contracts.py +0 -0
  143. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_settings.py +0 -0
  144. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_sidebar.py +0 -0
  145. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_args.py +0 -0
  146. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_compat.py +0 -0
  147. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_display_benchmark.py +0 -0
  148. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_display_diagnostics.py +0 -0
  149. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_doctor.py +0 -0
  150. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_status.py +0 -0
  151. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_statusbar.py +0 -0
  152. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_system_deps.py +0 -0
  153. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_terminal_size.py +0 -0
  154. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_terminal_status.py +0 -0
  155. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_termux_local_touch.py +0 -0
  156. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_binding_manager.py +0 -0
  157. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_ctl.py +0 -0
  158. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_health.py +0 -0
  159. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_integration.py +0 -0
  160. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_server.py +0 -0
  161. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_status.py +0 -0
  162. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_attach_relay.py +0 -0
  169. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_bootstrap.py +0 -0
  170. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_msys2.py +0 -0
  172. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_pacman.py +0 -0
  173. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_tmux_lifecycle.py +0 -0
  174. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_wrapper_contract.py +0 -0
  175. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev21
3
+ Version: 0.4.0.dev23
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.4.0.dev23"
@@ -37,7 +37,7 @@ _TOOL_BLOCK_AGE_S = 120
37
37
 
38
38
 
39
39
  FileSignature = tuple[int, int] # (mtime_ns, size)
40
- _PERSISTENT_CACHE_SCHEMA = 1
40
+ _PERSISTENT_CACHE_SCHEMA = 2
41
41
  _PERSISTENT_CACHE_MAX_BYTES = 16 * 1024 * 1024
42
42
  _PERSISTENT_CACHE_MAX_RECORDS = 100_000
43
43
 
@@ -177,6 +177,7 @@ def _cache_session(meta: SessionMeta) -> dict:
177
177
  "pending_tool": meta.pending_tool,
178
178
  "attention": _cache_attention(meta.attention),
179
179
  "forked_from_id": meta.forked_from_id,
180
+ "codex_rollback_count": meta.codex_rollback_count,
180
181
  }
181
182
 
182
183
 
@@ -185,6 +186,7 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
185
186
  "session_id", "cwd", "project_key", "title", "message_count", "token_total",
186
187
  "last_mtime", "size_bytes", "git_branch", "last_user_message",
187
188
  "status", "pending_tool", "attention", "forked_from_id",
189
+ "codex_rollback_count",
188
190
  })
189
191
  if not isinstance(raw, dict) or frozenset(raw) != keys:
190
192
  raise ValueError("invalid cached session")
@@ -213,7 +215,12 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
213
215
  for value, limit, nullable in strings
214
216
  ):
215
217
  raise ValueError("invalid cached session string")
216
- counts = (raw["message_count"], raw["token_total"], raw["size_bytes"])
218
+ counts = (
219
+ raw["message_count"],
220
+ raw["token_total"],
221
+ raw["size_bytes"],
222
+ raw["codex_rollback_count"],
223
+ )
217
224
  if any(
218
225
  not isinstance(value, int)
219
226
  or isinstance(value, bool)
@@ -262,6 +269,7 @@ def _decode_cache_session(path: Path, raw: object) -> SessionMeta:
262
269
  session_type="codex",
263
270
  attention=_decode_cache_attention(raw["attention"]),
264
271
  forked_from_id=forked_from_id,
272
+ codex_rollback_count=raw["codex_rollback_count"],
265
273
  )
266
274
 
267
275
 
@@ -1049,6 +1057,7 @@ def _parse_codex_session(
1049
1057
  # -- scan remaining lines for messages, events -------------------
1050
1058
  title: str | None = None
1051
1059
  message_count = 0
1060
+ rollback_count = 0
1052
1061
  token_total = 0
1053
1062
  first_user_message: str | None = None
1054
1063
  last_user_message: str | None = None
@@ -1166,6 +1175,7 @@ def _parse_codex_session(
1166
1175
  attention = _codex_abort_attention(rec, event_order)
1167
1176
  current_turn_had_error = True
1168
1177
  elif et == "thread_rolled_back":
1178
+ rollback_count += 1
1169
1179
  # task_complete / turn_aborted / thread_rolled_back: the turn is
1170
1180
  # over and any dangling tool calls are dead — clear them so an
1171
1181
  # aborted/rolled-back session never reads as busy/blocked.
@@ -1243,6 +1253,7 @@ def _parse_codex_session(
1243
1253
  session_type="codex",
1244
1254
  attention=attention,
1245
1255
  forked_from_id=forked_from_id,
1256
+ codex_rollback_count=rollback_count,
1246
1257
  )
1247
1258
  if hidden_subagent:
1248
1259
  return HiddenCodexStatus(
@@ -281,6 +281,7 @@ class RemoteHello:
281
281
  config_status: str = "valid"
282
282
  tmux_configured: bool = False
283
283
  config_protocol: int = 0
284
+ platform: str = "posix"
284
285
 
285
286
 
286
287
  @dataclass(frozen=True)
@@ -296,6 +297,11 @@ class RemoteStartKind(Enum):
296
297
  TIMEOUT = "timeout"
297
298
 
298
299
 
300
+ class RemoteLaunchMode(Enum):
301
+ POSIX = "posix"
302
+ DIRECT = "direct"
303
+
304
+
299
305
  class RemoteAttachKind(Enum):
300
306
  ACCEPTED = "accepted"
301
307
  BUSY = "busy"
@@ -369,6 +375,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
369
375
  config_status = value.get("config_status", "valid")
370
376
  tmux_configured = value.get("tmux_configured", False)
371
377
  config_protocol = value.get("config_protocol", 0)
378
+ platform = value.get("platform", "posix")
372
379
  if (
373
380
  not isinstance(version, str)
374
381
  or not version
@@ -383,6 +390,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
383
390
  or not isinstance(config_protocol, int)
384
391
  or isinstance(config_protocol, bool)
385
392
  or not 0 <= config_protocol <= 65535
393
+ or platform not in {"posix", "windows-msys2"}
386
394
  ):
387
395
  raise ValueError("invalid Railmux remote hello")
388
396
  try:
@@ -397,6 +405,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
397
405
  config_status,
398
406
  tmux_configured,
399
407
  config_protocol,
408
+ platform,
400
409
  )
401
410
 
402
411
 
@@ -1540,12 +1549,18 @@ def _remote_launch_command(server_args: Sequence[str]) -> str:
1540
1549
  return "; ".join(branches)
1541
1550
 
1542
1551
 
1552
+ def _remote_direct_launch_command(server_args: Sequence[str]) -> str:
1553
+ """Build a shell-neutral command accepted by Windows OpenSSH PowerShell."""
1554
+ return shlex.join(["railmux", *server_args])
1555
+
1556
+
1543
1557
  def build_remote_command_argv(
1544
1558
  destination: str,
1545
1559
  *,
1546
1560
  remote_args: Sequence[str],
1547
1561
  ssh_args: Sequence[str],
1548
1562
  force_tty: bool = False,
1563
+ launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
1549
1564
  ) -> list[str]:
1550
1565
  """Build SSH argv through the shared remote Railmux discovery ladder."""
1551
1566
  return [
@@ -1556,7 +1571,11 @@ def build_remote_command_argv(
1556
1571
  # user arguments so a copied -t/-T flag cannot invert the transport.
1557
1572
  "-tt" if force_tty else "-T",
1558
1573
  destination,
1559
- _remote_launch_command(remote_args),
1574
+ (
1575
+ _remote_direct_launch_command(remote_args)
1576
+ if launch_mode is RemoteLaunchMode.DIRECT
1577
+ else _remote_launch_command(remote_args)
1578
+ ),
1560
1579
  ]
1561
1580
 
1562
1581
 
@@ -1570,6 +1589,7 @@ def build_ssh_argv(
1570
1589
  ssh_args: Sequence[str],
1571
1590
  replace_existing_client: bool = False,
1572
1591
  existing_session_only: bool = False,
1592
+ launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
1573
1593
  ) -> list[str]:
1574
1594
  server_args = _remote_server_args(
1575
1595
  session=session,
@@ -1592,6 +1612,7 @@ def build_ssh_argv(
1592
1612
  "-o",
1593
1613
  "ServerAliveCountMax=3",
1594
1614
  ),
1615
+ launch_mode=launch_mode,
1595
1616
  )
1596
1617
 
1597
1618
 
@@ -1757,6 +1778,43 @@ def remote_install_help(destination: str, version: str) -> str:
1757
1778
  )
1758
1779
 
1759
1780
 
1781
+ def remote_windows_install_help(destination: str, version: str) -> str:
1782
+ return (
1783
+ f"Update the user-level Windows preview on {destination} from "
1784
+ "PowerShell, then retry:\n"
1785
+ f" py -m pip install --upgrade railmux=={version}\n"
1786
+ " railmux runtime install --yes\n"
1787
+ "The runtime command installs only the matching Railmux app layer and "
1788
+ "reuses a verified managed MSYS2 base when its pinned version is "
1789
+ "unchanged. It does not modify Codex or Claude session files. If 'py' "
1790
+ "is unavailable, use the same Windows Python executable that installed "
1791
+ "the Railmux command."
1792
+ )
1793
+
1794
+
1795
+ def _remote_launch_mode(args: argparse.Namespace) -> RemoteLaunchMode:
1796
+ selected = getattr(args, "_selected_remote_launch_mode", None)
1797
+ if isinstance(selected, RemoteLaunchMode):
1798
+ return selected
1799
+ if getattr(args, "remote_platform", "auto") == "windows":
1800
+ return RemoteLaunchMode.DIRECT
1801
+ return RemoteLaunchMode.POSIX
1802
+
1803
+
1804
+ def _remember_remote_launch_mode(
1805
+ args: argparse.Namespace, mode: RemoteLaunchMode
1806
+ ) -> None:
1807
+ args._selected_remote_launch_mode = mode
1808
+
1809
+
1810
+ def _remote_uses_windows_runtime(
1811
+ args: argparse.Namespace, hello: RemoteHello | None = None
1812
+ ) -> bool:
1813
+ return _remote_launch_mode(args) is RemoteLaunchMode.DIRECT or (
1814
+ hello is not None and hello.platform == "windows-msys2"
1815
+ )
1816
+
1817
+
1760
1818
  def remote_tmux_help(destination: str) -> str:
1761
1819
  return (
1762
1820
  f"tmux is not installed or not on PATH on {destination}. Install it "
@@ -1989,6 +2047,7 @@ def _reconnect_remote_attach(
1989
2047
  ssh_args=ssh_args,
1990
2048
  replace_existing_client=replace_existing_client,
1991
2049
  existing_session_only=existing_session_only,
2050
+ launch_mode=_remote_launch_mode(args),
1992
2051
  )
1993
2052
  process = (
1994
2053
  _spawn_remote(argv, suppress_stderr=True)
@@ -2215,6 +2274,7 @@ def prepare_remote_process(
2215
2274
  if before_interaction is not None:
2216
2275
  before_interaction()
2217
2276
 
2277
+ launch_mode = _remote_launch_mode(args)
2218
2278
  argv = build_ssh_argv(
2219
2279
  args.destination,
2220
2280
  session=args.session,
@@ -2222,11 +2282,62 @@ def prepare_remote_process(
2222
2282
  height=current_size.lines,
2223
2283
  fps=args.fps,
2224
2284
  ssh_args=args.ssh_arg,
2285
+ launch_mode=launch_mode,
2225
2286
  )
2226
2287
  if on_stage is not None:
2227
2288
  on_stage("Connecting to remote host…")
2228
2289
  process = _spawn_remote(argv)
2229
2290
  startup = await_remote_startup(process)
2291
+ if (
2292
+ getattr(args, "remote_platform", "auto") == "auto"
2293
+ and launch_mode is RemoteLaunchMode.POSIX
2294
+ and startup.kind is RemoteStartKind.FAILED
2295
+ and startup.returncode != 255
2296
+ ):
2297
+ _stop_unstarted_remote(process)
2298
+ direct_argv = build_ssh_argv(
2299
+ args.destination,
2300
+ session=args.session,
2301
+ width=current_size.columns,
2302
+ height=current_size.lines,
2303
+ fps=args.fps,
2304
+ ssh_args=args.ssh_arg,
2305
+ launch_mode=RemoteLaunchMode.DIRECT,
2306
+ )
2307
+ process = _spawn_remote(direct_argv)
2308
+ startup = await_remote_startup(process)
2309
+ if startup.kind is RemoteStartKind.HELLO:
2310
+ launch_mode = RemoteLaunchMode.DIRECT
2311
+ _remember_remote_launch_mode(args, launch_mode)
2312
+ else:
2313
+ _stop_unstarted_remote(process)
2314
+ if startup.returncode == 255:
2315
+ raise ProbeError("ssh could not connect to the remote host")
2316
+ if startup.kind is RemoteStartKind.TIMEOUT:
2317
+ raise ProbeError(
2318
+ "timed out while checking the remote host's direct Railmux launcher"
2319
+ )
2320
+ raise ProbeError(
2321
+ "the remote shell did not accept the POSIX discovery command, "
2322
+ "and a direct Railmux launch did not complete. If this is a "
2323
+ "Windows preview host:\n"
2324
+ f"{remote_windows_install_help(args.destination, __version__)}"
2325
+ )
2326
+ elif startup.kind is RemoteStartKind.HELLO:
2327
+ _remember_remote_launch_mode(args, launch_mode)
2328
+
2329
+ if (
2330
+ getattr(args, "remote_platform", "auto") == "windows"
2331
+ and startup.kind is not RemoteStartKind.HELLO
2332
+ ):
2333
+ _stop_unstarted_remote(process)
2334
+ if startup.returncode == 255:
2335
+ raise ProbeError("ssh could not connect to the remote host")
2336
+ if startup.kind is RemoteStartKind.TIMEOUT:
2337
+ raise ProbeError(
2338
+ "timed out waiting for the Windows Railmux compatibility handshake"
2339
+ )
2340
+ raise ProbeError(remote_windows_install_help(args.destination, __version__))
2230
2341
  install_version = __version__
2231
2342
  install_reason: str | None = None
2232
2343
  if startup.kind is RemoteStartKind.MISSING:
@@ -2281,6 +2392,12 @@ def prepare_remote_process(
2281
2392
  if decision.action == "prompt" and decision.prompt == "remote_install":
2282
2393
  assert decision.reason is not None
2283
2394
  assert decision.install_version is not None
2395
+ if _remote_uses_windows_runtime(args, hello):
2396
+ _stop_unstarted_remote(process)
2397
+ raise ProbeError(
2398
+ f"{decision.reason}\n"
2399
+ f"{remote_windows_install_help(args.destination, decision.install_version)}"
2400
+ )
2284
2401
  consents["remote_install"] = _confirm_remote_install(
2285
2402
  args,
2286
2403
  decision.reason,
@@ -2295,9 +2412,14 @@ def prepare_remote_process(
2295
2412
  _stop_unstarted_remote(process)
2296
2413
  message = decision.reason or "incompatible remote Railmux"
2297
2414
  if decision.install_version is not None:
2415
+ install_help = (
2416
+ remote_windows_install_help
2417
+ if _remote_uses_windows_runtime(args, hello)
2418
+ else remote_install_help
2419
+ )
2298
2420
  message = (
2299
2421
  f"{message}\n"
2300
- f"{remote_install_help(args.destination, decision.install_version)}"
2422
+ f"{install_help(args.destination, decision.install_version)}"
2301
2423
  )
2302
2424
  raise ProbeError(message)
2303
2425
  if decision.warning is not None:
@@ -2320,6 +2442,14 @@ def prepare_remote_process(
2320
2442
  raise AssertionError(f"unknown compatibility action: {decision.action}")
2321
2443
 
2322
2444
  assert install_reason is not None
2445
+ if _remote_uses_windows_runtime(
2446
+ args, startup.hello if startup.kind is RemoteStartKind.HELLO else None
2447
+ ):
2448
+ _stop_unstarted_remote(process)
2449
+ raise ProbeError(
2450
+ f"{install_reason}\n"
2451
+ f"{remote_windows_install_help(args.destination, install_version)}"
2452
+ )
2323
2453
  if startup.kind is not RemoteStartKind.HELLO and not _confirm_remote_install(
2324
2454
  args,
2325
2455
  install_reason,
@@ -2430,6 +2560,16 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
2430
2560
  default=20.0,
2431
2561
  help="maximum coalesced display update rate, 1-60 (default: 20)",
2432
2562
  )
2563
+ parser.add_argument(
2564
+ "--remote-platform",
2565
+ choices=("auto", "posix", "windows"),
2566
+ default="auto",
2567
+ help=(
2568
+ "remote shell family: auto tries POSIX discovery then direct "
2569
+ "Windows launch; windows avoids the failed POSIX probe and an "
2570
+ "extra password prompt (default: auto)"
2571
+ ),
2572
+ )
2433
2573
  parser.add_argument(
2434
2574
  "--no-mouse",
2435
2575
  action="store_true",
@@ -248,6 +248,11 @@ def _emit_remote_hello(
248
248
  "config_status": config_status,
249
249
  "tmux_configured": tmux_configured,
250
250
  "config_protocol": REMOTE_CONFIG_PROTOCOL,
251
+ "platform": (
252
+ "windows-msys2"
253
+ if os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
254
+ else "posix"
255
+ ),
251
256
  "version": __version__,
252
257
  },
253
258
  separators=(",", ":"),
@@ -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
@@ -1848,13 +1848,16 @@ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
1848
1848
  # A confirmed branch transition prefixes the opaque SSH history cache epoch.
1849
1849
  # Plain generations use native pane history; only this explicit marker permits
1850
1850
  # the canonical transcript fallback that removes an abandoned rewind suffix.
1851
- # Only the evidence-gated v2 marker authorizes transcript-backed Codex
1852
- # scrolling. The released v1 marker treated every live direct child rollout
1853
- # as a rewind, but Codex can also create that shape while bootstrapping an
1854
- # ordinary resume. Keeping the prefix versioned makes existing over-broad
1855
- # markers fail back to raw pane history on upgrade.
1856
- RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v2:"
1857
- RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX = "canonical:"
1851
+ # Only the provider-rollback-gated v3 marker authorizes transcript-backed Codex
1852
+ # scrolling. Released v1/v2 markers used rollout-shape heuristics, but Codex
1853
+ # also creates identical parent/child rollouts for ordinary continuation and
1854
+ # compaction. Versioning makes both over-broad markers fail back to raw pane
1855
+ # history on upgrade without touching provider files.
1856
+ RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v3:"
1857
+ RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIXES = (
1858
+ "canonical:",
1859
+ "canonical-v2:",
1860
+ )
1858
1861
  # tmux names function keys only through F12. Private restore routes use
1859
1862
  # canonical xterm sequences in literal mode, so they cannot be confused with
1860
1863
  # user input forwarded to an agent pane.
@@ -389,11 +389,10 @@ class _Running:
389
389
  # provider generation lets the refresh path discard only that stale
390
390
  # presentation cache when the exact live writer advances to its child.
391
391
  codex_canonical_session_id: str | None = None
392
- # A direct rollout child is a rewind only after its parent was proved to
393
- # belong to this provider process generation. An explicit Codex resume
394
- # may create a child rollout during bootstrap without rewinding anything.
395
- codex_rollout_proven_in_pane: bool = False
396
- codex_baseline_message_count: int = 0
392
+ # Provider-emitted rollback count when the current rollout was adopted.
393
+ # Parent/child rollout shape is not sufficient: Codex also forks ordinary
394
+ # continuation and compaction checkpoints without a user rewind.
395
+ codex_baseline_rollback_count: int = 0
397
396
  codex_history_generation_stamped: bool = False
398
397
  lease_session_ids: frozenset[str] = frozenset()
399
398
  lease_warning: str | None = None
@@ -6855,8 +6854,7 @@ class App:
6855
6854
  )
6856
6855
 
6857
6856
  canonical_id = None
6858
- rollout_proven = False
6859
- baseline_count = 0
6857
+ baseline_rollback_count = 0
6860
6858
  if session_type == "codex":
6861
6859
  canonical_id = (
6862
6860
  raw.get("codex_canonical_session_id")
@@ -6875,24 +6873,19 @@ class App:
6875
6873
  canonical_uuid = None
6876
6874
  if canonical_uuid != canonical_id.lower():
6877
6875
  canonical_id = None
6878
- rollout_proven = (
6879
- raw.get("codex_rollout_proven_in_pane", False)
6880
- if trust_codex_history_state else False
6881
- )
6882
- baseline_count = (
6883
- raw.get("codex_baseline_message_count", 0)
6884
- if trust_codex_history_state else 0
6876
+ raw_rollback_count = (
6877
+ raw.get("codex_baseline_rollback_count")
6878
+ if trust_codex_history_state else None
6885
6879
  )
6886
- if (
6887
- not isinstance(rollout_proven, bool)
6888
- or not isinstance(baseline_count, int)
6889
- or isinstance(baseline_count, bool)
6890
- or baseline_count < 0
6880
+ if raw_rollback_count is not None and (
6881
+ not isinstance(raw_rollback_count, int)
6882
+ or isinstance(raw_rollback_count, bool)
6883
+ or raw_rollback_count < 0
6891
6884
  ):
6892
6885
  return None
6886
+ baseline_rollback_count = raw_rollback_count or 0
6893
6887
  if canonical_id is None:
6894
- rollout_proven = False
6895
- baseline_count = 0
6888
+ baseline_rollback_count = 0
6896
6889
  meta = self._codex_index.get(key, refresh=False)
6897
6890
  if (
6898
6891
  canonical_id is not None
@@ -6900,8 +6893,16 @@ class App:
6900
6893
  and canonical_id not in self._codex_lineage_ids(key)
6901
6894
  ):
6902
6895
  canonical_id = None
6903
- rollout_proven = False
6904
- baseline_count = 0
6896
+ baseline_rollback_count = 0
6897
+ if canonical_id is not None and raw_rollback_count is None:
6898
+ # Released bindings predate the explicit rollback baseline.
6899
+ # Seed it from that exact indexed generation so inherited old
6900
+ # rollback records cannot authorize a later ordinary child.
6901
+ canonical_meta = self._codex_index.get(
6902
+ canonical_id, refresh=False)
6903
+ if canonical_meta is not None:
6904
+ baseline_rollback_count = (
6905
+ canonical_meta.codex_rollback_count)
6905
6906
  if (meta is not None
6906
6907
  and self._path_key(meta.project.real_path)
6907
6908
  != self._path_key(cwd)):
@@ -6958,8 +6959,7 @@ class App:
6958
6959
  status="busy",
6959
6960
  session_type=session_type,
6960
6961
  codex_canonical_session_id=canonical_id,
6961
- codex_rollout_proven_in_pane=rollout_proven,
6962
- codex_baseline_message_count=baseline_count,
6962
+ codex_baseline_rollback_count=baseline_rollback_count,
6963
6963
  )
6964
6964
  meta = self._codex_representative(key) or meta
6965
6965
  else:
@@ -6976,10 +6976,8 @@ class App:
6976
6976
  session_type=session_type,
6977
6977
  codex_canonical_session_id=(
6978
6978
  canonical_id if session_type == "codex" else None),
6979
- codex_rollout_proven_in_pane=(
6980
- rollout_proven if session_type == "codex" else False),
6981
- codex_baseline_message_count=(
6982
- baseline_count if session_type == "codex" else 0),
6979
+ codex_baseline_rollback_count=(
6980
+ baseline_rollback_count if session_type == "codex" else 0),
6983
6981
  )
6984
6982
 
6985
6983
  def _running_binding_data(
@@ -7020,10 +7018,8 @@ class App:
7020
7018
  if running.codex_canonical_session_id is not None:
7021
7019
  data["codex_canonical_session_id"] = (
7022
7020
  running.codex_canonical_session_id)
7023
- data["codex_rollout_proven_in_pane"] = (
7024
- running.codex_rollout_proven_in_pane)
7025
- data["codex_baseline_message_count"] = (
7026
- running.codex_baseline_message_count)
7021
+ data["codex_baseline_rollback_count"] = (
7022
+ running.codex_baseline_rollback_count)
7027
7023
  return data
7028
7024
 
7029
7025
  def _stamp_running(self, running: _Running) -> bool:
@@ -7502,9 +7498,8 @@ class App:
7502
7498
  ):
7503
7499
  exact = self._codex_exact_meta(marker.session_id)
7504
7500
  running.codex_canonical_session_id = marker.session_id
7505
- running.codex_rollout_proven_in_pane = True
7506
- running.codex_baseline_message_count = (
7507
- exact.message_count if exact is not None else 0)
7501
+ running.codex_baseline_rollback_count = (
7502
+ exact.codex_rollback_count if exact is not None else 0)
7508
7503
  if running is None:
7509
7504
  pre_launch_ids: frozenset[str] = frozenset()
7510
7505
  pre_launch_complete = False
@@ -9207,14 +9202,11 @@ class App:
9207
9202
  the same terminal pane after a rewind. Its abandoned suffix can remain
9208
9203
  in raw tmux scrollback even though the canonical JSONL is correct.
9209
9204
 
9210
- A direct child is not sufficient evidence: Codex may also create one
9211
- while bootstrapping an ordinary ``resume``. Explicit-resume generations
9212
- therefore begin unproved. A child becomes a confirmed rewind only when
9213
- its parent was born/adopted in this pane generation or gained real
9214
- conversation messages after Railmux baselined it. An unproved first
9215
- child is adopted as the resume bootstrap generation without resetting
9216
- the pane or changing raw history format; descendants of that exact open
9217
- child can subsequently be confirmed normally.
9205
+ Parent/child shape is not rewind evidence: Codex uses the same shape
9206
+ for ordinary continuation, context compaction, and resume bootstrap.
9207
+ A child becomes canonical only when the provider's explicit
9208
+ ``thread_rolled_back`` count advanced after Railmux baselined its
9209
+ parent. Missing or ambiguous evidence keeps raw pane history.
9218
9210
  """
9219
9211
  current_id = meta.session_id
9220
9212
  previous_id = running.codex_canonical_session_id
@@ -9227,25 +9219,17 @@ class App:
9227
9219
  ):
9228
9220
  # The first coherent index generation may arrive after Codex
9229
9221
  # has already published its ordinary resume-bootstrap child.
9230
- # Adopt that exact descendant directly; it is now the proved
9231
- # generation for any later branch transition.
9222
+ # Adopt only that exact open descendant as a raw baseline.
9232
9223
  if not self._codex_rollout_is_exact_open(
9233
9224
  running, meta, server, codex_rollout_probes
9234
9225
  ):
9235
9226
  return
9236
- running.codex_rollout_proven_in_pane = True
9237
9227
  running.codex_canonical_session_id = current_id
9238
- running.codex_baseline_message_count = meta.message_count
9228
+ running.codex_baseline_rollback_count = meta.codex_rollback_count
9239
9229
  self._stamp_codex_history_state(running, meta)
9240
9230
  self._stamp_running(running)
9241
9231
  return
9242
9232
  if current_id == previous_id:
9243
- if (
9244
- not running.codex_rollout_proven_in_pane
9245
- and meta.message_count > running.codex_baseline_message_count
9246
- ):
9247
- running.codex_rollout_proven_in_pane = True
9248
- self._stamp_running(running)
9249
9233
  if not running.codex_history_generation_stamped:
9250
9234
  self._stamp_codex_history_state(running, meta)
9251
9235
  return
@@ -9259,22 +9243,19 @@ class App:
9259
9243
  return
9260
9244
 
9261
9245
  previous_meta = self._codex_exact_meta(previous_id)
9262
- parent_gained_conversation = bool(
9263
- previous_meta is not None
9264
- and previous_meta.message_count
9265
- > running.codex_baseline_message_count
9266
- )
9267
- if not (
9268
- running.codex_rollout_proven_in_pane
9269
- or parent_gained_conversation
9270
- ):
9271
- # Ordinary Codex resume bootstrap: the direct child is exact and
9272
- # open in this pane, but the dormant parent never participated in
9273
- # this process generation. Adopt it as the new raw-history baseline
9274
- # and deliberately overwrite any released v1 canonical marker.
9246
+ rollback_count = max(
9247
+ meta.codex_rollback_count,
9248
+ (
9249
+ previous_meta.codex_rollback_count
9250
+ if previous_meta is not None else 0
9251
+ ),
9252
+ )
9253
+ if rollback_count <= running.codex_baseline_rollback_count:
9254
+ # Ordinary continuation/compaction/resume: adopt the exact open
9255
+ # child as a new raw-history baseline. This also overwrites any
9256
+ # released v1/v2 marker that could have misclassified the pane.
9275
9257
  running.codex_canonical_session_id = current_id
9276
- running.codex_baseline_message_count = meta.message_count
9277
- running.codex_rollout_proven_in_pane = True
9258
+ running.codex_baseline_rollback_count = rollback_count
9278
9259
  self._stamp_codex_history_state(running, meta)
9279
9260
  self._stamp_running(running)
9280
9261
  return
@@ -9287,8 +9268,7 @@ class App:
9287
9268
  if self._codex_real_pane_identity(running) is None:
9288
9269
  return
9289
9270
  running.codex_canonical_session_id = current_id
9290
- running.codex_baseline_message_count = meta.message_count
9291
- running.codex_rollout_proven_in_pane = True
9271
+ running.codex_baseline_rollback_count = rollback_count
9292
9272
  self._stamp_codex_history_state(running, meta, canonical=True)
9293
9273
  self._stamp_running(running)
9294
9274
 
@@ -9332,10 +9312,10 @@ class App:
9332
9312
  """Publish Preview source plus the SSH history-source cache epoch.
9333
9313
 
9334
9314
  A plain UUID keeps managed SSH history on raw pane capture. Only a
9335
- confirmed rewind transition writes ``canonical-v2:<uuid>`` and permits
9336
- a transcript projection. Preserve that exact v2 marker across app
9337
- restarts; the released over-broad ``canonical:<uuid>`` marker is
9338
- intentionally replaced with a plain UUID.
9315
+ provider-confirmed rewind writes ``canonical-v3:<uuid>`` and permits a
9316
+ transcript projection. Preserve only that v3 marker across app
9317
+ restarts; released heuristic v1/v2 markers are intentionally replaced
9318
+ with a plain UUID.
9339
9319
  """
9340
9320
  if running.is_legacy:
9341
9321
  return
@@ -9373,8 +9353,6 @@ class App:
9373
9353
  marker == canonical_generation
9374
9354
  for marker in existing_generations.values()
9375
9355
  )
9376
- if effective_canonical:
9377
- running.codex_rollout_proven_in_pane = True
9378
9356
  desired_generation = (
9379
9357
  canonical_generation if effective_canonical else meta.session_id)
9380
9358
  generation_results = [
@@ -9588,8 +9566,7 @@ class App:
9588
9566
  # Placeholder exclusion plus exact rollout correlation proves
9589
9567
  # this generation was born in the pane we launched.
9590
9568
  r.codex_canonical_session_id = candidate.session_id
9591
- r.codex_rollout_proven_in_pane = True
9592
- r.codex_baseline_message_count = candidate.message_count
9569
+ r.codex_baseline_rollback_count = candidate.codex_rollback_count
9593
9570
  self._running[candidate.session_id] = r
9594
9571
  self._stamp_running(r)
9595
9572
  # A new provider session has no UUID to reserve before launch.