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.
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {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(
|
|
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(
|
|
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(
|
|
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
|
codex_autorunner/manifest.py
CHANGED
|
@@ -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
|
|
5
|
-
- base: Index,
|
|
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
|
-
-
|
|
9
|
-
-
|
|
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 .
|
|
26
|
-
from .
|
|
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
|
|
43
|
+
Build complete API router by combining all route modules.
|
|
40
44
|
|
|
41
45
|
Args:
|
|
42
|
-
static_dir: Path to
|
|
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(
|
|
54
|
-
router.include_router(
|
|
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
|
|