iCode-TUI 0.29.1__py3-none-any.whl
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.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,2013 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Session startup, restore, reset, fork, deletion, and shutdown orchestration."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import copy
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from dataclasses import dataclass, replace
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
18
|
+
|
|
19
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
20
|
+
from chrys.foundation.config.context import EvalContext
|
|
21
|
+
from chrys.foundation.config.process_settings import reattribute_command_line, route_restart_settings
|
|
22
|
+
from chrys.foundation.config.runtime_pointer import PointerToken, restore_model_pointer, set_model_pointer
|
|
23
|
+
from chrys.foundation.config.settings_store import load_settings
|
|
24
|
+
from chrys.foundation.config.spec import SettingOrigin, Source
|
|
25
|
+
from chrys.foundation.config.warnings import settings_warning_events
|
|
26
|
+
from chrys.foundation.events.types import (
|
|
27
|
+
Error,
|
|
28
|
+
SessionClear,
|
|
29
|
+
SessionDelete,
|
|
30
|
+
SessionDeleted,
|
|
31
|
+
SessionFork,
|
|
32
|
+
SessionForked,
|
|
33
|
+
SessionNew,
|
|
34
|
+
SessionRestore,
|
|
35
|
+
SessionRestored,
|
|
36
|
+
Warning,
|
|
37
|
+
)
|
|
38
|
+
from chrys.foundation.i18n import DisplayBlock, DisplayPath, DisplaySequence, MessageRef, msg
|
|
39
|
+
from chrys.foundation.i18n.formatting import format_message
|
|
40
|
+
from chrys.foundation.models.history_markers import SUB_AGENT_STATE_DISCARDED_MESSAGE, HistoryMarkerKind
|
|
41
|
+
from chrys.foundation.models.workspace import WorkingDir, Workspace
|
|
42
|
+
from chrys.foundation.platform import safe_getcwd
|
|
43
|
+
from chrys.foundation.platform.files import surrogate_safe_text
|
|
44
|
+
from chrys.foundation.trajectory.event_types import RuntimeFinishReason as TrajectoryRuntimeFinishReason
|
|
45
|
+
from chrys.foundation.util.lock import FileLock
|
|
46
|
+
from chrys.foundation.util.session_ids import session_short_id
|
|
47
|
+
from chrys.orchestration.engine.state import lifecycle_permits
|
|
48
|
+
from chrys.orchestration.engine.state.machine import Trigger
|
|
49
|
+
from chrys.orchestration.invoker.contracts import AbortCause
|
|
50
|
+
from chrys.orchestration.invoker.runtime import restore_phase4_state
|
|
51
|
+
from chrys.service.context.compaction.spill import SpillReconciliationResult, reconcile_spill_storage
|
|
52
|
+
from chrys.service.mutations.store import SnapshotPolicy, SnapshotStore
|
|
53
|
+
from chrys.service.mutations.tracker import MutationTracker
|
|
54
|
+
from chrys.service.mutations.workspace_changes import WorkspaceChangeTracker
|
|
55
|
+
from chrys.service.profiles.models.resolver import loaded_with_active_model_profile
|
|
56
|
+
from chrys.service.profiles.models.schema import API_STYLE_RESPONSES, is_model_profile_selectable
|
|
57
|
+
from chrys.service.session.history import stamp_history_item_ids
|
|
58
|
+
from chrys.service.session.persistence import has_real_messages
|
|
59
|
+
from chrys.service.session.runtime_metadata import SessionRuntimeMetadata
|
|
60
|
+
from chrys.service.session.sub_agent_logs import SubAgentSessionArtifactService
|
|
61
|
+
from chrys.service.state.store import (
|
|
62
|
+
SESSION_BACKUP_FILE_NAME,
|
|
63
|
+
SESSION_FILE_NAME,
|
|
64
|
+
SESSION_RECOVERY_FILE_NAME,
|
|
65
|
+
SESSION_WRITE_LOCK_TIMEOUT_SECONDS,
|
|
66
|
+
ChatSessionMeta,
|
|
67
|
+
SessionForkError,
|
|
68
|
+
SessionNotFoundError,
|
|
69
|
+
parse_snapshot_turn,
|
|
70
|
+
)
|
|
71
|
+
from chrys.service.todos.tracker import TodoTracker
|
|
72
|
+
from chrys.service.trajectory.preparation import PreparationOutcome
|
|
73
|
+
|
|
74
|
+
if TYPE_CHECKING:
|
|
75
|
+
from chrys.foundation.config.settings_store import LoadedSettings, SettingsHandle
|
|
76
|
+
from chrys.foundation.events.bus import EventBus
|
|
77
|
+
from chrys.orchestration.engine.loader import AgentLoader
|
|
78
|
+
from chrys.orchestration.engine.run.turn_state import TurnRuntimeState
|
|
79
|
+
from chrys.orchestration.engine.state.active_session import ActiveSession
|
|
80
|
+
from chrys.orchestration.engine.state.current_agent import CurrentAgent
|
|
81
|
+
from chrys.orchestration.engine.state.lifecycle_permits import LifecyclePermits
|
|
82
|
+
from chrys.orchestration.engine.state.machine import EngineStateMachine
|
|
83
|
+
from chrys.orchestration.engine.state.session_writer import SessionWriter
|
|
84
|
+
from chrys.orchestration.engine.trajectory import TrajectoryRecorder
|
|
85
|
+
from chrys.orchestration.engine.usage import UsagePublisher
|
|
86
|
+
from chrys.orchestration.invoker.kernel import KernelConversation
|
|
87
|
+
from chrys.service.profiles.agents.registry import AgentProfileRegistry
|
|
88
|
+
from chrys.service.profiles.agents.schema import AgentProfile
|
|
89
|
+
from chrys.service.profiles.models.registry import ModelProfileRegistry
|
|
90
|
+
from chrys.service.session.history import SessionHistoryManager
|
|
91
|
+
from chrys.service.session.persistence import SessionPersistence
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
_SHUTDOWN_POST_RUN_TIMEOUT_SECONDS = 2.0
|
|
96
|
+
_SESSION_FORK_AGENT_LOAD_TIMEOUT_SECONDS = 5.0
|
|
97
|
+
|
|
98
|
+
_FORK_AGENT_LOADING = msg(
|
|
99
|
+
"engine.fork_agent_loading",
|
|
100
|
+
fallback="Cannot fork while the agent is still loading.",
|
|
101
|
+
)
|
|
102
|
+
_FORK_NOT_READY = msg(
|
|
103
|
+
"engine.fork_not_ready",
|
|
104
|
+
fallback="Cannot fork before a session is ready.",
|
|
105
|
+
)
|
|
106
|
+
_FORK_SESSION_CHANGED = msg(
|
|
107
|
+
"engine.fork_session_changed",
|
|
108
|
+
fallback="Cannot fork because the active session changed.",
|
|
109
|
+
)
|
|
110
|
+
_FORK_TURN_ACTIVE = msg(
|
|
111
|
+
"engine.fork_turn_active",
|
|
112
|
+
fallback="Cannot fork while a turn is running.",
|
|
113
|
+
)
|
|
114
|
+
_FORK_STATE_STORE_MISSING = msg(
|
|
115
|
+
"engine.fork_state_store_missing",
|
|
116
|
+
fallback="State store not configured.",
|
|
117
|
+
)
|
|
118
|
+
_FORK_RESTORING = msg(
|
|
119
|
+
"engine.fork_restoring",
|
|
120
|
+
fallback="Cannot fork while session state is being restored.",
|
|
121
|
+
)
|
|
122
|
+
_FORK_LOCK_NOT_OWNED = msg(
|
|
123
|
+
"engine.fork_lock_not_owned",
|
|
124
|
+
fallback="Cannot fork because this window does not own the active session lock.",
|
|
125
|
+
)
|
|
126
|
+
_FORK_EMPTY_SESSION = msg(
|
|
127
|
+
"engine.fork_empty_session",
|
|
128
|
+
fallback="Cannot fork an empty session.",
|
|
129
|
+
)
|
|
130
|
+
_FORK_PREPARE_TIMEOUT = msg(
|
|
131
|
+
"engine.fork_prepare_timeout",
|
|
132
|
+
fallback="Timed out preparing session for fork: {detail}",
|
|
133
|
+
multiline=True,
|
|
134
|
+
)
|
|
135
|
+
_FORK_PREPARE_FAILED = msg(
|
|
136
|
+
"engine.fork_prepare_failed",
|
|
137
|
+
fallback="Failed to prepare session for fork: {detail}",
|
|
138
|
+
multiline=True,
|
|
139
|
+
)
|
|
140
|
+
_FORK_SESSION_NOT_FOUND = msg(
|
|
141
|
+
"engine.fork_session_not_found",
|
|
142
|
+
fallback="Session '{session_id}' not found.",
|
|
143
|
+
)
|
|
144
|
+
_FORK_FAILED = msg(
|
|
145
|
+
"engine.fork_failed",
|
|
146
|
+
fallback="Failed to fork session: {detail}",
|
|
147
|
+
multiline=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_CONSTRUCTION_TRAJECTORY_ACTIVATION_FAILED = msg(
|
|
152
|
+
"construction.trajectory_activation_failed",
|
|
153
|
+
fallback="Trajectory recording could not start and has been disabled for this session.",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_RESTORE_SERVICE_SESSION_INCOMPATIBLE = msg(
|
|
158
|
+
"restore.service_session_incompatible",
|
|
159
|
+
fallback=(
|
|
160
|
+
"This session was saved with an OpenAI Responses service session. "
|
|
161
|
+
"The active agent/model profile, workspace, service endpoint, or storage mode "
|
|
162
|
+
"is not compatible, so {app} will continue from local history only."
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
_RESTORE_SUB_AGENTS_DISCARDED = msg(
|
|
166
|
+
"restore.sub_agents_discarded",
|
|
167
|
+
fallback="{discarded} paused sub-agent(s) from a previous session were discarded: {names}",
|
|
168
|
+
)
|
|
169
|
+
_RESTORE_AGENT_PROFILE_UNRESOLVED_USING_CURRENT = msg(
|
|
170
|
+
"restore.agent_profile_unresolved_using_current",
|
|
171
|
+
fallback=(
|
|
172
|
+
"The saved agent profile {saved} could not be uniquely resolved. Continuing with the current agent {current}."
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
_RESTORE_AGENT_PROFILE_UNRESOLVED = msg(
|
|
176
|
+
"restore.agent_profile_unresolved",
|
|
177
|
+
fallback="The saved agent profile {saved} could not be uniquely resolved. Session restore was stopped.",
|
|
178
|
+
)
|
|
179
|
+
_RESTORE_SESSION_CWD_MISSING = msg(
|
|
180
|
+
"restore.session_cwd_missing",
|
|
181
|
+
fallback="The working directory of this session no longer exists: {path}",
|
|
182
|
+
)
|
|
183
|
+
_RESTORE_REQUESTED_AGENT_PROFILE_UNRESOLVED = msg(
|
|
184
|
+
"restore.requested_agent_profile_unresolved",
|
|
185
|
+
fallback="The requested agent profile {profile} could not be found. Session restore was stopped.",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _working_dirs_from_paths(paths: list[str], primary_cwd: str) -> list[WorkingDir]:
|
|
190
|
+
return [WorkingDir(path=path, is_primary=path == primary_cwd) for path in paths]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _restore_profile_override_switch(
|
|
194
|
+
state: dict | None,
|
|
195
|
+
*,
|
|
196
|
+
from_profile: str,
|
|
197
|
+
from_display: str,
|
|
198
|
+
to_profile: str,
|
|
199
|
+
to_display: str,
|
|
200
|
+
) -> bool:
|
|
201
|
+
"""Record an explicit restore-time profile override in session state."""
|
|
202
|
+
if state is None or not from_profile or from_profile == to_profile:
|
|
203
|
+
return False
|
|
204
|
+
switches = state.setdefault("agent_profile_switches", [])
|
|
205
|
+
switches.append(
|
|
206
|
+
{
|
|
207
|
+
"from": from_profile,
|
|
208
|
+
"to": to_profile,
|
|
209
|
+
"from_display": from_display or from_profile,
|
|
210
|
+
"to_display": to_display or to_profile,
|
|
211
|
+
"at_message_index": len(state.get("messages", [])),
|
|
212
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extra_roots(paths: list[str], primary_cwd: str) -> list[str]:
|
|
219
|
+
"""Working-dir paths with the primary cwd removed (effective extra roots).
|
|
220
|
+
|
|
221
|
+
Sessions persist ``working_dirs`` inconsistently: a no-extra session stores
|
|
222
|
+
``[]`` while a multi-root session stores ``[primary, *extras]``. Restore also
|
|
223
|
+
always re-inserts the primary for explicit ``additionalDirectories``. Service-
|
|
224
|
+
session identity only cares about the *effective* extra roots, so strip the
|
|
225
|
+
primary from both sides before comparing — otherwise loading a no-extra session
|
|
226
|
+
with ``additionalDirectories: []`` would discard a still-valid service session.
|
|
227
|
+
"""
|
|
228
|
+
return [path for path in paths if path != primary_cwd]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True)
|
|
232
|
+
class _ModelRestoreRollbackToken:
|
|
233
|
+
"""State needed to undo a model reapply before the build's commit installs it."""
|
|
234
|
+
|
|
235
|
+
conversation: KernelConversation | None
|
|
236
|
+
pointer: PointerToken
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _rollback_reapplied_model_profile(token: _ModelRestoreRollbackToken) -> None:
|
|
240
|
+
"""Undo the eager pointer write after a pre-commit start failure.
|
|
241
|
+
|
|
242
|
+
The settings half needs no undo: the reapplied selection lived only on the
|
|
243
|
+
staged load, and a build that failed before its commit never installed it.
|
|
244
|
+
Value and origin go back together — a bare env write would leave the
|
|
245
|
+
registry saying SESSION about a pointer the rollback just took away.
|
|
246
|
+
A refusal is the registry reporting that someone chose a profile while
|
|
247
|
+
this restore was failing; that choice is newer than the state this token
|
|
248
|
+
describes, so it stands.
|
|
249
|
+
"""
|
|
250
|
+
if not restore_model_pointer(token.pointer):
|
|
251
|
+
logger.debug("Model pointer moved during a failed restore; leaving the newer selection in place")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _reset_restore_history_state(history_state: dict) -> dict:
|
|
255
|
+
"""Deep copy of the live history captured before a reset deletes session files.
|
|
256
|
+
|
|
257
|
+
A failed reset restarts the engine from this copy, so it must own every
|
|
258
|
+
layer (messages, contents lists, content objects) — aliasing the live
|
|
259
|
+
objects of the session being shut down would let the restarted session
|
|
260
|
+
share identities with a torn-down state.
|
|
261
|
+
"""
|
|
262
|
+
return copy.deepcopy(history_state)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def cleanup_empty_session_dir_path(session_dir: Path | None, *, path_reused: bool = False) -> None:
|
|
266
|
+
"""Remove *session_dir* when it has no restorable session files.
|
|
267
|
+
|
|
268
|
+
``path_reused`` is for a caller that restarts on this very directory (a
|
|
269
|
+
reset keeps the session id).
|
|
270
|
+
"""
|
|
271
|
+
if session_dir is None or not session_dir.is_dir():
|
|
272
|
+
return
|
|
273
|
+
if (session_dir / SESSION_FILE_NAME).exists():
|
|
274
|
+
return
|
|
275
|
+
if (session_dir / SESSION_BACKUP_FILE_NAME).exists():
|
|
276
|
+
return
|
|
277
|
+
if (session_dir / SESSION_RECOVERY_FILE_NAME).exists():
|
|
278
|
+
return
|
|
279
|
+
if _has_restorable_rollback_snapshots(session_dir):
|
|
280
|
+
return
|
|
281
|
+
if _has_committed_sub_agent_artifacts(session_dir):
|
|
282
|
+
return
|
|
283
|
+
if _has_recorded_trajectory(session_dir):
|
|
284
|
+
return
|
|
285
|
+
from chrys.service.trajectory.tombstone import delete_session_directory
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
# Same disposal as an explicit delete: a trajectory writer that is
|
|
289
|
+
# still alive (a stuck worker, a writer in another process) turns this
|
|
290
|
+
# into a logical delete instead of a half-removed folder.
|
|
291
|
+
delete_session_directory(session_dir, sessions_root=session_dir.parent, path_reused=path_reused)
|
|
292
|
+
except OSError:
|
|
293
|
+
logger.debug("Failed to remove empty session directory %s", session_dir, exc_info=True)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _delete_reset_session_files(session_dir: Path) -> None:
|
|
297
|
+
for filename in (SESSION_FILE_NAME, SESSION_BACKUP_FILE_NAME, SESSION_RECOVERY_FILE_NAME):
|
|
298
|
+
path = session_dir / filename
|
|
299
|
+
if path.exists():
|
|
300
|
+
path.unlink()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _acquire_session_write_lock(lock_path: Path, session_id: str) -> FileLock | None:
|
|
304
|
+
lock = FileLock(lock_path, timeout=SESSION_WRITE_LOCK_TIMEOUT_SECONDS)
|
|
305
|
+
try:
|
|
306
|
+
lock.acquire()
|
|
307
|
+
return lock
|
|
308
|
+
except TimeoutError:
|
|
309
|
+
logger.warning("Timed out acquiring session lock for reset %s", session_id, exc_info=True)
|
|
310
|
+
return None
|
|
311
|
+
except OSError:
|
|
312
|
+
logger.warning("Failed to acquire session lock for reset %s", session_id, exc_info=True)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _has_restorable_rollback_snapshots(session_dir: Path) -> bool:
|
|
317
|
+
snap_dir = session_dir / "snapshots"
|
|
318
|
+
if not snap_dir.is_dir():
|
|
319
|
+
return False
|
|
320
|
+
return any(parse_snapshot_turn(path) >= 1 for path in snap_dir.glob("*.json"))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _has_recorded_trajectory(session_dir: Path) -> bool:
|
|
324
|
+
"""Whether this directory holds a log that outlives the conversation in it.
|
|
325
|
+
|
|
326
|
+
A rollback only ever appends to the log — including the reset to welcome,
|
|
327
|
+
which discards the session's messages but restarts on this very directory
|
|
328
|
+
and reads its branch from the last line written. Only an explicit clear or
|
|
329
|
+
delete takes a session's trajectory with it, and neither comes through
|
|
330
|
+
here.
|
|
331
|
+
"""
|
|
332
|
+
from chrys.service.trajectory.session import trajectory_events_path
|
|
333
|
+
|
|
334
|
+
events = trajectory_events_path(session_dir)
|
|
335
|
+
try:
|
|
336
|
+
return events.stat().st_size > 0
|
|
337
|
+
except FileNotFoundError:
|
|
338
|
+
return False
|
|
339
|
+
except OSError:
|
|
340
|
+
# This is the guard that keeps an audit log from being deleted, so a
|
|
341
|
+
# probe that cannot answer keeps the directory: "I could not look" is
|
|
342
|
+
# not "there is nothing there".
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _has_committed_sub_agent_artifacts(session_dir: Path) -> bool:
|
|
347
|
+
sub_agents = session_dir / "sub_agents"
|
|
348
|
+
if not sub_agents.is_dir():
|
|
349
|
+
return False
|
|
350
|
+
for path in sub_agents.rglob("*.json"):
|
|
351
|
+
if path.name.endswith(".tmp"):
|
|
352
|
+
continue
|
|
353
|
+
if path.is_file():
|
|
354
|
+
return True
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass(frozen=True, slots=True)
|
|
359
|
+
class _SessionDeleteFailure:
|
|
360
|
+
"""Why a session delete did not happen; the current session is intact."""
|
|
361
|
+
|
|
362
|
+
code: str
|
|
363
|
+
message: str
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class SessionLifecycle:
|
|
367
|
+
"""Orchestrates session startup, reload, restore, reset, fork, deletion, and shutdown."""
|
|
368
|
+
|
|
369
|
+
def __init__(
|
|
370
|
+
self,
|
|
371
|
+
*,
|
|
372
|
+
session: ActiveSession,
|
|
373
|
+
current: CurrentAgent,
|
|
374
|
+
loader: AgentLoader,
|
|
375
|
+
permits: LifecyclePermits,
|
|
376
|
+
writer: SessionWriter,
|
|
377
|
+
turn_state: TurnRuntimeState,
|
|
378
|
+
usage_publisher: UsagePublisher,
|
|
379
|
+
bus: EventBus,
|
|
380
|
+
fsm: EngineStateMachine,
|
|
381
|
+
history: SessionHistoryManager,
|
|
382
|
+
persistence: SessionPersistence,
|
|
383
|
+
settings_handle: SettingsHandle,
|
|
384
|
+
agent_registry: AgentProfileRegistry | None,
|
|
385
|
+
model_registry: ModelProfileRegistry | None,
|
|
386
|
+
trajectory_recorder: TrajectoryRecorder,
|
|
387
|
+
workspace_change_tracker: WorkspaceChangeTracker,
|
|
388
|
+
unregister_current_engine: Callable[[], None],
|
|
389
|
+
) -> None:
|
|
390
|
+
self._session = session
|
|
391
|
+
self._current = current
|
|
392
|
+
self._loader = loader
|
|
393
|
+
self._permits = permits
|
|
394
|
+
self._writer = writer
|
|
395
|
+
self._turn_state = turn_state
|
|
396
|
+
self._usage_publisher = usage_publisher
|
|
397
|
+
self._bus = bus
|
|
398
|
+
self._fsm = fsm
|
|
399
|
+
self._history = history
|
|
400
|
+
self._persistence = persistence
|
|
401
|
+
self._settings_handle = settings_handle
|
|
402
|
+
self._agent_registry = agent_registry
|
|
403
|
+
self._model_registry = model_registry
|
|
404
|
+
self._trajectory_recorder = trajectory_recorder
|
|
405
|
+
self._workspace_change_tracker = workspace_change_tracker
|
|
406
|
+
self._unregister_current_engine = unregister_current_engine
|
|
407
|
+
|
|
408
|
+
def reset_turn_runtime_after_session_shutdown(self) -> None:
|
|
409
|
+
"""Clear turn runtime state after shutdown has observed the old task."""
|
|
410
|
+
prompt_admission_owner = self._permits.prompt_admission_owner_for_current_task()
|
|
411
|
+
self._turn_state.reset_after_session_shutdown(
|
|
412
|
+
prompt_admission_owner=prompt_admission_owner,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
async def fire_session_end_hooks(self) -> None:
|
|
416
|
+
"""Fire ``session_end`` for the live session and wait for it to finish.
|
|
417
|
+
|
|
418
|
+
Fires at most once per session. ``shutdown()`` is the usual caller;
|
|
419
|
+
deleting the ACTIVE session calls it first, while ``_session_id`` and
|
|
420
|
+
the session files still exist, and the later shutdown then skips the
|
|
421
|
+
duplicate. ``fire()`` only *spawns* ``async``-mode hooks, so this
|
|
422
|
+
also drains them (without closing the manager) — otherwise a delete
|
|
423
|
+
could remove the files before such a hook ran. The next ``start()``
|
|
424
|
+
re-arms it for the session it brings up.
|
|
425
|
+
"""
|
|
426
|
+
hook_manager = self._session.hook_manager
|
|
427
|
+
if hook_manager is None or self._session.session_end_fired:
|
|
428
|
+
return
|
|
429
|
+
from chrys.service.hooks.events import HookEvent
|
|
430
|
+
|
|
431
|
+
self._session.mark_session_end_fired()
|
|
432
|
+
profile_name = self._session.agent_profile.name if self._session.agent_profile is not None else ""
|
|
433
|
+
await hook_manager.fire(
|
|
434
|
+
HookEvent.SESSION_END,
|
|
435
|
+
{"session_id": self._session.session_id, "profile": profile_name, "cwd": self._session.workspace_cwd()},
|
|
436
|
+
scope="session",
|
|
437
|
+
)
|
|
438
|
+
await hook_manager.drain_session(close=False)
|
|
439
|
+
|
|
440
|
+
async def close_trajectory_log(self) -> None:
|
|
441
|
+
"""Close the trajectory writer so the session directory can be removed.
|
|
442
|
+
|
|
443
|
+
Deleting the ACTIVE session removes its folder while this runtime is
|
|
444
|
+
still up. A live writer holds the log's lease, and a leased directory
|
|
445
|
+
is only tombstoned — swept at the next store startup, so the files a
|
|
446
|
+
user asked to delete would outlive the request for the rest of the
|
|
447
|
+
run. Closing first makes the delete physical.
|
|
448
|
+
|
|
449
|
+
A delete that then fails (the lock is busy) leaves the session
|
|
450
|
+
running unrecorded until the next runtime resumes its log. That is
|
|
451
|
+
the recorder's standing contract — recording never holds a session
|
|
452
|
+
up — and the log it leaves behind is closed, not torn.
|
|
453
|
+
"""
|
|
454
|
+
await self._trajectory_recorder.close(reason=TrajectoryRuntimeFinishReason.SESSION_SWITCH)
|
|
455
|
+
|
|
456
|
+
def prepare(self, profile: AgentProfile | None) -> None:
|
|
457
|
+
"""Seed the fallback profile used by session restores."""
|
|
458
|
+
if profile is not None:
|
|
459
|
+
self._session.agent_profile = profile
|
|
460
|
+
|
|
461
|
+
async def start(
|
|
462
|
+
self,
|
|
463
|
+
profile: AgentProfile,
|
|
464
|
+
*,
|
|
465
|
+
operation: str = "startup",
|
|
466
|
+
staged_loaded: LoadedSettings | None = None,
|
|
467
|
+
workspace: Workspace | None = None,
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Build and install the agent from a profile under a lifecycle permit."""
|
|
470
|
+
if (
|
|
471
|
+
self._permits.current_task_owns_rebuild_permit()
|
|
472
|
+
or self._permits.current_task_owns_session_transition_permit()
|
|
473
|
+
):
|
|
474
|
+
await self._start_locked(profile, operation=operation, staged_loaded=staged_loaded, workspace=workspace)
|
|
475
|
+
return
|
|
476
|
+
token = self._permits.capture_control_token()
|
|
477
|
+
permit = await self._permits.acquire_rebuild_permit(token)
|
|
478
|
+
if isinstance(permit, lifecycle_permits.RebuildPermitDenied):
|
|
479
|
+
raise RuntimeError(permit.message)
|
|
480
|
+
try:
|
|
481
|
+
await self._start_locked(profile, operation=operation, staged_loaded=staged_loaded, workspace=workspace)
|
|
482
|
+
finally:
|
|
483
|
+
self._permits.release_rebuild_permit(permit)
|
|
484
|
+
|
|
485
|
+
async def _start_locked(
|
|
486
|
+
self,
|
|
487
|
+
profile: AgentProfile,
|
|
488
|
+
*,
|
|
489
|
+
operation: str = "startup",
|
|
490
|
+
staged_loaded: LoadedSettings | None = None,
|
|
491
|
+
workspace: Workspace | None = None,
|
|
492
|
+
) -> None:
|
|
493
|
+
"""Start the engine while the caller owns the rebuild boundary."""
|
|
494
|
+
# Every session (fresh, restored, reset) starts here: re-arm the
|
|
495
|
+
# once-per-session ``session_end`` hook fired by shutdown/delete.
|
|
496
|
+
self._session.begin(agent_profile=profile, workspace=workspace)
|
|
497
|
+
if self._session.session_id is None:
|
|
498
|
+
raise RuntimeError("Session begin did not establish a session id.")
|
|
499
|
+
staged_workspace = workspace if workspace is not None else self._session.workspace
|
|
500
|
+
|
|
501
|
+
if not self._session.guard.ensure(self._session.session_id):
|
|
502
|
+
message = self._session.guard.conflict_message(self._session.session_id)
|
|
503
|
+
await self._bus.publish(Error(code="session_in_use", message=message, session_id=self._session.session_id))
|
|
504
|
+
raise RuntimeError(message)
|
|
505
|
+
|
|
506
|
+
# Build the hook manager once per engine instance when a hook config
|
|
507
|
+
# file exists. With no config file, hooks stay a true no-op: no
|
|
508
|
+
# manager, no outbox/log/tmp directories, and no recovery task.
|
|
509
|
+
#
|
|
510
|
+
# The manager survives profile switches within the same session —
|
|
511
|
+
# hooks are global, not profile-scoped. A no-executor workspace-change
|
|
512
|
+
# retry can happen after a startup failure, so reload there because
|
|
513
|
+
# project hooks are primary-cwd scoped. The candidate stays staged: it
|
|
514
|
+
# goes live with the build's commit, and until then the old manager
|
|
515
|
+
# keeps running untouched.
|
|
516
|
+
old_hook_manager = self._session.hook_manager
|
|
517
|
+
hook_manager = await self._loader.stage_hook_manager(
|
|
518
|
+
operation=operation, staged_loaded=staged_loaded, workspace=staged_workspace
|
|
519
|
+
)
|
|
520
|
+
if (old_hook_manager is None or operation == "workspace_change") and hook_manager is not None:
|
|
521
|
+
# Session / turn lifecycle hooks fire outside any model run, so
|
|
522
|
+
# they record under the recorder's current scope instead.
|
|
523
|
+
hook_manager.trajectory_context_provider = self._trajectory_recorder.context
|
|
524
|
+
|
|
525
|
+
session_dir = self._session.session_dir if self._persistence.state_store is not None else None
|
|
526
|
+
|
|
527
|
+
from chrys.foundation.observability.sink import get_otel_sink
|
|
528
|
+
|
|
529
|
+
otel_sink = get_otel_sink()
|
|
530
|
+
if otel_sink is not None:
|
|
531
|
+
otel_sink.activate(self._session.session_id, session_dir)
|
|
532
|
+
# The trajectory recorder is bound here and activates (opens the log,
|
|
533
|
+
# takes the writer lease) on its first event, so a session that never
|
|
534
|
+
# records anything never creates ``trajectory/``.
|
|
535
|
+
event_loop = asyncio.get_running_loop()
|
|
536
|
+
trajectory_session_id = self._session.session_id
|
|
537
|
+
trajectory_warning_tasks: set[asyncio.Task[None]] = set()
|
|
538
|
+
|
|
539
|
+
def _report_trajectory_activation_failure(_reason: str) -> None:
|
|
540
|
+
def _publish_warning() -> None:
|
|
541
|
+
task = event_loop.create_task(
|
|
542
|
+
self._bus.publish(
|
|
543
|
+
Warning(
|
|
544
|
+
code="trajectory_activation_failed",
|
|
545
|
+
message="Trajectory recording could not start and has been disabled for this session.",
|
|
546
|
+
display_message=_CONSTRUCTION_TRAJECTORY_ACTIVATION_FAILED.bind(),
|
|
547
|
+
session_id=trajectory_session_id,
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
trajectory_warning_tasks.add(task)
|
|
552
|
+
task.add_done_callback(trajectory_warning_tasks.discard)
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
event_loop.call_soon_threadsafe(_publish_warning)
|
|
556
|
+
except RuntimeError:
|
|
557
|
+
logger.debug("Trajectory activation failure could not be published because the event loop is closed")
|
|
558
|
+
|
|
559
|
+
trajectory = self._trajectory_recorder.bind_session(
|
|
560
|
+
session_id=trajectory_session_id,
|
|
561
|
+
session_dir=session_dir,
|
|
562
|
+
write_lock_path=self._session.session_write_lock_path(trajectory_session_id)
|
|
563
|
+
if session_dir is not None
|
|
564
|
+
else None,
|
|
565
|
+
session_start_info=self._loader.trajectory_session_start_info,
|
|
566
|
+
on_activation_failed=_report_trajectory_activation_failure,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
await self._loader.load(
|
|
570
|
+
profile,
|
|
571
|
+
operation=operation,
|
|
572
|
+
staged_loaded=staged_loaded,
|
|
573
|
+
workspace=staged_workspace,
|
|
574
|
+
hook_manager=hook_manager,
|
|
575
|
+
trajectory=trajectory,
|
|
576
|
+
old_hook_manager=old_hook_manager,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
async def start_with_rebuild_permit(
|
|
580
|
+
self,
|
|
581
|
+
permit: lifecycle_permits.RebuildPermit,
|
|
582
|
+
profile: AgentProfile,
|
|
583
|
+
*,
|
|
584
|
+
operation: str = "startup",
|
|
585
|
+
staged_loaded: LoadedSettings | None = None,
|
|
586
|
+
workspace: Workspace | None = None,
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Start using an already-acquired rebuild permit."""
|
|
589
|
+
self._permits.ensure_rebuild_permit(permit)
|
|
590
|
+
await self.start(profile, operation=operation, staged_loaded=staged_loaded, workspace=workspace)
|
|
591
|
+
|
|
592
|
+
async def reload(
|
|
593
|
+
self,
|
|
594
|
+
new_profile: AgentProfile,
|
|
595
|
+
workspace: Workspace | None = None,
|
|
596
|
+
*,
|
|
597
|
+
operation: str = "switch",
|
|
598
|
+
staged_loaded: LoadedSettings | None = None,
|
|
599
|
+
) -> None:
|
|
600
|
+
"""Restart the agent with a new profile/workspace while preserving history."""
|
|
601
|
+
if (
|
|
602
|
+
self._permits.current_task_owns_rebuild_permit()
|
|
603
|
+
or self._permits.current_task_owns_session_transition_permit()
|
|
604
|
+
):
|
|
605
|
+
await self._reload_locked(new_profile, workspace, operation=operation, staged_loaded=staged_loaded)
|
|
606
|
+
return
|
|
607
|
+
token = self._permits.capture_control_token()
|
|
608
|
+
permit = await self._permits.acquire_rebuild_permit(token)
|
|
609
|
+
if isinstance(permit, lifecycle_permits.RebuildPermitDenied):
|
|
610
|
+
raise RuntimeError(permit.message)
|
|
611
|
+
try:
|
|
612
|
+
await self._reload_locked(new_profile, workspace, operation=operation, staged_loaded=staged_loaded)
|
|
613
|
+
finally:
|
|
614
|
+
self._permits.release_rebuild_permit(permit)
|
|
615
|
+
|
|
616
|
+
async def _reload_locked(
|
|
617
|
+
self,
|
|
618
|
+
new_profile: AgentProfile,
|
|
619
|
+
workspace: Workspace | None = None,
|
|
620
|
+
*,
|
|
621
|
+
operation: str = "switch",
|
|
622
|
+
staged_loaded: LoadedSettings | None = None,
|
|
623
|
+
) -> None:
|
|
624
|
+
"""Soft-restart while the caller owns the rebuild boundary."""
|
|
625
|
+
await self._loader.reload(new_profile, workspace, operation=operation, staged_loaded=staged_loaded)
|
|
626
|
+
|
|
627
|
+
async def reload_with_rebuild_permit(
|
|
628
|
+
self,
|
|
629
|
+
permit: lifecycle_permits.RebuildPermit,
|
|
630
|
+
new_profile: AgentProfile,
|
|
631
|
+
workspace: Workspace | None = None,
|
|
632
|
+
*,
|
|
633
|
+
operation: str = "switch",
|
|
634
|
+
staged_loaded: LoadedSettings | None = None,
|
|
635
|
+
) -> None:
|
|
636
|
+
"""Soft-restart using an already-acquired rebuild permit."""
|
|
637
|
+
self._permits.ensure_rebuild_permit(permit)
|
|
638
|
+
await self.reload(new_profile, workspace=workspace, operation=operation, staged_loaded=staged_loaded)
|
|
639
|
+
|
|
640
|
+
async def close_session(self) -> None:
|
|
641
|
+
"""Close the session and release its lock, retaining process resources."""
|
|
642
|
+
await self._teardown(release_lock=True, close_mcp=False)
|
|
643
|
+
|
|
644
|
+
async def close_session_in_place(self) -> None:
|
|
645
|
+
"""Close the session while retaining its lock and process resources."""
|
|
646
|
+
await self._teardown(release_lock=False, close_mcp=False)
|
|
647
|
+
|
|
648
|
+
async def shutdown(self) -> None:
|
|
649
|
+
"""Close the session and process resources."""
|
|
650
|
+
await self._teardown(release_lock=True, close_mcp=True)
|
|
651
|
+
|
|
652
|
+
async def _teardown(self, *, release_lock: bool, close_mcp: bool) -> None:
|
|
653
|
+
"""Gracefully shut down the engine."""
|
|
654
|
+
self._session.mark_closing()
|
|
655
|
+
self._turn_state.shutdown_used_cancel_fallback = False
|
|
656
|
+
suppress_trailing_save = False
|
|
657
|
+
self._unregister_current_engine()
|
|
658
|
+
self._fsm.try_transition(Trigger.SHUTDOWN)
|
|
659
|
+
if self._current.loaded is not None and self._current.loaded.bindings.state.running:
|
|
660
|
+
handle = self._current.loaded.bindings.backend.active_handle
|
|
661
|
+
if handle is not None:
|
|
662
|
+
await self._current.loaded.bindings.backend.abort(handle, AbortCause.OWNER_CLOSE)
|
|
663
|
+
run_task = self._turn_state.lease.run_task
|
|
664
|
+
if run_task is not None and not run_task.done():
|
|
665
|
+
try:
|
|
666
|
+
await asyncio.wait_for(
|
|
667
|
+
asyncio.shield(run_task),
|
|
668
|
+
timeout=_SHUTDOWN_POST_RUN_TIMEOUT_SECONDS,
|
|
669
|
+
)
|
|
670
|
+
except TimeoutError:
|
|
671
|
+
self._turn_state.shutdown_used_cancel_fallback = True
|
|
672
|
+
suppress_trailing_save = True
|
|
673
|
+
run_task.cancel()
|
|
674
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
675
|
+
await run_task
|
|
676
|
+
except asyncio.CancelledError:
|
|
677
|
+
pass
|
|
678
|
+
self._turn_state.lease.release_run_task()
|
|
679
|
+
await self._turn_state.lease.settle_notifications()
|
|
680
|
+
# A cancelled/timed-out worker can leave a queued retry or active
|
|
681
|
+
# admission behind. Terminalize their preparations while the trajectory
|
|
682
|
+
# writer is still open; the post-shutdown reset is too late to emit.
|
|
683
|
+
self._turn_state.lease.clear_pending_retry(outcome=PreparationOutcome.DROPPED)
|
|
684
|
+
self._turn_state.lease.clear_active_admissions(outcome=PreparationOutcome.OWNER_CHANGED)
|
|
685
|
+
self._turn_state.lease.clear_pre_admission_preparations()
|
|
686
|
+
if self._current.loaded is not None:
|
|
687
|
+
for injection in self._current.loaded.injection.drain_pending():
|
|
688
|
+
if injection.preparation is not None:
|
|
689
|
+
injection.preparation.finished_soon(
|
|
690
|
+
outcome=PreparationOutcome.TARGET_STALE,
|
|
691
|
+
target_turn_id=injection.target_turn_id,
|
|
692
|
+
)
|
|
693
|
+
await self._loader.cancel_outbox_recovery()
|
|
694
|
+
# Drain any queued UsageUpdate publish tasks before the bus is torn
|
|
695
|
+
# down — orphaned tasks otherwise either deliver a stale UsageUpdate
|
|
696
|
+
# into the next session's subscriber set (engine instance is reused
|
|
697
|
+
# across session restore / new-session) or get garbage-collected mid-
|
|
698
|
+
# await, producing "Task was destroyed but it is pending" warnings on
|
|
699
|
+
# process exit.
|
|
700
|
+
await self._usage_publisher.settle()
|
|
701
|
+
# Fire ``session_end`` hooks BEFORE we tear down anything — they
|
|
702
|
+
# may want a live session to inspect. Then drain any in-flight
|
|
703
|
+
# async hooks within the configured shutdown grace. Detached
|
|
704
|
+
# hooks survive this call by design.
|
|
705
|
+
if self._session.hook_manager is not None:
|
|
706
|
+
await self.fire_session_end_hooks()
|
|
707
|
+
await self._session.hook_manager.drain_session()
|
|
708
|
+
# ``drain_session()`` closes the manager. The same engine
|
|
709
|
+
# instance is reused for restore/new-session flows, so force
|
|
710
|
+
# the next ``start()`` to reload config and create a fresh
|
|
711
|
+
# manager instead of carrying a closed no-op instance forward.
|
|
712
|
+
self._session.hook_manager = None
|
|
713
|
+
# Auto-save before shutdown
|
|
714
|
+
with self._session.saves_suppressed() if suppress_trailing_save else contextlib.nullcontext():
|
|
715
|
+
await self._writer.save_current_session()
|
|
716
|
+
# Stamp the coordination registry closed (peers stop warning
|
|
717
|
+
# about us); files are kept for peers' late rollbacks — GC only.
|
|
718
|
+
if self._session.mutation_coordinator is not None:
|
|
719
|
+
try:
|
|
720
|
+
await asyncio.get_running_loop().run_in_executor(None, self._session.mutation_coordinator.close)
|
|
721
|
+
except Exception:
|
|
722
|
+
logger.debug("Mutation coordinator close failed", exc_info=True)
|
|
723
|
+
# The trajectory writer must be closed before the directory it writes
|
|
724
|
+
# into can be judged empty. ``close_mcp=False`` is how the
|
|
725
|
+
# session-switch flows (new/restore/clear/reset) shut the engine down
|
|
726
|
+
# while the process keeps running; every other caller is exiting.
|
|
727
|
+
await self._trajectory_recorder.close(
|
|
728
|
+
reason=(
|
|
729
|
+
TrajectoryRuntimeFinishReason.GRACEFUL_SHUTDOWN
|
|
730
|
+
if close_mcp
|
|
731
|
+
else TrajectoryRuntimeFinishReason.SESSION_SWITCH
|
|
732
|
+
),
|
|
733
|
+
)
|
|
734
|
+
# Clean up empty session directory (no messages were sent)
|
|
735
|
+
self.cleanup_empty_session_dir()
|
|
736
|
+
await self._loader.release_current()
|
|
737
|
+
self._turn_state.paused_sub_agents.clear()
|
|
738
|
+
if close_mcp:
|
|
739
|
+
await self._loader.close()
|
|
740
|
+
if release_lock:
|
|
741
|
+
self._session.guard.release()
|
|
742
|
+
|
|
743
|
+
async def refresh_mutation_attribution(self, *, force: bool = False) -> bool:
|
|
744
|
+
"""Reclassify the live mutation log against peer registry claims.
|
|
745
|
+
|
|
746
|
+
The engine-level read-side entry point for cross-session
|
|
747
|
+
coordination: display
|
|
748
|
+
surfaces call it before building net summaries; the rollback
|
|
749
|
+
path calls it with ``force=True`` as the authoritative last
|
|
750
|
+
check. Returns True when any row changed — the change is
|
|
751
|
+
persisted via the normal session-state save so serialized
|
|
752
|
+
consumers (TUI /diff) observe it too.
|
|
753
|
+
"""
|
|
754
|
+
coordinator = self._session.mutation_coordinator
|
|
755
|
+
tracker = self._session.mutation_tracker
|
|
756
|
+
if coordinator is None or tracker is None:
|
|
757
|
+
return False
|
|
758
|
+
loop = asyncio.get_running_loop()
|
|
759
|
+
try:
|
|
760
|
+
changed = await loop.run_in_executor(
|
|
761
|
+
None,
|
|
762
|
+
lambda: coordinator.reclassify(tracker, force=force, fallback_root=self._session.workspace_cwd()),
|
|
763
|
+
)
|
|
764
|
+
except Exception:
|
|
765
|
+
logger.debug("Mutation attribution refresh failed", exc_info=True)
|
|
766
|
+
return False
|
|
767
|
+
# The coordinator's flag, not this call's return value: a
|
|
768
|
+
# finalize-time reclassify may have changed rows earlier with no
|
|
769
|
+
# saver, and its signature update makes this very call report
|
|
770
|
+
# "unchanged". While a run is active the save must not happen here:
|
|
771
|
+
# a primary save deletes the recovery sidecar, which mid-turn is the
|
|
772
|
+
# only durable copy of committed-but-unmerged tool exchanges. The
|
|
773
|
+
# flag stays set, and the turn-end save persists the reclassification.
|
|
774
|
+
if (
|
|
775
|
+
not (self._current.loaded is not None and self._current.loaded.bindings.state.running)
|
|
776
|
+
and self._session.mutation_coordinator is not None
|
|
777
|
+
and self._session.mutation_coordinator.consume_unsaved_reclassification()
|
|
778
|
+
):
|
|
779
|
+
await self._writer.save_current_session()
|
|
780
|
+
return changed
|
|
781
|
+
|
|
782
|
+
async def on_session_fork(self, event: SessionFork) -> None:
|
|
783
|
+
"""Handle session fork requests for the active saved session."""
|
|
784
|
+
try:
|
|
785
|
+
await asyncio.wait_for(
|
|
786
|
+
self._permits.wait_for_agent_load_idle(),
|
|
787
|
+
timeout=_SESSION_FORK_AGENT_LOAD_TIMEOUT_SECONDS,
|
|
788
|
+
)
|
|
789
|
+
except TimeoutError:
|
|
790
|
+
await self._publish_session_fork_error(
|
|
791
|
+
"session_fork_busy",
|
|
792
|
+
"Cannot fork while the agent is still loading.",
|
|
793
|
+
session_id=event.session_id or self._session.session_id or "",
|
|
794
|
+
display_message=_FORK_AGENT_LOADING.bind(),
|
|
795
|
+
)
|
|
796
|
+
return
|
|
797
|
+
|
|
798
|
+
active_session_id = self._session.session_id
|
|
799
|
+
state_store = self._persistence.state_store
|
|
800
|
+
if active_session_id is None or self._current.loaded is None:
|
|
801
|
+
await self._publish_session_fork_error(
|
|
802
|
+
"session_fork_not_ready",
|
|
803
|
+
"Cannot fork before a session is ready.",
|
|
804
|
+
session_id=active_session_id or event.session_id,
|
|
805
|
+
display_message=_FORK_NOT_READY.bind(),
|
|
806
|
+
)
|
|
807
|
+
return
|
|
808
|
+
if event.session_id != active_session_id:
|
|
809
|
+
await self._publish_session_fork_error(
|
|
810
|
+
"session_fork_stale",
|
|
811
|
+
"Cannot fork because the active session changed.",
|
|
812
|
+
session_id=event.session_id or active_session_id,
|
|
813
|
+
display_message=_FORK_SESSION_CHANGED.bind(),
|
|
814
|
+
)
|
|
815
|
+
return
|
|
816
|
+
if self._fsm.is_running():
|
|
817
|
+
await self._publish_session_fork_error(
|
|
818
|
+
"session_fork_busy",
|
|
819
|
+
"Cannot fork while a turn is running.",
|
|
820
|
+
session_id=active_session_id,
|
|
821
|
+
display_message=_FORK_TURN_ACTIVE.bind(),
|
|
822
|
+
)
|
|
823
|
+
return
|
|
824
|
+
if state_store is None:
|
|
825
|
+
await self._publish_session_fork_error(
|
|
826
|
+
"session_fork_failed",
|
|
827
|
+
"State store not configured.",
|
|
828
|
+
session_id=active_session_id,
|
|
829
|
+
display_message=_FORK_STATE_STORE_MISSING.bind(),
|
|
830
|
+
)
|
|
831
|
+
return
|
|
832
|
+
if self._session.suppress_save:
|
|
833
|
+
await self._publish_session_fork_error(
|
|
834
|
+
"session_fork_busy",
|
|
835
|
+
"Cannot fork while session state is being restored.",
|
|
836
|
+
session_id=active_session_id,
|
|
837
|
+
display_message=_FORK_RESTORING.bind(),
|
|
838
|
+
)
|
|
839
|
+
return
|
|
840
|
+
if not self._session.guard.owns(active_session_id):
|
|
841
|
+
await self._publish_session_fork_error(
|
|
842
|
+
"session_fork_busy",
|
|
843
|
+
"Cannot fork because this window does not own the active session lock.",
|
|
844
|
+
session_id=active_session_id,
|
|
845
|
+
display_message=_FORK_LOCK_NOT_OWNED.bind(),
|
|
846
|
+
)
|
|
847
|
+
return
|
|
848
|
+
if not has_real_messages(self._current.loaded.bindings.backend.history_state):
|
|
849
|
+
await self._publish_session_fork_error(
|
|
850
|
+
"session_fork_empty",
|
|
851
|
+
"Cannot fork an empty session.",
|
|
852
|
+
session_id=active_session_id,
|
|
853
|
+
display_message=_FORK_EMPTY_SESSION.bind(),
|
|
854
|
+
)
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
await self._writer.save_current_session(raise_on_error=True)
|
|
859
|
+
fresh_state = await state_store.load_session(active_session_id)
|
|
860
|
+
except TimeoutError as exc:
|
|
861
|
+
logger.warning("Timed out preparing session %s for fork", active_session_id, exc_info=True)
|
|
862
|
+
await self._publish_session_fork_error(
|
|
863
|
+
"session_fork_busy",
|
|
864
|
+
f"Timed out preparing session for fork: {exc}",
|
|
865
|
+
session_id=active_session_id,
|
|
866
|
+
display_message=_FORK_PREPARE_TIMEOUT.bind(detail=DisplayBlock(str(exc))),
|
|
867
|
+
)
|
|
868
|
+
return
|
|
869
|
+
except Exception as exc:
|
|
870
|
+
logger.warning("Failed to prepare session %s for fork", active_session_id, exc_info=True)
|
|
871
|
+
await self._publish_session_fork_error(
|
|
872
|
+
"session_fork_failed",
|
|
873
|
+
f"Failed to prepare session for fork: {exc}",
|
|
874
|
+
session_id=active_session_id,
|
|
875
|
+
display_message=_FORK_PREPARE_FAILED.bind(detail=DisplayBlock(str(exc))),
|
|
876
|
+
)
|
|
877
|
+
return
|
|
878
|
+
if fresh_state is None or not has_real_messages(fresh_state):
|
|
879
|
+
await self._publish_session_fork_error(
|
|
880
|
+
"session_fork_empty",
|
|
881
|
+
"Cannot fork an empty session.",
|
|
882
|
+
session_id=active_session_id,
|
|
883
|
+
display_message=_FORK_EMPTY_SESSION.bind(),
|
|
884
|
+
)
|
|
885
|
+
return
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
# Forking is work on the new session, so it starts out on this launch's surface.
|
|
889
|
+
new_session_id = await asyncio.to_thread(
|
|
890
|
+
state_store.fork_session, active_session_id, last_surface=self._session.surface
|
|
891
|
+
)
|
|
892
|
+
except SessionNotFoundError:
|
|
893
|
+
await self._publish_session_fork_error(
|
|
894
|
+
"session_fork_not_found",
|
|
895
|
+
f"Session '{active_session_id}' not found.",
|
|
896
|
+
session_id=active_session_id,
|
|
897
|
+
display_message=_FORK_SESSION_NOT_FOUND.bind(session_id=active_session_id),
|
|
898
|
+
)
|
|
899
|
+
return
|
|
900
|
+
except SessionForkError as exc:
|
|
901
|
+
logger.warning("Failed to fork session %s", active_session_id, exc_info=True)
|
|
902
|
+
await self._publish_session_fork_error(
|
|
903
|
+
"session_fork_failed",
|
|
904
|
+
f"Failed to fork session: {exc}",
|
|
905
|
+
session_id=active_session_id,
|
|
906
|
+
display_message=_FORK_FAILED.bind(detail=DisplayBlock(str(exc))),
|
|
907
|
+
)
|
|
908
|
+
return
|
|
909
|
+
|
|
910
|
+
# The fork copies the conversation, not the parent's trajectory; it
|
|
911
|
+
# gets its own closed opening runtime pointing back at the parent.
|
|
912
|
+
await self._trajectory_recorder.fork(
|
|
913
|
+
origin_session_id=active_session_id,
|
|
914
|
+
fork_session_id=new_session_id,
|
|
915
|
+
fork_session_dir=self._session.session_dir_for(new_session_id),
|
|
916
|
+
fork_write_lock_path=self._session.session_write_lock_path(new_session_id),
|
|
917
|
+
session_start_info=self._loader.trajectory_session_start_info(),
|
|
918
|
+
)
|
|
919
|
+
await self._bus.publish(
|
|
920
|
+
SessionForked(
|
|
921
|
+
session_id=active_session_id,
|
|
922
|
+
parent_session_id=active_session_id,
|
|
923
|
+
new_session_id=new_session_id,
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
async def _publish_session_fork_error(
|
|
928
|
+
self, code: str, message: str, *, session_id: str, display_message: MessageRef | None = None
|
|
929
|
+
) -> None:
|
|
930
|
+
await self._bus.publish(
|
|
931
|
+
Error(code=code, message=message, session_id=session_id, display_message=display_message)
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
async def _reconcile_existing_spill_storage(self) -> SpillReconciliationResult:
|
|
935
|
+
"""Rebuild and account for retained spill storage without creating a session directory."""
|
|
936
|
+
session_dir = self._session.session_dir
|
|
937
|
+
if session_dir is None or not session_dir.is_dir():
|
|
938
|
+
return SpillReconciliationResult(0, 0, frozenset())
|
|
939
|
+
try:
|
|
940
|
+
return await asyncio.to_thread(reconcile_spill_storage, session_dir, self._session.spill_quota)
|
|
941
|
+
except OSError, RuntimeError, UnicodeError:
|
|
942
|
+
# Spill records are auxiliary context. Filesystem damage or permissions
|
|
943
|
+
# must not prevent the authoritative session state from hydrating.
|
|
944
|
+
logger.warning("Unable to reconcile spill storage under %s", session_dir, exc_info=True)
|
|
945
|
+
self._session.spill_quota.disable_storage()
|
|
946
|
+
return SpillReconciliationResult(0, 0, frozenset())
|
|
947
|
+
|
|
948
|
+
def _workspace_cwd(self) -> str:
|
|
949
|
+
"""Return the engine workspace cwd, falling back only for legacy unstarted engines."""
|
|
950
|
+
if self._session.workspace is not None:
|
|
951
|
+
return self._session.workspace.primary_cwd
|
|
952
|
+
return safe_getcwd()
|
|
953
|
+
|
|
954
|
+
def _can_restore_service_session(self, meta: ChatSessionMeta) -> bool:
|
|
955
|
+
"""Return True when the active model can continue a saved service session id."""
|
|
956
|
+
profile = self._current.manifest.active_profile
|
|
957
|
+
if (
|
|
958
|
+
profile is None
|
|
959
|
+
or self._current.loaded is None
|
|
960
|
+
or meta.model_provider != "openai"
|
|
961
|
+
or meta.model_api_style != API_STYLE_RESPONSES
|
|
962
|
+
or profile.provider != "openai"
|
|
963
|
+
or profile.api_style != API_STYLE_RESPONSES
|
|
964
|
+
or meta.model_id != profile.model_id
|
|
965
|
+
or not meta.model_profile_fingerprint
|
|
966
|
+
or meta.model_profile_fingerprint != self._current.manifest.model_profile_fingerprint
|
|
967
|
+
or not meta.agent_profile_fingerprint
|
|
968
|
+
or meta.agent_profile_fingerprint != self._current.manifest.agent_profile_fingerprint
|
|
969
|
+
or self._session.workspace is None
|
|
970
|
+
or meta.primary_cwd != self._session.workspace.primary_cwd
|
|
971
|
+
or _extra_roots(meta.working_dirs, meta.primary_cwd)
|
|
972
|
+
!= _extra_roots(
|
|
973
|
+
[working_dir.path for working_dir in self._session.workspace.working_dirs],
|
|
974
|
+
self._session.workspace.primary_cwd,
|
|
975
|
+
)
|
|
976
|
+
):
|
|
977
|
+
return False
|
|
978
|
+
from chrys.service.llm.clients import effective_model_base_url
|
|
979
|
+
|
|
980
|
+
if meta.model_base_url != effective_model_base_url(profile):
|
|
981
|
+
return False
|
|
982
|
+
return self._current.loaded.bindings.backend.service_session_storage_enabled
|
|
983
|
+
|
|
984
|
+
def _restore_terminal_fsm_from_history(self) -> None:
|
|
985
|
+
"""Rebuild runtime terminal state from persisted history markers."""
|
|
986
|
+
if not self._history.is_bound:
|
|
987
|
+
return
|
|
988
|
+
marker = self._history.trailing_status_marker()
|
|
989
|
+
if marker is None:
|
|
990
|
+
return
|
|
991
|
+
kind, source = marker
|
|
992
|
+
if kind != HistoryMarkerKind.INTERRUPTED:
|
|
993
|
+
return
|
|
994
|
+
self._fsm.restore_terminal_state(failed=source == "error")
|
|
995
|
+
|
|
996
|
+
def _session_pin_overrides(self) -> dict[str, Any]:
|
|
997
|
+
"""Snapshot the per-session pins every re-load must carry.
|
|
998
|
+
|
|
999
|
+
Keep this contract in sync with the same-named implementation in
|
|
1000
|
+
state/controls.py; changes to either implementation must update both.
|
|
1001
|
+
Pins travel only while pinned, so an unpinned session picks up a changed
|
|
1002
|
+
env value on its next load.
|
|
1003
|
+
"""
|
|
1004
|
+
overrides: dict[str, Any] = {}
|
|
1005
|
+
if self._session.ask_user_timeout_pinned:
|
|
1006
|
+
overrides["ask_user_timeout_seconds"] = self._settings_handle.settings.ask_user_timeout_seconds
|
|
1007
|
+
if self._session.model_profile_pinned:
|
|
1008
|
+
overrides["model_profile"] = self._settings_handle.settings.model_profile
|
|
1009
|
+
overrides["model_profile_override"] = self._settings_handle.settings.model_profile_override
|
|
1010
|
+
overrides["model_profile_override_sub_agents"] = (
|
|
1011
|
+
self._settings_handle.settings.model_profile_override_sub_agents
|
|
1012
|
+
)
|
|
1013
|
+
return overrides
|
|
1014
|
+
|
|
1015
|
+
def _reload_eval_context(self) -> EvalContext:
|
|
1016
|
+
"""The launch mode's retry policy, passed *into* the load (as every re-load must)."""
|
|
1017
|
+
return EvalContext(
|
|
1018
|
+
frontend_default_max_transient_retries=self._settings_handle.settings.frontend_default_max_transient_retries
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
def _reapply_saved_model_profile(
|
|
1022
|
+
self,
|
|
1023
|
+
meta: ChatSessionMeta | None,
|
|
1024
|
+
profile: AgentProfile,
|
|
1025
|
+
staged_loaded: LoadedSettings,
|
|
1026
|
+
) -> tuple[LoadedSettings, _ModelRestoreRollbackToken | None]:
|
|
1027
|
+
"""Apply a selectable saved model when the restored agent has no live binding.
|
|
1028
|
+
|
|
1029
|
+
Transforms the staged load rather than installing anything: the selection
|
|
1030
|
+
goes live with the build's commit, exactly when the executor built from it
|
|
1031
|
+
does. Only the process pointer is written eagerly (§7: an intended fork,
|
|
1032
|
+
other hosts do see it) — but as SESSION, never ENV: the panel must not
|
|
1033
|
+
claim the shell exported what this restore wrote. The rollback token
|
|
1034
|
+
carries that one eager write, plus the executor identity that tells a
|
|
1035
|
+
pre-commit failure from a post-commit one.
|
|
1036
|
+
"""
|
|
1037
|
+
registry = self._model_registry
|
|
1038
|
+
if meta is None or not meta.model_profile_id or registry is None:
|
|
1039
|
+
return staged_loaded, None
|
|
1040
|
+
saved = registry.get(meta.model_profile_id)
|
|
1041
|
+
if saved is None or not is_model_profile_selectable(saved):
|
|
1042
|
+
return staged_loaded, None
|
|
1043
|
+
|
|
1044
|
+
bound_profile_id = profile.model.profile_id
|
|
1045
|
+
if bound_profile_id and registry.get(bound_profile_id) is not None:
|
|
1046
|
+
return staged_loaded, None
|
|
1047
|
+
if saved.id == staged_loaded.settings.model_profile:
|
|
1048
|
+
return staged_loaded, None
|
|
1049
|
+
|
|
1050
|
+
token = _ModelRestoreRollbackToken(
|
|
1051
|
+
conversation=self._current.loaded.bindings.backend if self._current.loaded is not None else None,
|
|
1052
|
+
pointer=set_model_pointer(saved.id, origin=SettingOrigin(layer=Source.SESSION)),
|
|
1053
|
+
)
|
|
1054
|
+
# Through the overlay, and as the whole selection, not one field of it.
|
|
1055
|
+
# Setting only ``model_profile`` would leave the previous session's pin in
|
|
1056
|
+
# ``model_profile_override``, which outranks it — the restore would claim
|
|
1057
|
+
# the saved model and resolve the old session's.
|
|
1058
|
+
return loaded_with_active_model_profile(staged_loaded, saved, Source.SESSION), token
|
|
1059
|
+
|
|
1060
|
+
async def reset_after_failed_startup_restore(self) -> None:
|
|
1061
|
+
"""Discard every partially installed restore field before fresh startup.
|
|
1062
|
+
|
|
1063
|
+
Restore can fail after installing the target session id, workspace, active
|
|
1064
|
+
lock, or a partial runtime. Suppress persistence while tearing those down so
|
|
1065
|
+
fallback startup cannot overwrite the saved target session.
|
|
1066
|
+
"""
|
|
1067
|
+
with self._session.saves_suppressed():
|
|
1068
|
+
try:
|
|
1069
|
+
await self.close_session()
|
|
1070
|
+
finally:
|
|
1071
|
+
# ``shutdown`` normally releases this; keep the failure path idempotent
|
|
1072
|
+
# if teardown raised after acquiring/installing the restore lock.
|
|
1073
|
+
self._session.guard.release()
|
|
1074
|
+
self.reset_turn_runtime_after_session_shutdown()
|
|
1075
|
+
self._session.workspace = None
|
|
1076
|
+
self.reset_for_restart(None)
|
|
1077
|
+
# Fallback startup runs in the process cwd, not the failed target's root,
|
|
1078
|
+
# so the settings the next build reads must be re-derived for that root —
|
|
1079
|
+
# whatever the failed restore left installed described the wrong project
|
|
1080
|
+
# trust domain. Best-effort: the reset must never mask the original
|
|
1081
|
+
# failure, and the live settings remain a usable baseline without it.
|
|
1082
|
+
try:
|
|
1083
|
+
old_loaded = self._settings_handle.loaded
|
|
1084
|
+
candidate = await asyncio.to_thread(
|
|
1085
|
+
load_settings,
|
|
1086
|
+
project_root=Path(safe_getcwd()),
|
|
1087
|
+
eval_context=self._reload_eval_context(),
|
|
1088
|
+
**self._session_pin_overrides(),
|
|
1089
|
+
)
|
|
1090
|
+
routed, _ = route_restart_settings(reattribute_command_line(candidate, old_loaded), old_loaded)
|
|
1091
|
+
self._settings_handle.install(routed)
|
|
1092
|
+
except Exception:
|
|
1093
|
+
logger.warning("Settings re-derivation for the fallback startup root failed", exc_info=True)
|
|
1094
|
+
|
|
1095
|
+
def reset_for_restart(self, session_id: str | None) -> None:
|
|
1096
|
+
"""Reset per-session engine state in preparation for a fresh ``start()``.
|
|
1097
|
+
|
|
1098
|
+
The approval mode is not per-session state: the mode the user last chose stays in
|
|
1099
|
+
force for every session of this launch, and only the next launch reads the saved default.
|
|
1100
|
+
"""
|
|
1101
|
+
self._fsm.reset()
|
|
1102
|
+
self._session.reset(session_id=session_id, workspace=self._session.workspace)
|
|
1103
|
+
self._workspace_change_tracker.reset_for_restart()
|
|
1104
|
+
|
|
1105
|
+
async def reset_session_to_welcome(
|
|
1106
|
+
self,
|
|
1107
|
+
session_id: str,
|
|
1108
|
+
*,
|
|
1109
|
+
write_lock_held: bool = False,
|
|
1110
|
+
after_delete: Callable[[], Awaitable[None]] | None = None,
|
|
1111
|
+
before_restart: Callable[[], None] | None = None,
|
|
1112
|
+
) -> bool:
|
|
1113
|
+
"""Delete ``session.json`` and snapshots, then reload to a welcome state."""
|
|
1114
|
+
transition_owner = (
|
|
1115
|
+
None
|
|
1116
|
+
if self._permits.current_task_owns_session_transition_permit()
|
|
1117
|
+
else await self._permits.begin_session_transition("reset")
|
|
1118
|
+
)
|
|
1119
|
+
reset_lock: FileLock | None = None
|
|
1120
|
+
try:
|
|
1121
|
+
session_dir = None
|
|
1122
|
+
lock_path = None
|
|
1123
|
+
if self._persistence.state_store is not None:
|
|
1124
|
+
session_dir = self._persistence.state_store.session_dir(session_id)
|
|
1125
|
+
if session_dir is not None and session_dir.is_dir():
|
|
1126
|
+
lock_path = self._session.session_write_lock_path(session_id)
|
|
1127
|
+
if not write_lock_held:
|
|
1128
|
+
await self._writer.flush()
|
|
1129
|
+
if lock_path is not None:
|
|
1130
|
+
reset_lock = _acquire_session_write_lock(lock_path, session_id)
|
|
1131
|
+
if reset_lock is None:
|
|
1132
|
+
return False
|
|
1133
|
+
|
|
1134
|
+
profile = self._session.agent_profile
|
|
1135
|
+
restore_state = (
|
|
1136
|
+
_reset_restore_history_state(self._current.loaded.bindings.backend.history_state)
|
|
1137
|
+
if self._current.loaded is not None
|
|
1138
|
+
else None
|
|
1139
|
+
)
|
|
1140
|
+
restore_mutations = (
|
|
1141
|
+
copy.deepcopy(self._session.mutation_tracker.serialize())
|
|
1142
|
+
if self._session.mutation_tracker is not None
|
|
1143
|
+
else None
|
|
1144
|
+
)
|
|
1145
|
+
if restore_state is not None and restore_mutations is not None:
|
|
1146
|
+
restore_state["chrys_mutations"] = copy.deepcopy(restore_mutations)
|
|
1147
|
+
restore_todos = self._session.todo_tracker.serialize() if self._session.todo_tracker is not None else None
|
|
1148
|
+
if restore_state is not None and restore_todos:
|
|
1149
|
+
restore_state["chrys_todos"] = copy.deepcopy(restore_todos)
|
|
1150
|
+
restore_turn_number = self._session.turn_number
|
|
1151
|
+
restore_runtime_meta = copy.deepcopy(self._session.runtime_meta)
|
|
1152
|
+
with self._session.saves_suppressed():
|
|
1153
|
+
await self.close_session_in_place()
|
|
1154
|
+
self.reset_turn_runtime_after_session_shutdown()
|
|
1155
|
+
|
|
1156
|
+
if session_dir is not None and session_dir.is_dir():
|
|
1157
|
+
try:
|
|
1158
|
+
_delete_reset_session_files(session_dir)
|
|
1159
|
+
snap_dir = session_dir / "snapshots"
|
|
1160
|
+
if snap_dir.is_dir():
|
|
1161
|
+
shutil.rmtree(snap_dir, ignore_errors=True)
|
|
1162
|
+
except OSError:
|
|
1163
|
+
logger.warning("Failed to delete session files for reset %s", session_id, exc_info=True)
|
|
1164
|
+
if reset_lock is not None:
|
|
1165
|
+
reset_lock.release()
|
|
1166
|
+
reset_lock = None
|
|
1167
|
+
if before_restart is not None:
|
|
1168
|
+
before_restart()
|
|
1169
|
+
await self._restart_after_failed_reset(
|
|
1170
|
+
session_id=session_id,
|
|
1171
|
+
profile=profile,
|
|
1172
|
+
state=restore_state,
|
|
1173
|
+
mutations=restore_mutations,
|
|
1174
|
+
todos=restore_todos,
|
|
1175
|
+
turn_number=restore_turn_number,
|
|
1176
|
+
runtime_meta=restore_runtime_meta,
|
|
1177
|
+
)
|
|
1178
|
+
return False
|
|
1179
|
+
|
|
1180
|
+
if after_delete is not None:
|
|
1181
|
+
await after_delete()
|
|
1182
|
+
if session_dir is not None and session_dir.is_dir():
|
|
1183
|
+
# The restart below keeps this session id, so the directory is
|
|
1184
|
+
# written to again: a delete that cannot finish now must leave
|
|
1185
|
+
# no intent naming the path the new run lands in.
|
|
1186
|
+
cleanup_empty_session_dir_path(session_dir, path_reused=True)
|
|
1187
|
+
|
|
1188
|
+
# Both the normal directory path and a concurrently removed
|
|
1189
|
+
# directory reach the restart below. The new runtime can activate
|
|
1190
|
+
# trajectory recording from a session_start hook, which reacquires
|
|
1191
|
+
# this same path-keyed lock, so ownership must end before restart
|
|
1192
|
+
# regardless of what happened to the directory entry.
|
|
1193
|
+
if reset_lock is not None:
|
|
1194
|
+
reset_lock.release()
|
|
1195
|
+
reset_lock = None
|
|
1196
|
+
if before_restart is not None:
|
|
1197
|
+
before_restart()
|
|
1198
|
+
|
|
1199
|
+
self.reset_for_restart(session_id)
|
|
1200
|
+
await self._reconcile_existing_spill_storage()
|
|
1201
|
+
if profile is None:
|
|
1202
|
+
await self._bus.publish(
|
|
1203
|
+
Error(
|
|
1204
|
+
code="no_agent_profile",
|
|
1205
|
+
message="Agent profile not configured.",
|
|
1206
|
+
session_id=session_id,
|
|
1207
|
+
),
|
|
1208
|
+
)
|
|
1209
|
+
return True
|
|
1210
|
+
await self.start(profile, operation="reset")
|
|
1211
|
+
return True
|
|
1212
|
+
finally:
|
|
1213
|
+
if reset_lock is not None:
|
|
1214
|
+
reset_lock.release()
|
|
1215
|
+
if transition_owner is not None:
|
|
1216
|
+
self._permits.finish_session_transition(transition_owner)
|
|
1217
|
+
|
|
1218
|
+
def cleanup_empty_session_dir(self) -> None:
|
|
1219
|
+
"""Remove the current session directory if it was never saved."""
|
|
1220
|
+
cleanup_empty_session_dir_path(self._session.session_dir)
|
|
1221
|
+
|
|
1222
|
+
async def _restart_after_failed_reset(
|
|
1223
|
+
self,
|
|
1224
|
+
*,
|
|
1225
|
+
session_id: str,
|
|
1226
|
+
profile: AgentProfile | None,
|
|
1227
|
+
state: dict | None,
|
|
1228
|
+
mutations: dict[str, Any] | None,
|
|
1229
|
+
todos: list[dict[str, str]] | None,
|
|
1230
|
+
turn_number: int,
|
|
1231
|
+
runtime_meta: SessionRuntimeMetadata,
|
|
1232
|
+
) -> None:
|
|
1233
|
+
self.reset_for_restart(session_id)
|
|
1234
|
+
await self._reconcile_existing_spill_storage()
|
|
1235
|
+
self._session.restore_position(runtime_meta=runtime_meta, turn_number=turn_number)
|
|
1236
|
+
mutation_state = (
|
|
1237
|
+
mutations if mutations is not None else state.get("chrys_mutations") if state is not None else None
|
|
1238
|
+
)
|
|
1239
|
+
if mutation_state is not None:
|
|
1240
|
+
session_dir = self._session.session_dir
|
|
1241
|
+
if session_dir is not None:
|
|
1242
|
+
snapshot_store = SnapshotStore(
|
|
1243
|
+
session_dir, policy=SnapshotPolicy.from_settings(self._settings_handle.settings)
|
|
1244
|
+
)
|
|
1245
|
+
self._session.mutation_tracker = MutationTracker.deserialize(mutation_state, snapshot_store)
|
|
1246
|
+
if state is not None:
|
|
1247
|
+
self._workspace_change_tracker.restore(
|
|
1248
|
+
state.get("chrys_workspace_baseline"),
|
|
1249
|
+
self._session.workspace,
|
|
1250
|
+
resolve_scope=self._settings_handle.settings.workspace_change_notice,
|
|
1251
|
+
)
|
|
1252
|
+
# Rehydrate the todo tracker from the pre-shutdown snapshot: reattaching
|
|
1253
|
+
# ``history_state`` alone is not enough — save reads the TRACKER, so an
|
|
1254
|
+
# empty one would pop ``chrys_todos`` on the next save.
|
|
1255
|
+
todo_state = todos if todos is not None else state.get("chrys_todos") if state is not None else None
|
|
1256
|
+
if todo_state:
|
|
1257
|
+
self._session.todo_tracker = TodoTracker()
|
|
1258
|
+
await self._session.todo_tracker.restore(todo_state)
|
|
1259
|
+
if profile is None:
|
|
1260
|
+
return
|
|
1261
|
+
try:
|
|
1262
|
+
await self.start(profile, operation="reset_failed")
|
|
1263
|
+
except Exception:
|
|
1264
|
+
logger.warning("Failed to restart session after reset failure %s", session_id, exc_info=True)
|
|
1265
|
+
return
|
|
1266
|
+
if state is not None and self._current.loaded is not None:
|
|
1267
|
+
if mutation_state is not None:
|
|
1268
|
+
state["chrys_mutations"] = copy.deepcopy(mutation_state)
|
|
1269
|
+
if todo_state:
|
|
1270
|
+
state["chrys_todos"] = copy.deepcopy(todo_state)
|
|
1271
|
+
self._current.loaded.bindings.backend.history_state = state
|
|
1272
|
+
if self._current.loaded is not None:
|
|
1273
|
+
loaded = self._current.loaded
|
|
1274
|
+
restore_phase4_state(loaded.reminder_middleware, loaded.last_words, state)
|
|
1275
|
+
stamp_history_item_ids(self._current.loaded.bindings.backend.history_state)
|
|
1276
|
+
self._history.bind(self._current.loaded.bindings.backend.history_state)
|
|
1277
|
+
|
|
1278
|
+
async def on_new_session(self, _event: SessionNew) -> None:
|
|
1279
|
+
"""Handle new session request: save current session, then start fresh."""
|
|
1280
|
+
transition_owner = await self._permits.begin_session_transition("new_session")
|
|
1281
|
+
try:
|
|
1282
|
+
# Sample the live profile after the transition fence so a queued
|
|
1283
|
+
# profile switch that won the rebuild gate determines the new session.
|
|
1284
|
+
profile = self._session.agent_profile
|
|
1285
|
+
if profile is None:
|
|
1286
|
+
return
|
|
1287
|
+
await self._start_fresh_session(profile)
|
|
1288
|
+
finally:
|
|
1289
|
+
self._permits.finish_session_transition(transition_owner)
|
|
1290
|
+
|
|
1291
|
+
async def _start_fresh_session(self, profile: AgentProfile) -> None:
|
|
1292
|
+
"""Shut the current session down and start an empty one under *profile*.
|
|
1293
|
+
|
|
1294
|
+
Callers hold a COMMITTED session transition (``_begin_session_transition``
|
|
1295
|
+
or prepare + ``_commit_session_transition``); the shutdown here relies on
|
|
1296
|
+
the fence to have already invalidated the old session's turn state.
|
|
1297
|
+
"""
|
|
1298
|
+
await self.close_session()
|
|
1299
|
+
self.reset_turn_runtime_after_session_shutdown()
|
|
1300
|
+
self.cleanup_empty_session_dir()
|
|
1301
|
+
self.reset_for_restart(None)
|
|
1302
|
+
await self.start(profile, operation="new_session")
|
|
1303
|
+
|
|
1304
|
+
async def on_session_clear(self, event: SessionClear) -> None:
|
|
1305
|
+
"""Delete the ACTIVE session and start a fresh one as one fenced transition.
|
|
1306
|
+
|
|
1307
|
+
Prompt admission is closed before the deletion starts and stays closed
|
|
1308
|
+
until the fresh session is ready, so no prompt can be admitted against the
|
|
1309
|
+
detached session in between. The transition is committed only after the
|
|
1310
|
+
delete succeeded: a failed delete leaves the current session and its turn
|
|
1311
|
+
state intact (admission simply reopens), reports
|
|
1312
|
+
``Error(code="session_clear_failed")``, and never starts a new session.
|
|
1313
|
+
"""
|
|
1314
|
+
transition_owner = await self._permits.prepare_session_transition("clear")
|
|
1315
|
+
if transition_owner is None: # No owner filter was given, so this cannot happen.
|
|
1316
|
+
raise RuntimeError("Session transition acquisition unexpectedly failed")
|
|
1317
|
+
failure_message: str | None = None
|
|
1318
|
+
try:
|
|
1319
|
+
# Sample under the fence: a queued restore/new/switch cannot move the
|
|
1320
|
+
# active session while we hold the gate.
|
|
1321
|
+
profile = self._session.agent_profile
|
|
1322
|
+
if not self._session.guard.owns(event.session_id):
|
|
1323
|
+
failure_message = "Only the active session can be cleared"
|
|
1324
|
+
elif profile is None:
|
|
1325
|
+
failure_message = "No agent profile is active"
|
|
1326
|
+
else:
|
|
1327
|
+
failure = await self._delete_session_reporting(event.session_id)
|
|
1328
|
+
if failure is not None:
|
|
1329
|
+
failure_message = failure.message
|
|
1330
|
+
else:
|
|
1331
|
+
self._permits.commit_session_transition(transition_owner)
|
|
1332
|
+
await self._start_fresh_session(profile)
|
|
1333
|
+
finally:
|
|
1334
|
+
self._permits.finish_session_transition(transition_owner)
|
|
1335
|
+
# Published after the fence is released — an Error handler must not find
|
|
1336
|
+
# the gate still held by the failed clear.
|
|
1337
|
+
if failure_message is not None:
|
|
1338
|
+
await self._bus.publish(
|
|
1339
|
+
Error(code="session_clear_failed", message=failure_message, session_id=event.session_id),
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
async def on_session_restore(self, event: SessionRestore) -> None:
|
|
1343
|
+
"""Handle session restore by loading saved state and rebuilding the agent."""
|
|
1344
|
+
from chrys.service.state.locks import SESSION_RESTORE_ACTIVE_LOCK_TIMEOUT_SECONDS
|
|
1345
|
+
|
|
1346
|
+
if self._persistence.state_store is None:
|
|
1347
|
+
await self._bus.publish(
|
|
1348
|
+
Error(code="no_state_store", message="State store not configured", session_id=event.session_id)
|
|
1349
|
+
)
|
|
1350
|
+
return
|
|
1351
|
+
|
|
1352
|
+
restoring_current = self._session.guard.owns(event.session_id)
|
|
1353
|
+
target_lock: FileLock | None = None
|
|
1354
|
+
transition_owner: str | None = None
|
|
1355
|
+
if not restoring_current:
|
|
1356
|
+
try:
|
|
1357
|
+
target_lock = await asyncio.to_thread(
|
|
1358
|
+
self._session.guard.acquire_for_restore,
|
|
1359
|
+
event.session_id,
|
|
1360
|
+
timeout=SESSION_RESTORE_ACTIVE_LOCK_TIMEOUT_SECONDS,
|
|
1361
|
+
)
|
|
1362
|
+
except TimeoutError:
|
|
1363
|
+
await self._bus.publish(
|
|
1364
|
+
Error(
|
|
1365
|
+
code="session_in_use",
|
|
1366
|
+
message=self._session.guard.conflict_message(event.session_id),
|
|
1367
|
+
session_id=event.session_id,
|
|
1368
|
+
),
|
|
1369
|
+
)
|
|
1370
|
+
return
|
|
1371
|
+
|
|
1372
|
+
try:
|
|
1373
|
+
if event.ignore_recovery:
|
|
1374
|
+
try:
|
|
1375
|
+
await asyncio.to_thread(self._persistence.state_store.delete_recovery_session, event.session_id)
|
|
1376
|
+
except Exception:
|
|
1377
|
+
logger.debug("Failed to delete ignored recovery sidecar for %s", event.session_id, exc_info=True)
|
|
1378
|
+
|
|
1379
|
+
prefer_recovery = not event.ignore_recovery and (
|
|
1380
|
+
not restoring_current or self._session.recovered_from_sidecar
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
state = await self._persistence.load_session(event.session_id, prefer_recovery=prefer_recovery)
|
|
1384
|
+
if state is None:
|
|
1385
|
+
if event.ignore_recovery and not restoring_current:
|
|
1386
|
+
cleanup_empty_session_dir_path(self._persistence.state_store.session_dir(event.session_id))
|
|
1387
|
+
await self._bus.publish(
|
|
1388
|
+
Error(
|
|
1389
|
+
code="session_not_found",
|
|
1390
|
+
message=f"Session '{event.session_id}' not found",
|
|
1391
|
+
session_id=event.session_id,
|
|
1392
|
+
)
|
|
1393
|
+
)
|
|
1394
|
+
return
|
|
1395
|
+
|
|
1396
|
+
# Resolve meta by id directly. ``list_sessions`` relies on
|
|
1397
|
+
# ``iterdir`` which can briefly miss a just-written session
|
|
1398
|
+
# folder on Windows; a direct envelope read avoids that race.
|
|
1399
|
+
meta = await self._persistence.load_session_meta(event.session_id, prefer_recovery=prefer_recovery)
|
|
1400
|
+
if meta is not None and meta.kind == "workflow":
|
|
1401
|
+
await self._bus.publish(
|
|
1402
|
+
Error(
|
|
1403
|
+
code="session_kind_mismatch",
|
|
1404
|
+
message="Open this session in Workflow mode.",
|
|
1405
|
+
session_id=event.session_id,
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1408
|
+
return
|
|
1409
|
+
recovered_from_sidecar = (
|
|
1410
|
+
await self._persistence.recovery_session_wins(event.session_id) if prefer_recovery else False
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
cwd_warning = ""
|
|
1414
|
+
saved_cwd = meta.primary_cwd if meta and meta.primary_cwd else ""
|
|
1415
|
+
cwd_overridden = bool(event.primary_cwd) and event.primary_cwd != saved_cwd
|
|
1416
|
+
target_cwd = event.primary_cwd or saved_cwd or self._workspace_cwd()
|
|
1417
|
+
target_cwd_exists = os.path.isdir(target_cwd)
|
|
1418
|
+
if target_cwd and not target_cwd_exists:
|
|
1419
|
+
if not restoring_current:
|
|
1420
|
+
# Never switch to a session whose directory is gone; the
|
|
1421
|
+
# frontend lets the user pick another one (``primary_cwd``).
|
|
1422
|
+
# Rolling back the current session keeps it and only warns.
|
|
1423
|
+
await self._bus.publish(
|
|
1424
|
+
Error(
|
|
1425
|
+
code="session_cwd_missing",
|
|
1426
|
+
message=(
|
|
1427
|
+
f"Working directory of session {session_short_id(event.session_id)} "
|
|
1428
|
+
f"no longer exists: {surrogate_safe_text(target_cwd)}"
|
|
1429
|
+
),
|
|
1430
|
+
display_message=_RESTORE_SESSION_CWD_MISSING.bind(path=DisplayPath(target_cwd)),
|
|
1431
|
+
session_id=event.session_id,
|
|
1432
|
+
)
|
|
1433
|
+
)
|
|
1434
|
+
return
|
|
1435
|
+
cwd_warning = f"Working directory no longer exists: {surrogate_safe_text(target_cwd)}"
|
|
1436
|
+
|
|
1437
|
+
profile_name = event.profile_name or (meta.agent_profile if meta else "")
|
|
1438
|
+
saved_profile_id = meta.agent_profile_id if meta else ""
|
|
1439
|
+
profile_selector = event.profile_name or saved_profile_id or profile_name
|
|
1440
|
+
profile: AgentProfile | None = None
|
|
1441
|
+
profile_switch: tuple[str, str] | None = None
|
|
1442
|
+
profile_resolution_warning: Warning | None = None
|
|
1443
|
+
if profile_selector and self._agent_registry:
|
|
1444
|
+
if event.profile_name:
|
|
1445
|
+
resolved = self._agent_registry.resolve_selector(event.profile_name)
|
|
1446
|
+
elif saved_profile_id:
|
|
1447
|
+
resolved = self._agent_registry.get_by_id(
|
|
1448
|
+
saved_profile_id,
|
|
1449
|
+
disambiguating_name=profile_name,
|
|
1450
|
+
)
|
|
1451
|
+
else:
|
|
1452
|
+
resolved = self._agent_registry.get(profile_name)
|
|
1453
|
+
if resolved is not None:
|
|
1454
|
+
profile = resolved
|
|
1455
|
+
profile_name = resolved.name
|
|
1456
|
+
if event.profile_name and meta and meta.agent_profile and meta.agent_profile != resolved.name:
|
|
1457
|
+
from_display = meta.agent_display_name or meta.agent_profile
|
|
1458
|
+
to_display = resolved.display_name or resolved.name
|
|
1459
|
+
if _restore_profile_override_switch(
|
|
1460
|
+
state,
|
|
1461
|
+
from_profile=meta.agent_profile,
|
|
1462
|
+
from_display=from_display,
|
|
1463
|
+
to_profile=resolved.name,
|
|
1464
|
+
to_display=to_display,
|
|
1465
|
+
):
|
|
1466
|
+
profile_switch = (from_display, to_display)
|
|
1467
|
+
else:
|
|
1468
|
+
saved_profile_name = profile_name
|
|
1469
|
+
if event.profile_name:
|
|
1470
|
+
logger.warning(
|
|
1471
|
+
"Requested agent profile '%s' could not be resolved; stopping session restore",
|
|
1472
|
+
event.profile_name,
|
|
1473
|
+
)
|
|
1474
|
+
await self._bus.publish(
|
|
1475
|
+
Error(
|
|
1476
|
+
code="requested_agent_profile_unresolved",
|
|
1477
|
+
message=(
|
|
1478
|
+
f"Requested agent profile '{event.profile_name}' could not be resolved; "
|
|
1479
|
+
"session restore was stopped."
|
|
1480
|
+
),
|
|
1481
|
+
display_message=_RESTORE_REQUESTED_AGENT_PROFILE_UNRESOLVED.bind(
|
|
1482
|
+
profile=event.profile_name
|
|
1483
|
+
),
|
|
1484
|
+
session_id=event.session_id,
|
|
1485
|
+
)
|
|
1486
|
+
)
|
|
1487
|
+
return
|
|
1488
|
+
unresolved_saved_identity = bool(saved_profile_id)
|
|
1489
|
+
saved_display = (meta.agent_display_name if meta else "") or saved_profile_name or saved_profile_id
|
|
1490
|
+
if self._session.agent_profile is not None:
|
|
1491
|
+
profile_name = self._session.agent_profile.name
|
|
1492
|
+
logger.warning(
|
|
1493
|
+
"Agent profile '%s' not found, keeping current profile '%s'",
|
|
1494
|
+
saved_profile_name,
|
|
1495
|
+
profile_name,
|
|
1496
|
+
)
|
|
1497
|
+
if unresolved_saved_identity:
|
|
1498
|
+
current_display = self._session.agent_profile.display_name or profile_name
|
|
1499
|
+
profile_resolution_warning = Warning(
|
|
1500
|
+
code="saved_agent_profile_unresolved",
|
|
1501
|
+
message=(
|
|
1502
|
+
f"Saved agent profile id '{saved_profile_id}' for '{saved_profile_name}' "
|
|
1503
|
+
f"could not be uniquely resolved; keeping current profile '{profile_name}'."
|
|
1504
|
+
),
|
|
1505
|
+
display_message=_RESTORE_AGENT_PROFILE_UNRESOLVED_USING_CURRENT.bind(
|
|
1506
|
+
saved=saved_display,
|
|
1507
|
+
current=current_display,
|
|
1508
|
+
),
|
|
1509
|
+
session_id=event.session_id,
|
|
1510
|
+
)
|
|
1511
|
+
elif unresolved_saved_identity:
|
|
1512
|
+
logger.warning(
|
|
1513
|
+
"Agent profile id '%s' for saved profile '%s' could not be uniquely resolved; "
|
|
1514
|
+
"stopping restore because no current profile is active",
|
|
1515
|
+
saved_profile_id,
|
|
1516
|
+
saved_profile_name,
|
|
1517
|
+
)
|
|
1518
|
+
await self._bus.publish(
|
|
1519
|
+
Error(
|
|
1520
|
+
code="saved_agent_profile_unresolved",
|
|
1521
|
+
message=(
|
|
1522
|
+
f"Saved agent profile id '{saved_profile_id}' for '{saved_profile_name}' "
|
|
1523
|
+
"could not be uniquely resolved, and no current profile is active."
|
|
1524
|
+
),
|
|
1525
|
+
display_message=_RESTORE_AGENT_PROFILE_UNRESOLVED.bind(saved=saved_display),
|
|
1526
|
+
session_id=event.session_id,
|
|
1527
|
+
)
|
|
1528
|
+
)
|
|
1529
|
+
return
|
|
1530
|
+
else:
|
|
1531
|
+
available = self._agent_registry.list_profiles()
|
|
1532
|
+
if available:
|
|
1533
|
+
profile = available[0]
|
|
1534
|
+
logger.warning(
|
|
1535
|
+
"Agent profile '%s' not found, falling back to '%s'",
|
|
1536
|
+
saved_profile_name,
|
|
1537
|
+
profile.name,
|
|
1538
|
+
)
|
|
1539
|
+
profile_name = profile.name
|
|
1540
|
+
|
|
1541
|
+
if event.working_dirs is not None:
|
|
1542
|
+
# Client-supplied additionalDirectories are authoritative (ACP load
|
|
1543
|
+
# semantics): the list REPLACES the saved roots so a client can narrow,
|
|
1544
|
+
# swap, or clear (empty list) workspace scope. Keep the primary first to
|
|
1545
|
+
# mirror new-session storage; service-session compatibility canonicalizes
|
|
1546
|
+
# away the primary (see _extra_roots), so its inclusion here is cosmetic.
|
|
1547
|
+
merged_dirs = [target_cwd]
|
|
1548
|
+
seen_dirs: set[str] = {target_cwd}
|
|
1549
|
+
for path in event.working_dirs:
|
|
1550
|
+
if path and path not in seen_dirs:
|
|
1551
|
+
seen_dirs.add(path)
|
|
1552
|
+
merged_dirs.append(path)
|
|
1553
|
+
else:
|
|
1554
|
+
# Caller did not specify roots: keep the saved working_dirs verbatim (it may
|
|
1555
|
+
# include the primary cwd, and _can_restore_service_session compares the path
|
|
1556
|
+
# list exactly). A moved cwd invalidates the saved layout, so drop it.
|
|
1557
|
+
merged_dirs = [] if cwd_overridden else list(meta.working_dirs if meta else [])
|
|
1558
|
+
session_ws = Workspace(
|
|
1559
|
+
primary_cwd=target_cwd,
|
|
1560
|
+
working_dirs=_working_dirs_from_paths(merged_dirs, target_cwd),
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
# The transition boundary is taken in two phases. The *prepared* fence
|
|
1564
|
+
# comes first: it shares the rebuild gate with settings reloads and
|
|
1565
|
+
# model switches, so everything read below — loaded settings, eval
|
|
1566
|
+
# context, session pins — is a committed state no concurrent rebuild
|
|
1567
|
+
# can move. Snapshotting before the fence would let a reload commit
|
|
1568
|
+
# during the load's thread hop and then be silently overwritten by a
|
|
1569
|
+
# staged load routed against the stale copy. But the *commit* — which
|
|
1570
|
+
# bumps the session generation and invalidates the old session's turn
|
|
1571
|
+
# state, retries and injection — waits until the load has succeeded:
|
|
1572
|
+
# an unreadable config file must abort the restore with the current
|
|
1573
|
+
# session genuinely intact, not admission-open but generation-dead.
|
|
1574
|
+
if not self._permits.current_task_owns_session_transition_permit():
|
|
1575
|
+
transition_owner = await self._permits.prepare_session_transition("restore")
|
|
1576
|
+
if transition_owner is None: # No owner filter was given, so this cannot happen.
|
|
1577
|
+
raise RuntimeError("Session transition acquisition unexpectedly failed")
|
|
1578
|
+
|
|
1579
|
+
# The restore crosses into the target session's project trust domain:
|
|
1580
|
+
# its settings are re-derived for that root so the build below reads
|
|
1581
|
+
# them, exactly as a workspace change would. Loaded here — before
|
|
1582
|
+
# anything is torn down — so an unreadable config file aborts the
|
|
1583
|
+
# restore with the current session intact.
|
|
1584
|
+
old_loaded = self._settings_handle.loaded
|
|
1585
|
+
candidate = await asyncio.to_thread(
|
|
1586
|
+
load_settings,
|
|
1587
|
+
project_root=Path(target_cwd),
|
|
1588
|
+
eval_context=self._reload_eval_context(),
|
|
1589
|
+
**self._session_pin_overrides(),
|
|
1590
|
+
)
|
|
1591
|
+
staged_loaded, _ = route_restart_settings(reattribute_command_line(candidate, old_loaded), old_loaded)
|
|
1592
|
+
if profile is None:
|
|
1593
|
+
profile = self._session.agent_profile
|
|
1594
|
+
if not profile_name and profile is not None:
|
|
1595
|
+
profile_name = profile.name
|
|
1596
|
+
if transition_owner is not None:
|
|
1597
|
+
self._permits.commit_session_transition(transition_owner)
|
|
1598
|
+
if restoring_current:
|
|
1599
|
+
await self.close_session_in_place()
|
|
1600
|
+
else:
|
|
1601
|
+
await self.close_session()
|
|
1602
|
+
self.reset_turn_runtime_after_session_shutdown()
|
|
1603
|
+
self.cleanup_empty_session_dir()
|
|
1604
|
+
|
|
1605
|
+
self._session.workspace = session_ws
|
|
1606
|
+
|
|
1607
|
+
if target_lock is not None:
|
|
1608
|
+
self._session.guard.install(event.session_id, target_lock)
|
|
1609
|
+
target_lock = None
|
|
1610
|
+
|
|
1611
|
+
await self._hydrate_restored_session(
|
|
1612
|
+
event=event,
|
|
1613
|
+
state=state,
|
|
1614
|
+
meta=meta,
|
|
1615
|
+
profile=profile,
|
|
1616
|
+
profile_name=profile_name,
|
|
1617
|
+
profile_switch=profile_switch,
|
|
1618
|
+
profile_resolution_warning=profile_resolution_warning,
|
|
1619
|
+
recovered_from_sidecar=recovered_from_sidecar,
|
|
1620
|
+
cwd_warning=cwd_warning,
|
|
1621
|
+
target_cwd=target_cwd,
|
|
1622
|
+
staged_loaded=staged_loaded,
|
|
1623
|
+
)
|
|
1624
|
+
except BaseException as exc:
|
|
1625
|
+
if transition_owner is not None:
|
|
1626
|
+
self._permits.finish_session_transition(transition_owner)
|
|
1627
|
+
transition_owner = None
|
|
1628
|
+
if isinstance(exc, Exception):
|
|
1629
|
+
# The interactive callers publish this event with the bus's
|
|
1630
|
+
# default swallow-and-log delivery: without a terminal event the
|
|
1631
|
+
# restore loading UI has nothing to clear it. Published after the
|
|
1632
|
+
# fence is released — an Error handler must not find the gate
|
|
1633
|
+
# still held by the failed restore.
|
|
1634
|
+
await self._bus.publish(
|
|
1635
|
+
Error(code="session_restore_failed", message=str(exc), session_id=event.session_id)
|
|
1636
|
+
)
|
|
1637
|
+
raise
|
|
1638
|
+
finally:
|
|
1639
|
+
if target_lock is not None:
|
|
1640
|
+
target_lock.release()
|
|
1641
|
+
if transition_owner is not None:
|
|
1642
|
+
self._permits.finish_session_transition(transition_owner)
|
|
1643
|
+
|
|
1644
|
+
async def _hydrate_restored_session(
|
|
1645
|
+
self,
|
|
1646
|
+
*,
|
|
1647
|
+
event: SessionRestore,
|
|
1648
|
+
state: dict[str, Any],
|
|
1649
|
+
meta: ChatSessionMeta | None,
|
|
1650
|
+
profile: AgentProfile | None,
|
|
1651
|
+
profile_name: str,
|
|
1652
|
+
profile_switch: tuple[str, str] | None,
|
|
1653
|
+
profile_resolution_warning: Warning | None,
|
|
1654
|
+
recovered_from_sidecar: bool,
|
|
1655
|
+
cwd_warning: str,
|
|
1656
|
+
target_cwd: str,
|
|
1657
|
+
staged_loaded: LoadedSettings,
|
|
1658
|
+
) -> None:
|
|
1659
|
+
"""Hydrate live engine state after restore shutdown while admission remains closed."""
|
|
1660
|
+
self._fsm.reset()
|
|
1661
|
+
self._session.adopt_restore_identity(session_id=event.session_id, recovered_from_sidecar=recovered_from_sidecar)
|
|
1662
|
+
session_dir = self._session.session_dir
|
|
1663
|
+
# The staged settings describe the target session; the live ones still
|
|
1664
|
+
# belong to the previous session until the build below commits them.
|
|
1665
|
+
self._workspace_change_tracker.restore(
|
|
1666
|
+
state.get("chrys_workspace_baseline"),
|
|
1667
|
+
self._session.workspace,
|
|
1668
|
+
resolve_scope=staged_loaded.settings.workspace_change_notice,
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
spill_reconciliation = await self._reconcile_existing_spill_storage()
|
|
1672
|
+
|
|
1673
|
+
# Installing ``event.session_id`` above makes the engine session directory
|
|
1674
|
+
# available for the remainder of successful hydration. The policy reads
|
|
1675
|
+
# the staged settings: they are what the build below commits, and the
|
|
1676
|
+
# tracker hydrated here outlives that commit.
|
|
1677
|
+
snapshot_store = SnapshotStore(
|
|
1678
|
+
cast("Path", session_dir), policy=SnapshotPolicy.from_settings(staged_loaded.settings)
|
|
1679
|
+
)
|
|
1680
|
+
if state and state.get("chrys_mutations"):
|
|
1681
|
+
self._session.mutation_tracker = MutationTracker.deserialize(state["chrys_mutations"], snapshot_store)
|
|
1682
|
+
else:
|
|
1683
|
+
self._session.mutation_tracker = MutationTracker(snapshot_store)
|
|
1684
|
+
# Hydrate todos before ``restore_phase4_state`` below:
|
|
1685
|
+
# ``LastWordsState.restore_last_words`` re-captures the restored note's
|
|
1686
|
+
# todo section from the tracker.
|
|
1687
|
+
self._session.todo_tracker = TodoTracker()
|
|
1688
|
+
if state:
|
|
1689
|
+
await self._session.todo_tracker.restore(state.get("chrys_todos"))
|
|
1690
|
+
# The restored session gets its own coordinator (session id changed;
|
|
1691
|
+
# the previous one was closed during shutdown). Construction builds
|
|
1692
|
+
# it during ``start()`` below.
|
|
1693
|
+
self._session.mutation_coordinator = None
|
|
1694
|
+
|
|
1695
|
+
# Install the target session's runtime metadata BEFORE the build:
|
|
1696
|
+
# construction's ``_publish_results`` hydrates the fresh strategy from
|
|
1697
|
+
# ``engine.session.runtime_meta``, so leaving the previous session's metadata in
|
|
1698
|
+
# place would leak its calibration record into this session's strategy
|
|
1699
|
+
# whenever the build fingerprints happen to match.
|
|
1700
|
+
self._session.runtime_meta = SessionRuntimeMetadata.from_state_dict(state)
|
|
1701
|
+
|
|
1702
|
+
if profile is not None:
|
|
1703
|
+
rollback_token = None
|
|
1704
|
+
if event.apply_saved_model:
|
|
1705
|
+
staged_loaded, rollback_token = self._reapply_saved_model_profile(meta, profile, staged_loaded)
|
|
1706
|
+
try:
|
|
1707
|
+
await self.start(profile, operation="restore", staged_loaded=staged_loaded)
|
|
1708
|
+
except BaseException:
|
|
1709
|
+
if (
|
|
1710
|
+
rollback_token is not None
|
|
1711
|
+
and (self._current.loaded.bindings.backend if self._current.loaded is not None else None)
|
|
1712
|
+
is rollback_token.conversation
|
|
1713
|
+
):
|
|
1714
|
+
_rollback_reapplied_model_profile(rollback_token)
|
|
1715
|
+
raise
|
|
1716
|
+
if profile_switch is not None and self._current.loaded is not None:
|
|
1717
|
+
self._current.loaded.reminder_middleware.sources.profile_switch.set_profile_switch(*profile_switch)
|
|
1718
|
+
else:
|
|
1719
|
+
# Nothing to build, so no commit will install the staged load; this
|
|
1720
|
+
# degenerate path installs it directly, like a reload with nothing
|
|
1721
|
+
# built.
|
|
1722
|
+
self._settings_handle.install(staged_loaded)
|
|
1723
|
+
|
|
1724
|
+
# The verdicts describe the target root's files — the same report a
|
|
1725
|
+
# reload or workspace change makes — published only now that the staged
|
|
1726
|
+
# load is in force (committed by the build above, or installed by the
|
|
1727
|
+
# degenerate path), under the restored session's id.
|
|
1728
|
+
restore_warnings = settings_warning_events(staged_loaded)
|
|
1729
|
+
if profile_resolution_warning is not None:
|
|
1730
|
+
restore_warnings.insert(0, profile_resolution_warning)
|
|
1731
|
+
for warning in restore_warnings:
|
|
1732
|
+
await self._bus.publish(replace(warning, session_id=self._session.session_id))
|
|
1733
|
+
|
|
1734
|
+
self._session.turn_number = state.get("turn_counter", 0) if state else 0
|
|
1735
|
+
|
|
1736
|
+
if self._current.loaded is not None and state:
|
|
1737
|
+
self._current.loaded.bindings.backend.history_state = state
|
|
1738
|
+
if self._current.loaded is not None:
|
|
1739
|
+
loaded = self._current.loaded
|
|
1740
|
+
restore_phase4_state(
|
|
1741
|
+
loaded.reminder_middleware,
|
|
1742
|
+
loaded.last_words,
|
|
1743
|
+
state,
|
|
1744
|
+
available_relative_paths=spill_reconciliation.available_relative_paths,
|
|
1745
|
+
)
|
|
1746
|
+
if meta and meta.service_session_id:
|
|
1747
|
+
if self._can_restore_service_session(meta):
|
|
1748
|
+
self._current.loaded.bindings.backend.service_session_id = meta.service_session_id
|
|
1749
|
+
else:
|
|
1750
|
+
self._current.loaded.bindings.backend.service_session_id = ""
|
|
1751
|
+
await self._bus.publish(
|
|
1752
|
+
Warning(
|
|
1753
|
+
code="service_session_incompatible",
|
|
1754
|
+
message=(
|
|
1755
|
+
"This session was saved with an OpenAI Responses service session. "
|
|
1756
|
+
"The active agent/model profile, workspace, service endpoint, or storage mode "
|
|
1757
|
+
"is not compatible, "
|
|
1758
|
+
f"so {APP_DISPLAY_NAME} will continue from local history only."
|
|
1759
|
+
),
|
|
1760
|
+
display_message=_RESTORE_SERVICE_SESSION_INCOMPATIBLE.bind(app=APP_DISPLAY_NAME),
|
|
1761
|
+
session_id=event.session_id,
|
|
1762
|
+
)
|
|
1763
|
+
)
|
|
1764
|
+
stamp_history_item_ids(self._current.loaded.bindings.backend.history_state)
|
|
1765
|
+
self._history.bind(self._current.loaded.bindings.backend.history_state)
|
|
1766
|
+
|
|
1767
|
+
# ``engine.start`` above already hydrated the fresh strategy through
|
|
1768
|
+
# ``_publish_results``; this covers the no-rebuild path (``profile is
|
|
1769
|
+
# None``) where the surviving executor's strategy is the only target.
|
|
1770
|
+
# The provenance gate makes the repeat call idempotent.
|
|
1771
|
+
if (
|
|
1772
|
+
self._session.runtime_meta.context_calibration is not None
|
|
1773
|
+
and self._current.loaded.bindings.backend.compaction_strategy is not None
|
|
1774
|
+
):
|
|
1775
|
+
self._session.runtime_meta.restore_context_calibration(
|
|
1776
|
+
self._current.loaded.bindings.backend.compaction_strategy,
|
|
1777
|
+
model_profile_fingerprint=self._current.manifest.model_profile_fingerprint,
|
|
1778
|
+
agent_profile_fingerprint=self._current.manifest.agent_profile_fingerprint,
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
restored_history_changed = profile_switch is not None
|
|
1782
|
+
paused_records: list[dict[str, Any]] = []
|
|
1783
|
+
injected_paused_results = 0
|
|
1784
|
+
artifact_service = (
|
|
1785
|
+
SubAgentSessionArtifactService(self._session.session_dir)
|
|
1786
|
+
if self._session.session_dir is not None and self._current.loaded is not None
|
|
1787
|
+
else None
|
|
1788
|
+
)
|
|
1789
|
+
if artifact_service is not None and self._current.loaded is not None:
|
|
1790
|
+
paused_records = artifact_service.drain_paused_records()
|
|
1791
|
+
# Repair dangling sub-agent function_calls even when drain dropped or
|
|
1792
|
+
# quarantined every record (over-cap/corrupt): otherwise the parent
|
|
1793
|
+
# assistant call is left with no tool_result and the next turn is
|
|
1794
|
+
# provider-invalid. The live tool registry resolves dangling calls whose
|
|
1795
|
+
# record did not survive drain — records only enrich the error text.
|
|
1796
|
+
registry_sub_agent_names = (
|
|
1797
|
+
set(self._current.loaded.sub_agent_tools.tool_names())
|
|
1798
|
+
if (self._current.loaded is not None and self._current.loaded.sub_agent_tools is not None)
|
|
1799
|
+
else set()
|
|
1800
|
+
)
|
|
1801
|
+
record_sub_agent_names = {
|
|
1802
|
+
tool_name
|
|
1803
|
+
for record in paused_records
|
|
1804
|
+
if isinstance(tool_name := record.get("tool_name"), str) and tool_name
|
|
1805
|
+
}
|
|
1806
|
+
# Always attempt injection — never gate it on records/registry being
|
|
1807
|
+
# non-empty. A persisted sub-agent function_call self-identifies via its
|
|
1808
|
+
# ``_chrys_tool_kind`` marker, so a dangling call must be repaired even
|
|
1809
|
+
# when drain lost every record AND the restored profile no longer registers
|
|
1810
|
+
# the tool (removed/disabled/depth-skipped). The method self-gates on the
|
|
1811
|
+
# history being bound and does nothing when there is no dangling call.
|
|
1812
|
+
injected_paused_results = self._history.inject_error_results_for_sub_agents(
|
|
1813
|
+
paused_records, registry_sub_agent_names | record_sub_agent_names
|
|
1814
|
+
)
|
|
1815
|
+
restored_history_changed = restored_history_changed or injected_paused_results > 0
|
|
1816
|
+
if paused_records:
|
|
1817
|
+
logger.info(
|
|
1818
|
+
"Session restore: loaded %d paused sub-agent record(s), injected %d error tool_result(s)",
|
|
1819
|
+
len(paused_records),
|
|
1820
|
+
injected_paused_results,
|
|
1821
|
+
)
|
|
1822
|
+
discarded_tools = sorted({r.get("tool_name", "?") for r in paused_records})
|
|
1823
|
+
tool_names = ", ".join(discarded_tools)
|
|
1824
|
+
await self._bus.publish(
|
|
1825
|
+
Warning(
|
|
1826
|
+
code="sub_agents_reload_discarded",
|
|
1827
|
+
message=(
|
|
1828
|
+
f"{len(paused_records)} paused sub-agent(s) from a previous session were discarded: {tool_names}"
|
|
1829
|
+
),
|
|
1830
|
+
display_message=_RESTORE_SUB_AGENTS_DISCARDED.bind(
|
|
1831
|
+
discarded=len(paused_records),
|
|
1832
|
+
names=DisplaySequence(tuple(discarded_tools)),
|
|
1833
|
+
),
|
|
1834
|
+
session_id=event.session_id,
|
|
1835
|
+
),
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
if self._history.is_bound:
|
|
1839
|
+
awaiting_count = sum(
|
|
1840
|
+
1
|
|
1841
|
+
for message in self._history.messages
|
|
1842
|
+
if message.additional_properties.get(HistoryMarkerKind.KEY) == HistoryMarkerKind.AWAITING_SUB_AGENTS
|
|
1843
|
+
)
|
|
1844
|
+
self._history.remove_awaiting_sub_agents_marker()
|
|
1845
|
+
restored_history_changed = restored_history_changed or awaiting_count > 0
|
|
1846
|
+
if (awaiting_count > 0 or injected_paused_results > 0) and self._history.trailing_status_marker() is None:
|
|
1847
|
+
self._history.insert_interrupted_marker(
|
|
1848
|
+
reason=format_message(SUB_AGENT_STATE_DISCARDED_MESSAGE.bind()),
|
|
1849
|
+
source="error",
|
|
1850
|
+
status_code=HistoryMarkerKind.STATUS_SUB_AGENT_STATE_DISCARDED,
|
|
1851
|
+
)
|
|
1852
|
+
restored_history_changed = True
|
|
1853
|
+
if artifact_service is not None:
|
|
1854
|
+
orphaned = artifact_service.reconcile_orphaned_running_logs(self._history.messages, paused_records)
|
|
1855
|
+
if orphaned:
|
|
1856
|
+
logger.info("Session restore: marked %d sub-agent audit log(s) orphaned", orphaned)
|
|
1857
|
+
|
|
1858
|
+
self._restore_terminal_fsm_from_history()
|
|
1859
|
+
|
|
1860
|
+
if restored_history_changed:
|
|
1861
|
+
saved = False
|
|
1862
|
+
try:
|
|
1863
|
+
saved = await self._writer.save_current_session(raise_on_error=bool(paused_records))
|
|
1864
|
+
except Exception:
|
|
1865
|
+
logger.warning("Session restore: failed to save repaired session; preserving paused sub-agent records")
|
|
1866
|
+
if saved and paused_records and artifact_service is not None:
|
|
1867
|
+
artifact_service.finalize_restored_paused_records(paused_records)
|
|
1868
|
+
elif paused_records and artifact_service is not None:
|
|
1869
|
+
artifact_service.archive_unconsumed_restored_paused_records(paused_records)
|
|
1870
|
+
|
|
1871
|
+
await self._bus.publish(
|
|
1872
|
+
SessionRestored(
|
|
1873
|
+
session_id=event.session_id,
|
|
1874
|
+
agent_profile=profile_name,
|
|
1875
|
+
display_name=profile.display_name if profile else "",
|
|
1876
|
+
initial_agent_profile=(
|
|
1877
|
+
meta.agent_profile_history[0]
|
|
1878
|
+
if meta is not None and meta.agent_profile_history
|
|
1879
|
+
else profile.display_name
|
|
1880
|
+
if profile
|
|
1881
|
+
else profile_name
|
|
1882
|
+
),
|
|
1883
|
+
message_count=meta.message_count if meta else 0,
|
|
1884
|
+
cwd_warning=cwd_warning,
|
|
1885
|
+
primary_cwd=target_cwd,
|
|
1886
|
+
recovered_from_sidecar=recovered_from_sidecar,
|
|
1887
|
+
working_dirs=(
|
|
1888
|
+
[working_dir.path for working_dir in self._session.workspace.working_dirs]
|
|
1889
|
+
if self._session.workspace is not None
|
|
1890
|
+
else []
|
|
1891
|
+
),
|
|
1892
|
+
),
|
|
1893
|
+
)
|
|
1894
|
+
# Restore-time UsageUpdate must follow SessionRestored so the TUI has
|
|
1895
|
+
# already bound the new ``main_usage_source_id`` before classifying it as
|
|
1896
|
+
# the session window — otherwise the chat panel keeps stale window tokens
|
|
1897
|
+
# from the previous session. Route through the ordered chain so any
|
|
1898
|
+
# pending sub-agent UsageUpdate that was in-flight before the switch can't
|
|
1899
|
+
# overtake this restored snapshot.
|
|
1900
|
+
self._usage_publisher.enqueue_usage_event(self._usage_publisher.make_usage_event(session_id=event.session_id))
|
|
1901
|
+
await self._fire_session_restored_hook(restored_session_id=event.session_id, profile_name=profile_name)
|
|
1902
|
+
|
|
1903
|
+
async def _fire_session_restored_hook(
|
|
1904
|
+
self,
|
|
1905
|
+
*,
|
|
1906
|
+
restored_session_id: str,
|
|
1907
|
+
profile_name: str,
|
|
1908
|
+
) -> None:
|
|
1909
|
+
if self._session.hook_manager is None:
|
|
1910
|
+
return
|
|
1911
|
+
from chrys.service.hooks.events import HookEvent
|
|
1912
|
+
|
|
1913
|
+
if not self._session.hook_manager.has_hooks_for(HookEvent.SESSION_RESTORED):
|
|
1914
|
+
return
|
|
1915
|
+
await self._session.hook_manager.fire(
|
|
1916
|
+
HookEvent.SESSION_RESTORED,
|
|
1917
|
+
{
|
|
1918
|
+
"session_id": self._session.session_id,
|
|
1919
|
+
"profile": profile_name,
|
|
1920
|
+
"cwd": self._workspace_cwd(),
|
|
1921
|
+
"restored_session_id": restored_session_id,
|
|
1922
|
+
},
|
|
1923
|
+
scope="session",
|
|
1924
|
+
)
|
|
1925
|
+
|
|
1926
|
+
async def on_session_delete(self, event: SessionDelete) -> None:
|
|
1927
|
+
"""Handle session deletion."""
|
|
1928
|
+
failure = await self._delete_session_reporting(event.session_id)
|
|
1929
|
+
if failure is not None:
|
|
1930
|
+
await self._bus.publish(
|
|
1931
|
+
Error(code=failure.code, message=failure.message, session_id=event.session_id),
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
async def _delete_session_reporting(self, session_id: str) -> _SessionDeleteFailure | None:
|
|
1935
|
+
"""Delete *session_id* from disk, detaching the engine first when it is the active session.
|
|
1936
|
+
|
|
1937
|
+
Publishes ``SessionDeleted`` on success and returns ``None``; on failure
|
|
1938
|
+
returns the reason with the engine's session ownership restored, leaving
|
|
1939
|
+
the caller to report it (``on_session_delete`` publishes the raw code,
|
|
1940
|
+
``on_session_clear`` folds it into ``session_clear_failed``).
|
|
1941
|
+
"""
|
|
1942
|
+
if self._persistence.state_store is None:
|
|
1943
|
+
return _SessionDeleteFailure(code="no_state_store", message="State store not configured")
|
|
1944
|
+
|
|
1945
|
+
release_current_lock = self._session.guard.owns(session_id)
|
|
1946
|
+
detached_session_id = self._session.session_id if release_current_lock else None
|
|
1947
|
+
otel_sink = None
|
|
1948
|
+
if release_current_lock:
|
|
1949
|
+
from chrys.foundation.observability.sink import get_otel_sink
|
|
1950
|
+
|
|
1951
|
+
# Deleting the live session is its end: fire ``session_end`` now and
|
|
1952
|
+
# wait for it (async hooks included), while the id and the files still
|
|
1953
|
+
# exist, so hooks keep the "before teardown" contract; the shutdown
|
|
1954
|
+
# that follows (clear / delete-current -> new) skips the duplicate. A
|
|
1955
|
+
# failed delete re-arms it below — the session then lives on and hooks
|
|
1956
|
+
# see one early ``session_end`` plus the real one at shutdown. This
|
|
1957
|
+
# duplicate is accepted by design: exactly-once would need the hook to
|
|
1958
|
+
# run only after the delete is known to succeed, i.e. under the store's
|
|
1959
|
+
# write lock from a thread (rejected — user hooks may touch the store),
|
|
1960
|
+
# and NOT re-arming would instead drop the real ``session_end`` of a
|
|
1961
|
+
# session that keeps going, which loses more than a repeat costs.
|
|
1962
|
+
await self.fire_session_end_hooks()
|
|
1963
|
+
# The trajectory writer holds the log's lease, and a leased directory
|
|
1964
|
+
# is tombstoned instead of removed — close it so the delete below is
|
|
1965
|
+
# physical.
|
|
1966
|
+
await self.close_trajectory_log()
|
|
1967
|
+
otel_sink = get_otel_sink()
|
|
1968
|
+
self._session.detach_for_delete()
|
|
1969
|
+
|
|
1970
|
+
delete_succeeded = False
|
|
1971
|
+
# Not cancellation-safe on purpose: the delete runs in a thread and keeps
|
|
1972
|
+
# going if this task is cancelled, and ``CancelledError`` skips the restore
|
|
1973
|
+
# branches below, leaving the engine detached with the guard held. The
|
|
1974
|
+
# only publisher of ``SessionClear``/``SessionDelete`` is a MainScreen
|
|
1975
|
+
# worker, which Textual cancels solely on app exit — and there a detached
|
|
1976
|
+
# engine is exactly right: ``shutdown()`` must not save (resurrect) a
|
|
1977
|
+
# session whose deletion may have just completed. Restoring ``_session_id``
|
|
1978
|
+
# blindly on cancel would do precisely that; shield-and-reconcile would only
|
|
1979
|
+
# release a guard and publish an ack inside an app that is going away.
|
|
1980
|
+
try:
|
|
1981
|
+
await self._persistence.delete_session(session_id, allow_active=release_current_lock)
|
|
1982
|
+
delete_succeeded = True
|
|
1983
|
+
except TimeoutError as exc:
|
|
1984
|
+
if release_current_lock:
|
|
1985
|
+
self._session.reattach_after_failed_delete(detached_session_id)
|
|
1986
|
+
return _SessionDeleteFailure(
|
|
1987
|
+
code="session_busy",
|
|
1988
|
+
message=f"Timed out waiting for session write lock: {exc}",
|
|
1989
|
+
)
|
|
1990
|
+
return _SessionDeleteFailure(
|
|
1991
|
+
code="session_in_use",
|
|
1992
|
+
message=self._session.guard.conflict_message(session_id),
|
|
1993
|
+
)
|
|
1994
|
+
except Exception as exc:
|
|
1995
|
+
logger.exception("Failed to delete session %s", session_id)
|
|
1996
|
+
if release_current_lock:
|
|
1997
|
+
self._session.reattach_after_failed_delete(detached_session_id)
|
|
1998
|
+
return _SessionDeleteFailure(code="session_delete_failed", message=f"Failed to delete session: {exc}")
|
|
1999
|
+
finally:
|
|
2000
|
+
if release_current_lock and delete_succeeded:
|
|
2001
|
+
self._session.guard.release()
|
|
2002
|
+
|
|
2003
|
+
# Post-delete cleanup runs only after the delete has committed. Keep it out
|
|
2004
|
+
# of the try above: a failure here must not reach the delete-failure handler
|
|
2005
|
+
# (which would wrongly restore engine.session.session_id to a now-deleted session)
|
|
2006
|
+
# nor be reported as a delete failure.
|
|
2007
|
+
if release_current_lock and otel_sink is not None:
|
|
2008
|
+
try:
|
|
2009
|
+
otel_sink.deactivate()
|
|
2010
|
+
except Exception:
|
|
2011
|
+
logger.exception("Failed to deactivate OTel sink after deleting session %s", session_id)
|
|
2012
|
+
await self._bus.publish(SessionDeleted(session_id=session_id))
|
|
2013
|
+
return None
|