Skip to content

Commit d29b0fd

Browse files
solomonneascodex
andauthored
feat: journal run lifecycle transitions (#608)
Co-authored-by: Codex <codex@openai.com>
1 parent 673d669 commit d29b0fd

5 files changed

Lines changed: 1016 additions & 5 deletions

File tree

src/brigade/aboyeur.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from . import localio
2828
from . import proc, receipt_schema, runguard
2929
from . import run_control
30+
from . import run_lifecycle
3031
from .result_integrity import validate_final_output
3132
from .run_receipts import (
3233
agent_result_from_worker as _agent_result_from_worker,
@@ -489,6 +490,17 @@ def _write_json(path: Path, payload: object) -> None:
489490
# run.json is polled by `brigade runs watch/steer/interrupt` while the run
490491
# rewrites it, so the write must be atomic or a concurrent reader can
491492
# observe a truncated file.
493+
if path.name == "run.json" and isinstance(payload, dict):
494+
status = payload.get("status")
495+
if isinstance(status, str) and status:
496+
run_lifecycle.record_lifecycle_transition(
497+
path.parent,
498+
status=status,
499+
# The receipt payload identifies the lock workspace
500+
# (lock_workspace or cwd); the run directory layout does not.
501+
workspace=runguard.resolve_run_lock_workspace(payload, path.parent),
502+
incoming_snapshot=payload,
503+
)
492504
localio.write_text_atomic(path, json.dumps(payload, indent=2, sort_keys=True) + "\n")
493505

494506

@@ -1937,7 +1949,7 @@ def record_artifact_collection(
19371949
payload["artifact_collection"] = collection
19381950
try:
19391951
_write_json(run_path, receipt_schema.stamp_run_receipt(payload))
1940-
except OSError as exc:
1952+
except (OSError, run_lifecycle.LifecycleJournalError) as exc:
19411953
raise runguard.RetainRunLockError(f"failed to update run receipt after artifact collection: {exc}") from exc
19421954

19431955

@@ -2010,7 +2022,7 @@ def record_run_termination(
20102022
)
20112023
try:
20122024
_write_json(run_path, receipt_schema.stamp_run_receipt(payload))
2013-
except OSError as exc:
2025+
except (OSError, run_lifecycle.LifecycleJournalError) as exc:
20142026
raise runguard.RetainRunLockError(f"failed to write terminal run receipt: {exc}") from exc
20152027

20162028

@@ -2038,7 +2050,7 @@ def record_dispatch_stage(output_dir: Path, *, stage: int, seats: tuple[str, ...
20382050
)
20392051
try:
20402052
_write_json(run_path, receipt_schema.stamp_run_receipt(payload))
2041-
except OSError as exc:
2053+
except (OSError, run_lifecycle.LifecycleJournalError) as exc:
20422054
raise runguard.RetainRunLockError(f"failed to write dispatch stage receipt: {exc}") from exc
20432055

20442056

@@ -2067,7 +2079,7 @@ def record_result_processing(output_dir: Path, *, seat: str) -> None:
20672079
payload.pop("active_seats", None)
20682080
try:
20692081
_write_json(run_path, receipt_schema.stamp_run_receipt(payload))
2070-
except OSError as exc:
2082+
except (OSError, run_lifecycle.LifecycleJournalError) as exc:
20712083
raise runguard.RetainRunLockError(f"failed to record result-processing phase: {exc}") from exc
20722084

20732085

@@ -2148,6 +2160,7 @@ def _run_payload(
21482160
include_git: bool = True,
21492161
pre_run_snapshot: dict[str, object] | None = None,
21502162
scheduler: dict[str, object] | None = None,
2163+
lifecycle_journal_requested: bool | None = None,
21512164
) -> dict[str, object]:
21522165
payload: dict[str, object] = {
21532166
"schema": receipt_schema.RUN_RECEIPT_SCHEMA,
@@ -2182,6 +2195,8 @@ def _run_payload(
21822195
payload["cwd"] = str(cwd)
21832196
if scheduler is not None:
21842197
payload["scheduler"] = scheduler
2198+
if lifecycle_journal_requested:
2199+
payload["lifecycle_journal_requested"] = True
21852200
if resolution := _roster_resolution_payload(roster):
21862201
payload["roster"] = resolution
21872202
if lock_workspace is not None:
@@ -2280,6 +2295,19 @@ def record_run_start(
22802295
output_dir = output_dir.expanduser().resolve()
22812296
started_at = started_at or datetime.now(timezone.utc)
22822297
output_dir.mkdir(parents=True, exist_ok=True)
2298+
run_json = output_dir / "run.json"
2299+
run_json_exists = run_json.is_file()
2300+
existing_requested = False
2301+
if run_json_exists:
2302+
try:
2303+
existing = json.loads(run_json.read_text())
2304+
except (OSError, json.JSONDecodeError):
2305+
existing = None
2306+
if isinstance(existing, dict) and existing.get("lifecycle_journal_requested") is True:
2307+
existing_requested = True
2308+
lifecycle_requested = existing_requested or (
2309+
not run_json_exists and run_lifecycle.is_lifecycle_journaling_enabled()
2310+
)
22832311
_write_json(
22842312
output_dir / "run.json",
22852313
_run_payload(
@@ -2301,6 +2329,7 @@ def record_run_start(
23012329
scheduler=(
23022330
{"requested": scheduler, "used": None, "fallback_reason": None} if scheduler is not None else None
23032331
),
2332+
lifecycle_journal_requested=True if lifecycle_requested else None,
23042333
),
23052334
)
23062335
_write_json(output_dir / "roster.json", _roster_payload(roster))
@@ -2488,6 +2517,15 @@ def scheduler_resolved(used: str, fallback_reason: str | None) -> None:
24882517
def _payload(**kwargs: Any) -> dict[str, object]:
24892518
if "skill_route_policy" not in kwargs and skill_policy is not None:
24902519
kwargs["skill_route_policy"] = skill_policy
2520+
if output_dir is not None and "lifecycle_journal_requested" not in kwargs:
2521+
run_path = output_dir / "run.json"
2522+
if run_path.is_file():
2523+
try:
2524+
existing = json.loads(run_path.read_text())
2525+
except (OSError, json.JSONDecodeError):
2526+
existing = None
2527+
if isinstance(existing, dict) and existing.get("lifecycle_journal_requested") is True:
2528+
kwargs["lifecycle_journal_requested"] = True
24912529
return _run_payload(
24922530
lock_workspace=lock_workspace,
24932531
pre_run_snapshot=pre_run_snapshot_payload,

0 commit comments

Comments
 (0)