codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -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 +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -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/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- 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/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -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 +1364 -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 +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- 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 +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- 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 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- 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 +344 -325
- 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 +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -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 +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.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/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,611 @@
|
|
|
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 Awaitable, Callable, 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.logging_utils import log_event
|
|
14
|
+
from ...core.utils import canonicalize_path
|
|
15
|
+
from ...flows.ticket_flow import build_ticket_flow_definition
|
|
16
|
+
from ...manifest import load_manifest
|
|
17
|
+
from ...tickets import AgentPool
|
|
18
|
+
from .adapter import chunk_message
|
|
19
|
+
from .constants import TELEGRAM_MAX_MESSAGE_LENGTH
|
|
20
|
+
from .state import parse_topic_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TelegramTicketFlowBridge:
|
|
24
|
+
"""Encapsulate ticket_flow pause/resume plumbing for Telegram service."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
logger: logging.Logger,
|
|
30
|
+
store,
|
|
31
|
+
pause_targets: dict[str, str],
|
|
32
|
+
send_message_with_outbox,
|
|
33
|
+
send_document: Callable[..., Awaitable[bool]],
|
|
34
|
+
pause_config,
|
|
35
|
+
default_notification_chat_id: Optional[int],
|
|
36
|
+
hub_root: Optional[Path] = None,
|
|
37
|
+
manifest_path: Optional[Path] = None,
|
|
38
|
+
config_root: Optional[Path] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._logger = logger
|
|
41
|
+
self._store = store
|
|
42
|
+
self._pause_targets = pause_targets
|
|
43
|
+
self._send_message_with_outbox = send_message_with_outbox
|
|
44
|
+
self._send_document = send_document
|
|
45
|
+
self._pause_config = pause_config
|
|
46
|
+
self._default_notification_chat_id = default_notification_chat_id
|
|
47
|
+
self._hub_root = hub_root
|
|
48
|
+
self._manifest_path = manifest_path
|
|
49
|
+
self._config_root = config_root
|
|
50
|
+
self._last_default_notification: dict[Path, str] = {}
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _select_ticket_flow_topic(
|
|
54
|
+
entries: list[tuple[str, object]],
|
|
55
|
+
) -> Optional[tuple[str, object]]:
|
|
56
|
+
if not entries:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def score(entry: tuple[str, object]) -> tuple[int, float, str]:
|
|
60
|
+
key, record = entry
|
|
61
|
+
thread_id = None
|
|
62
|
+
try:
|
|
63
|
+
_chat_id, thread_id, _scope = parse_topic_key(key)
|
|
64
|
+
except Exception:
|
|
65
|
+
thread_id = None
|
|
66
|
+
active_raw = getattr(record, "active_thread_id", None)
|
|
67
|
+
try:
|
|
68
|
+
active_thread = int(active_raw) if active_raw is not None else None
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
active_thread = None
|
|
71
|
+
active_match = (
|
|
72
|
+
int(thread_id) == active_thread if thread_id is not None else False
|
|
73
|
+
)
|
|
74
|
+
last_active_at = getattr(record, "last_active_at", None)
|
|
75
|
+
last_active = TelegramTicketFlowBridge._parse_last_active(last_active_at)
|
|
76
|
+
return (1 if active_match else 0, last_active, key)
|
|
77
|
+
|
|
78
|
+
return max(entries, key=score)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _parse_last_active(raw: Optional[str]) -> float:
|
|
82
|
+
if not isinstance(raw, str):
|
|
83
|
+
return float("-inf")
|
|
84
|
+
try:
|
|
85
|
+
return datetime.strptime(raw, "%Y-%m-%dT%H:%M:%SZ").timestamp()
|
|
86
|
+
except ValueError:
|
|
87
|
+
return float("-inf")
|
|
88
|
+
|
|
89
|
+
async def watch_ticket_flow_pauses(self, interval_seconds: float) -> None:
|
|
90
|
+
interval = max(interval_seconds, 1.0)
|
|
91
|
+
while True:
|
|
92
|
+
try:
|
|
93
|
+
await self._scan_and_notify_pauses()
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
log_event(
|
|
96
|
+
self._logger,
|
|
97
|
+
logging.WARNING,
|
|
98
|
+
"telegram.ticket_flow.watch_failed",
|
|
99
|
+
exc=exc,
|
|
100
|
+
)
|
|
101
|
+
await asyncio.sleep(interval)
|
|
102
|
+
|
|
103
|
+
async def _scan_and_notify_pauses(self) -> None:
|
|
104
|
+
if not self._pause_config.enabled:
|
|
105
|
+
return
|
|
106
|
+
topics = await self._store.list_topics()
|
|
107
|
+
workspace_topics = self._get_all_workspaces(topics or {})
|
|
108
|
+
|
|
109
|
+
tasks = []
|
|
110
|
+
for workspace_root, entries in workspace_topics.items():
|
|
111
|
+
if entries:
|
|
112
|
+
tasks.append(
|
|
113
|
+
asyncio.create_task(
|
|
114
|
+
self._notify_ticket_flow_pause(workspace_root, entries)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
tasks.append(
|
|
119
|
+
asyncio.create_task(self._notify_via_default_chat(workspace_root))
|
|
120
|
+
)
|
|
121
|
+
if tasks:
|
|
122
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
123
|
+
|
|
124
|
+
async def _notify_ticket_flow_pause(
|
|
125
|
+
self,
|
|
126
|
+
workspace_root: Path,
|
|
127
|
+
entries: list[tuple[str, object]],
|
|
128
|
+
) -> None:
|
|
129
|
+
try:
|
|
130
|
+
pause = await asyncio.to_thread(
|
|
131
|
+
self._load_ticket_flow_pause, workspace_root
|
|
132
|
+
)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
log_event(
|
|
135
|
+
self._logger,
|
|
136
|
+
logging.WARNING,
|
|
137
|
+
"telegram.ticket_flow.scan_failed",
|
|
138
|
+
exc=exc,
|
|
139
|
+
workspace_root=str(workspace_root),
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
if pause is None:
|
|
143
|
+
return
|
|
144
|
+
run_id, seq, content, archived_dir = pause
|
|
145
|
+
marker = f"{run_id}:{seq}"
|
|
146
|
+
pending = [
|
|
147
|
+
(key, record)
|
|
148
|
+
for key, record in entries
|
|
149
|
+
if getattr(record, "last_ticket_dispatch_seq", None) != marker
|
|
150
|
+
]
|
|
151
|
+
if not pending:
|
|
152
|
+
return
|
|
153
|
+
primary = self._select_ticket_flow_topic(pending)
|
|
154
|
+
if not primary:
|
|
155
|
+
return
|
|
156
|
+
updates: list[tuple[str, Optional[str]]] = [
|
|
157
|
+
(key, getattr(record, "last_ticket_dispatch_seq", None))
|
|
158
|
+
for key, record in pending
|
|
159
|
+
]
|
|
160
|
+
for key, _previous in updates:
|
|
161
|
+
await self._store.update_topic(
|
|
162
|
+
key, self._set_ticket_dispatch_marker(marker)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
primary_key, _primary_record = primary
|
|
166
|
+
try:
|
|
167
|
+
chat_id, thread_id, _scope = parse_topic_key(primary_key)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
self._logger.debug("Failed to parse topic key: %s", exc)
|
|
170
|
+
for key, previous in updates:
|
|
171
|
+
await self._store.update_topic(
|
|
172
|
+
key, self._set_ticket_dispatch_marker(previous)
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
await self._send_full_dispatch(
|
|
178
|
+
chat_id,
|
|
179
|
+
thread_id,
|
|
180
|
+
run_id=run_id,
|
|
181
|
+
seq=seq,
|
|
182
|
+
content=content,
|
|
183
|
+
archived_dir=archived_dir,
|
|
184
|
+
)
|
|
185
|
+
self._pause_targets[str(workspace_root)] = run_id
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
log_event(
|
|
188
|
+
self._logger,
|
|
189
|
+
logging.WARNING,
|
|
190
|
+
"telegram.ticket_flow.notify_failed",
|
|
191
|
+
exc=exc,
|
|
192
|
+
topic_key=primary_key,
|
|
193
|
+
run_id=run_id,
|
|
194
|
+
seq=seq,
|
|
195
|
+
)
|
|
196
|
+
for key, previous in updates:
|
|
197
|
+
await self._store.update_topic(
|
|
198
|
+
key, self._set_ticket_dispatch_marker(previous)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _set_ticket_dispatch_marker(
|
|
203
|
+
value: Optional[str],
|
|
204
|
+
):
|
|
205
|
+
def apply(topic) -> None:
|
|
206
|
+
topic.last_ticket_dispatch_seq = value
|
|
207
|
+
|
|
208
|
+
return apply
|
|
209
|
+
|
|
210
|
+
def _get_all_workspaces(
|
|
211
|
+
self, topics: dict[str, object]
|
|
212
|
+
) -> dict[Path, list[tuple[str, object]]]:
|
|
213
|
+
workspace_topics: dict[Path, list[tuple[str, object]]] = {}
|
|
214
|
+
for key, record in topics.items():
|
|
215
|
+
if not isinstance(record.workspace_path, str) or not record.workspace_path:
|
|
216
|
+
continue
|
|
217
|
+
workspace_root = canonicalize_path(Path(record.workspace_path))
|
|
218
|
+
workspace_topics.setdefault(workspace_root, []).append((key, record))
|
|
219
|
+
|
|
220
|
+
# Include config root
|
|
221
|
+
if self._config_root:
|
|
222
|
+
workspace_topics.setdefault(self._config_root.resolve(), [])
|
|
223
|
+
|
|
224
|
+
# Include hub manifest worktrees (for web-originated flows)
|
|
225
|
+
if self._hub_root and self._manifest_path and self._manifest_path.exists():
|
|
226
|
+
try:
|
|
227
|
+
manifest = load_manifest(self._manifest_path, self._hub_root)
|
|
228
|
+
for repo in manifest.repos:
|
|
229
|
+
path = canonicalize_path((self._hub_root / repo.path).resolve())
|
|
230
|
+
workspace_topics.setdefault(path, [])
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
self._logger.debug(
|
|
233
|
+
"telegram.ticket_flow.manifest_load_failed", exc_info=exc
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return workspace_topics
|
|
237
|
+
|
|
238
|
+
def _load_ticket_flow_pause(
|
|
239
|
+
self, workspace_root: Path
|
|
240
|
+
) -> Optional[tuple[str, str, str, Optional[Path]]]:
|
|
241
|
+
db_path = workspace_root / ".codex-autorunner" / "flows.db"
|
|
242
|
+
if not db_path.exists():
|
|
243
|
+
return None
|
|
244
|
+
store = FlowStore(db_path)
|
|
245
|
+
try:
|
|
246
|
+
store.initialize()
|
|
247
|
+
runs = store.list_flow_runs(
|
|
248
|
+
flow_type="ticket_flow", status=FlowRunStatus.PAUSED
|
|
249
|
+
)
|
|
250
|
+
if not runs:
|
|
251
|
+
return None
|
|
252
|
+
latest = runs[0]
|
|
253
|
+
runs_dir_raw = latest.input_data.get("runs_dir")
|
|
254
|
+
runs_dir = (
|
|
255
|
+
Path(runs_dir_raw)
|
|
256
|
+
if isinstance(runs_dir_raw, str) and runs_dir_raw
|
|
257
|
+
else Path(".codex-autorunner/runs")
|
|
258
|
+
)
|
|
259
|
+
from ...tickets.outbox import resolve_outbox_paths
|
|
260
|
+
|
|
261
|
+
paths = resolve_outbox_paths(
|
|
262
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=latest.id
|
|
263
|
+
)
|
|
264
|
+
history_dir = paths.dispatch_history_dir
|
|
265
|
+
seq = self._latest_dispatch_seq(history_dir)
|
|
266
|
+
if not seq:
|
|
267
|
+
reason = self._format_ticket_flow_pause_reason(latest)
|
|
268
|
+
return latest.id, "paused", reason, None
|
|
269
|
+
message_path = history_dir / seq / "DISPATCH.md"
|
|
270
|
+
try:
|
|
271
|
+
content = message_path.read_text(encoding="utf-8")
|
|
272
|
+
except OSError:
|
|
273
|
+
return None
|
|
274
|
+
return latest.id, seq, content, history_dir / seq
|
|
275
|
+
finally:
|
|
276
|
+
store.close()
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _latest_dispatch_seq(history_dir: Path) -> Optional[str]:
|
|
280
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
281
|
+
return None
|
|
282
|
+
seqs = [
|
|
283
|
+
child.name
|
|
284
|
+
for child in history_dir.iterdir()
|
|
285
|
+
if child.is_dir()
|
|
286
|
+
and not child.name.startswith(".")
|
|
287
|
+
and child.name.isdigit()
|
|
288
|
+
]
|
|
289
|
+
if not seqs:
|
|
290
|
+
return None
|
|
291
|
+
return max(seqs)
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _format_ticket_flow_pause_reason(record: FlowRunRecord) -> str:
|
|
295
|
+
state = record.state or {}
|
|
296
|
+
engine = state.get("ticket_engine") or {}
|
|
297
|
+
reason = (
|
|
298
|
+
engine.get("reason") or record.error_message or "Paused without details."
|
|
299
|
+
)
|
|
300
|
+
return f"Reason: {reason}"
|
|
301
|
+
|
|
302
|
+
def get_paused_ticket_flow(
|
|
303
|
+
self, workspace_root: Path, preferred_run_id: Optional[str] = None
|
|
304
|
+
) -> Optional[tuple[str, FlowRunRecord]]:
|
|
305
|
+
db_path = workspace_root / ".codex-autorunner" / "flows.db"
|
|
306
|
+
if not db_path.exists():
|
|
307
|
+
return None
|
|
308
|
+
store = FlowStore(db_path)
|
|
309
|
+
try:
|
|
310
|
+
store.initialize()
|
|
311
|
+
if preferred_run_id:
|
|
312
|
+
preferred = store.get_flow_run(preferred_run_id)
|
|
313
|
+
if preferred and preferred.status == FlowRunStatus.PAUSED:
|
|
314
|
+
return preferred.id, preferred
|
|
315
|
+
runs = store.list_flow_runs(
|
|
316
|
+
flow_type="ticket_flow", status=FlowRunStatus.PAUSED
|
|
317
|
+
)
|
|
318
|
+
if not runs:
|
|
319
|
+
return None
|
|
320
|
+
latest = runs[0]
|
|
321
|
+
return latest.id, latest
|
|
322
|
+
finally:
|
|
323
|
+
store.close()
|
|
324
|
+
|
|
325
|
+
async def auto_resume_run(self, workspace_root: Path, run_id: str) -> None:
|
|
326
|
+
"""Best-effort resume + worker spawn; failures are logged only."""
|
|
327
|
+
try:
|
|
328
|
+
controller = _ticket_controller_for(workspace_root)
|
|
329
|
+
updated = await controller.resume_flow(run_id)
|
|
330
|
+
if updated:
|
|
331
|
+
_spawn_ticket_worker(workspace_root, updated.id, self._logger)
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
log_event(
|
|
334
|
+
self._logger,
|
|
335
|
+
logging.WARNING,
|
|
336
|
+
"telegram.ticket_flow.auto_resume_failed",
|
|
337
|
+
exc=exc,
|
|
338
|
+
run_id=run_id,
|
|
339
|
+
workspace_root=str(workspace_root),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def _notify_via_default_chat(self, workspace_root: Path) -> None:
|
|
343
|
+
if not self._pause_config.enabled or self._default_notification_chat_id is None:
|
|
344
|
+
return
|
|
345
|
+
try:
|
|
346
|
+
pause = await asyncio.to_thread(
|
|
347
|
+
self._load_ticket_flow_pause, workspace_root
|
|
348
|
+
)
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
log_event(
|
|
351
|
+
self._logger,
|
|
352
|
+
logging.WARNING,
|
|
353
|
+
"telegram.ticket_flow.scan_failed",
|
|
354
|
+
exc=exc,
|
|
355
|
+
workspace_root=str(workspace_root),
|
|
356
|
+
)
|
|
357
|
+
return
|
|
358
|
+
if pause is None:
|
|
359
|
+
return
|
|
360
|
+
run_id, seq, content, archived_dir = pause
|
|
361
|
+
marker = f"{run_id}:{seq}"
|
|
362
|
+
previous = self._last_default_notification.get(workspace_root)
|
|
363
|
+
if previous == marker:
|
|
364
|
+
return
|
|
365
|
+
try:
|
|
366
|
+
await self._send_full_dispatch(
|
|
367
|
+
self._default_notification_chat_id,
|
|
368
|
+
None,
|
|
369
|
+
run_id=run_id,
|
|
370
|
+
seq=seq,
|
|
371
|
+
content=content,
|
|
372
|
+
archived_dir=archived_dir,
|
|
373
|
+
)
|
|
374
|
+
self._last_default_notification[workspace_root] = marker
|
|
375
|
+
self._pause_targets[str(workspace_root)] = run_id
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
log_event(
|
|
378
|
+
self._logger,
|
|
379
|
+
logging.WARNING,
|
|
380
|
+
"telegram.ticket_flow.notify_default_failed",
|
|
381
|
+
exc=exc,
|
|
382
|
+
chat_id=self._default_notification_chat_id,
|
|
383
|
+
run_id=run_id,
|
|
384
|
+
seq=seq,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
async def _send_full_dispatch(
|
|
388
|
+
self,
|
|
389
|
+
chat_id: int,
|
|
390
|
+
thread_id: Optional[int],
|
|
391
|
+
*,
|
|
392
|
+
run_id: str,
|
|
393
|
+
seq: str,
|
|
394
|
+
content: str,
|
|
395
|
+
archived_dir: Optional[Path],
|
|
396
|
+
) -> None:
|
|
397
|
+
await self._send_dispatch_text(
|
|
398
|
+
chat_id,
|
|
399
|
+
thread_id,
|
|
400
|
+
run_id=run_id,
|
|
401
|
+
seq=seq,
|
|
402
|
+
content=content,
|
|
403
|
+
)
|
|
404
|
+
if self._pause_config.send_attachments and archived_dir:
|
|
405
|
+
await self._send_dispatch_attachments(
|
|
406
|
+
chat_id,
|
|
407
|
+
thread_id,
|
|
408
|
+
run_id=run_id,
|
|
409
|
+
seq=seq,
|
|
410
|
+
archived_dir=archived_dir,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
async def _send_dispatch_text(
|
|
414
|
+
self,
|
|
415
|
+
chat_id: int,
|
|
416
|
+
thread_id: Optional[int],
|
|
417
|
+
*,
|
|
418
|
+
run_id: str,
|
|
419
|
+
seq: str,
|
|
420
|
+
content: str,
|
|
421
|
+
) -> None:
|
|
422
|
+
body = content.strip() or "(no dispatch message)"
|
|
423
|
+
header = f"Ticket flow paused (run {run_id}). Latest dispatch #{seq}:\n\n"
|
|
424
|
+
footer = "\n\nUse /flow resume to continue."
|
|
425
|
+
full_text = f"{header}{body}{footer}"
|
|
426
|
+
|
|
427
|
+
if self._pause_config.chunk_long_messages:
|
|
428
|
+
chunks = chunk_message(
|
|
429
|
+
full_text,
|
|
430
|
+
max_len=TELEGRAM_MAX_MESSAGE_LENGTH,
|
|
431
|
+
with_numbering=True,
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
chunks = [full_text]
|
|
435
|
+
|
|
436
|
+
for idx, chunk in enumerate(chunks):
|
|
437
|
+
await self._send_message_with_outbox(
|
|
438
|
+
chat_id,
|
|
439
|
+
chunk,
|
|
440
|
+
thread_id=thread_id,
|
|
441
|
+
reply_to=None,
|
|
442
|
+
)
|
|
443
|
+
if idx == 0:
|
|
444
|
+
await asyncio.sleep(0)
|
|
445
|
+
|
|
446
|
+
async def _send_dispatch_attachments(
|
|
447
|
+
self,
|
|
448
|
+
chat_id: int,
|
|
449
|
+
thread_id: Optional[int],
|
|
450
|
+
*,
|
|
451
|
+
run_id: str,
|
|
452
|
+
seq: str,
|
|
453
|
+
archived_dir: Path,
|
|
454
|
+
) -> None:
|
|
455
|
+
try:
|
|
456
|
+
items = sorted(
|
|
457
|
+
[
|
|
458
|
+
child
|
|
459
|
+
for child in archived_dir.iterdir()
|
|
460
|
+
if child.is_file()
|
|
461
|
+
and child.name != "DISPATCH.md"
|
|
462
|
+
and not child.name.startswith(".")
|
|
463
|
+
],
|
|
464
|
+
key=lambda p: p.name,
|
|
465
|
+
)
|
|
466
|
+
except OSError as exc:
|
|
467
|
+
log_event(
|
|
468
|
+
self._logger,
|
|
469
|
+
logging.WARNING,
|
|
470
|
+
"telegram.ticket_flow.attachments_list_failed",
|
|
471
|
+
exc=exc,
|
|
472
|
+
run_id=run_id,
|
|
473
|
+
seq=seq,
|
|
474
|
+
dir=str(archived_dir),
|
|
475
|
+
)
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
for item in items:
|
|
479
|
+
await self._send_single_attachment(
|
|
480
|
+
chat_id,
|
|
481
|
+
thread_id,
|
|
482
|
+
run_id=run_id,
|
|
483
|
+
seq=seq,
|
|
484
|
+
path=item,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
async def _send_single_attachment(
|
|
488
|
+
self,
|
|
489
|
+
chat_id: int,
|
|
490
|
+
thread_id: Optional[int],
|
|
491
|
+
*,
|
|
492
|
+
run_id: str,
|
|
493
|
+
seq: str,
|
|
494
|
+
path: Path,
|
|
495
|
+
) -> None:
|
|
496
|
+
try:
|
|
497
|
+
size = path.stat().st_size
|
|
498
|
+
except OSError:
|
|
499
|
+
size = None
|
|
500
|
+
if size is not None and size > self._pause_config.max_file_size_bytes:
|
|
501
|
+
warning = (
|
|
502
|
+
f"Skipped attachment {path.name} "
|
|
503
|
+
f"({size} bytes > {self._pause_config.max_file_size_bytes} limit)."
|
|
504
|
+
)
|
|
505
|
+
await self._send_message_with_outbox(
|
|
506
|
+
chat_id,
|
|
507
|
+
warning,
|
|
508
|
+
thread_id=thread_id,
|
|
509
|
+
reply_to=None,
|
|
510
|
+
)
|
|
511
|
+
return
|
|
512
|
+
try:
|
|
513
|
+
data = path.read_bytes()
|
|
514
|
+
except OSError as exc:
|
|
515
|
+
log_event(
|
|
516
|
+
self._logger,
|
|
517
|
+
logging.WARNING,
|
|
518
|
+
"telegram.ticket_flow.attachment_read_failed",
|
|
519
|
+
exc=exc,
|
|
520
|
+
file=str(path),
|
|
521
|
+
run_id=run_id,
|
|
522
|
+
seq=seq,
|
|
523
|
+
)
|
|
524
|
+
await self._send_message_with_outbox(
|
|
525
|
+
chat_id,
|
|
526
|
+
f"Failed to read attachment {path.name}.",
|
|
527
|
+
thread_id=thread_id,
|
|
528
|
+
reply_to=None,
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
caption = f"[run {run_id} dispatch #{seq}] {path.name}"
|
|
532
|
+
send_ok = False
|
|
533
|
+
try:
|
|
534
|
+
send_ok = await self._send_document(
|
|
535
|
+
chat_id,
|
|
536
|
+
data,
|
|
537
|
+
filename=path.name,
|
|
538
|
+
thread_id=thread_id,
|
|
539
|
+
reply_to=None,
|
|
540
|
+
caption=caption[:1024],
|
|
541
|
+
)
|
|
542
|
+
if not send_ok:
|
|
543
|
+
log_event(
|
|
544
|
+
self._logger,
|
|
545
|
+
logging.WARNING,
|
|
546
|
+
"telegram.ticket_flow.attachment_send_failed",
|
|
547
|
+
file=str(path),
|
|
548
|
+
run_id=run_id,
|
|
549
|
+
seq=seq,
|
|
550
|
+
)
|
|
551
|
+
except Exception as exc:
|
|
552
|
+
log_event(
|
|
553
|
+
self._logger,
|
|
554
|
+
logging.WARNING,
|
|
555
|
+
"telegram.ticket_flow.attachment_send_failed",
|
|
556
|
+
exc=exc,
|
|
557
|
+
file=str(path),
|
|
558
|
+
run_id=run_id,
|
|
559
|
+
seq=seq,
|
|
560
|
+
)
|
|
561
|
+
if not send_ok:
|
|
562
|
+
await self._send_message_with_outbox(
|
|
563
|
+
chat_id,
|
|
564
|
+
f"Failed to send attachment {path.name}.",
|
|
565
|
+
thread_id=thread_id,
|
|
566
|
+
reply_to=None,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _ticket_controller_for(repo_root: Path) -> FlowController:
|
|
571
|
+
repo_root = repo_root.resolve()
|
|
572
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
573
|
+
artifacts_root = repo_root / ".codex-autorunner" / "flows"
|
|
574
|
+
from ...agents.registry import validate_agent_id
|
|
575
|
+
from ...core.config import load_repo_config
|
|
576
|
+
from ...core.engine import Engine
|
|
577
|
+
from ...integrations.agents.wiring import (
|
|
578
|
+
build_agent_backend_factory,
|
|
579
|
+
build_app_server_supervisor_factory,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
config = load_repo_config(repo_root)
|
|
583
|
+
engine = Engine(
|
|
584
|
+
repo_root,
|
|
585
|
+
config=config,
|
|
586
|
+
backend_factory=build_agent_backend_factory(repo_root, config),
|
|
587
|
+
app_server_supervisor_factory=build_app_server_supervisor_factory(config),
|
|
588
|
+
agent_id_validator=validate_agent_id,
|
|
589
|
+
)
|
|
590
|
+
agent_pool = AgentPool(engine.config)
|
|
591
|
+
definition = build_ticket_flow_definition(agent_pool=agent_pool)
|
|
592
|
+
definition.validate()
|
|
593
|
+
controller = FlowController(
|
|
594
|
+
definition=definition, db_path=db_path, artifacts_root=artifacts_root
|
|
595
|
+
)
|
|
596
|
+
controller.initialize()
|
|
597
|
+
return controller
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _spawn_ticket_worker(repo_root: Path, run_id: str, logger: logging.Logger) -> None:
|
|
601
|
+
try:
|
|
602
|
+
proc, out, err = spawn_flow_worker(repo_root, run_id)
|
|
603
|
+
out.close()
|
|
604
|
+
err.close()
|
|
605
|
+
logger.info("Started ticket_flow worker for %s (pid=%s)", run_id, proc.pid)
|
|
606
|
+
except Exception as exc:
|
|
607
|
+
logger.warning(
|
|
608
|
+
"ticket_flow.worker.spawn_failed",
|
|
609
|
+
exc_info=exc,
|
|
610
|
+
extra={"run_id": run_id},
|
|
611
|
+
)
|