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.
- {railmux-0.4.0.dev21/src/railmux.egg-info → railmux-0.4.0.dev23}/PKG-INFO +1 -1
- railmux-0.4.0.dev23/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/codex_index.py +13 -2
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_client.py +142 -2
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_server.py +5 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/models.py +9 -3
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_ctl.py +10 -7
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/app.py +56 -79
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_index.py +1 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_lineage.py +26 -22
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fast_full_window.py +193 -6
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_soft_quit.py +5 -9
- railmux-0.4.0.dev21/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/LICENSE +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/README.md +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/pyproject.toml +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/setup.cfg +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev21 → railmux-0.4.0.dev23}/tools/wheel_smoke.py +0 -0
|
@@ -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 =
|
|
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 = (
|
|
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
|
-
|
|
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"{
|
|
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
|
|
75
|
-
#
|
|
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
|
|
1852
|
-
# scrolling.
|
|
1853
|
-
#
|
|
1854
|
-
#
|
|
1855
|
-
#
|
|
1856
|
-
RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-
|
|
1857
|
-
|
|
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
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6879
|
-
raw.get("
|
|
6880
|
-
if trust_codex_history_state else
|
|
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(
|
|
6888
|
-
or
|
|
6889
|
-
or
|
|
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
|
-
|
|
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
|
-
|
|
6904
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6980
|
-
|
|
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["
|
|
7024
|
-
running.
|
|
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.
|
|
7506
|
-
|
|
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
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
#
|
|
9272
|
-
#
|
|
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.
|
|
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.
|
|
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
|
|
9336
|
-
|
|
9337
|
-
restarts;
|
|
9338
|
-
|
|
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.
|
|
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.
|