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
@@ -4,15 +4,20 @@ import asyncio
4
4
  import logging
5
5
  from datetime import datetime
6
6
  from pathlib import Path
7
- from typing import Optional
7
+ from typing import Awaitable, Callable, Optional
8
8
 
9
+ from ...core.config import load_repo_config
9
10
  from ...core.flows import FlowStore
10
11
  from ...core.flows.controller import FlowController
11
12
  from ...core.flows.models import FlowRunRecord, FlowRunStatus
12
13
  from ...core.flows.worker_process import spawn_flow_worker
14
+ from ...core.logging_utils import log_event
13
15
  from ...core.utils import canonicalize_path
14
16
  from ...flows.ticket_flow import build_ticket_flow_definition
17
+ from ...manifest import load_manifest
15
18
  from ...tickets import AgentPool
19
+ from .adapter import chunk_message
20
+ from .constants import TELEGRAM_MAX_MESSAGE_LENGTH
16
21
  from .state import parse_topic_key
17
22
 
18
23
 
@@ -26,11 +31,24 @@ class TelegramTicketFlowBridge:
26
31
  store,
27
32
  pause_targets: dict[str, str],
28
33
  send_message_with_outbox,
34
+ send_document: Callable[..., Awaitable[bool]],
35
+ pause_config,
36
+ default_notification_chat_id: Optional[int],
37
+ hub_root: Optional[Path] = None,
38
+ manifest_path: Optional[Path] = None,
39
+ config_root: Optional[Path] = None,
29
40
  ) -> None:
30
41
  self._logger = logger
31
42
  self._store = store
32
43
  self._pause_targets = pause_targets
33
44
  self._send_message_with_outbox = send_message_with_outbox
45
+ self._send_document = send_document
46
+ self._pause_config = pause_config
47
+ self._default_notification_chat_id = default_notification_chat_id
48
+ self._hub_root = hub_root
49
+ self._manifest_path = manifest_path
50
+ self._config_root = config_root
51
+ self._last_default_notification: dict[Path, str] = {}
34
52
 
35
53
  @staticmethod
36
54
  def _select_ticket_flow_topic(
@@ -75,24 +93,32 @@ class TelegramTicketFlowBridge:
75
93
  try:
76
94
  await self._scan_and_notify_pauses()
77
95
  except Exception as exc:
78
- self._logger.warning("telegram.ticket_flow.watch_failed", exc_info=exc)
96
+ log_event(
97
+ self._logger,
98
+ logging.WARNING,
99
+ "telegram.ticket_flow.watch_failed",
100
+ exc=exc,
101
+ )
79
102
  await asyncio.sleep(interval)
80
103
 
81
104
  async def _scan_and_notify_pauses(self) -> None:
82
- topics = await self._store.list_topics()
83
- if not topics:
105
+ if not self._pause_config.enabled:
84
106
  return
85
- workspace_topics: dict[Path, list[tuple[str, object]]] = {}
86
- for key, record in topics.items():
87
- if not isinstance(record.workspace_path, str) or not record.workspace_path:
88
- continue
89
- workspace_root = canonicalize_path(Path(record.workspace_path))
90
- workspace_topics.setdefault(workspace_root, []).append((key, record))
107
+ topics = await self._store.list_topics()
108
+ workspace_topics = self._get_all_workspaces(topics or {})
91
109
 
92
- tasks = [
93
- asyncio.create_task(self._notify_ticket_flow_pause(workspace_root, entries))
94
- for workspace_root, entries in workspace_topics.items()
95
- ]
110
+ tasks = []
111
+ for workspace_root, entries in workspace_topics.items():
112
+ if entries:
113
+ tasks.append(
114
+ asyncio.create_task(
115
+ self._notify_ticket_flow_pause(workspace_root, entries)
116
+ )
117
+ )
118
+ else:
119
+ tasks.append(
120
+ asyncio.create_task(self._notify_via_default_chat(workspace_root))
121
+ )
96
122
  if tasks:
97
123
  await asyncio.gather(*tasks, return_exceptions=True)
98
124
 
@@ -106,15 +132,17 @@ class TelegramTicketFlowBridge:
106
132
  self._load_ticket_flow_pause, workspace_root
107
133
  )
108
134
  except Exception as exc:
109
- self._logger.warning(
135
+ log_event(
136
+ self._logger,
137
+ logging.WARNING,
110
138
  "telegram.ticket_flow.scan_failed",
111
- exc_info=exc,
139
+ exc=exc,
112
140
  workspace_root=str(workspace_root),
113
141
  )
114
142
  return
115
143
  if pause is None:
116
144
  return
117
- run_id, seq, content = pause
145
+ run_id, seq, content, archived_dir = pause
118
146
  marker = f"{run_id}:{seq}"
119
147
  pending = [
120
148
  (key, record)
@@ -126,7 +154,6 @@ class TelegramTicketFlowBridge:
126
154
  primary = self._select_ticket_flow_topic(pending)
127
155
  if not primary:
128
156
  return
129
- message_text = self._format_ticket_flow_pause_message(run_id, seq, content)
130
157
  updates: list[tuple[str, Optional[str]]] = [
131
158
  (key, getattr(record, "last_ticket_dispatch_seq", None))
132
159
  for key, record in pending
@@ -148,17 +175,21 @@ class TelegramTicketFlowBridge:
148
175
  return
149
176
 
150
177
  try:
151
- await self._send_message_with_outbox(
178
+ await self._send_full_dispatch(
152
179
  chat_id,
153
- message_text,
154
- thread_id=thread_id,
155
- reply_to=None,
180
+ thread_id,
181
+ run_id=run_id,
182
+ seq=seq,
183
+ content=content,
184
+ archived_dir=archived_dir,
156
185
  )
157
186
  self._pause_targets[str(workspace_root)] = run_id
158
187
  except Exception as exc:
159
- self._logger.warning(
188
+ log_event(
189
+ self._logger,
190
+ logging.WARNING,
160
191
  "telegram.ticket_flow.notify_failed",
161
- exc_info=exc,
192
+ exc=exc,
162
193
  topic_key=primary_key,
163
194
  run_id=run_id,
164
195
  seq=seq,
@@ -177,13 +208,42 @@ class TelegramTicketFlowBridge:
177
208
 
178
209
  return apply
179
210
 
211
+ def _get_all_workspaces(
212
+ self, topics: dict[str, object]
213
+ ) -> dict[Path, list[tuple[str, object]]]:
214
+ workspace_topics: dict[Path, list[tuple[str, object]]] = {}
215
+ for key, record in topics.items():
216
+ if not isinstance(record.workspace_path, str) or not record.workspace_path:
217
+ continue
218
+ workspace_root = canonicalize_path(Path(record.workspace_path))
219
+ workspace_topics.setdefault(workspace_root, []).append((key, record))
220
+
221
+ # Include config root
222
+ if self._config_root:
223
+ workspace_topics.setdefault(self._config_root.resolve(), [])
224
+
225
+ # Include hub manifest worktrees (for web-originated flows)
226
+ if self._hub_root and self._manifest_path and self._manifest_path.exists():
227
+ try:
228
+ manifest = load_manifest(self._manifest_path, self._hub_root)
229
+ for repo in manifest.repos:
230
+ path = canonicalize_path((self._hub_root / repo.path).resolve())
231
+ workspace_topics.setdefault(path, [])
232
+ except Exception as exc:
233
+ self._logger.debug(
234
+ "telegram.ticket_flow.manifest_load_failed", exc_info=exc
235
+ )
236
+
237
+ return workspace_topics
238
+
180
239
  def _load_ticket_flow_pause(
181
240
  self, workspace_root: Path
182
- ) -> Optional[tuple[str, str, str]]:
241
+ ) -> Optional[tuple[str, str, str, Optional[Path]]]:
183
242
  db_path = workspace_root / ".codex-autorunner" / "flows.db"
184
243
  if not db_path.exists():
185
244
  return None
186
- store = FlowStore(db_path)
245
+ config = load_repo_config(workspace_root)
246
+ store = FlowStore(db_path, durable=config.durable_writes)
187
247
  try:
188
248
  store.initialize()
189
249
  runs = store.list_flow_runs(
@@ -207,13 +267,13 @@ class TelegramTicketFlowBridge:
207
267
  seq = self._latest_dispatch_seq(history_dir)
208
268
  if not seq:
209
269
  reason = self._format_ticket_flow_pause_reason(latest)
210
- return latest.id, "paused", reason
270
+ return latest.id, "paused", reason, None
211
271
  message_path = history_dir / seq / "DISPATCH.md"
212
272
  try:
213
273
  content = message_path.read_text(encoding="utf-8")
214
274
  except OSError:
215
275
  return None
216
- return latest.id, seq, content
276
+ return latest.id, seq, content, history_dir / seq
217
277
  finally:
218
278
  store.close()
219
279
 
@@ -241,24 +301,14 @@ class TelegramTicketFlowBridge:
241
301
  )
242
302
  return f"Reason: {reason}"
243
303
 
244
- def _format_ticket_flow_pause_message(
245
- self, run_id: str, seq: str, content: str
246
- ) -> str:
247
- from .helpers import _truncate_text
248
-
249
- trimmed = _truncate_text(content.strip() or "(no dispatch message)", 3000)
250
- return (
251
- f"Ticket flow paused (run {run_id}). Latest dispatch #{seq}:\n\n"
252
- f"{trimmed}\n\nUse /flow resume to continue."
253
- )
254
-
255
304
  def get_paused_ticket_flow(
256
305
  self, workspace_root: Path, preferred_run_id: Optional[str] = None
257
306
  ) -> Optional[tuple[str, FlowRunRecord]]:
258
307
  db_path = workspace_root / ".codex-autorunner" / "flows.db"
259
308
  if not db_path.exists():
260
309
  return None
261
- store = FlowStore(db_path)
310
+ config = load_repo_config(workspace_root)
311
+ store = FlowStore(db_path, durable=config.durable_writes)
262
312
  try:
263
313
  store.initialize()
264
314
  if preferred_run_id:
@@ -283,21 +333,266 @@ class TelegramTicketFlowBridge:
283
333
  if updated:
284
334
  _spawn_ticket_worker(workspace_root, updated.id, self._logger)
285
335
  except Exception as exc:
286
- self._logger.warning(
336
+ log_event(
337
+ self._logger,
338
+ logging.WARNING,
287
339
  "telegram.ticket_flow.auto_resume_failed",
288
340
  exc=exc,
289
341
  run_id=run_id,
290
342
  workspace_root=str(workspace_root),
291
343
  )
292
344
 
345
+ async def _notify_via_default_chat(self, workspace_root: Path) -> None:
346
+ if not self._pause_config.enabled or self._default_notification_chat_id is None:
347
+ return
348
+ try:
349
+ pause = await asyncio.to_thread(
350
+ self._load_ticket_flow_pause, workspace_root
351
+ )
352
+ except Exception as exc:
353
+ log_event(
354
+ self._logger,
355
+ logging.WARNING,
356
+ "telegram.ticket_flow.scan_failed",
357
+ exc=exc,
358
+ workspace_root=str(workspace_root),
359
+ )
360
+ return
361
+ if pause is None:
362
+ return
363
+ run_id, seq, content, archived_dir = pause
364
+ marker = f"{run_id}:{seq}"
365
+ previous = self._last_default_notification.get(workspace_root)
366
+ if previous == marker:
367
+ return
368
+ try:
369
+ await self._send_full_dispatch(
370
+ self._default_notification_chat_id,
371
+ None,
372
+ run_id=run_id,
373
+ seq=seq,
374
+ content=content,
375
+ archived_dir=archived_dir,
376
+ )
377
+ self._last_default_notification[workspace_root] = marker
378
+ self._pause_targets[str(workspace_root)] = run_id
379
+ except Exception as exc:
380
+ log_event(
381
+ self._logger,
382
+ logging.WARNING,
383
+ "telegram.ticket_flow.notify_default_failed",
384
+ exc=exc,
385
+ chat_id=self._default_notification_chat_id,
386
+ run_id=run_id,
387
+ seq=seq,
388
+ )
389
+
390
+ async def _send_full_dispatch(
391
+ self,
392
+ chat_id: int,
393
+ thread_id: Optional[int],
394
+ *,
395
+ run_id: str,
396
+ seq: str,
397
+ content: str,
398
+ archived_dir: Optional[Path],
399
+ ) -> None:
400
+ await self._send_dispatch_text(
401
+ chat_id,
402
+ thread_id,
403
+ run_id=run_id,
404
+ seq=seq,
405
+ content=content,
406
+ )
407
+ if self._pause_config.send_attachments and archived_dir:
408
+ await self._send_dispatch_attachments(
409
+ chat_id,
410
+ thread_id,
411
+ run_id=run_id,
412
+ seq=seq,
413
+ archived_dir=archived_dir,
414
+ )
415
+
416
+ async def _send_dispatch_text(
417
+ self,
418
+ chat_id: int,
419
+ thread_id: Optional[int],
420
+ *,
421
+ run_id: str,
422
+ seq: str,
423
+ content: str,
424
+ ) -> None:
425
+ body = content.strip() or "(no dispatch message)"
426
+ header = f"Ticket flow paused (run {run_id}). Latest dispatch #{seq}:\n\n"
427
+ footer = "\n\nUse /flow resume to continue."
428
+ full_text = f"{header}{body}{footer}"
429
+
430
+ if self._pause_config.chunk_long_messages:
431
+ chunks = chunk_message(
432
+ full_text,
433
+ max_len=TELEGRAM_MAX_MESSAGE_LENGTH,
434
+ with_numbering=True,
435
+ )
436
+ else:
437
+ chunks = [full_text]
438
+
439
+ for idx, chunk in enumerate(chunks):
440
+ await self._send_message_with_outbox(
441
+ chat_id,
442
+ chunk,
443
+ thread_id=thread_id,
444
+ reply_to=None,
445
+ )
446
+ if idx == 0:
447
+ await asyncio.sleep(0)
448
+
449
+ async def _send_dispatch_attachments(
450
+ self,
451
+ chat_id: int,
452
+ thread_id: Optional[int],
453
+ *,
454
+ run_id: str,
455
+ seq: str,
456
+ archived_dir: Path,
457
+ ) -> None:
458
+ try:
459
+ items = sorted(
460
+ [
461
+ child
462
+ for child in archived_dir.iterdir()
463
+ if child.is_file()
464
+ and child.name != "DISPATCH.md"
465
+ and not child.name.startswith(".")
466
+ ],
467
+ key=lambda p: p.name,
468
+ )
469
+ except OSError as exc:
470
+ log_event(
471
+ self._logger,
472
+ logging.WARNING,
473
+ "telegram.ticket_flow.attachments_list_failed",
474
+ exc=exc,
475
+ run_id=run_id,
476
+ seq=seq,
477
+ dir=str(archived_dir),
478
+ )
479
+ return
480
+
481
+ for item in items:
482
+ await self._send_single_attachment(
483
+ chat_id,
484
+ thread_id,
485
+ run_id=run_id,
486
+ seq=seq,
487
+ path=item,
488
+ )
489
+
490
+ async def _send_single_attachment(
491
+ self,
492
+ chat_id: int,
493
+ thread_id: Optional[int],
494
+ *,
495
+ run_id: str,
496
+ seq: str,
497
+ path: Path,
498
+ ) -> None:
499
+ try:
500
+ size = path.stat().st_size
501
+ except OSError:
502
+ size = None
503
+ if size is not None and size > self._pause_config.max_file_size_bytes:
504
+ warning = (
505
+ f"Skipped attachment {path.name} "
506
+ f"({size} bytes > {self._pause_config.max_file_size_bytes} limit)."
507
+ )
508
+ await self._send_message_with_outbox(
509
+ chat_id,
510
+ warning,
511
+ thread_id=thread_id,
512
+ reply_to=None,
513
+ )
514
+ return
515
+ try:
516
+ data = path.read_bytes()
517
+ except OSError as exc:
518
+ log_event(
519
+ self._logger,
520
+ logging.WARNING,
521
+ "telegram.ticket_flow.attachment_read_failed",
522
+ exc=exc,
523
+ file=str(path),
524
+ run_id=run_id,
525
+ seq=seq,
526
+ )
527
+ await self._send_message_with_outbox(
528
+ chat_id,
529
+ f"Failed to read attachment {path.name}.",
530
+ thread_id=thread_id,
531
+ reply_to=None,
532
+ )
533
+ return
534
+ caption = f"[run {run_id} dispatch #{seq}] {path.name}"
535
+ send_ok = False
536
+ try:
537
+ send_ok = await self._send_document(
538
+ chat_id,
539
+ data,
540
+ filename=path.name,
541
+ thread_id=thread_id,
542
+ reply_to=None,
543
+ caption=caption[:1024],
544
+ )
545
+ if not send_ok:
546
+ log_event(
547
+ self._logger,
548
+ logging.WARNING,
549
+ "telegram.ticket_flow.attachment_send_failed",
550
+ file=str(path),
551
+ run_id=run_id,
552
+ seq=seq,
553
+ )
554
+ except Exception as exc:
555
+ log_event(
556
+ self._logger,
557
+ logging.WARNING,
558
+ "telegram.ticket_flow.attachment_send_failed",
559
+ exc=exc,
560
+ file=str(path),
561
+ run_id=run_id,
562
+ seq=seq,
563
+ )
564
+ if not send_ok:
565
+ await self._send_message_with_outbox(
566
+ chat_id,
567
+ f"Failed to send attachment {path.name}.",
568
+ thread_id=thread_id,
569
+ reply_to=None,
570
+ )
571
+
293
572
 
294
573
  def _ticket_controller_for(repo_root: Path) -> FlowController:
295
574
  repo_root = repo_root.resolve()
296
575
  db_path = repo_root / ".codex-autorunner" / "flows.db"
297
576
  artifacts_root = repo_root / ".codex-autorunner" / "flows"
298
- from ...core.engine import Engine
577
+ from ...agents.registry import validate_agent_id
578
+ from ...core.config import load_repo_config
579
+ from ...core.runtime import RuntimeContext
580
+ from ...integrations.agents import build_backend_orchestrator
581
+ from ...integrations.agents.wiring import (
582
+ build_agent_backend_factory,
583
+ build_app_server_supervisor_factory,
584
+ )
299
585
 
300
- engine = Engine(repo_root)
586
+ config = load_repo_config(repo_root)
587
+ backend_orchestrator = build_backend_orchestrator(repo_root, config)
588
+ engine = RuntimeContext(
589
+ repo_root,
590
+ config=config,
591
+ backend_orchestrator=backend_orchestrator,
592
+ backend_factory=build_agent_backend_factory(repo_root, config),
593
+ app_server_supervisor_factory=build_app_server_supervisor_factory(config),
594
+ agent_id_validator=validate_agent_id,
595
+ )
301
596
  agent_pool = AgentPool(engine.config)
302
597
  definition = build_ticket_flow_definition(agent_pool=agent_pool)
303
598
  definition.validate()
@@ -265,6 +265,7 @@ class TelegramMessageTransport:
265
265
  thread_id: Optional[int] = None,
266
266
  reply_to: Optional[int] = None,
267
267
  reply_markup: Optional[dict[str, Any]] = None,
268
+ parse_mode: Optional[str] = None,
268
269
  ) -> None:
269
270
  if _should_trace_message(text):
270
271
  text = _with_conversation_id(
@@ -279,9 +280,15 @@ class TelegramMessageTransport:
279
280
  )
280
281
  if prefix:
281
282
  text = f"{prefix}{text}"
282
- parse_mode = self._config.parse_mode
283
- if parse_mode:
284
- rendered, used_mode = self._render_message(text)
283
+ effective_parse_mode = parse_mode or self._config.parse_mode
284
+ if effective_parse_mode:
285
+ try:
286
+ rendered, used_mode = self._render_message(
287
+ text, parse_mode=effective_parse_mode
288
+ )
289
+ except TypeError:
290
+ # Back-compat for subclasses/tests that don't accept parse_mode kwarg
291
+ rendered, used_mode = self._render_message(text) # type: ignore[misc]
285
292
  if used_mode and len(rendered) > TELEGRAM_MAX_MESSAGE_LENGTH:
286
293
  overflow_mode = getattr(self._config, "message_overflow", "document")
287
294
  if overflow_mode == "split" and used_mode in (
@@ -384,7 +391,7 @@ class TelegramMessageTransport:
384
391
  thread_id: Optional[int] = None,
385
392
  reply_to: Optional[int] = None,
386
393
  caption: Optional[str] = None,
387
- ) -> None:
394
+ ) -> bool:
388
395
  try:
389
396
  await self._bot.send_document(
390
397
  chat_id,
@@ -394,6 +401,7 @@ class TelegramMessageTransport:
394
401
  reply_to_message_id=reply_to,
395
402
  caption=caption,
396
403
  )
404
+ return True
397
405
  except Exception as exc:
398
406
  log_event(
399
407
  self._logger,
@@ -404,6 +412,7 @@ class TelegramMessageTransport:
404
412
  reply_to_message_id=reply_to,
405
413
  exc=exc,
406
414
  )
415
+ return False
407
416
 
408
417
  async def _answer_callback(
409
418
  self, callback: Optional[TelegramCallbackQuery], text: str
@@ -0,0 +1,27 @@
1
+ """Template integration helpers."""
2
+
3
+ from .scan_agent import (
4
+ TemplateScanBackendError,
5
+ TemplateScanDecision,
6
+ TemplateScanError,
7
+ TemplateScanFormatError,
8
+ TemplateScanRejectedError,
9
+ build_template_scan_prompt,
10
+ format_template_scan_rejection,
11
+ parse_template_scan_output,
12
+ run_template_scan,
13
+ run_template_scan_with_orchestrator,
14
+ )
15
+
16
+ __all__ = [
17
+ "TemplateScanBackendError",
18
+ "TemplateScanDecision",
19
+ "TemplateScanError",
20
+ "TemplateScanFormatError",
21
+ "TemplateScanRejectedError",
22
+ "build_template_scan_prompt",
23
+ "format_template_scan_rejection",
24
+ "parse_template_scan_output",
25
+ "run_template_scan",
26
+ "run_template_scan_with_orchestrator",
27
+ ]