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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from .config import load_repo_config
|
|
11
|
+
from .lifecycle_events import LifecycleEvent, LifecycleEventType
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class DispatchInterceptRule:
|
|
18
|
+
pattern: str
|
|
19
|
+
reply: str
|
|
20
|
+
description: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class InterceptionResult:
|
|
25
|
+
action: str
|
|
26
|
+
reason: str
|
|
27
|
+
reply: Optional[str] = None
|
|
28
|
+
escalated: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def default_dispatch_rules() -> list[DispatchInterceptRule]:
|
|
32
|
+
return [
|
|
33
|
+
DispatchInterceptRule(
|
|
34
|
+
pattern=r"(?i)^(proceed|continue|resume|ok|yes|go ahead)([\s!.]*)?$",
|
|
35
|
+
reply="Continuing with the current task.",
|
|
36
|
+
description="Simple continuation confirmation",
|
|
37
|
+
),
|
|
38
|
+
DispatchInterceptRule(
|
|
39
|
+
pattern=r"(?i)^skip.*(?:pre.?commit|commit.*hook|hook)([\s!.]*)?$",
|
|
40
|
+
reply="Skipping pre-commit hooks and proceeding.",
|
|
41
|
+
description="Skip pre-commit hooks",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _match_dispatch(
|
|
47
|
+
body: str, rules: list[DispatchInterceptRule]
|
|
48
|
+
) -> Optional[DispatchInterceptRule]:
|
|
49
|
+
body_stripped = body.strip()
|
|
50
|
+
for rule in rules:
|
|
51
|
+
try:
|
|
52
|
+
if re.match(rule.pattern, body_stripped):
|
|
53
|
+
return rule
|
|
54
|
+
except re.error:
|
|
55
|
+
logger.warning("Invalid dispatch pattern: %s", rule.pattern)
|
|
56
|
+
continue
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _can_auto_resume_run(repo_root: Path, run_id: str) -> bool:
|
|
61
|
+
from .flows.models import FlowRunStatus
|
|
62
|
+
from .flows.store import FlowStore
|
|
63
|
+
|
|
64
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
65
|
+
if not db_path.exists():
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
config = load_repo_config(repo_root)
|
|
69
|
+
with FlowStore(db_path, durable=config.durable_writes) as store:
|
|
70
|
+
run = store.get_flow_run(run_id)
|
|
71
|
+
if not run:
|
|
72
|
+
return False
|
|
73
|
+
return run.status == FlowRunStatus.PAUSED
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.warning("Failed to check run status for %s: %s", run_id, exc)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _write_reply(repo_root: Path, run_id: str, reply_body: str) -> bool:
|
|
80
|
+
from ..tickets.replies import ensure_reply_dirs, resolve_reply_paths
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
config = load_repo_config(repo_root)
|
|
84
|
+
runs_dir = Path(config.raw.get("runs_dir") or ".codex-autorunner/runs")
|
|
85
|
+
workspace_root = repo_root
|
|
86
|
+
|
|
87
|
+
reply_paths = resolve_reply_paths(
|
|
88
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
|
|
89
|
+
)
|
|
90
|
+
ensure_reply_dirs(reply_paths)
|
|
91
|
+
|
|
92
|
+
reply_history_dir = reply_paths.reply_history_dir
|
|
93
|
+
if not reply_history_dir:
|
|
94
|
+
logger.warning("Reply history dir not found for run %s", run_id)
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
next_seq = 1
|
|
98
|
+
for child in reply_history_dir.iterdir():
|
|
99
|
+
if child.is_dir() and child.name.isdigit():
|
|
100
|
+
next_seq = max(next_seq, int(child.name) + 1)
|
|
101
|
+
|
|
102
|
+
seq_dir = reply_history_dir / f"{next_seq:04d}"
|
|
103
|
+
seq_dir.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
reply_content = f"""---
|
|
106
|
+
title: Auto-resolved by PMA
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
{reply_body}
|
|
110
|
+
"""
|
|
111
|
+
reply_path = seq_dir / "USER_REPLY.md"
|
|
112
|
+
reply_path.write_text(reply_content, encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
except Exception:
|
|
117
|
+
logger.exception("Failed to write reply for run %s", run_id)
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PmaDispatchInterceptor:
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
hub_root: Path,
|
|
125
|
+
supervisor: Any,
|
|
126
|
+
rules: Optional[list[DispatchInterceptRule]] = None,
|
|
127
|
+
on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
|
|
128
|
+
):
|
|
129
|
+
self._hub_root = hub_root
|
|
130
|
+
self._supervisor = supervisor
|
|
131
|
+
self._rules = rules or default_dispatch_rules()
|
|
132
|
+
self._on_intercept = on_intercept
|
|
133
|
+
self._processing = set[str]()
|
|
134
|
+
self._logger = logging.getLogger(f"{__name__}.PmaDispatchInterceptor")
|
|
135
|
+
|
|
136
|
+
async def process_dispatch_event(
|
|
137
|
+
self, event: LifecycleEvent, repo_root: Path
|
|
138
|
+
) -> Optional[InterceptionResult]:
|
|
139
|
+
if event.event_type != LifecycleEventType.DISPATCH_CREATED:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
event_id = event.event_id
|
|
143
|
+
if event_id in self._processing:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
run_id = event.run_id
|
|
147
|
+
repo_id = event.data.get("repo_id") or event.repo_id
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
self._processing.add(event_id)
|
|
151
|
+
|
|
152
|
+
self._logger.info(
|
|
153
|
+
"Processing dispatch event %s for run %s in repo %s",
|
|
154
|
+
event_id,
|
|
155
|
+
run_id,
|
|
156
|
+
repo_id,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
from .pma_context import _latest_dispatch
|
|
160
|
+
|
|
161
|
+
latest = _latest_dispatch(repo_root, run_id, {}, max_text_chars=10000)
|
|
162
|
+
if not latest or not latest.get("dispatch"):
|
|
163
|
+
return InterceptionResult(
|
|
164
|
+
action="ignore",
|
|
165
|
+
reason="No dispatch found or dispatch is invalid",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
dispatch_info = latest["dispatch"]
|
|
169
|
+
mode = dispatch_info.get("mode")
|
|
170
|
+
body = dispatch_info.get("body", "")
|
|
171
|
+
|
|
172
|
+
if mode != "pause":
|
|
173
|
+
return InterceptionResult(
|
|
174
|
+
action="ignore",
|
|
175
|
+
reason=f"Dispatch mode is '{mode}', only 'pause' is intercepted",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
matched_rule = _match_dispatch(body, self._rules)
|
|
179
|
+
if matched_rule:
|
|
180
|
+
self._logger.info(
|
|
181
|
+
"Dispatch matched auto-resolve rule: %s",
|
|
182
|
+
matched_rule.description,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
can_resume = await _can_auto_resume_run(repo_root, run_id)
|
|
186
|
+
if not can_resume:
|
|
187
|
+
return InterceptionResult(
|
|
188
|
+
action="escalate",
|
|
189
|
+
reason="Run cannot be auto-resumed",
|
|
190
|
+
escalated=True,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
success = await _write_reply(repo_root, run_id, matched_rule.reply)
|
|
194
|
+
if success:
|
|
195
|
+
result = InterceptionResult(
|
|
196
|
+
action="auto_resolved",
|
|
197
|
+
reason=matched_rule.description,
|
|
198
|
+
reply=matched_rule.reply,
|
|
199
|
+
)
|
|
200
|
+
if self._on_intercept:
|
|
201
|
+
self._on_intercept(event_id, result)
|
|
202
|
+
return result
|
|
203
|
+
else:
|
|
204
|
+
return InterceptionResult(
|
|
205
|
+
action="escalate",
|
|
206
|
+
reason="Failed to write reply",
|
|
207
|
+
escalated=True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return InterceptionResult(
|
|
211
|
+
action="escalate",
|
|
212
|
+
reason="Dispatch does not match any auto-resolve rule",
|
|
213
|
+
escalated=True,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
self._logger.exception(
|
|
218
|
+
"Error processing dispatch event %s for run %s", event_id, run_id
|
|
219
|
+
)
|
|
220
|
+
return InterceptionResult(
|
|
221
|
+
action="error",
|
|
222
|
+
reason=f"Processing error: {exc}",
|
|
223
|
+
)
|
|
224
|
+
finally:
|
|
225
|
+
self._processing.discard(event_id)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def run_dispatch_interceptor(
|
|
229
|
+
hub_root: Path,
|
|
230
|
+
supervisor: Any,
|
|
231
|
+
interval_seconds: float = 5.0,
|
|
232
|
+
on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
|
|
233
|
+
) -> asyncio.Task[None]:
|
|
234
|
+
interceptor = PmaDispatchInterceptor(
|
|
235
|
+
hub_root=hub_root,
|
|
236
|
+
supervisor=supervisor,
|
|
237
|
+
on_intercept=on_intercept,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def loop() -> None:
|
|
241
|
+
while True:
|
|
242
|
+
try:
|
|
243
|
+
lifecycle_store = supervisor.lifecycle_store
|
|
244
|
+
unprocessed = lifecycle_store.get_unprocessed(limit=50)
|
|
245
|
+
|
|
246
|
+
for event in unprocessed:
|
|
247
|
+
if event.event_type != LifecycleEventType.DISPATCH_CREATED:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
repo_id = event.repo_id
|
|
251
|
+
snapshots = supervisor.list_repos()
|
|
252
|
+
repo_snapshot = None
|
|
253
|
+
for snap in snapshots:
|
|
254
|
+
if snap.id == repo_id:
|
|
255
|
+
repo_snapshot = snap
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
if not repo_snapshot or not repo_snapshot.exists_on_disk:
|
|
259
|
+
lifecycle_store.mark_processed(event.event_id)
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
result = await interceptor.process_dispatch_event(
|
|
263
|
+
event, repo_snapshot.path
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if result and result.action != "ignore":
|
|
267
|
+
lifecycle_store.mark_processed(event.event_id)
|
|
268
|
+
|
|
269
|
+
await asyncio.sleep(interval_seconds)
|
|
270
|
+
except asyncio.CancelledError:
|
|
271
|
+
break
|
|
272
|
+
except Exception:
|
|
273
|
+
logger.exception("Dispatch interceptor loop error")
|
|
274
|
+
|
|
275
|
+
return asyncio.create_task(loop())
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
"DispatchInterceptRule",
|
|
280
|
+
"InterceptionResult",
|
|
281
|
+
"default_dispatch_rules",
|
|
282
|
+
"PmaDispatchInterceptor",
|
|
283
|
+
"run_dispatch_interceptor",
|
|
284
|
+
]
|