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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
83
|
-
if not topics:
|
|
105
|
+
if not self._pause_config.enabled:
|
|
84
106
|
return
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
135
|
+
log_event(
|
|
136
|
+
self._logger,
|
|
137
|
+
logging.WARNING,
|
|
110
138
|
"telegram.ticket_flow.scan_failed",
|
|
111
|
-
|
|
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.
|
|
178
|
+
await self._send_full_dispatch(
|
|
152
179
|
chat_id,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
188
|
+
log_event(
|
|
189
|
+
self._logger,
|
|
190
|
+
logging.WARNING,
|
|
160
191
|
"telegram.ticket_flow.notify_failed",
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ...
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
if
|
|
284
|
-
|
|
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
|
-
) ->
|
|
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
|
+
]
|