tsugite-cli 0.8.2__tar.gz → 0.9.1__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 (281) hide show
  1. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/PKG-INFO +1 -1
  2. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/pyproject.toml +1 -1
  3. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/history_integration.py +18 -7
  4. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/runner.py +7 -1
  5. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/agent.py +7 -1
  6. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/claude_code.py +2 -1
  7. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/base.py +25 -3
  8. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/discord.py +61 -1
  9. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/http.py +71 -0
  10. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/scheduler_adapter.py +1 -0
  11. tsugite_cli-0.9.1/tsugite/daemon/commands.py +168 -0
  12. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/config.py +1 -0
  13. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/memory.py +50 -0
  14. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/session_runner.py +17 -3
  15. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/session_store.py +4 -1
  16. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/styles.css +23 -1
  17. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/index.html +105 -11
  18. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/conversations.js +198 -23
  19. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/sessions.py +16 -0
  20. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/uv.lock +13 -13
  21. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/copilot-instructions.md +0 -0
  22. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/ci.yml +0 -0
  23. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/docker-publish.yml +0 -0
  24. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.github/workflows/pypi-publish.yml +0 -0
  25. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/.gitignore +0 -0
  26. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/AGENTS.md +0 -0
  27. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/CLAUDE.md +0 -0
  28. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/LICENSE +0 -0
  29. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/README.md +0 -0
  30. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/scripts/regenerate_schema.py +0 -0
  31. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/README.md +0 -0
  32. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/__init__.py +0 -0
  33. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/conftest.py +0 -0
  34. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/__init__.py +0 -0
  35. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_agent.py +0 -0
  36. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_agent_ui_events.py +0 -0
  37. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_content_blocks.py +0 -0
  38. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_executor.py +0 -0
  39. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_memory.py +0 -0
  40. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_proxy.py +0 -0
  41. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_sandbox.py +0 -0
  42. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_subprocess_executor.py +0 -0
  43. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/core/test_tools.py +0 -0
  44. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/daemon/__init__.py +0 -0
  45. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/daemon/test_http_adapter.py +0 -0
  46. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/events/test_event_consolidation.py +0 -0
  47. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/smoke_test.sh +0 -0
  48. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_file_hot_loading.py +0 -0
  49. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_inheritance.py +0 -0
  50. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_parser.py +0 -0
  51. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_sessions.py +0 -0
  52. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_skills.py +0 -0
  53. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agent_utils.py +0 -0
  54. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_agents_tool.py +0 -0
  55. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_attachment_deduplication.py +0 -0
  56. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_attachments.py +0 -0
  57. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_auto_context_handler.py +0 -0
  58. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_auto_discovery.py +0 -0
  59. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_background_task_status.py +0 -0
  60. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_background_tasks.py +0 -0
  61. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_builtin_agent_paths.py +0 -0
  62. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_builtin_agents.py +0 -0
  63. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cache_control.py +0 -0
  64. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_chat_cli.py +0 -0
  65. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_chat_error_handling.py +0 -0
  66. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_claude_code_attachments.py +0 -0
  67. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_claude_code_provider.py +0 -0
  68. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli.py +0 -0
  69. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_arguments.py +0 -0
  70. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_rendering.py +0 -0
  71. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_cli_subcommands.py +0 -0
  72. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_config.py +0 -0
  73. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_continuation.py +0 -0
  74. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_custom_shell_tools.py +0 -0
  75. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_custom_ui.py +0 -0
  76. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_compaction_scheduler.py +0 -0
  77. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_config.py +0 -0
  78. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_history_persistence.py +0 -0
  79. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_memory.py +0 -0
  80. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_push.py +0 -0
  81. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_scheduler.py +0 -0
  82. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_session_isolation.py +0 -0
  83. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_daemon_unified_sessions.py +0 -0
  84. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_discord_progress.py +0 -0
  85. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_error_display.py +0 -0
  86. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_file_references.py +0 -0
  87. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_file_tools.py +0 -0
  88. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history.py +0 -0
  89. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_integration.py +0 -0
  90. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_models.py +0 -0
  91. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_performance.py +0 -0
  92. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_history_tools.py +0 -0
  93. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_hooks.py +0 -0
  94. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_http_tools.py +0 -0
  95. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interaction_backends.py +0 -0
  96. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interactive_context.py +0 -0
  97. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_interactive_tool.py +0 -0
  98. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_jsonl_ui.py +0 -0
  99. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_kvstore.py +0 -0
  100. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_list_agents_tool.py +0 -0
  101. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_mcp_client.py +0 -0
  102. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_mcp_server.py +0 -0
  103. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_models.py +0 -0
  104. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_multi_agent.py +0 -0
  105. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_multistep_agents.py +0 -0
  106. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_orchestrator_heartbeat.py +0 -0
  107. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_plugins.py +0 -0
  108. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_reasoning_models.py +0 -0
  109. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_renderer.py +0 -0
  110. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_rendering_scenarios.py +0 -0
  111. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_commands.py +0 -0
  112. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_completer.py +0 -0
  113. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_repl_handler.py +0 -0
  114. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_retry_system.py +0 -0
  115. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_run_if.py +0 -0
  116. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_schedule_model_override.py +0 -0
  117. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_scheduler_history_injection.py +0 -0
  118. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_schema.py +0 -0
  119. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_security_phase1.py +0 -0
  120. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_send_message.py +0 -0
  121. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_skill_discovery.py +0 -0
  122. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_skill_tools.py +0 -0
  123. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_stdin.py +0 -0
  124. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_subagent_subprocess.py +0 -0
  125. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tmux_tools.py +0 -0
  126. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tool_directives.py +0 -0
  127. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_tool_registry.py +0 -0
  128. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_auto_continue.py +0 -0
  129. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_cwd.py +0 -0
  130. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tests/test_workspace_discovery.py +0 -0
  131. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/__init__.py +0 -0
  132. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_inheritance.py +0 -0
  133. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_preparation.py +0 -0
  134. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/__init__.py +0 -0
  135. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/helpers.py +0 -0
  136. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/metrics.py +0 -0
  137. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/models.py +0 -0
  138. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_runner/validation.py +0 -0
  139. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/agent_utils.py +0 -0
  140. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/__init__.py +0 -0
  141. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/auto_context.py +0 -0
  142. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/base.py +0 -0
  143. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/file.py +0 -0
  144. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/inline.py +0 -0
  145. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/storage.py +0 -0
  146. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/url.py +0 -0
  147. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/attachments/youtube.py +0 -0
  148. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/.gitkeep +0 -0
  149. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/code_searcher.md +0 -0
  150. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/default.md +0 -0
  151. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/file_searcher.md +0 -0
  152. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_agents/onboard.md +0 -0
  153. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/.gitkeep +0 -0
  154. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/codebase_exploration.md +0 -0
  155. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/python_math.md +0 -0
  156. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/response_patterns.md +0 -0
  157. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/scheduling.md +0 -0
  158. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/skill_authoring.md +0 -0
  159. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
  160. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
  161. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
  162. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cache.py +0 -0
  163. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/__init__.py +0 -0
  164. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/agents.py +0 -0
  165. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/attachments.py +0 -0
  166. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/cache.py +0 -0
  167. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/chat.py +0 -0
  168. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/config.py +0 -0
  169. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/daemon.py +0 -0
  170. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/helpers.py +0 -0
  171. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/history.py +0 -0
  172. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/init.py +0 -0
  173. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/mcp.py +0 -0
  174. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/plugins.py +0 -0
  175. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/render.py +0 -0
  176. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/run.py +0 -0
  177. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/serve.py +0 -0
  178. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/tools.py +0 -0
  179. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/validate.py +0 -0
  180. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/cli/workspace.py +0 -0
  181. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/config.py +0 -0
  182. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/console.py +0 -0
  183. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/constants.py +0 -0
  184. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/__init__.py +0 -0
  185. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/content_blocks.py +0 -0
  186. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/executor.py +0 -0
  187. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/memory.py +0 -0
  188. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/proxy.py +0 -0
  189. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/sandbox.py +0 -0
  190. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/subprocess_executor.py +0 -0
  191. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/core/tools.py +0 -0
  192. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/__init__.py +0 -0
  193. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/__init__.py +0 -0
  194. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/compaction_scheduler.py +0 -0
  195. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/gateway.py +0 -0
  196. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/push.py +0 -0
  197. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/scheduler.py +0 -0
  198. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/responsive.css +0 -0
  199. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/theme.css +0 -0
  200. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-192.png +0 -0
  201. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  202. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512.png +0 -0
  203. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  204. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  205. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/api.js +0 -0
  206. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/app.js +0 -0
  207. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/utils.js +0 -0
  208. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/dashboard.js +0 -0
  209. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  210. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/kvstore.js +0 -0
  211. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/schedules.js +0 -0
  212. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  213. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/workspace.js +0 -0
  214. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/manifest.json +0 -0
  215. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/web/sw.js +0 -0
  216. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/daemon/webhook_store.py +0 -0
  217. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/__init__.py +0 -0
  218. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/base.py +0 -0
  219. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/bus.py +0 -0
  220. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/events.py +0 -0
  221. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/events/helpers.py +0 -0
  222. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/exceptions.py +0 -0
  223. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/__init__.py +0 -0
  224. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/models.py +0 -0
  225. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/reconstruction.py +0 -0
  226. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/history/storage.py +0 -0
  227. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/hooks.py +0 -0
  228. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/interaction.py +0 -0
  229. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/__init__.py +0 -0
  230. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/backend.py +0 -0
  231. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/kvstore/sqlite.py +0 -0
  232. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_client.py +0 -0
  233. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_config.py +0 -0
  234. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/mcp_server.py +0 -0
  235. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/md_agents.py +0 -0
  236. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/models.py +0 -0
  237. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/options.py +0 -0
  238. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/plugins.py +0 -0
  239. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/renderer.py +0 -0
  240. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/schemas/__init__.py +0 -0
  241. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/schemas/agent.schema.json +0 -0
  242. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/shell_tool_config.py +0 -0
  243. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/skill_discovery.py +0 -0
  244. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/AGENTS.md +0 -0
  245. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/IDENTITY.md +0 -0
  246. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/MEMORY.md +0 -0
  247. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/USER.md +0 -0
  248. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/casual-technical.md +0 -0
  249. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/marvin.md +0 -0
  250. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/templates/personas/minimal.md +0 -0
  251. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/__init__.py +0 -0
  252. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/agents.py +0 -0
  253. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/fs.py +0 -0
  254. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/history.py +0 -0
  255. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/http.py +0 -0
  256. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/interactive.py +0 -0
  257. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/kv.py +0 -0
  258. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/notify.py +0 -0
  259. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/schedule.py +0 -0
  260. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/shell.py +0 -0
  261. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/shell_tools.py +0 -0
  262. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/skills.py +0 -0
  263. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tools/tmux.py +0 -0
  264. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/tsugite.py +0 -0
  265. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/__init__.py +0 -0
  266. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/base.py +0 -0
  267. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/chat.py +0 -0
  268. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/helpers.py +0 -0
  269. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/jsonl.py +0 -0
  270. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/plain.py +0 -0
  271. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_chat.py +0 -0
  272. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_commands.py +0 -0
  273. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_completer.py +0 -0
  274. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui/repl_handler.py +0 -0
  275. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/ui_context.py +0 -0
  276. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/utils.py +0 -0
  277. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/__init__.py +0 -0
  278. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/context.py +0 -0
  279. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/models.py +0 -0
  280. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/session.py +0 -0
  281. {tsugite_cli-0.8.2 → tsugite_cli-0.9.1}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.8.2
3
+ Version: 0.9.1
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.8.2"
3
+ version = "0.9.1"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -241,16 +241,27 @@ def get_claude_code_session_info(conversation_id: str) -> Optional["ClaudeCodeSe
241
241
  storage = SessionStorage(session_path)
242
242
  records = storage.load_records()
243
243
 
244
- # Single reversed pass: find the last Turn with a session ID,
245
- # but bail if we hit a CompactionSummary (stale Claude Code session)
246
- for record in reversed(records):
244
+ # Find the last CompactionSummary index (if any).
245
+ # Session IDs from retained turns (carried over from pre-compaction)
246
+ # are stale, but session IDs from turns AFTER the compaction are valid.
247
+ compaction_idx = -1
248
+ for i, record in enumerate(records):
247
249
  if isinstance(record, CompactionSummary):
248
- return None
250
+ compaction_idx = i
251
+
252
+ for record in reversed(records):
249
253
  if isinstance(record, Turn) and record.metadata:
250
254
  session_id = record.metadata.get("claude_code_session_id")
251
- if session_id:
252
- compacted = record.metadata.get("claude_code_compacted", False)
253
- return ClaudeCodeSessionInfo(session_id, compacted)
255
+ if not session_id:
256
+ continue
257
+ # If this turn is a retained turn from before compaction
258
+ # (carried over with stale session ID), skip it
259
+ if compaction_idx >= 0:
260
+ turn_idx = records.index(record)
261
+ if turn_idx <= compaction_idx:
262
+ continue
263
+ compacted = record.metadata.get("claude_code_compacted", False)
264
+ return ClaudeCodeSessionInfo(session_id, compacted)
254
265
  return None
255
266
  except Exception:
256
267
  return None
@@ -1,7 +1,10 @@
1
1
  """Agent execution engine using TsugiteAgent."""
2
2
 
3
3
  import asyncio
4
+ import logging
4
5
  import time
6
+
7
+ logger = logging.getLogger(__name__)
5
8
  from pathlib import Path
6
9
  from types import SimpleNamespace
7
10
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
@@ -779,6 +782,9 @@ async def run_agent_async(
779
782
  if session_info:
780
783
  claude_code_resume_session = session_info.session_id
781
784
  claude_code_resume_after_compaction = session_info.compacted
785
+ logger.info("Resuming Claude Code session %s (compacted=%s)", claude_code_resume_session, claude_code_resume_after_compaction)
786
+ else:
787
+ logger.debug("No Claude Code session to resume for %s", continue_conversation_id)
782
788
 
783
789
  if not claude_code_resume_session:
784
790
  # No Claude Code session to resume -- load history for serialization
@@ -838,7 +844,7 @@ async def run_agent_async(
838
844
  )
839
845
  except (RuntimeError, AgentExecutionError) as e:
840
846
  err_str = str(e).lower()
841
- if claude_code_resume_session and ("process ended" in err_str or "no conversation found" in err_str):
847
+ if claude_code_resume_session and ("process ended" in err_str or "no conversation found" in err_str or "prompt too long" in err_str):
842
848
  logger.warning("Claude Code resume failed (%s), retrying with full history", e)
843
849
  try:
844
850
  previous_messages = load_and_apply_history(continue_conversation_id)
@@ -210,6 +210,7 @@ class TsugiteAgent:
210
210
  self._claude_code_model = self.litellm_params.get("model") if self._is_claude_code else None
211
211
  self._claude_code_session_id: Optional[str] = None
212
212
  self._claude_code_last_turn_tokens: int = 0
213
+ self._claude_code_context_tokens: int = 0
213
214
  self._claude_code_context_window: Optional[int] = None
214
215
  self._claude_code_cache_creation_tokens: int = 0
215
216
  self._claude_code_cache_read_tokens: int = 0
@@ -627,9 +628,10 @@ class TsugiteAgent:
627
628
  self._claude_code_session_id = claude_process.session_id
628
629
 
629
630
  if return_full_result:
631
+ context_tokens = self._claude_code_context_tokens or total_tokens
630
632
  return AgentResult(
631
633
  output=exec_result.final_answer,
632
- token_usage=total_tokens,
634
+ token_usage=context_tokens,
633
635
  cost=self.total_cost if self.total_cost > 0 else None,
634
636
  steps=self.memory.steps,
635
637
  claude_code_session_id=self._claude_code_session_id,
@@ -747,6 +749,7 @@ class TsugiteAgent:
747
749
  output_tokens = event.get("output_tokens") or 0
748
750
  turn_total = input_tokens + cache_creation + cache_read + output_tokens
749
751
  self._claude_code_last_turn_tokens = turn_total
752
+ self._claude_code_context_tokens = input_tokens + cache_creation + cache_read
750
753
  self._claude_code_cache_creation_tokens += cache_creation
751
754
  self._claude_code_cache_read_tokens += cache_read
752
755
  self.total_tokens += turn_total
@@ -756,6 +759,9 @@ class TsugiteAgent:
756
759
  if stream and self.event_bus:
757
760
  self.event_bus.emit(StreamCompleteEvent())
758
761
 
762
+ if accumulated.strip().lower() == "prompt is too long":
763
+ raise RuntimeError(f"Claude Code prompt too long (session={self._claude_code_session_id})")
764
+
759
765
  parsed = self._parse_response_from_text(accumulated)
760
766
 
761
767
  # If no code block was found, preserve the raw text as thought so the
@@ -132,7 +132,8 @@ class ClaudeCodeProcess:
132
132
 
133
133
  # Start draining stderr in background to prevent pipe buffer deadlock
134
134
  self._stderr_task = asyncio.create_task(self._drain_stderr())
135
- logger.info("Claude Code subprocess started (pid=%d, model=%s)", self._process.pid, model)
135
+ mode = f"resume={resume_session}" if resume_session else "new session"
136
+ logger.info("Claude Code subprocess started (pid=%d, model=%s, %s)", self._process.pid, model, mode)
136
137
 
137
138
  async def send_message(self, content: str) -> AsyncIterator[dict]:
138
139
  """Write user message to stdin and yield streaming events from stdout.
@@ -430,6 +430,8 @@ class BaseAdapter(ABC):
430
430
  # Get the new session ID after compaction
431
431
  session = self.session_store.get_or_create_interactive(user_id, self.agent_name)
432
432
  conv_id = session.id
433
+ if self.event_bus:
434
+ self.event_bus.emit("session_update", {"action": "compacted", "id": conv_id})
433
435
 
434
436
  metadata = channel_context.to_dict()
435
437
  metadata["daemon_agent"] = self.agent_name
@@ -520,12 +522,29 @@ class BaseAdapter(ABC):
520
522
  self.session_store.update_context_limit(self.agent_name, result.context_window)
521
523
  self.agent_config.context_limit = result.context_window
522
524
 
523
- if result.token_count:
524
- self.session_store.update_token_count(conv_id, result.token_count)
525
+ self.session_store.update_token_count(conv_id, result.token_count or 0)
526
+
527
+ try:
528
+ session = self.session_store.get_session(conv_id)
529
+ if session and session.message_count <= 1 and not session.title:
530
+ asyncio.ensure_future(self._auto_title_session(conv_id, message, str(result)))
531
+ except Exception as e:
532
+ logger.debug("Auto-title check failed for session '%s': %s", conv_id, e)
525
533
 
526
534
  return str(result)
527
535
 
528
- async def _compact_session(self, session_id: str) -> None:
536
+ async def _auto_title_session(self, session_id: str, user_message: str, assistant_response: str) -> None:
537
+ try:
538
+ from tsugite.daemon.memory import auto_title_session
539
+
540
+ await auto_title_session(
541
+ session_id, user_message, assistant_response,
542
+ self.resolve_model(), self.session_store, self.event_bus,
543
+ )
544
+ except Exception as e:
545
+ logger.debug("Auto-title failed for session '%s': %s", session_id, e)
546
+
547
+ async def _compact_session(self, session_id: str, instructions: str | None = None) -> None:
529
548
  """Compact session when approaching context limit.
530
549
 
531
550
  Uses a sliding window: recent turns are kept verbatim while older
@@ -613,6 +632,9 @@ class BaseAdapter(ABC):
613
632
 
614
633
  old_messages.extend(msg for turn in old_turns for msg in turn.messages)
615
634
 
635
+ if instructions:
636
+ old_messages.append({"role": "user", "content": f"<compaction_instructions>{instructions}</compaction_instructions>"})
637
+
616
638
  try:
617
639
  summary = await summarize_session(
618
640
  old_messages, model=model, max_context_tokens=self.agent_config.context_limit
@@ -1,6 +1,7 @@
1
1
  """Discord bot adapter."""
2
2
 
3
3
  import asyncio
4
+ import inspect
4
5
  import logging
5
6
  import re
6
7
  from types import SimpleNamespace
@@ -422,10 +423,25 @@ class DiscordAdapter(BaseAdapter):
422
423
  intents.message_content = True
423
424
 
424
425
  self.bot = commands.Bot(command_prefix=bot_config.command_prefix, intents=intents)
426
+ self._register_commands()
425
427
 
426
428
  @self.bot.event
427
429
  async def on_ready():
428
- logger.info("Discord bot '%s' logged in as %s (agent: %s)", bot_config.name, self.bot.user, agent_name)
430
+ try:
431
+ if bot_config.guild_id:
432
+ guild = discord.Object(id=int(bot_config.guild_id))
433
+ self.bot.tree.copy_global_to(guild=guild)
434
+ await self.bot.tree.sync(guild=guild)
435
+ else:
436
+ await self.bot.tree.sync()
437
+ synced = len(self.bot.tree.get_commands())
438
+ logger.info(
439
+ "Discord bot '%s' logged in as %s (agent: %s, %d app commands synced)",
440
+ bot_config.name, self.bot.user, agent_name, synced,
441
+ )
442
+ except Exception as e:
443
+ logger.error("Failed to sync app commands for '%s': %s", bot_config.name, e)
444
+ logger.info("Discord bot '%s' logged in as %s (agent: %s)", bot_config.name, self.bot.user, agent_name)
429
445
 
430
446
  @self.bot.event
431
447
  async def on_error(event_method, *args, **kwargs):
@@ -473,6 +489,50 @@ class DiscordAdapter(BaseAdapter):
473
489
  task = asyncio.create_task(self._process_message(message, user_msg, bot_config.name))
474
490
  task.add_done_callback(lambda t: _handle_async_exception(t, bot_config.name))
475
491
 
492
+ def _register_commands(self):
493
+ """Auto-register adapter commands from the shared registry as Discord app commands."""
494
+ from tsugite.daemon.commands import get_commands
495
+
496
+ for cmd in get_commands().values():
497
+ self._add_app_command(cmd)
498
+
499
+ def _add_app_command(self, cmd: "AdapterCommand"):
500
+ """Convert an AdapterCommand to a discord app_commands.Command and add to the bot tree."""
501
+ adapter = self
502
+
503
+ # user_id is auto-injected from the interaction, so hide it from the Discord UI
504
+ visible_params = [p for p in cmd.params if p.name != "user_id"]
505
+ auto_inject_user_id = len(visible_params) < len(cmd.params)
506
+
507
+ sig_params = [
508
+ inspect.Parameter("interaction", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=discord.Interaction)
509
+ ]
510
+ for p in visible_params:
511
+ ann = Optional[p.type] if not p.required else p.type
512
+ default = inspect.Parameter.empty if p.required else None
513
+ sig_params.append(inspect.Parameter(p.name, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=ann, default=default))
514
+
515
+ async def callback(interaction: discord.Interaction, **kwargs):
516
+ await interaction.response.defer()
517
+ if auto_inject_user_id:
518
+ kwargs.setdefault("user_id", str(interaction.user.id))
519
+ try:
520
+ result = await cmd.handler(adapter, **kwargs)
521
+ except Exception as e:
522
+ logger.error("App command '%s' failed: %s", cmd.name, e)
523
+ result = f"Command failed: {e}"
524
+ await interaction.followup.send(str(result)[:2000])
525
+
526
+ callback.__signature__ = inspect.Signature(sig_params)
527
+ callback.__annotations__ = {p.name: p.annotation for p in sig_params}
528
+
529
+ descriptions = {p.name: p.description for p in visible_params}
530
+ if descriptions:
531
+ callback = discord.app_commands.describe(**descriptions)(callback)
532
+
533
+ app_cmd = discord.app_commands.Command(name=cmd.name, description=cmd.description, callback=callback)
534
+ self.bot.tree.add_command(app_cmd)
535
+
476
536
  async def _process_message(self, message, user_msg: str, bot_name: str):
477
537
  """Process a message in an isolated task."""
478
538
  is_thread = isinstance(message.channel, discord.Thread)
@@ -365,6 +365,7 @@ class HTTPServer:
365
365
  Route("/api/sessions", self._api_list_sessions, methods=["GET"]),
366
366
  Route("/api/sessions", self._api_start_session, methods=["POST"]),
367
367
  Route("/api/sessions/{session_id}", self._api_get_session, methods=["GET"]),
368
+ Route("/api/sessions/{session_id}", self._api_update_session, methods=["PATCH"]),
368
369
  Route("/api/sessions/{session_id}/cancel", self._api_cancel_session, methods=["POST"]),
369
370
  Route("/api/sessions/{session_id}/restart", self._api_restart_session, methods=["POST"]),
370
371
  Route("/api/sessions/{session_id}/events", self._api_session_events, methods=["GET"]),
@@ -389,6 +390,8 @@ class HTTPServer:
389
390
  Route("/api/kv/namespaces", self._kv_namespaces, methods=["GET"]),
390
391
  Route("/api/kv/{namespace}/keys", self._kv_keys, methods=["GET"]),
391
392
  Route("/api/kv/{namespace}/keys/{key:path}", self._kv_get, methods=["GET"]),
393
+ Route("/api/commands", self._list_commands, methods=["GET"]),
394
+ Route("/api/agents/{agent}/commands/{command_name}", self._run_command, methods=["POST"]),
392
395
  Mount("/static", app=StaticFiles(directory=str(WEB_DIR)), name="static"),
393
396
  Route("/", self._serve_ui, methods=["GET"]),
394
397
  ]
@@ -397,6 +400,53 @@ class HTTPServer:
397
400
  async def _health(self, request: Request) -> JSONResponse:
398
401
  return JSONResponse({"status": "ok", "agents": list(self.adapters.keys())})
399
402
 
403
+ async def _list_commands(self, request: Request) -> JSONResponse:
404
+ if err := self._check_auth(request):
405
+ return err
406
+ from tsugite.daemon.commands import get_commands
407
+
408
+ return JSONResponse({"commands": [
409
+ {
410
+ "name": cmd.name,
411
+ "description": cmd.description,
412
+ "params": [
413
+ {"name": p.name, "type": p.type.__name__, "description": p.description, "required": p.required, **({"choices": p.choices} if p.choices else {})}
414
+ for p in cmd.params
415
+ ],
416
+ }
417
+ for cmd in get_commands().values()
418
+ ]})
419
+
420
+ async def _run_command(self, request: Request) -> JSONResponse:
421
+ adapter, err = self._get_adapter(request)
422
+ if err:
423
+ return err
424
+ from tsugite.daemon.commands import get_commands
425
+
426
+ command_name = request.path_params["command_name"]
427
+ commands = get_commands()
428
+ if command_name not in commands:
429
+ return JSONResponse({"error": f"Unknown command: {command_name}"}, status_code=404)
430
+
431
+ cmd = commands[command_name]
432
+ try:
433
+ body = await request.json()
434
+ except Exception:
435
+ body = {}
436
+
437
+ allowed_keys = {p.name for p in cmd.params}
438
+ filtered = {k: v for k, v in body.items() if k in allowed_keys}
439
+
440
+ missing = [p.name for p in cmd.params if p.required and p.name not in filtered]
441
+ if missing:
442
+ return JSONResponse({"error": f"Missing required params: {', '.join(missing)}"}, status_code=400)
443
+
444
+ try:
445
+ result = await cmd.handler(adapter, **filtered)
446
+ except Exception as e:
447
+ return JSONResponse({"error": str(e)}, status_code=500)
448
+ return JSONResponse({"result": result})
449
+
400
450
  async def _list_agents(self, request: Request) -> JSONResponse:
401
451
  if err := self._check_auth(request):
402
452
  return err
@@ -452,6 +502,7 @@ class HTTPServer:
452
502
  "model": s.model,
453
503
  "error": s.error,
454
504
  "result": s.result,
505
+ "title": s.title,
455
506
  }
456
507
  )
457
508
 
@@ -725,6 +776,8 @@ class HTTPServer:
725
776
 
726
777
  new_session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
727
778
  self.event_bus.emit("agent_status", {"agent": agent_name})
779
+ if new_session:
780
+ self.event_bus.emit("session_update", {"action": "compacted", "id": new_session.id})
728
781
  return JSONResponse(
729
782
  {
730
783
  "status": "compacted",
@@ -1143,6 +1196,7 @@ class HTTPServer:
1143
1196
  "created_at": s.created_at,
1144
1197
  "updated_at": s.last_active,
1145
1198
  "error": s.error,
1199
+ "title": s.title,
1146
1200
  }
1147
1201
  for s in sessions
1148
1202
  ]
@@ -1196,6 +1250,23 @@ class HTTPServer:
1196
1250
  except ValueError as e:
1197
1251
  return JSONResponse({"error": str(e)}, status_code=404)
1198
1252
 
1253
+ async def _api_update_session(self, request: Request) -> JSONResponse:
1254
+ if err := self._require_auth_and_sessions(request):
1255
+ return err
1256
+ session_id = request.path_params["session_id"]
1257
+ try:
1258
+ body = await request.json()
1259
+ except Exception:
1260
+ return JSONResponse({"error": "Invalid JSON"}, status_code=400)
1261
+ title = body.get("title")
1262
+ if title is None:
1263
+ return JSONResponse({"error": "No updatable fields provided"}, status_code=400)
1264
+ try:
1265
+ self.session_runner.store.update_session(session_id, title=title)
1266
+ return JSONResponse({"ok": True, "title": title})
1267
+ except ValueError as e:
1268
+ return JSONResponse({"error": str(e)}, status_code=404)
1269
+
1199
1270
  async def _api_cancel_session(self, request: Request) -> JSONResponse:
1200
1271
  if err := self._require_auth_and_sessions(request):
1201
1272
  return err
@@ -74,6 +74,7 @@ class SchedulerAdapter:
74
74
  status=SessionStatus.RUNNING.value,
75
75
  parent_id=entry.id,
76
76
  prompt=entry.prompt or entry.command or "",
77
+ title=entry.id,
77
78
  )
78
79
  try:
79
80
  adapter.session_store.create_session(sched_session)
@@ -0,0 +1,168 @@
1
+ """Adapter command registry — define commands once, auto-register across all adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Callable
8
+
9
+ if TYPE_CHECKING:
10
+ from tsugite.daemon.adapters.base import BaseAdapter
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _COMMANDS: dict[str, AdapterCommand] = {}
15
+
16
+
17
+ @dataclass
18
+ class CommandParam:
19
+ name: str
20
+ type: type
21
+ description: str
22
+ required: bool = True
23
+ choices: list[str] | None = None
24
+
25
+
26
+ @dataclass
27
+ class AdapterCommand:
28
+ name: str
29
+ description: str
30
+ handler: Callable
31
+ params: list[CommandParam] = field(default_factory=list)
32
+
33
+
34
+ def adapter_command(
35
+ name: str,
36
+ description: str,
37
+ params: list[CommandParam] | None = None,
38
+ ):
39
+ """Decorator to register an adapter command."""
40
+
41
+ def decorator(fn: Callable) -> Callable:
42
+ if name in _COMMANDS:
43
+ logger.warning("Overwriting existing adapter command '%s'", name)
44
+ _COMMANDS[name] = AdapterCommand(
45
+ name=name,
46
+ description=description,
47
+ handler=fn,
48
+ params=params or [],
49
+ )
50
+ return fn
51
+
52
+ return decorator
53
+
54
+
55
+ def get_commands() -> dict[str, AdapterCommand]:
56
+ return _COMMANDS
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Built-in commands
61
+ # ---------------------------------------------------------------------------
62
+
63
+
64
+ @adapter_command(
65
+ name="bg",
66
+ description="Run a task in the background",
67
+ params=[
68
+ CommandParam("prompt", str, "The task to run"),
69
+ CommandParam("agent", str, "Target agent", required=False),
70
+ ],
71
+ )
72
+ async def cmd_bg(adapter: BaseAdapter, prompt: str, agent: str | None = None) -> str:
73
+ """Start a background session with the given prompt."""
74
+ from tsugite.daemon.session_store import Session, SessionSource
75
+ from tsugite.tools.sessions import _session_runner
76
+
77
+ if not _session_runner:
78
+ return "Background sessions require the daemon session runner to be enabled."
79
+
80
+ target_agent = agent or adapter.agent_name
81
+
82
+ session = Session(
83
+ id="",
84
+ agent=target_agent,
85
+ source=SessionSource.BACKGROUND.value,
86
+ prompt=prompt,
87
+ )
88
+
89
+ try:
90
+ result = _session_runner.start_session(session)
91
+ except Exception as e:
92
+ return f"Failed to start background session: {e}"
93
+
94
+ return f"Background session started (ID: {result.id})"
95
+
96
+
97
+ @adapter_command(
98
+ name="compact",
99
+ description="Compact the current conversation to free context space",
100
+ params=[
101
+ CommandParam("user_id", str, "User whose session to compact"),
102
+ CommandParam("message", str, "Extra instructions for compaction (e.g. remember/forget specific things)", required=False),
103
+ ],
104
+ )
105
+ async def cmd_compact(adapter: BaseAdapter, user_id: str, message: str | None = None) -> str:
106
+ """Compact the interactive session for the given user."""
107
+ session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
108
+
109
+ if session.message_count == 0:
110
+ return "No conversation to compact."
111
+
112
+ if not adapter.session_store.begin_compaction(user_id, adapter.agent_name):
113
+ return "Compaction already in progress."
114
+
115
+ old_id = session.id
116
+ adapter._broadcast_compaction(adapter.agent_name, started=True)
117
+ try:
118
+ await adapter._compact_session(session.id, instructions=message)
119
+ except Exception as e:
120
+ return f"Compaction failed: {e}"
121
+ finally:
122
+ adapter.session_store.end_compaction(user_id, adapter.agent_name)
123
+ adapter._broadcast_compaction(adapter.agent_name, started=False)
124
+
125
+ new_session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
126
+ return f"Session compacted (old: {old_id[:12]}, new: {new_session.id[:12]})"
127
+
128
+
129
+ @adapter_command(
130
+ name="status",
131
+ description="Show agent status and context usage",
132
+ params=[CommandParam("user_id", str, "User to check status for")],
133
+ )
134
+ async def cmd_status(adapter: BaseAdapter, user_id: str) -> str:
135
+ """Show current agent status, token usage, and context window info."""
136
+ session = adapter.session_store.get_or_create_interactive(user_id, adapter.agent_name)
137
+ context_limit = adapter.session_store.get_context_limit(adapter.agent_name)
138
+ tokens = session.cumulative_tokens
139
+ pct = int(tokens / context_limit * 100) if context_limit else 0
140
+ compacting = adapter.session_store.is_compacting(user_id, adapter.agent_name)
141
+
142
+ lines = [
143
+ f"Model: {adapter.resolve_model()}",
144
+ f"Context: {tokens:,} / {context_limit:,} tokens ({pct}%)",
145
+ f"Messages: {session.message_count}",
146
+ ]
147
+ if compacting:
148
+ lines.append("Compaction: in progress")
149
+ return "\n".join(lines)
150
+
151
+
152
+ @adapter_command(
153
+ name="sessions",
154
+ description="List active and recent background sessions",
155
+ params=[CommandParam("status", str, "Filter by status (running, completed, failed)", required=False)],
156
+ )
157
+ async def cmd_sessions(adapter: BaseAdapter, status: str | None = None) -> str:
158
+ """List background sessions for the current agent."""
159
+ sessions = adapter.session_store.list_sessions(agent=adapter.agent_name, status=status)
160
+ if not sessions:
161
+ return "No sessions found."
162
+ lines = []
163
+ for s in sessions[:10]:
164
+ label = s.title or (s.prompt or "")[:60]
165
+ lines.append(f"[{s.status}] {s.id[:12]} — {label}")
166
+ if len(sessions) > 10:
167
+ lines.append(f"... and {len(sessions) - 10} more")
168
+ return "\n".join(lines)
@@ -42,6 +42,7 @@ class DiscordBotConfig(BaseModel):
42
42
  token: str
43
43
  agent: str # References agents key
44
44
  command_prefix: str = "!"
45
+ guild_id: Optional[str] = None # Sync app commands to this guild only (instant; good for dev)
45
46
  dm_policy: Literal["allowlist", "open"] = "allowlist"
46
47
  allow_from: List[str] = Field(default_factory=list)
47
48
 
@@ -266,6 +266,56 @@ async def summarize_session(
266
266
  return await _combine_summaries(chunk_summaries, model)
267
267
 
268
268
 
269
+ TITLE_SYSTEM_PROMPT = (
270
+ "Generate a short title (3-6 words) for this conversation. "
271
+ "Return only the title, nothing else. No quotes, no punctuation at the end."
272
+ )
273
+
274
+ TITLE_TIMEOUT = 30
275
+ SHORT_TITLE_THRESHOLD = 60
276
+
277
+
278
+ async def generate_session_title(messages: list[dict], model: str) -> str:
279
+ """Generate a short title from conversation messages using a cheap model."""
280
+ text_parts = []
281
+ for msg in messages:
282
+ content = _message_text(msg)
283
+ if content:
284
+ text_parts.append(f"{msg.get('role', 'user').upper()}: {content[:500]}")
285
+ if not text_parts:
286
+ return ""
287
+ convo_text = "\n\n".join(text_parts)
288
+ title = await asyncio.wait_for(
289
+ _llm_complete(TITLE_SYSTEM_PROMPT, convo_text, model),
290
+ timeout=TITLE_TIMEOUT,
291
+ )
292
+ return title.strip().strip('"\'')[:80]
293
+
294
+
295
+ async def auto_title_session(
296
+ session_id: str,
297
+ user_content: str,
298
+ assistant_content: str,
299
+ agent_model: str,
300
+ store,
301
+ event_bus=None,
302
+ ) -> None:
303
+ """Generate and store a title for a session. Skips LLM call for short prompts."""
304
+ if len(user_content) <= SHORT_TITLE_THRESHOLD:
305
+ store.update_session(session_id, title=user_content)
306
+ else:
307
+ model = infer_compaction_model(agent_model)
308
+ messages = [
309
+ {"role": "user", "content": user_content},
310
+ {"role": "assistant", "content": assistant_content},
311
+ ]
312
+ title = await generate_session_title(messages, model)
313
+ if title:
314
+ store.update_session(session_id, title=title)
315
+ if event_bus:
316
+ event_bus.emit("session_update", {"action": "titled", "id": session_id})
317
+
318
+
269
319
  def extract_file_paths_from_turns(turns: "list[Turn]") -> list[str]:
270
320
  """Extract file paths mentioned in tool calls across turns.
271
321