Coherence-Divergence Monitor (Strained Coherence)¶
What it is¶
A per-run trajectory-level reliability signal. Every other Airlock layer inspects a single tool call in isolation; this monitor watches the whole run trajectory for a sequential failure no per-call check can see: the agent states a blocking fact that should change its plan ("tests still failing", "permission denied", "the file does not exist") and the very next advancing action proceeds as if that fact were untrue (marks the task done, commits, reports success).
Anchored on Strained Coherence (arXiv:2606.07889): trajectories exhibiting this divergence failed 94% of the time versus 46% without it (Fisher's exact p = 0.003) — a pre-failure signal worth surfacing before the run finishes, not only in a post-hoc autopsy.
Honest scope — a deterministic keyword matcher¶
Detection is a deterministic lexical matcher, not an LLM judge: a statement
is a blocking fact when it contains a category keyword (test failure /
permission denied / missing resource / build error / generic error); an action
is a closure when it contains an advancing keyword (commit, mark complete,
deploy, report success…). It is pure and CI-stable — same trajectory → same
verdict on every machine. False-positive guards keep it honest: a run that
resolves the fact (states tests pass, file created, build succeeded), a
generic resolution ("issue resolved"), or an action that disclaims
success ("cannot mark complete: permission denied") does not fire, and a
min_confidence floor + a lookahead staleness window bound spurious pairings.
Wired live at the gateway run stream¶
As each step is submitted (submit_step_result), the run's step is projected
into trajectory events and fed to a process-wide CoherenceMonitor keyed per
run:
- a Tool step → an advancing action (the invocation, e.g.
git_commit) followed by a statement (its observed output/error, e.g. "tests failed"); - a reasoning / other step → statements only.
Feeding action-before-statement means a later step's closure action is checked
against blocking facts opened by earlier steps' outputs — the sequential
pattern the monitor detects. observe_event returns a CoherenceSpan the
instant a divergence completes, mid-run. At completion, a synthetic
"reports success" closure action carrying the final output is fed, so a run that
terminates with an unresolved blocking fact (and does not disclaim it) also
flags; an honest disclaimer in the final output suppresses it.
Graduated response — R1–R3 (shadow default, opt-in enforce)¶
A divergence is not just logged: it is mapped onto the existing reversibility
ladder (fd_policy::reversibility) via
[CoherenceSpan::response_level], reusing the R1–R3 ResponseLevel rather than
inventing a new shape. Severity → rung:
Severity (risk_level) |
Rung | ResponseLevel |
|---|---|---|
Critical / High (default risk_score 70 → High) |
R3 | require_approval |
| Medium | R2 | allow_under_budget |
| Low | R1 | allow_and_log |
Two modes, mirroring the Airlock shadow/enforce convention:
shadow(default) — the chosen rung is recorded + surfaced (audit details, SSE, the run'sresponse_level), but the run is never gated. Safe for the heuristic's false-positive rate, which is 10.20% (25/245 benign trajectories, Wilson 95% CI [7.16%, 14.92%] —make eval-coherence-fp).enforce(FERRUMDECK_COHERENCE_MODE=enforce) — an R3 divergence gates the run: its status is set toWaitingApprovalfor human review instead ofCompleted. R1/R2 are recorded but not gated.
The deny-by-default posture for tool permissions is unchanged; this adds a graduated response for a trajectory signal.
Operating at R3 vs R1 — when to flip enforce¶
The shadow/enforce switch is exactly the DeepMind AI Control Roadmap
(2026-06-18) Response-tier choice applied to the coherence signal:
shadow(default) ≈ R1 (asynchronous). The divergence and its R-tier are recorded to the audit trail + surfaced (SSE, the Coherence card), and a human reviews after the fact. The roadmap deems R1 acceptable for low-risk, easily reversible actions — which is the right default here because the detector is a lexical heuristic whose measured false-positive rate is 10.20% (Wilson 95% CI [7.01%, 14.63%]), so you do not want it halting runs unattended.enforce≈ R3 (synchronous). An R3 divergence (default severity → R3) blocks before the run proceeds: the run is set toWaitingApprovalfor human review instead ofCompleted. The roadmap's guidance is that R1 async review is insufficient for critical-infrastructure agents — a harmful, irreversible action (a deploy, a spend, an external send) cannot be undone after the fact, so it must be gated synchronously.
Rule of thumb. Flip FERRUMDECK_COHERENCE_MODE=enforce for agents that can
take irreversible, high-consequence actions (prod deploys, payments, data
deletion, external comms) and where a false-positive gate (a run parked at
WaitingApproval until a human clears it) is an acceptable cost. Price that cost
with the measured rate rather than a feeling: at 10.20%, roughly one correct
run in ten is parked, so enforce needs someone actually watching the approval
queue. Keep the
default shadow for exploratory / reversible workloads where you want the
signal recorded but not the friction. Coverage and time-to-response are
unchanged by the mode — only the response (record vs gate) differs.
Reference: DeepMind, An Approach to Technical AGI Safety — AI Control Roadmap
(blog,
PDF).
The response ladder is anchored in fd_policy::reversibility
(deepmind-ai-control-roadmap-r1-r3).
Where it surfaces¶
- Audit: an
airlock.violation_detectedevent withdetails.violation_type = coherence_divergence— the sameaudit_events.detailspath every other Airlock layer uses (no parallel store).detailsalso carriesresponse_level/response_rung,mode(shadow|enforce), andgated;details.coherenceis the fullCoherenceSpan.details.blockedequalsgated. - SSE: a
coherence.divergence.detectedevent on the run stream (run:{run_id}) carrying the category, confidence,response_level/response_rung,gated, and the stated-fact / contradicting-action quotes. Gateway→BFF push is deferred (same pattern asrun.forecast.updated/routing.decision.recorded); the BFF locks the wire shape and the console reads the persisted flag + rung on the next poll. - Run row:
runs.coherence_divergence_flagged(truewhen a divergence fired,falsefor a coherent completed run,NULLfor legacy runs) andruns.response_level(the selected rung), returned onGET /v1/runs/{id}. - Span:
ferrumdeck.reliability.coherence_divergence(boolean) on the run-completion span. - Dashboard: a "Coherence" stat card on the run header — amber "Divergent"
when flagged, green "Coherent" otherwise, hidden (null-for-legacy) when the
field is absent; the R-rung shows on the existing
ResponseLevelBadge.
Configuration¶
CoherenceConfig (separate from the per-call AirlockConfig) — enabled,
lookahead (default 8), risk_score (70), min_confidence (0.5). Env toggles:
FERRUMDECK_COHERENCE_ENABLED=false— disable the monitor entirely.FERRUMDECK_COHERENCE_MODE=enforce— request gating of R3 divergences (defaultshadow, which records + surfaces but never gates).FERRUMDECK_COHERENCE_FP_SERIES— path to the append-only measurement series (defaultdocs/eval-health-series.jsonl).
Enforce is requested, not switched¶
Setting enforce does not by itself gate anything. coherence_evidence.rs reads
the newest coherence_fp row from the measurement series and refuses to
activate enforcement when any of these holds:
| Condition | Why it refuses |
|---|---|
| no series file | gating on an unmeasured lexical matcher is an availability risk of unknown size |
no coherence_fp row |
same — the file exists but nothing has measured this detector |
| measurement older than 14 days | the keyword lists are hand-edited, so an old rate describes a different matcher |
There is no maximum-rate condition, and that is deliberate. Through 0.8.17 a
fourth row here read rate above MAX_FP_RATE_FOR_ENFORCE (15%). That gate was
circular: 15% was the Wilson 95% upper bound of the one measurement it gated
(10.42%, CI [7.16%, 14.92%]) rounded up, so it could not fail against the number
that set it, and a detector regressing to 14.9% would still have passed.
Deriving an honest limit needs two inputs this project does not have: how many runs you put through the gate, and how quickly a human clears a parked one. With those the limit is a sentence in your terms — "no more than N correct runs parked per week, cleared within M minutes" — and without them any number is invented. So the gate reports instead: when it activates it logs the measured rate, its sample size, its interval, and the cost in parked runs, and says plainly that you are accepting that rate. You hold the missing inputs, so you own the decision. Absence of evidence is still a refusal.
A refusal logs at ERROR and is reported on /ready under
coherence_enforcement as {requested, active, reason} — because an operator who
asked for a gate and did not get one must not have to infer that from runs that
never park. Re-measure with make eval-coherence-fp and commit the report.
Limitation — single-process trajectory state¶
The per-run trajectory state lives in gateway memory (CoherenceMonitor's
per-run map). In a multi-instance gateway deployment, a run is tracked only
within the instance that receives its steps; steps routed to a different
instance would not share the open-fact window. The completion-time synthetic
closure still flags the common "reports success despite an in-step blocker"
case within the completing instance. A shared/replayed-from-DB trajectory
source is the follow-up for multi-instance parity.
Dual implementation, one contract¶
| Plane | Module | Role |
|---|---|---|
| Rust | fd_policy::airlock::coherence |
the live gateway monitor (observe_event) + stateless scan_trajectory |
| Python | fd_evals.coherence |
eval-plane mirror — replays a trajectory offline with the identical detection core |
A shared golden fixture
(fd-evals/tests/fixtures/coherence_divergence.golden.json) is asserted by
both the Rust golden_fixture_matches_python test and the Python
TestGoldenParity test, pinning that the two planes classify an identical
trajectory identically.
Reproducible demo¶
examples/demo/coherence-drift.py (section 8 of examples/demo/run-demo.sh)
feeds a deliberately drifting trajectory through the same detection core and
prints each divergence + its R-tier + the shadow/enforce action + the SSE shape.
Pure, no stack, self-verifying (exits non-zero if the drift is not caught):
Verifying¶
- Rust unit + parity:
cargo test -p fd-policy coherence(incl.response_level_maps_severity_to_r_tier). - Rust live-consumer integration:
cargo test -p gateway coherence_live_consumer(incl.enforce_mode_gates_r3_divergence). - Demo:
uv run python examples/demo/coherence-drift.py. - Python (CI-gated):
uv run pytest python/packages/fd-evals/tests/test_coherence.py. - Cross-plane: the Rust
golden_fixture_matches_pythonand PythonTestGoldenParityassert the same divergence categories on the shared fixture.