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
@@ -27,6 +27,7 @@ from .....agents.opencode.runtime import (
27
27
  split_model_id,
28
28
  )
29
29
  from .....agents.opencode.supervisor import OpenCodeSupervisorError
30
+ from .....core.about_car import CAR_CONTEXT_HINT, CAR_CONTEXT_KEYWORDS
30
31
  from .....core.config import load_repo_config
31
32
  from .....core.injected_context import wrap_injected_context
32
33
  from .....core.logging_utils import log_event
@@ -46,7 +47,6 @@ from ...constants import (
46
47
  DEFAULT_INTERRUPT_TIMEOUT_SECONDS,
47
48
  MAX_MENTION_BYTES,
48
49
  MAX_TOPIC_THREAD_HISTORY,
49
- OPENCODE_TURN_TIMEOUT_SECONDS,
50
50
  PLACEHOLDER_TEXT,
51
51
  QUEUED_PLACEHOLDER_TEXT,
52
52
  RESUME_PREVIEW_ASSISTANT_LIMIT,
@@ -94,20 +94,6 @@ OUTBOX_CONTEXT_RE = re.compile(
94
94
  re.IGNORECASE,
95
95
  )
96
96
 
97
- CAR_CONTEXT_KEYWORDS = (
98
- "car",
99
- "codex",
100
- "todo",
101
- "progress",
102
- "opinions",
103
- "spec",
104
- "summary",
105
- "autorunner",
106
- "work docs",
107
- )
108
- CAR_CONTEXT_HINT = (
109
- "Context: read .codex-autorunner/ABOUT_CAR.md for repo-specific rules."
110
- )
111
97
 
112
98
  FILES_HINT_TEMPLATE = (
113
99
  "Inbox: {inbox}\n"
@@ -1692,6 +1678,7 @@ class ExecutionCommands(SharedHelpers):
1692
1678
  opencode_client,
1693
1679
  session_id=thread_id,
1694
1680
  workspace_path=str(workspace_root),
1681
+ model_payload=model_payload,
1695
1682
  progress_session_ids=watched_session_ids,
1696
1683
  permission_policy=permission_policy,
1697
1684
  permission_handler=(
@@ -1719,9 +1706,14 @@ class ExecutionCommands(SharedHelpers):
1719
1706
  codex_thread_id=thread_id,
1720
1707
  sse_ready_ms=sse_ready_ms,
1721
1708
  )
1722
- timeout_task = asyncio.create_task(
1723
- asyncio.sleep(OPENCODE_TURN_TIMEOUT_SECONDS)
1709
+ timeout_seconds = self._config.agent_turn_timeout_seconds.get(
1710
+ "opencode"
1724
1711
  )
1712
+ timeout_task: Optional[asyncio.Task] = None
1713
+ if timeout_seconds is not None and timeout_seconds > 0:
1714
+ timeout_task = asyncio.create_task(
1715
+ asyncio.sleep(timeout_seconds)
1716
+ )
1725
1717
  prompt_sent_at = time.monotonic()
1726
1718
  prompt_task = asyncio.create_task(
1727
1719
  opencode_client.prompt_async(
@@ -1745,52 +1737,54 @@ class ExecutionCommands(SharedHelpers):
1745
1737
  endpoint="/session/{id}/prompt_async",
1746
1738
  )
1747
1739
  except Exception as exc:
1748
- timeout_task.cancel()
1749
- with suppress(asyncio.CancelledError):
1750
- await timeout_task
1740
+ if timeout_task is not None:
1741
+ timeout_task.cancel()
1742
+ with suppress(asyncio.CancelledError):
1743
+ await timeout_task
1751
1744
  output_task.cancel()
1752
1745
  with suppress(asyncio.CancelledError):
1753
1746
  await output_task
1754
1747
  raise exc
1755
- done, _pending = await asyncio.wait(
1756
- {output_task, timeout_task},
1757
- return_when=asyncio.FIRST_COMPLETED,
1758
- )
1759
- if timeout_task in done:
1760
- runtime.interrupt_requested = True
1761
- await _abort_opencode()
1762
- output_task.cancel()
1763
- with suppress(asyncio.CancelledError):
1764
- await output_task
1748
+ if timeout_task is not None:
1749
+ done, _pending = await asyncio.wait(
1750
+ {output_task, timeout_task},
1751
+ return_when=asyncio.FIRST_COMPLETED,
1752
+ )
1753
+ if timeout_task in done:
1754
+ runtime.interrupt_requested = True
1755
+ await _abort_opencode()
1756
+ output_task.cancel()
1757
+ with suppress(asyncio.CancelledError):
1758
+ await output_task
1759
+ timeout_task.cancel()
1760
+ with suppress(asyncio.CancelledError):
1761
+ await timeout_task
1762
+ turn_elapsed_seconds = time.monotonic() - turn_started_at
1763
+ completion_mode = (
1764
+ "timeout"
1765
+ if not runtime.interrupt_requested
1766
+ else "interrupt"
1767
+ )
1768
+ log_event(
1769
+ self._logger,
1770
+ logging.INFO,
1771
+ "telegram.opencode.completed",
1772
+ topic_key=key,
1773
+ chat_id=message.chat_id,
1774
+ thread_id=message.thread_id,
1775
+ codex_thread_id=thread_id,
1776
+ completion_mode=completion_mode,
1777
+ elapsed_seconds=turn_elapsed_seconds,
1778
+ )
1779
+ return _TurnRunFailure(
1780
+ "OpenCode turn timed out.",
1781
+ placeholder_id,
1782
+ transcript_message_id,
1783
+ transcript_text,
1784
+ )
1765
1785
  timeout_task.cancel()
1766
1786
  with suppress(asyncio.CancelledError):
1767
1787
  await timeout_task
1768
- turn_elapsed_seconds = time.monotonic() - turn_started_at
1769
- completion_mode = (
1770
- "timeout"
1771
- if not runtime.interrupt_requested
1772
- else "interrupt"
1773
- )
1774
- log_event(
1775
- self._logger,
1776
- logging.INFO,
1777
- "telegram.opencode.completed",
1778
- topic_key=key,
1779
- chat_id=message.chat_id,
1780
- thread_id=message.thread_id,
1781
- codex_thread_id=thread_id,
1782
- completion_mode=completion_mode,
1783
- elapsed_seconds=turn_elapsed_seconds,
1784
- )
1785
- return _TurnRunFailure(
1786
- "OpenCode turn timed out.",
1787
- placeholder_id,
1788
- transcript_message_id,
1789
- transcript_text,
1790
- )
1791
- timeout_task.cancel()
1792
- with suppress(asyncio.CancelledError):
1793
- await timeout_task
1794
1788
  output_result = await output_task
1795
1789
  turn_elapsed_seconds = time.monotonic() - turn_started_at
1796
1790
  log_event(
@@ -2225,7 +2219,9 @@ class ExecutionCommands(SharedHelpers):
2225
2219
  result = await self._wait_for_turn_result(
2226
2220
  client,
2227
2221
  turn_handle,
2228
- timeout_seconds=self._config.app_server_turn_timeout_seconds,
2222
+ timeout_seconds=self._config.agent_turn_timeout_seconds.get(
2223
+ "codex"
2224
+ ),
2229
2225
  topic_key=key,
2230
2226
  chat_id=message.chat_id,
2231
2227
  thread_id=message.thread_id,
@@ -8,7 +8,7 @@ import secrets
8
8
  import time
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
- from typing import TYPE_CHECKING, Any, Optional, Sequence
11
+ from typing import Any, Optional, Sequence
12
12
 
13
13
  from .....core.injected_context import wrap_injected_context
14
14
  from .....core.logging_utils import log_event
@@ -16,13 +16,10 @@ from .....core.state import now_iso
16
16
  from ...adapter import TelegramMessage
17
17
  from ...config import TelegramMediaCandidate
18
18
  from ...helpers import _path_within
19
+ from ...state import PendingVoiceRecord, TelegramTopicRecord
19
20
  from .. import messages as message_handlers
20
21
  from .shared import SharedHelpers
21
22
 
22
- if TYPE_CHECKING:
23
- from ...state import PendingVoiceRecord, TelegramTopicRecord
24
-
25
-
26
23
  FILES_HINT_TEMPLATE = (
27
24
  "Inbox: {inbox}\n"
28
25
  "Outbox (pending): {outbox}\n"
@@ -100,7 +97,6 @@ class MediaBatchResult:
100
97
 
101
98
 
102
99
  class FilesCommands(SharedHelpers):
103
-
104
100
  def _format_telegram_download_error(self, exc: Exception) -> Optional[str]:
105
101
  for current in _iter_exception_chain(exc):
106
102
  if isinstance(current, Exception):
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from .....core.engine import Engine
7
+ from .....core.flows import FlowController, FlowStore
8
+ from .....core.flows.models import FlowRunStatus
9
+ from .....core.flows.worker_process import (
10
+ check_worker_health,
11
+ spawn_flow_worker,
12
+ )
13
+ from .....core.utils import canonicalize_path
14
+ from .....flows.ticket_flow import build_ticket_flow_definition
15
+ from .....tickets import AgentPool
16
+ from ...adapter import TelegramMessage
17
+ from ...helpers import _truncate_text
18
+ from .shared import SharedHelpers
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _flow_paths(repo_root: Path) -> tuple[Path, Path]:
24
+ repo_root = repo_root.resolve()
25
+ db_path = repo_root / ".codex-autorunner" / "flows.db"
26
+ artifacts_root = repo_root / ".codex-autorunner" / "flows"
27
+ return db_path, artifacts_root
28
+
29
+
30
+ def _get_ticket_controller(repo_root: Path) -> FlowController:
31
+ db_path, artifacts_root = _flow_paths(repo_root)
32
+ engine = Engine(repo_root)
33
+ agent_pool = AgentPool(engine.config)
34
+ definition = build_ticket_flow_definition(agent_pool=agent_pool)
35
+ definition.validate()
36
+ controller = FlowController(
37
+ definition=definition, db_path=db_path, artifacts_root=artifacts_root
38
+ )
39
+ controller.initialize()
40
+ return controller
41
+
42
+
43
+ def _spawn_flow_worker(repo_root: Path, run_id: str) -> None:
44
+ health = check_worker_health(repo_root, run_id)
45
+ if health.is_alive:
46
+ _logger.info("Worker already active for run %s (pid=%s)", run_id, health.pid)
47
+ return
48
+
49
+ proc, out, err = spawn_flow_worker(repo_root, run_id)
50
+ try:
51
+ # We don't track handles in Telegram commands, close in parent after spawn.
52
+ out.close()
53
+ err.close()
54
+ finally:
55
+ if proc.poll() is not None:
56
+ _logger.warning("Flow worker for %s exited immediately", run_id)
57
+
58
+
59
+ class FlowCommands(SharedHelpers):
60
+ async def _handle_flow(self, message: TelegramMessage, args: str) -> None:
61
+ """
62
+ /flow start - seed tickets if missing and start ticket_flow
63
+ /flow resume - resume latest paused ticket_flow run
64
+ /flow status - show latest ticket_flow run status
65
+ """
66
+ key = await self._resolve_topic_key(message.chat_id, message.thread_id)
67
+ record = await self._store.get_topic(key)
68
+ if not record or not record.workspace_path:
69
+ await self._send_message(
70
+ message.chat_id,
71
+ "No workspace bound. Use /bind to bind this topic to a repo first.",
72
+ thread_id=message.thread_id,
73
+ reply_to=message.message_id,
74
+ )
75
+ return
76
+
77
+ repo_root = canonicalize_path(Path(record.workspace_path))
78
+ cmd = (args or "").strip().lower().split()
79
+ action = cmd[0] if cmd else "status"
80
+
81
+ controller = _get_ticket_controller(repo_root)
82
+
83
+ store = FlowStore(_flow_paths(repo_root)[0])
84
+ try:
85
+ store.initialize()
86
+ runs = store.list_flow_runs(flow_type="ticket_flow")
87
+ latest = runs[0] if runs else None
88
+ finally:
89
+ store.close()
90
+
91
+ if action == "start":
92
+ if latest and latest.status.is_active():
93
+ await self._send_message(
94
+ message.chat_id,
95
+ f"Ticket flow already active (run {latest.id}, status {latest.status.value}).",
96
+ thread_id=message.thread_id,
97
+ reply_to=message.message_id,
98
+ )
99
+ return
100
+ # seed ticket if missing
101
+ ticket_dir = repo_root / ".codex-autorunner" / "tickets"
102
+ ticket_dir.mkdir(parents=True, exist_ok=True)
103
+ first_ticket = ticket_dir / "TICKET-001.md"
104
+ seeded = False
105
+ if not first_ticket.exists():
106
+ first_ticket.write_text(
107
+ """---
108
+ agent: codex
109
+ done: false
110
+ title: Bootstrap ticket flow
111
+ goal: Create SPEC.md and additional tickets, then pause for review
112
+ ---
113
+
114
+ Create SPEC.md and additional tickets under .codex-autorunner/tickets/. Then write a pause DISPATCH.md for review.
115
+ """,
116
+ encoding="utf-8",
117
+ )
118
+ seeded = True
119
+
120
+ flow_record = await controller.start_flow(
121
+ input_data={},
122
+ metadata={"seeded_ticket": seeded, "origin": "telegram"},
123
+ )
124
+ _spawn_flow_worker(repo_root, flow_record.id)
125
+ await self._send_message(
126
+ message.chat_id,
127
+ f"Started ticket flow run {flow_record.id}.",
128
+ thread_id=message.thread_id,
129
+ reply_to=message.message_id,
130
+ )
131
+ return
132
+
133
+ if action == "resume":
134
+ if not latest:
135
+ await self._send_message(
136
+ message.chat_id,
137
+ "No ticket flow run found.",
138
+ thread_id=message.thread_id,
139
+ reply_to=message.message_id,
140
+ )
141
+ return
142
+ if latest.status != FlowRunStatus.PAUSED:
143
+ await self._send_message(
144
+ message.chat_id,
145
+ f"Latest run is {latest.status.value}, not paused.",
146
+ thread_id=message.thread_id,
147
+ reply_to=message.message_id,
148
+ )
149
+ return
150
+ updated = await controller.resume_flow(latest.id)
151
+ _spawn_flow_worker(repo_root, updated.id)
152
+ await self._send_message(
153
+ message.chat_id,
154
+ f"Resumed run {updated.id}.",
155
+ thread_id=message.thread_id,
156
+ reply_to=message.message_id,
157
+ )
158
+ return
159
+
160
+ # status (default)
161
+ if not latest:
162
+ await self._send_message(
163
+ message.chat_id,
164
+ "No ticket flow run found. Use /flow start to start.",
165
+ thread_id=message.thread_id,
166
+ reply_to=message.message_id,
167
+ )
168
+ return
169
+ state = latest.state or {}
170
+ engine = state.get("ticket_engine") or {}
171
+ current = engine.get("current_ticket") or "–"
172
+ reason = engine.get("reason") or latest.error_message or ""
173
+ text = f"Run {latest.id}\nStatus: {latest.status.value}\nCurrent: {current}"
174
+ if reason:
175
+ text += f"\nReason: {_truncate_text(str(reason), 400)}"
176
+ text += "\n\nUse /flow resume to resume a paused run."
177
+ await self._send_message(
178
+ message.chat_id,
179
+ text,
180
+ thread_id=message.thread_id,
181
+ reply_to=message.message_id,
182
+ )
183
+
184
+ async def _handle_reply(self, message: TelegramMessage, args: str) -> None:
185
+ key = await self._resolve_topic_key(message.chat_id, message.thread_id)
186
+ record = await self._store.get_topic(key)
187
+ if not record or not record.workspace_path:
188
+ await self._send_message(
189
+ message.chat_id,
190
+ "No workspace bound. Use /bind to bind this topic to a repo first.",
191
+ thread_id=message.thread_id,
192
+ reply_to=message.message_id,
193
+ )
194
+ return
195
+
196
+ repo_root = canonicalize_path(Path(record.workspace_path))
197
+ text = args.strip()
198
+ if not text:
199
+ await self._send_message(
200
+ message.chat_id,
201
+ "Provide a reply: `/reply <message>`",
202
+ thread_id=message.thread_id,
203
+ reply_to=message.message_id,
204
+ )
205
+ return
206
+
207
+ target_run_id = self._ticket_flow_pause_targets.get(str(repo_root))
208
+ paused = self._get_paused_ticket_flow(repo_root, preferred_run_id=target_run_id)
209
+ if not paused:
210
+ await self._send_message(
211
+ message.chat_id,
212
+ "No paused ticket flow run found for this workspace.",
213
+ thread_id=message.thread_id,
214
+ reply_to=message.message_id,
215
+ )
216
+ return
217
+
218
+ run_id, run_record = paused
219
+ success, result = await self._write_user_reply_from_telegram(
220
+ repo_root, run_id, run_record, message, text
221
+ )
222
+ await self._send_message(
223
+ message.chat_id,
224
+ result,
225
+ thread_id=message.thread_id,
226
+ reply_to=message.message_id,
227
+ )
@@ -75,7 +75,7 @@ class FormattingHelpers:
75
75
  def _build_compact_seed_prompt(self, summary_text: str) -> str:
76
76
  summary_text = summary_text.strip() or "(no summary)"
77
77
  return (
78
- "Context handoff from previous thread:\n\n"
78
+ "Context from previous thread:\n\n"
79
79
  f"{summary_text}\n\n"
80
80
  "Continue from this context. Ask for missing info if needed."
81
81
  )