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
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import threading
|
|
4
|
-
from
|
|
2
|
+
from typing import Callable, Optional
|
|
5
3
|
|
|
6
|
-
from .engine import Engine, LockError
|
|
7
4
|
from .locks import DEFAULT_RUNNER_CMD_HINTS, assess_lock, process_alive, read_lock_info
|
|
8
5
|
from .runner_process import build_runner_cmd, spawn_detached
|
|
6
|
+
from .runtime import LockError, RuntimeContext
|
|
9
7
|
from .state import RunnerState, load_state, now_iso, save_state, state_lock
|
|
10
8
|
|
|
11
|
-
SpawnRunnerFn = Callable[[list[str],
|
|
9
|
+
SpawnRunnerFn = Callable[[list[str], RuntimeContext], object]
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
def _spawn_detached(cmd: list[str],
|
|
15
|
-
return spawn_detached(cmd, cwd=
|
|
12
|
+
def _spawn_detached(cmd: list[str], ctx: RuntimeContext) -> object:
|
|
13
|
+
return spawn_detached(cmd, cwd=ctx.repo_root)
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class ProcessRunnerController:
|
|
19
|
-
def __init__(
|
|
20
|
-
self
|
|
17
|
+
def __init__(
|
|
18
|
+
self, ctx: RuntimeContext, *, spawn_fn: Optional[SpawnRunnerFn] = None
|
|
19
|
+
):
|
|
20
|
+
self.ctx = ctx
|
|
21
21
|
self._spawn_fn = spawn_fn or _spawn_detached
|
|
22
22
|
self._lock = threading.Lock()
|
|
23
23
|
|
|
24
24
|
@property
|
|
25
25
|
def running(self) -> bool:
|
|
26
|
-
return self.
|
|
26
|
+
return self.ctx.runner_pid() is not None
|
|
27
27
|
|
|
28
28
|
def reconcile(self) -> None:
|
|
29
29
|
lock_pid = None
|
|
30
|
-
if self.
|
|
31
|
-
info = read_lock_info(self.
|
|
30
|
+
if self.ctx.lock_path.exists():
|
|
31
|
+
info = read_lock_info(self.ctx.lock_path)
|
|
32
32
|
lock_pid = info.pid if info.pid and process_alive(info.pid) else None
|
|
33
33
|
if not lock_pid:
|
|
34
|
-
self.
|
|
34
|
+
self.ctx.lock_path.unlink(missing_ok=True)
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
durable = self.ctx.config.durable_writes
|
|
37
|
+
with state_lock(self.ctx.state_path):
|
|
38
|
+
state = load_state(self.ctx.state_path, durable=durable)
|
|
38
39
|
if lock_pid:
|
|
39
40
|
if state.runner_pid != lock_pid or state.status != "running":
|
|
40
41
|
new_state = RunnerState(
|
|
@@ -53,8 +54,8 @@ class ProcessRunnerController:
|
|
|
53
54
|
sessions=state.sessions,
|
|
54
55
|
repo_to_session=state.repo_to_session,
|
|
55
56
|
)
|
|
56
|
-
save_state(self.
|
|
57
|
-
self.
|
|
57
|
+
save_state(self.ctx.state_path, new_state, durable=durable)
|
|
58
|
+
self.ctx.reconcile_run_index()
|
|
58
59
|
return
|
|
59
60
|
|
|
60
61
|
pid = state.runner_pid
|
|
@@ -84,17 +85,17 @@ class ProcessRunnerController:
|
|
|
84
85
|
sessions=state.sessions,
|
|
85
86
|
repo_to_session=state.repo_to_session,
|
|
86
87
|
)
|
|
87
|
-
save_state(self.
|
|
88
|
+
save_state(self.ctx.state_path, new_state, durable=durable)
|
|
88
89
|
|
|
89
|
-
self.
|
|
90
|
+
self.ctx.reconcile_run_index()
|
|
90
91
|
|
|
91
92
|
def _ensure_unlocked(self) -> None:
|
|
92
|
-
if not self.
|
|
93
|
+
if not self.ctx.lock_path.exists():
|
|
93
94
|
return
|
|
94
95
|
assessment = self._clear_freeable_lock()
|
|
95
96
|
if assessment.freeable:
|
|
96
97
|
return
|
|
97
|
-
info = read_lock_info(self.
|
|
98
|
+
info = read_lock_info(self.ctx.lock_path)
|
|
98
99
|
pid = info.pid
|
|
99
100
|
if pid and process_alive(pid):
|
|
100
101
|
raise LockError(
|
|
@@ -104,14 +105,15 @@ class ProcessRunnerController:
|
|
|
104
105
|
|
|
105
106
|
def _clear_freeable_lock(self):
|
|
106
107
|
assessment = assess_lock(
|
|
107
|
-
self.
|
|
108
|
+
self.ctx.lock_path,
|
|
108
109
|
expected_cmd_substrings=DEFAULT_RUNNER_CMD_HINTS,
|
|
109
110
|
)
|
|
110
111
|
if not assessment.freeable:
|
|
111
112
|
return assessment
|
|
112
|
-
self.
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
self.ctx.lock_path.unlink(missing_ok=True)
|
|
114
|
+
durable = self.ctx.config.durable_writes
|
|
115
|
+
with state_lock(self.ctx.state_path):
|
|
116
|
+
state = load_state(self.ctx.state_path, durable=durable)
|
|
115
117
|
if state.status == "running" or state.runner_pid:
|
|
116
118
|
exit_code = state.last_exit_code
|
|
117
119
|
if exit_code is None:
|
|
@@ -132,7 +134,7 @@ class ProcessRunnerController:
|
|
|
132
134
|
sessions=state.sessions,
|
|
133
135
|
repo_to_session=state.repo_to_session,
|
|
134
136
|
)
|
|
135
|
-
save_state(self.
|
|
137
|
+
save_state(self.ctx.state_path, new_state, durable=durable)
|
|
136
138
|
return assessment
|
|
137
139
|
|
|
138
140
|
def clear_freeable_lock(self):
|
|
@@ -141,17 +143,17 @@ class ProcessRunnerController:
|
|
|
141
143
|
|
|
142
144
|
def _spawn_runner(self, *, action: str, once: bool = False) -> None:
|
|
143
145
|
cmd = build_runner_cmd(
|
|
144
|
-
self.
|
|
146
|
+
self.ctx.repo_root,
|
|
145
147
|
action=action,
|
|
146
148
|
once=once,
|
|
147
149
|
)
|
|
148
|
-
self._spawn_fn(cmd, self.
|
|
150
|
+
self._spawn_fn(cmd, self.ctx)
|
|
149
151
|
|
|
150
152
|
def start(self, once: bool = False) -> None:
|
|
151
153
|
with self._lock:
|
|
152
154
|
self.reconcile()
|
|
153
155
|
self._ensure_unlocked()
|
|
154
|
-
self.
|
|
156
|
+
self.ctx.clear_stop_request()
|
|
155
157
|
action = "once" if once else "run"
|
|
156
158
|
self._spawn_runner(action=action)
|
|
157
159
|
|
|
@@ -159,13 +161,13 @@ class ProcessRunnerController:
|
|
|
159
161
|
with self._lock:
|
|
160
162
|
self.reconcile()
|
|
161
163
|
self._ensure_unlocked()
|
|
162
|
-
self.
|
|
164
|
+
self.ctx.clear_stop_request()
|
|
163
165
|
self._spawn_runner(action="resume", once=once)
|
|
164
166
|
|
|
165
167
|
def stop(self) -> None:
|
|
166
168
|
with self._lock:
|
|
167
|
-
self.
|
|
169
|
+
self.ctx.request_stop()
|
|
168
170
|
|
|
169
|
-
def kill(self) -> int
|
|
171
|
+
def kill(self) -> Optional[int]:
|
|
170
172
|
with self._lock:
|
|
171
|
-
return self.
|
|
173
|
+
return self.ctx.kill_running_process()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Runner state and lock management utilities.
|
|
2
|
+
|
|
3
|
+
This module provides runner state operations extracted from Engine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .locks import (
|
|
12
|
+
DEFAULT_RUNNER_CMD_HINTS,
|
|
13
|
+
FileLock,
|
|
14
|
+
FileLockBusy,
|
|
15
|
+
assess_lock,
|
|
16
|
+
process_alive,
|
|
17
|
+
read_lock_info,
|
|
18
|
+
write_lock_info,
|
|
19
|
+
)
|
|
20
|
+
from .state import load_state, now_iso
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LockError(Exception):
|
|
24
|
+
"""Raised when a runner lock cannot be acquired."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _timestamp() -> str:
|
|
28
|
+
return now_iso()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RunnerStateManager:
|
|
32
|
+
"""Manages runner state and locks for ticket flows."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
repo_root: Path,
|
|
37
|
+
lock_path: Optional[Path] = None,
|
|
38
|
+
state_path: Optional[Path] = None,
|
|
39
|
+
):
|
|
40
|
+
self.repo_root = repo_root
|
|
41
|
+
self.lock_path = lock_path or (repo_root / ".codex-autorunner" / "lock")
|
|
42
|
+
self.state_path = state_path or (
|
|
43
|
+
repo_root / ".codex-autorunner" / "state.sqlite3"
|
|
44
|
+
)
|
|
45
|
+
self.stop_path = repo_root / ".codex-autorunner" / "stop"
|
|
46
|
+
self._lock_handle: Optional[FileLock] = None
|
|
47
|
+
|
|
48
|
+
def acquire_lock(self, force: bool = False) -> None:
|
|
49
|
+
"""Acquire the runner lock."""
|
|
50
|
+
self._lock_handle = FileLock(self.lock_path)
|
|
51
|
+
try:
|
|
52
|
+
self._lock_handle.acquire(blocking=False)
|
|
53
|
+
except FileLockBusy as exc:
|
|
54
|
+
info = read_lock_info(self.lock_path)
|
|
55
|
+
pid = info.pid
|
|
56
|
+
if pid and process_alive(pid):
|
|
57
|
+
raise LockError(
|
|
58
|
+
f"Another autorunner is active (pid={pid}); stop it before continuing"
|
|
59
|
+
) from exc
|
|
60
|
+
raise LockError(
|
|
61
|
+
"Another autorunner is active; stop it before continuing"
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
info = read_lock_info(self.lock_path)
|
|
65
|
+
pid = info.pid
|
|
66
|
+
if pid and process_alive(pid) and not force:
|
|
67
|
+
self._lock_handle.release()
|
|
68
|
+
self._lock_handle = None
|
|
69
|
+
raise LockError(
|
|
70
|
+
f"Another autorunner is active (pid={pid}); use --force to override"
|
|
71
|
+
)
|
|
72
|
+
write_lock_info(
|
|
73
|
+
self.lock_path,
|
|
74
|
+
os.getpid(),
|
|
75
|
+
started_at=_timestamp(),
|
|
76
|
+
lock_file=self._lock_handle.file,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def release_lock(self) -> None:
|
|
80
|
+
"""Release the runner lock."""
|
|
81
|
+
if self._lock_handle is not None:
|
|
82
|
+
self._lock_handle.release()
|
|
83
|
+
self._lock_handle = None
|
|
84
|
+
if self.lock_path.exists():
|
|
85
|
+
self.lock_path.unlink()
|
|
86
|
+
|
|
87
|
+
def repo_busy_reason(self) -> Optional[str]:
|
|
88
|
+
"""Return a reason why the repo is busy, or None if not busy."""
|
|
89
|
+
if self.lock_path.exists():
|
|
90
|
+
assessment = assess_lock(
|
|
91
|
+
self.lock_path,
|
|
92
|
+
expected_cmd_substrings=DEFAULT_RUNNER_CMD_HINTS,
|
|
93
|
+
)
|
|
94
|
+
if assessment.freeable:
|
|
95
|
+
return "Autorunner lock is stale; clear it before continuing."
|
|
96
|
+
pid = assessment.pid
|
|
97
|
+
if pid and process_alive(pid):
|
|
98
|
+
host = f" on {assessment.host}" if assessment.host else ""
|
|
99
|
+
return f"Autorunner is running (pid={pid}{host}); try again later."
|
|
100
|
+
return "Autorunner lock present; clear or resume before continuing."
|
|
101
|
+
|
|
102
|
+
state = load_state(self.state_path)
|
|
103
|
+
if state.status == "running":
|
|
104
|
+
if state.runner_pid and process_alive(state.runner_pid):
|
|
105
|
+
return f"Autorunner is currently running (pid={state.runner_pid}); try again later."
|
|
106
|
+
return "Autorunner state is stale; use 'car resume' to continue."
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def request_stop(self) -> None:
|
|
110
|
+
"""Request a stop by writing to the stop path."""
|
|
111
|
+
self.stop_path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
self.stop_path.write_text(f"{_timestamp()}\n")
|
|
113
|
+
|
|
114
|
+
def clear_stop_request(self) -> None:
|
|
115
|
+
"""Clear a stop request."""
|
|
116
|
+
self.stop_path.unlink(missing_ok=True)
|
|
117
|
+
|
|
118
|
+
def stop_requested(self) -> bool:
|
|
119
|
+
"""Check if a stop has been requested."""
|
|
120
|
+
return self.stop_path.exists()
|
|
121
|
+
|
|
122
|
+
def kill_running_process(self) -> Optional[int]:
|
|
123
|
+
"""Force-kill the process holding the lock, if any. Returns pid if killed."""
|
|
124
|
+
if not self.lock_path.exists():
|
|
125
|
+
return None
|
|
126
|
+
info = read_lock_info(self.lock_path)
|
|
127
|
+
pid = info.pid
|
|
128
|
+
if pid and process_alive(pid):
|
|
129
|
+
try:
|
|
130
|
+
os.kill(pid, signal.SIGTERM)
|
|
131
|
+
return pid
|
|
132
|
+
except OSError:
|
|
133
|
+
return None
|
|
134
|
+
# stale lock
|
|
135
|
+
self.lock_path.unlink(missing_ok=True)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def runner_pid(self) -> Optional[int]:
|
|
139
|
+
"""Get the PID of the running runner."""
|
|
140
|
+
state = load_state(self.state_path)
|
|
141
|
+
pid = state.runner_pid
|
|
142
|
+
if pid and process_alive(pid):
|
|
143
|
+
return pid
|
|
144
|
+
info = read_lock_info(self.lock_path)
|
|
145
|
+
if info.pid and process_alive(info.pid):
|
|
146
|
+
return info.pid
|
|
147
|
+
return None
|