codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,21 @@ from pathlib import Path
8
8
  from typing import Any, Iterable, Optional
9
9
 
10
10
  from .adapter import TelegramAllowlist
11
+ from .constants import (
12
+ CACHE_CLEANUP_INTERVAL_SECONDS,
13
+ COALESCE_BUFFER_TTL_SECONDS,
14
+ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
15
+ MEDIA_BATCH_BUFFER_TTL_SECONDS,
16
+ MODEL_PENDING_TTL_SECONDS,
17
+ OVERSIZE_WARNING_TTL_SECONDS,
18
+ PENDING_APPROVAL_TTL_SECONDS,
19
+ PENDING_QUESTION_TTL_SECONDS,
20
+ PROGRESS_STREAM_TTL_SECONDS,
21
+ REASONING_BUFFER_TTL_SECONDS,
22
+ SELECTION_STATE_TTL_SECONDS,
23
+ TURN_PREVIEW_TTL_SECONDS,
24
+ UPDATE_ID_PERSIST_INTERVAL_SECONDS,
25
+ )
11
26
  from .state import APPROVAL_MODE_YOLO, normalize_approval_mode
12
27
 
13
28
  DEFAULT_ALLOWED_UPDATES = ("message", "edited_message", "callback_query")
@@ -17,6 +32,8 @@ DEFAULT_SAFE_APPROVAL_POLICY = "on-request"
17
32
  DEFAULT_YOLO_APPROVAL_POLICY = "never"
18
33
  DEFAULT_YOLO_SANDBOX_POLICY = "dangerFullAccess"
19
34
  DEFAULT_PARSE_MODE = "HTML"
35
+ DEFAULT_TRIGGER_MODE = "all"
36
+ TRIGGER_MODE_OPTIONS = {"all", "mentions"}
20
37
  DEFAULT_STATE_FILE = ".codex-autorunner/telegram_state.sqlite3"
21
38
  DEFAULT_APP_SERVER_COMMAND = ["codex", "app-server"]
22
39
  DEFAULT_APP_SERVER_MAX_HANDLES = 20
@@ -45,6 +62,7 @@ DEFAULT_MESSAGE_OVERFLOW = "document"
45
62
  MESSAGE_OVERFLOW_OPTIONS = {"document", "split", "trim"}
46
63
  DEFAULT_METRICS_MODE = "separate"
47
64
  METRICS_MODE_OPTIONS = {"separate", "append_to_response", "append_to_progress"}
65
+ DEFAULT_PAUSE_DISPATCH_MAX_FILE_BYTES = 50 * 1024 * 1024
48
66
 
49
67
  PARSE_MODE_ALIASES = {
50
68
  "html": "HTML",
@@ -107,6 +125,22 @@ class TelegramBotShellConfig:
107
125
  max_output_chars: int
108
126
 
109
127
 
128
+ @dataclass(frozen=True)
129
+ class TelegramBotCacheConfig:
130
+ cleanup_interval_seconds: float
131
+ coalesce_buffer_ttl_seconds: float
132
+ media_batch_buffer_ttl_seconds: float
133
+ model_pending_ttl_seconds: float
134
+ pending_approval_ttl_seconds: float
135
+ pending_question_ttl_seconds: float
136
+ reasoning_buffer_ttl_seconds: float
137
+ selection_state_ttl_seconds: float
138
+ turn_preview_ttl_seconds: float
139
+ progress_stream_ttl_seconds: float
140
+ oversize_warning_ttl_seconds: float
141
+ update_id_persist_interval_seconds: float
142
+
143
+
110
144
  @dataclass(frozen=True)
111
145
  class TelegramBotCommandScope:
112
146
  scope: dict[str, Any]
@@ -127,6 +161,14 @@ class TelegramBotProgressStreamConfig:
127
161
  min_edit_interval_seconds: float
128
162
 
129
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
+
130
172
  @dataclass(frozen=True)
131
173
  class TelegramMediaCandidate:
132
174
  kind: str
@@ -150,12 +192,15 @@ class TelegramBotConfig:
150
192
  allowed_chat_ids: set[int]
151
193
  allowed_user_ids: set[int]
152
194
  require_topics: bool
195
+ trigger_mode: str
153
196
  defaults: TelegramBotDefaults
154
197
  concurrency: TelegramBotConcurrency
155
198
  media: TelegramBotMediaConfig
156
199
  shell: TelegramBotShellConfig
200
+ cache: TelegramBotCacheConfig
157
201
  progress_stream: TelegramBotProgressStreamConfig
158
202
  command_registration: TelegramBotCommandRegistration
203
+ opencode_command: list[str]
159
204
  state_file: Path
160
205
  app_server_command_env: str
161
206
  app_server_command: list[str]
@@ -164,6 +209,7 @@ class TelegramBotConfig:
164
209
  app_server_start_timeout_seconds: float
165
210
  app_server_start_max_attempts: Optional[int]
166
211
  app_server_turn_timeout_seconds: Optional[float]
212
+ agent_turn_timeout_seconds: dict[str, Optional[float]]
167
213
  poll_timeout_seconds: int
168
214
  poll_request_timeout_seconds: Optional[float]
169
215
  poll_allowed_updates: list[str]
@@ -171,6 +217,9 @@ class TelegramBotConfig:
171
217
  metrics_mode: str
172
218
  coalesce_window_seconds: float
173
219
  agent_binaries: dict[str, str]
220
+ ticket_flow_auto_resume: bool
221
+ pause_dispatch_notifications: PauseDispatchNotifications
222
+ default_notification_chat_id: Optional[int]
174
223
 
175
224
  @classmethod
176
225
  def from_raw(
@@ -183,6 +232,16 @@ class TelegramBotConfig:
183
232
  ) -> "TelegramBotConfig":
184
233
  env = env or dict(os.environ)
185
234
  cfg: dict[str, Any] = raw if isinstance(raw, dict) else {}
235
+
236
+ def _positive_float(value: Any, default: float) -> float:
237
+ try:
238
+ parsed = float(value)
239
+ except (TypeError, ValueError):
240
+ return default
241
+ if parsed <= 0:
242
+ return default
243
+ return parsed
244
+
186
245
  enabled = bool(cfg.get("enabled", False))
187
246
  mode = str(cfg.get("mode", "polling"))
188
247
  bot_token_env = str(cfg.get("bot_token_env", "CAR_TELEGRAM_BOT_TOKEN"))
@@ -204,6 +263,10 @@ class TelegramBotConfig:
204
263
 
205
264
  require_topics = bool(cfg.get("require_topics", False))
206
265
 
266
+ trigger_mode = (
267
+ str(cfg.get("trigger_mode", DEFAULT_TRIGGER_MODE)).strip().lower()
268
+ )
269
+
207
270
  defaults_raw_value = cfg.get("defaults")
208
271
  defaults_raw: dict[str, Any] = (
209
272
  defaults_raw_value if isinstance(defaults_raw_value, dict) else {}
@@ -313,6 +376,81 @@ class TelegramBotConfig:
313
376
  timeout_ms=shell_timeout_ms,
314
377
  max_output_chars=shell_max_output_chars,
315
378
  )
379
+ cache_raw_value = cfg.get("cache")
380
+ cache_raw: dict[str, Any] = (
381
+ cache_raw_value if isinstance(cache_raw_value, dict) else {}
382
+ )
383
+ cache = TelegramBotCacheConfig(
384
+ cleanup_interval_seconds=_positive_float(
385
+ cache_raw.get(
386
+ "cleanup_interval_seconds", CACHE_CLEANUP_INTERVAL_SECONDS
387
+ ),
388
+ CACHE_CLEANUP_INTERVAL_SECONDS,
389
+ ),
390
+ coalesce_buffer_ttl_seconds=_positive_float(
391
+ cache_raw.get(
392
+ "coalesce_buffer_ttl_seconds", COALESCE_BUFFER_TTL_SECONDS
393
+ ),
394
+ COALESCE_BUFFER_TTL_SECONDS,
395
+ ),
396
+ media_batch_buffer_ttl_seconds=_positive_float(
397
+ cache_raw.get(
398
+ "media_batch_buffer_ttl_seconds", MEDIA_BATCH_BUFFER_TTL_SECONDS
399
+ ),
400
+ MEDIA_BATCH_BUFFER_TTL_SECONDS,
401
+ ),
402
+ model_pending_ttl_seconds=_positive_float(
403
+ cache_raw.get("model_pending_ttl_seconds", MODEL_PENDING_TTL_SECONDS),
404
+ MODEL_PENDING_TTL_SECONDS,
405
+ ),
406
+ pending_approval_ttl_seconds=_positive_float(
407
+ cache_raw.get(
408
+ "pending_approval_ttl_seconds", PENDING_APPROVAL_TTL_SECONDS
409
+ ),
410
+ PENDING_APPROVAL_TTL_SECONDS,
411
+ ),
412
+ pending_question_ttl_seconds=_positive_float(
413
+ cache_raw.get(
414
+ "pending_question_ttl_seconds", PENDING_QUESTION_TTL_SECONDS
415
+ ),
416
+ PENDING_QUESTION_TTL_SECONDS,
417
+ ),
418
+ reasoning_buffer_ttl_seconds=_positive_float(
419
+ cache_raw.get(
420
+ "reasoning_buffer_ttl_seconds", REASONING_BUFFER_TTL_SECONDS
421
+ ),
422
+ REASONING_BUFFER_TTL_SECONDS,
423
+ ),
424
+ selection_state_ttl_seconds=_positive_float(
425
+ cache_raw.get(
426
+ "selection_state_ttl_seconds", SELECTION_STATE_TTL_SECONDS
427
+ ),
428
+ SELECTION_STATE_TTL_SECONDS,
429
+ ),
430
+ turn_preview_ttl_seconds=_positive_float(
431
+ cache_raw.get("turn_preview_ttl_seconds", TURN_PREVIEW_TTL_SECONDS),
432
+ TURN_PREVIEW_TTL_SECONDS,
433
+ ),
434
+ progress_stream_ttl_seconds=_positive_float(
435
+ cache_raw.get(
436
+ "progress_stream_ttl_seconds", PROGRESS_STREAM_TTL_SECONDS
437
+ ),
438
+ PROGRESS_STREAM_TTL_SECONDS,
439
+ ),
440
+ oversize_warning_ttl_seconds=_positive_float(
441
+ cache_raw.get(
442
+ "oversize_warning_ttl_seconds", OVERSIZE_WARNING_TTL_SECONDS
443
+ ),
444
+ OVERSIZE_WARNING_TTL_SECONDS,
445
+ ),
446
+ update_id_persist_interval_seconds=_positive_float(
447
+ cache_raw.get(
448
+ "update_id_persist_interval_seconds",
449
+ UPDATE_ID_PERSIST_INTERVAL_SECONDS,
450
+ ),
451
+ UPDATE_ID_PERSIST_INTERVAL_SECONDS,
452
+ ),
453
+ )
316
454
 
317
455
  progress_raw_value = cfg.get("progress_stream")
318
456
  progress_raw: dict[str, Any] = (
@@ -374,6 +512,44 @@ class TelegramBotConfig:
374
512
  if coalesce_window_seconds <= 0:
375
513
  coalesce_window_seconds = DEFAULT_COALESCE_WINDOW_SECONDS
376
514
 
515
+ ticket_flow_raw = (
516
+ cfg.get("ticket_flow") if isinstance(cfg.get("ticket_flow"), dict) else {}
517
+ )
518
+ ticket_flow_auto_resume = bool(ticket_flow_raw.get("auto_resume", False))
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
+
377
553
  agent_binaries = dict(agent_binaries or {})
378
554
  command_reg_raw_value = cfg.get("command_registration")
379
555
  command_reg_raw: dict[str, Any] = (
@@ -385,9 +561,21 @@ class TelegramBotConfig:
385
561
  enabled=command_reg_enabled, scopes=scopes
386
562
  )
387
563
 
564
+ opencode_command = []
565
+ opencode_env_command = env.get("CAR_OPENCODE_COMMAND")
566
+ if opencode_env_command:
567
+ opencode_command = _parse_command(opencode_env_command)
568
+ if not opencode_command:
569
+ opencode_command = _parse_command(cfg.get("opencode_command"))
570
+
388
571
  state_file = Path(cfg.get("state_file", DEFAULT_STATE_FILE))
389
572
  if not state_file.is_absolute():
390
573
  state_file = (root / state_file).resolve()
574
+ if state_file.suffix == ".json":
575
+ raise TelegramBotConfigError(
576
+ "telegram_bot.state_file must point to a SQLite database "
577
+ "(.sqlite3). Update your config to .codex-autorunner/telegram_state.sqlite3"
578
+ )
391
579
 
392
580
  app_server_command_env = str(
393
581
  cfg.get("app_server_command_env", "CAR_TELEGRAM_APP_SERVER_COMMAND")
@@ -440,6 +628,30 @@ class TelegramBotConfig:
440
628
  if app_server_turn_timeout_seconds <= 0:
441
629
  app_server_turn_timeout_seconds = None
442
630
 
631
+ agent_timeouts_raw = cfg.get("agent_timeouts")
632
+ has_explicit_codex_timeout = False
633
+ agent_timeouts: dict[str, Optional[float]] = dict(
634
+ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS
635
+ )
636
+ if isinstance(agent_timeouts_raw, dict):
637
+ for key, value in agent_timeouts_raw.items():
638
+ if str(key) == "codex":
639
+ has_explicit_codex_timeout = True
640
+ if value is None:
641
+ agent_timeouts[str(key)] = None
642
+ continue
643
+ try:
644
+ timeout_value = float(value)
645
+ except (TypeError, ValueError):
646
+ continue
647
+ if timeout_value <= 0:
648
+ agent_timeouts[str(key)] = None
649
+ else:
650
+ agent_timeouts[str(key)] = timeout_value
651
+
652
+ if not has_explicit_codex_timeout:
653
+ agent_timeouts["codex"] = app_server_turn_timeout_seconds
654
+
443
655
  polling_raw_value = cfg.get("polling")
444
656
  polling_raw: dict[str, Any] = (
445
657
  polling_raw_value if isinstance(polling_raw_value, dict) else {}
@@ -472,12 +684,15 @@ class TelegramBotConfig:
472
684
  allowed_chat_ids=allowed_chat_ids,
473
685
  allowed_user_ids=allowed_user_ids,
474
686
  require_topics=require_topics,
687
+ trigger_mode=trigger_mode,
475
688
  defaults=defaults,
476
689
  concurrency=concurrency,
477
690
  media=media,
478
691
  shell=shell,
692
+ cache=cache,
479
693
  progress_stream=progress_stream,
480
694
  command_registration=command_registration,
695
+ opencode_command=opencode_command,
481
696
  state_file=state_file,
482
697
  app_server_command_env=app_server_command_env,
483
698
  app_server_command=app_server_command,
@@ -486,6 +701,7 @@ class TelegramBotConfig:
486
701
  app_server_start_timeout_seconds=app_server_start_timeout_seconds,
487
702
  app_server_start_max_attempts=app_server_start_max_attempts,
488
703
  app_server_turn_timeout_seconds=app_server_turn_timeout_seconds,
704
+ agent_turn_timeout_seconds=agent_timeouts,
489
705
  poll_timeout_seconds=poll_timeout_seconds,
490
706
  poll_request_timeout_seconds=poll_request_timeout_seconds,
491
707
  poll_allowed_updates=poll_allowed_updates,
@@ -493,6 +709,9 @@ class TelegramBotConfig:
493
709
  metrics_mode=metrics_mode,
494
710
  coalesce_window_seconds=coalesce_window_seconds,
495
711
  agent_binaries=agent_binaries,
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,
496
715
  )
497
716
 
498
717
  def validate(self) -> None:
@@ -516,6 +735,8 @@ class TelegramBotConfig:
516
735
  issues.append(
517
736
  "poll_request_timeout_seconds must be greater than poll_timeout_seconds"
518
737
  )
738
+ if self.trigger_mode not in TRIGGER_MODE_OPTIONS:
739
+ issues.append(f"trigger_mode must be one of {sorted(TRIGGER_MODE_OPTIONS)}")
519
740
  if issues:
520
741
  raise TelegramBotConfigError("; ".join(issues))
521
742
 
@@ -18,8 +18,12 @@ RESUME_REFRESH_LIMIT = 10
18
18
  TOKEN_USAGE_CACHE_LIMIT = 256
19
19
  TOKEN_USAGE_TURN_CACHE_LIMIT = 512
20
20
  DEFAULT_INTERRUPT_TIMEOUT_SECONDS = 30.0
21
- OPENCODE_TURN_TIMEOUT_SECONDS = 300.0
22
- DEFAULT_WORKSPACE_STATE_ROOT = "~/.codex-autorunner/workspaces"
21
+
22
+
23
+ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS = {
24
+ "codex": 28800.0,
25
+ "opencode": 28800.0,
26
+ }
23
27
  DEFAULT_AGENT = "codex"
24
28
  APP_SERVER_START_BACKOFF_INITIAL_SECONDS = 1.0
25
29
  APP_SERVER_START_BACKOFF_MAX_SECONDS = 30.0
@@ -61,6 +65,7 @@ UPDATE_PICKER_PROMPT = "Select update target (buttons below)."
61
65
  REVIEW_COMMIT_PICKER_PROMPT = (
62
66
  "Select a commit to review (buttons below or reply with number)."
63
67
  )
68
+ FLOW_RUNS_PICKER_PROMPT = "Select a ticket flow run (buttons below)."
64
69
  REVIEW_COMMIT_BUTTON_LABEL_LIMIT = 80
65
70
  UPDATE_TARGET_OPTIONS = (
66
71
  ("both", "Both (web + Telegram)"),
@@ -100,6 +105,16 @@ TURN_PROGRESS_MAX_LEN = 160
100
105
  TURN_PROGRESS_MIN_EDIT_INTERVAL_SECONDS = 1.0
101
106
  TURN_PROGRESS_TTL_SECONDS = 900.0
102
107
  PROGRESS_HEARTBEAT_INTERVAL_SECONDS = 5.0
108
+ COMPACT_MAX_ACTIONS = 10
109
+ COMPACT_MAX_TEXT_LENGTH = 80
110
+ STATUS_ICONS = {
111
+ "done": "✓",
112
+ "fail": "✗",
113
+ "warn": "⚠",
114
+ "running": "▸",
115
+ "update": "↻",
116
+ "thinking": "🧠",
117
+ }
103
118
  COMMAND_DISABLED_TEMPLATE = "'/{name}' is disabled while a task is in progress."
104
119
  MAX_MENTION_BYTES = 200_000
105
120
  VALID_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"}
@@ -72,14 +72,26 @@ def _log_denied(handlers: Any, update: TelegramUpdate) -> None:
72
72
  chat_id = None
73
73
  user_id = None
74
74
  thread_id = None
75
+ message_id = None
76
+ update_id = None
77
+ conversation_id = None
75
78
  if update.message:
76
79
  chat_id = update.message.chat_id
77
80
  user_id = update.message.from_user_id
78
81
  thread_id = update.message.thread_id
82
+ message_id = update.message.message_id
83
+ update_id = update.message.update_id
79
84
  elif update.callback:
80
85
  chat_id = update.callback.chat_id
81
86
  user_id = update.callback.from_user_id
82
87
  thread_id = update.callback.thread_id
88
+ message_id = update.callback.message_id
89
+ update_id = update.callback.update_id
90
+ if chat_id is not None:
91
+ try:
92
+ conversation_id = topic_key(chat_id, thread_id)
93
+ except Exception:
94
+ conversation_id = None
83
95
  log_event(
84
96
  handlers._logger,
85
97
  logging.INFO,
@@ -87,6 +99,9 @@ def _log_denied(handlers: Any, update: TelegramUpdate) -> None:
87
99
  chat_id=chat_id,
88
100
  user_id=user_id,
89
101
  thread_id=thread_id,
102
+ message_id=message_id,
103
+ update_id=update_id,
104
+ conversation_id=conversation_id,
90
105
  )
91
106
 
92
107
 
@@ -172,6 +187,7 @@ async def dispatch_update(handlers: Any, update: TelegramUpdate) -> None:
172
187
  has_message=bool(update.message),
173
188
  has_callback=bool(update.callback),
174
189
  update_received_at=now_iso(),
190
+ conversation_id=conversation_id,
175
191
  )
176
192
  if (
177
193
  update.update_id is not None
@@ -188,6 +204,7 @@ async def dispatch_update(handlers: Any, update: TelegramUpdate) -> None:
188
204
  chat_id=context.chat_id,
189
205
  thread_id=context.thread_id,
190
206
  message_id=context.message_id,
207
+ conversation_id=conversation_id,
191
208
  )
192
209
  return
193
210
  if not allowlist_allows(update, handlers._allowlist):
@@ -0,0 +1,47 @@
1
+ """Telegram integration doctor checks."""
2
+
3
+ from typing import Any, Dict, Union
4
+
5
+ from ...core.config import HubConfig, RepoConfig
6
+ from ...core.engine import DoctorCheck
7
+ from ...core.optional_dependencies import missing_optional_dependencies
8
+
9
+
10
+ def telegram_doctor_checks(
11
+ config: Union[HubConfig, RepoConfig, Dict[str, Any]],
12
+ ) -> list[DoctorCheck]:
13
+ """Run Telegram-specific doctor checks.
14
+
15
+ Returns a list of DoctorCheck objects for Telegram integration.
16
+ Works with HubConfig, RepoConfig, or raw dict.
17
+ """
18
+ checks: list[DoctorCheck] = []
19
+ telegram_cfg = None
20
+
21
+ if isinstance(config, dict):
22
+ telegram_cfg = config.get("telegram_bot")
23
+ elif isinstance(config.raw, dict):
24
+ telegram_cfg = config.raw.get("telegram_bot")
25
+
26
+ if isinstance(telegram_cfg, dict) and telegram_cfg.get("enabled") is True:
27
+ missing_telegram = missing_optional_dependencies((("httpx", "httpx"),))
28
+ if missing_telegram:
29
+ deps_list = ", ".join(missing_telegram)
30
+ checks.append(
31
+ DoctorCheck(
32
+ check_id="telegram.dependencies",
33
+ status="error",
34
+ message=f"Telegram is enabled but missing optional deps: {deps_list}",
35
+ fix="Install with `pip install codex-autorunner[telegram]`.",
36
+ )
37
+ )
38
+ else:
39
+ checks.append(
40
+ DoctorCheck(
41
+ check_id="telegram.dependencies",
42
+ status="ok",
43
+ message="Telegram dependencies are installed.",
44
+ )
45
+ )
46
+
47
+ return checks
@@ -9,9 +9,10 @@ from ..adapter import (
9
9
  CancelCallback,
10
10
  CompactCallback,
11
11
  EffortCallback,
12
+ FlowCallback,
13
+ FlowRunCallback,
12
14
  ModelCallback,
13
15
  PageCallback,
14
- PrFlowStartCallback,
15
16
  QuestionCancelCallback,
16
17
  QuestionCustomCallback,
17
18
  QuestionDoneCallback,
@@ -80,9 +81,6 @@ async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> Non
80
81
  elif isinstance(parsed, ReviewCommitCallback):
81
82
  if key:
82
83
  await handlers._handle_review_commit_callback(key, callback, parsed)
83
- elif isinstance(parsed, PrFlowStartCallback):
84
- if key:
85
- await handlers._handle_pr_flow_start_callback(key, callback, parsed)
86
84
  elif isinstance(parsed, CancelCallback):
87
85
  if key:
88
86
  if parsed.kind == "interrupt":
@@ -95,3 +93,8 @@ async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> Non
95
93
  elif isinstance(parsed, PageCallback):
96
94
  if key:
97
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)
@@ -7,6 +7,7 @@ from ..commands_spec import CommandSpec, build_command_specs
7
7
  from .approvals import ApprovalsCommands
8
8
  from .execution import ExecutionCommands
9
9
  from .files import FilesCommands
10
+ from .flows import FlowCommands
10
11
  from .formatting import FormattingHelpers
11
12
  from .github import GitHubCommands
12
13
  from .shared import SharedHelpers
@@ -19,6 +20,7 @@ __all__ = [
19
20
  "GitHubCommands",
20
21
  "FilesCommands",
21
22
  "VoiceCommands",
23
+ "FlowCommands",
22
24
  "ExecutionCommands",
23
25
  "ApprovalsCommands",
24
26
  "FormattingHelpers",