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,568 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sqlite3
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Generator, List, Optional, cast
|
|
9
|
+
|
|
10
|
+
from ..sqlite_utils import SQLITE_PRAGMAS
|
|
11
|
+
from .models import (
|
|
12
|
+
FlowArtifact,
|
|
13
|
+
FlowEvent,
|
|
14
|
+
FlowEventType,
|
|
15
|
+
FlowRunRecord,
|
|
16
|
+
FlowRunStatus,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
SCHEMA_VERSION = 2
|
|
22
|
+
UNSET = object()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def now_iso() -> str:
|
|
26
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FlowStore:
|
|
30
|
+
def __init__(self, db_path: Path):
|
|
31
|
+
self.db_path = db_path
|
|
32
|
+
self._local: threading.local = threading.local()
|
|
33
|
+
|
|
34
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
35
|
+
if not hasattr(self._local, "conn"):
|
|
36
|
+
# Ensure parent directory exists so sqlite can create/open the file.
|
|
37
|
+
try:
|
|
38
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
except Exception:
|
|
40
|
+
# Let sqlite raise a clearer error below if directory creation failed.
|
|
41
|
+
pass
|
|
42
|
+
self._local.conn = sqlite3.connect(
|
|
43
|
+
self.db_path, check_same_thread=False, isolation_level=None
|
|
44
|
+
)
|
|
45
|
+
self._local.conn.row_factory = sqlite3.Row
|
|
46
|
+
for pragma in SQLITE_PRAGMAS:
|
|
47
|
+
self._local.conn.execute(pragma)
|
|
48
|
+
return cast(sqlite3.Connection, self._local.conn)
|
|
49
|
+
|
|
50
|
+
@contextmanager
|
|
51
|
+
def transaction(self) -> Generator[sqlite3.Connection, None, None]:
|
|
52
|
+
conn = self._get_conn()
|
|
53
|
+
try:
|
|
54
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
55
|
+
yield conn
|
|
56
|
+
conn.commit()
|
|
57
|
+
except Exception:
|
|
58
|
+
conn.rollback()
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
def initialize(self) -> None:
|
|
62
|
+
with self.transaction() as conn:
|
|
63
|
+
self._create_schema(conn)
|
|
64
|
+
self._ensure_schema_version(conn)
|
|
65
|
+
|
|
66
|
+
def _create_schema(self, conn: sqlite3.Connection) -> None:
|
|
67
|
+
conn.execute(
|
|
68
|
+
"""
|
|
69
|
+
CREATE TABLE IF NOT EXISTS schema_info (
|
|
70
|
+
version INTEGER NOT NULL PRIMARY KEY
|
|
71
|
+
)
|
|
72
|
+
"""
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
conn.execute(
|
|
76
|
+
"""
|
|
77
|
+
CREATE TABLE IF NOT EXISTS flow_runs (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
flow_type TEXT NOT NULL,
|
|
80
|
+
status TEXT NOT NULL,
|
|
81
|
+
input_data TEXT NOT NULL,
|
|
82
|
+
state TEXT NOT NULL,
|
|
83
|
+
current_step TEXT,
|
|
84
|
+
stop_requested INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
created_at TEXT NOT NULL,
|
|
86
|
+
started_at TEXT,
|
|
87
|
+
finished_at TEXT,
|
|
88
|
+
error_message TEXT,
|
|
89
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
90
|
+
)
|
|
91
|
+
"""
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
conn.execute(
|
|
95
|
+
"""
|
|
96
|
+
CREATE TABLE IF NOT EXISTS flow_events (
|
|
97
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
id TEXT NOT NULL UNIQUE,
|
|
99
|
+
run_id TEXT NOT NULL,
|
|
100
|
+
event_type TEXT NOT NULL,
|
|
101
|
+
timestamp TEXT NOT NULL,
|
|
102
|
+
data TEXT NOT NULL,
|
|
103
|
+
step_id TEXT,
|
|
104
|
+
FOREIGN KEY (run_id) REFERENCES flow_runs(id) ON DELETE CASCADE
|
|
105
|
+
)
|
|
106
|
+
"""
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
conn.execute(
|
|
110
|
+
"""
|
|
111
|
+
CREATE TABLE IF NOT EXISTS flow_artifacts (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
run_id TEXT NOT NULL,
|
|
114
|
+
kind TEXT NOT NULL,
|
|
115
|
+
path TEXT NOT NULL,
|
|
116
|
+
created_at TEXT NOT NULL,
|
|
117
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
118
|
+
FOREIGN KEY (run_id) REFERENCES flow_runs(id) ON DELETE CASCADE
|
|
119
|
+
)
|
|
120
|
+
"""
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
conn.execute(
|
|
124
|
+
"CREATE INDEX IF NOT EXISTS idx_flow_runs_status ON flow_runs(status)"
|
|
125
|
+
)
|
|
126
|
+
conn.execute(
|
|
127
|
+
"CREATE INDEX IF NOT EXISTS idx_flow_events_run_id ON flow_events(run_id, seq)"
|
|
128
|
+
)
|
|
129
|
+
conn.execute(
|
|
130
|
+
"CREATE INDEX IF NOT EXISTS idx_flow_artifacts_run_id ON flow_artifacts(run_id)"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _ensure_schema_version(self, conn: sqlite3.Connection) -> None:
|
|
134
|
+
result = conn.execute("SELECT version FROM schema_info").fetchone()
|
|
135
|
+
if result is None:
|
|
136
|
+
conn.execute(
|
|
137
|
+
"INSERT INTO schema_info (version) VALUES (?)", (SCHEMA_VERSION,)
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
current_version = result[0]
|
|
141
|
+
if current_version < SCHEMA_VERSION:
|
|
142
|
+
self._migrate_schema(conn, current_version, SCHEMA_VERSION)
|
|
143
|
+
|
|
144
|
+
def _migrate_schema(
|
|
145
|
+
self, conn: sqlite3.Connection, from_version: int, to_version: int
|
|
146
|
+
) -> None:
|
|
147
|
+
_logger.info("Migrating schema from version %d to %d", from_version, to_version)
|
|
148
|
+
for version in range(from_version, to_version):
|
|
149
|
+
self._apply_migration(conn, version + 1)
|
|
150
|
+
conn.execute("UPDATE schema_info SET version = ?", (to_version,))
|
|
151
|
+
|
|
152
|
+
def _apply_migration(self, conn: sqlite3.Connection, version: int) -> None:
|
|
153
|
+
if version == 1:
|
|
154
|
+
pass
|
|
155
|
+
elif version == 2:
|
|
156
|
+
conn.execute("ALTER TABLE flow_events RENAME TO flow_events_old")
|
|
157
|
+
conn.execute(
|
|
158
|
+
"""
|
|
159
|
+
CREATE TABLE flow_events (
|
|
160
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
161
|
+
id TEXT NOT NULL UNIQUE,
|
|
162
|
+
run_id TEXT NOT NULL,
|
|
163
|
+
event_type TEXT NOT NULL,
|
|
164
|
+
timestamp TEXT NOT NULL,
|
|
165
|
+
data TEXT NOT NULL,
|
|
166
|
+
step_id TEXT,
|
|
167
|
+
FOREIGN KEY (run_id) REFERENCES flow_runs(id) ON DELETE CASCADE
|
|
168
|
+
)
|
|
169
|
+
"""
|
|
170
|
+
)
|
|
171
|
+
conn.execute(
|
|
172
|
+
"""
|
|
173
|
+
INSERT INTO flow_events (id, run_id, event_type, timestamp, data, step_id)
|
|
174
|
+
SELECT id, run_id, event_type, timestamp, data, step_id
|
|
175
|
+
FROM flow_events_old
|
|
176
|
+
ORDER BY timestamp ASC
|
|
177
|
+
"""
|
|
178
|
+
)
|
|
179
|
+
conn.execute("DROP TABLE flow_events_old")
|
|
180
|
+
conn.execute(
|
|
181
|
+
"CREATE INDEX IF NOT EXISTS idx_flow_events_run_id ON flow_events(run_id, seq)"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def create_flow_run(
|
|
185
|
+
self,
|
|
186
|
+
run_id: str,
|
|
187
|
+
flow_type: str,
|
|
188
|
+
input_data: Dict[str, Any],
|
|
189
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
190
|
+
state: Optional[Dict[str, Any]] = None,
|
|
191
|
+
current_step: Optional[str] = None,
|
|
192
|
+
) -> FlowRunRecord:
|
|
193
|
+
now = now_iso()
|
|
194
|
+
record = FlowRunRecord(
|
|
195
|
+
id=run_id,
|
|
196
|
+
flow_type=flow_type,
|
|
197
|
+
status=FlowRunStatus.PENDING,
|
|
198
|
+
input_data=input_data,
|
|
199
|
+
state=state or {},
|
|
200
|
+
current_step=current_step,
|
|
201
|
+
stop_requested=False,
|
|
202
|
+
created_at=now,
|
|
203
|
+
metadata=metadata or {},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
with self.transaction() as conn:
|
|
207
|
+
conn.execute(
|
|
208
|
+
"""
|
|
209
|
+
INSERT INTO flow_runs (
|
|
210
|
+
id, flow_type, status, input_data, state, current_step,
|
|
211
|
+
stop_requested, created_at, metadata
|
|
212
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
213
|
+
""",
|
|
214
|
+
(
|
|
215
|
+
record.id,
|
|
216
|
+
record.flow_type,
|
|
217
|
+
record.status.value,
|
|
218
|
+
json.dumps(record.input_data),
|
|
219
|
+
json.dumps(record.state),
|
|
220
|
+
record.current_step,
|
|
221
|
+
1 if record.stop_requested else 0,
|
|
222
|
+
record.created_at,
|
|
223
|
+
json.dumps(record.metadata),
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return record
|
|
228
|
+
|
|
229
|
+
def get_flow_run(self, run_id: str) -> Optional[FlowRunRecord]:
|
|
230
|
+
conn = self._get_conn()
|
|
231
|
+
row = conn.execute("SELECT * FROM flow_runs WHERE id = ?", (run_id,)).fetchone()
|
|
232
|
+
if row is None:
|
|
233
|
+
return None
|
|
234
|
+
return self._row_to_flow_run(row)
|
|
235
|
+
|
|
236
|
+
def update_flow_run_status(
|
|
237
|
+
self,
|
|
238
|
+
run_id: str,
|
|
239
|
+
status: FlowRunStatus,
|
|
240
|
+
current_step: Any = UNSET,
|
|
241
|
+
state: Any = UNSET,
|
|
242
|
+
started_at: Any = UNSET,
|
|
243
|
+
finished_at: Any = UNSET,
|
|
244
|
+
error_message: Any = UNSET,
|
|
245
|
+
) -> Optional[FlowRunRecord]:
|
|
246
|
+
updates = ["status = ?"]
|
|
247
|
+
params: List[Any] = [status.value]
|
|
248
|
+
|
|
249
|
+
if current_step is not UNSET:
|
|
250
|
+
updates.append("current_step = ?")
|
|
251
|
+
params.append(current_step)
|
|
252
|
+
|
|
253
|
+
if state is not UNSET:
|
|
254
|
+
updates.append("state = ?")
|
|
255
|
+
params.append(json.dumps(state))
|
|
256
|
+
|
|
257
|
+
if started_at is not UNSET:
|
|
258
|
+
updates.append("started_at = ?")
|
|
259
|
+
params.append(started_at)
|
|
260
|
+
|
|
261
|
+
if finished_at is not UNSET:
|
|
262
|
+
updates.append("finished_at = ?")
|
|
263
|
+
params.append(finished_at)
|
|
264
|
+
|
|
265
|
+
if error_message is not UNSET:
|
|
266
|
+
updates.append("error_message = ?")
|
|
267
|
+
params.append(error_message)
|
|
268
|
+
|
|
269
|
+
params.append(run_id)
|
|
270
|
+
|
|
271
|
+
with self.transaction() as conn:
|
|
272
|
+
conn.execute(
|
|
273
|
+
f"UPDATE flow_runs SET {', '.join(updates)} WHERE id = ?",
|
|
274
|
+
params,
|
|
275
|
+
)
|
|
276
|
+
row = conn.execute(
|
|
277
|
+
"SELECT * FROM flow_runs WHERE id = ?", (run_id,)
|
|
278
|
+
).fetchone()
|
|
279
|
+
if row is None:
|
|
280
|
+
return None
|
|
281
|
+
return self._row_to_flow_run(row)
|
|
282
|
+
|
|
283
|
+
def set_stop_requested(
|
|
284
|
+
self, run_id: str, stop_requested: bool
|
|
285
|
+
) -> Optional[FlowRunRecord]:
|
|
286
|
+
with self.transaction() as conn:
|
|
287
|
+
conn.execute(
|
|
288
|
+
"UPDATE flow_runs SET stop_requested = ? WHERE id = ?",
|
|
289
|
+
(1 if stop_requested else 0, run_id),
|
|
290
|
+
)
|
|
291
|
+
row = conn.execute(
|
|
292
|
+
"SELECT * FROM flow_runs WHERE id = ?", (run_id,)
|
|
293
|
+
).fetchone()
|
|
294
|
+
if row is None:
|
|
295
|
+
return None
|
|
296
|
+
return self._row_to_flow_run(row)
|
|
297
|
+
|
|
298
|
+
def update_current_step(
|
|
299
|
+
self, run_id: str, current_step: str
|
|
300
|
+
) -> Optional[FlowRunRecord]:
|
|
301
|
+
with self.transaction() as conn:
|
|
302
|
+
conn.execute(
|
|
303
|
+
"UPDATE flow_runs SET current_step = ? WHERE id = ?",
|
|
304
|
+
(current_step, run_id),
|
|
305
|
+
)
|
|
306
|
+
row = conn.execute(
|
|
307
|
+
"SELECT * FROM flow_runs WHERE id = ?", (run_id,)
|
|
308
|
+
).fetchone()
|
|
309
|
+
if row is None:
|
|
310
|
+
return None
|
|
311
|
+
return self._row_to_flow_run(row)
|
|
312
|
+
|
|
313
|
+
def list_flow_runs(
|
|
314
|
+
self, flow_type: Optional[str] = None, status: Optional[FlowRunStatus] = None
|
|
315
|
+
) -> List[FlowRunRecord]:
|
|
316
|
+
conn = self._get_conn()
|
|
317
|
+
query = "SELECT * FROM flow_runs WHERE 1=1"
|
|
318
|
+
params: List[Any] = []
|
|
319
|
+
|
|
320
|
+
if flow_type is not None:
|
|
321
|
+
query += " AND flow_type = ?"
|
|
322
|
+
params.append(flow_type)
|
|
323
|
+
|
|
324
|
+
if status is not None:
|
|
325
|
+
query += " AND status = ?"
|
|
326
|
+
params.append(status.value)
|
|
327
|
+
|
|
328
|
+
query += " ORDER BY created_at DESC"
|
|
329
|
+
|
|
330
|
+
rows = conn.execute(query, params).fetchall()
|
|
331
|
+
return [self._row_to_flow_run(row) for row in rows]
|
|
332
|
+
|
|
333
|
+
def create_event(
|
|
334
|
+
self,
|
|
335
|
+
event_id: str,
|
|
336
|
+
run_id: str,
|
|
337
|
+
event_type: FlowEventType,
|
|
338
|
+
data: Optional[Dict[str, Any]] = None,
|
|
339
|
+
step_id: Optional[str] = None,
|
|
340
|
+
) -> FlowEvent:
|
|
341
|
+
timestamp = now_iso()
|
|
342
|
+
|
|
343
|
+
with self.transaction() as conn:
|
|
344
|
+
conn.execute(
|
|
345
|
+
"""
|
|
346
|
+
INSERT INTO flow_events (id, run_id, event_type, timestamp, data, step_id)
|
|
347
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
348
|
+
""",
|
|
349
|
+
(
|
|
350
|
+
event_id,
|
|
351
|
+
run_id,
|
|
352
|
+
event_type.value,
|
|
353
|
+
timestamp,
|
|
354
|
+
json.dumps(data or {}),
|
|
355
|
+
step_id,
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
row = conn.execute(
|
|
359
|
+
"SELECT * FROM flow_events WHERE id = ?", (event_id,)
|
|
360
|
+
).fetchone()
|
|
361
|
+
|
|
362
|
+
if row is None:
|
|
363
|
+
raise RuntimeError("Failed to persist flow event")
|
|
364
|
+
|
|
365
|
+
return self._row_to_flow_event(row)
|
|
366
|
+
|
|
367
|
+
def get_events(
|
|
368
|
+
self,
|
|
369
|
+
run_id: str,
|
|
370
|
+
after_seq: Optional[int] = None,
|
|
371
|
+
limit: Optional[int] = None,
|
|
372
|
+
) -> List[FlowEvent]:
|
|
373
|
+
conn = self._get_conn()
|
|
374
|
+
query = "SELECT * FROM flow_events WHERE run_id = ?"
|
|
375
|
+
params: List[Any] = [run_id]
|
|
376
|
+
|
|
377
|
+
if after_seq is not None:
|
|
378
|
+
query += " AND seq > ?"
|
|
379
|
+
params.append(after_seq)
|
|
380
|
+
|
|
381
|
+
query += " ORDER BY seq ASC"
|
|
382
|
+
|
|
383
|
+
if limit is not None:
|
|
384
|
+
query += " LIMIT ?"
|
|
385
|
+
params.append(limit)
|
|
386
|
+
|
|
387
|
+
rows = conn.execute(query, params).fetchall()
|
|
388
|
+
return [self._row_to_flow_event(row) for row in rows]
|
|
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
|
+
|
|
470
|
+
def create_artifact(
|
|
471
|
+
self,
|
|
472
|
+
artifact_id: str,
|
|
473
|
+
run_id: str,
|
|
474
|
+
kind: str,
|
|
475
|
+
path: str,
|
|
476
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
477
|
+
) -> FlowArtifact:
|
|
478
|
+
artifact = FlowArtifact(
|
|
479
|
+
id=artifact_id,
|
|
480
|
+
run_id=run_id,
|
|
481
|
+
kind=kind,
|
|
482
|
+
path=path,
|
|
483
|
+
created_at=now_iso(),
|
|
484
|
+
metadata=metadata or {},
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
with self.transaction() as conn:
|
|
488
|
+
conn.execute(
|
|
489
|
+
"""
|
|
490
|
+
INSERT INTO flow_artifacts (id, run_id, kind, path, created_at, metadata)
|
|
491
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
492
|
+
""",
|
|
493
|
+
(
|
|
494
|
+
artifact.id,
|
|
495
|
+
artifact.run_id,
|
|
496
|
+
artifact.kind,
|
|
497
|
+
artifact.path,
|
|
498
|
+
artifact.created_at,
|
|
499
|
+
json.dumps(artifact.metadata),
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
return artifact
|
|
504
|
+
|
|
505
|
+
def get_artifacts(self, run_id: str) -> List[FlowArtifact]:
|
|
506
|
+
conn = self._get_conn()
|
|
507
|
+
rows = conn.execute(
|
|
508
|
+
"SELECT * FROM flow_artifacts WHERE run_id = ? ORDER BY created_at ASC",
|
|
509
|
+
(run_id,),
|
|
510
|
+
).fetchall()
|
|
511
|
+
return [self._row_to_flow_artifact(row) for row in rows]
|
|
512
|
+
|
|
513
|
+
def get_artifact(self, artifact_id: str) -> Optional[FlowArtifact]:
|
|
514
|
+
conn = self._get_conn()
|
|
515
|
+
row = conn.execute(
|
|
516
|
+
"SELECT * FROM flow_artifacts WHERE id = ?", (artifact_id,)
|
|
517
|
+
).fetchone()
|
|
518
|
+
if row is None:
|
|
519
|
+
return None
|
|
520
|
+
return self._row_to_flow_artifact(row)
|
|
521
|
+
|
|
522
|
+
def delete_flow_run(self, run_id: str) -> bool:
|
|
523
|
+
"""Delete a flow run and its events/artifacts (cascading)."""
|
|
524
|
+
with self.transaction() as conn:
|
|
525
|
+
cursor = conn.execute("DELETE FROM flow_runs WHERE id = ?", (run_id,))
|
|
526
|
+
return cursor.rowcount > 0
|
|
527
|
+
|
|
528
|
+
def _row_to_flow_run(self, row: sqlite3.Row) -> FlowRunRecord:
|
|
529
|
+
return FlowRunRecord(
|
|
530
|
+
id=row["id"],
|
|
531
|
+
flow_type=row["flow_type"],
|
|
532
|
+
status=FlowRunStatus(row["status"]),
|
|
533
|
+
input_data=json.loads(row["input_data"]),
|
|
534
|
+
state=json.loads(row["state"]),
|
|
535
|
+
current_step=row["current_step"],
|
|
536
|
+
stop_requested=bool(row["stop_requested"]),
|
|
537
|
+
created_at=row["created_at"],
|
|
538
|
+
started_at=row["started_at"],
|
|
539
|
+
finished_at=row["finished_at"],
|
|
540
|
+
error_message=row["error_message"],
|
|
541
|
+
metadata=json.loads(row["metadata"]),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _row_to_flow_event(self, row: sqlite3.Row) -> FlowEvent:
|
|
545
|
+
return FlowEvent(
|
|
546
|
+
seq=row["seq"],
|
|
547
|
+
id=row["id"],
|
|
548
|
+
run_id=row["run_id"],
|
|
549
|
+
event_type=FlowEventType(row["event_type"]),
|
|
550
|
+
timestamp=row["timestamp"],
|
|
551
|
+
data=json.loads(row["data"]),
|
|
552
|
+
step_id=row["step_id"],
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def _row_to_flow_artifact(self, row: sqlite3.Row) -> FlowArtifact:
|
|
556
|
+
return FlowArtifact(
|
|
557
|
+
id=row["id"],
|
|
558
|
+
run_id=row["run_id"],
|
|
559
|
+
kind=row["kind"],
|
|
560
|
+
path=row["path"],
|
|
561
|
+
created_at=row["created_at"],
|
|
562
|
+
metadata=json.loads(row["metadata"]),
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def close(self) -> None:
|
|
566
|
+
if hasattr(self._local, "conn"):
|
|
567
|
+
self._local.conn.close()
|
|
568
|
+
del self._local.conn
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from codex_autorunner.core.flows.models import FlowRunRecord, FlowRunStatus
|
|
7
|
+
from codex_autorunner.core.flows.reasons import ensure_reason_summary
|
|
8
|
+
from codex_autorunner.core.flows.store import now_iso
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class TransitionDecision:
|
|
13
|
+
"""Result of resolving a flow's next status.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
status: FlowRunStatus
|
|
18
|
+
The resolved outer status.
|
|
19
|
+
finished_at: Optional[str]
|
|
20
|
+
Completion timestamp when the flow reaches a terminal state.
|
|
21
|
+
state: dict[str, Any]
|
|
22
|
+
Updated state payload (ticket_engine etc.).
|
|
23
|
+
note: Optional[str]
|
|
24
|
+
Reason for the transition (useful for tests/logging).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
status: FlowRunStatus
|
|
28
|
+
finished_at: Optional[str]
|
|
29
|
+
state: dict[str, Any]
|
|
30
|
+
note: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_flow_transition(
|
|
34
|
+
record: FlowRunRecord, health: Any, now: Optional[str] = None
|
|
35
|
+
) -> TransitionDecision:
|
|
36
|
+
"""Derive the flow status from worker liveness and inner ticket_engine state.
|
|
37
|
+
|
|
38
|
+
This is intentionally pure and side-effect free to keep recovery/test logic simple.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
now = now or now_iso()
|
|
42
|
+
state: dict[str, Any] = record.state or {}
|
|
43
|
+
engine_raw = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
44
|
+
engine: dict[str, Any] = engine_raw if isinstance(engine_raw, dict) else {}
|
|
45
|
+
inner_status = engine.get("status")
|
|
46
|
+
reason_code = engine.get("reason_code")
|
|
47
|
+
|
|
48
|
+
# 1) Worker liveness overrides for active flows.
|
|
49
|
+
if (
|
|
50
|
+
record.status in (FlowRunStatus.RUNNING, FlowRunStatus.STOPPING)
|
|
51
|
+
and not health.is_alive
|
|
52
|
+
):
|
|
53
|
+
new_status = (
|
|
54
|
+
FlowRunStatus.STOPPED
|
|
55
|
+
if record.status == FlowRunStatus.STOPPING
|
|
56
|
+
else FlowRunStatus.FAILED
|
|
57
|
+
)
|
|
58
|
+
state = ensure_reason_summary(state, status=new_status, default="Worker died")
|
|
59
|
+
return TransitionDecision(
|
|
60
|
+
status=new_status, finished_at=now, state=state, note="worker-dead"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# 2) Inner engine reconciliation (worker is alive or not required).
|
|
64
|
+
if record.status == FlowRunStatus.RUNNING:
|
|
65
|
+
if inner_status == "paused":
|
|
66
|
+
state = ensure_reason_summary(state, status=FlowRunStatus.PAUSED)
|
|
67
|
+
return TransitionDecision(
|
|
68
|
+
status=FlowRunStatus.PAUSED,
|
|
69
|
+
finished_at=None,
|
|
70
|
+
state=state,
|
|
71
|
+
note="engine-paused",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if inner_status == "completed":
|
|
75
|
+
return TransitionDecision(
|
|
76
|
+
status=FlowRunStatus.COMPLETED,
|
|
77
|
+
finished_at=now,
|
|
78
|
+
state=state,
|
|
79
|
+
note="engine-completed",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return TransitionDecision(
|
|
83
|
+
status=FlowRunStatus.RUNNING, finished_at=None, state=state, note="running"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if record.status == FlowRunStatus.PAUSED:
|
|
87
|
+
if inner_status == "completed":
|
|
88
|
+
return TransitionDecision(
|
|
89
|
+
status=FlowRunStatus.COMPLETED,
|
|
90
|
+
finished_at=now,
|
|
91
|
+
state=state,
|
|
92
|
+
note="paused-engine-completed",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
inner_status in (None, "running")
|
|
97
|
+
and reason_code != "user_pause"
|
|
98
|
+
and health.is_alive
|
|
99
|
+
):
|
|
100
|
+
# Treat as stale pause; resume and clear pause metadata.
|
|
101
|
+
engine.pop("reason", None)
|
|
102
|
+
engine.pop("reason_details", None)
|
|
103
|
+
engine.pop("reason_code", None)
|
|
104
|
+
state.pop("reason_summary", None)
|
|
105
|
+
engine["status"] = "running"
|
|
106
|
+
state["ticket_engine"] = engine
|
|
107
|
+
return TransitionDecision(
|
|
108
|
+
status=FlowRunStatus.RUNNING,
|
|
109
|
+
finished_at=None,
|
|
110
|
+
state=state,
|
|
111
|
+
note="stale-pause-resumed",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not health.is_alive:
|
|
115
|
+
return TransitionDecision(
|
|
116
|
+
status=FlowRunStatus.PAUSED,
|
|
117
|
+
finished_at=None,
|
|
118
|
+
state=state,
|
|
119
|
+
note="paused-worker-dead",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
state = ensure_reason_summary(state, status=FlowRunStatus.PAUSED)
|
|
123
|
+
return TransitionDecision(
|
|
124
|
+
status=FlowRunStatus.PAUSED, finished_at=None, state=state, note="paused"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# STOPPING/STOPPED/COMPLETED/FAILED: leave unchanged.
|
|
128
|
+
if record.status.is_terminal() or record.status == FlowRunStatus.STOPPED:
|
|
129
|
+
return TransitionDecision(
|
|
130
|
+
status=record.status,
|
|
131
|
+
finished_at=record.finished_at,
|
|
132
|
+
state=state,
|
|
133
|
+
note="terminal",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return TransitionDecision(
|
|
137
|
+
status=record.status, finished_at=None, state=state, note="unchanged"
|
|
138
|
+
)
|