codex-autorunner 1.0.0__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/agents/codex/harness.py +1 -1
- 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/registry.py +22 -3
- codex_autorunner/bootstrap.py +7 -3
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +6 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +11 -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 +197 -3
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/engine.py +1329 -680
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/controller.py +25 -1
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +35 -4
- codex_autorunner/core/flows/store.py +83 -0
- codex_autorunner/core/flows/transition.py +5 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +121 -7
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +11 -3
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +22 -2
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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 +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +91 -9
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/definition.py +9 -2
- codex_autorunner/integrations/agents/__init__.py +9 -19
- 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 +158 -17
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -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/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1203 -66
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +4 -3
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +8 -2
- codex_autorunner/integrations/telegram/handlers/messages.py +1 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +24 -1
- codex_autorunner/integrations/telegram/service.py +15 -10
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +329 -40
- codex_autorunner/integrations/telegram/transport.py +3 -1
- 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 +2 -2
- codex_autorunner/static/agentControls.js +40 -11
- codex_autorunner/static/app.js +11 -3
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/hub.js +112 -94
- codex_autorunner/static/index.html +80 -33
- codex_autorunner/static/messages.js +486 -83
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +125 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +1373 -101
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketEditor.js +99 -5
- codex_autorunner/static/tickets.js +760 -87
- codex_autorunner/static/utils.js +11 -0
- codex_autorunner/static/workspace.js +133 -40
- codex_autorunner/static/workspaceFileBrowser.js +9 -9
- 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 +8 -1
- codex_autorunner/tickets/agent_pool.py +26 -4
- codex_autorunner/tickets/files.py +6 -2
- codex_autorunner/tickets/models.py +3 -1
- codex_autorunner/tickets/outbox.py +12 -0
- codex_autorunner/tickets/runner.py +63 -5
- 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.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- 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.1.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -103,7 +103,31 @@ class FlowController:
|
|
|
103
103
|
cleared = self.store.set_stop_requested(run_id, False)
|
|
104
104
|
if not cleared:
|
|
105
105
|
raise RuntimeError(f"Failed to clear stop flag for run {run_id}")
|
|
106
|
-
|
|
106
|
+
if record.status == FlowRunStatus.COMPLETED:
|
|
107
|
+
return cleared
|
|
108
|
+
state = dict(record.state or {})
|
|
109
|
+
engine = state.get("ticket_engine")
|
|
110
|
+
if isinstance(engine, dict):
|
|
111
|
+
engine = dict(engine)
|
|
112
|
+
engine["status"] = "running"
|
|
113
|
+
engine.pop("reason", None)
|
|
114
|
+
engine.pop("reason_details", None)
|
|
115
|
+
engine.pop("reason_code", None)
|
|
116
|
+
state["ticket_engine"] = engine
|
|
117
|
+
state.pop("reason_summary", None)
|
|
118
|
+
|
|
119
|
+
updated = self.store.update_flow_run_status(
|
|
120
|
+
run_id=run_id,
|
|
121
|
+
status=FlowRunStatus.RUNNING,
|
|
122
|
+
state=state,
|
|
123
|
+
)
|
|
124
|
+
if updated:
|
|
125
|
+
return updated
|
|
126
|
+
|
|
127
|
+
updated = self.store.get_flow_run(run_id)
|
|
128
|
+
if not updated:
|
|
129
|
+
raise RuntimeError(f"Failed to get record for run {run_id}")
|
|
130
|
+
return updated
|
|
107
131
|
|
|
108
132
|
def get_status(self, run_id: str) -> Optional[FlowRunRecord]:
|
|
109
133
|
return self.store.get_flow_run(run_id)
|
|
@@ -32,13 +32,26 @@ class FlowEventType(str, Enum):
|
|
|
32
32
|
STEP_COMPLETED = "step_completed"
|
|
33
33
|
STEP_FAILED = "step_failed"
|
|
34
34
|
AGENT_STREAM_DELTA = "agent_stream_delta"
|
|
35
|
+
AGENT_MESSAGE_COMPLETE = "agent_message_complete"
|
|
36
|
+
AGENT_FAILED = "agent_failed"
|
|
35
37
|
APP_SERVER_EVENT = "app_server_event"
|
|
38
|
+
TOOL_CALL = "tool_call"
|
|
39
|
+
TOOL_RESULT = "tool_result"
|
|
40
|
+
APPROVAL_REQUESTED = "approval_requested"
|
|
36
41
|
TOKEN_USAGE = "token_usage"
|
|
37
42
|
FLOW_STARTED = "flow_started"
|
|
38
43
|
FLOW_STOPPED = "flow_stopped"
|
|
39
44
|
FLOW_RESUMED = "flow_resumed"
|
|
40
45
|
FLOW_COMPLETED = "flow_completed"
|
|
41
46
|
FLOW_FAILED = "flow_failed"
|
|
47
|
+
RUN_STARTED = "run_started"
|
|
48
|
+
RUN_FINISHED = "run_finished"
|
|
49
|
+
RUN_STATE_CHANGED = "run_state_changed"
|
|
50
|
+
RUN_NO_PROGRESS = "run_no_progress"
|
|
51
|
+
PLAN_UPDATED = "plan_updated"
|
|
52
|
+
DIFF_UPDATED = "diff_updated"
|
|
53
|
+
RUN_TIMEOUT = "run_timeout"
|
|
54
|
+
RUN_CANCELLED = "run_cancelled"
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
class FlowRunRecord(BaseModel):
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .models import FlowRunStatus
|
|
6
|
+
|
|
7
|
+
MAX_REASON_SUMMARY_LEN = 120
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _truncate(text: str, max_len: int = MAX_REASON_SUMMARY_LEN) -> str:
|
|
11
|
+
if len(text) <= max_len:
|
|
12
|
+
return text
|
|
13
|
+
return f"{text[:max_len].rstrip()}…"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_reason_summary(
|
|
17
|
+
state: Any,
|
|
18
|
+
*,
|
|
19
|
+
status: FlowRunStatus,
|
|
20
|
+
error_message: Optional[str] = None,
|
|
21
|
+
default: Optional[str] = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Ensure state includes a short reason_summary when stopping/pausing/failing."""
|
|
24
|
+
normalized: dict[str, Any] = dict(state) if isinstance(state, dict) else {}
|
|
25
|
+
existing = normalized.get("reason_summary")
|
|
26
|
+
if isinstance(existing, str) and existing.strip():
|
|
27
|
+
return normalized
|
|
28
|
+
|
|
29
|
+
reason: Optional[str] = None
|
|
30
|
+
engine = normalized.get("ticket_engine")
|
|
31
|
+
if isinstance(engine, dict):
|
|
32
|
+
engine_reason = engine.get("reason")
|
|
33
|
+
if isinstance(engine_reason, str) and engine_reason.strip():
|
|
34
|
+
reason = engine_reason.strip()
|
|
35
|
+
|
|
36
|
+
if not reason and isinstance(error_message, str) and error_message.strip():
|
|
37
|
+
reason = error_message.strip()
|
|
38
|
+
|
|
39
|
+
if not reason:
|
|
40
|
+
if default:
|
|
41
|
+
reason = default
|
|
42
|
+
else:
|
|
43
|
+
fallback = {
|
|
44
|
+
FlowRunStatus.PAUSED: "Paused",
|
|
45
|
+
FlowRunStatus.FAILED: "Failed",
|
|
46
|
+
FlowRunStatus.STOPPED: "Stopped",
|
|
47
|
+
}
|
|
48
|
+
reason = fallback.get(status)
|
|
49
|
+
|
|
50
|
+
if reason:
|
|
51
|
+
normalized["reason_summary"] = _truncate(reason)
|
|
52
|
+
return normalized
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..locks import FileLockBusy, file_lock
|
|
9
|
+
from .models import FlowRunRecord, FlowRunStatus
|
|
10
|
+
from .store import UNSET, FlowStore
|
|
11
|
+
from .transition import resolve_flow_transition
|
|
12
|
+
from .worker_process import FlowWorkerHealth, check_worker_health, clear_worker_metadata
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_ACTIVE_STATUSES = (
|
|
17
|
+
FlowRunStatus.RUNNING,
|
|
18
|
+
FlowRunStatus.STOPPING,
|
|
19
|
+
FlowRunStatus.PAUSED,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FlowReconcileSummary:
|
|
25
|
+
checked: int = 0
|
|
26
|
+
active: int = 0
|
|
27
|
+
updated: int = 0
|
|
28
|
+
locked: int = 0
|
|
29
|
+
errors: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FlowReconcileResult:
|
|
34
|
+
records: list[FlowRunRecord]
|
|
35
|
+
summary: FlowReconcileSummary
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reconcile_lock_path(repo_root: Path, run_id: str) -> Path:
|
|
39
|
+
return repo_root / ".codex-autorunner" / "flows" / run_id / "reconcile.lock"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_worker_not_stale(health: FlowWorkerHealth) -> None:
|
|
43
|
+
if health.status in {"dead", "mismatch", "invalid"}:
|
|
44
|
+
try:
|
|
45
|
+
clear_worker_metadata(health.artifact_path.parent)
|
|
46
|
+
except Exception:
|
|
47
|
+
_logger.debug("Failed to clear worker metadata: %s", health.artifact_path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def reconcile_flow_run(
|
|
51
|
+
repo_root: Path,
|
|
52
|
+
record: FlowRunRecord,
|
|
53
|
+
store: FlowStore,
|
|
54
|
+
*,
|
|
55
|
+
logger: Optional[logging.Logger] = None,
|
|
56
|
+
) -> tuple[FlowRunRecord, bool, bool]:
|
|
57
|
+
if record.status not in _ACTIVE_STATUSES:
|
|
58
|
+
return record, False, False
|
|
59
|
+
|
|
60
|
+
lock_path = _reconcile_lock_path(repo_root, record.id)
|
|
61
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
try:
|
|
63
|
+
with file_lock(lock_path, blocking=False):
|
|
64
|
+
health = check_worker_health(repo_root, record.id)
|
|
65
|
+
decision = resolve_flow_transition(record, health)
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
decision.status == record.status
|
|
69
|
+
and decision.finished_at == record.finished_at
|
|
70
|
+
and decision.state == (record.state or {})
|
|
71
|
+
):
|
|
72
|
+
return record, False, False
|
|
73
|
+
|
|
74
|
+
(logger or _logger).info(
|
|
75
|
+
"Reconciling flow %s: %s -> %s (%s)",
|
|
76
|
+
record.id,
|
|
77
|
+
record.status.value,
|
|
78
|
+
decision.status.value,
|
|
79
|
+
decision.note or "reconcile",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
updated = store.update_flow_run_status(
|
|
83
|
+
run_id=record.id,
|
|
84
|
+
status=decision.status,
|
|
85
|
+
state=decision.state,
|
|
86
|
+
finished_at=decision.finished_at if decision.finished_at else UNSET,
|
|
87
|
+
)
|
|
88
|
+
_ensure_worker_not_stale(health)
|
|
89
|
+
return (updated or record), bool(updated), False
|
|
90
|
+
except FileLockBusy:
|
|
91
|
+
return record, False, True
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
(logger or _logger).warning("Failed to reconcile flow %s: %s", record.id, exc)
|
|
94
|
+
return record, False, False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def reconcile_flow_runs(
|
|
98
|
+
repo_root: Path,
|
|
99
|
+
*,
|
|
100
|
+
flow_type: Optional[str] = None,
|
|
101
|
+
logger: Optional[logging.Logger] = None,
|
|
102
|
+
) -> FlowReconcileResult:
|
|
103
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
104
|
+
if not db_path.exists():
|
|
105
|
+
return FlowReconcileResult(records=[], summary=FlowReconcileSummary())
|
|
106
|
+
store = FlowStore(db_path)
|
|
107
|
+
summary = FlowReconcileSummary()
|
|
108
|
+
records: list[FlowRunRecord] = []
|
|
109
|
+
try:
|
|
110
|
+
store.initialize()
|
|
111
|
+
for record in store.list_flow_runs(flow_type=flow_type):
|
|
112
|
+
if record.status in _ACTIVE_STATUSES:
|
|
113
|
+
summary.active += 1
|
|
114
|
+
summary.checked += 1
|
|
115
|
+
record, updated, locked = reconcile_flow_run(
|
|
116
|
+
repo_root, record, store, logger=logger
|
|
117
|
+
)
|
|
118
|
+
if updated:
|
|
119
|
+
summary.updated += 1
|
|
120
|
+
if locked:
|
|
121
|
+
summary.locked += 1
|
|
122
|
+
records.append(record)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
summary.errors += 1
|
|
125
|
+
(logger or _logger).warning("Flow reconcile run failed: %s", exc)
|
|
126
|
+
finally:
|
|
127
|
+
try:
|
|
128
|
+
store.close()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return FlowReconcileResult(records=records, summary=summary)
|
|
@@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, Optional, Set, cast
|
|
|
5
5
|
|
|
6
6
|
from .definition import FlowDefinition, StepFn, StepFn2, StepFn3
|
|
7
7
|
from .models import FlowEvent, FlowEventType, FlowRunRecord, FlowRunStatus
|
|
8
|
+
from .reasons import ensure_reason_summary
|
|
8
9
|
from .store import FlowStore, now_iso
|
|
9
10
|
|
|
10
11
|
_logger = logging.getLogger(__name__)
|
|
@@ -97,10 +98,16 @@ class FlowRuntime:
|
|
|
97
98
|
if record.stop_requested:
|
|
98
99
|
self._emit(FlowEventType.FLOW_STOPPED, run_id)
|
|
99
100
|
now = now_iso()
|
|
101
|
+
state = ensure_reason_summary(
|
|
102
|
+
dict(record.state or {}),
|
|
103
|
+
status=FlowRunStatus.STOPPED,
|
|
104
|
+
default="Stopped by user",
|
|
105
|
+
)
|
|
100
106
|
updated = self.store.update_flow_run_status(
|
|
101
107
|
run_id=run_id,
|
|
102
108
|
status=FlowRunStatus.STOPPED,
|
|
103
109
|
finished_at=now,
|
|
110
|
+
state=state,
|
|
104
111
|
)
|
|
105
112
|
if not updated:
|
|
106
113
|
raise RuntimeError(f"Failed to stop flow run {run_id}")
|
|
@@ -128,11 +135,17 @@ class FlowRuntime:
|
|
|
128
135
|
data={"error": str(e)},
|
|
129
136
|
)
|
|
130
137
|
now = now_iso()
|
|
138
|
+
state = ensure_reason_summary(
|
|
139
|
+
dict(record.state or {}),
|
|
140
|
+
status=FlowRunStatus.FAILED,
|
|
141
|
+
error_message=str(e),
|
|
142
|
+
)
|
|
131
143
|
updated = self.store.update_flow_run_status(
|
|
132
144
|
run_id=run_id,
|
|
133
145
|
status=FlowRunStatus.FAILED,
|
|
134
146
|
finished_at=now,
|
|
135
147
|
error_message=str(e),
|
|
148
|
+
state=state,
|
|
136
149
|
)
|
|
137
150
|
if not updated:
|
|
138
151
|
raise RuntimeError(
|
|
@@ -267,12 +280,17 @@ class FlowRuntime:
|
|
|
267
280
|
)
|
|
268
281
|
|
|
269
282
|
now = now_iso()
|
|
283
|
+
state = ensure_reason_summary(
|
|
284
|
+
dict(record.state or {}),
|
|
285
|
+
status=FlowRunStatus.FAILED,
|
|
286
|
+
error_message=outcome.error,
|
|
287
|
+
)
|
|
270
288
|
updated = self.store.update_flow_run_status(
|
|
271
289
|
run_id=record.id,
|
|
272
290
|
status=FlowRunStatus.FAILED,
|
|
273
291
|
finished_at=now,
|
|
274
292
|
error_message=outcome.error,
|
|
275
|
-
state=
|
|
293
|
+
state=state,
|
|
276
294
|
current_step=None,
|
|
277
295
|
)
|
|
278
296
|
if not updated:
|
|
@@ -290,11 +308,15 @@ class FlowRuntime:
|
|
|
290
308
|
)
|
|
291
309
|
|
|
292
310
|
now = now_iso()
|
|
311
|
+
state = ensure_reason_summary(
|
|
312
|
+
dict(record.state or {}),
|
|
313
|
+
status=FlowRunStatus.STOPPED,
|
|
314
|
+
)
|
|
293
315
|
updated = self.store.update_flow_run_status(
|
|
294
316
|
run_id=record.id,
|
|
295
317
|
status=FlowRunStatus.STOPPED,
|
|
296
318
|
finished_at=now,
|
|
297
|
-
state=
|
|
319
|
+
state=state,
|
|
298
320
|
current_step=None,
|
|
299
321
|
)
|
|
300
322
|
if not updated:
|
|
@@ -311,10 +333,14 @@ class FlowRuntime:
|
|
|
311
333
|
step_id=step_id,
|
|
312
334
|
)
|
|
313
335
|
|
|
336
|
+
state = ensure_reason_summary(
|
|
337
|
+
dict(record.state or {}),
|
|
338
|
+
status=FlowRunStatus.PAUSED,
|
|
339
|
+
)
|
|
314
340
|
updated = self.store.update_flow_run_status(
|
|
315
341
|
run_id=record.id,
|
|
316
342
|
status=FlowRunStatus.PAUSED,
|
|
317
|
-
state=
|
|
343
|
+
state=state,
|
|
318
344
|
current_step=step_id,
|
|
319
345
|
)
|
|
320
346
|
if not updated:
|
|
@@ -335,12 +361,17 @@ class FlowRuntime:
|
|
|
335
361
|
)
|
|
336
362
|
|
|
337
363
|
now = now_iso()
|
|
364
|
+
state = ensure_reason_summary(
|
|
365
|
+
dict(record.state or {}),
|
|
366
|
+
status=FlowRunStatus.FAILED,
|
|
367
|
+
error_message=str(e),
|
|
368
|
+
)
|
|
338
369
|
updated = self.store.update_flow_run_status(
|
|
339
370
|
run_id=record.id,
|
|
340
371
|
status=FlowRunStatus.FAILED,
|
|
341
372
|
finished_at=now,
|
|
342
373
|
error_message=str(e),
|
|
343
|
-
state=
|
|
374
|
+
state=state,
|
|
344
375
|
current_step=None,
|
|
345
376
|
)
|
|
346
377
|
if not updated:
|
|
@@ -7,6 +7,7 @@ from datetime import datetime, timezone
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Dict, Generator, List, Optional, cast
|
|
9
9
|
|
|
10
|
+
from ..sqlite_utils import SQLITE_PRAGMAS
|
|
10
11
|
from .models import (
|
|
11
12
|
FlowArtifact,
|
|
12
13
|
FlowEvent,
|
|
@@ -42,6 +43,8 @@ class FlowStore:
|
|
|
42
43
|
self.db_path, check_same_thread=False, isolation_level=None
|
|
43
44
|
)
|
|
44
45
|
self._local.conn.row_factory = sqlite3.Row
|
|
46
|
+
for pragma in SQLITE_PRAGMAS:
|
|
47
|
+
self._local.conn.execute(pragma)
|
|
45
48
|
return cast(sqlite3.Connection, self._local.conn)
|
|
46
49
|
|
|
47
50
|
@contextmanager
|
|
@@ -384,6 +387,86 @@ class FlowStore:
|
|
|
384
387
|
rows = conn.execute(query, params).fetchall()
|
|
385
388
|
return [self._row_to_flow_event(row) for row in rows]
|
|
386
389
|
|
|
390
|
+
def get_last_event_meta(self, run_id: str) -> tuple[Optional[int], Optional[str]]:
|
|
391
|
+
conn = self._get_conn()
|
|
392
|
+
row = conn.execute(
|
|
393
|
+
"SELECT seq, timestamp FROM flow_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1",
|
|
394
|
+
(run_id,),
|
|
395
|
+
).fetchone()
|
|
396
|
+
if row is None:
|
|
397
|
+
return None, None
|
|
398
|
+
return row["seq"], row["timestamp"]
|
|
399
|
+
|
|
400
|
+
def get_last_event_seq_by_types(
|
|
401
|
+
self, run_id: str, event_types: list[FlowEventType]
|
|
402
|
+
) -> Optional[int]:
|
|
403
|
+
if not event_types:
|
|
404
|
+
return None
|
|
405
|
+
conn = self._get_conn()
|
|
406
|
+
placeholders = ", ".join("?" for _ in event_types)
|
|
407
|
+
params = [run_id, *[t.value for t in event_types]]
|
|
408
|
+
row = conn.execute(
|
|
409
|
+
f"""
|
|
410
|
+
SELECT seq
|
|
411
|
+
FROM flow_events
|
|
412
|
+
WHERE run_id = ? AND event_type IN ({placeholders})
|
|
413
|
+
ORDER BY seq DESC
|
|
414
|
+
LIMIT 1
|
|
415
|
+
""",
|
|
416
|
+
params,
|
|
417
|
+
).fetchone()
|
|
418
|
+
if row is None:
|
|
419
|
+
return None
|
|
420
|
+
return cast(int, row["seq"])
|
|
421
|
+
|
|
422
|
+
def get_last_event_by_type(
|
|
423
|
+
self, run_id: str, event_type: FlowEventType
|
|
424
|
+
) -> Optional[FlowEvent]:
|
|
425
|
+
conn = self._get_conn()
|
|
426
|
+
row = conn.execute(
|
|
427
|
+
"""
|
|
428
|
+
SELECT *
|
|
429
|
+
FROM flow_events
|
|
430
|
+
WHERE run_id = ? AND event_type = ?
|
|
431
|
+
ORDER BY seq DESC
|
|
432
|
+
LIMIT 1
|
|
433
|
+
""",
|
|
434
|
+
(run_id, event_type.value),
|
|
435
|
+
).fetchone()
|
|
436
|
+
if row is None:
|
|
437
|
+
return None
|
|
438
|
+
return self._row_to_flow_event(row)
|
|
439
|
+
|
|
440
|
+
def get_latest_step_progress_current_ticket(
|
|
441
|
+
self, run_id: str, *, after_seq: Optional[int] = None, limit: int = 50
|
|
442
|
+
) -> Optional[str]:
|
|
443
|
+
"""Return the most recent step_progress.data.current_ticket for a run.
|
|
444
|
+
|
|
445
|
+
This is intentionally lightweight to support UI polling endpoints.
|
|
446
|
+
"""
|
|
447
|
+
conn = self._get_conn()
|
|
448
|
+
query = """
|
|
449
|
+
SELECT seq, data
|
|
450
|
+
FROM flow_events
|
|
451
|
+
WHERE run_id = ? AND event_type = ?
|
|
452
|
+
"""
|
|
453
|
+
params: List[Any] = [run_id, FlowEventType.STEP_PROGRESS.value]
|
|
454
|
+
if after_seq is not None:
|
|
455
|
+
query += " AND seq > ?"
|
|
456
|
+
params.append(after_seq)
|
|
457
|
+
query += " ORDER BY seq DESC LIMIT ?"
|
|
458
|
+
params.append(limit)
|
|
459
|
+
rows = conn.execute(query, params).fetchall()
|
|
460
|
+
for row in rows:
|
|
461
|
+
try:
|
|
462
|
+
data = json.loads(row["data"] or "{}")
|
|
463
|
+
except Exception:
|
|
464
|
+
data = {}
|
|
465
|
+
current_ticket = data.get("current_ticket")
|
|
466
|
+
if isinstance(current_ticket, str) and current_ticket.strip():
|
|
467
|
+
return current_ticket.strip()
|
|
468
|
+
return None
|
|
469
|
+
|
|
387
470
|
def create_artifact(
|
|
388
471
|
self,
|
|
389
472
|
artifact_id: str,
|
|
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
from codex_autorunner.core.flows.models import FlowRunRecord, FlowRunStatus
|
|
7
|
+
from codex_autorunner.core.flows.reasons import ensure_reason_summary
|
|
7
8
|
from codex_autorunner.core.flows.store import now_iso
|
|
8
9
|
|
|
9
10
|
|
|
@@ -54,6 +55,7 @@ def resolve_flow_transition(
|
|
|
54
55
|
if record.status == FlowRunStatus.STOPPING
|
|
55
56
|
else FlowRunStatus.FAILED
|
|
56
57
|
)
|
|
58
|
+
state = ensure_reason_summary(state, status=new_status, default="Worker died")
|
|
57
59
|
return TransitionDecision(
|
|
58
60
|
status=new_status, finished_at=now, state=state, note="worker-dead"
|
|
59
61
|
)
|
|
@@ -61,6 +63,7 @@ def resolve_flow_transition(
|
|
|
61
63
|
# 2) Inner engine reconciliation (worker is alive or not required).
|
|
62
64
|
if record.status == FlowRunStatus.RUNNING:
|
|
63
65
|
if inner_status == "paused":
|
|
66
|
+
state = ensure_reason_summary(state, status=FlowRunStatus.PAUSED)
|
|
64
67
|
return TransitionDecision(
|
|
65
68
|
status=FlowRunStatus.PAUSED,
|
|
66
69
|
finished_at=None,
|
|
@@ -98,6 +101,7 @@ def resolve_flow_transition(
|
|
|
98
101
|
engine.pop("reason", None)
|
|
99
102
|
engine.pop("reason_details", None)
|
|
100
103
|
engine.pop("reason_code", None)
|
|
104
|
+
state.pop("reason_summary", None)
|
|
101
105
|
engine["status"] = "running"
|
|
102
106
|
state["ticket_engine"] = engine
|
|
103
107
|
return TransitionDecision(
|
|
@@ -115,6 +119,7 @@ def resolve_flow_transition(
|
|
|
115
119
|
note="paused-worker-dead",
|
|
116
120
|
)
|
|
117
121
|
|
|
122
|
+
state = ensure_reason_summary(state, status=FlowRunStatus.PAUSED)
|
|
118
123
|
return TransitionDecision(
|
|
119
124
|
status=FlowRunStatus.PAUSED, finished_at=None, state=state, note="paused"
|
|
120
125
|
)
|