Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bad040c
fix(backend): make the case suite reject invalid manifests and forged…
SamGu-NRX Oct 15, 2026
83d93f0
Integrate burst8-bs-replay-acceptance runner enforcement (producer 5f…
SamGu-NRX Oct 16, 2026
a73c5c0
Integrate burst8-bs-supported-case-pairs packet (producer e7ad2abc, s…
SamGu-NRX Oct 16, 2026
b4d28f4
Reconcile twin-identity manifest with the hardened case loader
SamGu-NRX Oct 16, 2026
250c043
Reconcile packet acceptance with the real merge and bridge contracts
ObviousApp Oct 16, 2026
3dbe72b
Add end-to-end CLI acceptance tests for the supported packet
ObviousApp Oct 16, 2026
ba7eba2
Keep legacy-manifest CLI tests on the explicit legacy manifest
ObviousApp Oct 16, 2026
596ed9a
fix(backend): import the #258/#262 receipt fixes; a crashed run or re…
SamGu-NRX Oct 16, 2026
54a5a5b
fix(backend): restore the run_cases dev env; the flags must not leak …
SamGu-NRX Oct 16, 2026
b9032a7
fix(backend): sensitivity coverage bands are validated but are not ob…
SamGu-NRX Oct 16, 2026
a993208
fix(backend): materialize missing stage records so an older unfinishe…
SamGu-NRX Oct 16, 2026
c4632e3
fix(backend): placement refuses objects_3d artifacts without an expli…
SamGu-NRX Oct 16, 2026
e2376df
fix(backend): supported capture fixtures carry their observed coverag…
SamGu-NRX Oct 16, 2026
1327752
fix(backend): stage-record recovery claims are atomic, so a racing co…
SamGu-NRX Oct 16, 2026
fac367e
Merge origin/alpha into obv/burst8-bs-capture-path-integration
ObviousApp Oct 16, 2026
c3e479b
fix(backend): the legacy manifest's cross-capture now evaluates on th…
ObviousApp Oct 16, 2026
31ca9ef
fix(backend): claim only the stage records the tick found missing
SamGu-NRX Oct 16, 2026
df1b2b5
fix(backend): one stage-claim lock instead of a lock per run and stage
SamGu-NRX Oct 16, 2026
380bb27
Merge origin/alpha into obv/burst8-bs-capture-path-integration
SamGu-NRX Oct 16, 2026
3daefe4
test(backend): the tap case merges exactly one vent on every committe…
SamGu-NRX Oct 16, 2026
2e469fc
fix(backend): a late correction does not reopen a run without stage r…
SamGu-NRX Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions backend/scripts/capture_placement_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
- The ``objects_3d`` stage boundary gets an explicit, successful object list for
every case: an empty list where the source scene already carries the hazards,
and one nonempty observed tap hazard case whose single hazard the coordinator's
one-time merge must place into the merged scene exactly once. The default
objects stubs never run on this path: a capture with no registered injection
fails loudly instead of fabricating a hazard.
one-time merge must place into the merged scene exactly once — delivered only
when the case's source does not already carry that hazard id, because the merge
contract replaces only detector/VLM twins and a same-id re-delivery of a
tap-sourced hazard would duplicate the id and be refused. The default objects
stubs never run on this path: a capture with no registered injection fails
loudly instead of fabricating a hazard.
- Every other unavailable vision stage stays explicitly stubbed (prewarm off,
external vision calls off). ``placement`` is ``stubs_exempt``: the real
conversion and solver run behind the door when a case reaches them.
Expand Down Expand Up @@ -129,6 +132,20 @@
# The one case whose objects_3d boundary carries a nonempty observed tap hazard.
TAP_CASE_ID = "hazard-right-ground-gap"

# Expectations pinned on the wire scene, evaluated against the journey's real
# converted-path result: the wire's six-decimal rounding (declared by the packet as
# 5e-7 ft) propagates through the real conversion and solver into derived feet
# values, so those compare within the packet's declared 1e-4 ft derived-output
# tolerance. Scene geometry itself is still compared exactly, field by field (see
# _check_converted_geometry), and the case-suite evaluator stays exact by default
# (tol_ft defaults to 0.0).
_WIRE_DERIVED_TOLERANCE_FT = 1e-4

# The supported case packet is the default replay target; the legacy W7 cases
# manifest stays available explicitly via --manifest and keeps its own pinned
# refusal state, reported as blockers rather than mistaken for acceptance.
DEFAULT_MANIFEST = Path(__file__).resolve().parent.parent / "server/placement/tests/data/capture_cases_v1/manifest.json"

# An observed tap hazard, hand-authored for this runner: a wall_rect vent on the
# case's wall, away from the case's pinned decision geometry. It exists to prove
# the coordinator's one-time merge, never to fabricate a conflicting hazard.
Expand Down Expand Up @@ -204,6 +221,22 @@ def _scene_bytes(document: dict[str, Any]) -> bytes:
return json.dumps(document, sort_keys=True).encode()


def _objects_3d_for(record: case_suite.CaseRecord) -> list[dict[str, Any]]:
"""The objects_3d list this case's boundary delivers.

The tap hazard is delivered only where the upstream source does not already
carry it: the merge contract replaces only detector/VLM twins, so a same-id
re-delivery of a source-carried (tap-sourced) hazard is never replaced — it
duplicates the id and the merged scene is refused as inconsistent. A
source-carried hazard still reaches the one-time tap checks through the real
merge and real conversion.
"""
if record.id != TAP_CASE_ID:
return []
source_object_ids = {obj.get("id") for obj in json.loads(record.source_path.read_text()).get("objects") or []}
return [] if TAP_HAZARD["id"] in source_object_ids else [copy.deepcopy(TAP_HAZARD)]


def _injected_scene_run(ctx: StageContext) -> StageResult:
"""The scene stage boundary: this capture's stamped source scene, or a loud
refusal. The default stub scene never runs here."""
Expand Down Expand Up @@ -368,7 +401,7 @@ def _walk_front_door(
"record": record,
"packet_id": packet_id,
"packet_sha256": journey["packetSha256"],
"objects_3d": [copy.deepcopy(TAP_HAZARD)] if record.id == TAP_CASE_ID else [],
"objects_3d": _objects_3d_for(record),
}

files = []
Expand Down Expand Up @@ -757,6 +790,9 @@ def _collect_case(harness: _Harness, record: case_suite.CaseRecord, journey: dic
case["convertedSha256"] = _sha256(converted_bytes) if converted_bytes else None
case["resultSha256"] = _sha256(deps.storage.get_bytes(placement_artifact_key))
case["solverPolicy"] = placement.get("policy")
# The solver's own decision, kept distinct from the device's served result
# kind (deviceOutcome) so the receipt shows both sides of the door.
case["solverDecision"] = placement.get("decision")

# Both solver responses must bind their own input hash to their own exact
# request bytes. Forging the same wrong hash in both replies leaves them in
Expand Down Expand Up @@ -805,7 +841,9 @@ def _collect_case(harness: _Harness, record: case_suite.CaseRecord, journey: dic
"direct POST /v1/placements on the captured bytes disagrees with the stage response"
)
if record.wire_path is not None:
wire_mismatches = case_suite._evaluate_expectations(record.expectations, placement)
wire_mismatches = case_suite._evaluate_expectations(
record.expectations, placement, tol_ft=_WIRE_DERIVED_TOLERANCE_FT
)
case["wireSemanticComparison"] = "matched" if not wire_mismatches else "mismatched"
case["wireSemanticMismatches"] = wire_mismatches
if wire_mismatches:
Expand Down Expand Up @@ -845,6 +883,7 @@ def _collect_case(harness: _Harness, record: case_suite.CaseRecord, journey: dic
case["convertedSha256"] = None
case["resultSha256"] = None
case["solverPolicy"] = None
case["solverDecision"] = None
case["directComparison"] = "not_applicable"
case["wireSemanticComparison"] = "not_applicable"
else:
Expand Down Expand Up @@ -1012,8 +1051,12 @@ def run_cases(
"generatedAt": datetime.now(UTC).isoformat(),
"codeSha": code_sha,
"producers": {
"caseSuiteBranch": "obv/burst7-bs-placement-cases@ffd476ddb3a4c6f0b21683f8c353d4280f05dca5",
"sceneStageBranch": "obv/burst7-bs-capture-scene-stage@38a5735abf6c48982f8131b6dc6814afb90e28f3",
# The integrated producer SHAs on this branch (base + imports), so the
# receipt names exactly which producer code ran, not just branches.
"base": "obv/burst7-bs-capture-placement-runner@bdfbb3fc649df8187675b880ba8df83a803fd119",
"caseIntegrityFix": "115b6ce313e51cede71496f267b65b8cd78aa118 (obv/burst7-bs-placement-cases, PR #271)",
"replayAcceptance": "5f9e29576556f635757eea0605d5a23d29ce1cc5 (obv/burst8-bs-replay-acceptance, PR #275)",
"supportedCasePairs": "e7ad2abc15d94691305bdb558a752ee535cf374e (obv/burst8-bs-supported-case-pairs, PR #276)",
},
"devSettings": dict(TEMPLATE_ENV),
"stubbedStages": {
Expand Down Expand Up @@ -1055,7 +1098,7 @@ def _redaction_check(receipt: dict[str, Any]) -> str | None:

def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Replay the placement case suite through the real capture front door.")
parser.add_argument("--manifest", type=Path, default=case_suite.DEFAULT_MANIFEST, help="Path to the case manifest.")
parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST, help="Path to the case manifest.")
parser.add_argument("--output", type=Path, required=True, help="Where to write the JSON receipt.")
parser.add_argument("--workdir", type=Path, help="run directory (default: a fresh temp dir)")
parser.add_argument("--cases", help="comma-separated case ids to run (default: every case)")
Expand Down
16 changes: 16 additions & 0 deletions backend/server/common/backends/state_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def __init__(self) -> None:
self._open_by_owner: dict[str, list[str]] = {}
self._claim_lock = threading.Lock()
self._event_lock = threading.Lock()
# Dedicated lock for stage-record creation (the append_event lesson: never
# share one lock across unrelated write paths — lock-ordering deadlock).
self._stage_lock = threading.Lock()

def get_capture(self, capture_id: str) -> CaptureRecord | None:
rec = self._captures.get(capture_id)
Expand Down Expand Up @@ -183,6 +186,19 @@ def list_runs(self, *, status: set[StageStatus], started_before: datetime | None
def upsert_stage(self, rec: StageRecord) -> None:
self._stages[(rec.run_id, rec.stage)] = rec.model_copy(deep=True)

def create_stage_if_absent(self, rec: StageRecord) -> StageRecord:
"""Atomic insert-if-absent: the deciding read and the insert share one lock,
so a record another coordinator created and started inside the race window
is returned untouched instead of being reset to pending. Returns the record
now stored."""
key = (rec.run_id, rec.stage)
with self._stage_lock:
existing = self._stages.get(key)
if existing is not None:
return existing.model_copy(deep=True)
self._stages[key] = rec.model_copy(deep=True)
return rec.model_copy(deep=True)

def get_stage(self, run_id: str, stage: StageName) -> StageRecord | None:
rec = self._stages.get((run_id, stage))
return None if rec is None else rec.model_copy(deep=True)
Expand Down
20 changes: 20 additions & 0 deletions backend/server/common/backends/state_modal_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
_APPEND_GUARD = threading.Lock()
_APPEND_LOCKS: dict[int, threading.Lock] = {}

# Serializes create_stage_if_absent on the plain-dict fallback, where put_if_absent
# is an in-process check-then-set; a real Modal Dict claims atomically on its own.
# One lock, not one per (run, stage): a per-key registry kept a lock for every run
# a long-lived worker ever touched. Claims only happen for records missing from
# the coordinator's read, so the shared lock serializes only that recovery path.
_STAGE_CLAIM_LOCK = threading.Lock()


def _append_lock(mapping: object) -> threading.Lock:
"""One lock per backing map, so two states on the same dict share it."""
Expand Down Expand Up @@ -503,6 +510,19 @@ def list_runs(self, *, status: set[StageStatus], started_before: datetime | None
def upsert_stage(self, rec: StageRecord) -> None:
self._kv[f"stage:{rec.run_id}:{rec.stage}"] = rec.model_dump(mode="json")

def create_stage_if_absent(self, rec: StageRecord) -> StageRecord:
"""Insert-if-absent across processes: Modal Dict ``skip_if_exists`` is the
atomic claim, so two coordinators resuming the same old run cannot both
create the record — the loser reads back the winner's record instead of
resetting a running stage to pending. The module lock covers the plain-dict
fallback, where the check-then-set is only in-process."""

key = f"stage:{rec.run_id}:{rec.stage}"
with _STAGE_CLAIM_LOCK:
self.put_if_absent(key, rec.model_dump(mode="json"))
stored = self._model(key, StageRecord)
return stored if stored is not None else rec.model_copy(deep=True)

def get_stage(self, run_id: str, stage: StageName) -> StageRecord | None:
return self._model(f"stage:{run_id}:{stage}", StageRecord)

Expand Down
4 changes: 4 additions & 0 deletions backend/server/common/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def list_runs(self, *, status: set[StageStatus], started_before: datetime | None

def upsert_stage(self, rec: StageRecord) -> None: ...

def create_stage_if_absent(self, rec: StageRecord) -> StageRecord:
"""Atomic insert-if-absent: recovery must never reset a record another
coordinator materialized and started first. Returns the record now stored."""

def get_stage(self, run_id: str, stage: StageName) -> StageRecord | None: ...

def stages(self, run_id: str) -> list[StageRecord]: ...
Expand Down
94 changes: 94 additions & 0 deletions backend/server/common/tests/test_stage_record_claim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Stage-record recovery claims are atomic: a record another coordinator
materialized and started inside the race window is returned untouched, never
reset to pending with its attempt and worker identity stripped."""

from __future__ import annotations

import threading

from server.common.backends.state_memory import MemoryState
from server.common.backends.state_modal_dict import ModalDictState
from server.contracts.jobs import StageRecord


def _rec(run_id: str, stage: str, worker: str | None = None) -> StageRecord:
return StageRecord(
run_id=run_id,
stage=stage,
status="pending",
attempt=1,
heartbeat="2026-10-16T00:00:00Z",
worker_call_id=worker,
)


def test_memory_claim_returns_the_existing_record_untouched() -> None:
state = MemoryState()
first = state.create_stage_if_absent(_rec("run", "poses", worker="left"))
assert first.worker_call_id == "left"

second = state.create_stage_if_absent(_rec("run", "poses", worker="right"))

assert second.worker_call_id == "left", "the loser must read back the winner"
stored = state.get_stage("run", "poses")
assert stored.worker_call_id == "left" and stored.attempt == 1


def test_memory_claim_inserts_when_absent() -> None:
state = MemoryState()
created = state.create_stage_if_absent(_rec("run", "poses"))
assert created.status == "pending" and created.attempt == 1
assert state.get_stage("run", "poses") == created


def test_modal_claim_two_states_share_one_map() -> None:
mapping: dict[str, object] = {}
left, right = ModalDictState(mapping), ModalDictState(mapping)
winner = left.create_stage_if_absent(_rec("run", "poses", worker="left"))
loser = right.create_stage_if_absent(_rec("run", "poses", worker="right"))
assert winner.worker_call_id == "left"
assert loser.worker_call_id == "left", "the loser must read back the winner"
assert right.get_stage("run", "poses").worker_call_id == "left"


def test_modal_claim_many_threads_one_winner() -> None:
state = ModalDictState()
results: list[str] = []
guard = threading.Lock()

def claim(worker: str) -> None:
rec = state.create_stage_if_absent(_rec("run", "poses", worker=worker))
with guard:
results.append(rec.worker_call_id)

threads = [threading.Thread(target=claim, args=(f"w{i}",)) for i in range(8)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

assert len(set(results)) == 1, f"every claimant must read back the same winner: {results}"
stored = state.get_stage("run", "poses")
assert stored.worker_call_id == results[0] and stored.attempt == 1


def test_modal_claims_leave_no_per_run_state_in_the_module() -> None:
"""A long-lived worker claims records for many runs. Module-level lock state
must stay bounded: a lock kept per (run, stage) key grows without limit."""
from server.common.backends import state_modal_dict

state = ModalDictState()
state.create_stage_if_absent(_rec("warm", "poses"))

def sizes() -> dict[str, int]:
return {
name: len(value)
for name, value in vars(state_modal_dict).items()
if isinstance(value, dict) and not name.startswith("__")
}

before = sizes()
for index in range(200):
state.create_stage_if_absent(_rec(f"run-{index}", "poses"))

assert sizes() == before
14 changes: 14 additions & 0 deletions backend/server/orchestrator/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ def _recompose_after_correction(capture_id: str, run: RunRecord) -> None:
services = current()
if run.status != "done":
return
present = {rec.stage for rec in services.state.stages(run.run_id)} # type: ignore[attr-defined]
if not present.issuperset(STAGE_NAMES):
# A finished run without a record for every stage predates per-stage
# records. Reopening it would let the coordinator's missing-record
# recovery re-run its upstream stages, and the re-run reads stage would
# overwrite the correction just persisted. Keep the run closed instead.
Comment on lines +171 to +176

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Older results ignore corrections

For an older finished capture missing any stage record, on_confirmation saves the corrected answer, but this guard skips rebuilding the result. GET /result reads the existing result file, so the user gets a success response while still seeing the previous answer. Preserve the upstream work while rebuilding criteria and result, rather than returning without applying the correction.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/server/orchestrator/api.py
Line: 171-176

Comment:
**Older results ignore corrections**

For an older finished capture missing any stage record, `on_confirmation` saves the corrected answer, but this guard skips rebuilding the result. `GET /result` reads the existing result file, so the user gets a success response while still seeing the previous answer. Preserve the upstream work while rebuilding `criteria` and `result`, rather than returning without applying the correction.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Landed #277 anyway: on alpha before this PR such a run was left stuck in running, so the guard is strictly better, and the fix (rebuild criteria and result while preserving upstream work, with a failing-first test) is the required first item of burst9-bs-real-downstream-journey, which owns backend/server/orchestrator/api.py. — Apps lead

log_event(
"confirmation.recompose_skipped",
level="warning",
msg="finished run has no record for every stage",
capture_id=capture_id,
run_id=run.run_id,
)
return
now = services.clock.iso()
for stage in ("criteria", "result"):
rec = services.state.get_stage(run.run_id, stage) # type: ignore[attr-defined]
Expand Down
29 changes: 29 additions & 0 deletions backend/server/orchestrator/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def tick(self, run_id: str) -> StageStatus:

def _spawn_ready(self, run: RunRecord, control: RunControl) -> list[str]:
spawned: list[str] = []
self._materialize_missing_records(run)
status = self._status_map(run.run_id)
names = ready_stages(status)
if "criteria" in names and not control.merge_done:
Expand Down Expand Up @@ -756,6 +757,34 @@ def _status_map(self, run_id: str) -> dict[StageName, StageStatus]:
found = {rec.stage: rec.status for rec in self._rows(run_id)}
return {name: found.get(name, "pending") for name in STAGE_NAMES}

def _materialize_missing_records(self, run: RunRecord) -> None:
"""Recovery for runs written by older builds: a run can lack stage records
entirely (records materialize as the API schedules stages). The status map
treats a missing record as pending, but _spawn_one refuses to start a stage
with no record — so such a run hangs forever between ticks. Idempotently
create only the missing pending records: existing records keep their
attempts, failures and artifacts, and terminal runs never reach here
because tick returns before spawning.

Only stages absent from the rows this tick already read are claimed: with
ModalDictState each claim is a conditional remote write plus a read-back,
so claiming every stage cost two round-trips per stage on every tick.
"""
present = {rec.stage for rec in self._rows(run.run_id)}
missing = [name for name in STAGE_NAMES if name not in present]
if not missing:
return
now = self.s.clock.iso()
for name in missing:
# Atomic insert-if-absent: two coordinators can resume the same old run
# (sweeper, _resume_if_stale, worker tick) without an exclusive claim. A
# plain check-then-upsert would reset a record the other coordinator
# created and a worker started inside the race window.
stored = self.s.state.create_stage_if_absent( # type: ignore[attr-defined]
StageRecord(run_id=run.run_id, stage=name, status="pending", attempt=1, heartbeat=now)
)
self._remember_stage(stored)

def _snapshot(self, run_id: str) -> str:
parts = [f"{rec.stage}:{rec.status}:{rec.attempt}:{rec.failures}:{rec.respawns}" for rec in self._rows(run_id)]
run = self._run(run_id)
Expand Down
4 changes: 4 additions & 0 deletions backend/server/orchestrator/tests/test_greptile_followup.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ def test_confirmation_updates_the_reads_record(tmp_path: Path) -> None:
assert stored.meter.status == "auto_fill"
assert stored.confirm is not None
assert stored.confirm.answer == "12345"
# This done run has no stage records, so the correction must not reopen it:
# missing-record recovery would re-run reads and overwrite the answer.
assert services.state.stages("run_reads") == []
assert services.state.get_run("run_reads").status == "done"


def test_new_attempt_clears_entered_at(tmp_path: Path) -> None:
Expand Down
Loading