railmux 0.3.6.dev4__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.
- {railmux-0.3.6.dev4/src/railmux.egg-info → railmux-0.3.6.dev6}/PKG-INFO +4 -2
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/README.md +3 -1
- railmux-0.3.6.dev6/src/railmux/__init__.py +1 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/codex_index.py +13 -2
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fast_display_client.py +114 -25
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fast_display_history.py +6 -9
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/models.py +9 -3
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/tmux_ctl.py +10 -7
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/app.py +56 -79
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/modals.py +47 -25
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6/src/railmux.egg-info}/PKG-INFO +4 -2
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_codex_index.py +1 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_codex_lineage.py +26 -22
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_fast_full_window.py +93 -19
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_modals.py +23 -11
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_soft_quit.py +5 -9
- railmux-0.3.6.dev4/src/railmux/__init__.py +0 -1
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/LICENSE +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/MANIFEST.in +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/pyproject.toml +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/setup.cfg +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/__main__.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/background_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/discovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/favorites.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/launcher.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/local_open.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/modes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/renames.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/self_update.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/session_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/session_lease.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/settings.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_background_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_config_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_discovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_display_transport.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_help_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_keymap.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_launcher.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_local_open.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_modes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_new_project.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_options.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_path_codec.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_release_notes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_remote_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_renames.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_restart_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_running_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_running_sort.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_self_update.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_session_cache.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_session_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_session_lease.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_settings.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_sidebar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_statusbar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_system_deps.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_widgets.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev6}/tools/release_notes.py +0 -0
- {railmux-0.3.6.dev4 → 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.
|
|
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
|
|
@@ -445,7 +445,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
|
445
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
446
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
447
447
|
quit ends all running sessions instead and therefore requires a second
|
|
448
|
-
confirmation
|
|
448
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
449
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
450
|
+
existing confirm meaning in both steps.
|
|
449
451
|
|
|
450
452
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
451
453
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -403,7 +403,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
|
403
403
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
404
404
|
restores the usable workspace when those sessions are still available. A normal
|
|
405
405
|
quit ends all running sessions instead and therefore requires a second
|
|
406
|
-
confirmation
|
|
406
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
407
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
408
|
+
existing confirm meaning in both steps.
|
|
407
409
|
|
|
408
410
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
409
411
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -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 =
|
|
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 = (
|
|
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(
|
|
@@ -39,7 +39,7 @@ from railmux.fast_display_history import (
|
|
|
39
39
|
HistoryAction,
|
|
40
40
|
LocalHistoryView,
|
|
41
41
|
PeriodicPrefetchGate,
|
|
42
|
-
|
|
42
|
+
claim_batched_forwarded_wheel,
|
|
43
43
|
input_may_change_routes,
|
|
44
44
|
)
|
|
45
45
|
from railmux.fast_display_input import (
|
|
@@ -1423,6 +1423,73 @@ class TerminalSurface:
|
|
|
1423
1423
|
self.interaction_active = False
|
|
1424
1424
|
|
|
1425
1425
|
|
|
1426
|
+
@dataclass
|
|
1427
|
+
class _DeferredHistoryPaint:
|
|
1428
|
+
"""Collapse one local-input batch into its final history viewport paint."""
|
|
1429
|
+
|
|
1430
|
+
render_history: bool = False
|
|
1431
|
+
restore_live: bool = False
|
|
1432
|
+
|
|
1433
|
+
def defer(self, action: HistoryAction) -> None:
|
|
1434
|
+
self.render_history = self.render_history or action.render_history
|
|
1435
|
+
self.restore_live = self.restore_live or action.restore_live
|
|
1436
|
+
|
|
1437
|
+
def flush(
|
|
1438
|
+
self,
|
|
1439
|
+
surface: TerminalSurface,
|
|
1440
|
+
screen: AppliedScreen | None,
|
|
1441
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1442
|
+
selection: tuple[SelectionSegment, ...],
|
|
1443
|
+
) -> None:
|
|
1444
|
+
if screen is not None:
|
|
1445
|
+
if self.restore_live:
|
|
1446
|
+
# A viewport may have reached the live bottom and another may
|
|
1447
|
+
# still be frozen. Restore the authoritative frame and compose
|
|
1448
|
+
# only the final overlays in one terminal write.
|
|
1449
|
+
surface.paint(full_repaint(screen), overlays, selection)
|
|
1450
|
+
elif self.render_history:
|
|
1451
|
+
surface.paint_overlays(screen, overlays, selection)
|
|
1452
|
+
self.render_history = False
|
|
1453
|
+
self.restore_live = False
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
class _LocalInputBatch:
|
|
1457
|
+
"""Preserve local wheel distance while bounding forwarded wheel bursts."""
|
|
1458
|
+
|
|
1459
|
+
def __init__(self) -> None:
|
|
1460
|
+
self._forwarded_wheel_directions: set[int] = set()
|
|
1461
|
+
self._history_paint = _DeferredHistoryPaint()
|
|
1462
|
+
|
|
1463
|
+
def prepare(
|
|
1464
|
+
self,
|
|
1465
|
+
event: SgrMouseEvent,
|
|
1466
|
+
action: HistoryAction,
|
|
1467
|
+
) -> tuple[HistoryAction | None, _DeferredHistoryPaint | None]:
|
|
1468
|
+
if event.wheel_direction == 0:
|
|
1469
|
+
return action, None
|
|
1470
|
+
if action.forwarded_input:
|
|
1471
|
+
if not claim_batched_forwarded_wheel(
|
|
1472
|
+
event,
|
|
1473
|
+
self._forwarded_wheel_directions,
|
|
1474
|
+
):
|
|
1475
|
+
return None, None
|
|
1476
|
+
return action, None
|
|
1477
|
+
# Every locally owned history tick has already advanced the viewport.
|
|
1478
|
+
# Defer only its terminal paint so a Windows/RDP read containing
|
|
1479
|
+
# several SGR packets preserves the complete distance without issuing
|
|
1480
|
+
# one synchronous pane repaint per packet.
|
|
1481
|
+
return action, self._history_paint
|
|
1482
|
+
|
|
1483
|
+
def flush(
|
|
1484
|
+
self,
|
|
1485
|
+
surface: TerminalSurface,
|
|
1486
|
+
screen: AppliedScreen | None,
|
|
1487
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1488
|
+
selection: tuple[SelectionSegment, ...],
|
|
1489
|
+
) -> None:
|
|
1490
|
+
self._history_paint.flush(surface, screen, overlays, selection)
|
|
1491
|
+
|
|
1492
|
+
|
|
1426
1493
|
def _remote_server_args(
|
|
1427
1494
|
*,
|
|
1428
1495
|
session: str,
|
|
@@ -2605,22 +2672,28 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2605
2672
|
if frame is not None:
|
|
2606
2673
|
send_protocol_frame(frame)
|
|
2607
2674
|
|
|
2608
|
-
def apply_history_action(
|
|
2675
|
+
def apply_history_action(
|
|
2676
|
+
action: HistoryAction,
|
|
2677
|
+
deferred_paint: _DeferredHistoryPaint | None = None,
|
|
2678
|
+
) -> None:
|
|
2609
2679
|
nonlocal route_refresh_needed
|
|
2610
2680
|
nonlocal history_info_until, claude_history_prompt_input
|
|
2611
2681
|
overlays = history.overlays()
|
|
2612
|
-
if
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2682
|
+
if deferred_paint is not None:
|
|
2683
|
+
deferred_paint.defer(action)
|
|
2684
|
+
else:
|
|
2685
|
+
if action.restore_live and latest_screen is not None:
|
|
2686
|
+
surface.paint(
|
|
2687
|
+
full_repaint(latest_screen),
|
|
2688
|
+
overlays,
|
|
2689
|
+
selection.segments(),
|
|
2690
|
+
)
|
|
2691
|
+
elif action.render_history and latest_screen is not None:
|
|
2692
|
+
surface.paint_overlays(
|
|
2693
|
+
latest_screen,
|
|
2694
|
+
overlays,
|
|
2695
|
+
selection.segments(),
|
|
2696
|
+
)
|
|
2624
2697
|
if action.protocol_frame:
|
|
2625
2698
|
send_protocol_frame(action.protocol_frame)
|
|
2626
2699
|
if action.forwarded_input:
|
|
@@ -2637,7 +2710,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2637
2710
|
|
|
2638
2711
|
def handle_terminal_part(
|
|
2639
2712
|
part: bytes | SgrMouseEvent,
|
|
2640
|
-
|
|
2713
|
+
input_batch: _LocalInputBatch,
|
|
2641
2714
|
) -> None:
|
|
2642
2715
|
nonlocal route_refresh_needed
|
|
2643
2716
|
nonlocal claude_history_prompt_input, claude_history_pending_choice
|
|
@@ -2900,15 +2973,18 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2900
2973
|
selection_changed_rows(selection_before, selection_after),
|
|
2901
2974
|
)
|
|
2902
2975
|
for replay_event in selection_action.replay_events:
|
|
2903
|
-
if not claim_batched_wheel(replay_event, handled_wheels):
|
|
2904
|
-
continue
|
|
2905
2976
|
replay_action = history.pointer_event(
|
|
2906
2977
|
replay_event,
|
|
2907
2978
|
focused_pane_id,
|
|
2908
2979
|
status_row=status_row,
|
|
2909
2980
|
now=time.monotonic(),
|
|
2910
2981
|
)
|
|
2911
|
-
|
|
2982
|
+
replay_action, deferred_paint = input_batch.prepare(
|
|
2983
|
+
replay_event,
|
|
2984
|
+
replay_action,
|
|
2985
|
+
)
|
|
2986
|
+
if replay_action is not None:
|
|
2987
|
+
apply_history_action(replay_action, deferred_paint)
|
|
2912
2988
|
if selection_action.copy_data is not None:
|
|
2913
2989
|
surface.copy_to_clipboard(selection_action.copy_data)
|
|
2914
2990
|
character_count = len(
|
|
@@ -2944,15 +3020,15 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2944
3020
|
begin_path_open(target)
|
|
2945
3021
|
if selection_action.handled:
|
|
2946
3022
|
return
|
|
2947
|
-
if not claim_batched_wheel(part, handled_wheels):
|
|
2948
|
-
return
|
|
2949
3023
|
action = history.pointer_event(
|
|
2950
3024
|
part,
|
|
2951
3025
|
focused_pane_id,
|
|
2952
3026
|
status_row=status_row,
|
|
2953
3027
|
now=time.monotonic(),
|
|
2954
3028
|
)
|
|
2955
|
-
|
|
3029
|
+
action, deferred_paint = input_batch.prepare(part, action)
|
|
3030
|
+
if action is not None:
|
|
3031
|
+
apply_history_action(action, deferred_paint)
|
|
2956
3032
|
return
|
|
2957
3033
|
if not part:
|
|
2958
3034
|
return
|
|
@@ -3327,19 +3403,32 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3327
3403
|
data, emergency_exit = split_local_escape(data)
|
|
3328
3404
|
if emergency_exit:
|
|
3329
3405
|
local_exit = True
|
|
3330
|
-
|
|
3406
|
+
input_batch = _LocalInputBatch()
|
|
3331
3407
|
for part in terminal_input.feed(data):
|
|
3332
3408
|
if isinstance(part, bytes):
|
|
3333
3409
|
for key_part in split_page_key_input(part):
|
|
3334
|
-
handle_terminal_part(key_part,
|
|
3410
|
+
handle_terminal_part(key_part, input_batch)
|
|
3335
3411
|
else:
|
|
3336
|
-
handle_terminal_part(part,
|
|
3412
|
+
handle_terminal_part(part, input_batch)
|
|
3413
|
+
input_batch.flush(
|
|
3414
|
+
surface,
|
|
3415
|
+
latest_screen,
|
|
3416
|
+
history.overlays(),
|
|
3417
|
+
selection.segments(),
|
|
3418
|
+
)
|
|
3337
3419
|
if local_exit:
|
|
3338
3420
|
break
|
|
3339
3421
|
if not local_exit:
|
|
3340
3422
|
for part in terminal_input.flush_pending():
|
|
3423
|
+
input_batch = _LocalInputBatch()
|
|
3341
3424
|
for key_part in split_page_key_input(part):
|
|
3342
|
-
handle_terminal_part(key_part,
|
|
3425
|
+
handle_terminal_part(key_part, input_batch)
|
|
3426
|
+
input_batch.flush(
|
|
3427
|
+
surface,
|
|
3428
|
+
latest_screen,
|
|
3429
|
+
history.overlays(),
|
|
3430
|
+
selection.segments(),
|
|
3431
|
+
)
|
|
3343
3432
|
now = time.monotonic()
|
|
3344
3433
|
keyboard_projected = touch_keyboard.keyboard_projected
|
|
3345
3434
|
if touch_keyboard.expire(now):
|
|
@@ -1021,18 +1021,15 @@ class LocalHistoryView:
|
|
|
1021
1021
|
return was_active
|
|
1022
1022
|
|
|
1023
1023
|
|
|
1024
|
-
def
|
|
1024
|
+
def claim_batched_forwarded_wheel(
|
|
1025
1025
|
event: SgrMouseEvent,
|
|
1026
1026
|
handled_directions: set[int],
|
|
1027
1027
|
) -> bool:
|
|
1028
|
-
"""Admit at most one wheel tick per direction
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
lines at once. Bounding each read preserves the established one-line
|
|
1034
|
-
local scroll feel without using a timing heuristic; a later read remains
|
|
1035
|
-
immediately eligible.
|
|
1028
|
+
"""Admit at most one forwarded wheel tick per direction in one read.
|
|
1029
|
+
|
|
1030
|
+
Remote sidebar/modal owners need one bounded event rather than a stale
|
|
1031
|
+
backlog. Locally owned agent history must not use this helper: it retains
|
|
1032
|
+
the complete reported distance and coalesces only its terminal paint.
|
|
1036
1033
|
"""
|
|
1037
1034
|
direction = event.wheel_direction
|
|
1038
1035
|
if direction == 0:
|
|
@@ -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
|
|
@@ -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
|
|
1843
|
-
# scrolling.
|
|
1844
|
-
#
|
|
1845
|
-
#
|
|
1846
|
-
#
|
|
1847
|
-
RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-
|
|
1848
|
-
|
|
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
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6695
|
-
raw.get("
|
|
6696
|
-
if trust_codex_history_state else
|
|
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(
|
|
6704
|
-
or
|
|
6705
|
-
or
|
|
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
|
-
|
|
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
|
-
|
|
6720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6796
|
-
|
|
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["
|
|
6840
|
-
running.
|
|
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.
|
|
7322
|
-
|
|
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
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
#
|
|
9071
|
-
#
|
|
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.
|
|
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.
|
|
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
|
|
9135
|
-
|
|
9136
|
-
restarts;
|
|
9137
|
-
|
|
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.
|
|
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.
|