Skip to content

The witnessed genesis: CORA records a TomoScan capture it only watched - #658

Merged
xmap merged 20 commits into
mainfrom
worktree-2bm-run-watcher
Aug 14, 2026
Merged

The witnessed genesis: CORA records a TomoScan capture it only watched#658
xmap merged 20 commits into
mainfrom
worktree-2bm-run-watcher

Conversation

@xmap

@xmap xmap commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Summary

Rung 1 of the 2-BM watched-run ladder ("CORA watches a TomoScan scan happen"): a second, independent Run genesis for captures CORA observes but does not drive, plus the live promotion loop that turns a real capture into a real Run record.

  • Slices 5-7: reify ConductMode (who drove the act), the capture-observer port + shadow-only watcher runtime, and the 2-BM recipe ladder + Asset enclosure locations.
  • Slice 8 (27be6cbd2e + corrections): the watched genesis itself — record_witnessed_run, a second Run command that witnesses (not enforces) the live enclosure-permit and beam-availability gates while still refusing on clearance/supply, with a full laundering-wall fitness test proving no operator path can reach it.
  • Follow-up: wires the runtime into a live promotion loop (seeded Agent identity, list_runs conduct_mode filter, boot-time recording gate, restart-rebuild dedup, a real fly_scan recipe).
  • Gate review (six lenses: safety/interlock, security/authz, record/publishing, naming, cross-BC, beamline domain) found and fixed three real issues: an unguarded boot-time authz probe that could crash the whole app, a redaction-disposition inconsistency (the new safety-envelope reading exported unredacted while the equivalent Enclosure BC fact does not), and a physically-incomplete fly_scan recipe missing its Rotary stage Asset.
  • Rename sweep (bdbe2d403e): executes a naming decision locked in a parallel session — ConductMode.RECORDED was not actually a contrast pair with CONDUCTED (every Conducted Run is also recorded), so it becomes WITNESSED, cascading to RunWatcher -> RunWitness and the whole cluster.

Every commit is naming-reviewed and the full unit + architecture + integration suites are green throughout.

Deliberately NOT part of this PR: RUN_WITNESS_RECORDING_ENABLED stays off by default and must not be turned on for real until slice 9 (terminal recording) ships — a Witnessed Run currently has no automated path back out of Running once its capture ends.

Test plan

  • Full unit + architecture suite green after every commit (40788 passed)
  • Full integration suite green after every commit (1164 passed)
  • naming-r3-reviewer pass on every rename/new-name commit
  • Six-lens gate review on the watched/witnessed genesis slice
  • Deploy to arcturus in shadow mode only (RUN_WITNESS_ENABLED=true, recording left off) and verify the floor

xmap and others added 20 commits August 13, 2026 16:59
Rung 1 of watching a TomoScan capture live over EPICS needs the same
reach-tier vocabulary the Enclosure permit-probe trail already uses,
but cora.run cannot depend on cora.enclosure.aggregates. Hoist
ReachTier to cora.shared (zero dependencies, any BC may use it) and
re-export it from its original home so every existing import keeps
working unchanged.

CaptureObserver mirrors EnclosureObserver at every layer: BC-local
Protocol, a closed CapturePhase vocabulary with a first-class
UNRECOGNIZED member for substrate vocabulary drift, and the same
dual-clock and reach-tier shape. QuietCaptureObserver yields nothing
rather than a synthesized reading, since there is no safe "always"
value for a capture phase the way AlwaysPermittedEnclosureObserver has
one for a permit status.
Rung 1 needs a place to declare which PVs to watch and what a
deployment's tomoscan literal vocabulary means, without hardcoding
2-BM's status strings in the spine. capture_watch_pvs is code -> role
-> PV (2-BM runs several tomoscan variants off one base class, so the
outer key is the capture code); capture_status_phases maps a
deployment's own ScanStatus literals onto the closed CapturePhase
vocabulary, validated at boot so a typo fails loud rather than
silently classifying everything as UNRECOGNIZED.

CapturePhase itself gets the same hoist ReachTier already got:
cora.infrastructure validates the deployment's declared table against
it and cannot depend on cora.run.ports (tach), so the enum moves to
cora.shared and is re-exported from its original home.

Everything defaults empty/off, so a generic boot is unaffected.
The first slice with anything to see on the floor. RunWatcher drains a
CaptureObserver and logs one event per observed phase (or the
unreached/probe-only case), and does nothing else: no event store, no
entries table, no Run command. This lets a deployment watch a real
capture end to end, over a real substrate, before any write path
exists to get wrong.

Not yet wired into main.py: it takes any CaptureObserver, and the real
ControlPort-backed one is the next slice. Tested here against a fake
observer exercising every phase, the probe-only/unreached case, an
observer that raises mid-stream, and the no-op-when-unconfigured
lifespan shape.
ControlPortCaptureObserver bridges the Run BC's CaptureObserver port
to the Operation BC's ControlPort at the composition root, mirroring
ControlPortEnclosureObserver at every layer: sibling poll tasks feeding
a shared queue, the same _PumpDone-counted drain, the same queue-drain
race fix on teardown.

One deliberate inversion from the Enclosure precedent, load-bearing
enough to be its own module docstring section: the Enclosure adapter
synthesizes an Unknown STATUS on disconnect so the run-start gate
fails closed. There is no such gate here, so a disconnect carries no
status claim and no phase at all, the same shape a probe-only poll
tick already uses. Reading a disconnect as a real terminal would
fabricate one the substrate never reported.

Wired into main.py's lifespan behind run_watcher_enabled, a separate
gate from capture_watch_pvs being configured, so a deployment can
declare its PVs ahead of turning the watcher on. Still shadow only:
nothing this slice adds can write anything.
docs/reference/modeling.md already named "Conducted vs recorded (who
drives the act)" as an axis orthogonal to the Run/Procedure boundary,
but nothing encoded it. Adds ConductMode (CONDUCTED / RECORDED) to
Run's genesis, defaulting to CONDUCTED since every StartRun caller
today is CORA's own Conductor. Nothing yet constructs a RECORDED Run:
that is slice 8's not-yet-built promotion from RunWatcher's shadow
observations into a real recorded genesis. This slice only gives that
future work a field to import and pass RECORDED into, instead of
inventing the vocabulary under gate-review pressure later.

Pure additive threading: RunStarted event, Run state, the evolver's
per-arm preservation, StartRun command, decider, and the
proj_run_summary read model (new migration). No change to
check_safety_envelope (its gates apply uniformly regardless of who
drove the act) and no Procedure-side mirror (no second consumer yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ce 6)

start_run needs a real plan_id (Plan -> Practice -> Method -> Capability);
nothing in that chain had ever been registered for 2-BM, so a real Run
would 404 with PlanNotFoundError on its first precondition. Checking what
actually exists on arcturus also surfaced that the dark_field/flat_field
recipes target StationShutter and the 2bmSP1: camera, neither of which
was registered either (only the beamline root and the 31MP Camera_HighRes
existed).

Extends pilot_seed.py, the existing 2-BM ceremony pattern, rather than
inventing a second mechanism: registers the two missing Assets, then the
Capability/Method/Practice/Plan chain for dark_field and flat_field (both
"conductible today" per recipes.md), bound to those Assets. Factored a
shared seed_genesis helper for the four Recipe BC aggregates, since a
fourth near-identical closure alongside Asset's and Supply's own crossed
the rule-of-three the file's existing pattern otherwise avoids.
energy_setting and hexapod_reboot stay unregistered: recipes.md marks
both "design, pending executor", so a Plan for either would fail on its
first conduct step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lice 7)

located_in_enclosure_id is genesis-only (no update command exists), and
a Run whose scoped Assets declare none is Permit-by-default per
check_safety_envelope: leaving it unset on slice 6's StationShutter and
Camera was a silent Enclosure-permit gate skip, not a cosmetic gap.

Fixing it on already-registered Assets means decommission + re-register
(the Lock A rebind path already used for controller_id), which means the
two Plans binding them by id must be replaced too -- Capability, Method,
and Practice reference no specific Asset id, so none of those three
change. This is a one-time migration, permanent in pilot_seed.py from
here on: the old StationShutter/Camera are decommissioned if present
(no-op on a fresh deployment, which goes straight to the located _v2
registration), the old Plans are deprecated (hygiene; start_run already
refuses a Plan bound to a Decommissioned asset regardless), and new
_v2 Assets/Plans are registered with located_in_enclosure_id resolved
by name via the same seed_enclosures() the real app's boot lifespan
already uses -- never a hardcoded id, since Enclosure ids are minted at
boot, not deterministic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ce (slice 5 correction)

Slice 5 shipped two gaps against its own roadmap spec, both latent only
because nothing yet constructs a Recorded Run. StartRun carried a
caller-settable conduct_mode that the driven decider copied verbatim,
letting any caller of the driven path claim Recorded while taking the
enforcing path: the exact laundering hole the axis exists to close.
Separately, none of the three standing runtimes (RunSupervisor's hold
FSM, its liveness/truncate/signal rules, RunInitiator's in-flight cap)
distinguished conduct_mode at all, so enabling any of them against a
mix of driven and watched Runs would try to hold, truncate, or
otherwise act on a Run an external tool actually controls.

StartRun now carries no conduct_mode field; the driven decider
hardcodes CONDUCTED. list_runs surfaces conduct_mode so the runtimes
have something to filter on. The RunSupervisor filters watched Runs
out of its running/held population once, upstream of every mechanism,
before the hold pass, liveness check, or Rule Q/R signal observation
ever sees them. The RunInitiator's max_in_flight count is left
unchanged and documented: it deliberately counts every Run regardless
of conduct_mode, because the cap serializes access to shared
single-stage hardware a watched capture is actively occupying.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_start_run_persists_event_with_full_upstream_chain_against_postgres
asserts the persisted RunStarted payload field by field. Slice 5 added
conduct_mode to that payload but never updated this test, so it has
been silently broken since, caught now by running the integration
suite (not part of slice 5's own unit+architecture verification sweep).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Slice 8's watched genesis needs a way to record the enclosure and beam
readings instead of enforcing them, without risking the two paths ever
disagreeing about what a gate actually checks. Refactor
check_safety_envelope's four inline checks into standalone gate
functions: check_clearance_gate and check_supply_gate still raise
unconditionally (both are CORA's own data, not live facility signals,
so they refuse on every path per the roadmap's rule), while
enclosure_gate_refusal and beam_gate_refusal return the refusal they
would raise instead of raising it.

check_safety_envelope is unchanged in signature, behaviour, and raise
order, now composed from the four gates; every existing caller needed
zero changes, confirmed by the full pre-existing test suite passing
unmodified. New witness_safety_envelope shares the exact same gate
functions and converts only the enclosure/beam refusals into a
SafetyEnvelopeVerdict, so the two entry points cannot silently drift:
a change to a gate's logic changes what both paths see.

SafetyEnvelopeVerdict carries plain bools only (the record exporter's
generator drops bare str and Any) and lives beside ConductMode in
state.py rather than in safety_envelope.py or events.py, so neither of
those modules gains a new import edge to carry it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…es CORA only observes

Slice 8's whole point: today only start_run can create a Run, and it
always drives the act through CORA's own Conductor. A capture an
external tool runs (2-BM's TomoScan) has nowhere honest to land except
as a fabricated driven start, or not at all. RecordWatchedRun is that
second path: same aggregate, same RunStarted event, but the decider
hardcodes ConductMode.RECORDED and the command carries none of the
operator-facing fields a driven start has, so a caller of the driven
path can never claim to be recording, and a caller of this path can
never claim to be driving.

The governing rule, unchanged from the roadmap: refuse on what CORA can
fix, witness what CORA cannot. Every CORA-side genesis invariant the
driven decider enforces (deprecated Plan, decommissioned Asset,
capability shortfall, bad wires, absent Clearance or Supply) still
raises here. Only the two genuinely external live facility signals,
enclosure permit and beam availability, are witnessed instead: recorded
as a SafetyEnvelopeVerdict on the emitted event rather than gating the
genesis. A trigger guard (RunMonitorTriggerNotPermittedError, mirroring
the Enclosure BC's shipped lock) closes the one operator-reachable path
in: only a literal "Monitor" trigger is accepted, and there is no REST
route, no MCP tool, and no other construction site for
ConductMode.RECORDED anywhere in the tree, each pinned by a dedicated
architecture fitness test rather than left to convention.

A naming-r3-reviewer pass on the staged diff caught five real R1/R2
issues before this landed: a property named `held` that collided with
the Run BC's own RunStatus.HELD vocabulary, a field named
`envelope_verdict` that collided with the "event envelope" infra term,
two classes using a skeleton that didn't match their siblings
(RunStartContext/RunStartEvents), and two gate-check functions whose
word order didn't match their two siblings. All five renamed before
commit; the rename also surfaced one more pre-existing exact-payload
test gap of the same shape the previous commit fixed, caught by the
same full-suite run and fixed alongside.

Deferred to a follow-up commit: wiring this handler into
_run_watcher.py's live promotion loop (the deployment settings, the
seeded Agent principal, the edge-triggered dedup state machine, and the
restart-rebuild logic), and seeding an actual fly-scan Plan for it to
point at. Those are mechanical integration work once this slice exists;
this commit is the safety-critical reviewable unit on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RunWatcher (cora.api._run_watcher) needs a seeded Agent + Actor at a
pinned id to author RecordWatchedRun calls once it starts promoting
BEGUN captures to real watched Runs, same shape as every other
in-process runtime (RunSupervisor, RunInitiator, CalibrationWatcher).
No behavior change to the watcher itself yet: this only gets the
principal into the event store.
RunSummaryItem already carries conduct_mode (Conducted/Recorded), but
the query had no way to filter on it. The RunWatcher's restart-rebuild
query (a follow-up commit) needs exactly this: page through open
Recorded Runs without also seeing Conducted ones. Pure filter-plumbing,
mirroring the existing status/plan_id/campaign_id ScalarFilter shape;
no projection or migration change.
Two new settings for the RunWatcher's promotion path (a follow-up
commit): capture_watch_plan_id (which Plan a promoted watched Run
references) and run_watcher_recording_enabled (a second kill switch
above run_watcher_enabled, gating whether a BEGUN observation is
actually promoted rather than just logged). Boot refuses to start with
recording enabled unless both prerequisites are set, mirroring the
existing _enforce_production_principal_policy shape: cheaper to catch a
half-configured deployment at boot than the first time a real capture
begins.
Wires the shadow-only capture-observation runtime to actually create a
real watched (Recorded) Run via record_watched_run when it observes a
BEGUN capture, behind the run_watcher_recording_enabled kill switch
added in a prior commit. A per-capture-code dedup state machine
(RunWatcherRecorder) ensures a single in-progress capture is promoted
at most once; ENDED/ABORTED clears the local entry so a later capture
on the same code can promote again. A boot-time restart-rebuild query
(rebuild_open_captures) seeds that dedup state from every currently-
Running Recorded Run's external_refs, so a capture still open across a
process restart is never re-promoted.

Shadow mode (recording disabled, the default) is byte-for-byte
unchanged: observe_capture's body is untouched, and every existing
lifespan/loop test that constructs no recorder keeps passing unmodified.

Reviewed by naming-r3-reviewer before landing: renamed
rebuild_open_capture_map -> rebuild_open_captures, capture_closed ->
open_capture_cleared (it isn't a CapturePhase, so it doesn't belong to
that log-event family), _CLOSING_PHASES -> _TERMINAL_PHASES (matching
the codebase's terminal/closing vocabulary split), and the recorder's
observe method -> observe_capture (so both dispatch branches in
run_run_watcher read identically).
Seeds the actual 2-BM TomoScan workflow (fly_scan) as a third rung on
the acquisition recipe ladder, alongside the existing dark_field /
flat_field baseline captures. Watch-only: no operator REST/UI surface
ever selects this Plan for start_run, matching record_watched_run's
own stub route/tool, so capture_watch_plan_id has something real to
point at once a deployment turns recording on.
Gate-review finding (security/authz lens): rebuild_open_captures ran
unconditionally at boot with no exception handling, so a deployment
that enabled run_watcher_enabled under enforced TrustAuthorize but
forgot to grant the seeded RunWatcher agent ListRuns would crash the
entire API's boot, not just this feature. Every sibling watcher probes
its read grant via probe_read_grant and stays resilient in its own
per-tick loop; this one-time boot read had neither.

Adds the same probe_read_grant call (refuses boot with a clear message
in strict mode) plus a fallback to an empty dedup map on
UnauthorizedError in non-strict mode, so a missing grant degrades to
"cold start, one possible re-promotion" rather than taking down every
other bounded context's routes.
Gate-review finding (record/publishing lens): enclosure_permitted and
beam_available exported unredacted (keep:number) purely because they
are bool-typed, while the equivalent live PSS/interlock fact on
EnclosurePermitObserved (from_status/to_status) is drop:text. Nobody
made a deliberate export-sensitivity call for the new fields; the
generator's type-driven default decided it.

Adds _OVERRIDE_DISPOSITIONS to gen_record_dispositions.py (sibling to
the existing _OVERRIDE_WIRE_KEYS per-field override table) so the two
fields classify as drop:text like their EnclosurePermitObserved
counterpart, while staying genuinely typed bool at the source (no str
coercion just to game the classifier). Regenerated _dispositions.py;
the diff is exactly the two flipped entries.
Gate-review finding (beamline domain lens): the seeded fly_scan recipe
required only a Shutter + Camera, the same pair as the two static
dark_field/flat_field baseline captures. A real fly-scan's defining
feature is continuous sample rotation; omitting the Rotary stage meant
an exported watched Run record would understate the equipment a real
2-BM TomoScan fly-scan actually uses.

Seeds a RotaryStage-family Device Asset (RotaryStage is already a
globally-bootstrapped family) and threads it through
seed_acquisition_recipe via a new include_rotary flag, used only by
the fly_scan call; dark_field/flat_field are unchanged.
cluster to witnessed (RunWatcher -> RunWitness)

Executes a naming decision locked in a separate session
(project_conduct_mode_witnessed_rename.md): RECORDED was not actually a
contrast pair with CONDUCTED, since every Conducted Run is also
recorded (in the event store, in proj_run_summary, in the export
bundle). WITNESSED is the only candidate mutually exclusive with
CONDUCTED, and is already the vocabulary the decider's own governing
rule uses ("refuse on what CORA can fix, witness what CORA cannot").

That memo's tripwire fires when a deployment is about to enable
run_watcher_enabled, which is exactly the next step after this
follow-up's five commits landed live promotion. Executing the sweep now
(worktree is clean, no in-flight edits to reconcile by hand) rather than
deferring further.

Cascades from the enum rename: RunWatcher -> RunWitness (runtime class,
module, seeded agent identity, settings, log namespace),
record_watched_run -> record_witnessed_run (feature slice, command,
context/events classes), and prose throughout ("watched genesis" ->
"witnessed genesis"). Also fixes a separate pre-existing bug the memo
called out: trigger_source referenced a class (CaptureWatcher) that
never existed in the codebase; it now names the real runtime
(RunWitness).

Naming-reviewed before landing (naming-r3-reviewer, two passes): first
pass found the RunWatcherRecorder.observe/RunWatcherRecorder naming
(now RunWitnessRecorder.observe_capture), rebuild_open_capture_map ->
rebuild_open_captures, and _CLOSING_PHASES -> _TERMINAL_PHASES from the
original commit. Second pass (this rename) found several persisted
strings and half-renamed prose (mixed CONDUCTED/witnessed vocabulary in
the same sentence), all fixed, plus a genuine architecture-fitness gap:
RunWitness doesn't end in any R5 doer-suffix
(-er/-or/-ist/-ant/-tor) the existing test/test_agent_kind_doer_form.py
checked, so a new _ZERO_CHANGE_DOER_WORDS allowlist was added for
whole-word doer nouns like Monitor/Coordinator's siblings.
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/agent
  seed_run_witness.py
  apps/api/src/cora/api
  _capture_observer.py 151-152
  _run_supervisor.py
  _run_witness.py 190-194, 210, 275, 304-307, 312
  main.py 1097-1109
  pilot_seed.py 297-302, 415-416, 459-496, 561-562, 573-574, 753-755, 925-955
  apps/api/src/cora/enclosure/aggregates/enclosure
  permit_probes.py
  apps/api/src/cora/infrastructure
  config.py
  apps/api/src/cora/run
  routes.py
  tools.py
  apps/api/src/cora/run/aggregates/run
  events.py
  safety_envelope.py
  state.py
  apps/api/src/cora/run/features/list_runs
  handler.py
  apps/api/src/cora/run/features/record_witnessed_run
  __init__.py
  command.py
  context.py
  decider.py 172
  handler.py 91-100, 104, 108, 112, 118, 123-125, 163-166
  route.py
  tool.py
  apps/api/src/cora/run/ports
  capture_observer.py 171
  apps/api/src/cora/run/projections
  summary.py
  apps/api/src/cora/shared
  capture_phase.py
  reach.py
Project Total  

The report is truncated to 25 files out of 31. To see the full report, please visit the workflow summary page.

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit fd59486 into main Aug 14, 2026
19 checks passed
@xmap
xmap deleted the worktree-2bm-run-watcher branch August 14, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant