engineering-platform 2.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.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Canonical active-run ownership leases backed solely by Engineering SQLite."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sqlite3
|
|
9
|
+
import socket
|
|
10
|
+
from threading import Event, Thread
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from .storage import EngineeringStorageError, open_storage
|
|
14
|
+
from .execution_timing import reconcile_interrupted_phases
|
|
15
|
+
from .provider_process_identity import ProcessIdentity, verify_process_identity
|
|
16
|
+
|
|
17
|
+
LEASE_VERSION = 1
|
|
18
|
+
HEARTBEAT_INTERVAL_SECONDS = 15
|
|
19
|
+
LEASE_TIMEOUT_SECONDS = 90
|
|
20
|
+
TERMINAL_PHASES = frozenset({"COMPLETE", "BLOCKED", "FAILED"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _connection(root: Path, central_database: Path | None = None) -> sqlite3.Connection:
|
|
24
|
+
"""Open an explicitly supplied CENTRAL lease authority when available."""
|
|
25
|
+
if central_database is None:
|
|
26
|
+
return open_storage(root)
|
|
27
|
+
database = central_database.resolve()
|
|
28
|
+
if not database.is_file():
|
|
29
|
+
raise EngineeringStorageError("CENTRAL lease database is unavailable.")
|
|
30
|
+
connection = sqlite3.connect(database, isolation_level=None)
|
|
31
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
32
|
+
connection.execute("PRAGMA busy_timeout=10000")
|
|
33
|
+
return connection
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LeaseConflictError(EngineeringStorageError):
|
|
37
|
+
"""Raised when another non-expired host instance owns the run."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Lease:
|
|
42
|
+
lease_id: str
|
|
43
|
+
run_id: str
|
|
44
|
+
host_identity: str
|
|
45
|
+
host_instance_id: str
|
|
46
|
+
acquired_at: str
|
|
47
|
+
last_heartbeat_at: str
|
|
48
|
+
expires_at: str
|
|
49
|
+
lease_state: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def host_identity() -> str:
|
|
53
|
+
return socket.gethostname()[:120] or "execution-host"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def host_instance_id() -> str:
|
|
57
|
+
return f"{uuid.uuid4()}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _now() -> datetime:
|
|
61
|
+
return datetime.now(timezone.utc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _event(connection: object, lease_id: str, run_id: str, event: str, outcome: str | None = None) -> None:
|
|
65
|
+
connection.execute(
|
|
66
|
+
"INSERT OR IGNORE INTO execution_lease_events(lease_id,run_id,event_type,outcome,recorded_at) VALUES(?,?,?,?,?)",
|
|
67
|
+
(lease_id, run_id, event, outcome or "", _now().isoformat()),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _recovery_provider_lease_state(connection: object, run_id: str) -> str:
|
|
72
|
+
"""Read recovery receipts through the caller's lease transaction.
|
|
73
|
+
|
|
74
|
+
This avoids opening a second SQLite writer while stale-lease
|
|
75
|
+
reconciliation owns ``BEGIN IMMEDIATE``. Only a session-bound, PID reuse
|
|
76
|
+
protected PROCESS_STARTED receipt can retain an expired host lease.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
recovery = connection.execute(
|
|
80
|
+
"SELECT state,replacement_invocation_id,provider_session_id FROM provider_recovery_attempts WHERE run_id=?",
|
|
81
|
+
(run_id,),
|
|
82
|
+
).fetchone()
|
|
83
|
+
if not recovery or recovery[0] != "RECOVERY_IN_PROGRESS":
|
|
84
|
+
return "NOT_ACTIVE"
|
|
85
|
+
rows = connection.execute(
|
|
86
|
+
"SELECT provider_session_id,process_pid,process_group,process_start_fingerprint,process_executable_identity "
|
|
87
|
+
"FROM provider_invocation_receipts WHERE run_id=? AND invocation_id=? AND launch_state='PROCESS_STARTED'",
|
|
88
|
+
(run_id, recovery[1]),
|
|
89
|
+
).fetchall()
|
|
90
|
+
except Exception:
|
|
91
|
+
return "NOT_ACTIVE"
|
|
92
|
+
if len(rows) != 1 or not isinstance(recovery[2], str):
|
|
93
|
+
return "AMBIGUOUS"
|
|
94
|
+
session_id, pid, group, fingerprint, executable = rows[0]
|
|
95
|
+
if (
|
|
96
|
+
session_id != recovery[2] or not isinstance(pid, int) or not isinstance(group, int)
|
|
97
|
+
or not isinstance(fingerprint, str) or not isinstance(executable, str)
|
|
98
|
+
):
|
|
99
|
+
return "AMBIGUOUS"
|
|
100
|
+
return "ACTIVE" if verify_process_identity(ProcessIdentity(pid, group, fingerprint, executable)) == "MATCH" else "AMBIGUOUS"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LeaseHeartbeat:
|
|
104
|
+
"""Bounded background heartbeat; lifecycle state remains runner-owned."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, root: Path, lease: Lease, *, central_database: Path | None = None, interval_seconds: int = HEARTBEAT_INTERVAL_SECONDS) -> None:
|
|
107
|
+
self.root = root
|
|
108
|
+
self.lease = lease
|
|
109
|
+
self.central_database = central_database
|
|
110
|
+
self.interval_seconds = interval_seconds
|
|
111
|
+
self._stop = Event()
|
|
112
|
+
self._thread: Thread | None = None
|
|
113
|
+
self.error: Exception | None = None
|
|
114
|
+
|
|
115
|
+
def start(self) -> None:
|
|
116
|
+
if self._thread is not None:
|
|
117
|
+
return
|
|
118
|
+
self._thread = Thread(target=self._run, name=f"engineering-lease-{self.lease.run_id}", daemon=True)
|
|
119
|
+
self._thread.start()
|
|
120
|
+
|
|
121
|
+
def _run(self) -> None:
|
|
122
|
+
while not self._stop.wait(self.interval_seconds):
|
|
123
|
+
try:
|
|
124
|
+
self.lease = heartbeat(self.root, self.lease, central_database=self.central_database)
|
|
125
|
+
except Exception as error: # The expiry boundary remains fail-closed.
|
|
126
|
+
self.error = error
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
def stop(self) -> Lease:
|
|
130
|
+
self._stop.set()
|
|
131
|
+
if self._thread is not None:
|
|
132
|
+
self._thread.join(timeout=max(1, self.interval_seconds))
|
|
133
|
+
return self.lease
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def acquire(root: Path, run_id: str, *, identity: str, instance_id: str, process_id: int | None = None, timeout_seconds: int = LEASE_TIMEOUT_SECONDS, central_database: Path | None = None) -> Lease:
|
|
137
|
+
if not 0 < HEARTBEAT_INTERVAL_SECONDS < timeout_seconds:
|
|
138
|
+
raise EngineeringStorageError("Active-run lease policy is invalid.")
|
|
139
|
+
now = _now()
|
|
140
|
+
expiry = now + timedelta(seconds=timeout_seconds)
|
|
141
|
+
connection = _connection(root, central_database)
|
|
142
|
+
try:
|
|
143
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
144
|
+
connection.execute(
|
|
145
|
+
"UPDATE execution_run_leases SET lease_state='EXPIRED',updated_at=? WHERE run_id=? AND lease_state='ACTIVE' AND expires_at<?",
|
|
146
|
+
(now.isoformat(), run_id, now.isoformat()),
|
|
147
|
+
)
|
|
148
|
+
active = connection.execute(
|
|
149
|
+
"SELECT lease_id,host_instance_id FROM execution_run_leases WHERE run_id=? AND lease_state='ACTIVE'", (run_id,)
|
|
150
|
+
).fetchone()
|
|
151
|
+
if active:
|
|
152
|
+
raise LeaseConflictError("A live Execution Host instance already owns this run.")
|
|
153
|
+
lease_id = f"lease-{uuid.uuid4()}"
|
|
154
|
+
values = (lease_id, run_id, identity, instance_id, process_id, now.isoformat(), now.isoformat(), expiry.isoformat(), "ACTIVE", LEASE_VERSION, now.isoformat(), now.isoformat())
|
|
155
|
+
connection.execute("INSERT INTO execution_run_leases(lease_id,run_id,host_identity,host_instance_id,process_id,acquired_at,last_heartbeat_at,expires_at,lease_state,lease_version,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", values)
|
|
156
|
+
_event(connection, lease_id, run_id, "LEASE_ACQUIRED")
|
|
157
|
+
connection.execute("COMMIT")
|
|
158
|
+
return Lease(lease_id, run_id, identity, instance_id, now.isoformat(), now.isoformat(), expiry.isoformat(), "ACTIVE")
|
|
159
|
+
except Exception:
|
|
160
|
+
connection.execute("ROLLBACK")
|
|
161
|
+
raise
|
|
162
|
+
finally:
|
|
163
|
+
connection.close()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def heartbeat(root: Path, lease: Lease, *, timeout_seconds: int = LEASE_TIMEOUT_SECONDS, central_database: Path | None = None) -> Lease:
|
|
167
|
+
now = _now()
|
|
168
|
+
expiry = now + timedelta(seconds=timeout_seconds)
|
|
169
|
+
connection = _connection(root, central_database)
|
|
170
|
+
try:
|
|
171
|
+
updated = connection.execute("UPDATE execution_run_leases SET last_heartbeat_at=?,expires_at=?,updated_at=? WHERE lease_id=? AND run_id=? AND host_instance_id=? AND lease_state='ACTIVE'", (now.isoformat(), expiry.isoformat(), now.isoformat(), lease.lease_id, lease.run_id, lease.host_instance_id)).rowcount
|
|
172
|
+
if updated != 1:
|
|
173
|
+
raise LeaseConflictError("Execution Host no longer owns the active-run lease.")
|
|
174
|
+
finally:
|
|
175
|
+
connection.close()
|
|
176
|
+
return Lease(lease.lease_id, lease.run_id, lease.host_identity, lease.host_instance_id, lease.acquired_at, now.isoformat(), expiry.isoformat(), "ACTIVE")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def release(root: Path, lease: Lease, *, central_database: Path | None = None) -> None:
|
|
180
|
+
connection = _connection(root, central_database)
|
|
181
|
+
try:
|
|
182
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
183
|
+
changed = connection.execute("UPDATE execution_run_leases SET lease_state='RELEASED',updated_at=? WHERE lease_id=? AND host_instance_id=? AND lease_state='ACTIVE'", (_now().isoformat(), lease.lease_id, lease.host_instance_id)).rowcount
|
|
184
|
+
if changed:
|
|
185
|
+
_event(connection, lease.lease_id, lease.run_id, "LEASE_RELEASED")
|
|
186
|
+
connection.execute("COMMIT")
|
|
187
|
+
finally:
|
|
188
|
+
connection.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def release_terminal_lease(root: Path, run_id: str, *, central_database: Path | None = None) -> bool:
|
|
192
|
+
"""Release a departed host's lease only after its checkpoint is terminal.
|
|
193
|
+
|
|
194
|
+
The watcher calls this after its foreground child has exited and after it
|
|
195
|
+
has persisted the terminal transaction. It is not a stale-lease shortcut
|
|
196
|
+
and never decides lifecycle state itself.
|
|
197
|
+
"""
|
|
198
|
+
connection = _connection(root, central_database)
|
|
199
|
+
try:
|
|
200
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
201
|
+
row = connection.execute(
|
|
202
|
+
"SELECT phase,payload FROM engineering_transactions WHERE run_id=?", (run_id,)
|
|
203
|
+
).fetchone()
|
|
204
|
+
terminal = False
|
|
205
|
+
if row and row[0] in TERMINAL_PHASES:
|
|
206
|
+
try:
|
|
207
|
+
payload = json.loads(row[1])
|
|
208
|
+
except (TypeError, json.JSONDecodeError):
|
|
209
|
+
payload = {}
|
|
210
|
+
terminal = isinstance(payload, dict) and payload.get("terminal") is True
|
|
211
|
+
if not terminal:
|
|
212
|
+
connection.execute("ROLLBACK")
|
|
213
|
+
return False
|
|
214
|
+
rows = connection.execute(
|
|
215
|
+
"SELECT lease_id FROM execution_run_leases WHERE run_id=? AND lease_state='ACTIVE'",
|
|
216
|
+
(run_id,),
|
|
217
|
+
).fetchall()
|
|
218
|
+
now = _now().isoformat()
|
|
219
|
+
changed = connection.execute(
|
|
220
|
+
"UPDATE execution_run_leases SET lease_state='RELEASED',updated_at=? "
|
|
221
|
+
"WHERE run_id=? AND lease_state='ACTIVE'",
|
|
222
|
+
(now, run_id),
|
|
223
|
+
).rowcount
|
|
224
|
+
for (lease_id,) in rows:
|
|
225
|
+
_event(connection, str(lease_id), run_id, "LEASE_RELEASED")
|
|
226
|
+
connection.execute("COMMIT")
|
|
227
|
+
return bool(changed)
|
|
228
|
+
except Exception:
|
|
229
|
+
connection.execute("ROLLBACK")
|
|
230
|
+
raise
|
|
231
|
+
finally:
|
|
232
|
+
connection.close()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def reconcile_stale(root: Path, *, central_database: Path | None = None) -> list[dict[str, str]]:
|
|
236
|
+
"""Reconcile datastore ownership only; never fabricate a terminal lifecycle state."""
|
|
237
|
+
now = _now().isoformat()
|
|
238
|
+
outcomes: list[dict[str, str]] = []
|
|
239
|
+
interrupted_runs: list[str] = []
|
|
240
|
+
connection = _connection(root, central_database)
|
|
241
|
+
try:
|
|
242
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
243
|
+
rows = connection.execute("SELECT lease_id,run_id,host_instance_id,last_heartbeat_at FROM execution_run_leases WHERE lease_state='ACTIVE' AND expires_at<?", (now,)).fetchall()
|
|
244
|
+
for lease_id, run_id, instance, heartbeat_at in rows:
|
|
245
|
+
# Provider execution is independent of the host heartbeat. A
|
|
246
|
+
# recovered provider turn can still be authoritatively active
|
|
247
|
+
# after its parent host disappeared; expiring this run lease
|
|
248
|
+
# would falsely make repository mutation ownership available.
|
|
249
|
+
recovery_lease_state = _recovery_provider_lease_state(connection, str(run_id))
|
|
250
|
+
if recovery_lease_state in {"ACTIVE", "AMBIGUOUS"}:
|
|
251
|
+
renewed = (_now() + timedelta(seconds=LEASE_TIMEOUT_SECONDS)).isoformat()
|
|
252
|
+
connection.execute(
|
|
253
|
+
"UPDATE execution_run_leases SET last_heartbeat_at=?,expires_at=?,updated_at=? "
|
|
254
|
+
"WHERE lease_id=? AND lease_state='ACTIVE'",
|
|
255
|
+
(now, renewed, now, lease_id),
|
|
256
|
+
)
|
|
257
|
+
outcome = (
|
|
258
|
+
"RECOVERY_PROVIDER_STILL_ACTIVE"
|
|
259
|
+
if recovery_lease_state == "ACTIVE" else "RECOVERY_PROVIDER_AMBIGUOUS"
|
|
260
|
+
)
|
|
261
|
+
_event(connection, str(lease_id), str(run_id), outcome)
|
|
262
|
+
outcomes.append({
|
|
263
|
+
"run_id": str(run_id), "host_instance_id": str(instance),
|
|
264
|
+
"last_heartbeat": str(heartbeat_at), "outcome": outcome,
|
|
265
|
+
})
|
|
266
|
+
continue
|
|
267
|
+
transaction = connection.execute(
|
|
268
|
+
"SELECT phase,payload FROM engineering_transactions WHERE run_id=?", (run_id,)
|
|
269
|
+
).fetchone()
|
|
270
|
+
phase = transaction[0] if transaction else None
|
|
271
|
+
terminal_phase: str | None = phase if phase in TERMINAL_PHASES else None
|
|
272
|
+
if transaction and terminal_phase is None:
|
|
273
|
+
try:
|
|
274
|
+
payload = json.loads(transaction[1])
|
|
275
|
+
except (TypeError, json.JSONDecodeError):
|
|
276
|
+
payload = {}
|
|
277
|
+
candidate = payload.get("phase") if isinstance(payload, dict) and payload.get("terminal") is True else None
|
|
278
|
+
if candidate in TERMINAL_PHASES:
|
|
279
|
+
terminal_phase = candidate
|
|
280
|
+
connection.execute(
|
|
281
|
+
"UPDATE engineering_transactions SET phase=?,updated_at=? WHERE run_id=?",
|
|
282
|
+
(terminal_phase, now, run_id),
|
|
283
|
+
)
|
|
284
|
+
outcome = "TERMINAL_EVIDENCE_PRESENT" if terminal_phase else "RECOVERABLE"
|
|
285
|
+
connection.execute("UPDATE execution_run_leases SET lease_state='EXPIRED',updated_at=? WHERE lease_id=?", (now, lease_id))
|
|
286
|
+
_event(connection, lease_id, run_id, "LEASE_EXPIRED")
|
|
287
|
+
_event(connection, lease_id, run_id, "STALE_DETECTED", outcome)
|
|
288
|
+
_event(connection, lease_id, run_id, "STALE_RECONCILED", outcome)
|
|
289
|
+
connection.execute(
|
|
290
|
+
"INSERT INTO execution_run_reconciliations(run_id,outcome,reason,reconciled_at,updated_at) VALUES(?,?,?,?,?) "
|
|
291
|
+
"ON CONFLICT(run_id) DO UPDATE SET outcome=excluded.outcome,reason=excluded.reason,reconciled_at=excluded.reconciled_at,updated_at=excluded.updated_at",
|
|
292
|
+
(run_id, outcome, f"lease_expired;terminal_phase={terminal_phase or 'none'}", now, now),
|
|
293
|
+
)
|
|
294
|
+
# Close active spans after this lease transaction commits.
|
|
295
|
+
interrupted_runs.append(run_id)
|
|
296
|
+
outcomes.append({"run_id": run_id, "host_instance_id": instance, "last_heartbeat": heartbeat_at, "outcome": outcome})
|
|
297
|
+
active_runs = connection.execute(
|
|
298
|
+
"SELECT run_id,phase FROM engineering_transactions WHERE phase NOT IN ('COMPLETE','BLOCKED','FAILED')"
|
|
299
|
+
).fetchall()
|
|
300
|
+
for run_id, phase in active_runs:
|
|
301
|
+
has_live_lease = connection.execute(
|
|
302
|
+
"SELECT 1 FROM execution_run_leases WHERE run_id=? AND lease_state='ACTIVE' AND expires_at>=?",
|
|
303
|
+
(run_id, now),
|
|
304
|
+
).fetchone()
|
|
305
|
+
if has_live_lease:
|
|
306
|
+
continue
|
|
307
|
+
prior = connection.execute(
|
|
308
|
+
"SELECT 1 FROM execution_run_reconciliations WHERE run_id=?", (run_id,)
|
|
309
|
+
).fetchone()
|
|
310
|
+
if prior:
|
|
311
|
+
continue
|
|
312
|
+
outcome = "INCONSISTENT" if phase in TERMINAL_PHASES else "OPERATOR_INTERVENTION_REQUIRED"
|
|
313
|
+
connection.execute(
|
|
314
|
+
"INSERT INTO execution_run_reconciliations(run_id,outcome,reason,reconciled_at,updated_at) VALUES(?,?,?,?,?)",
|
|
315
|
+
(run_id, outcome, "active_transaction_without_live_lease", now, now),
|
|
316
|
+
)
|
|
317
|
+
outcomes.append({"run_id": run_id, "host_instance_id": "unknown", "last_heartbeat": "unknown", "outcome": outcome})
|
|
318
|
+
connection.execute("COMMIT")
|
|
319
|
+
finally:
|
|
320
|
+
connection.close()
|
|
321
|
+
for run_id in interrupted_runs:
|
|
322
|
+
# Completed timing evidence remains immutable. This deliberately uses
|
|
323
|
+
# the actual reconciliation boundary rather than an invented expiry.
|
|
324
|
+
reconcile_interrupted_phases(root, run_id, central_database=central_database)
|
|
325
|
+
return outcomes
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def liveness(root: Path, run_id: object, *, central_database: Path | None = None) -> dict[str, object]:
|
|
329
|
+
"""Project canonical liveness without consulting status files or processes."""
|
|
330
|
+
if not isinstance(run_id, str):
|
|
331
|
+
return {"state": "UNAVAILABLE"}
|
|
332
|
+
now = _now().isoformat()
|
|
333
|
+
connection = _connection(root, central_database)
|
|
334
|
+
try:
|
|
335
|
+
row = connection.execute("SELECT host_identity,host_instance_id,last_heartbeat_at,expires_at,lease_state FROM execution_run_leases WHERE run_id=? ORDER BY created_at DESC LIMIT 1", (run_id,)).fetchone()
|
|
336
|
+
reconciliation = connection.execute(
|
|
337
|
+
"SELECT outcome,reason,reconciled_at FROM execution_run_reconciliations WHERE run_id=?", (run_id,)
|
|
338
|
+
).fetchone()
|
|
339
|
+
finally:
|
|
340
|
+
connection.close()
|
|
341
|
+
if not row:
|
|
342
|
+
result: dict[str, object] = {"state": "STALE"}
|
|
343
|
+
if reconciliation:
|
|
344
|
+
result.update({"reconciliation_outcome": reconciliation[0], "reconciliation_reason": reconciliation[1], "reconciled_at": reconciliation[2]})
|
|
345
|
+
return result
|
|
346
|
+
identity, instance, heartbeat_at, expires_at, state = row
|
|
347
|
+
result = {"state": "LIVE" if state == "ACTIVE" and expires_at >= now else "STALE", "lease_state": state, "host_identity": identity, "host_instance_id": instance, "last_heartbeat": heartbeat_at, "lease_expiry": expires_at}
|
|
348
|
+
if reconciliation:
|
|
349
|
+
result.update({"reconciliation_outcome": reconciliation[0], "reconciliation_reason": reconciliation[1], "reconciled_at": reconciliation[2]})
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def history(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object]:
|
|
354
|
+
"""Return bounded canonical lease evidence for reports and history views."""
|
|
355
|
+
connection = _connection(root, central_database)
|
|
356
|
+
try:
|
|
357
|
+
row = connection.execute(
|
|
358
|
+
"SELECT host_identity,host_instance_id,acquired_at,last_heartbeat_at,expires_at,lease_state "
|
|
359
|
+
"FROM execution_run_leases WHERE run_id=? ORDER BY created_at DESC LIMIT 1", (run_id,)
|
|
360
|
+
).fetchone()
|
|
361
|
+
finally:
|
|
362
|
+
connection.close()
|
|
363
|
+
if not row:
|
|
364
|
+
return {"state": "UNAVAILABLE"}
|
|
365
|
+
return {"host_identity": row[0], "host_instance_id": row[1], "acquired_at": row[2], "last_heartbeat": row[3], "lease_expiry": row[4], "lease_state": row[5]}
|