codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ __all__ = [
8
+ "BaseTelegramSchema",
9
+ "TelegramPhotoSizeSchema",
10
+ "TelegramDocumentSchema",
11
+ "TelegramAudioSchema",
12
+ "TelegramVoiceSchema",
13
+ "TelegramMessageEntitySchema",
14
+ "TelegramMessageSchema",
15
+ "TelegramCallbackQuerySchema",
16
+ "TelegramUpdateSchema",
17
+ "parse_update_payload",
18
+ "parse_message_payload",
19
+ "parse_callback_query_payload",
20
+ ]
21
+
22
+
23
+ class BaseTelegramSchema(BaseModel):
24
+ model_config = {"extra": "ignore", "validate_assignment": False}
25
+
26
+
27
+ class TelegramPhotoSizeSchema(BaseTelegramSchema):
28
+ file_id: str
29
+ file_unique_id: Optional[str] = None
30
+ width: int
31
+ height: int
32
+ file_size: Optional[int] = None
33
+
34
+
35
+ class TelegramDocumentSchema(BaseTelegramSchema):
36
+ file_id: str
37
+ file_unique_id: Optional[str] = None
38
+ file_name: Optional[str] = None
39
+ mime_type: Optional[str] = None
40
+ file_size: Optional[int] = None
41
+
42
+
43
+ class TelegramAudioSchema(BaseTelegramSchema):
44
+ file_id: str
45
+ file_unique_id: Optional[str] = None
46
+ duration: Optional[int] = None
47
+ file_name: Optional[str] = None
48
+ mime_type: Optional[str] = None
49
+ file_size: Optional[int] = None
50
+
51
+
52
+ class TelegramVoiceSchema(BaseTelegramSchema):
53
+ file_id: str
54
+ file_unique_id: Optional[str] = None
55
+ duration: Optional[int] = None
56
+ mime_type: Optional[str] = None
57
+ file_size: Optional[int] = None
58
+
59
+
60
+ class TelegramMessageEntitySchema(BaseTelegramSchema):
61
+ type: str
62
+ offset: int
63
+ length: int
64
+
65
+
66
+ class TelegramMessageSchema(BaseTelegramSchema):
67
+ message_id: int
68
+ chat: dict[str, Any] = Field(default_factory=dict)
69
+ from_user: Optional[dict[str, Any]] = Field(default=None, alias="from")
70
+ message_thread_id: Optional[int] = None
71
+ date: Optional[int] = None
72
+ text: Optional[str] = None
73
+ caption: Optional[str] = None
74
+ entities: Optional[list[dict[str, Any]]] = None
75
+ caption_entities: Optional[list[dict[str, Any]]] = None
76
+ photo: Optional[list[dict[str, Any]]] = None
77
+ document: Optional[dict[str, Any]] = None
78
+ audio: Optional[dict[str, Any]] = None
79
+ voice: Optional[dict[str, Any]] = None
80
+ media_group_id: Optional[str] = None
81
+ is_topic_message: bool = False
82
+ reply_to_message: Optional[dict[str, Any]] = None
83
+
84
+
85
+ class TelegramCallbackQuerySchema(BaseTelegramSchema):
86
+ id: str
87
+ from_user: dict[str, Any] = Field(alias="from")
88
+ data: Optional[str] = None
89
+ message: Optional[dict[str, Any]] = None
90
+
91
+
92
+ class TelegramUpdateSchema(BaseTelegramSchema):
93
+ update_id: int
94
+ message: Optional[dict[str, Any]] = None
95
+ edited_message: Optional[dict[str, Any]] = Field(
96
+ default=None, alias="edited_message"
97
+ )
98
+ callback_query: Optional[dict[str, Any]] = Field(
99
+ default=None, alias="callback_query"
100
+ )
101
+
102
+
103
+ def parse_update_payload(payload: dict[str, Any]) -> TelegramUpdateSchema:
104
+ return TelegramUpdateSchema.model_validate(payload)
105
+
106
+
107
+ def parse_message_payload(payload: dict[str, Any]) -> Optional[TelegramMessageSchema]:
108
+ try:
109
+ return TelegramMessageSchema.model_validate(payload)
110
+ except Exception:
111
+ return None
112
+
113
+
114
+ def parse_callback_query_payload(
115
+ payload: dict[str, Any],
116
+ ) -> Optional[TelegramCallbackQuerySchema]:
117
+ try:
118
+ return TelegramCallbackQuerySchema.model_validate(payload)
119
+ except Exception:
120
+ return None
@@ -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
@@ -107,6 +124,22 @@ class TelegramBotShellConfig:
107
124
  max_output_chars: int
108
125
 
109
126
 
127
+ @dataclass(frozen=True)
128
+ class TelegramBotCacheConfig:
129
+ cleanup_interval_seconds: float
130
+ coalesce_buffer_ttl_seconds: float
131
+ media_batch_buffer_ttl_seconds: float
132
+ model_pending_ttl_seconds: float
133
+ pending_approval_ttl_seconds: float
134
+ pending_question_ttl_seconds: float
135
+ reasoning_buffer_ttl_seconds: float
136
+ selection_state_ttl_seconds: float
137
+ turn_preview_ttl_seconds: float
138
+ progress_stream_ttl_seconds: float
139
+ oversize_warning_ttl_seconds: float
140
+ update_id_persist_interval_seconds: float
141
+
142
+
110
143
  @dataclass(frozen=True)
111
144
  class TelegramBotCommandScope:
112
145
  scope: dict[str, Any]
@@ -150,12 +183,15 @@ class TelegramBotConfig:
150
183
  allowed_chat_ids: set[int]
151
184
  allowed_user_ids: set[int]
152
185
  require_topics: bool
186
+ trigger_mode: str
153
187
  defaults: TelegramBotDefaults
154
188
  concurrency: TelegramBotConcurrency
155
189
  media: TelegramBotMediaConfig
156
190
  shell: TelegramBotShellConfig
191
+ cache: TelegramBotCacheConfig
157
192
  progress_stream: TelegramBotProgressStreamConfig
158
193
  command_registration: TelegramBotCommandRegistration
194
+ opencode_command: list[str]
159
195
  state_file: Path
160
196
  app_server_command_env: str
161
197
  app_server_command: list[str]
@@ -164,6 +200,7 @@ class TelegramBotConfig:
164
200
  app_server_start_timeout_seconds: float
165
201
  app_server_start_max_attempts: Optional[int]
166
202
  app_server_turn_timeout_seconds: Optional[float]
203
+ agent_turn_timeout_seconds: dict[str, Optional[float]]
167
204
  poll_timeout_seconds: int
168
205
  poll_request_timeout_seconds: Optional[float]
169
206
  poll_allowed_updates: list[str]
@@ -171,6 +208,7 @@ class TelegramBotConfig:
171
208
  metrics_mode: str
172
209
  coalesce_window_seconds: float
173
210
  agent_binaries: dict[str, str]
211
+ ticket_flow_auto_resume: bool
174
212
 
175
213
  @classmethod
176
214
  def from_raw(
@@ -183,6 +221,16 @@ class TelegramBotConfig:
183
221
  ) -> "TelegramBotConfig":
184
222
  env = env or dict(os.environ)
185
223
  cfg: dict[str, Any] = raw if isinstance(raw, dict) else {}
224
+
225
+ def _positive_float(value: Any, default: float) -> float:
226
+ try:
227
+ parsed = float(value)
228
+ except (TypeError, ValueError):
229
+ return default
230
+ if parsed <= 0:
231
+ return default
232
+ return parsed
233
+
186
234
  enabled = bool(cfg.get("enabled", False))
187
235
  mode = str(cfg.get("mode", "polling"))
188
236
  bot_token_env = str(cfg.get("bot_token_env", "CAR_TELEGRAM_BOT_TOKEN"))
@@ -204,6 +252,10 @@ class TelegramBotConfig:
204
252
 
205
253
  require_topics = bool(cfg.get("require_topics", False))
206
254
 
255
+ trigger_mode = (
256
+ str(cfg.get("trigger_mode", DEFAULT_TRIGGER_MODE)).strip().lower()
257
+ )
258
+
207
259
  defaults_raw_value = cfg.get("defaults")
208
260
  defaults_raw: dict[str, Any] = (
209
261
  defaults_raw_value if isinstance(defaults_raw_value, dict) else {}
@@ -313,6 +365,81 @@ class TelegramBotConfig:
313
365
  timeout_ms=shell_timeout_ms,
314
366
  max_output_chars=shell_max_output_chars,
315
367
  )
368
+ cache_raw_value = cfg.get("cache")
369
+ cache_raw: dict[str, Any] = (
370
+ cache_raw_value if isinstance(cache_raw_value, dict) else {}
371
+ )
372
+ cache = TelegramBotCacheConfig(
373
+ cleanup_interval_seconds=_positive_float(
374
+ cache_raw.get(
375
+ "cleanup_interval_seconds", CACHE_CLEANUP_INTERVAL_SECONDS
376
+ ),
377
+ CACHE_CLEANUP_INTERVAL_SECONDS,
378
+ ),
379
+ coalesce_buffer_ttl_seconds=_positive_float(
380
+ cache_raw.get(
381
+ "coalesce_buffer_ttl_seconds", COALESCE_BUFFER_TTL_SECONDS
382
+ ),
383
+ COALESCE_BUFFER_TTL_SECONDS,
384
+ ),
385
+ media_batch_buffer_ttl_seconds=_positive_float(
386
+ cache_raw.get(
387
+ "media_batch_buffer_ttl_seconds", MEDIA_BATCH_BUFFER_TTL_SECONDS
388
+ ),
389
+ MEDIA_BATCH_BUFFER_TTL_SECONDS,
390
+ ),
391
+ model_pending_ttl_seconds=_positive_float(
392
+ cache_raw.get("model_pending_ttl_seconds", MODEL_PENDING_TTL_SECONDS),
393
+ MODEL_PENDING_TTL_SECONDS,
394
+ ),
395
+ pending_approval_ttl_seconds=_positive_float(
396
+ cache_raw.get(
397
+ "pending_approval_ttl_seconds", PENDING_APPROVAL_TTL_SECONDS
398
+ ),
399
+ PENDING_APPROVAL_TTL_SECONDS,
400
+ ),
401
+ pending_question_ttl_seconds=_positive_float(
402
+ cache_raw.get(
403
+ "pending_question_ttl_seconds", PENDING_QUESTION_TTL_SECONDS
404
+ ),
405
+ PENDING_QUESTION_TTL_SECONDS,
406
+ ),
407
+ reasoning_buffer_ttl_seconds=_positive_float(
408
+ cache_raw.get(
409
+ "reasoning_buffer_ttl_seconds", REASONING_BUFFER_TTL_SECONDS
410
+ ),
411
+ REASONING_BUFFER_TTL_SECONDS,
412
+ ),
413
+ selection_state_ttl_seconds=_positive_float(
414
+ cache_raw.get(
415
+ "selection_state_ttl_seconds", SELECTION_STATE_TTL_SECONDS
416
+ ),
417
+ SELECTION_STATE_TTL_SECONDS,
418
+ ),
419
+ turn_preview_ttl_seconds=_positive_float(
420
+ cache_raw.get("turn_preview_ttl_seconds", TURN_PREVIEW_TTL_SECONDS),
421
+ TURN_PREVIEW_TTL_SECONDS,
422
+ ),
423
+ progress_stream_ttl_seconds=_positive_float(
424
+ cache_raw.get(
425
+ "progress_stream_ttl_seconds", PROGRESS_STREAM_TTL_SECONDS
426
+ ),
427
+ PROGRESS_STREAM_TTL_SECONDS,
428
+ ),
429
+ oversize_warning_ttl_seconds=_positive_float(
430
+ cache_raw.get(
431
+ "oversize_warning_ttl_seconds", OVERSIZE_WARNING_TTL_SECONDS
432
+ ),
433
+ OVERSIZE_WARNING_TTL_SECONDS,
434
+ ),
435
+ update_id_persist_interval_seconds=_positive_float(
436
+ cache_raw.get(
437
+ "update_id_persist_interval_seconds",
438
+ UPDATE_ID_PERSIST_INTERVAL_SECONDS,
439
+ ),
440
+ UPDATE_ID_PERSIST_INTERVAL_SECONDS,
441
+ ),
442
+ )
316
443
 
317
444
  progress_raw_value = cfg.get("progress_stream")
318
445
  progress_raw: dict[str, Any] = (
@@ -374,6 +501,11 @@ class TelegramBotConfig:
374
501
  if coalesce_window_seconds <= 0:
375
502
  coalesce_window_seconds = DEFAULT_COALESCE_WINDOW_SECONDS
376
503
 
504
+ ticket_flow_raw = (
505
+ cfg.get("ticket_flow") if isinstance(cfg.get("ticket_flow"), dict) else {}
506
+ )
507
+ ticket_flow_auto_resume = bool(ticket_flow_raw.get("auto_resume", False))
508
+
377
509
  agent_binaries = dict(agent_binaries or {})
378
510
  command_reg_raw_value = cfg.get("command_registration")
379
511
  command_reg_raw: dict[str, Any] = (
@@ -385,9 +517,21 @@ class TelegramBotConfig:
385
517
  enabled=command_reg_enabled, scopes=scopes
386
518
  )
387
519
 
520
+ opencode_command = []
521
+ opencode_env_command = env.get("CAR_OPENCODE_COMMAND")
522
+ if opencode_env_command:
523
+ opencode_command = _parse_command(opencode_env_command)
524
+ if not opencode_command:
525
+ opencode_command = _parse_command(cfg.get("opencode_command"))
526
+
388
527
  state_file = Path(cfg.get("state_file", DEFAULT_STATE_FILE))
389
528
  if not state_file.is_absolute():
390
529
  state_file = (root / state_file).resolve()
530
+ if state_file.suffix == ".json":
531
+ raise TelegramBotConfigError(
532
+ "telegram_bot.state_file must point to a SQLite database "
533
+ "(.sqlite3). Update your config to .codex-autorunner/telegram_state.sqlite3"
534
+ )
391
535
 
392
536
  app_server_command_env = str(
393
537
  cfg.get("app_server_command_env", "CAR_TELEGRAM_APP_SERVER_COMMAND")
@@ -440,6 +584,30 @@ class TelegramBotConfig:
440
584
  if app_server_turn_timeout_seconds <= 0:
441
585
  app_server_turn_timeout_seconds = None
442
586
 
587
+ agent_timeouts_raw = cfg.get("agent_timeouts")
588
+ has_explicit_codex_timeout = False
589
+ agent_timeouts: dict[str, Optional[float]] = dict(
590
+ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS
591
+ )
592
+ if isinstance(agent_timeouts_raw, dict):
593
+ for key, value in agent_timeouts_raw.items():
594
+ if str(key) == "codex":
595
+ has_explicit_codex_timeout = True
596
+ if value is None:
597
+ agent_timeouts[str(key)] = None
598
+ continue
599
+ try:
600
+ timeout_value = float(value)
601
+ except (TypeError, ValueError):
602
+ continue
603
+ if timeout_value <= 0:
604
+ agent_timeouts[str(key)] = None
605
+ else:
606
+ agent_timeouts[str(key)] = timeout_value
607
+
608
+ if not has_explicit_codex_timeout:
609
+ agent_timeouts["codex"] = app_server_turn_timeout_seconds
610
+
443
611
  polling_raw_value = cfg.get("polling")
444
612
  polling_raw: dict[str, Any] = (
445
613
  polling_raw_value if isinstance(polling_raw_value, dict) else {}
@@ -472,12 +640,15 @@ class TelegramBotConfig:
472
640
  allowed_chat_ids=allowed_chat_ids,
473
641
  allowed_user_ids=allowed_user_ids,
474
642
  require_topics=require_topics,
643
+ trigger_mode=trigger_mode,
475
644
  defaults=defaults,
476
645
  concurrency=concurrency,
477
646
  media=media,
478
647
  shell=shell,
648
+ cache=cache,
479
649
  progress_stream=progress_stream,
480
650
  command_registration=command_registration,
651
+ opencode_command=opencode_command,
481
652
  state_file=state_file,
482
653
  app_server_command_env=app_server_command_env,
483
654
  app_server_command=app_server_command,
@@ -486,6 +657,7 @@ class TelegramBotConfig:
486
657
  app_server_start_timeout_seconds=app_server_start_timeout_seconds,
487
658
  app_server_start_max_attempts=app_server_start_max_attempts,
488
659
  app_server_turn_timeout_seconds=app_server_turn_timeout_seconds,
660
+ agent_turn_timeout_seconds=agent_timeouts,
489
661
  poll_timeout_seconds=poll_timeout_seconds,
490
662
  poll_request_timeout_seconds=poll_request_timeout_seconds,
491
663
  poll_allowed_updates=poll_allowed_updates,
@@ -493,6 +665,7 @@ class TelegramBotConfig:
493
665
  metrics_mode=metrics_mode,
494
666
  coalesce_window_seconds=coalesce_window_seconds,
495
667
  agent_binaries=agent_binaries,
668
+ ticket_flow_auto_resume=ticket_flow_auto_resume,
496
669
  )
497
670
 
498
671
  def validate(self) -> None:
@@ -516,6 +689,8 @@ class TelegramBotConfig:
516
689
  issues.append(
517
690
  "poll_request_timeout_seconds must be greater than poll_timeout_seconds"
518
691
  )
692
+ if self.trigger_mode not in TRIGGER_MODE_OPTIONS:
693
+ issues.append(f"trigger_mode must be one of {sorted(TRIGGER_MODE_OPTIONS)}")
519
694
  if issues:
520
695
  raise TelegramBotConfigError("; ".join(issues))
521
696
 
@@ -18,7 +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
21
+
22
+
23
+ DEFAULT_AGENT_TURN_TIMEOUT_SECONDS = {
24
+ "codex": 28800.0,
25
+ "opencode": 28800.0,
26
+ }
22
27
  DEFAULT_WORKSPACE_STATE_ROOT = "~/.codex-autorunner/workspaces"
23
28
  DEFAULT_AGENT = "codex"
24
29
  APP_SERVER_START_BACKOFF_INITIAL_SECONDS = 1.0
@@ -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
@@ -11,7 +11,6 @@ from ..adapter import (
11
11
  EffortCallback,
12
12
  ModelCallback,
13
13
  PageCallback,
14
- PrFlowStartCallback,
15
14
  QuestionCancelCallback,
16
15
  QuestionCustomCallback,
17
16
  QuestionDoneCallback,
@@ -80,9 +79,6 @@ async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> Non
80
79
  elif isinstance(parsed, ReviewCommitCallback):
81
80
  if key:
82
81
  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
82
  elif isinstance(parsed, CancelCallback):
87
83
  if key:
88
84
  if parsed.kind == "interrupt":
@@ -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",