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,322 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from ...core.flows import FlowStore
10
+ from ...core.flows.controller import FlowController
11
+ from ...core.flows.models import FlowRunRecord, FlowRunStatus
12
+ from ...core.flows.worker_process import spawn_flow_worker
13
+ from ...core.utils import canonicalize_path
14
+ from ...flows.ticket_flow import build_ticket_flow_definition
15
+ from ...tickets import AgentPool
16
+ from .state import parse_topic_key
17
+
18
+
19
+ class TelegramTicketFlowBridge:
20
+ """Encapsulate ticket_flow pause/resume plumbing for Telegram service."""
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ logger: logging.Logger,
26
+ store,
27
+ pause_targets: dict[str, str],
28
+ send_message_with_outbox,
29
+ ) -> None:
30
+ self._logger = logger
31
+ self._store = store
32
+ self._pause_targets = pause_targets
33
+ self._send_message_with_outbox = send_message_with_outbox
34
+
35
+ @staticmethod
36
+ def _select_ticket_flow_topic(
37
+ entries: list[tuple[str, object]],
38
+ ) -> Optional[tuple[str, object]]:
39
+ if not entries:
40
+ return None
41
+
42
+ def score(entry: tuple[str, object]) -> tuple[int, float, str]:
43
+ key, record = entry
44
+ thread_id = None
45
+ try:
46
+ _chat_id, thread_id, _scope = parse_topic_key(key)
47
+ except Exception:
48
+ thread_id = None
49
+ active_raw = getattr(record, "active_thread_id", None)
50
+ try:
51
+ active_thread = int(active_raw) if active_raw is not None else None
52
+ except (TypeError, ValueError):
53
+ active_thread = None
54
+ active_match = (
55
+ int(thread_id) == active_thread if thread_id is not None else False
56
+ )
57
+ last_active_at = getattr(record, "last_active_at", None)
58
+ last_active = TelegramTicketFlowBridge._parse_last_active(last_active_at)
59
+ return (1 if active_match else 0, last_active, key)
60
+
61
+ return max(entries, key=score)
62
+
63
+ @staticmethod
64
+ def _parse_last_active(raw: Optional[str]) -> float:
65
+ if not isinstance(raw, str):
66
+ return float("-inf")
67
+ try:
68
+ return datetime.strptime(raw, "%Y-%m-%dT%H:%M:%SZ").timestamp()
69
+ except ValueError:
70
+ return float("-inf")
71
+
72
+ async def watch_ticket_flow_pauses(self, interval_seconds: float) -> None:
73
+ interval = max(interval_seconds, 1.0)
74
+ while True:
75
+ try:
76
+ await self._scan_and_notify_pauses()
77
+ except Exception as exc:
78
+ self._logger.warning("telegram.ticket_flow.watch_failed", exc_info=exc)
79
+ await asyncio.sleep(interval)
80
+
81
+ async def _scan_and_notify_pauses(self) -> None:
82
+ topics = await self._store.list_topics()
83
+ if not topics:
84
+ 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))
91
+
92
+ tasks = [
93
+ asyncio.create_task(self._notify_ticket_flow_pause(workspace_root, entries))
94
+ for workspace_root, entries in workspace_topics.items()
95
+ ]
96
+ if tasks:
97
+ await asyncio.gather(*tasks, return_exceptions=True)
98
+
99
+ async def _notify_ticket_flow_pause(
100
+ self,
101
+ workspace_root: Path,
102
+ entries: list[tuple[str, object]],
103
+ ) -> None:
104
+ try:
105
+ pause = await asyncio.to_thread(
106
+ self._load_ticket_flow_pause, workspace_root
107
+ )
108
+ except Exception as exc:
109
+ self._logger.warning(
110
+ "telegram.ticket_flow.scan_failed",
111
+ exc_info=exc,
112
+ workspace_root=str(workspace_root),
113
+ )
114
+ return
115
+ if pause is None:
116
+ return
117
+ run_id, seq, content = pause
118
+ marker = f"{run_id}:{seq}"
119
+ pending = [
120
+ (key, record)
121
+ for key, record in entries
122
+ if getattr(record, "last_ticket_dispatch_seq", None) != marker
123
+ ]
124
+ if not pending:
125
+ return
126
+ primary = self._select_ticket_flow_topic(pending)
127
+ if not primary:
128
+ return
129
+ message_text = self._format_ticket_flow_pause_message(run_id, seq, content)
130
+ updates: list[tuple[str, Optional[str]]] = [
131
+ (key, getattr(record, "last_ticket_dispatch_seq", None))
132
+ for key, record in pending
133
+ ]
134
+ for key, _previous in updates:
135
+ await self._store.update_topic(
136
+ key, self._set_ticket_dispatch_marker(marker)
137
+ )
138
+
139
+ primary_key, _primary_record = primary
140
+ try:
141
+ chat_id, thread_id, _scope = parse_topic_key(primary_key)
142
+ except Exception as exc:
143
+ self._logger.debug("Failed to parse topic key: %s", exc)
144
+ for key, previous in updates:
145
+ await self._store.update_topic(
146
+ key, self._set_ticket_dispatch_marker(previous)
147
+ )
148
+ return
149
+
150
+ try:
151
+ await self._send_message_with_outbox(
152
+ chat_id,
153
+ message_text,
154
+ thread_id=thread_id,
155
+ reply_to=None,
156
+ )
157
+ self._pause_targets[str(workspace_root)] = run_id
158
+ except Exception as exc:
159
+ self._logger.warning(
160
+ "telegram.ticket_flow.notify_failed",
161
+ exc_info=exc,
162
+ topic_key=primary_key,
163
+ run_id=run_id,
164
+ seq=seq,
165
+ )
166
+ for key, previous in updates:
167
+ await self._store.update_topic(
168
+ key, self._set_ticket_dispatch_marker(previous)
169
+ )
170
+
171
+ @staticmethod
172
+ def _set_ticket_dispatch_marker(
173
+ value: Optional[str],
174
+ ):
175
+ def apply(topic) -> None:
176
+ topic.last_ticket_dispatch_seq = value
177
+
178
+ return apply
179
+
180
+ def _load_ticket_flow_pause(
181
+ self, workspace_root: Path
182
+ ) -> Optional[tuple[str, str, str]]:
183
+ db_path = workspace_root / ".codex-autorunner" / "flows.db"
184
+ if not db_path.exists():
185
+ return None
186
+ store = FlowStore(db_path)
187
+ try:
188
+ store.initialize()
189
+ runs = store.list_flow_runs(
190
+ flow_type="ticket_flow", status=FlowRunStatus.PAUSED
191
+ )
192
+ if not runs:
193
+ return None
194
+ latest = runs[0]
195
+ runs_dir_raw = latest.input_data.get("runs_dir")
196
+ runs_dir = (
197
+ Path(runs_dir_raw)
198
+ if isinstance(runs_dir_raw, str) and runs_dir_raw
199
+ else Path(".codex-autorunner/runs")
200
+ )
201
+ from ...tickets.outbox import resolve_outbox_paths
202
+
203
+ paths = resolve_outbox_paths(
204
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=latest.id
205
+ )
206
+ history_dir = paths.dispatch_history_dir
207
+ seq = self._latest_dispatch_seq(history_dir)
208
+ if not seq:
209
+ reason = self._format_ticket_flow_pause_reason(latest)
210
+ return latest.id, "paused", reason
211
+ message_path = history_dir / seq / "DISPATCH.md"
212
+ try:
213
+ content = message_path.read_text(encoding="utf-8")
214
+ except OSError:
215
+ return None
216
+ return latest.id, seq, content
217
+ finally:
218
+ store.close()
219
+
220
+ @staticmethod
221
+ def _latest_dispatch_seq(history_dir: Path) -> Optional[str]:
222
+ if not history_dir.exists() or not history_dir.is_dir():
223
+ return None
224
+ seqs = [
225
+ child.name
226
+ for child in history_dir.iterdir()
227
+ if child.is_dir()
228
+ and not child.name.startswith(".")
229
+ and child.name.isdigit()
230
+ ]
231
+ if not seqs:
232
+ return None
233
+ return max(seqs)
234
+
235
+ @staticmethod
236
+ def _format_ticket_flow_pause_reason(record: FlowRunRecord) -> str:
237
+ state = record.state or {}
238
+ engine = state.get("ticket_engine") or {}
239
+ reason = (
240
+ engine.get("reason") or record.error_message or "Paused without details."
241
+ )
242
+ return f"Reason: {reason}"
243
+
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
+ def get_paused_ticket_flow(
256
+ self, workspace_root: Path, preferred_run_id: Optional[str] = None
257
+ ) -> Optional[tuple[str, FlowRunRecord]]:
258
+ db_path = workspace_root / ".codex-autorunner" / "flows.db"
259
+ if not db_path.exists():
260
+ return None
261
+ store = FlowStore(db_path)
262
+ try:
263
+ store.initialize()
264
+ if preferred_run_id:
265
+ preferred = store.get_flow_run(preferred_run_id)
266
+ if preferred and preferred.status == FlowRunStatus.PAUSED:
267
+ return preferred.id, preferred
268
+ runs = store.list_flow_runs(
269
+ flow_type="ticket_flow", status=FlowRunStatus.PAUSED
270
+ )
271
+ if not runs:
272
+ return None
273
+ latest = runs[0]
274
+ return latest.id, latest
275
+ finally:
276
+ store.close()
277
+
278
+ async def auto_resume_run(self, workspace_root: Path, run_id: str) -> None:
279
+ """Best-effort resume + worker spawn; failures are logged only."""
280
+ try:
281
+ controller = _ticket_controller_for(workspace_root)
282
+ updated = await controller.resume_flow(run_id)
283
+ if updated:
284
+ _spawn_ticket_worker(workspace_root, updated.id, self._logger)
285
+ except Exception as exc:
286
+ self._logger.warning(
287
+ "telegram.ticket_flow.auto_resume_failed",
288
+ exc=exc,
289
+ run_id=run_id,
290
+ workspace_root=str(workspace_root),
291
+ )
292
+
293
+
294
+ def _ticket_controller_for(repo_root: Path) -> FlowController:
295
+ repo_root = repo_root.resolve()
296
+ db_path = repo_root / ".codex-autorunner" / "flows.db"
297
+ artifacts_root = repo_root / ".codex-autorunner" / "flows"
298
+ from ...core.engine import Engine
299
+
300
+ engine = Engine(repo_root)
301
+ agent_pool = AgentPool(engine.config)
302
+ definition = build_ticket_flow_definition(agent_pool=agent_pool)
303
+ definition.validate()
304
+ controller = FlowController(
305
+ definition=definition, db_path=db_path, artifacts_root=artifacts_root
306
+ )
307
+ controller.initialize()
308
+ return controller
309
+
310
+
311
+ def _spawn_ticket_worker(repo_root: Path, run_id: str, logger: logging.Logger) -> None:
312
+ try:
313
+ proc, out, err = spawn_flow_worker(repo_root, run_id)
314
+ out.close()
315
+ err.close()
316
+ logger.info("Started ticket_flow worker for %s (pid=%s)", run_id, proc.pid)
317
+ except Exception as exc:
318
+ logger.warning(
319
+ "ticket_flow.worker.spawn_failed",
320
+ exc_info=exc,
321
+ extra={"run_id": run_id},
322
+ )
@@ -41,14 +41,33 @@ class TelegramMessageTransport:
41
41
  message_id: int,
42
42
  text: str,
43
43
  *,
44
+ message_thread_id: Optional[int] = None,
44
45
  reply_markup: Optional[dict[str, Any]] = None,
45
46
  ) -> bool:
46
47
  try:
47
48
  payload_text, parse_mode = self._prepare_message(text)
49
+ if len(payload_text) > TELEGRAM_MAX_MESSAGE_LENGTH:
50
+ trimmed = trim_markdown_message(
51
+ payload_text,
52
+ max_len=TELEGRAM_MAX_MESSAGE_LENGTH,
53
+ render=(
54
+ _format_telegram_html
55
+ if parse_mode == "HTML"
56
+ else (
57
+ lambda v: (
58
+ _format_telegram_markdown(v, parse_mode)
59
+ if parse_mode in ("Markdown", "MarkdownV2")
60
+ else v
61
+ )
62
+ )
63
+ ),
64
+ )
65
+ payload_text = trimmed
48
66
  await self._bot.edit_message_text(
49
67
  chat_id,
50
68
  message_id,
51
69
  payload_text,
70
+ message_thread_id=message_thread_id,
52
71
  reply_markup=reply_markup,
53
72
  parse_mode=parse_mode,
54
73
  )
@@ -56,11 +75,17 @@ class TelegramMessageTransport:
56
75
  return False
57
76
  return True
58
77
 
59
- async def _delete_message(self, chat_id: int, message_id: Optional[int]) -> bool:
78
+ async def _delete_message(
79
+ self, chat_id: int, message_id: Optional[int], thread_id: Optional[int] = None
80
+ ) -> bool:
60
81
  if message_id is None:
61
82
  return False
62
83
  try:
63
- return bool(await self._bot.delete_message(chat_id, message_id))
84
+ return bool(
85
+ await self._bot.delete_message(
86
+ chat_id, message_id, message_thread_id=thread_id
87
+ )
88
+ )
64
89
  except Exception:
65
90
  return False
66
91
 
@@ -77,6 +102,7 @@ class TelegramMessageTransport:
77
102
  callback.chat_id,
78
103
  callback.message_id,
79
104
  text,
105
+ message_thread_id=callback.thread_id,
80
106
  reply_markup=reply_markup,
81
107
  )
82
108
 
@@ -385,7 +411,13 @@ class TelegramMessageTransport:
385
411
  if callback is None:
386
412
  return
387
413
  try:
388
- await self._bot.answer_callback_query(callback.callback_id, text=text)
414
+ await self._bot.answer_callback_query(
415
+ callback.callback_id,
416
+ chat_id=callback.chat_id,
417
+ thread_id=callback.thread_id,
418
+ message_id=callback.message_id,
419
+ text=text,
420
+ )
389
421
  except Exception as exc:
390
422
  log_event(
391
423
  self._logger,
@@ -393,6 +425,7 @@ class TelegramMessageTransport:
393
425
  "telegram.answer_callback.failed",
394
426
  chat_id=callback.chat_id,
395
427
  thread_id=callback.thread_id,
428
+ message_id=callback.message_id,
396
429
  callback_id=callback.callback_id,
397
430
  exc=exc,
398
431
  )
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from .adapter import TelegramMessage
6
+
7
+ TriggerMode = Literal["all", "mentions"]
8
+
9
+
10
+ def should_trigger_run(
11
+ message: TelegramMessage,
12
+ *,
13
+ text: str,
14
+ bot_username: Optional[str],
15
+ ) -> bool:
16
+ """Return True if this message should start a run in mentions-only mode.
17
+
18
+ This mirrors Takopi's "mentions" trigger mode semantics (subset):
19
+
20
+ - Always trigger in private chats.
21
+ - Trigger when the bot is explicitly mentioned: "@<bot_username>" anywhere in the text.
22
+ - Trigger when replying to a bot message (but ignore the common forum-topic
23
+ "implicit root reply" case where clients set reply_to_message_id == thread_id).
24
+ - Otherwise, do not trigger (commands and other explicit affordances are handled elsewhere).
25
+ """
26
+
27
+ if message.chat_type == "private":
28
+ return True
29
+
30
+ lowered = (text or "").lower()
31
+ if bot_username:
32
+ needle = f"@{bot_username}".lower()
33
+ if needle in lowered:
34
+ return True
35
+
36
+ implicit_topic_reply = (
37
+ message.thread_id is not None
38
+ and message.reply_to_message_id is not None
39
+ and message.reply_to_message_id == message.thread_id
40
+ )
41
+
42
+ if message.reply_to_is_bot and not implicit_topic_reply:
43
+ return True
44
+
45
+ if (
46
+ bot_username
47
+ and message.reply_to_username
48
+ and message.reply_to_username.lower() == bot_username.lower()
49
+ and not implicit_topic_reply
50
+ ):
51
+ return True
52
+
53
+ return False
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, cast
7
7
  import yaml
8
8
 
9
9
  MANIFEST_VERSION = 2
10
+ MANIFEST_HEADER = "# GENERATED by CAR - DO NOT EDIT\n"
10
11
  _SAFE_REPO_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
11
12
  _SANITIZE_REPO_ID_PATTERN = re.compile(r"[^A-Za-z0-9._-]+")
12
13
 
@@ -194,4 +195,5 @@ def save_manifest(manifest_path: Path, manifest: Manifest, hub_root: Path) -> No
194
195
  "repos": [repo.to_dict(hub_root) for repo in manifest.repos],
195
196
  }
196
197
  with manifest_path.open("w", encoding="utf-8") as f:
198
+ f.write(MANIFEST_HEADER)
197
199
  yaml.safe_dump(payload, f, sort_keys=False)
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ """Codex Autorunner plugin API metadata.
4
+
5
+ This module is intentionally small and stable. External plugins SHOULD depend
6
+ only on the public API in `codex_autorunner.api` + this version constant.
7
+
8
+ Notes:
9
+ - Backwards-incompatible changes to the plugin API MUST bump
10
+ `CAR_PLUGIN_API_VERSION`.
11
+ """
12
+
13
+ CAR_PLUGIN_API_VERSION = 1
14
+
15
+ # Entry point groups (Python packaging entry points).
16
+ #
17
+ # Plugins can publish new agent backends by defining an entry point:
18
+ #
19
+ # [project.entry-points."codex_autorunner.agent_backends"]
20
+ # myagent = "my_package.my_module:AGENT_BACKEND"
21
+ #
22
+ CAR_AGENT_ENTRYPOINT_GROUP = "codex_autorunner.agent_backends"
@@ -1,16 +1,17 @@
1
1
  """
2
2
  Modular API routes for the codex-autorunner server.
3
3
 
4
- This package splits the monolithic api_routes.py into focused modules:
5
- - base: Index, state streaming, and general endpoints
4
+ This package splits monolithic api_routes.py into focused modules:
5
+ - base: Index, WebSocket terminal, and general endpoints
6
6
  - agents: Agent harness models and event streaming
7
7
  - app_server: App-server thread registry endpoints
8
- - docs: Document management (read/write) and chat
9
- - github: GitHub integration endpoints
8
+ - workspace: Optional workspace docs (active_context/decisions/spec)
9
+ - flows: Flow runtime management (start/stop/resume/status/events/artifacts)
10
+ - messages: Inbox/message wrappers over ticket_flow dispatch + reply histories
10
11
  - repos: Run control (start/stop/resume/reset)
11
- - runs: Run telemetry and artifacts
12
12
  - sessions: Terminal session registry endpoints
13
13
  - settings: Session settings for autorunner overrides
14
+ - file_chat: Unified file chat (tickets + workspace docs)
14
15
  - voice: Voice transcription and config
15
16
  - terminal_images: Terminal image uploads
16
17
  """
@@ -20,26 +21,29 @@ from pathlib import Path
20
21
  from fastapi import APIRouter
21
22
 
22
23
  from .agents import build_agents_routes
24
+ from .analytics import build_analytics_routes
23
25
  from .app_server import build_app_server_routes
24
- from .base import build_base_routes
25
- from .docs import build_docs_routes
26
- from .github import build_github_routes
26
+ from .base import build_base_routes, build_frontend_routes
27
+ from .file_chat import build_file_chat_routes
28
+ from .flows import build_flow_routes
29
+ from .messages import build_messages_routes
27
30
  from .repos import build_repos_routes
28
31
  from .review import build_review_routes
29
- from .runs import build_runs_routes
30
32
  from .sessions import build_sessions_routes
31
33
  from .settings import build_settings_routes
32
34
  from .system import build_system_routes
33
35
  from .terminal_images import build_terminal_image_routes
36
+ from .usage import build_usage_routes
34
37
  from .voice import build_voice_routes
38
+ from .workspace import build_workspace_routes
35
39
 
36
40
 
37
41
  def build_repo_router(static_dir: Path) -> APIRouter:
38
42
  """
39
- Build the complete API router by combining all route modules.
43
+ Build complete API router by combining all route modules.
40
44
 
41
45
  Args:
42
- static_dir: Path to the static assets directory
46
+ static_dir: Path to static assets directory
43
47
 
44
48
  Returns:
45
49
  Combined APIRouter with all endpoints
@@ -48,18 +52,23 @@ def build_repo_router(static_dir: Path) -> APIRouter:
48
52
 
49
53
  # Include all route modules
50
54
  router.include_router(build_base_routes(static_dir))
55
+ router.include_router(build_analytics_routes())
51
56
  router.include_router(build_agents_routes())
52
57
  router.include_router(build_app_server_routes())
53
- router.include_router(build_docs_routes())
54
- router.include_router(build_github_routes())
58
+ router.include_router(build_workspace_routes())
59
+ router.include_router(build_flow_routes())
60
+ router.include_router(build_file_chat_routes())
61
+ router.include_router(build_messages_routes())
55
62
  router.include_router(build_repos_routes())
56
63
  router.include_router(build_review_routes())
57
- router.include_router(build_runs_routes())
58
64
  router.include_router(build_sessions_routes())
59
65
  router.include_router(build_settings_routes())
60
66
  router.include_router(build_system_routes())
61
67
  router.include_router(build_terminal_image_routes())
68
+ router.include_router(build_usage_routes())
62
69
  router.include_router(build_voice_routes())
70
+ # Include frontend routes last to avoid shadowing API routes
71
+ router.include_router(build_frontend_routes(static_dir))
63
72
 
64
73
  return router
65
74