Failure policy
The rules the edge follows when a dependency is missing, a write fails or a measurement is unknown. When the runtime could not observe, record or enforce something, it writes a gap record saying so, in the same log as everything else, so a reader can tell “nothing happened” from “this could not say what happened”.
The rules apply to batch runs and host sessions alike. Each clause names
what holds it: a test where the behaviour can be witnessed from outside,
otherwise the implementation, and the clause says which. A change to the
behaviour changes this page in the same commit. Terms such as crossing, run,
evidence log and gap are defined in docs/GLOSSARY.md.
1. Durable before acknowledged
Section titled “1. Durable before acknowledged”Every evidence record is fsynced before the append returns, and the directory entry is synced too on the append that creates the log. “Recorded” means on disk, not in a buffer.
The two halves are witnessed differently. Records are appended in order and read back in order under test; durability itself is not — a test in one process cannot tell a synced write from a buffered one, and this repository has no way to cut power to a disk. The fsync is held by the implementation and by review of it, not by a test.
records_are_durable_and_readable_in_order(crates/commonmeasure-runtime/tests/evidence_integrity.rs) — order and readabilityappend_line(crates/commonmeasure-runtime/src/evidence.rs) — thesync_allon the file and, for a newly created log, on its parent directory
2. Write-failure windows are materialised
Section titled “2. Write-failure windows are materialised”When an append fails, the window is held and written as an evidence_gap
record by the next successful append. If the gap record itself cannot be
written, the gap stays pending — nothing is lost and nothing is silently
dropped. run.evidence_complete is false while a gap is owed.
A pending gap is held by the log instance that owes it, so it reaches the file
only through a later append on that same instance. A run is one process and one
log, so the limit costs nothing there; a session is not one process, and
docs/contracts/session-evidence.md §Where states what the limit costs there.
A process killed inside an append owes a gap it can never write, and leaves a
line without its newline. Every append holds the log’s advisory lock, so a
later append that finds such a line under the lock knows no process is
writing it: it terminates the line and writes the evidence_gap record that
marks it, naming its length, before its own record. Readers skip a line that
does not parse only where that record follows it; any other damage stays an
error for every reader (session evidence §Reading a
log).
A run does not abort because its log could not take a line: the work still happened. It must not finish claiming a complete record.
That obligation fixes an ordering. The log is closed — terminal record written, any owed gap given a last chance to be written — before completeness is read and the summary is built. The other order publishes a claim that the next write can falsify, and since a failed append is deliberately not fatal, nothing downstream would correct it. The summary therefore attests to the log (record count and digest) rather than the log attesting to the summary: a finished log cannot carry the hash of a document that does not exist yet.
a_write_failure_window_is_materialised_as_a_gap,a_failed_terminal_append_is_reported_as_an_incomplete_record,a_closed_log_reports_its_own_size_and_digest(crates/commonmeasure-runtime/tests/evidence_integrity.rs)a_write_that_lands_after_a_torn_record_marks_it_and_the_log_reads,a_live_writers_record_in_progress_is_waited_for_and_left_whole,a_record_left_by_a_killed_writer_is_marked_by_the_next_append(crates/commonmeasure-runtime/tests/torn_lines.rs)the_summary_attests_to_a_finished_evidence_log(crates/commonmeasure-runtime/tests/end_to_end.rs)
3. One log, one run — and one log, one session
Section titled “3. One log, one run — and one log, one session”A run’s log is created exclusively: a second run cannot append to a first run’s file, because a log holding two runs describes neither.
A session’s log is the opposite and for the same reason. A session spans many
short-lived hook processes, so its log is opened for append and seq resumes
from what is there. Concurrent hooks can share a seq; a reader orders by
position.
a_second_run_cannot_append_to_a_first_runs_log(crates/commonmeasure-runtime/tests/evidence_integrity.rs)separate_hook_processes_append_to_one_session(crates/commonmeasure-cli/tests/hook_e2e.rs)
4. A run is published only when it is complete
Section titled “4. A run is published only when it is complete”Artefacts are written into a staging directory beside the published one, named for the run that is writing it, and moved into place atomically. An interruption leaves the previously published run exactly as it was; a partial run never appears under the published name.
a_completed_run_replaces_the_previous_one,an_abandoned_run_leaves_the_published_one_intact(crates/commonmeasure-runtime/tests/evidence_integrity.rs)
5. A missing dependency produces an explicit unavailable result
Section titled “5. A missing dependency produces an explicit unavailable result”No credential, no gateway, or an unreachable gateway each produce a plan with
status unavailable, no answer, and a gap naming what is missing. There is no
weaker successful mode and nothing to fall back to.
without_a_gateway_every_plan_is_explicitly_unavailable(crates/commonmeasure-runtime/tests/end_to_end.rs)a_run_with_no_dependencies_claims_nothing_and_explains_every_absence(crates/commonmeasure-cli/tests/run_contract.rs)an_unreachable_gateway_is_unavailable(crates/commonmeasure-inference/tests/gateway.rs)
6. Per-mode enforcement, identical recording
Section titled “6. Per-mode enforcement, identical recording”PolicyMode governs the operator’s own policy: the host lists and access
rules, required licences, allowances and caps, and processor verdicts. It
decides what happens to a breach of that policy, never whether it is
recorded. Strict refuses; Prefer and Observe carry the breach and
record it identically. A required licence is the operator’s policy, so a
source whose licence is unknown is a breach of it in every mode, never a
source that satisfies it: strict refuses the source, and observe and
prefer carry it with the breach and an evidence_missing gap recorded.
The source’s terms bind in every mode on every edge, enrolled or not,
because they are not a matter of operator preference: the operator’s mode is
not the source’s consent. A mediated fetch is refused, in observe and
prefer as in strict, on:
- a
robots.txtDisallowfor this fetcher, or arobots.txtthat could not be reached or read with no copy held; - a
Crawl-delayor a back-off the fetch cannot keep; - a licence the source names that could not be read, including one in
which choosing the
<content>entry for the page would take more work than the edge’s selection budget, or would leave no entry governing: no entry is selected in its place, and the record says which cause applied; - a
Content-SignalorContent-Usagestatement that disallows AI input; - a licence that prohibits AI input in the
<content>entry governing the page, or in an entry read with it, whatever another licence there permits: the narrowest matching entries govern, and entries that govern together are read as one; - a licence whose AI-input permission needs a payment this edge cannot make
or a licence token it cannot obtain: no settlement rail means no fetch. A
payment term is unmet unless the licence says it is free, a licence with
no usage
<permits>covers AI input on its own payment and licence-server terms where no licence of the same<content>, entries read as one (session evidence §Source declarations), names AI input, and a licence server binds whatever the offers; - a reporting demand the session cannot meet, whether a licence states it or the operator’s recorded agreement with the source does. The offer whose demand is ruled is chosen on payment and then document order, not on whether its demand can be met, so of two free offers that differ only in such a demand the first written decides whether the page is delivered (recorded in session evidence §Source declarations).
A term known before the request refuses before it, and the page is never
asked for. A term known only from the response (a Content-Usage header, a
licence named in a Link header) withholds the bytes from context, with
their hash on the refused crossing. Each refusal is a crossing_refused
with the term in refusal and grounded false
(session evidence §Source declarations).
A source that carries no text is refused in every mode as well, because it cannot ground an answer.
A processor’s verdict is under the same discipline, with one stated
exception. The injection screen’s match refuses the crossing in strict and
is carried as a recorded breach in observe and prefer, recorded
identically, because it reaches the decision through the same Ruling every
other constraint uses. The PII detector’s finding is the exception: it is
carried as a recorded breach in every mode, strict included, for every
source, and refuses nothing. What the detector establishes is evidence, the
finding’s categories and offsets on the invocation record; keeping a source
out of a model is the work of the operator’s constraints and the mode.
A PDF the edge hands over as a file is not read, so neither screen rules on
it: each records a capability_unavailable gap, and the unknown verdict is
a breach through the same Ruling. strict refuses the crossing on it
whatever the source, and observe and prefer deliver the file with the
breach and the gap recorded (§5, §7)
(strict_refuses_a_pdf_the_screens_cannot_read_and_keeps_nothing,
a_pdf_is_saved_under_the_session_and_its_path_hash_and_size_returned,
crates/commonmeasure-cli/tests/fetch_file_e2e.rs).
strict_source_policy_refuses_a_disallowed_host_before_inference,observe_mode_carries_the_same_breach_it_does_not_lose_it,a_strict_pii_finding_on_a_public_source_is_recorded_and_the_source_carried,a_strict_pii_finding_on_the_internal_corpus_is_carried_and_recorded,a_strict_pii_finding_on_a_private_address_from_a_supplier_is_carried,observe_mode_records_the_pii_finding_and_carries_the_source(crates/commonmeasure-runtime/tests/end_to_end.rs)a_finding_is_carried_as_a_breach_and_never_refuses(crates/commonmeasure-runtime/src/processor/pii.rs)observe_mode_records_the_breach_it_does_not_enforce,a_source_without_text_is_refused_in_every_mode,a_required_licence_is_not_satisfied_by_an_unknown_one,an_unknown_licence_against_a_required_one_is_a_breach_outside_strict(crates/commonmeasure-runtime/tests/policy_and_selection.rs)a_content_signal_disallowing_ai_input_refuses_in_every_mode,a_payment_term_with_no_settlement_rail_refuses_in_every_mode,a_licence_server_with_no_token_rail_refuses_in_every_mode,a_content_usage_disallow_withholds_the_page_in_every_mode(crates/commonmeasure-harness/src/mcp.rs)observe_mode_carries_the_crossing_and_still_records_it,a_compliant_crossing_records_no_breach,a_fetch_carrying_pii_from_a_private_address_is_carried_in_strict_mode_and_recorded,a_fetch_carrying_pii_from_a_named_internal_prefix_is_carried_in_strict_mode,observe_mode_carries_a_pii_finding_and_records_it(crates/commonmeasure-cli/tests/mediated_e2e.rs)
7. Unknown is never zero
Section titled “7. Unknown is never zero”An unreported charge makes a cap unenforceable and leaves a gap saying so; it
never satisfies the cap. A gateway that reports no usage yields null tokens,
not zero. A gateway that does not name the model it ran does not thereby confirm
the requested one.
A provider that declares no price is bounded by the principal’s allowance only
at the dispatch after the one that exhausts it: the charge counts from the
receipt, so one unpriced dispatch can exceed the allowance by any amount
(docs/contracts/run-output.md, acquisition.allowance). A settlement the
allowance ledger does not record after one retry is a gap naming the
reservation, the observed charge and the reason; the ledger’s total then omits
money that moved, and the record says so rather than the ledger counting zero.
A latency total is the sum of the legs that ran. A plan whose inference never happened has no inference leg to add, which is not the same as adding a zero-length one — and written as a defaulted zero, an absent measurement becomes arithmetically indistinguishable from an instantaneous one.
An evaluation that measured nothing is unevaluated or unmeasured with
the reason stated, never a zero: a suite with no coverage rubric or as-of
date, a plan with no assembled window, and a window part with no declared
date each leave the fraction absent rather than fabricated — and one
unmeasured candidate blocks a fraction-weighted selection rather than
silently losing it.
an_unreported_charge_makes_the_cap_unenforceable_not_satisfied,cost_selection_abstains_when_a_candidate_disclosed_no_price,one_unmeasured_candidate_blocks_a_fraction_ranking_and_is_named(crates/commonmeasure-runtime/tests/policy_and_selection.rs)an_unpriced_provider_is_refused_only_once_the_allowance_is_exhausted,a_settlement_the_ledger_cannot_write_is_a_gap_naming_the_money_that_moved(crates/commonmeasure-runtime/tests/allowance_dispatch.rs),a_settlement_the_ledger_cannot_write_leaves_a_gap_naming_the_money_that_moved(crates/commonmeasure-harness/src/mcp.rs)no_rubric_is_unmeasured_never_zero(crates/commonmeasure-runtime/src/coverage.rs),an_undated_part_makes_the_plan_unmeasured_naming_the_part(crates/commonmeasure-runtime/src/freshness.rs)a_silent_gateway_yields_unknowns_never_zeroes_or_the_requested_route(crates/commonmeasure-inference/tests/gateway.rs)a_plan_without_inference_reports_the_acquisition_leg_alone(crates/commonmeasure-runtime/tests/end_to_end.rs)
8. Capture never breaks the agent
Section titled “8. Capture never breaks the agent”A hook exits zero whatever happens, and a payload it cannot read produces no records rather than an error. Interrupting the operator’s work over a recording problem is worse than missing a row, and a row the hook cannot understand must not be invented.
no_payload_makes_the_hook_fail_the_tool_call(crates/commonmeasure-cli/tests/hook_e2e.rs)malformed_input_gets_an_error_and_the_session_continues(crates/commonmeasure-cli/tests/mediated_e2e.rs)
9. Observed and mediated are never merged
Section titled “9. Observed and mediated are never merged”Which path recorded a crossing is a recorded field. A mediated crossing was governed before it happened and could have been refused; an observed one was seen after it had happened. Recording them alike would claim enforcement that did not occur.
Common Measure’s own MCP tools are therefore excluded from observed capture: they are already recorded by the server that carried them.
a_mediated_fetch_returns_the_bytes_and_records_the_crossing,strict_policy_refuses_the_crossing_and_records_the_refusal(crates/commonmeasure-cli/tests/mediated_e2e.rs)our_own_mediated_tools_are_not_observed_a_second_time(crates/commonmeasure-harness/src/hook.rs)
10. Grounding is claimed only where bytes entered context
Section titled “10. Grounding is claimed only where bytes entered context”WebFetch grounds. Search and third-party MCP results do not: nothing ties a
returned URL to the bytes the model saw. The hash covers the ingested text, not
the transport wrapper around it, or it can never be matched against a
transcript.
a_websearch_records_each_result_and_grounds_none,a_webfetch_is_recorded_with_the_hash_of_what_entered_context(crates/commonmeasure-cli/tests/hook_e2e.rs)a_citation_outside_the_window_is_uncovered(crates/commonmeasure-runtime/tests/evaluation.rs): the grounding evaluator gives a cited URL that never entered the window theuncoveredverdict, not a supported one
11. Context is admitted whole or not at all
Section titled “11. Context is admitted whole or not at all”A source that will not fit the job’s budget is left out whole, with a gap. A truncated source has a content hash that matches nothing, and a citation into text the model never saw is worse than a missing source.
the_context_budget_drops_whole_sources_rather_than_truncating_them(crates/commonmeasure-runtime/tests/end_to_end.rs)- The grounding evaluator is what checks a citation against the admitted
text:
a_faithful_citation_is_supported_and_its_span_excises_to_the_quote,a_quote_no_window_source_contains_is_contradicted,a_citation_outside_the_window_is_uncovered(crates/commonmeasure-runtime/tests/evaluation.rs)
12. A transformation is evidence, not a rewrite
Section titled “12. A transformation is evidence, not a rewrite”A transform-stage processor that changes context leaves a record a reviewer
can re-derive without trusting the runtime: excising the recorded spans from
the sealed source text must reproduce the recorded output hash, and the
original envelope — its own content hash, its provider metadata — stays
addressable beside the transformed text that entered the window. An
extraction record is re-derived by applying the rules the configuration
digest pins to the bytes retrieved_hash names, which reproduces
content_hash; the crossing carries both hashes, and the record that ties
them is written before it. A processor
finding never copies the content it found: the PII detector records categories
and byte offsets, checkable against the sealed bytes, because an evidence
trail that repeats an identifier has disclosed the data it exists to detect.
the_optimiser_reduces_admitted_tokens_without_dropping_a_source(crates/commonmeasure-runtime/tests/end_to_end.rs)the_output_hash_is_rederivable_by_excising_the_recorded_spans(crates/commonmeasure-runtime/src/processor/optimise.rs)the_output_is_a_function_of_the_input_under_the_pinned_rules(crates/commonmeasure-runtime/src/processor/extract.rs),an_html_page_is_delivered_as_extracted_text_with_both_hashes_recorded,a_gzip_page_is_carried_with_the_retrieved_hash_over_the_coded_bytes(crates/commonmeasure-cli/tests/mediated_e2e.rs)a_finding_is_carried_as_a_breach_and_never_refuses(crates/commonmeasure-runtime/src/processor/pii.rs),a_fetch_carrying_pii_from_a_private_address_is_carried_in_strict_mode_and_recorded(crates/commonmeasure-cli/tests/mediated_e2e.rs) — the identifier itself appears in no record.
13. A replay run refuses to start rather than degrade
Section titled “13. A replay run refuses to start rather than degrade”A missing or altered recording aborts a replay run before any run directory
exists. This is deliberately not clause 5: a plan marked unavailable would
make a weaker run that still calls itself a replay, and a reader diffing two
“replay” runs must never discover afterwards that one replayed less than the
other without saying so. The refusal names the provider or the hash
disagreement; nothing is published.
a_provider_without_a_recording_refuses_to_start,an_altered_capture_refuses_to_serve(crates/commonmeasure-runtime/tests/replay_mode.rs)
14. A host’s failure paces its next request
Section titled “14. A host’s failure paces its next request”Mediated fetches keep response-driven back-off per host and per edge in the
crawl-delay store. A 429 or 503 honours a usable Retry-After in seconds
or HTTP-date, capped at one hour and recorded as capped. Other 5xx answers,
or 429 and 503 without a usable header, start at 10 seconds, double for
consecutive failure periods and stop at 15 minutes. Failed answers inside
one period keep the count and the later end. Every non-5xx answer other
than 429 resets the count. A success does not shorten a wait another
concurrent answer already imposed.
The edge does not retry a failed request. Pages, redirect hops, robots,
licence and manifest probes all observe back-off. The next request waits
only if the wait fits the fetch’s remaining budget; otherwise it is refused
with back-off named. Own edges name the host and the end. Hosted refusals
withhold another tenant’s status, failure count and timing; they name the end
only when an uncapped Retry-After HTTP-date set it. Crawl-delay and back-off both
bind: the later permitted send wins. The crossing records back-off
separately from its crawl-delay ruling, including waits, refusals and store
failures. A back-off refusal before a crawl-delay turn writes no delay ruling.
After back-off ends, one sender reserves the host for its request timeout;
other callers wait for its answer within their budgets. Its answer clears or
renews the reservation. A request that ends without an answer, such as a
refused connection, releases it without counting a failure, and a
reservation left by a stopped process expires. A response whose headers or
body cannot be read whole counts as no answer: its status and Retry-After
are not recorded.
An unreadable store refuses before the request. A read-only store does not
prevent a healthy host’s successful answer when no failure needs resetting:
that answer takes no back-off lock and writes nothing to the back-off store.
A host this edge paces for Crawl-delay is different: its answer takes the
host’s crawl-delay lock and writes the time of the answer to its record, and
when that cannot be done the answer is not used, as for a back-off update
that cannot be kept. An update that cannot be kept
is an edge failure, never cached or attributed to the host as an unreachable
robots.txt. A page’s crossing keeps the answer’s http_status, and its
failure says the host answered and the answer was not used. The error
retains its cause and names what to repair: that host’s back-off file where
the record cannot be read, and the back-off directory where it cannot be
created, locked, written or removed. A lock another process held for the
whole wait is named as held, and a lock file that exists and cannot be
opened is named, with its owner’s uid, to be made readable and writable by
this user, or removed once any commonmeasure process run as another user,
such as under sudo, has been stopped. The next crossing asks again after repair.
A store whose directory cannot be written keeps no back-off, so a failing
host’s 429 or 5xx paces nothing: a host that states no Crawl-delay is asked
again at the rate the agent calls, and each crossing records the answer as
not used and names the directory. A host that states a Crawl-delay is not
asked, because its crawl-delay turn cannot be kept either, and a turn that
cannot be kept refuses the request
(session evidence).
retry_after_is_shared_by_sessions_and_a_restarted_process,a_503_http_date_sets_the_next_send_and_refuses_a_short_budget,a_429_without_retry_after_waits_ten_seconds_then_success_resets_the_count,robots_licence_and_manifest_failures_all_set_host_backoff(crates/commonmeasure-cli/tests/crawl_delay.rs) — loopback transport, session evidence, shared state and restartfailures_double_cap_and_reset_using_response_times(crates/commonmeasure-harness/src/crawl_delay/backoff.rs) — injected response times, exponential cap and reset
Execution modes
Section titled “Execution modes”Each execution mode is named in the run’s output and in the console, and no mode is presented as another:
| Mode | Context supply | Inference | Evidence claim |
|---|---|---|---|
| no external acquisition | internal corpus query, if configured; no external call | configured gateway, if any | internal and baseline plans may complete; external plans are unavailable |
| recorded replay | recorded provider responses served through the real adapters | real local or configured model route | replay-tested |
| live experiment | authenticated provider calls, response bytes sealed | real pinned model route | live-verified per plan |
| harness session | observed or mediated harness activity | model used by the host | real local evidence |
No mode may silently fall back to a weaker one: a missing dependency produces an explicit unavailable result or gap (§5).