code-puppy 0.0.784__tar.gz → 0.0.786__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. {code_puppy-0.0.784 → code_puppy-0.0.786}/.gitignore +4 -0
  2. {code_puppy-0.0.784 → code_puppy-0.0.786}/PKG-INFO +5 -4
  3. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_run_signals.py +65 -1
  4. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/claude_cache_client.py +21 -16
  5. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/cli_runner.py +82 -28
  6. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/trust_command.py +5 -2
  7. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/config.py +10 -1
  8. code_puppy-0.0.786/code_puppy/diagnostics.py +124 -0
  9. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/en-US.json +2 -0
  10. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/manager.py +17 -6
  11. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/__init__.py +19 -14
  12. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_surrogate_unpickler.py +3 -11
  13. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_modifications.py +94 -0
  14. {code_puppy-0.0.784 → code_puppy-0.0.786}/pyproject.toml +5 -4
  15. {code_puppy-0.0.784 → code_puppy-0.0.786}/LICENSE +0 -0
  16. {code_puppy-0.0.784 → code_puppy-0.0.786}/README.md +0 -0
  17. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/__init__.py +0 -0
  18. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/__main__.py +0 -0
  19. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agent_execution_context.py +0 -0
  20. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/__init__.py +0 -0
  21. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_builder.py +0 -0
  22. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_compaction.py +0 -0
  23. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_diagnostics.py +0 -0
  24. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_history.py +0 -0
  25. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_key_listeners.py +0 -0
  26. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_model_message_transform.py +0 -0
  27. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_non_streaming_render.py +0 -0
  28. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_output_limits.py +0 -0
  29. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_runtime.py +0 -0
  30. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_steer_processor.py +0 -0
  31. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/_subagent_recursion.py +0 -0
  32. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_code_puppy.py +0 -0
  33. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_creator_agent.py +0 -0
  34. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_helios.py +0 -0
  35. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_manager.py +0 -0
  36. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_model_judge.py +0 -0
  37. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_planning.py +0 -0
  38. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_qa_kitten.py +0 -0
  39. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/agent_web_retriever.py +0 -0
  40. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/base_agent.py +0 -0
  41. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/event_stream_handler.py +0 -0
  42. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/json_agent.py +0 -0
  43. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/retry_profiles.py +0 -0
  44. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/run_stats.py +0 -0
  45. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/smooth_stream.py +0 -0
  46. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/agents/subagent_stream_handler.py +0 -0
  47. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/atomic_io.py +0 -0
  48. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/atomic_json.py +0 -0
  49. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/callbacks.py +0 -0
  50. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/chatgpt_codex_client.py +0 -0
  51. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/__init__.py +0 -0
  52. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/add_model_details.py +0 -0
  53. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/add_model_menu.py +0 -0
  54. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/agent_menu.py +0 -0
  55. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/attachments.py +0 -0
  56. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/autosave_menu.py +0 -0
  57. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/autosave_search.py +0 -0
  58. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/clipboard.py +0 -0
  59. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/colors_menu.py +0 -0
  60. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/command_handler.py +0 -0
  61. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/command_registry.py +0 -0
  62. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/completers.py +0 -0
  63. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/completion_cache.py +0 -0
  64. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/config_apply.py +0 -0
  65. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/config_commands.py +0 -0
  66. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/core_commands.py +0 -0
  67. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/file_index.py +0 -0
  68. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/file_path_completion.py +0 -0
  69. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/help_catalog.py +0 -0
  70. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/help_overlay.py +0 -0
  71. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/image_utils.py +0 -0
  72. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/load_context_completion.py +0 -0
  73. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/__init__.py +0 -0
  74. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/base.py +0 -0
  75. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  76. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  77. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  78. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/edit_command.py +0 -0
  79. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/handler.py +0 -0
  80. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/help_command.py +0 -0
  81. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/install_command.py +0 -0
  82. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/install_menu.py +0 -0
  83. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/list_command.py +0 -0
  84. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/logs_command.py +0 -0
  85. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
  86. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/remove_command.py +0 -0
  87. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/restart_command.py +0 -0
  88. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/search_command.py +0 -0
  89. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
  90. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  91. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/start_command.py +0 -0
  92. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/status_command.py +0 -0
  93. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  94. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/stop_command.py +0 -0
  95. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/utils.py +0 -0
  96. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  97. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp_binding_menu.py +0 -0
  98. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/mcp_completion.py +0 -0
  99. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/menu_session.py +0 -0
  100. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_picker_completion.py +0 -0
  101. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_settings_defs.py +0 -0
  102. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/model_settings_menu.py +0 -0
  103. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/onboarding_slides.py +0 -0
  104. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/onboarding_wizard.py +0 -0
  105. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/pagination.py +0 -0
  106. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/pin_command_completion.py +0 -0
  107. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_browser.py +0 -0
  108. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_browser_data.py +0 -0
  109. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/session_commands.py +0 -0
  110. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu.py +0 -0
  111. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_catalog.py +0 -0
  112. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_schema.py +0 -0
  113. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_settings.py +0 -0
  114. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_shims.py +0 -0
  115. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/set_menu_values.py +0 -0
  116. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/shell_passthrough.py +0 -0
  117. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/skills_completion.py +0 -0
  118. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/tui_style.py +0 -0
  119. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/uc_menu.py +0 -0
  120. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/command_line/utils.py +0 -0
  121. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/config_file.py +0 -0
  122. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/error_logging.py +0 -0
  123. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/gemini_code_assist.py +0 -0
  124. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/gemini_model.py +0 -0
  125. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/README.md +0 -0
  126. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/__init__.py +0 -0
  127. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/aliases.py +0 -0
  128. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/engine.py +0 -0
  129. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/executor.py +0 -0
  130. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/matcher.py +0 -0
  131. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/models.py +0 -0
  132. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/registry.py +0 -0
  133. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/hook_engine/validator.py +0 -0
  134. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/http_utils.py +0 -0
  135. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/__init__.py +0 -0
  136. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/audit.py +0 -0
  137. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/catalog.py +0 -0
  138. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/formats.py +0 -0
  139. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locale.py +0 -0
  140. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/es.json +0 -0
  141. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/locales/fr-CA.json +0 -0
  142. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/plurals.py +0 -0
  143. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/pseudo.py +0 -0
  144. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/i18n/translate.py +0 -0
  145. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/kennel_provider.py +0 -0
  146. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/keymap.py +0 -0
  147. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/list_filtering.py +0 -0
  148. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/main.py +0 -0
  149. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/__init__.py +0 -0
  150. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/agent_bindings.py +0 -0
  151. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/async_lifecycle.py +0 -0
  152. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/blocking_startup.py +0 -0
  153. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/circuit_breaker.py +0 -0
  154. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/config_wizard.py +0 -0
  155. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/dashboard.py +0 -0
  156. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/error_isolation.py +0 -0
  157. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/examples/retry_example.py +0 -0
  158. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/health_monitor.py +0 -0
  159. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/managed_server.py +0 -0
  160. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/mcp_logs.py +0 -0
  161. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/project_config.py +0 -0
  162. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/registry.py +0 -0
  163. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/retry_manager.py +0 -0
  164. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  165. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/status_tracker.py +0 -0
  166. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/system_tools.py +0 -0
  167. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
  168. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_/toolset_utils.py +0 -0
  169. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_prompts/__init__.py +0 -0
  170. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/mcp_prompts/hook_creator.py +0 -0
  171. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/__init__.py +0 -0
  172. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bar_painters.py +0 -0
  173. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bar_rendering.py +0 -0
  174. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bottom_bar.py +0 -0
  175. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/bus.py +0 -0
  176. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/chords.py +0 -0
  177. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/commands.py +0 -0
  178. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_actions.py +0 -0
  179. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_completion.py +0 -0
  180. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_display.py +0 -0
  181. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_history.py +0 -0
  182. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_keys.py +0 -0
  183. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/editor_paste.py +0 -0
  184. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/external_editor.py +0 -0
  185. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/inline_bar.py +0 -0
  186. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/line_editor.py +0 -0
  187. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/markdown_patches.py +0 -0
  188. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/message_queue.py +0 -0
  189. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/messages.py +0 -0
  190. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/pause_controller.py +0 -0
  191. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/prompt_prefix_style.py +0 -0
  192. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/queue_console.py +0 -0
  193. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/renderers.py +0 -0
  194. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/rich_renderer.py +0 -0
  195. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/run_ui.py +0 -0
  196. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/run_ui_wiring.py +0 -0
  197. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/spinner/__init__.py +0 -0
  198. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/subagent_console.py +0 -0
  199. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/messaging/transcript_guard.py +0 -0
  200. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_descriptions.py +0 -0
  201. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_factory.py +0 -0
  202. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_switching.py +0 -0
  203. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/model_utils.py +0 -0
  204. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models.json +0 -0
  205. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models_dev_api.json +0 -0
  206. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/models_dev_parser.py +0 -0
  207. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/observability.py +0 -0
  208. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/platform_utils.py +0 -0
  209. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/config.py +0 -0
  210. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/oauth_pasteback.py +0 -0
  211. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  212. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/trust.py +0 -0
  213. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/plugins/trust_notice.py +0 -0
  214. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/provider_credentials.py +0 -0
  215. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/provider_identity.py +0 -0
  216. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/pydantic_patches.py +0 -0
  217. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/reopenable_async_client.py +0 -0
  218. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/round_robin_model.py +0 -0
  219. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/secret_store.py +0 -0
  220. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/secret_store_backends.py +0 -0
  221. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_format_migration.py +0 -0
  222. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_lifecycle.py +0 -0
  223. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_migration.py +0 -0
  224. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/session_storage.py +0 -0
  225. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/skill_provider.py +0 -0
  226. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/splash.py +0 -0
  227. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/status_display.py +0 -0
  228. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/terminal_utils.py +0 -0
  229. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/token_usage.py +0 -0
  230. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/__init__.py +0 -0
  231. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/agent_tools.py +0 -0
  232. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/__init__.py +0 -0
  233. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/constants.py +0 -0
  234. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
  235. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/handler.py +0 -0
  236. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/models.py +0 -0
  237. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/registration.py +0 -0
  238. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/renderers.py +0 -0
  239. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
  240. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/theme.py +0 -0
  241. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
  242. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/__init__.py +0 -0
  243. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_control.py +0 -0
  244. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_interactions.py +0 -0
  245. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
  246. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_locators.py +0 -0
  247. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_manager.py +0 -0
  248. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_navigation.py +0 -0
  249. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
  250. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  251. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_scripts.py +0 -0
  252. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
  253. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/browser_workflows.py +0 -0
  254. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/browser/tool_registry.py +0 -0
  255. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/command_runner.py +0 -0
  256. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/common.py +0 -0
  257. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/display.py +0 -0
  258. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_operations.py +0 -0
  259. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/file_permission_state.py +0 -0
  260. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/fs_access.py +0 -0
  261. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/image_tools.py +0 -0
  262. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/io_backends.py +0 -0
  263. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/model_tools.py +0 -0
  264. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/shell_backgrounding.py +0 -0
  265. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/skills_tools.py +0 -0
  266. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_context.py +0 -0
  267. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_invocation.py +0 -0
  268. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/subagent_usage_metrics.py +0 -0
  269. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/tools_content.py +0 -0
  270. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/tools/universal_constructor.py +0 -0
  271. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/undo_manager.py +0 -0
  272. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/universal_constructor_provider.py +0 -0
  273. {code_puppy-0.0.784 → code_puppy-0.0.786}/code_puppy/version_checker.py +0 -0
@@ -51,3 +51,7 @@ code_puppy/bundled_skills/
51
51
 
52
52
  # Local git worktrees
53
53
  .worktrees/
54
+
55
+ # Cancel-wedge repro harness output (scripts/repro_cancel_wedge.py)
56
+ wedge_artifacts/
57
+ wedge_soak.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-puppy
3
- Version: 0.0.784
3
+ Version: 0.0.786
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -16,11 +16,12 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Software Development :: Code Generators
17
17
  Requires-Python: <3.15,>=3.11
18
18
  Requires-Dist: agent-client-protocol<0.12,>=0.11.0
19
- Requires-Dist: anthropic<1,>=0.108.0
19
+ Requires-Dist: anthropic<2,>=1.0.0
20
20
  Requires-Dist: anyio>=4.7.0
21
21
  Requires-Dist: azure-identity>=1.15.0
22
22
  Requires-Dist: boto3>=1.43.9
23
- Requires-Dist: code-puppy-core-plugins>=0.0.26
23
+ Requires-Dist: code-puppy-core-plugins>=0.0.27
24
+ Requires-Dist: httpx2>=2.7
24
25
  Requires-Dist: httpx[http2]>=0.24.1
25
26
  Requires-Dist: json-repair>=0.46.2
26
27
  Requires-Dist: keyring>=24.0.0
@@ -31,7 +32,7 @@ Requires-Dist: packaging>=24.0
31
32
  Requires-Dist: pillow>=10.0.0
32
33
  Requires-Dist: playwright>=1.40.0; sys_platform != 'android'
33
34
  Requires-Dist: pydantic-ai-harness==0.23.0
34
- Requires-Dist: pydantic-ai-slim[anthropic,mcp,openai]==2.31.0
35
+ Requires-Dist: pydantic-ai-slim[anthropic,mcp,openai]==2.35.0
35
36
  Requires-Dist: pydantic>=2.4.0
36
37
  Requires-Dist: pyfiglet>=0.8.post1
37
38
  Requires-Dist: python-dotenv>=1.0.0
@@ -9,12 +9,51 @@ daemon thread.
9
9
  from __future__ import annotations
10
10
 
11
11
  import asyncio
12
+ import time
12
13
  from typing import Any, Callable, Optional
13
14
 
14
15
  from code_puppy.command_line.attachments import resolve_steer_content
15
16
  from code_puppy.messaging import emit_info, emit_warning
16
17
  from code_puppy.tools.agent_tools import _active_subagent_tasks
17
18
 
19
+ # =============================================================================
20
+ # Run-detach seam — the escape hatch for zombie cancellations
21
+ # =============================================================================
22
+ #
23
+ # A cancelled run with sub-agents/MCP servers can get stuck unwinding (anyio
24
+ # cancel-scope teardown, plugin cancel hooks, ...). The REPL awaits the run
25
+ # task, the persistent prompt stays visually alive, and every quit gesture is
26
+ # gated on ``is_run_active()`` — so a stuck unwind freezes the whole app.
27
+ # The seam below lets repeated cancel gestures escalate to "abandon the run":
28
+ # ``run_prompt_with_attachments`` installs an event and races it against the
29
+ # agent task, so setting it unblocks the REPL even if the task never finishes.
30
+
31
+ #: A second cancel gesture this many seconds after the first one on a run
32
+ #: that STILL hasn't finished unwinding escalates to a detach.
33
+ CANCEL_ESCALATE_AFTER_S = 3.0
34
+
35
+ _detach_event: Optional[asyncio.Event] = None
36
+
37
+
38
+ def install_detach_event(event: asyncio.Event) -> None:
39
+ """Register the top-level run's detach event (cli_runner owns it)."""
40
+ global _detach_event
41
+ _detach_event = event
42
+
43
+
44
+ def clear_detach_event() -> None:
45
+ """Drop the seam once the run's await has resolved (normal or detached)."""
46
+ global _detach_event
47
+ _detach_event = None
48
+
49
+
50
+ def request_run_detach() -> bool:
51
+ """Fire the detach event. Loop-thread only. Returns True if one was armed."""
52
+ if _detach_event is None:
53
+ return False
54
+ _detach_event.set()
55
+ return True
56
+
18
57
 
19
58
  def sigint_should_cancel() -> bool:
20
59
  """Buffer-first Ctrl+C gate for the run's SIGINT handler.
@@ -61,6 +100,12 @@ def make_schedule_cancel(
61
100
  terminal) while letting the cancel actually proceed.
62
101
  """
63
102
 
103
+ # Closure state: monotonic time of the first cancel request, so a later
104
+ # gesture on a still-stuck unwind can escalate to a detach (list, not a
105
+ # bare float, to stay writable from the nested function without nonlocal
106
+ # gymnastics on multiple call sites).
107
+ first_cancel_at: list = []
108
+
64
109
  def schedule_agent_cancel(force: bool = False) -> None:
65
110
  from code_puppy.tools.command_runner import (
66
111
  _RUNNING_PROCESSES,
@@ -70,6 +115,21 @@ def make_schedule_cancel(
70
115
 
71
116
  if agent_task.done():
72
117
  return
118
+ # Escalation: the user already cancelled, waited, and is cancelling
119
+ # again — the unwind is stuck. Abandon the run so the REPL (and
120
+ # every quit gesture it gates) comes back. The zombie task is left
121
+ # to finish — or not — in the background.
122
+ now = time.monotonic()
123
+ if first_cancel_at and now - first_cancel_at[0] >= CANCEL_ESCALATE_AFTER_S:
124
+ _tear_down_live_panels()
125
+ emit_warning(
126
+ "\nRun is stuck cancelling — abandoning it and returning "
127
+ "to the prompt..."
128
+ )
129
+ loop.call_soon_threadsafe(request_run_detach)
130
+ return
131
+ if not first_cancel_at:
132
+ first_cancel_at.append(now)
73
133
  if _RUNNING_PROCESSES and not force:
74
134
  # Ordering matters (see _shell_sigint_handler): banner BEFORE the
75
135
  # kill — the sweep blocks this thread ~2s per process, so the
@@ -236,11 +296,15 @@ def drain_pause_state_on_cancel() -> None:
236
296
  )
237
297
 
238
298
 
239
- all__ = [
299
+ __all__ = [
300
+ "CANCEL_ESCALATE_AFTER_S",
301
+ "clear_detach_event",
240
302
  "drain_pause_state_on_cancel",
241
303
  "inject_interrupted_subagent_notes",
304
+ "install_detach_event",
242
305
  "make_schedule_cancel",
243
306
  "prepare_queued_steer_injection",
307
+ "request_run_detach",
244
308
  "reset_pause_state_at_run_start",
245
309
  "sigint_should_cancel",
246
310
  ]
@@ -5,6 +5,11 @@ Prompt caching is configured through pydantic-ai's native
5
5
  markers; it only owns transport concerns that cannot be expressed there:
6
6
  OAuth refresh/retry, Claude Code tool-name prefixing, request headers, URL
7
7
  parameters, and the Opus summarized-thinking compatibility transform.
8
+
9
+ Built on ``httpx2`` (not ``httpx``): every consumer of this client hands it
10
+ to ``anthropic.AsyncAnthropic``, and the Anthropic SDK moved to httpx2 in
11
+ its 1.0 release (pydantic-ai >= 2.35 followed). The rest of Code Puppy
12
+ (OpenAI providers, http_utils) still rides classic httpx.
8
13
  """
9
14
 
10
15
  from __future__ import annotations
@@ -17,7 +22,7 @@ import time
17
22
  from typing import Any, Callable, MutableMapping
18
23
  from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
19
24
 
20
- import httpx
25
+ import httpx2
21
26
 
22
27
  logger = logging.getLogger(__name__)
23
28
 
@@ -61,7 +66,7 @@ def _enforce_thinking_display_summary(payload):
61
66
  return True
62
67
 
63
68
 
64
- class ClaudeCacheAsyncClient(httpx.AsyncClient):
69
+ class ClaudeCacheAsyncClient(httpx2.AsyncClient):
65
70
  """Async HTTP client with Claude Code OAuth transformations.
66
71
 
67
72
  Handles:
@@ -128,7 +133,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
128
133
  logger.debug("Failed to decode JWT age: %s", exc)
129
134
  return None
130
135
 
131
- def _extract_bearer_token(self, request: httpx.Request) -> str | None:
136
+ def _extract_bearer_token(self, request: httpx2.Request) -> str | None:
132
137
  """Extract the bearer token from request headers."""
133
138
  auth_header = request.headers.get("Authorization") or request.headers.get(
134
139
  "authorization"
@@ -137,7 +142,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
137
142
  return auth_header[7:] # Strip "Bearer " prefix
138
143
  return None
139
144
 
140
- def _jwt_refresh_decision(self, request: httpx.Request) -> bool | None:
145
+ def _jwt_refresh_decision(self, request: httpx2.Request) -> bool | None:
141
146
  """Return a JWT-based refresh decision, or ``None`` for stored fallback."""
142
147
  token = self._extract_bearer_token(request)
143
148
  if not token:
@@ -163,14 +168,14 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
163
168
  )
164
169
  return should_refresh
165
170
 
166
- def _should_refresh_token(self, request: httpx.Request) -> bool:
171
+ def _should_refresh_token(self, request: httpx2.Request) -> bool:
167
172
  """Synchronously check JWT age, then the stored-token callback."""
168
173
  decision = self._jwt_refresh_decision(request)
169
174
  if decision is not None:
170
175
  return decision
171
176
  return self._log_stored_token_refresh(self._check_stored_token_expiry())
172
177
 
173
- async def _should_refresh_token_async(self, request: httpx.Request) -> bool:
178
+ async def _should_refresh_token_async(self, request: httpx2.Request) -> bool:
174
179
  """Check token expiry while awaiting async providers in ``send()``."""
175
180
  decision = self._jwt_refresh_decision(request)
176
181
  if decision is not None:
@@ -320,7 +325,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
320
325
  del headers[key]
321
326
 
322
327
  @staticmethod
323
- def _add_beta_query_param(url: httpx.URL) -> httpx.URL:
328
+ def _add_beta_query_param(url: httpx2.URL) -> httpx2.URL:
324
329
  """Add ?beta=true query parameter to the URL if not already present."""
325
330
  parsed = urlparse(str(url))
326
331
  query_params = parse_qs(parsed.query)
@@ -328,12 +333,12 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
328
333
  query_params["beta"] = ["true"]
329
334
  new_query = urlencode(query_params, doseq=True)
330
335
  new_parsed = parsed._replace(query=new_query)
331
- return httpx.URL(urlunparse(new_parsed))
336
+ return httpx2.URL(urlunparse(new_parsed))
332
337
  return url
333
338
 
334
339
  async def send(
335
- self, request: httpx.Request, *args: Any, **kwargs: Any
336
- ) -> httpx.Response: # type: ignore[override]
340
+ self, request: httpx2.Request, *args: Any, **kwargs: Any
341
+ ) -> httpx2.Response: # type: ignore[override]
337
342
  is_messages_endpoint = request.url.path.endswith("/v1/messages")
338
343
  if not request.extensions.get("claude_oauth_proactive_refresh_attempted"):
339
344
  try:
@@ -450,10 +455,10 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
450
455
  return response
451
456
 
452
457
  async def _send_with_retries(
453
- self, request: httpx.Request, *args: Any, **kwargs: Any
454
- ) -> httpx.Response:
458
+ self, request: httpx2.Request, *args: Any, **kwargs: Any
459
+ ) -> httpx2.Response:
455
460
  """Retry rate limits, server failures, and transient connections."""
456
- last_response: httpx.Response | None = None
461
+ last_response: httpx2.Response | None = None
457
462
  last_exception: Exception | None = None
458
463
  for attempt in range(MAX_RETRIES + 1):
459
464
  status_code: int | None = None
@@ -467,7 +472,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
467
472
  return response
468
473
  status_code = response.status_code
469
474
  await response.aclose()
470
- except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as exc:
475
+ except (httpx2.ConnectError, httpx2.ReadTimeout, httpx2.PoolTimeout) as exc:
471
476
  last_exception = exc
472
477
  if attempt >= MAX_RETRIES:
473
478
  raise
@@ -515,7 +520,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
515
520
  raise RuntimeError("Retry loop completed without response or exception")
516
521
 
517
522
  @staticmethod
518
- def _extract_body_bytes(request: httpx.Request) -> bytes | None:
523
+ def _extract_body_bytes(request: httpx2.Request) -> bytes | None:
519
524
  try:
520
525
  content = request.content
521
526
  if content:
@@ -547,7 +552,7 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
547
552
  headers["Authorization"] = bearer_value
548
553
 
549
554
  @staticmethod
550
- async def _is_cloudflare_html_error(response: httpx.Response) -> bool:
555
+ async def _is_cloudflare_html_error(response: httpx2.Response) -> bool:
551
556
  """Return whether a 400 HTML response is a Cloudflare auth failure."""
552
557
  if "text/html" not in response.headers.get("content-type", "").lower():
553
558
  return False
@@ -741,6 +741,35 @@ def _interactive_sigint_guard(_sig, _frame):
741
741
  return
742
742
 
743
743
 
744
+ #: How long the quit paths (double Ctrl+C / Ctrl+D / exit) wait for a
745
+ #: lingering agent task to finish cancelling before abandoning it. A stuck
746
+ #: unwind must never freeze the exit — process teardown reaps the zombie.
747
+ _QUIT_CANCEL_TIMEOUT_S = 5.0
748
+
749
+
750
+ async def _shutdown_agent_task(agent_task) -> None:
751
+ """Cancel a lingering agent task on quit, bounded by a timeout.
752
+
753
+ ``asyncio.wait`` (not ``wait_for``) on purpose: ``wait_for``'s timeout
754
+ path awaits the inner cancellation, which is exactly the await that can
755
+ wedge. ``wait`` just stops waiting.
756
+ """
757
+ from code_puppy.messaging import emit_warning
758
+
759
+ if agent_task is None or agent_task.done():
760
+ return
761
+ emit_info(t("cli.agent.cancelling"))
762
+ agent_task.cancel()
763
+ done, _ = await asyncio.wait({agent_task}, timeout=_QUIT_CANCEL_TIMEOUT_S)
764
+ if agent_task not in done:
765
+ emit_warning(t("cli.agent.quit_cancel_timeout", seconds=_QUIT_CANCEL_TIMEOUT_S))
766
+ return
767
+ try:
768
+ agent_task.result()
769
+ except BaseException:
770
+ pass # Expected when cancelling — nothing to do with it on the way out.
771
+
772
+
744
773
  async def interactive_mode(message_renderer, initial_command: str = None) -> None:
745
774
  """Run the agent in interactive mode."""
746
775
  from code_puppy.command_line.command_handler import handle_command
@@ -837,6 +866,16 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
837
866
  except (ValueError, OSError):
838
867
  pass
839
868
 
869
+ # Field diagnostics: `kill -USR2 <pid>` dumps thread + asyncio-task
870
+ # stacks to ~/.code_puppy/stackdumps/ — the conviction kit for wedged
871
+ # cancellations (a frozen REPL can't be introspected any other way).
872
+ try:
873
+ from code_puppy.diagnostics import install_stack_dump_handler
874
+
875
+ install_stack_dump_handler(asyncio.get_running_loop())
876
+ except Exception:
877
+ pass # diagnostics must never block the REPL
878
+
840
879
  # ------------------------------------------------------------------
841
880
  # Persistent-prompt mode: the bottom-bar editor is THE prompt (idle AND
842
881
  # running) — pinned input row, output scrolls above. Classic path behind
@@ -921,13 +960,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
921
960
  now = time.monotonic()
922
961
  if was_plain_ctrl_c and now - last_idle_ctrl_c <= DOUBLE_CTRL_C_WINDOW_S:
923
962
  emit_success("\n" + t("cli.goodbye_ctrld"))
924
- if current_agent_task and not current_agent_task.done():
925
- emit_info(t("cli.agent.cancelling"))
926
- current_agent_task.cancel()
927
- try:
928
- await current_agent_task
929
- except asyncio.CancelledError:
930
- pass # Expected when cancelling
963
+ await _shutdown_agent_task(current_agent_task)
931
964
  break
932
965
  last_idle_ctrl_c = now if was_plain_ctrl_c else 0.0
933
966
 
@@ -940,14 +973,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
940
973
 
941
974
  emit_success("\n" + t("cli.goodbye_ctrld"))
942
975
 
943
- # Cancel any running agent task for clean shutdown
944
- if current_agent_task and not current_agent_task.done():
945
- emit_info(t("cli.agent.cancelling"))
946
- current_agent_task.cancel()
947
- try:
948
- await current_agent_task
949
- except asyncio.CancelledError:
950
- pass # Expected when cancelling
976
+ # Cancel any running agent task for clean shutdown (bounded — a
977
+ # stuck unwind must not freeze Ctrl+D).
978
+ await _shutdown_agent_task(current_agent_task)
951
979
 
952
980
  break
953
981
 
@@ -975,14 +1003,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
975
1003
 
976
1004
  emit_success(t("cli.goodbye"))
977
1005
 
978
- # Cancel any running agent task for clean shutdown
979
- if current_agent_task and not current_agent_task.done():
980
- emit_info(t("cli.agent.cancelling"))
981
- current_agent_task.cancel()
982
- try:
983
- await current_agent_task
984
- except asyncio.CancelledError:
985
- pass # Expected when cancelling
1006
+ # Cancel any running agent task for clean shutdown (bounded — a
1007
+ # stuck unwind must not freeze `exit`).
1008
+ await _shutdown_agent_task(current_agent_task)
986
1009
 
987
1010
  # The renderer is stopped in the finally block of main().
988
1011
  break
@@ -1365,13 +1388,44 @@ async def run_prompt_with_attachments(
1365
1388
  )
1366
1389
  )
1367
1390
 
1391
+ # Escape hatch: a cancelled run can wedge mid-unwind (sub-agent/MCP
1392
+ # cancel-scope teardown). Racing the task against a detach event keeps
1393
+ # the REPL recoverable — repeated cancel gestures escalate to a detach
1394
+ # (see _run_signals.make_schedule_cancel) and we abandon the zombie.
1395
+ from code_puppy.agents._run_signals import (
1396
+ clear_detach_event,
1397
+ install_detach_event,
1398
+ )
1399
+
1400
+ detach_event = asyncio.Event()
1401
+ install_detach_event(detach_event)
1402
+
1368
1403
  async def _await_agent():
1404
+ detach_wait = asyncio.create_task(detach_event.wait())
1369
1405
  try:
1370
- result = await agent_task
1371
- return result, agent_task
1372
- except asyncio.CancelledError:
1373
- emit_info(t("cli.agent.task_cancelled"))
1374
- return None, agent_task
1406
+ try:
1407
+ done, _ = await asyncio.wait(
1408
+ {agent_task, detach_wait}, return_when=asyncio.FIRST_COMPLETED
1409
+ )
1410
+ except asyncio.CancelledError:
1411
+ # The waiter itself was cancelled (shutdown teardown) — same
1412
+ # contract as the old bare ``await agent_task`` path.
1413
+ emit_info(t("cli.agent.task_cancelled"))
1414
+ return None, agent_task
1415
+ if agent_task not in done:
1416
+ # Escalated cancel: abandon the stuck unwind, free the REPL.
1417
+ emit_warning(t("cli.agent.detached"))
1418
+ return None, agent_task
1419
+ if agent_task.cancelled():
1420
+ emit_info(t("cli.agent.task_cancelled"))
1421
+ return None, agent_task
1422
+ exc = agent_task.exception()
1423
+ if exc is not None:
1424
+ raise exc
1425
+ return agent_task.result(), agent_task
1426
+ finally:
1427
+ detach_wait.cancel()
1428
+ clear_detach_event()
1375
1429
 
1376
1430
  if use_run_ui:
1377
1431
  # Interactive run: bottom bar stays up while the agent works. run_ui() is
@@ -150,11 +150,14 @@ class TrustCommand(MCPCommandBase):
150
150
 
151
151
  def _revoke(self, group_id: str) -> None:
152
152
  if revoke_project_mcp():
153
+ try:
154
+ self.manager.sync_from_config()
155
+ except Exception as exc: # pragma: no cover - defensive
156
+ logger.warning("Post-revoke registry sync failed: %s", exc)
153
157
  emit_info(
154
158
  Text.from_markup(
155
159
  "[green]\u2713 Revoked[/green] trust for this project's MCP "
156
- "config. Its servers will no longer load. Restart or re-sync "
157
- "to drop already-registered ones."
160
+ "config. Its servers will no longer load."
158
161
  ),
159
162
  message_group=group_id,
160
163
  )
@@ -595,7 +595,7 @@ def _parse_mcp_servers_mapping(raw_text: str) -> dict:
595
595
  return servers
596
596
 
597
597
 
598
- def load_mcp_server_configs():
598
+ def load_mcp_server_configs(*, raise_on_error: bool = False):
599
599
  """Load MCP server configs, merging user-level and trusted project-level.
600
600
 
601
601
  Sources, in ascending order of precedence:
@@ -610,6 +610,11 @@ def load_mcp_server_configs():
610
610
  Project entries win on name collision, matching how project agents, skills,
611
611
  and plugins override their user-level counterparts. Returns an empty dict
612
612
  when nothing is configured.
613
+
614
+ When *raise_on_error* is true, a parse/IO failure of an existing user-level
615
+ file (or a failure of the project loader) is re-raised after the error is
616
+ emitted, so callers that unregister missing names can skip that drop
617
+ instead of treating ``{}`` as "configure nothing".
613
618
  """
614
619
  from code_puppy.messaging.message_queue import emit_error
615
620
 
@@ -622,6 +627,8 @@ def load_mcp_server_configs():
622
627
  configs.update(_parse_mcp_servers_mapping(f.read()))
623
628
  except Exception as e:
624
629
  emit_error(f"Failed to load MCP servers - {str(e)}")
630
+ if raise_on_error:
631
+ raise
625
632
 
626
633
  # 2. Project-level config (opt-in, trust-gated). A broken or untrusted
627
634
  # project file must never break user-level loading.
@@ -633,6 +640,8 @@ def load_mcp_server_configs():
633
640
  configs.update(project_configs)
634
641
  except Exception as e:
635
642
  emit_error(f"Failed to load project MCP servers - {str(e)}")
643
+ if raise_on_error:
644
+ raise
636
645
 
637
646
  return configs
638
647
 
@@ -0,0 +1,124 @@
1
+ """On-demand stack dumps for wedged runs (``kill -USR2 <pid>``).
2
+
3
+ Field diagnosis for the "cancelled run wedges mid-unwind" class of bug
4
+ (Discord: "death spiral of 2 asyncs waiting for the other to finish").
5
+ ``faulthandler`` alone is useless there — the event-loop thread is parked
6
+ in ``select()`` and every interesting frame lives in a *suspended asyncio
7
+ task*, which faulthandler cannot see. This module dumps BOTH:
8
+
9
+ - OS-thread stacks via ``faulthandler.dump_traceback`` (key listener,
10
+ renderer, shell readers, ...); and
11
+ - every asyncio task's coroutine stack via ``Task.print_stack`` (the REPL
12
+ coroutine stuck on ``await agent_task``, the agent task stuck in its
13
+ cancel-scope teardown — the actual smoking gun).
14
+
15
+ Dumps are written to ``~/.code_puppy/stackdumps/`` (never the terminal:
16
+ the bottom-bar TUI owns it, and a wedged app may not repaint anyway) with
17
+ a one-line breadcrumb on stderr. POSIX only — Windows has no SIGUSR2;
18
+ ``install_stack_dump_handler`` degrades to a no-op there.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import faulthandler
25
+ import logging
26
+ import os
27
+ import signal
28
+ import sys
29
+ import time
30
+ from pathlib import Path
31
+ from typing import Optional, TextIO
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ DUMP_DIR = Path.home() / ".code_puppy" / "stackdumps"
36
+
37
+ #: Frames per task stack. Wedges live near the leaf; unbounded dumps of
38
+ #: deep pydantic-ai graphs just bury the signal.
39
+ TASK_STACK_LIMIT = 40
40
+
41
+ _captured_loop: Optional[asyncio.AbstractEventLoop] = None
42
+
43
+
44
+ def dump_stacks(file: TextIO, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
45
+ """Write thread stacks + asyncio task stacks to ``file``.
46
+
47
+ Never raises: each section is best-effort so a half-broken process
48
+ can still confess as much as it knows.
49
+ """
50
+ print(f"=== code-puppy stack dump (pid {os.getpid()}) ===", file=file)
51
+ print("--- OS threads (faulthandler) ---", file=file)
52
+ try:
53
+ file.flush()
54
+ faulthandler.dump_traceback(file=file, all_threads=True)
55
+ except Exception:
56
+ logger.debug("faulthandler dump failed", exc_info=True)
57
+ print("<faulthandler dump failed>", file=file)
58
+
59
+ loop = loop or _captured_loop
60
+ if loop is None:
61
+ print("--- asyncio tasks: no loop captured ---", file=file)
62
+ return
63
+ try:
64
+ tasks = asyncio.all_tasks(loop)
65
+ except Exception:
66
+ logger.debug("asyncio.all_tasks failed", exc_info=True)
67
+ print("--- asyncio tasks: enumeration failed ---", file=file)
68
+ return
69
+ print(f"--- asyncio tasks: {len(tasks)} ---", file=file)
70
+ for task in tasks:
71
+ print(f"\nTask: {task!r}", file=file)
72
+ try:
73
+ task.print_stack(limit=TASK_STACK_LIMIT, file=file)
74
+ except Exception:
75
+ logger.debug("task.print_stack failed", exc_info=True)
76
+ print("<task stack unavailable>", file=file)
77
+
78
+
79
+ def write_stack_dump(
80
+ loop: Optional[asyncio.AbstractEventLoop] = None,
81
+ ) -> Optional[Path]:
82
+ """Dump to a timestamped file under ``DUMP_DIR``; return its path.
83
+
84
+ Returns ``None`` (and stays silent beyond a debug log) if even the
85
+ file can't be created — diagnostics must never hurt the patient.
86
+ """
87
+ try:
88
+ DUMP_DIR.mkdir(parents=True, exist_ok=True)
89
+ path = DUMP_DIR / f"stackdump-{os.getpid()}-{int(time.time())}.txt"
90
+ with open(path, "w", encoding="utf-8") as f:
91
+ dump_stacks(f, loop=loop)
92
+ # stderr, not the message bus: a wedged loop may never render a
93
+ # bus message, and stderr survives a hung TUI.
94
+ print(f"[code-puppy] stack dump written to {path}", file=sys.stderr)
95
+ return path
96
+ except Exception:
97
+ logger.debug("stack dump failed", exc_info=True)
98
+ return None
99
+
100
+
101
+ def install_stack_dump_handler(
102
+ loop: Optional[asyncio.AbstractEventLoop] = None,
103
+ ) -> bool:
104
+ """Register the SIGUSR2 stack-dump handler. Returns True if installed.
105
+
106
+ Safe to call more than once (last loop reference wins). No-op on
107
+ platforms without SIGUSR2 (Windows) and off the main thread, where
108
+ ``signal.signal`` raises.
109
+ """
110
+ global _captured_loop
111
+ if not hasattr(signal, "SIGUSR2"):
112
+ return False
113
+ if loop is not None:
114
+ _captured_loop = loop
115
+
116
+ def _handler(signum, frame) -> None:
117
+ write_stack_dump()
118
+
119
+ try:
120
+ signal.signal(signal.SIGUSR2, _handler)
121
+ except (ValueError, OSError):
122
+ logger.debug("SIGUSR2 handler install failed", exc_info=True)
123
+ return False
124
+ return True
@@ -44,6 +44,8 @@
44
44
  "cli.agent.set_error": "Error setting agent: {error}",
45
45
  "cli.agent.cancelling": "Cancelling running agent task...",
46
46
  "cli.agent.task_cancelled": "Agent task cancelled",
47
+ "cli.agent.detached": "Run abandoned — it was stuck cancelling. Back at the prompt (the old run may still unwind in the background).",
48
+ "cli.agent.quit_cancel_timeout": "Agent task didn't finish cancelling within {seconds}s — exiting anyway.",
47
49
  "cli.resume.quick_searching": "🔍 Quick Resume selected - finding latest session for {scope}",
48
50
  "cli.resume.none_found": "No previous session found for this scope; starting fresh.",
49
51
  "cli.resume.available_sessions": "Available sessions: {sessions}",
@@ -161,10 +161,7 @@ class MCPManager:
161
161
  try:
162
162
  from code_puppy.config import load_mcp_server_configs
163
163
 
164
- configs = load_mcp_server_configs()
165
- if not configs:
166
- logger.debug("No servers found in mcp_servers.json")
167
- return
164
+ configs = load_mcp_server_configs(raise_on_error=True) or {}
168
165
 
169
166
  synced_count = 0
170
167
  updated_count = 0
@@ -209,9 +206,23 @@ class MCPManager:
209
206
  logger.warning(f"Failed to sync server '{name}' from config: {e}")
210
207
  continue
211
208
 
212
- if synced_count > 0 or updated_count > 0:
209
+ configured_names = {
210
+ name for name, conf in configs.items() if isinstance(conf, dict)
211
+ }
212
+ dropped_count = 0
213
+ for existing in list(self.registry.list_all()):
214
+ if existing.name not in configured_names:
215
+ if self.remove_server(existing.id):
216
+ dropped_count += 1
217
+ logger.debug(
218
+ "Dropped server no longer in mcp_servers.json: %s",
219
+ existing.name,
220
+ )
221
+
222
+ if synced_count > 0 or updated_count > 0 or dropped_count > 0:
213
223
  logger.info(
214
- f"Synced {synced_count} new and updated {updated_count} servers from mcp_servers.json"
224
+ f"Synced {synced_count} new, updated {updated_count}, "
225
+ f"dropped {dropped_count} servers from mcp_servers.json"
215
226
  )
216
227
 
217
228
  except Exception as e: