codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -62,6 +62,7 @@ DEFAULT_MESSAGE_OVERFLOW = "document"
62
62
  MESSAGE_OVERFLOW_OPTIONS = {"document", "split", "trim"}
63
63
  DEFAULT_METRICS_MODE = "separate"
64
64
  METRICS_MODE_OPTIONS = {"separate", "append_to_response", "append_to_progress"}
65
+ DEFAULT_PAUSE_DISPATCH_MAX_FILE_BYTES = 50 * 1024 * 1024
65
66
 
66
67
  PARSE_MODE_ALIASES = {
67
68
  "html": "HTML",
@@ -160,6 +161,14 @@ class TelegramBotProgressStreamConfig:
160
161
  min_edit_interval_seconds: float
161
162
 
162
163
 
164
+ @dataclass(frozen=True)
165
+ class PauseDispatchNotifications:
166
+ enabled: bool
167
+ send_attachments: bool
168
+ max_file_size_bytes: int
169
+ chunk_long_messages: bool
170
+
171
+
163
172
  @dataclass(frozen=True)
164
173
  class TelegramMediaCandidate:
165
174
  kind: str
@@ -209,6 +218,8 @@ class TelegramBotConfig:
209
218
  coalesce_window_seconds: float
210
219
  agent_binaries: dict[str, str]
211
220
  ticket_flow_auto_resume: bool
221
+ pause_dispatch_notifications: PauseDispatchNotifications
222
+ default_notification_chat_id: Optional[int]
212
223
 
213
224
  @classmethod
214
225
  def from_raw(
@@ -506,6 +517,39 @@ class TelegramBotConfig:
506
517
  )
507
518
  ticket_flow_auto_resume = bool(ticket_flow_raw.get("auto_resume", False))
508
519
 
520
+ pause_raw_value = cfg.get("pause_dispatch_notifications")
521
+ pause_raw: dict[str, Any] = (
522
+ pause_raw_value if isinstance(pause_raw_value, dict) else {}
523
+ )
524
+ pause_enabled = bool(pause_raw.get("enabled", enabled))
525
+ pause_send_attachments = bool(pause_raw.get("send_attachments", True))
526
+ pause_max_file_size_bytes = int(
527
+ pause_raw.get("max_file_size_bytes", DEFAULT_PAUSE_DISPATCH_MAX_FILE_BYTES)
528
+ )
529
+ if pause_max_file_size_bytes <= 0:
530
+ pause_max_file_size_bytes = DEFAULT_PAUSE_DISPATCH_MAX_FILE_BYTES
531
+ pause_chunk_long_messages = bool(pause_raw.get("chunk_long_messages", True))
532
+ pause_dispatch_notifications = PauseDispatchNotifications(
533
+ enabled=pause_enabled,
534
+ send_attachments=pause_send_attachments,
535
+ max_file_size_bytes=pause_max_file_size_bytes,
536
+ chunk_long_messages=pause_chunk_long_messages,
537
+ )
538
+
539
+ default_notification_chat_raw = cfg.get("default_notification_chat_id")
540
+ default_notification_chat_id: Optional[int] = None
541
+ env_chat_candidates = _parse_int_list(env.get(chat_id_env))
542
+ if default_notification_chat_raw is not None:
543
+ try:
544
+ default_notification_chat_id = int(default_notification_chat_raw)
545
+ except (TypeError, ValueError):
546
+ default_notification_chat_id = None
547
+ if default_notification_chat_id is None:
548
+ if env_chat_candidates:
549
+ default_notification_chat_id = env_chat_candidates[0]
550
+ elif allowed_chat_ids:
551
+ default_notification_chat_id = min(allowed_chat_ids)
552
+
509
553
  agent_binaries = dict(agent_binaries or {})
510
554
  command_reg_raw_value = cfg.get("command_registration")
511
555
  command_reg_raw: dict[str, Any] = (
@@ -666,6 +710,8 @@ class TelegramBotConfig:
666
710
  coalesce_window_seconds=coalesce_window_seconds,
667
711
  agent_binaries=agent_binaries,
668
712
  ticket_flow_auto_resume=ticket_flow_auto_resume,
713
+ pause_dispatch_notifications=pause_dispatch_notifications,
714
+ default_notification_chat_id=default_notification_chat_id,
669
715
  )
670
716
 
671
717
  def validate(self) -> None:
@@ -24,7 +24,6 @@ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS = {
24
24
  "codex": 28800.0,
25
25
  "opencode": 28800.0,
26
26
  }
27
- DEFAULT_WORKSPACE_STATE_ROOT = "~/.codex-autorunner/workspaces"
28
27
  DEFAULT_AGENT = "codex"
29
28
  APP_SERVER_START_BACKOFF_INITIAL_SECONDS = 1.0
30
29
  APP_SERVER_START_BACKOFF_MAX_SECONDS = 30.0
@@ -66,6 +65,7 @@ UPDATE_PICKER_PROMPT = "Select update target (buttons below)."
66
65
  REVIEW_COMMIT_PICKER_PROMPT = (
67
66
  "Select a commit to review (buttons below or reply with number)."
68
67
  )
68
+ FLOW_RUNS_PICKER_PROMPT = "Select a ticket flow run (buttons below)."
69
69
  REVIEW_COMMIT_BUTTON_LABEL_LIMIT = 80
70
70
  UPDATE_TARGET_OPTIONS = (
71
71
  ("both", "Both (web + Telegram)"),
@@ -1,47 +1,269 @@
1
1
  """Telegram integration doctor checks."""
2
2
 
3
+ import logging
4
+ import os
5
+ from datetime import datetime, timedelta, timezone
6
+ from pathlib import Path
3
7
  from typing import Any, Dict, Union
4
8
 
5
9
  from ...core.config import HubConfig, RepoConfig
6
- from ...core.engine import DoctorCheck
7
10
  from ...core.optional_dependencies import missing_optional_dependencies
11
+ from ...core.runtime import DoctorCheck
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ STUCK_TURN_THRESHOLD_MINUTES = 30
16
+ STATE_FILE_CHECK = ".codex-autorunner/telegram_state.sqlite3"
8
17
 
9
18
 
10
19
  def telegram_doctor_checks(
11
20
  config: Union[HubConfig, RepoConfig, Dict[str, Any]],
21
+ repo_root: Union[Path, None] = None,
12
22
  ) -> list[DoctorCheck]:
13
23
  """Run Telegram-specific doctor checks.
14
24
 
15
25
  Returns a list of DoctorCheck objects for Telegram integration.
16
26
  Works with HubConfig, RepoConfig, or raw dict.
27
+
28
+ Args:
29
+ config: HubConfig, RepoConfig, or raw dict
30
+ repo_root: Optional repo root path for state file checks
17
31
  """
18
32
  checks: list[DoctorCheck] = []
19
33
  telegram_cfg = None
20
34
 
21
35
  if isinstance(config, dict):
22
36
  telegram_cfg = config.get("telegram_bot")
37
+ if not telegram_cfg:
38
+ telegram_cfg = config.get("notifications", {}).get("telegram", {})
23
39
  elif isinstance(config.raw, dict):
24
40
  telegram_cfg = config.raw.get("telegram_bot")
41
+ if not telegram_cfg:
42
+ telegram_cfg = config.raw.get("notifications", {}).get("telegram", {})
25
43
 
26
- if isinstance(telegram_cfg, dict) and telegram_cfg.get("enabled") is True:
44
+ enabled = isinstance(telegram_cfg, dict) and telegram_cfg.get("enabled") is True
45
+
46
+ if enabled:
27
47
  missing_telegram = missing_optional_dependencies((("httpx", "httpx"),))
28
48
  if missing_telegram:
29
49
  deps_list = ", ".join(missing_telegram)
30
50
  checks.append(
31
51
  DoctorCheck(
32
- check_id="telegram.dependencies",
33
- status="error",
52
+ name="Telegram dependencies",
53
+ passed=False,
34
54
  message=f"Telegram is enabled but missing optional deps: {deps_list}",
55
+ check_id="telegram.dependencies",
35
56
  fix="Install with `pip install codex-autorunner[telegram]`.",
36
57
  )
37
58
  )
59
+ return checks
38
60
  else:
39
61
  checks.append(
40
62
  DoctorCheck(
41
- check_id="telegram.dependencies",
42
- status="ok",
63
+ name="Telegram dependencies",
64
+ passed=True,
43
65
  message="Telegram dependencies are installed.",
66
+ check_id="telegram.dependencies",
67
+ severity="info",
44
68
  )
45
69
  )
46
70
 
71
+ bot_token_env = telegram_cfg.get("bot_token_env", "CAR_TELEGRAM_BOT_TOKEN")
72
+ chat_id_env = telegram_cfg.get("chat_id_env", "CAR_TELEGRAM_CHAT_ID")
73
+
74
+ bot_token = os.environ.get(bot_token_env)
75
+ chat_id = os.environ.get(chat_id_env)
76
+
77
+ if not bot_token:
78
+ checks.append(
79
+ DoctorCheck(
80
+ name="Telegram bot token",
81
+ passed=False,
82
+ message=f"Telegram bot token not found in environment: {bot_token_env}",
83
+ check_id="telegram.bot_token",
84
+ fix=f"Set {bot_token_env} environment variable or disable Telegram.",
85
+ )
86
+ )
87
+ else:
88
+ checks.append(
89
+ DoctorCheck(
90
+ name="Telegram bot token",
91
+ passed=True,
92
+ message=f"Bot token configured (env: {bot_token_env}).",
93
+ check_id="telegram.bot_token",
94
+ severity="info",
95
+ )
96
+ )
97
+
98
+ if not chat_id:
99
+ checks.append(
100
+ DoctorCheck(
101
+ name="Telegram chat ID",
102
+ passed=False,
103
+ message=f"Telegram chat_id not found in environment: {chat_id_env}",
104
+ check_id="telegram.chat_id",
105
+ fix=f"Set {chat_id_env} environment variable for notifications.",
106
+ severity="warning",
107
+ )
108
+ )
109
+ else:
110
+ checks.append(
111
+ DoctorCheck(
112
+ name="Telegram chat ID",
113
+ passed=True,
114
+ message=f"Chat ID configured (env: {chat_id_env}).",
115
+ check_id="telegram.chat_id",
116
+ severity="info",
117
+ )
118
+ )
119
+
120
+ allowed_chats = telegram_cfg.get("allowed_chat_ids", [])
121
+ allowed_users = telegram_cfg.get("allowed_user_ids", [])
122
+ if not allowed_chats and not allowed_users:
123
+ checks.append(
124
+ DoctorCheck(
125
+ name="Telegram access control",
126
+ passed=False,
127
+ message="No allowed_chat_ids or allowed_user_ids configured",
128
+ check_id="telegram.access_control",
129
+ fix="Configure allowed_chat_ids or allowed_user_ids in telegram_bot config.",
130
+ severity="warning",
131
+ )
132
+ )
133
+ else:
134
+ checks.append(
135
+ DoctorCheck(
136
+ name="Telegram access control",
137
+ passed=True,
138
+ message=f"Access control configured: {len(allowed_chats)} chats, {len(allowed_users)} users.",
139
+ check_id="telegram.access_control",
140
+ severity="info",
141
+ )
142
+ )
143
+
144
+ state_file_path = None
145
+ if repo_root:
146
+ state_file_path = repo_root / STATE_FILE_CHECK
147
+ if not state_file_path.exists():
148
+ checks.append(
149
+ DoctorCheck(
150
+ name="Telegram state file",
151
+ passed=False,
152
+ message=f"Telegram state file not found: {state_file_path}",
153
+ check_id="telegram.state_file",
154
+ severity="warning",
155
+ fix="Run a Telegram command to initialize the state file.",
156
+ )
157
+ )
158
+ else:
159
+ checks.append(
160
+ DoctorCheck(
161
+ name="Telegram state file",
162
+ passed=True,
163
+ message=f"State file exists: {state_file_path}",
164
+ check_id="telegram.state_file",
165
+ severity="info",
166
+ )
167
+ )
168
+
169
+ _check_stuck_turns(checks, state_file_path)
170
+
171
+ mode = telegram_cfg.get("mode", "polling")
172
+ if mode not in ("polling", "webhook"):
173
+ checks.append(
174
+ DoctorCheck(
175
+ name="Telegram mode",
176
+ passed=False,
177
+ message=f"Invalid Telegram mode: {mode}",
178
+ check_id="telegram.mode",
179
+ fix="Set mode to 'polling' or 'webhook' in telegram_bot config.",
180
+ )
181
+ )
182
+ else:
183
+ checks.append(
184
+ DoctorCheck(
185
+ name="Telegram mode",
186
+ passed=True,
187
+ message=f"Telegram mode: {mode}",
188
+ check_id="telegram.mode",
189
+ severity="info",
190
+ )
191
+ )
192
+ else:
193
+ checks.append(
194
+ DoctorCheck(
195
+ name="Telegram enabled",
196
+ passed=True,
197
+ message="Telegram integration is disabled.",
198
+ check_id="telegram.enabled",
199
+ severity="info",
200
+ fix="Set telegram_bot.enabled=true in config to enable.",
201
+ )
202
+ )
203
+
47
204
  return checks
205
+
206
+
207
+ def _check_stuck_turns(checks: list[DoctorCheck], state_file_path: Path) -> None:
208
+ """Check for stuck turns in Telegram state."""
209
+ try:
210
+ from ...core.sqlite_utils import connect_sqlite
211
+
212
+ conn = connect_sqlite(state_file_path)
213
+ cursor = conn.cursor()
214
+
215
+ cursor.execute("PRAGMA table_info(turns)")
216
+ columns = {row[1] for row in cursor.fetchall()}
217
+
218
+ if "status" not in columns:
219
+ return
220
+
221
+ threshold = datetime.now(timezone.utc) - timedelta(
222
+ minutes=STUCK_TURN_THRESHOLD_MINUTES
223
+ )
224
+ cursor.execute(
225
+ """
226
+ SELECT topic_key, status, updated_at
227
+ FROM turns
228
+ WHERE status = 'running' AND updated_at < ?
229
+ ORDER BY updated_at ASC
230
+ LIMIT 5
231
+ """,
232
+ (threshold.isoformat(),),
233
+ )
234
+
235
+ stuck_turns = cursor.fetchall()
236
+ conn.close()
237
+
238
+ if stuck_turns:
239
+ topics = ", ".join([turn[0] for turn in stuck_turns])
240
+ checks.append(
241
+ DoctorCheck(
242
+ name="Telegram stuck turns",
243
+ passed=False,
244
+ message=f"Found {len(stuck_turns)} stuck turns (inactive > {STUCK_TURN_THRESHOLD_MINUTES}m): {topics}",
245
+ check_id="telegram.stuck_turns",
246
+ fix="Review logs and consider restarting the bot or clearing stuck turns.",
247
+ )
248
+ )
249
+ else:
250
+ checks.append(
251
+ DoctorCheck(
252
+ name="Telegram stuck turns",
253
+ passed=True,
254
+ message="No stuck turns detected.",
255
+ check_id="telegram.stuck_turns",
256
+ severity="info",
257
+ )
258
+ )
259
+ except Exception as exc:
260
+ logger.debug("Failed to check for stuck turns: %s", exc)
261
+ checks.append(
262
+ DoctorCheck(
263
+ name="Telegram stuck turns",
264
+ passed=True,
265
+ message=f"Could not check for stuck turns: {exc}",
266
+ check_id="telegram.stuck_turns",
267
+ severity="warning",
268
+ )
269
+ )
@@ -9,6 +9,8 @@ from ..adapter import (
9
9
  CancelCallback,
10
10
  CompactCallback,
11
11
  EffortCallback,
12
+ FlowCallback,
13
+ FlowRunCallback,
12
14
  ModelCallback,
13
15
  PageCallback,
14
16
  QuestionCancelCallback,
@@ -91,3 +93,8 @@ async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> Non
91
93
  elif isinstance(parsed, PageCallback):
92
94
  if key:
93
95
  await handlers._handle_selection_page(key, parsed, callback)
96
+ elif isinstance(parsed, FlowCallback):
97
+ await handlers._handle_flow_callback(callback, parsed)
98
+ elif isinstance(parsed, FlowRunCallback):
99
+ if key:
100
+ await handlers._handle_flow_run_callback(key, callback, parsed)