ccgram 2.2.0__tar.gz → 2.2.2__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.
- {ccgram-2.2.0 → ccgram-2.2.2}/CHANGELOG.md +15 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/PKG-INFO +1 -1
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/_version.py +2 -2
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/bot.py +4 -1
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/cc_commands.py +37 -6
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/hook_events.py +66 -23
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/message_queue.py +25 -8
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/status_polling.py +6 -5
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_hook_events.py +226 -24
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_status_polling.py +49 -2
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_tool_batching.py +38 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.claude/rules/architecture.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.claude/rules/message-handling.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.claude/rules/topic-architecture.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.claude/skills/releasing/SKILL.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.env.example +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.github/workflows/ci.yml +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.github/workflows/release.yml +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/.gitignore +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/CLAUDE.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/LICENSE +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/Makefile +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/README.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/cliff.toml +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/ai-agents/README.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/ai-agents/architecture-map.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/ai-agents/codebase-index.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/ai-agents/extension-and-fix-playbook.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/ai-agents/tooling-and-tests.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/docs/guides.md +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/llm.txt +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/pyproject.toml +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/scripts/generate_homebrew_formula.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/scripts/restart.sh +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/cli.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/codex_status.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/command_catalog.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/config.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/doctor_cmd.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/JetBrainsMono-Regular.ttf +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/LICENSE-JetBrainsMono.txt +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/LICENSE-NotoSansMono.txt +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/LICENSE-Symbola.txt +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/NotoSansMonoCJKsc-Regular.otf +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/fonts/Symbola.ttf +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/callback_data.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/callback_helpers.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/cleanup.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/command_history.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/directory_browser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/directory_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/file_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/history.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/history_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/interactive_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/interactive_ui.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/message_sender.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/recovery_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/response_builder.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/restore_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/resume_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/screenshot_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/sessions_dashboard.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/sync_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/text_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/topic_emoji.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/upgrade.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/user_state.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/voice_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/voice_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/handlers/window_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/hook.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/interactive_prompt_formatter.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/main.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/markdown_v2.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/monitor_state.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/_jsonl.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/base.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/claude.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/codex.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/gemini.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/providers/registry.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/screen_buffer.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/screenshot.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/session.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/session_monitor.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/state_persistence.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/status_cmd.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/telegram_request.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/telegram_sender.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/terminal_parser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/tmux_manager.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/transcript_parser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/utils.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/whisper/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/whisper/base.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/whisper/httpx_transcriber.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/src/ccgram/window_resolver.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/conftest.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/handlers/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/handlers/test_command_history.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/handlers/test_history.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/handlers/test_response_builder.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/handlers/test_voice_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_bot_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_callback_auth.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_cc_commands.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_claude_characterization.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_cleanup.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_cli.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_codex_status.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_command_catalog.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_commands_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_config.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_directory_browser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_doctor_cmd.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_emdash_integration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_external_discovery.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_file_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_forward_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_group_filter.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_handle_new_window.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_hook.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_interactive_prompt_formatter.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_interactive_ui.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_jsonl_providers.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_kill_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_markdown_v2.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_message_queue_properties.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_message_sender.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_monitor_state.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_new_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_new_window_sync.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_provider_autodetect.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_provider_contracts.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_provider_registry.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_provider_selection.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_recovery_ui.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_restore_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_resume_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_screen_buffer.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_session.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_session_favorites.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_session_monitor.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_session_monitor_events.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_session_notification_mode.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_sessions_dashboard.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_state_migration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_status_buttons.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_status_cmd.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_status_recall_callback.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_sync_command.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_task_utils.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_telegram_request.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_telegram_sender.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_terminal_parser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_text_handler.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_tmux_autodetect.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_topic_edited.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_topic_emoji.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_transcript_parser.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_utils.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_vim_mode.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/test_window_callbacks.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/whisper/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/ccgram/whisper/test_transcriber.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/conftest.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/__init__.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/_helpers.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/conftest.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/test_claude_lifecycle.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/test_codex_lifecycle.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/test_gemini_lifecycle.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/e2e/test_voice_lifecycle.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/conftest.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_autodetect_integration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_config_integration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_hook_pipeline.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_message_dispatch.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_monitor_flow.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_monitor_state_integration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_state_roundtrip.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_tmux_manager.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/tests/integration/test_whisper_integration.py +0 -0
- {ccgram-2.2.0 → ccgram-2.2.2}/uv.lock +0 -0
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
## [2.2.2] - 2026-03-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Handle Telegram flood control during startup command registration
|
|
11
|
+
|
|
12
|
+
## [2.2.1] - 2026-03-20
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Subagent context binding ([#32](https://github.com/alexei-led/ccgram/pull/32))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
- Update CHANGELOG.md for v2.2.1
|
|
20
|
+
|
|
7
21
|
## [2.2.0] - 2026-03-20
|
|
8
22
|
|
|
9
23
|
### Added
|
|
@@ -12,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
26
|
|
|
13
27
|
### Documentation
|
|
14
28
|
- Update release process in CLAUDE.md [skip ci]
|
|
29
|
+
- Update CHANGELOG.md for v2.2.0
|
|
15
30
|
|
|
16
31
|
## [2.1.2] - 2026-03-20
|
|
17
32
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 2,
|
|
31
|
+
__version__ = version = '2.2.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 2, 2)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1649,7 +1649,10 @@ async def post_init(application: Application) -> None:
|
|
|
1649
1649
|
global session_monitor, _status_poll_task, _global_provider_menu
|
|
1650
1650
|
|
|
1651
1651
|
default_provider = get_provider()
|
|
1652
|
-
|
|
1652
|
+
try:
|
|
1653
|
+
await register_commands(application.bot, provider=default_provider)
|
|
1654
|
+
except TelegramError:
|
|
1655
|
+
logger.warning("Failed to register bot commands at startup, will retry later")
|
|
1653
1656
|
_global_provider_menu = default_provider.capabilities.name
|
|
1654
1657
|
|
|
1655
1658
|
# Refresh bot command menu every 10 minutes.
|
|
@@ -286,10 +286,41 @@ async def register_commands(
|
|
|
286
286
|
bot_commands.append(BotCommand(cmd.telegram_name, desc))
|
|
287
287
|
cc_count += 1
|
|
288
288
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
289
|
+
import asyncio
|
|
290
|
+
|
|
291
|
+
from telegram.error import RetryAfter
|
|
292
|
+
|
|
293
|
+
max_attempts = 3
|
|
294
|
+
for attempt in range(1, max_attempts + 1):
|
|
295
|
+
try:
|
|
296
|
+
if scope is None:
|
|
297
|
+
await bot.delete_my_commands()
|
|
298
|
+
await bot.set_my_commands(bot_commands)
|
|
299
|
+
else:
|
|
300
|
+
await bot.delete_my_commands(scope=scope)
|
|
301
|
+
await bot.set_my_commands(bot_commands, scope=scope)
|
|
302
|
+
break
|
|
303
|
+
except RetryAfter as e:
|
|
304
|
+
retry_secs = min(
|
|
305
|
+
60,
|
|
306
|
+
(
|
|
307
|
+
e.retry_after
|
|
308
|
+
if isinstance(e.retry_after, int)
|
|
309
|
+
else int(e.retry_after.total_seconds())
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
if attempt < max_attempts:
|
|
313
|
+
logger.warning(
|
|
314
|
+
"Telegram flood control registering commands, retry in %ds (attempt %d/%d)",
|
|
315
|
+
retry_secs,
|
|
316
|
+
attempt,
|
|
317
|
+
max_attempts,
|
|
318
|
+
)
|
|
319
|
+
await asyncio.sleep(retry_secs)
|
|
320
|
+
else:
|
|
321
|
+
logger.warning(
|
|
322
|
+
"Telegram flood control registering commands, giving up after %d attempts",
|
|
323
|
+
max_attempts,
|
|
324
|
+
)
|
|
325
|
+
return
|
|
295
326
|
logger.info("Registered %d bot commands (%d CC)", len(bot_commands), cc_count)
|
|
@@ -133,13 +133,28 @@ async def _handle_stop(event: HookEvent, bot: Bot) -> None:
|
|
|
133
133
|
await enqueue_status_update(bot, user_id, window_id, None, thread_id=thread_id)
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
# Track active subagents per window: window_id ->
|
|
137
|
-
_active_subagents: dict[str,
|
|
136
|
+
# Track active subagents per window: window_id -> {subagent_id -> name}
|
|
137
|
+
_active_subagents: dict[str, dict[str, str]] = {}
|
|
138
138
|
|
|
139
|
+
_MAX_DISPLAYED_NAMES = 3
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
|
|
142
|
+
def get_subagent_names(window_id: str) -> list[str]:
|
|
143
|
+
"""Return names of active subagents for a window."""
|
|
144
|
+
return list(_active_subagents.get(window_id, {}).values())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_subagent_label(names: list[str]) -> str | None:
|
|
148
|
+
"""Build a display label for active subagents.
|
|
149
|
+
|
|
150
|
+
Returns None if no subagents are active.
|
|
151
|
+
"""
|
|
152
|
+
if not names:
|
|
153
|
+
return None
|
|
154
|
+
if len(names) == 1:
|
|
155
|
+
return f"\U0001f916 {names[0]}"
|
|
156
|
+
joined = ", ".join(names[:_MAX_DISPLAYED_NAMES])
|
|
157
|
+
return f"\U0001f916 {len(names)} subagents: {joined}"
|
|
143
158
|
|
|
144
159
|
|
|
145
160
|
def clear_subagents(window_id: str) -> None:
|
|
@@ -147,28 +162,46 @@ def clear_subagents(window_id: str) -> None:
|
|
|
147
162
|
_active_subagents.pop(window_id, None)
|
|
148
163
|
|
|
149
164
|
|
|
150
|
-
async def _handle_subagent_start(event: HookEvent, bot: Bot) -> None:
|
|
151
|
-
"""Handle SubagentStart — track active subagent."""
|
|
165
|
+
async def _handle_subagent_start(event: HookEvent, bot: Bot) -> None:
|
|
166
|
+
"""Handle SubagentStart — track active subagent and notify."""
|
|
167
|
+
from .message_queue import enqueue_status_update
|
|
168
|
+
|
|
152
169
|
users = _resolve_users_for_window_key(event.window_key)
|
|
153
170
|
if not users:
|
|
154
171
|
return
|
|
155
172
|
|
|
156
173
|
window_id = users[0][2] # all users share the same window_id
|
|
157
174
|
subagent_id = event.data.get("subagent_id", "")
|
|
175
|
+
name = (
|
|
176
|
+
(event.data.get("name") or "").strip()
|
|
177
|
+
or (event.data.get("description") or "").strip()
|
|
178
|
+
or subagent_id[:12]
|
|
179
|
+
or "subagent"
|
|
180
|
+
)
|
|
158
181
|
|
|
159
|
-
_active_subagents.setdefault(window_id,
|
|
182
|
+
_active_subagents.setdefault(window_id, {})[subagent_id] = name
|
|
160
183
|
|
|
161
|
-
count = len(_active_subagents[window_id])
|
|
162
184
|
logger.debug(
|
|
163
185
|
"Subagent started: window=%s, count=%d, name=%s",
|
|
164
186
|
window_id,
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
len(_active_subagents[window_id]),
|
|
188
|
+
name,
|
|
167
189
|
)
|
|
168
190
|
|
|
191
|
+
for user_id, thread_id, _ in users:
|
|
192
|
+
await enqueue_status_update(
|
|
193
|
+
bot,
|
|
194
|
+
user_id,
|
|
195
|
+
window_id,
|
|
196
|
+
f"\U0001f916 Subagent started: {name}",
|
|
197
|
+
thread_id=thread_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def _handle_subagent_stop(event: HookEvent, bot: Bot) -> None:
|
|
202
|
+
"""Handle SubagentStop — remove subagent from tracking and notify."""
|
|
203
|
+
from .message_queue import enqueue_status_update
|
|
169
204
|
|
|
170
|
-
async def _handle_subagent_stop(event: HookEvent, bot: Bot) -> None: # noqa: ARG001
|
|
171
|
-
"""Handle SubagentStop — remove subagent from tracking."""
|
|
172
205
|
users = _resolve_users_for_window_key(event.window_key)
|
|
173
206
|
if not users:
|
|
174
207
|
return
|
|
@@ -176,21 +209,29 @@ async def _handle_subagent_stop(event: HookEvent, bot: Bot) -> None: # noqa: AR
|
|
|
176
209
|
window_id = users[0][2]
|
|
177
210
|
subagent_id = event.data.get("subagent_id", "")
|
|
178
211
|
|
|
179
|
-
|
|
180
|
-
if not
|
|
212
|
+
agents = _active_subagents.get(window_id)
|
|
213
|
+
if not agents:
|
|
181
214
|
return
|
|
182
|
-
|
|
183
|
-
if not
|
|
215
|
+
name = agents.pop(subagent_id, subagent_id[:12] or "subagent")
|
|
216
|
+
if not agents:
|
|
184
217
|
_active_subagents.pop(window_id, None)
|
|
185
218
|
|
|
186
|
-
count = get_subagent_count(window_id)
|
|
187
219
|
logger.debug(
|
|
188
|
-
"Subagent stopped: window=%s, remaining=%d,
|
|
220
|
+
"Subagent stopped: window=%s, remaining=%d, name=%s",
|
|
189
221
|
window_id,
|
|
190
|
-
|
|
191
|
-
|
|
222
|
+
len(_active_subagents.get(window_id, {})),
|
|
223
|
+
name,
|
|
192
224
|
)
|
|
193
225
|
|
|
226
|
+
for user_id, thread_id, _ in users:
|
|
227
|
+
await enqueue_status_update(
|
|
228
|
+
bot,
|
|
229
|
+
user_id,
|
|
230
|
+
window_id,
|
|
231
|
+
f"\U0001f916 Subagent done: {name}",
|
|
232
|
+
thread_id=thread_id,
|
|
233
|
+
)
|
|
234
|
+
|
|
194
235
|
|
|
195
236
|
async def _handle_teammate_idle(event: HookEvent, bot: Bot) -> None:
|
|
196
237
|
"""Handle TeammateIdle — notify topic that a teammate went idle."""
|
|
@@ -254,9 +295,11 @@ async def _handle_session_end(event: HookEvent, bot: Bot) -> None:
|
|
|
254
295
|
reason,
|
|
255
296
|
)
|
|
256
297
|
|
|
257
|
-
# Clear session association so next launch
|
|
298
|
+
# Clear session association and subagent tracking so next launch starts fresh
|
|
258
299
|
if users:
|
|
259
|
-
|
|
300
|
+
window_id = users[0][2]
|
|
301
|
+
session_manager.clear_window_session(window_id)
|
|
302
|
+
clear_subagents(window_id)
|
|
260
303
|
|
|
261
304
|
for user_id, thread_id, window_id in users:
|
|
262
305
|
clear_seen_status(window_id)
|
|
@@ -83,18 +83,23 @@ def _is_batch_eligible(task: MessageTask) -> bool:
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def format_batch_message(
|
|
86
|
+
def format_batch_message(
|
|
87
|
+
entries: list[ToolBatchEntry], subagent_label: str | None = None
|
|
88
|
+
) -> str:
|
|
87
89
|
"""Render a batch of tool calls as a single compact message.
|
|
88
90
|
|
|
89
91
|
Format:
|
|
90
|
-
⚡ 3 tool calls
|
|
92
|
+
⚡ 3 tool calls [🤖 write-tests]
|
|
91
93
|
📖 Read src/foo.py ⎿ 42 lines
|
|
92
94
|
✏️ Edit src/foo.py ⎿ +3 −1
|
|
93
95
|
⚡ Bash make test ⏳
|
|
94
96
|
"""
|
|
95
97
|
count = len(entries)
|
|
96
98
|
label = "tool call" if count == 1 else "tool calls"
|
|
97
|
-
|
|
99
|
+
header = f"\u26a1 {count} {label}"
|
|
100
|
+
if subagent_label:
|
|
101
|
+
header = f"{header} [{subagent_label}]"
|
|
102
|
+
lines = [header]
|
|
98
103
|
|
|
99
104
|
for entry in entries:
|
|
100
105
|
line = entry.tool_use_text
|
|
@@ -400,7 +405,10 @@ async def _process_batch_task(bot: Bot, user_id: int, task: MessageTask) -> None
|
|
|
400
405
|
return
|
|
401
406
|
|
|
402
407
|
# Send or edit batch message
|
|
403
|
-
|
|
408
|
+
from .hook_events import build_subagent_label, get_subagent_names
|
|
409
|
+
|
|
410
|
+
subagent_label = build_subagent_label(get_subagent_names(window_id))
|
|
411
|
+
batch_text = format_batch_message(batch.entries, subagent_label=subagent_label)
|
|
404
412
|
|
|
405
413
|
if batch.telegram_msg_id is None:
|
|
406
414
|
# Clear status message first, then send new batch message
|
|
@@ -445,7 +453,11 @@ async def _flush_batch(bot: Bot, user_id: int, thread_id_or_0: int) -> None:
|
|
|
445
453
|
|
|
446
454
|
thread_id: int | None = thread_id_or_0 if thread_id_or_0 != 0 else None
|
|
447
455
|
chat_id = session_manager.resolve_chat_id(user_id, thread_id)
|
|
448
|
-
|
|
456
|
+
|
|
457
|
+
from .hook_events import build_subagent_label, get_subagent_names
|
|
458
|
+
|
|
459
|
+
subagent_label = build_subagent_label(get_subagent_names(batch.window_id))
|
|
460
|
+
batch_text = format_batch_message(batch.entries, subagent_label=subagent_label)
|
|
449
461
|
|
|
450
462
|
if batch.telegram_msg_id is None:
|
|
451
463
|
# First send failed earlier — attempt one send before dropping
|
|
@@ -878,9 +890,14 @@ async def _check_and_send_status(
|
|
|
878
890
|
|
|
879
891
|
status = get_provider_for_window(window_id).parse_terminal_status(pane_text)
|
|
880
892
|
if status and not status.is_interactive:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
893
|
+
from .hook_events import build_subagent_label, get_subagent_names
|
|
894
|
+
|
|
895
|
+
display = status.display_label
|
|
896
|
+
subagent_names = get_subagent_names(window_id)
|
|
897
|
+
if subagent_names:
|
|
898
|
+
label = build_subagent_label(subagent_names)
|
|
899
|
+
display = f"{display} ({label})"
|
|
900
|
+
await _do_send_status_message(bot, user_id, thread_id_or_0, window_id, display)
|
|
884
901
|
|
|
885
902
|
|
|
886
903
|
async def enqueue_content_message(
|
|
@@ -824,13 +824,14 @@ async def update_status_message(
|
|
|
824
824
|
ws.startup_time = None
|
|
825
825
|
await _send_typing_throttled(bot, user_id, thread_id)
|
|
826
826
|
if notif_mode not in ("muted", "errors_only"):
|
|
827
|
-
# Append subagent
|
|
828
|
-
from .hook_events import
|
|
827
|
+
# Append subagent names if any are active
|
|
828
|
+
from .hook_events import build_subagent_label, get_subagent_names
|
|
829
829
|
|
|
830
|
-
|
|
830
|
+
subagent_names = get_subagent_names(window_id)
|
|
831
831
|
display_status = status_line
|
|
832
|
-
if
|
|
833
|
-
|
|
832
|
+
if subagent_names:
|
|
833
|
+
label = build_subagent_label(subagent_names)
|
|
834
|
+
display_status = f"{status_line} ({label})"
|
|
834
835
|
await enqueue_status_update(
|
|
835
836
|
bot,
|
|
836
837
|
user_id,
|
|
@@ -8,9 +8,10 @@ from ccgram.handlers.hook_events import (
|
|
|
8
8
|
HookEvent,
|
|
9
9
|
_active_subagents,
|
|
10
10
|
_resolve_users_for_window_key,
|
|
11
|
+
build_subagent_label,
|
|
11
12
|
clear_subagents,
|
|
12
13
|
dispatch_hook_event,
|
|
13
|
-
|
|
14
|
+
get_subagent_names,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
@@ -64,17 +65,55 @@ class TestSubagentTracking:
|
|
|
64
65
|
def setup_method(self) -> None:
|
|
65
66
|
_active_subagents.clear()
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
-
_active_subagents["@0"] = {"a1"}
|
|
69
|
-
assert
|
|
68
|
+
def test_count_via_names(self) -> None:
|
|
69
|
+
_active_subagents["@0"] = {"a1": "agent-1"}
|
|
70
|
+
assert len(get_subagent_names("@0")) == 1
|
|
70
71
|
|
|
71
72
|
def test_clear_removes_all(self) -> None:
|
|
72
|
-
_active_subagents["@0"] = {"a1", "a2"}
|
|
73
|
+
_active_subagents["@0"] = {"a1": "agent-1", "a2": "agent-2"}
|
|
73
74
|
clear_subagents("@0")
|
|
74
|
-
assert
|
|
75
|
+
assert get_subagent_names("@0") == []
|
|
75
76
|
|
|
76
|
-
def
|
|
77
|
-
assert
|
|
77
|
+
def test_names_missing_window(self) -> None:
|
|
78
|
+
assert get_subagent_names("@999") == []
|
|
79
|
+
|
|
80
|
+
def test_get_names_returns_values(self) -> None:
|
|
81
|
+
_active_subagents["@0"] = {"a1": "write-tests", "a2": "refactor"}
|
|
82
|
+
names = get_subagent_names("@0")
|
|
83
|
+
assert sorted(names) == ["refactor", "write-tests"]
|
|
84
|
+
|
|
85
|
+
def test_get_names_empty_after_clear(self) -> None:
|
|
86
|
+
_active_subagents["@0"] = {"a1": "agent-1"}
|
|
87
|
+
clear_subagents("@0")
|
|
88
|
+
assert get_subagent_names("@0") == []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestBuildSubagentLabel:
|
|
92
|
+
def test_empty_list(self) -> None:
|
|
93
|
+
assert build_subagent_label([]) is None
|
|
94
|
+
|
|
95
|
+
def test_single_name(self) -> None:
|
|
96
|
+
assert build_subagent_label(["write-tests"]) == "\U0001f916 write-tests"
|
|
97
|
+
|
|
98
|
+
def test_multiple_names(self) -> None:
|
|
99
|
+
result = build_subagent_label(["write-tests", "refactor"])
|
|
100
|
+
assert result is not None
|
|
101
|
+
assert "\U0001f916" in result
|
|
102
|
+
assert "2 subagents" in result
|
|
103
|
+
assert "write-tests" in result
|
|
104
|
+
assert "refactor" in result
|
|
105
|
+
|
|
106
|
+
def test_three_names(self) -> None:
|
|
107
|
+
result = build_subagent_label(["a", "b", "c"])
|
|
108
|
+
assert result is not None
|
|
109
|
+
assert "3 subagents" in result
|
|
110
|
+
|
|
111
|
+
def test_truncates_at_three(self) -> None:
|
|
112
|
+
result = build_subagent_label(["a", "b", "c", "d"])
|
|
113
|
+
assert result is not None
|
|
114
|
+
assert "4 subagents" in result
|
|
115
|
+
assert "a, b, c" in result
|
|
116
|
+
assert "d" not in result
|
|
78
117
|
|
|
79
118
|
|
|
80
119
|
class TestDispatchHookEvent:
|
|
@@ -219,25 +258,132 @@ class TestHandleSubagentStart:
|
|
|
219
258
|
lambda: iter([(100, 42, "@0")]),
|
|
220
259
|
)
|
|
221
260
|
bot = AsyncMock(spec=Bot)
|
|
261
|
+
with patch(
|
|
262
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
263
|
+
) as mock_enqueue:
|
|
264
|
+
event = _make_event(
|
|
265
|
+
event_type="SubagentStart",
|
|
266
|
+
data={"subagent_id": "sub-1", "name": "researcher"},
|
|
267
|
+
)
|
|
268
|
+
await dispatch_hook_event(event, bot)
|
|
269
|
+
assert len(get_subagent_names("@0")) == 1
|
|
270
|
+
assert get_subagent_names("@0") == ["researcher"]
|
|
271
|
+
mock_enqueue.assert_called_once_with(
|
|
272
|
+
bot, 100, "@0", "\U0001f916 Subagent started: researcher", thread_id=42
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def test_tracks_multiple_subagents(self, monkeypatch) -> None:
|
|
276
|
+
monkeypatch.setattr(
|
|
277
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
278
|
+
lambda: iter([(100, 42, "@0")]),
|
|
279
|
+
)
|
|
280
|
+
bot = AsyncMock(spec=Bot)
|
|
281
|
+
with patch(
|
|
282
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
283
|
+
) as mock_enqueue:
|
|
284
|
+
for sub_id in ("sub-1", "sub-2"):
|
|
285
|
+
event = _make_event(
|
|
286
|
+
event_type="SubagentStart", data={"subagent_id": sub_id}
|
|
287
|
+
)
|
|
288
|
+
await dispatch_hook_event(event, bot)
|
|
289
|
+
assert len(get_subagent_names("@0")) == 2
|
|
290
|
+
assert mock_enqueue.call_count == 2
|
|
291
|
+
|
|
292
|
+
async def test_name_fallback_to_description(self, monkeypatch) -> None:
|
|
293
|
+
monkeypatch.setattr(
|
|
294
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
295
|
+
lambda: iter([(100, 42, "@0")]),
|
|
296
|
+
)
|
|
297
|
+
bot = AsyncMock(spec=Bot)
|
|
298
|
+
with patch(
|
|
299
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
300
|
+
) as mock_enqueue:
|
|
301
|
+
event = _make_event(
|
|
302
|
+
event_type="SubagentStart",
|
|
303
|
+
data={"subagent_id": "sub-1", "description": "explore code"},
|
|
304
|
+
)
|
|
305
|
+
await dispatch_hook_event(event, bot)
|
|
306
|
+
assert get_subagent_names("@0") == ["explore code"]
|
|
307
|
+
assert "explore code" in mock_enqueue.call_args[0][3]
|
|
308
|
+
|
|
309
|
+
async def test_name_fallback_to_truncated_id(self, monkeypatch) -> None:
|
|
310
|
+
monkeypatch.setattr(
|
|
311
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
312
|
+
lambda: iter([(100, 42, "@0")]),
|
|
313
|
+
)
|
|
314
|
+
bot = AsyncMock(spec=Bot)
|
|
315
|
+
with patch(
|
|
316
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
317
|
+
) as mock_enqueue:
|
|
318
|
+
event = _make_event(
|
|
319
|
+
event_type="SubagentStart",
|
|
320
|
+
data={"subagent_id": "abcdef123456789"},
|
|
321
|
+
)
|
|
322
|
+
await dispatch_hook_event(event, bot)
|
|
323
|
+
assert get_subagent_names("@0") == ["abcdef123456"]
|
|
324
|
+
assert "abcdef123456" in mock_enqueue.call_args[0][3]
|
|
325
|
+
|
|
326
|
+
async def test_whitespace_name_falls_back(self, monkeypatch) -> None:
|
|
327
|
+
monkeypatch.setattr(
|
|
328
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
329
|
+
lambda: iter([(100, 42, "@0")]),
|
|
330
|
+
)
|
|
331
|
+
bot = AsyncMock(spec=Bot)
|
|
332
|
+
with patch("ccgram.handlers.message_queue.enqueue_status_update"):
|
|
333
|
+
event = _make_event(
|
|
334
|
+
event_type="SubagentStart",
|
|
335
|
+
data={"subagent_id": "sub-1", "name": " ", "description": "real"},
|
|
336
|
+
)
|
|
337
|
+
await dispatch_hook_event(event, bot)
|
|
338
|
+
assert get_subagent_names("@0") == ["real"]
|
|
339
|
+
|
|
340
|
+
async def test_empty_everything_uses_fallback(self, monkeypatch) -> None:
|
|
341
|
+
monkeypatch.setattr(
|
|
342
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
343
|
+
lambda: iter([(100, 42, "@0")]),
|
|
344
|
+
)
|
|
345
|
+
bot = AsyncMock(spec=Bot)
|
|
346
|
+
with patch("ccgram.handlers.message_queue.enqueue_status_update"):
|
|
347
|
+
event = _make_event(
|
|
348
|
+
event_type="SubagentStart",
|
|
349
|
+
data={"subagent_id": "", "name": "", "description": ""},
|
|
350
|
+
)
|
|
351
|
+
await dispatch_hook_event(event, bot)
|
|
352
|
+
assert get_subagent_names("@0") == ["subagent"]
|
|
353
|
+
|
|
354
|
+
async def test_no_users_does_not_track(self, monkeypatch) -> None:
|
|
355
|
+
monkeypatch.setattr(
|
|
356
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
357
|
+
lambda: iter([]),
|
|
358
|
+
)
|
|
359
|
+
bot = AsyncMock(spec=Bot)
|
|
222
360
|
event = _make_event(
|
|
223
361
|
event_type="SubagentStart",
|
|
224
|
-
data={"subagent_id": "sub-1", "name": "
|
|
362
|
+
data={"subagent_id": "sub-1", "name": "test"},
|
|
225
363
|
)
|
|
226
364
|
await dispatch_hook_event(event, bot)
|
|
227
|
-
assert
|
|
365
|
+
assert _active_subagents == {}
|
|
228
366
|
|
|
229
|
-
async def
|
|
367
|
+
async def test_notifies_multiple_users(self, monkeypatch) -> None:
|
|
230
368
|
monkeypatch.setattr(
|
|
231
369
|
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
232
|
-
lambda: iter([(100, 42, "@0")]),
|
|
370
|
+
lambda: iter([(100, 42, "@0"), (200, 99, "@0")]),
|
|
233
371
|
)
|
|
234
372
|
bot = AsyncMock(spec=Bot)
|
|
235
|
-
|
|
373
|
+
with patch(
|
|
374
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
375
|
+
) as mock_enqueue:
|
|
236
376
|
event = _make_event(
|
|
237
|
-
event_type="SubagentStart",
|
|
377
|
+
event_type="SubagentStart",
|
|
378
|
+
data={"subagent_id": "sub-1", "name": "researcher"},
|
|
238
379
|
)
|
|
239
380
|
await dispatch_hook_event(event, bot)
|
|
240
|
-
|
|
381
|
+
assert mock_enqueue.call_count == 2
|
|
382
|
+
calls = mock_enqueue.call_args_list
|
|
383
|
+
assert calls[0][0][1] == 100 # first user_id
|
|
384
|
+
assert calls[1][0][1] == 200 # second user_id
|
|
385
|
+
assert calls[0][0][2] == "@0" # window_id from outer scope
|
|
386
|
+
assert calls[1][0][2] == "@0"
|
|
241
387
|
|
|
242
388
|
|
|
243
389
|
class TestHandleSubagentStop:
|
|
@@ -249,23 +395,49 @@ class TestHandleSubagentStop:
|
|
|
249
395
|
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
250
396
|
lambda: iter([(100, 42, "@0")]),
|
|
251
397
|
)
|
|
252
|
-
_active_subagents["@0"] = {"sub-1", "sub-2"}
|
|
398
|
+
_active_subagents["@0"] = {"sub-1": "agent-1", "sub-2": "agent-2"}
|
|
253
399
|
bot = AsyncMock(spec=Bot)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
400
|
+
with patch(
|
|
401
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
402
|
+
) as mock_enqueue:
|
|
403
|
+
event = _make_event(
|
|
404
|
+
event_type="SubagentStop", data={"subagent_id": "sub-1"}
|
|
405
|
+
)
|
|
406
|
+
await dispatch_hook_event(event, bot)
|
|
407
|
+
assert len(get_subagent_names("@0")) == 1
|
|
408
|
+
mock_enqueue.assert_called_once_with(
|
|
409
|
+
bot, 100, "@0", "\U0001f916 Subagent done: agent-1", thread_id=42
|
|
410
|
+
)
|
|
257
411
|
|
|
258
412
|
async def test_removes_last_subagent_cleans_dict(self, monkeypatch) -> None:
|
|
259
413
|
monkeypatch.setattr(
|
|
260
414
|
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
261
415
|
lambda: iter([(100, 42, "@0")]),
|
|
262
416
|
)
|
|
263
|
-
_active_subagents["@0"] = {"sub-1"}
|
|
417
|
+
_active_subagents["@0"] = {"sub-1": "agent-1"}
|
|
264
418
|
bot = AsyncMock(spec=Bot)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
419
|
+
with patch("ccgram.handlers.message_queue.enqueue_status_update"):
|
|
420
|
+
event = _make_event(
|
|
421
|
+
event_type="SubagentStop", data={"subagent_id": "sub-1"}
|
|
422
|
+
)
|
|
423
|
+
await dispatch_hook_event(event, bot)
|
|
424
|
+
assert get_subagent_names("@0") == []
|
|
425
|
+
assert "@0" not in _active_subagents
|
|
426
|
+
|
|
427
|
+
async def test_unknown_id_no_notification(self, monkeypatch) -> None:
|
|
428
|
+
monkeypatch.setattr(
|
|
429
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
430
|
+
lambda: iter([(100, 42, "@0")]),
|
|
431
|
+
)
|
|
432
|
+
bot = AsyncMock(spec=Bot)
|
|
433
|
+
with patch(
|
|
434
|
+
"ccgram.handlers.message_queue.enqueue_status_update"
|
|
435
|
+
) as mock_enqueue:
|
|
436
|
+
event = _make_event(
|
|
437
|
+
event_type="SubagentStop", data={"subagent_id": "never-seen"}
|
|
438
|
+
)
|
|
439
|
+
await dispatch_hook_event(event, bot)
|
|
440
|
+
mock_enqueue.assert_not_called()
|
|
269
441
|
|
|
270
442
|
|
|
271
443
|
class TestHandleTeammateIdle:
|
|
@@ -383,6 +555,9 @@ class TestHandleStopFailure:
|
|
|
383
555
|
|
|
384
556
|
|
|
385
557
|
class TestHandleSessionEnd:
|
|
558
|
+
def setup_method(self) -> None:
|
|
559
|
+
_active_subagents.clear()
|
|
560
|
+
|
|
386
561
|
async def test_transitions_to_done(self, monkeypatch) -> None:
|
|
387
562
|
monkeypatch.setattr(
|
|
388
563
|
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
@@ -415,6 +590,33 @@ class TestHandleSessionEnd:
|
|
|
415
590
|
mock_enqueue.assert_called_once_with(bot, 100, "@0", None, thread_id=42)
|
|
416
591
|
mock_clear_session.assert_called_once_with("@0")
|
|
417
592
|
|
|
593
|
+
async def test_clears_subagents_on_session_end(self, monkeypatch) -> None:
|
|
594
|
+
monkeypatch.setattr(
|
|
595
|
+
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|
|
596
|
+
lambda: iter([(100, 42, "@0")]),
|
|
597
|
+
)
|
|
598
|
+
_active_subagents["@0"] = {"sub-1": "researcher"}
|
|
599
|
+
bot = AsyncMock(spec=Bot)
|
|
600
|
+
with (
|
|
601
|
+
patch(
|
|
602
|
+
"ccgram.handlers.hook_events.session_manager.resolve_chat_id",
|
|
603
|
+
return_value=-100,
|
|
604
|
+
),
|
|
605
|
+
patch(
|
|
606
|
+
"ccgram.handlers.hook_events.session_manager.get_display_name",
|
|
607
|
+
return_value="project",
|
|
608
|
+
),
|
|
609
|
+
patch(
|
|
610
|
+
"ccgram.handlers.hook_events.session_manager.clear_window_session",
|
|
611
|
+
),
|
|
612
|
+
patch("ccgram.handlers.topic_emoji.update_topic_emoji"),
|
|
613
|
+
patch("ccgram.handlers.message_queue.enqueue_status_update"),
|
|
614
|
+
patch("ccgram.handlers.status_polling.clear_seen_status"),
|
|
615
|
+
):
|
|
616
|
+
event = _make_event(event_type="SessionEnd", data={"reason": "clear"})
|
|
617
|
+
await dispatch_hook_event(event, bot)
|
|
618
|
+
assert get_subagent_names("@0") == []
|
|
619
|
+
|
|
418
620
|
async def test_no_users_skips(self, monkeypatch) -> None:
|
|
419
621
|
monkeypatch.setattr(
|
|
420
622
|
"ccgram.handlers.hook_events.session_manager.iter_thread_bindings",
|