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,647 @@
|
|
|
1
|
+
"""Durable, one-shot provider interruption recovery evidence.
|
|
2
|
+
|
|
3
|
+
This module deliberately owns storage transitions only. The Execution Host
|
|
4
|
+
owns lifecycle decisions and the watcher only decides whether a host may be
|
|
5
|
+
resumed for an already existing run.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import sqlite3
|
|
15
|
+
import tempfile
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
from .provider_process_identity import ProcessIdentity, capture_process_identity, verify_process_identity
|
|
19
|
+
from .storage import EngineeringStorageError, open_storage, record_artifact, verify_artifact_integrity
|
|
20
|
+
from .execution_lease import liveness as lease_liveness
|
|
21
|
+
from .agent_state import PHASES, StateError, StateStore, redact_diagnostic
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
RECOVERY_STATES = frozenset({
|
|
25
|
+
"RECOVERY_AVAILABLE", "RECOVERY_STARTING", "RECOVERY_IN_PROGRESS",
|
|
26
|
+
"RECOVERED", "EXHAUSTED", "PRECHECK_FAILED", "AMBIGUOUS",
|
|
27
|
+
})
|
|
28
|
+
ACTIVE_RECOVERY_STATES = frozenset({"RECOVERY_AVAILABLE", "RECOVERY_STARTING", "RECOVERY_IN_PROGRESS"})
|
|
29
|
+
TERMINAL_RECOVERY_STATES = RECOVERY_STATES - ACTIVE_RECOVERY_STATES
|
|
30
|
+
CONTROLLED_INTERRUPTION_PHASES = frozenset({"QUALITY_CONTROL_AGENT"})
|
|
31
|
+
CONTROL_DIRECTORY = Path(".engineering/artifacts/provider-recovery-fault-injection")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _connection(root: Path, central_database: Path | None = None) -> sqlite3.Connection:
|
|
35
|
+
"""Open recovery authority from an explicit CENTRAL binding when supplied."""
|
|
36
|
+
if central_database is None:
|
|
37
|
+
return open_storage(root)
|
|
38
|
+
database = central_database.resolve()
|
|
39
|
+
if not database.is_file():
|
|
40
|
+
raise EngineeringStorageError("CENTRAL recovery database is unavailable")
|
|
41
|
+
connection = sqlite3.connect(database, isolation_level=None)
|
|
42
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
43
|
+
connection.execute("PRAGMA busy_timeout=10000")
|
|
44
|
+
return connection
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ControlledInterruptionControlError(ValueError):
|
|
48
|
+
"""A bounded operator control request is invalid or unsafe."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _now() -> str:
|
|
52
|
+
return datetime.now(timezone.utc).isoformat()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _control_paths(root: Path, run_id: str, phase: str) -> tuple[Path, Path]:
|
|
56
|
+
directory = root / CONTROL_DIRECTORY
|
|
57
|
+
return directory / "armed" / f"{run_id}-{phase}.json", directory / f"{run_id}-{phase}.json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def controlled_interruption_status(root: Path, *, run_id: str, phase: str) -> str:
|
|
61
|
+
armed, consumed = _control_paths(root, run_id, phase)
|
|
62
|
+
if consumed.is_file():
|
|
63
|
+
return "CONSUMED"
|
|
64
|
+
return "ARMED" if armed.is_file() else "NOT_ARMED"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _validate_control_target(root: Path, *, run_id: str, phase: str) -> object:
|
|
68
|
+
if phase not in CONTROLLED_INTERRUPTION_PHASES:
|
|
69
|
+
raise ControlledInterruptionControlError("phase is not supported for controlled interruption")
|
|
70
|
+
try:
|
|
71
|
+
state = StateStore(root / ".engineering" / "engineering-runs").load(run_id)
|
|
72
|
+
except StateError as error:
|
|
73
|
+
raise ControlledInterruptionControlError(str(error)) from error
|
|
74
|
+
if state.terminal:
|
|
75
|
+
raise ControlledInterruptionControlError("run is terminal")
|
|
76
|
+
# The hook is deliberately offered only before its lifecycle boundary. A
|
|
77
|
+
# phase already entered may have an active provider that cannot be raced.
|
|
78
|
+
if state.phase not in PHASES or state.phase in {"QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "FINALIZE_AGENT", "RECONCILE_AGENT", "WAIT_FOR_TERMINAL_EVIDENCE", "WAIT_FOR_OPERATOR_MERGE", "REPOSITORY_CLEANUP"}:
|
|
79
|
+
raise ControlledInterruptionControlError("target phase is already active or has passed")
|
|
80
|
+
return state
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def arm_controlled_interruption(root: Path, *, run_id: str, phase: str, armed_by: str | None = None, reason: str | None = None) -> dict[str, object]:
|
|
84
|
+
_validate_control_target(root, run_id=run_id, phase=phase)
|
|
85
|
+
armed, consumed = _control_paths(root, run_id, phase)
|
|
86
|
+
if consumed.is_file():
|
|
87
|
+
raise ControlledInterruptionControlError("controlled interruption is already consumed")
|
|
88
|
+
payload = {"version": 1, "state": "ARMED", "run_id": run_id, "phase": phase, "armed_at": _now(), "armed_by": redact_diagnostic(armed_by or os.environ.get("USER", "operator"), limit=120)}
|
|
89
|
+
if reason:
|
|
90
|
+
payload["reason"] = redact_diagnostic(reason, limit=240)
|
|
91
|
+
armed.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
92
|
+
try:
|
|
93
|
+
descriptor = os.open(armed, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
94
|
+
except FileExistsError as error:
|
|
95
|
+
raise ControlledInterruptionControlError("controlled interruption is already armed") from error
|
|
96
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
97
|
+
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
|
|
98
|
+
handle.write("\n")
|
|
99
|
+
handle.flush()
|
|
100
|
+
os.fsync(handle.fileno())
|
|
101
|
+
return payload
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def disarm_controlled_interruption(root: Path, *, run_id: str, phase: str) -> str:
|
|
105
|
+
armed, consumed = _control_paths(root, run_id, phase)
|
|
106
|
+
if consumed.is_file():
|
|
107
|
+
raise ControlledInterruptionControlError("controlled interruption is already consumed and cannot be disarmed")
|
|
108
|
+
try:
|
|
109
|
+
armed.unlink()
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
return "NOT_ARMED"
|
|
112
|
+
return "DISARMED"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def consume_controlled_interruption_hook(root: Path, *, run_id: str, phase: str) -> bool:
|
|
116
|
+
"""Durably consume the explicit qualification-only interruption hook.
|
|
117
|
+
|
|
118
|
+
The marker is a run-bound artifact rather than a provider recovery row:
|
|
119
|
+
the first invocation has not yet produced the canonical interruption
|
|
120
|
+
evidence needed to create that row. ``O_EXCL`` makes consumption survive
|
|
121
|
+
a host crash in that small interval and prevents an unchanged environment
|
|
122
|
+
setting from firing in a later host. It contains no prompt or provider
|
|
123
|
+
command data.
|
|
124
|
+
"""
|
|
125
|
+
requested = os.environ.get("ENGINEERING_PLATFORM_TEST_INTERRUPT_PROVIDER_ONCE")
|
|
126
|
+
armed, path = _control_paths(root, run_id, phase)
|
|
127
|
+
durable_armed = armed.is_file()
|
|
128
|
+
if (requested != f"{run_id}:{phase}" and not durable_armed) or load_recovery_state(root, run_id) is not None:
|
|
129
|
+
return False
|
|
130
|
+
artifact_id = f"provider-recovery-fault-injection:{run_id}:{phase}"
|
|
131
|
+
directory = root / CONTROL_DIRECTORY
|
|
132
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
133
|
+
payload = {
|
|
134
|
+
"kind": "CONTROLLED_PROVIDER_INTERRUPTION",
|
|
135
|
+
"run_id": run_id,
|
|
136
|
+
"phase": phase,
|
|
137
|
+
"consumed_at": _now(),
|
|
138
|
+
}
|
|
139
|
+
try:
|
|
140
|
+
if durable_armed:
|
|
141
|
+
# Claim the ARMED record by atomically turning it into the existing
|
|
142
|
+
# consumed marker. A concurrent disarm therefore cannot erase an
|
|
143
|
+
# already-claimed proof, and a crash cannot permit a refire.
|
|
144
|
+
os.replace(armed, path)
|
|
145
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_TRUNC)
|
|
146
|
+
else:
|
|
147
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
return False
|
|
150
|
+
except FileExistsError:
|
|
151
|
+
return False
|
|
152
|
+
try:
|
|
153
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
154
|
+
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
|
|
155
|
+
handle.write("\n")
|
|
156
|
+
handle.flush()
|
|
157
|
+
os.fsync(handle.fileno())
|
|
158
|
+
record_artifact(
|
|
159
|
+
root, path, artifact_id=artifact_id,
|
|
160
|
+
artifact_type="CONTROLLED_PROVIDER_INTERRUPTION", content_type="application/json",
|
|
161
|
+
created_at=str(payload["consumed_at"]), run_id=run_id,
|
|
162
|
+
)
|
|
163
|
+
except Exception:
|
|
164
|
+
# The exclusive marker remains intentionally: after an uncertain
|
|
165
|
+
# qualification-hook write, failing closed is safer than firing twice.
|
|
166
|
+
return False
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv: list[str] | None = None) -> int:
|
|
171
|
+
parser = argparse.ArgumentParser(description="Manage run-scoped controlled provider interruption proof controls")
|
|
172
|
+
parser.add_argument("command", choices=("arm-controlled-interruption", "controlled-interruption-status", "disarm-controlled-interruption"))
|
|
173
|
+
parser.add_argument("--repo", type=Path, default=Path.cwd())
|
|
174
|
+
parser.add_argument("--run-id", required=True)
|
|
175
|
+
parser.add_argument("--phase", required=True)
|
|
176
|
+
parser.add_argument("--reason")
|
|
177
|
+
args = parser.parse_args(argv)
|
|
178
|
+
root = args.repo.resolve()
|
|
179
|
+
try:
|
|
180
|
+
if args.command == "arm-controlled-interruption":
|
|
181
|
+
payload = arm_controlled_interruption(root, run_id=args.run_id, phase=args.phase, reason=args.reason)
|
|
182
|
+
print(json.dumps({"status": "ARMED", **payload}, sort_keys=True))
|
|
183
|
+
elif args.command == "controlled-interruption-status":
|
|
184
|
+
print(json.dumps({"status": controlled_interruption_status(root, run_id=args.run_id, phase=args.phase), "run_id": args.run_id, "phase": args.phase}, sort_keys=True))
|
|
185
|
+
else:
|
|
186
|
+
print(json.dumps({"status": disarm_controlled_interruption(root, run_id=args.run_id, phase=args.phase), "run_id": args.run_id, "phase": args.phase}, sort_keys=True))
|
|
187
|
+
except ControlledInterruptionControlError as error:
|
|
188
|
+
parser.error(str(error))
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
raise SystemExit(main())
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def load_recovery_state(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object] | None:
|
|
197
|
+
connection = _connection(root, central_database)
|
|
198
|
+
try:
|
|
199
|
+
row = connection.execute(
|
|
200
|
+
"SELECT run_id,recovery_ordinal,maximum_attempts,triggering_invocation_id,replacement_invocation_id,"
|
|
201
|
+
"lifecycle_phase,state,requested_at,launch_claimed_at,process_receipt_id,process_pid,process_group,"
|
|
202
|
+
"provider_session_id,provider_confirmed_active_at,completed_at,result,result_evidence_ref,branch,worktree_identity,lease_id,"
|
|
203
|
+
"fault_injection_consumed_at,diagnostic_code FROM provider_recovery_attempts WHERE run_id=?",
|
|
204
|
+
(run_id,),
|
|
205
|
+
).fetchone()
|
|
206
|
+
finally:
|
|
207
|
+
connection.close()
|
|
208
|
+
if row is None:
|
|
209
|
+
return None
|
|
210
|
+
keys = ("run_id", "recovery_ordinal", "maximum_attempts", "triggering_invocation_id", "replacement_invocation_id",
|
|
211
|
+
"lifecycle_phase", "state", "requested_at", "launch_claimed_at", "process_receipt_id", "process_pid",
|
|
212
|
+
"process_group", "provider_session_id", "provider_confirmed_active_at", "completed_at", "result", "result_evidence_ref", "branch",
|
|
213
|
+
"worktree_identity", "lease_id", "fault_injection_consumed_at", "diagnostic_code")
|
|
214
|
+
return dict(zip(keys, row, strict=True))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def watcher_resume_action(root: Path, run_id: str) -> str | None:
|
|
218
|
+
"""Return the sole watcher action derived from durable recovery evidence.
|
|
219
|
+
|
|
220
|
+
The watcher never allocates an invocation or interprets reports. A host
|
|
221
|
+
resume is allowed only to let the host own the corresponding controller
|
|
222
|
+
transition or consume a durable result.
|
|
223
|
+
"""
|
|
224
|
+
recovery = load_recovery_state(root, run_id)
|
|
225
|
+
if recovery is None:
|
|
226
|
+
return None
|
|
227
|
+
if recovery.get("state") == "RECOVERY_IN_PROGRESS":
|
|
228
|
+
# A verified session-bound provider is still owned work, even if its
|
|
229
|
+
# original host has disappeared. The watcher must not start a second
|
|
230
|
+
# host merely to rediscover it.
|
|
231
|
+
if reconcile_recovery(root, run_id=run_id) == "SAME_PROVIDER_STILL_ACTIVE":
|
|
232
|
+
return None
|
|
233
|
+
recovery = load_recovery_state(root, run_id)
|
|
234
|
+
if recovery is None:
|
|
235
|
+
return None
|
|
236
|
+
# A live canonical run lease proves another host still owns this run. The
|
|
237
|
+
# watcher must never race it by launching a second resumed host.
|
|
238
|
+
if lease_liveness(root, run_id).get("state") == "LIVE":
|
|
239
|
+
return None
|
|
240
|
+
state = recovery.get("state")
|
|
241
|
+
if state == "RECOVERED":
|
|
242
|
+
connection = open_storage(root)
|
|
243
|
+
try:
|
|
244
|
+
transaction = connection.execute(
|
|
245
|
+
"SELECT phase FROM engineering_transactions WHERE run_id=?", (run_id,)
|
|
246
|
+
).fetchone()
|
|
247
|
+
finally:
|
|
248
|
+
connection.close()
|
|
249
|
+
# Result consumption persists the next lifecycle phase before the
|
|
250
|
+
# host returns. A later watcher scan must not consume it again.
|
|
251
|
+
if transaction and transaction[0] != recovery.get("lifecycle_phase"):
|
|
252
|
+
return None
|
|
253
|
+
if state == "RECOVERY_AVAILABLE":
|
|
254
|
+
return "RESUME_AVAILABLE"
|
|
255
|
+
if state == "RECOVERY_STARTING":
|
|
256
|
+
return "RECONCILE_STARTING"
|
|
257
|
+
if state == "RECOVERY_IN_PROGRESS":
|
|
258
|
+
return "RECONCILE_IN_PROGRESS"
|
|
259
|
+
if state == "RECOVERED":
|
|
260
|
+
return "CONSUME_RECOVERED"
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _receipts(root: Path, run_id: str, invocation_id: str, *, central_database: Path | None = None) -> list[dict[str, object]]:
|
|
265
|
+
connection = _connection(root, central_database)
|
|
266
|
+
try:
|
|
267
|
+
rows = connection.execute(
|
|
268
|
+
"SELECT receipt_id,run_id,invocation_id,launch_state,provider_session_id,process_pid,process_group,"
|
|
269
|
+
"process_start_fingerprint,process_executable_identity,started_at,completed_at,outcome,result_evidence_ref FROM provider_invocation_receipts "
|
|
270
|
+
"WHERE run_id=? AND invocation_id=? ORDER BY started_at,receipt_id",
|
|
271
|
+
(run_id, invocation_id),
|
|
272
|
+
).fetchall()
|
|
273
|
+
finally:
|
|
274
|
+
connection.close()
|
|
275
|
+
keys = (
|
|
276
|
+
"receipt_id", "run_id", "invocation_id", "launch_state", "provider_session_id", "process_pid", "process_group",
|
|
277
|
+
"process_start_fingerprint", "process_executable_identity", "started_at", "completed_at", "outcome", "result_evidence_ref",
|
|
278
|
+
)
|
|
279
|
+
return [dict(zip(keys, row, strict=True)) for row in rows]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def record_pre_execution_launch_failure(root: Path, *, run_id: str, diagnostic_code: str,
|
|
283
|
+
central_database: Path | None = None) -> bool:
|
|
284
|
+
"""Record the only proof that a claimed launch did not reach a provider.
|
|
285
|
+
|
|
286
|
+
The real provider adapter invokes its process callback at the spawn
|
|
287
|
+
boundary. Therefore this may only be written by the host while the row is
|
|
288
|
+
still STARTING and no PROCESS_STARTED receipt exists.
|
|
289
|
+
"""
|
|
290
|
+
connection = _connection(root, central_database)
|
|
291
|
+
try:
|
|
292
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
293
|
+
row = connection.execute(
|
|
294
|
+
"SELECT replacement_invocation_id,state,provider_session_id FROM provider_recovery_attempts WHERE run_id=?", (run_id,)
|
|
295
|
+
).fetchone()
|
|
296
|
+
if row is None or row[1] != "RECOVERY_STARTING":
|
|
297
|
+
connection.execute("ROLLBACK")
|
|
298
|
+
return False
|
|
299
|
+
started = connection.execute(
|
|
300
|
+
"SELECT 1 FROM provider_invocation_receipts WHERE run_id=? AND invocation_id=? "
|
|
301
|
+
"AND launch_state='PROCESS_STARTED'",
|
|
302
|
+
(run_id, row[0]),
|
|
303
|
+
).fetchone()
|
|
304
|
+
if started:
|
|
305
|
+
connection.execute("ROLLBACK")
|
|
306
|
+
return False
|
|
307
|
+
changed = connection.execute(
|
|
308
|
+
"UPDATE provider_recovery_attempts SET diagnostic_code=? WHERE run_id=? AND state='RECOVERY_STARTING'",
|
|
309
|
+
(f"LAUNCH_NOT_STARTED:{diagnostic_code[:120]}", run_id),
|
|
310
|
+
).rowcount
|
|
311
|
+
connection.execute("COMMIT")
|
|
312
|
+
return changed == 1
|
|
313
|
+
except Exception:
|
|
314
|
+
connection.execute("ROLLBACK")
|
|
315
|
+
raise
|
|
316
|
+
finally:
|
|
317
|
+
connection.close()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reconcile_recovery(root: Path, *, run_id: str, verifier=verify_process_identity,
|
|
321
|
+
central_database: Path | None = None) -> str:
|
|
322
|
+
"""Classify a restart solely from immutable receipts and recovery state.
|
|
323
|
+
|
|
324
|
+
A live provider is accepted only when its persisted session, birth
|
|
325
|
+
fingerprint and executable identity verify as one invocation-bound unit.
|
|
326
|
+
"""
|
|
327
|
+
recovery = load_recovery_state(root, run_id, central_database=central_database)
|
|
328
|
+
if recovery is None:
|
|
329
|
+
return "NOT_APPLICABLE"
|
|
330
|
+
state = recovery.get("state")
|
|
331
|
+
invocation_id = recovery.get("replacement_invocation_id")
|
|
332
|
+
if not isinstance(invocation_id, str):
|
|
333
|
+
mark_ambiguous(root, run_id=run_id, expected=str(state), diagnostic_code="missing_replacement_invocation", central_database=central_database)
|
|
334
|
+
return "AMBIGUOUS"
|
|
335
|
+
receipts = _receipts(root, run_id, invocation_id, central_database=central_database)
|
|
336
|
+
claims = [receipt for receipt in receipts if receipt["launch_state"] == "CLAIMED"]
|
|
337
|
+
starts = [receipt for receipt in receipts if receipt["launch_state"] == "PROCESS_STARTED"]
|
|
338
|
+
terminals = [receipt for receipt in receipts if receipt["launch_state"] == "TERMINAL"]
|
|
339
|
+
outcomes = {str(receipt["outcome"]) for receipt in terminals if receipt["outcome"] is not None}
|
|
340
|
+
if len(claims) > 1 or len(starts) > 1 or len(terminals) > 1 or len(outcomes) > 1:
|
|
341
|
+
if state in {"RECOVERY_STARTING", "RECOVERY_IN_PROGRESS"}:
|
|
342
|
+
mark_ambiguous(root, run_id=run_id, expected=str(state), diagnostic_code="contradictory_receipts", central_database=central_database)
|
|
343
|
+
return "AMBIGUOUS"
|
|
344
|
+
if state == "RECOVERY_STARTING":
|
|
345
|
+
if starts or terminals:
|
|
346
|
+
# Receipt ordering/state mismatch is never repaired by a launch.
|
|
347
|
+
mark_ambiguous(root, run_id=run_id, expected="RECOVERY_STARTING", diagnostic_code="starting_receipt_conflict", central_database=central_database)
|
|
348
|
+
return "AMBIGUOUS"
|
|
349
|
+
if not claims:
|
|
350
|
+
return "LAUNCH_UNCLAIMED"
|
|
351
|
+
diagnostic = str(recovery.get("diagnostic_code") or "")
|
|
352
|
+
if diagnostic.startswith("LAUNCH_NOT_STARTED:"):
|
|
353
|
+
return "LAUNCH_CLAIMED_PREEXEC_FAILURE"
|
|
354
|
+
mark_ambiguous(root, run_id=run_id, expected="RECOVERY_STARTING", diagnostic_code="claimed_launch_unresolved", central_database=central_database)
|
|
355
|
+
return "AMBIGUOUS"
|
|
356
|
+
if state == "RECOVERY_IN_PROGRESS":
|
|
357
|
+
if len(starts) != 1:
|
|
358
|
+
mark_ambiguous(root, run_id=run_id, expected="RECOVERY_IN_PROGRESS", diagnostic_code="missing_process_started_receipt", central_database=central_database)
|
|
359
|
+
return "AMBIGUOUS"
|
|
360
|
+
if not terminals:
|
|
361
|
+
start = starts[0]
|
|
362
|
+
session_id = recovery.get("provider_session_id")
|
|
363
|
+
if (
|
|
364
|
+
not isinstance(session_id, str)
|
|
365
|
+
or start.get("provider_session_id") != session_id
|
|
366
|
+
or not isinstance(start.get("process_pid"), int)
|
|
367
|
+
or not isinstance(start.get("process_group"), int)
|
|
368
|
+
or not isinstance(start.get("process_start_fingerprint"), str)
|
|
369
|
+
or not isinstance(start.get("process_executable_identity"), str)
|
|
370
|
+
):
|
|
371
|
+
mark_ambiguous(root, run_id=run_id, expected="RECOVERY_IN_PROGRESS", diagnostic_code="provider_session_identity_invalid", central_database=central_database)
|
|
372
|
+
return "AMBIGUOUS"
|
|
373
|
+
verification = verifier(ProcessIdentity(
|
|
374
|
+
pid=int(start["process_pid"]), process_group=int(start["process_group"]),
|
|
375
|
+
start_fingerprint=str(start["process_start_fingerprint"]),
|
|
376
|
+
executable_identity=str(start["process_executable_identity"]),
|
|
377
|
+
))
|
|
378
|
+
if verification == "MATCH":
|
|
379
|
+
return "SAME_PROVIDER_STILL_ACTIVE"
|
|
380
|
+
mark_ambiguous(
|
|
381
|
+
root, run_id=run_id, expected="RECOVERY_IN_PROGRESS",
|
|
382
|
+
diagnostic_code="provider_process_not_active" if verification == "NOT_ACTIVE" else "provider_process_identity_mismatch",
|
|
383
|
+
central_database=central_database,
|
|
384
|
+
)
|
|
385
|
+
return "AMBIGUOUS"
|
|
386
|
+
outcome = next(iter(outcomes), "")
|
|
387
|
+
if outcome == "SUCCESS" and isinstance(terminals[0].get("result_evidence_ref"), str):
|
|
388
|
+
transition_recovery_state(
|
|
389
|
+
root, run_id=run_id, expected="RECOVERY_IN_PROGRESS", target="RECOVERED",
|
|
390
|
+
result="SUCCESS", result_evidence_ref=str(terminals[0]["result_evidence_ref"]),
|
|
391
|
+
central_database=central_database,
|
|
392
|
+
)
|
|
393
|
+
return "RECOVERED"
|
|
394
|
+
if outcome == "INTERRUPTED":
|
|
395
|
+
transition_recovery_state(root, run_id=run_id, expected="RECOVERY_IN_PROGRESS", target="EXHAUSTED", result="INTERRUPTED", central_database=central_database)
|
|
396
|
+
return "EXHAUSTED"
|
|
397
|
+
mark_ambiguous(root, run_id=run_id, expected="RECOVERY_IN_PROGRESS", diagnostic_code="terminal_failure_or_invalid_result", central_database=central_database)
|
|
398
|
+
return "AMBIGUOUS"
|
|
399
|
+
return str(state)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def create_recovery_available(
|
|
403
|
+
root: Path, *, run_id: str, triggering_invocation_id: str, lifecycle_phase: str,
|
|
404
|
+
branch: str | None, worktree_identity: str, lease_id: str | None,
|
|
405
|
+
central_database: Path | None = None,
|
|
406
|
+
) -> dict[str, object]:
|
|
407
|
+
"""Create the only automatic-recovery budget atomically and idempotently."""
|
|
408
|
+
replacement = f"provider-recovery-{run_id}-{uuid4().hex[:12]}"
|
|
409
|
+
now = _now()
|
|
410
|
+
connection = _connection(root, central_database)
|
|
411
|
+
try:
|
|
412
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
413
|
+
connection.execute(
|
|
414
|
+
"INSERT OR IGNORE INTO provider_recovery_attempts("
|
|
415
|
+
"run_id,recovery_ordinal,maximum_attempts,triggering_invocation_id,replacement_invocation_id,"
|
|
416
|
+
"lifecycle_phase,state,requested_at,branch,worktree_identity,lease_id) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
|
417
|
+
(run_id, 1, 1, triggering_invocation_id, replacement, lifecycle_phase,
|
|
418
|
+
"RECOVERY_AVAILABLE", now, branch, worktree_identity, lease_id),
|
|
419
|
+
)
|
|
420
|
+
connection.execute("COMMIT")
|
|
421
|
+
except Exception:
|
|
422
|
+
connection.execute("ROLLBACK")
|
|
423
|
+
raise
|
|
424
|
+
finally:
|
|
425
|
+
connection.close()
|
|
426
|
+
state = load_recovery_state(root, run_id, central_database=central_database)
|
|
427
|
+
if state is None:
|
|
428
|
+
raise EngineeringStorageError("Provider recovery evidence could not be persisted.")
|
|
429
|
+
return state
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def transition_recovery_state(
|
|
433
|
+
root: Path, *, run_id: str, expected: str, target: str, diagnostic_code: str | None = None,
|
|
434
|
+
result: str | None = None, result_evidence_ref: str | None = None,
|
|
435
|
+
central_database: Path | None = None,
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""Compare-and-swap one durable recovery transition."""
|
|
438
|
+
if expected not in RECOVERY_STATES or target not in RECOVERY_STATES:
|
|
439
|
+
raise ValueError("invalid provider recovery transition")
|
|
440
|
+
completed = _now() if target in TERMINAL_RECOVERY_STATES else None
|
|
441
|
+
connection = _connection(root, central_database)
|
|
442
|
+
try:
|
|
443
|
+
changed = connection.execute(
|
|
444
|
+
"UPDATE provider_recovery_attempts SET state=?,diagnostic_code=COALESCE(?,diagnostic_code),"
|
|
445
|
+
"result=COALESCE(?,result),result_evidence_ref=COALESCE(?,result_evidence_ref),"
|
|
446
|
+
"completed_at=COALESCE(?,completed_at) WHERE run_id=? AND state=?",
|
|
447
|
+
(target, diagnostic_code, result, result_evidence_ref, completed, run_id, expected),
|
|
448
|
+
).rowcount
|
|
449
|
+
finally:
|
|
450
|
+
connection.close()
|
|
451
|
+
return changed == 1
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def claim_replacement_launch(root: Path, *, run_id: str, central_database: Path | None = None) -> dict[str, object] | None:
|
|
455
|
+
"""Claim the exact persisted replacement intent once, before process spawn."""
|
|
456
|
+
now = _now()
|
|
457
|
+
connection = _connection(root, central_database)
|
|
458
|
+
try:
|
|
459
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
460
|
+
row = connection.execute(
|
|
461
|
+
"SELECT replacement_invocation_id,state,provider_session_id FROM provider_recovery_attempts WHERE run_id=?", (run_id,)
|
|
462
|
+
).fetchone()
|
|
463
|
+
if row is None or row[1] != "RECOVERY_STARTING":
|
|
464
|
+
connection.execute("ROLLBACK")
|
|
465
|
+
return None
|
|
466
|
+
provider_session_id = row[2] or f"provider-session-{uuid4().hex}"
|
|
467
|
+
receipt_id = f"provider-launch-{uuid4().hex}"
|
|
468
|
+
connection.execute(
|
|
469
|
+
"INSERT INTO provider_invocation_receipts(receipt_id,run_id,invocation_id,launch_state,provider_session_id,started_at) VALUES(?,?,?,?,?,?)",
|
|
470
|
+
(receipt_id, run_id, row[0], "CLAIMED", provider_session_id, now),
|
|
471
|
+
)
|
|
472
|
+
changed = connection.execute(
|
|
473
|
+
"UPDATE provider_recovery_attempts SET provider_session_id=?,launch_claimed_at=?,process_receipt_id=? "
|
|
474
|
+
"WHERE run_id=? AND state='RECOVERY_STARTING'",
|
|
475
|
+
(provider_session_id, now, receipt_id, run_id),
|
|
476
|
+
).rowcount
|
|
477
|
+
if changed != 1:
|
|
478
|
+
connection.execute("ROLLBACK")
|
|
479
|
+
return None
|
|
480
|
+
connection.execute("COMMIT")
|
|
481
|
+
return {"receipt_id": receipt_id, "invocation_id": row[0], "provider_session_id": provider_session_id, "claimed_at": now}
|
|
482
|
+
except sqlite3.IntegrityError:
|
|
483
|
+
connection.execute("ROLLBACK")
|
|
484
|
+
return None
|
|
485
|
+
except Exception:
|
|
486
|
+
connection.execute("ROLLBACK")
|
|
487
|
+
raise
|
|
488
|
+
finally:
|
|
489
|
+
connection.close()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def record_provider_started(
|
|
493
|
+
root: Path, *, run_id: str, receipt_id: str, pid: int, process_group: int,
|
|
494
|
+
identity: ProcessIdentity | None = None, central_database: Path | None = None,
|
|
495
|
+
) -> bool:
|
|
496
|
+
"""Append immutable process-start evidence and enter IN_PROGRESS."""
|
|
497
|
+
now = _now()
|
|
498
|
+
connection = _connection(root, central_database)
|
|
499
|
+
try:
|
|
500
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
501
|
+
row = connection.execute(
|
|
502
|
+
"SELECT replacement_invocation_id,state,provider_session_id FROM provider_recovery_attempts WHERE run_id=?", (run_id,)
|
|
503
|
+
).fetchone()
|
|
504
|
+
if row is None or row[1] != "RECOVERY_STARTING" or not isinstance(row[2], str):
|
|
505
|
+
connection.execute("ROLLBACK")
|
|
506
|
+
return False
|
|
507
|
+
identity = identity or capture_process_identity(pid, process_group)
|
|
508
|
+
if identity is None:
|
|
509
|
+
connection.execute("ROLLBACK")
|
|
510
|
+
return False
|
|
511
|
+
connection.execute(
|
|
512
|
+
"INSERT INTO provider_invocation_receipts(receipt_id,run_id,invocation_id,launch_state,provider_session_id,process_pid,process_group,process_start_fingerprint,process_executable_identity,started_at) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
|
513
|
+
(f"provider-start-{uuid4().hex}", run_id, row[0], "PROCESS_STARTED", row[2], pid, process_group,
|
|
514
|
+
identity.start_fingerprint, identity.executable_identity, now),
|
|
515
|
+
)
|
|
516
|
+
changed = connection.execute(
|
|
517
|
+
"UPDATE provider_recovery_attempts SET state='RECOVERY_IN_PROGRESS',process_receipt_id=?,process_pid=?,process_group=?,provider_confirmed_active_at=? WHERE run_id=? AND state='RECOVERY_STARTING'",
|
|
518
|
+
(receipt_id, pid, process_group, now, run_id),
|
|
519
|
+
).rowcount
|
|
520
|
+
connection.execute("COMMIT")
|
|
521
|
+
return changed == 1
|
|
522
|
+
except Exception:
|
|
523
|
+
connection.execute("ROLLBACK")
|
|
524
|
+
raise
|
|
525
|
+
finally:
|
|
526
|
+
connection.close()
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def record_replacement_terminal(
|
|
530
|
+
root: Path, *, run_id: str, outcome: str, result_evidence_ref: str | None = None,
|
|
531
|
+
central_database: Path | None = None,
|
|
532
|
+
) -> bool:
|
|
533
|
+
"""Append terminal provider evidence once and advance the recovery state.
|
|
534
|
+
|
|
535
|
+
``SUCCESS`` requires a separately persisted structured-result artifact.
|
|
536
|
+
Interrupted attempt two is exhaustion; any unknown outcome is deliberately
|
|
537
|
+
ambiguous instead of becoming another launch opportunity.
|
|
538
|
+
"""
|
|
539
|
+
target = {
|
|
540
|
+
"SUCCESS": "RECOVERED",
|
|
541
|
+
"INTERRUPTED": "EXHAUSTED",
|
|
542
|
+
"FAILED": "AMBIGUOUS",
|
|
543
|
+
}.get(outcome)
|
|
544
|
+
if target is None:
|
|
545
|
+
raise ValueError("invalid provider recovery terminal outcome")
|
|
546
|
+
now = _now()
|
|
547
|
+
connection = _connection(root, central_database)
|
|
548
|
+
try:
|
|
549
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
550
|
+
row = connection.execute(
|
|
551
|
+
"SELECT replacement_invocation_id,state,provider_session_id FROM provider_recovery_attempts WHERE run_id=?", (run_id,)
|
|
552
|
+
).fetchone()
|
|
553
|
+
if row is None or row[1] != "RECOVERY_IN_PROGRESS":
|
|
554
|
+
connection.execute("ROLLBACK")
|
|
555
|
+
return False
|
|
556
|
+
connection.execute(
|
|
557
|
+
"INSERT INTO provider_invocation_receipts(receipt_id,run_id,invocation_id,launch_state,provider_session_id,started_at,completed_at,outcome,result_evidence_ref) VALUES(?,?,?,?,?,?,?,?,?)",
|
|
558
|
+
(f"provider-terminal-{uuid4().hex}", run_id, row[0], "TERMINAL", row[2], now, now, outcome, result_evidence_ref),
|
|
559
|
+
)
|
|
560
|
+
changed = connection.execute(
|
|
561
|
+
"UPDATE provider_recovery_attempts SET state=?,completed_at=?,result=?,result_evidence_ref=? WHERE run_id=? AND state='RECOVERY_IN_PROGRESS'",
|
|
562
|
+
(target, now, outcome, result_evidence_ref, run_id),
|
|
563
|
+
).rowcount
|
|
564
|
+
connection.execute("COMMIT")
|
|
565
|
+
return changed == 1
|
|
566
|
+
except Exception:
|
|
567
|
+
connection.execute("ROLLBACK")
|
|
568
|
+
raise
|
|
569
|
+
finally:
|
|
570
|
+
connection.close()
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def mark_precheck_failed(root: Path, *, run_id: str, diagnostic_code: str,
|
|
574
|
+
central_database: Path | None = None) -> bool:
|
|
575
|
+
return transition_recovery_state(
|
|
576
|
+
root, run_id=run_id, expected="RECOVERY_AVAILABLE", target="PRECHECK_FAILED",
|
|
577
|
+
diagnostic_code=diagnostic_code, central_database=central_database,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def mark_ambiguous(root: Path, *, run_id: str, expected: str, diagnostic_code: str,
|
|
582
|
+
central_database: Path | None = None) -> bool:
|
|
583
|
+
return transition_recovery_state(
|
|
584
|
+
root, run_id=run_id, expected=expected, target="AMBIGUOUS", diagnostic_code=diagnostic_code,
|
|
585
|
+
central_database=central_database,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def persist_recovery_agent_result(root: Path, *, run_id: str, invocation_id: str, result: object,
|
|
590
|
+
central_database: Path | None = None, artifact_root: Path | None = None) -> str:
|
|
591
|
+
"""Persist the bounded structured AgentResult once for post-crash consumption."""
|
|
592
|
+
fields = ("terminal_state", "branch", "pull_request", "terminal_condition", "diagnostic", "repository_path", "commit_sha", "validation_evidence", "quality_evidence", "validation_disposition")
|
|
593
|
+
payload = {field: getattr(result, field) for field in fields}
|
|
594
|
+
artifact_id = f"provider-recovery-result:{run_id}:{invocation_id}"
|
|
595
|
+
directory = ((artifact_root / "provider-recovery-results") if artifact_root else
|
|
596
|
+
(root / ".engineering" / "artifacts" / "provider-recovery-results"))
|
|
597
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
598
|
+
path = directory / f"{invocation_id}.json"
|
|
599
|
+
descriptor, temporary = tempfile.mkstemp(prefix=f".{invocation_id}.", suffix=".tmp", dir=directory)
|
|
600
|
+
try:
|
|
601
|
+
os.fchmod(descriptor, 0o600)
|
|
602
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
603
|
+
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
|
|
604
|
+
handle.write("\n")
|
|
605
|
+
handle.flush()
|
|
606
|
+
os.fsync(handle.fileno())
|
|
607
|
+
os.replace(temporary, path)
|
|
608
|
+
finally:
|
|
609
|
+
Path(temporary).unlink(missing_ok=True)
|
|
610
|
+
record_artifact(
|
|
611
|
+
root, path, artifact_id=artifact_id, artifact_type="PROVIDER_RECOVERY_AGENT_RESULT",
|
|
612
|
+
content_type="application/json", created_at=_now(), run_id=run_id, execution_id=invocation_id,
|
|
613
|
+
central_database=central_database, artifact_root=artifact_root,
|
|
614
|
+
)
|
|
615
|
+
return f"artifact:{artifact_id}"
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def load_recovery_agent_result(
|
|
619
|
+
root: Path, reference: str, *, run_id: str, invocation_id: str,
|
|
620
|
+
central_database: Path | None = None, artifact_root: Path | None = None,
|
|
621
|
+
) -> dict[str, object] | None:
|
|
622
|
+
if not reference.startswith("artifact:"):
|
|
623
|
+
return None
|
|
624
|
+
artifact_id = reference.removeprefix("artifact:")
|
|
625
|
+
if not verify_artifact_integrity(
|
|
626
|
+
root, artifact_id, central_database=central_database, artifact_root=artifact_root,
|
|
627
|
+
):
|
|
628
|
+
return None
|
|
629
|
+
connection = _connection(root, central_database)
|
|
630
|
+
try:
|
|
631
|
+
row = connection.execute(
|
|
632
|
+
"SELECT storage_location,artifact_type FROM execution_artifact_records "
|
|
633
|
+
"WHERE artifact_id=? AND execution_id=? AND (run_id=? OR run_id IS NULL)",
|
|
634
|
+
(artifact_id, invocation_id, run_id),
|
|
635
|
+
).fetchone()
|
|
636
|
+
finally:
|
|
637
|
+
connection.close()
|
|
638
|
+
if not row or row[1] != "PROVIDER_RECOVERY_AGENT_RESULT":
|
|
639
|
+
return None
|
|
640
|
+
try:
|
|
641
|
+
authority_root = artifact_root.resolve() if artifact_root is not None else (root / ".engineering").resolve()
|
|
642
|
+
payload_path = (authority_root / str(row[0])).resolve()
|
|
643
|
+
payload_path.relative_to(authority_root)
|
|
644
|
+
payload = json.loads(payload_path.read_text(encoding="utf-8"))
|
|
645
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
646
|
+
return None
|
|
647
|
+
return payload if isinstance(payload, dict) else None
|