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,440 @@
|
|
|
1
|
+
"""Canonical Engineering Platform execution phase timing evidence.
|
|
2
|
+
|
|
3
|
+
This module deliberately owns measurement, persistence and deterministic
|
|
4
|
+
projection only. It has no Mission, Producer, retry or lifecycle authority.
|
|
5
|
+
Wall-clock UTC timestamps describe when a span occurred; a monotonic duration
|
|
6
|
+
is captured at its runtime boundary. Repeated and nested spans stay separate
|
|
7
|
+
in SQLite. Aggregates use only top-level spans, so nested work is never
|
|
8
|
+
double-counted.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import sqlite3
|
|
17
|
+
from time import monotonic
|
|
18
|
+
from typing import Mapping
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
from .storage import EngineeringStorageError, open_storage
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PHASES = frozenset({
|
|
25
|
+
"QUEUE_WAIT", "SUBMISSION_CLAIM", "INITIALIZATION", "HOST_PREFLIGHT",
|
|
26
|
+
"WORKSPACE_PREFLIGHT", "CAPABILITY_PREFLIGHT", "DETERMINISTIC_ADMISSION", "CAPABILITY_REVIEW", "EXECUTION_PREPARATION",
|
|
27
|
+
"PROVIDER_EXECUTION", "VALIDATION", "QUALITY_CONTROL", "REPAIR", "REPOSITORY_FINALIZATION",
|
|
28
|
+
"PR_OR_MERGE", "FINALIZATION", "REPORT_GENERATION", "EVIDENCE_PERSISTENCE",
|
|
29
|
+
"REPOSITORY_CLEANUP", "RECONCILIATION", "EXTERNAL_CI_WAIT", "TOTAL_EXECUTION",
|
|
30
|
+
})
|
|
31
|
+
TERMINAL_OUTCOMES = frozenset({"COMPLETE", "FAILED", "INTERRUPTED", "STALE"})
|
|
32
|
+
_MAX_METADATA_BYTES = 2048
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _connection(root: Path, central_database: Path | None = None) -> sqlite3.Connection:
|
|
36
|
+
"""Open timing storage from an explicit CENTRAL binding when supplied."""
|
|
37
|
+
if central_database is None:
|
|
38
|
+
return open_storage(root)
|
|
39
|
+
database = central_database.resolve()
|
|
40
|
+
if not database.is_file():
|
|
41
|
+
raise EngineeringStorageError("CENTRAL timing database is unavailable.")
|
|
42
|
+
connection = sqlite3.connect(database, isolation_level=None)
|
|
43
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
44
|
+
connection.execute("PRAGMA busy_timeout=10000")
|
|
45
|
+
return connection
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _utc(value: datetime | None = None) -> str:
|
|
49
|
+
value = value or datetime.now(timezone.utc)
|
|
50
|
+
return (value.astimezone(timezone.utc) if value.tzinfo else value.replace(tzinfo=timezone.utc)).isoformat()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _metadata(value: Mapping[str, object] | None) -> str:
|
|
54
|
+
if value is None:
|
|
55
|
+
return "{}"
|
|
56
|
+
if not isinstance(value, Mapping):
|
|
57
|
+
raise EngineeringStorageError("Execution phase metadata must be a mapping.")
|
|
58
|
+
encoded = json.dumps(dict(value), sort_keys=True, separators=(",", ":"))
|
|
59
|
+
if len(encoded.encode()) > _MAX_METADATA_BYTES:
|
|
60
|
+
raise EngineeringStorageError("Execution phase metadata exceeds its bounded limit.")
|
|
61
|
+
return encoded
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ActivePhase:
|
|
66
|
+
run_id: str
|
|
67
|
+
phase_id: str
|
|
68
|
+
started_monotonic: float | None
|
|
69
|
+
central_database: Path | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def start_phase(root: Path, run_id: str, phase_name: str, *, category: str | None = None,
|
|
73
|
+
parent_phase_id: str | None = None, attempt: int = 1,
|
|
74
|
+
metadata: Mapping[str, object] | None = None, started_at: datetime | None = None,
|
|
75
|
+
monotonic_clock: float | None = None, central_database: Path | None = None) -> ActivePhase:
|
|
76
|
+
"""Persist a real active phase boundary and return its monotonic handle."""
|
|
77
|
+
if phase_name not in PHASES or not run_id or attempt < 1:
|
|
78
|
+
raise EngineeringStorageError("Execution phase identity is invalid.")
|
|
79
|
+
phase_id = f"phase-{uuid.uuid4()}"
|
|
80
|
+
connection = _connection(root, central_database)
|
|
81
|
+
try:
|
|
82
|
+
ordinal = connection.execute(
|
|
83
|
+
"SELECT COALESCE(MAX(ordinal), 0) + 1 FROM execution_phase_spans WHERE run_id=?", (run_id,)
|
|
84
|
+
).fetchone()[0]
|
|
85
|
+
connection.execute(
|
|
86
|
+
"INSERT INTO execution_phase_spans(phase_id,run_id,phase_name,phase_category,parent_phase_id,attempt,ordinal,started_at,outcome,metadata) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
|
87
|
+
(phase_id, run_id, phase_name, category or phase_name, parent_phase_id, attempt, ordinal,
|
|
88
|
+
_utc(started_at), "ACTIVE", _metadata(metadata)),
|
|
89
|
+
)
|
|
90
|
+
finally:
|
|
91
|
+
connection.close()
|
|
92
|
+
return ActivePhase(run_id, phase_id, monotonic() if monotonic_clock is None else monotonic_clock, central_database)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def complete_phase(root: Path, active: ActivePhase, *, outcome: str = "COMPLETE",
|
|
96
|
+
completed_at: datetime | None = None, monotonic_clock: float | None = None) -> None:
|
|
97
|
+
"""Close an active span with its directly measured monotonic duration."""
|
|
98
|
+
if outcome not in TERMINAL_OUTCOMES:
|
|
99
|
+
raise EngineeringStorageError("Execution phase outcome is invalid.")
|
|
100
|
+
connection = _connection(root, active.central_database)
|
|
101
|
+
try:
|
|
102
|
+
if active.started_monotonic is None:
|
|
103
|
+
# A phase can outlive the runner process. Its terminal boundary
|
|
104
|
+
# is still observable, but monotonic state is intentionally not
|
|
105
|
+
# fabricated across that restart.
|
|
106
|
+
row = connection.execute(
|
|
107
|
+
"SELECT started_at FROM execution_phase_spans WHERE phase_id=? AND run_id=? AND outcome='ACTIVE'",
|
|
108
|
+
(active.phase_id, active.run_id),
|
|
109
|
+
).fetchone()
|
|
110
|
+
if row is None:
|
|
111
|
+
raise EngineeringStorageError("Execution phase is not active.")
|
|
112
|
+
started = datetime.fromisoformat(str(row[0]).replace("Z", "+00:00"))
|
|
113
|
+
ended = completed_at or datetime.now(timezone.utc)
|
|
114
|
+
duration_ms = max(0, round((ended - started).total_seconds() * 1000))
|
|
115
|
+
else:
|
|
116
|
+
elapsed = (monotonic() if monotonic_clock is None else monotonic_clock) - active.started_monotonic
|
|
117
|
+
duration_ms = max(0, round(elapsed * 1000))
|
|
118
|
+
changed = connection.execute(
|
|
119
|
+
"UPDATE execution_phase_spans SET completed_at=?,duration_ms=?,outcome=? WHERE phase_id=? AND run_id=? AND outcome='ACTIVE'",
|
|
120
|
+
(_utc(completed_at), duration_ms, outcome, active.phase_id, active.run_id),
|
|
121
|
+
).rowcount
|
|
122
|
+
if changed != 1:
|
|
123
|
+
raise EngineeringStorageError("Execution phase is not active.")
|
|
124
|
+
finally:
|
|
125
|
+
connection.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def start_or_resume_phase(root: Path, run_id: str, phase_name: str, **kwargs: object) -> ActivePhase:
|
|
129
|
+
"""Start one envelope or resume its observed active database boundary.
|
|
130
|
+
|
|
131
|
+
Only lifecycle envelopes use this. The resumed handle deliberately has no
|
|
132
|
+
monotonic value, so its completion is explicitly bounded by persisted UTC
|
|
133
|
+
timestamps rather than pretending a monotonic clock survived restart.
|
|
134
|
+
"""
|
|
135
|
+
central_database = kwargs.get("central_database")
|
|
136
|
+
if central_database is not None and not isinstance(central_database, Path):
|
|
137
|
+
raise EngineeringStorageError("CENTRAL timing database is invalid.")
|
|
138
|
+
connection = _connection(root, central_database)
|
|
139
|
+
try:
|
|
140
|
+
row = connection.execute(
|
|
141
|
+
"SELECT phase_id FROM execution_phase_spans WHERE run_id=? AND phase_name=? AND outcome='ACTIVE' ORDER BY ordinal LIMIT 1",
|
|
142
|
+
(run_id, phase_name),
|
|
143
|
+
).fetchone()
|
|
144
|
+
finally:
|
|
145
|
+
connection.close()
|
|
146
|
+
if row:
|
|
147
|
+
return ActivePhase(run_id, str(row[0]), None, central_database)
|
|
148
|
+
return start_phase(root, run_id, phase_name, **kwargs)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def record_phase(root: Path, run_id: str, phase_name: str, *, started_at: datetime,
|
|
152
|
+
completed_at: datetime, category: str | None = None,
|
|
153
|
+
parent_phase_id: str | None = None, attempt: int = 1,
|
|
154
|
+
metadata: Mapping[str, object] | None = None, outcome: str = "COMPLETE") -> str:
|
|
155
|
+
"""Record a completed observed boundary when no monotonic handle survives.
|
|
156
|
+
|
|
157
|
+
This is reserved for cross-process boundaries such as queue submission to
|
|
158
|
+
claim. In-process work must use :func:`start_phase` and
|
|
159
|
+
:func:`complete_phase` instead.
|
|
160
|
+
"""
|
|
161
|
+
active = start_phase(root, run_id, phase_name, category=category, parent_phase_id=parent_phase_id,
|
|
162
|
+
attempt=attempt, metadata=metadata, started_at=started_at, monotonic_clock=0)
|
|
163
|
+
elapsed = max(0, (completed_at - started_at).total_seconds())
|
|
164
|
+
complete_phase(root, active, outcome=outcome, completed_at=completed_at, monotonic_clock=elapsed)
|
|
165
|
+
return active.phase_id
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def record_queue_wait_from_submission(root: Path, run_id: str, *, claimed_at: datetime | None = None) -> bool:
|
|
169
|
+
"""Record the persisted submission-to-claim delay when it is observable."""
|
|
170
|
+
connection = open_storage(root)
|
|
171
|
+
try:
|
|
172
|
+
row = connection.execute(
|
|
173
|
+
"SELECT submission.received_at FROM execution_submissions AS submission JOIN execution_submission_links AS link ON link.submission_id=submission.submission_id WHERE link.run_id=?", (run_id,)
|
|
174
|
+
).fetchone()
|
|
175
|
+
finally:
|
|
176
|
+
connection.close()
|
|
177
|
+
if not row or not isinstance(row[0], str):
|
|
178
|
+
return False
|
|
179
|
+
try:
|
|
180
|
+
submitted = datetime.fromisoformat(row[0].replace("Z", "+00:00"))
|
|
181
|
+
except ValueError:
|
|
182
|
+
return False
|
|
183
|
+
record_phase(root, run_id, "QUEUE_WAIT", started_at=submitted, completed_at=claimed_at or datetime.now(timezone.utc), category="QUEUE")
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def reconcile_interrupted_phases(root: Path, run_id: str, *, outcome: str = "STALE",
|
|
188
|
+
completed_at: datetime | None = None,
|
|
189
|
+
central_database: Path | None = None) -> int:
|
|
190
|
+
"""Close observable abandoned work at reconciliation, never backdating it."""
|
|
191
|
+
if outcome not in {"STALE", "INTERRUPTED"}:
|
|
192
|
+
raise EngineeringStorageError("Interrupted phase outcome is invalid.")
|
|
193
|
+
connection = _connection(root, central_database)
|
|
194
|
+
try:
|
|
195
|
+
now = _utc(completed_at)
|
|
196
|
+
# Monotonic state cannot survive a process restart, so a reconciled
|
|
197
|
+
# duration is intentionally wall-clock bounded and explicitly STALE.
|
|
198
|
+
return connection.execute(
|
|
199
|
+
"UPDATE execution_phase_spans SET completed_at=?,duration_ms=MAX(0,CAST((julianday(?) - julianday(started_at))*86400000 AS INTEGER)),outcome=? WHERE run_id=? AND outcome='ACTIVE'",
|
|
200
|
+
(now, now, outcome, run_id),
|
|
201
|
+
).rowcount
|
|
202
|
+
finally:
|
|
203
|
+
connection.close()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def complete_active_phase(
|
|
207
|
+
root: Path, run_id: str, phase_name: str, *, outcome: str = "COMPLETE",
|
|
208
|
+
central_database: Path | None = None,
|
|
209
|
+
) -> bool:
|
|
210
|
+
"""Close the one active lifecycle envelope when its owner observes completion.
|
|
211
|
+
|
|
212
|
+
Queue admission, the runner and the watcher are separate processes. The
|
|
213
|
+
watcher therefore owns the final total envelope boundary, after report and
|
|
214
|
+
evidence persistence have completed. This intentionally uses the stored
|
|
215
|
+
UTC boundary rather than claiming a monotonic clock crosses processes.
|
|
216
|
+
"""
|
|
217
|
+
connection = _connection(root, central_database)
|
|
218
|
+
try:
|
|
219
|
+
row = connection.execute(
|
|
220
|
+
"SELECT phase_id FROM execution_phase_spans WHERE run_id=? AND phase_name=? AND outcome='ACTIVE' ORDER BY ordinal LIMIT 1",
|
|
221
|
+
(run_id, phase_name),
|
|
222
|
+
).fetchone()
|
|
223
|
+
finally:
|
|
224
|
+
connection.close()
|
|
225
|
+
if row is None:
|
|
226
|
+
return False
|
|
227
|
+
complete_phase(root, ActivePhase(run_id, str(row[0]), None, central_database), outcome=outcome)
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def phase_spans(root: Path, run_id: str, *, central_database: Path | None = None) -> list[dict[str, object]]:
|
|
232
|
+
connection = _connection(root, central_database)
|
|
233
|
+
try:
|
|
234
|
+
rows = connection.execute(
|
|
235
|
+
"SELECT phase_id,phase_name,phase_category,parent_phase_id,attempt,ordinal,started_at,completed_at,duration_ms,outcome,metadata FROM execution_phase_spans WHERE run_id=? ORDER BY ordinal", (run_id,)
|
|
236
|
+
).fetchall()
|
|
237
|
+
finally:
|
|
238
|
+
connection.close()
|
|
239
|
+
keys = ("phase_id", "phase_name", "phase_category", "parent_phase_id", "attempt", "ordinal", "started_at", "completed_at", "duration_ms", "outcome", "metadata")
|
|
240
|
+
result = []
|
|
241
|
+
for row in rows:
|
|
242
|
+
item = dict(zip(keys, row, strict=True))
|
|
243
|
+
item["metadata"] = json.loads(item["metadata"])
|
|
244
|
+
result.append(item)
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def timing_summary(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object]:
|
|
249
|
+
"""Return the one canonical timing read model for a completed run.
|
|
250
|
+
|
|
251
|
+
``phase_aggregates`` and ``longest_individual_spans`` intentionally answer
|
|
252
|
+
different questions. Aggregates suppress only a same-category ancestor,
|
|
253
|
+
so a category is represented once without inventing a critical path.
|
|
254
|
+
Individual spans retain every observed occurrence (apart from the total
|
|
255
|
+
envelope) and are ranked independently. Consumers must use these fields
|
|
256
|
+
rather than deriving their own bottleneck order.
|
|
257
|
+
"""
|
|
258
|
+
spans = phase_spans(root, run_id, central_database=central_database)
|
|
259
|
+
historical_total: int | None = None
|
|
260
|
+
if not spans:
|
|
261
|
+
# Earlier runs already have a coarse immutable execution receipt. It
|
|
262
|
+
# remains useful total-duration evidence, but never becomes invented
|
|
263
|
+
# phase detail.
|
|
264
|
+
connection = _connection(root, central_database)
|
|
265
|
+
try:
|
|
266
|
+
row = connection.execute(
|
|
267
|
+
"SELECT total_execution_seconds FROM execution_runs WHERE run_id=?", (run_id,)
|
|
268
|
+
).fetchone()
|
|
269
|
+
finally:
|
|
270
|
+
connection.close()
|
|
271
|
+
if row and isinstance(row[0], (int, float)) and not isinstance(row[0], bool) and row[0] >= 0:
|
|
272
|
+
historical_total = round(float(row[0]) * 1000)
|
|
273
|
+
completed = [span for span in spans if span["outcome"] != "ACTIVE" and isinstance(span["duration_ms"], int)]
|
|
274
|
+
top_level = [span for span in completed if span["parent_phase_id"] is None]
|
|
275
|
+
by_id = {str(span["phase_id"]): span for span in completed}
|
|
276
|
+
|
|
277
|
+
def has_same_phase_ancestor(span: dict[str, object]) -> bool:
|
|
278
|
+
parent = span["parent_phase_id"]
|
|
279
|
+
while isinstance(parent, str) and parent in by_id:
|
|
280
|
+
ancestor = by_id[parent]
|
|
281
|
+
if ancestor["phase_name"] == span["phase_name"]:
|
|
282
|
+
return True
|
|
283
|
+
parent = ancestor["parent_phase_id"]
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
semantic = [span for span in completed if not has_same_phase_ancestor(span)]
|
|
287
|
+
by_phase: dict[str, int] = {}
|
|
288
|
+
for span in top_level:
|
|
289
|
+
by_phase[span["phase_name"]] = by_phase.get(span["phase_name"], 0) + int(span["duration_ms"])
|
|
290
|
+
total = by_phase.get("TOTAL_EXECUTION")
|
|
291
|
+
if total is None:
|
|
292
|
+
total = historical_total if historical_total is not None else sum(
|
|
293
|
+
duration for name, duration in by_phase.items() if name != "QUEUE_WAIT"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def timestamp(span: dict[str, object], key: str) -> datetime | None:
|
|
297
|
+
value = span.get(key)
|
|
298
|
+
if not isinstance(value, str):
|
|
299
|
+
return None
|
|
300
|
+
try:
|
|
301
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
302
|
+
except ValueError:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
total_envelopes = [
|
|
306
|
+
(timestamp(span, "started_at"), timestamp(span, "completed_at"))
|
|
307
|
+
for span in top_level
|
|
308
|
+
if span["phase_name"] == "TOTAL_EXECUTION"
|
|
309
|
+
]
|
|
310
|
+
total_envelopes = [
|
|
311
|
+
(started, completed) for started, completed in total_envelopes
|
|
312
|
+
if started is not None and completed is not None and completed >= started
|
|
313
|
+
]
|
|
314
|
+
if total_envelopes:
|
|
315
|
+
observed_total = round(sum((completed - started).total_seconds() * 1000 for started, completed in total_envelopes))
|
|
316
|
+
# Unit and recovery paths can preserve monotonic durations while their
|
|
317
|
+
# wall-clock timestamps are only boundary markers. Do not turn such
|
|
318
|
+
# non-comparable timestamps into invented overlap evidence.
|
|
319
|
+
if abs(observed_total - total) > 5_000:
|
|
320
|
+
total_envelopes = []
|
|
321
|
+
|
|
322
|
+
def envelope_overlap(span: dict[str, object]) -> int:
|
|
323
|
+
"""Return the measurable overlap with TOTAL_EXECUTION, never raw stale tail time."""
|
|
324
|
+
duration = int(span["duration_ms"])
|
|
325
|
+
if not total_envelopes:
|
|
326
|
+
return min(duration, total)
|
|
327
|
+
started, completed = timestamp(span, "started_at"), timestamp(span, "completed_at")
|
|
328
|
+
if started is None or completed is None or completed < started:
|
|
329
|
+
return min(duration, total)
|
|
330
|
+
overlap = sum(
|
|
331
|
+
max(0.0, (min(completed, envelope_end) - max(started, envelope_start)).total_seconds())
|
|
332
|
+
for envelope_start, envelope_end in total_envelopes
|
|
333
|
+
)
|
|
334
|
+
return min(duration, total, round(overlap * 1000))
|
|
335
|
+
|
|
336
|
+
def measured(name: str) -> int:
|
|
337
|
+
return sum(int(span["duration_ms"]) for span in semantic if span["phase_name"] == name)
|
|
338
|
+
|
|
339
|
+
provider = measured("PROVIDER_EXECUTION")
|
|
340
|
+
validation = measured("VALIDATION")
|
|
341
|
+
external = measured("EXTERNAL_CI_WAIT")
|
|
342
|
+
queue = by_phase.get("QUEUE_WAIT", 0)
|
|
343
|
+
report_generation = measured("REPORT_GENERATION")
|
|
344
|
+
evidence_persistence = measured("EVIDENCE_PERSISTENCE")
|
|
345
|
+
repository_finalization = measured("REPOSITORY_FINALIZATION")
|
|
346
|
+
active = max(0, total - external)
|
|
347
|
+
|
|
348
|
+
def has_processing_ancestor(span: dict[str, object]) -> bool:
|
|
349
|
+
"""Whether this work is already covered by an enclosing work span.
|
|
350
|
+
|
|
351
|
+
Provider command-boundary validation is deliberately nested under the
|
|
352
|
+
provider process. It remains independently measurable, but cannot
|
|
353
|
+
also consume a second portion of overhead. This ancestry rule keeps
|
|
354
|
+
the accounting partition deterministic even where the persisted UTC
|
|
355
|
+
timestamps have the normal small clock-resolution differences from
|
|
356
|
+
monotonic duration measurement.
|
|
357
|
+
"""
|
|
358
|
+
parent = span["parent_phase_id"]
|
|
359
|
+
while isinstance(parent, str) and parent in by_id:
|
|
360
|
+
if by_id[parent]["phase_name"] in {"PROVIDER_EXECUTION", "VALIDATION"}:
|
|
361
|
+
return True
|
|
362
|
+
parent = by_id[parent]["parent_phase_id"]
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
processing_coverage = sum(
|
|
366
|
+
envelope_overlap(span)
|
|
367
|
+
for span in semantic
|
|
368
|
+
if span["phase_name"] in {"PROVIDER_EXECUTION", "VALIDATION"}
|
|
369
|
+
and not has_processing_ancestor(span)
|
|
370
|
+
)
|
|
371
|
+
overhead = max(0, active - processing_coverage)
|
|
372
|
+
# Category aggregates use the same semantic selection as the named
|
|
373
|
+
# metrics. The deterministic category-name tie break keeps reports, API
|
|
374
|
+
# projections and dashboard detail identical.
|
|
375
|
+
aggregate_by_phase: dict[str, int] = {}
|
|
376
|
+
share_by_phase: dict[str, int] = {}
|
|
377
|
+
for span in semantic:
|
|
378
|
+
name = str(span["phase_name"])
|
|
379
|
+
if name != "TOTAL_EXECUTION":
|
|
380
|
+
aggregate_by_phase[name] = aggregate_by_phase.get(name, 0) + int(span["duration_ms"])
|
|
381
|
+
share_by_phase[name] = min(total, share_by_phase.get(name, 0) + envelope_overlap(span))
|
|
382
|
+
phase_aggregates = [
|
|
383
|
+
{"phase": phase, "duration_ms": duration}
|
|
384
|
+
for phase, duration in sorted(aggregate_by_phase.items(), key=lambda item: (-item[1], item[0]))
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
def span_label(span: dict[str, object]) -> str:
|
|
388
|
+
"""Give repeated spans bounded, typed context without prompt content."""
|
|
389
|
+
name = str(span["phase_name"])
|
|
390
|
+
metadata = span.get("metadata")
|
|
391
|
+
context: str | None = None
|
|
392
|
+
if isinstance(metadata, dict):
|
|
393
|
+
for key in ("validation_kind", "operation", "reason"):
|
|
394
|
+
value = metadata.get(key)
|
|
395
|
+
if isinstance(value, str) and value:
|
|
396
|
+
context = value.replace("_", " ")[:80]
|
|
397
|
+
break
|
|
398
|
+
if context is None and isinstance(metadata.get("iteration"), int):
|
|
399
|
+
context = f"iteration {metadata['iteration']}"
|
|
400
|
+
attempt = span.get("attempt")
|
|
401
|
+
if context:
|
|
402
|
+
return f"{name} — {context}"
|
|
403
|
+
if isinstance(attempt, int) and attempt > 1:
|
|
404
|
+
return f"{name} — attempt {attempt}"
|
|
405
|
+
return name
|
|
406
|
+
|
|
407
|
+
longest_individual_spans = [
|
|
408
|
+
{
|
|
409
|
+
"phase_id": item["phase_id"], "phase": item["phase_name"], "label": span_label(item),
|
|
410
|
+
"duration_ms": item["duration_ms"], "attempt": item["attempt"],
|
|
411
|
+
"ordinal": item["ordinal"], "outcome": item["outcome"],
|
|
412
|
+
}
|
|
413
|
+
for item in sorted(
|
|
414
|
+
(span for span in completed if span["phase_name"] != "TOTAL_EXECUTION"),
|
|
415
|
+
key=lambda item: (-int(item["duration_ms"]), str(item["phase_name"]), int(item["ordinal"])),
|
|
416
|
+
)
|
|
417
|
+
]
|
|
418
|
+
def share(value: int) -> float:
|
|
419
|
+
return round(value * 100 / total, 3) if total else 0.0
|
|
420
|
+
return {"phase_durations_ms": by_phase, "total_wall_time_ms": total,
|
|
421
|
+
"occurred_phases": tuple(sorted({str(span["phase_name"]) for span in completed})),
|
|
422
|
+
"active_ep_processing_time_ms": active, "provider_execution_time_ms": provider,
|
|
423
|
+
"validation_time_ms": validation, "external_wait_time_ms": external,
|
|
424
|
+
"queue_wait_time_ms": queue, "report_generation_time_ms": report_generation,
|
|
425
|
+
"evidence_persistence_time_ms": evidence_persistence,
|
|
426
|
+
"repository_finalization_time_ms": repository_finalization, "overhead_time_ms": overhead,
|
|
427
|
+
"provider_share_percent": share(share_by_phase.get("PROVIDER_EXECUTION", 0)), "validation_share_percent": share(share_by_phase.get("VALIDATION", 0)),
|
|
428
|
+
"external_wait_share_percent": share(share_by_phase.get("EXTERNAL_CI_WAIT", 0)), "queue_share_percent": share(share_by_phase.get("QUEUE_WAIT", 0)),
|
|
429
|
+
"overhead_share_percent": share(overhead),
|
|
430
|
+
"longest_phase": phase_aggregates[0]["phase"] if phase_aggregates else None,
|
|
431
|
+
"longest_phase_duration_ms": phase_aggregates[0]["duration_ms"] if phase_aggregates else None,
|
|
432
|
+
"phase_aggregates": phase_aggregates,
|
|
433
|
+
"phase_share_durations_ms": share_by_phase,
|
|
434
|
+
"top_phase_categories": phase_aggregates[:3],
|
|
435
|
+
"longest_individual_spans": longest_individual_spans[:3],
|
|
436
|
+
# Compatibility alias for pre-reconciliation API clients. It is
|
|
437
|
+
# intentionally category-only and no longer mixes individual spans.
|
|
438
|
+
"top_time_consumers": phase_aggregates[:3],
|
|
439
|
+
"phase_telemetry_available": bool(spans),
|
|
440
|
+
"historical_total_available": historical_total is not None}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Transaction-scoped Execution Host state; persistence remains in StateStore."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .agent_state import TransactionState
|
|
8
|
+
from .execution_lease import Lease
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ExecutionTransaction:
|
|
13
|
+
state: TransactionState
|
|
14
|
+
target_repository: Path
|
|
15
|
+
lease: Lease | None = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def run_id(self) -> str:
|
|
19
|
+
return self.state.run_id
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def execution_mode(self) -> str:
|
|
23
|
+
return self.state.execution_mode
|
|
24
|
+
|
|
25
|
+
def with_lease(self, lease: Lease) -> "ExecutionTransaction":
|
|
26
|
+
if lease.run_id != self.run_id:
|
|
27
|
+
raise ValueError("lease run identity conflicts with execution transaction")
|
|
28
|
+
return replace(self, lease=lease)
|