Interlock

Tests and experiments

Generated by python3 tests/report.py; do not edit by hand. Every number here is read from the repo or from the suite run that produced this page, on Python 3.14.6.

Numbers you can quote

1. Unit and integration tests

Run with python3 -m unittest discover -s tests. Stdlib only.

CI (.github/workflows/test.yml): a matrix over Python 3.9, 3.12, 3.13 with fail-fast: false. Steps: “Invariants, fault sweep, integration”; “Results tables match the code”. The last step reruns experiments/run_all.py and viewer/build.py, then git diff --exit-code on results/, so a results table that no longer matches the code fails the build.

Last run: 465 tests, 412 passed, 53 skipped, 0 failed, on Python 3.14.6.

file tests
tests/test_approval_recovery_fixes.py 7
tests/test_approvals.py 9
tests/test_azure_deploy.py 10
tests/test_backend.py 6
tests/test_confirmations.py 16
tests/test_contract.py 8
tests/test_demo.py 16
tests/test_demo_standalone.py 7
tests/test_escalation_fixes.py 37
tests/test_escalation_flow.py 6
tests/test_escalation_receipts.py 14
tests/test_explained.py 11
tests/test_export.py 13
tests/test_export_authority_fixes.py 2
tests/test_integration_recovery_fixes.py 5
tests/test_integrations.py 23
tests/test_interlock.py 20
tests/test_langchain.py 2
tests/test_lockin_review_fixes.py 10
tests/test_mcp_proxy.py 10
tests/test_merge_defects.py 9
tests/test_public.py 27
tests/test_receipts.py 8
tests/test_recovery_locking.py 5
tests/test_repair.py 11
tests/test_repo_recovery_fixes.py 5
tests/test_review_fixes.py 12
tests/test_routing.py 24
tests/test_runtime.py 21
tests/test_runtime_gate_parity.py 12
tests/test_runtime_sql_races.py 9
tests/test_runtime_stateful.py 1
tests/test_scenario_billing_credit.py 7
tests/test_scenario_calendar.py 6
tests/test_scenario_connect_payout.py 6
tests/test_scenario_email_tier3.py 14
tests/test_scenario_gcp_resource.py 13
tests/test_scenario_github_merge.py 10
tests/test_scenario_shared_cap.py 8
tests/test_scenario_stripe_dispute.py 9
tests/test_scoreboard.py 5
tests/test_site.py 9
tests/test_temporal_helper.py 2
total 465

tests/test_approval_recovery_fixes.py (7)

Regressions for approval audit records and retries after a fast restart.

ApprovalRecoveryFixes (7)

tests/test_approvals.py (9)

The approval inbox: routine requests never reach a person, judgment calls do, and a person’s approval is re-checked against the world when the refund is actually sent.

ApprovalInbox (9)

tests/test_azure_deploy.py (10)

Offline checks for infra/azure/deploy.sh. The dry run prints the plan without calling az or leaking secret values, and a Stripe key that is not test mode is refused. A real run against a stub az (logs its argv, returns canned values) in a throwaway git repo covers the app PUT and provisioning wait, the FQDN re-apply, the empty-FQDN stop, the env-name guard, the committed-only build context and temp file cleanup. Skipped if bash or git is missing.

AzureDeployScript (10)

tests/test_backend.py (6)

Offline checks for backend/: approval leases shared across connections and capped at the approved amount, the one-shot crash marker (a real SIGKILL of a child process), and validation of the model’s refund call.

Backend (6)

tests/test_confirmations.py (16)

Target-confirmed receipts: a signed Stripe webhook (or a refunds lookup) appends CONFIRMED to the effect’s chain. Unsigned, replayed, live-mode or mismatched events are never recorded. Offline: signed locally with a test endpoint secret, against a fake Stripe client.

Recording (8)

Signature (8)

tests/test_contract.py (8)

The escalation contract every lane builds on: atomic append_if, the gate refusing to send an escalated effect without a person’s decision on its latest escalation, and the shared shapes.

AppendIf (2)

GateBlocker (4)

Shapes (2)

tests/test_demo.py (16)

Offline checks for backend/demo.py: the verdict and wording shown on the page, the one-run-at-a-time guard, and that a mock run is the in-process simulation with every event marked mock. For backend/api.py: its routes for both demos (the standalone engine stubbed), Temporal unavailable, one run across both demos, and that the server starts without temporalio. No Stripe, model or Temporal.

ApiRequestChecks (1)

ApiRoutes (4)

Demo (9)

NoTemporal (2)

tests/test_demo_standalone.py (7)

Offline checks for backend/standalone.py and backend/standalone_worker.py: nothing they import pulls in temporalio, the one-run guard is shared with the Temporal demo, the mock run is labeled, and the worker’s saved decision and refund step behave as described, against a fake Stripe. No Stripe, model, Temporal or worker process.

Standalone (7)

tests/test_escalation_fixes.py (37)

Regressions for confirmed defects in escalation, routing, confirmation and scoreboard code. Each test failed before its fix.

AmbiguousKeepsItsGuard (1)

BoundRequest (2)

ConfirmationPayment (1)

Confirmations (5)

ExplainPicksTheOutcome (1)

OneBadCapture (3)

PersonLeaseAtDispatch (3)

ProxySettlesOnlyItsOwnSend (3)

ReadmeMatchesResults (1)

ReescalationReason (1)

RepairAutoApprovalWait (1)

RepairChildCrash (1)

RepairChildIsNotCleared (1)

RouteLostItsSla (1)

ScoreboardCountsOnce (2)

StaleCachedApproval (1)

StaleRepairs (1)

StripeSubclassPremises (1)

TamperedReceipt (1)

TargetRejection (3)

TemporalOutcome (1)

TickAfterOutage (2)

tests/test_escalation_flow.py (6)

The lanes together: a real gate refusal becomes a routed escalation, a person’s decision becomes the send’s authority, verify() checks it, a signed webhook confirms it, and the scoreboard reads it all back from the journal, before and after a restart.

Flow (6)

tests/test_escalation_receipts.py (14)

Escalations in receipts: verify() checks that a send a person authorized has that person’s decision, on the facts they saw, before the send, from someone the item was routed to, and reports what the target itself confirmed.

EscalationReceipts (14)

tests/test_explained.py (11)

Explained refusals: a stale or unverifiable outcome names what changed and, where nothing landed, suggests safe repairs. Suggestions only: nothing here sends a repair.

Explained (11)

tests/test_export.py (13)

Receipt exporters, offline: payload shapes, the ids that make re-exporting harmless, the BigQuery schema, and that a destination’s partial rejection is reported as a failure. No network: HTTP calls are patched.

BigQuery (5)

Logging (4)

Otlp (2)

Siem (2)

tests/test_export_authority_fixes.py (2)

All exporters name the authority of the send and preserve historical entry attribution.

ExportAuthority (2)

tests/test_integration_recovery_fixes.py (5)

Failed tool reads stay unresolved, and ADK recovery keeps the recorded target.

AdkRecordedTarget (1)

ToolReadFailures (4)

tests/test_integrations.py (23)

Offline checks for interlock/integrations. Without google-adk or the AP2 SDK installed, the tests that need them skip; the ADK callback logic and the mandate checks around the SDK still run, against fakes.

AdkGuard (8)

MandateChecks (9)

RealMandates (6)

tests/test_interlock.py (20)

Every claim in the README, as an assertion, plus a randomized fault sweep and the three-line integration. If a table in results/ and these tests disagree, the table is wrong.

ExperimentClaims (4)

RandomFaultSweep (1)

Receipts (4)

Resubmission (1)

SharedJournal (3)

ThreeLineIntegration (7)

tests/test_langchain.py (2)

uv run –no-project –with langchain-core –with langgraph python -m unittest tests.test_langchain

LangChainTools (2)

tests/test_lockin_review_fixes.py (10)

Regressions for the lock-in review of the recovery, approval, export and repository fixes.

LockinReviewFixes (10)

tests/test_mcp_proxy.py (10)

The MCP proxy against a real subprocess MCP server: passthrough, one refund per request, recovery after the proxy is killed mid-call (with and without the initialize handshake), and refusal when support refunded by hand while it was down.

McpProxy (6)

RealSdk (1)

StartupRecovery (3)

tests/test_merge_defects.py (9)

Defects the escalation merge kept or introduced, each reproduced before its fix.

EasyPremiseReads (2)

EnvelopeLeaseMessage (1)

OldNames (1)

ProxyUpstreamExit (1)

SameArgsRedecision (1)

TargetErrorIsFinal (3)

tests/test_public.py (27)

Offline checks for public mode in backend/api.py (docs/deploy.md): settings, the Host allowlist, which X-Forwarded-For entry is trusted, per-client and daily limits on a fake clock, /healthz, security headers, errors without exception text, and that with public mode off none of it applies. No Stripe, model, Temporal or worker.

ConnectionCap (1)

Limits (6)

PublicModeOff (1)

PublicProcess (2)

PublicServer (10)

Runs (1)

Settings (6)

tests/test_receipts.py (8)

Receipts as proof: verify() re-derives “happened once, authorized when it fired, assumptions held” from the hash-chained entries, and notices when they were tampered with.

Receipts (8)

tests/test_recovery_locking.py (5)

Recovery ownership and file locks must hold across calls, threads and processes.

RecoveryLocking (5)

tests/test_repair.py (11)

The repair loop through tools.protect(): a refused call says what changed, a corrected call inside its approval is sent once, and nothing outside the approval, or after it was used, is sent at all.

RepairLoop (11)

tests/test_repo_recovery_fixes.py (5)

Repository recovery recognizes this effect’s durable postimages, including partial writes.

RepositoryRecovery (5)

tests/test_review_fixes.py (12)

Regression tests for the defects an adversarial review of the package found. Each one reproduced the defect before its fix.

ReviewFixes (12)

tests/test_routing.py (24)

Routing, SLAs and a restart-safe inbox: an escalation goes to the group its route names, only that group decides it, an unanswered item moves up the chain, a repair is a new decision, and a fresh inbox rebuilds the same queue from the journal without losing or duplicating anything.

Routing (24)

tests/test_runtime.py (21)

interlock_runtime against real Postgres and real worker subprocesses.

Composition (3)

Decisions (1)

Effects (6)

FirstGate (1)

Signals (3)

Steps (3)

Timers (1)

Versioning (3)

tests/test_runtime_gate_parity.py (12)

Differential test: interlock.Gate against interlock_runtime, fault by fault, tier by tier.

GateParity (12)

tests/test_runtime_sql_races.py (9)

Interleaved races on real Postgres, no subprocesses: two connections, one transaction held open while the other runs.

Races (9)

tests/test_runtime_stateful.py (1)

R6: a Hypothesis RuleBasedStateMachine against real Postgres, real worker subprocesses and a real HTTP target.

Stateful (1)

tests/test_scenario_billing_credit.py (7)

Offline logic of scenarios/billing_credit: premises, judging, the target, and the hand check. No network.

HandCheck (2)

Judge (2)

Premises (2)

PublishedReceipts (1)

tests/test_scenario_calendar.py (6)

Offline logic for scenarios/calendar: an in-memory calendar stands in; the live run uses Google Calendar and SIGKILL.

CalendarScenario (6)

tests/test_scenario_connect_payout.py (6)

Offline logic for scenarios/connect_payout: an in-memory Stripe stands in; the live run uses real Stripe and SIGKILL.

ConnectPayoutLogic (6)

tests/test_scenario_email_tier3.py (14)

Scenario email_tier3, offline logic only: no network, no keys. The live run is experiments/scenario_email_tier3.py.

HandCheck (3)

Probe (3)

Recovery (5)

RefundAlreadyFailedAtDecision (1)

Scoring (2)

tests/test_scenario_gcp_resource.py (13)

Offline logic of the gcp_resource scenario: no network. The live run is experiments/scenario_gcp_resource.py.

HandCheck (3)

Judging (3)

Record (4)

Target (3)

tests/test_scenario_github_merge.py (10)

Offline logic for scenarios/github_merge. The live run is experiments/scenario_github_merge.py.

Decision (1)

Judge (2)

Systems (7)

tests/test_scenario_shared_cap.py (8)

Offline logic of scenarios/shared_cap: the cap reservation, the headroom premise, the hand check, ground truth.

SharedCap (8)

tests/test_scenario_stripe_dispute.py (9)

Offline logic for scenarios/stripe_dispute: an in-memory Stripe stands in; the live run uses real Stripe, a real model and SIGKILL.

StripeDisputeLogic (9)

tests/test_scoreboard.py (5)

The approvals scoreboard is derived from the journal alone, so it survives restarts and can be recomputed by anyone holding the entries.

Scoreboard (4)

SyntheticDay (1)

tests/test_site.py (9)

The landing page and the short README stay correct: the copied prompt matches docs/install-with-ai.md, snippets parse, the live demo URL lives in one constant, and every relative link in the moved docs resolves.

PromptCrashRecipe (1)

Site (8)

tests/test_temporal_helper.py (2)

interlock.temporal.gated() without a Temporal server: each attempt recovers before it submits, and refusals stop the retries. experiments/temporal_live.py covers a real server.

GatedActivity (2)

2. Experiments and live runs

Tables are copied from each results file; its check marks are written as words (held, VIOLATED, held, availability lost) and its middle-dot separators as /. Real Stripe ids are as they appear there.

refund_agent

fault naive idempotency@tier1 durable@tier1 gate@tier1 gate@tier2 gate@tier3
happy_path APPLIED $20 held APPLIED:ok $20 held COMPLETED:ok $20 held COMMITTED $20 held COMMITTED $20 held COMMITTED $20 held
crash_before_send RETRIED $20 held RETRIED $20 held RERUN:ok $20 held COMMITTED_BY_RETRY $20 held REAPPLIED_AFTER_QUERY $20 held AMBIGUOUS $0 held, availability lost
crash_before_ack RETRIED $40 VIOLATED RETRIED $20 held RERUN:already_processed $20 held COMMITTED_BY_RETRY $20 held COMMITTED_ON_QUERY $20 held AMBIGUOUS $20 held, availability lost
duplicate_submit APPLIED $40 VIOLATED APPLIED:already_processed $20 held REPLAYED:ok $20 held DUPLICATE_IGNORED $20 held DUPLICATE_IGNORED $20 held DUPLICATE_IGNORED $20 held
model_redecides APPLIED $50 VIOLATED APPLIED:key_reused_with_different_params $20 held REPLAYED:RERUN:already_processed $20 held REFUSED:conflicting_payload $20 held REFUSED:conflicting_payload $20 held REFUSED:conflicting_payload $20 held
conflicting_payload APPLIED $50 VIOLATED APPLIED:key_reused_with_different_params $20 held REPLAYED:ok $20 held REFUSED:conflicting_payload $20 held REFUSED:conflicting_payload $20 held REFUSED:conflicting_payload $20 held
lease_revoked APPLIED $20 VIOLATED APPLIED:ok $20 VIOLATED COMPLETED:ok $20 VIOLATED REFUSED:lease $0 held REFUSED:lease $0 held REFUSED:lease $0 held
stale_eligibility APPLIED $20 VIOLATED APPLIED:ok $20 VIOLATED COMPLETED:ok $20 VIOLATED REFUSED:stale_premise $0 held REFUSED:stale_premise $0 held REFUSED:stale_premise $0 held
refund_during_outage RETRIED $40 VIOLATED RETRIED $40 VIOLATED RERUN:ok $40 VIOLATED REFUSED:stale_premise_at_recovery $20 held REFUSED:stale_premise_at_recovery $20 held AMBIGUOUS $20 held, availability lost
lease_revoked_during_outage RETRIED $20 VIOLATED RETRIED $20 VIOLATED RERUN:ok $20 VIOLATED REFUSED:lease_at_recovery $0 held REFUSED:lease_at_recovery $0 held AMBIGUOUS $0 held, availability lost
key_expired RETRIED $40 VIOLATED RETRIED $40 VIOLATED RERUN:ok $40 VIOLATED COMMITTED_ON_QUERY $20 held COMMITTED_ON_QUERY $20 held AMBIGUOUS $20 held, availability lost

coding_agents

fault naive gate/file gate/symbol
rename_break APPLIED landed_BROKEN VIOLATED REFUSED:stale_premise not_landed held REFUSED:stale_premise not_landed held
duplicate_work APPLIED 2 impl VIOLATED REFUSED:claimed_by_A 1 impl held REFUSED:claimed_by_A 1 impl held
benign_reformat APPLIED landed_ok held REFUSED:stale_premise not_landed held, availability lost COMMITTED landed_ok held
crash_before_commit RETRIED landed_ok VIOLATED COMMITTED_BY_RETRY landed_ok held COMMITTED_BY_RETRY landed_ok held
duplicate_submit APPLIED landed_ok VIOLATED DUPLICATE_IGNORED landed_ok held DUPLICATE_IGNORED landed_ok held
lease_revoked APPLIED landed_ok VIOLATED REFUSED:lease not_landed held REFUSED:lease not_landed held
semantic_only APPLIED landed_BROKEN VIOLATED REFUSED:stale_premise not_landed held COMMITTED landed_BROKEN VIOLATED

stripe_live

fault naive (today) idempotency key only gate (Stripe: key + lookup)
crash_before_ack RETRIED $40 VIOLATED RETRIED $20 held COMMITTED_BY_RETRY $20 held
duplicate_submit APPLIED $40 VIOLATED APPLIED:already_processed $20 held DUPLICATE_IGNORED $20 held
refund_during_outage RETRIED $40 VIOLATED RETRIED $40 VIOLATED REFUSED:stale_premise_at_recovery $20 held

temporal_live

fault Temporal, recommended idempotency key Temporal with Interlock as the activity body
crash_before_ack COMPLETED / 2 attempts / $20 held COMMITTED_BY_RETRY / 2 attempts / $20 held
refund_during_outage COMPLETED / 2 attempts / $40 VIOLATED REFUSED:stale_premise_at_recovery / 2 attempts / $20 held
lease_revoked_during_outage COMPLETED / 2 attempts / $20 VIOLATED REFUSED:lease_at_recovery / 2 attempts / $0 held
stale_eligibility COMPLETED / 1 attempt / $20 VIOLATED REFUSED:stale_premise / 1 attempt / $0 held

e2e_live

scenario Temporal: idempotency key, no re-check in the activity Temporal: idempotency key plus a hand-written re-check Temporal with Interlock as the activity body
crash_after_commit REPLAYED_BY_STRIPE; $20 in 1 refund (want $20 in 1); attempt 2, 15.2s crash to close; held; answer matches Stripe FOUND_BY_LOOKUP; $20 in 1 refund (want $20 in 1); attempt 2, 14.7s crash to close; held; answer matches Stripe COMMITTED_BY_RETRY; $20 in 1 refund (want $20 in 1); attempt 8, 44.0s crash to close; held; answer matches Stripe
hand_refund_before_decision REPLAYED_BY_STRIPE; $25 in 2 refunds (want $25 in 2); attempt 2, 15.1s crash to close; held; answer matches Stripe FOUND_BY_LOOKUP; $25 in 2 refunds (want $25 in 2); attempt 2, 14.7s crash to close; held; answer matches Stripe COMMITTED_BY_RETRY; $25 in 2 refunds (want $25 in 2); attempt 8, 42.8s crash to close; held; answer matches Stripe
hand_refund_during_outage REFUNDED; $40 in 2 refunds (want $20 in 1); attempt 2, 16.8s crash to close; VIOLATED, $20 too much; answer matches Stripe REFUSED:stale_premise; $20 in 1 refund (want $20 in 1); attempt 2, 16.0s crash to close; held; answer matches Stripe REFUSED:stale_premise_at_recovery; $20 in 1 refund (want $20 in 1); attempt 8, 43.6s crash to close; held; answer matches Stripe
unrelated_refund_during_outage REFUNDED; $25 in 2 refunds (want $25 in 2); attempt 2, 16.5s crash to close; held; answer matches Stripe REFUSED:stale_premise; $5 in 1 refund (want $25 in 2); attempt 2, 15.7s crash to close; SHORT by $20; answer matches Stripe REFUSED:stale_premise_at_recovery; $5 in 1 refund (want $25 in 2); attempt 8, 43.3s crash to close; SHORT by $20; answer matches Stripe
approval_revoked_during_outage REFUNDED; $20 in 1 refund (want $0 in 0); attempt 2, 16.8s crash to close; VIOLATED, $20 too much; answer matches Stripe REFUSED:lease; $0 in 0 refunds (want $0 in 0); attempt 2, 16.0s crash to close; held; answer matches Stripe REFUSED:lease_at_recovery; $0 in 0 refunds (want $0 in 0); attempt 8, 43.6s crash to close; held; answer matches Stripe
approval_revoked_after_commit REPLAYED_BY_STRIPE; $20 in 1 refund (want $20 in 1); attempt 2, 14.8s crash to close; held; answer matches Stripe FOUND_BY_LOOKUP; $20 in 1 refund (want $20 in 1); attempt 2, 14.8s crash to close; held; answer matches Stripe COMMITTED_ON_QUERY; $20 in 1 refund (want $20 in 1); attempt 8, 43.3s crash to close; held; answer matches Stripe
key_pruned_after_24h (EMULATED) REFUNDED; $40 in 2 refunds (want $20 in 1); attempt 2, 15.9s crash to close; VIOLATED, $20 too much; answer CONTRADICTS Stripe FOUND_BY_LOOKUP; $20 in 1 refund (want $20 in 1); attempt 2, 15.0s crash to close; held; answer matches Stripe COMMITTED_ON_QUERY; $20 in 1 refund (want $20 in 1); attempt 8, 43.1s crash to close; held; answer matches Stripe
no_lookup_after_24h (EMULATED) n/a n/a AMBIGUOUS; $20 in 1 refund (want $20 in 1); attempt 8, 42.1s crash to close; held; answer matches Stripe

e2e_audit

scenario                        mode              cents  want  audit  flag      detail
crash_after_commit              temporal          2000   2000  PASS             
crash_after_commit              temporal_checked  2000   2000  PASS             
crash_after_commit              interlock         2000   2000  PASS             
hand_refund_before_decision     temporal          2500   2500  PASS             
hand_refund_before_decision     temporal_checked  2500   2500  PASS             
hand_refund_before_decision     interlock         2500   2500  PASS             
hand_refund_during_outage       temporal          4000   2000  PASS             
hand_refund_during_outage       temporal_checked  2000   2000  PASS             
hand_refund_during_outage       interlock         2000   2000  PASS             
unrelated_refund_during_outage  temporal          2500   2500  PASS             
unrelated_refund_during_outage  temporal_checked  500    2500  PASS             
unrelated_refund_during_outage  interlock         500    2500  PASS             
approval_revoked_during_outage  temporal          2000   0     PASS             
approval_revoked_during_outage  temporal_checked  0      0     PASS             
approval_revoked_during_outage  interlock         0      0     PASS             
approval_revoked_after_commit   temporal          2000   2000  PASS             
approval_revoked_after_commit   temporal_checked  2000   2000  PASS             
approval_revoked_after_commit   interlock         2000   2000  PASS             
key_pruned_after_24h            temporal          4000   2000  PASS   EMULATED  
key_pruned_after_24h            temporal_checked  2000   2000  PASS   EMULATED  
key_pruned_after_24h            interlock         2000   2000  PASS   EMULATED  
no_lookup_after_24h             interlock         2000   2000  PASS   EMULATED  receipt AMBIGUOUS, Stripe has 1 refund(s) for 6cce6c06a86a

22/22 cells verified against Stripe

approval_inbox

system reviews a person did orders refunded the wrong amount
everyone approves 100 10
rules only 25 10
rules + Interlock 36 0

repair_loop

system no person person wrong payouts overpaid
no gate 100 0 40 $558
hand check 97 3 0 $0
interlock 83 17 13 $130
interlock+repair 97 3 0 $0

escalation_live

step wall clock (UTC) detail
payment 2026-09-14T01:00:11+00:00 payment_intent=pi_3UFOFi88KhIqqdFL1ueAaUx9, amount=10000
listener 2026-09-14T01:00:13+00:00 ready=True
request 2026-09-14T01:00:13+00:00 request_id=refund-50/pi_3UFOFi88KhIqqdFL1ueAaUx9, status=QUEUED, reason=needs_judgment, detail=[‘amount at most 1000 cents’], group=ap-leads
approved 2026-09-14T01:00:13+00:00 by=ana, status=APPROVED
hand refund 2026-09-14T01:00:15+00:00 refund=re_3UFOFi88KhIqqdFL1Qpt3qv8, amount=3000, status=succeeded
send 2026-09-14T01:00:16+00:00 status=REFUSED:stale_premise, reason=stale_premise, changes=[{‘field’: ‘refunded_by_others’, ‘was’: 0, ‘now’: 3000}], repairs=[‘still_fits’]
repair accepted 2026-09-14T01:00:17+00:00 by=ana, repair=still_fits, status=COMMITTED
second request 2026-09-14T01:00:18+00:00 request_id=refund-15/pi_3UFOFi88KhIqqdFL1ueAaUx9, status=QUEUED, group=ap-leads
sla tick 2026-09-14T01:00:18+00:00 moved={‘refund-15/pi_3UFOFi88KhIqqdFL1ueAaUx9’: ‘finance-manager’}, group=finance-manager, level=1, breach=True
ap-leads after breach 2026-09-14T01:00:18+00:00 by=ana, status=REFUSED:lease
approved up the chain 2026-09-14T01:00:19+00:00 by=fm, status=COMMITTED
confirmation 2026-09-14T01:00:20+00:00 path=webhook

Real-world scenario suite (experiments/scenario_*.py)

Each scenario runs one agent step against the standard setup, a fair hand-written check and Interlock, with a real SIGKILL and ground truth read back from the live service. Offline logic for each is in tests/test_scenario_<key>.py. Summary: results/scenarios/README.md.

scenario system held proof settle (s) source
stripe_dispute no_check 2/2 0/2 6.8, 8.6 results/scenarios/stripe_dispute.md
  hand_check 2/2 2/2 6.4, 11.2 same
  interlock 2/2 2/2 42.4, 43.0 same
shared_cap no_check 0/40 0/40 median 1.1 / 2.3 results/scenarios/shared_cap.md
  hand_check (no shared state) 4/40 0/40 median 1.1 / 2.3 same
  hand_lock (hand_check inside an flock) 40/40 0/40 median 0.5 / 1.4 same
  interlock_core (unmodified Gate) 25/40 40/40 median 40.1 / 41.0 same
  interlock + CapJournal (scenario subclass) 40/40 40/40 median 40.1 / 41.7 same
billing_credit no_check 5/6 0/6 median 17 (13.7 to 25.7) results/scenarios/billing_credit.md
  hand_check 6/6 6/6 median 17 (14.1 to 23.7) same
  interlock 6/6 6/6 median 41 (41.0 to 41.8) same
github_merge no_check 3/6 0/6 median 8.5 / 2.4 results/scenarios/github_merge.md
  hand_check 6/6 6/6 median 7.7 / 2.5 same
  interlock 6/6 6/6 median 26.1 / 24.4 same
calendar no_check 5/7 0/7 median 0.6 results/scenarios/calendar.md
  hand_check 7/7 0/7 median 0.6 same
  interlock (slot premise must be empty) 7/7 7/7 median 31.2 same
  interlock_change_only (easy.py as shipped) 0/1 1/1 31.5 same
email_tier3 no_check 2/3 0/3 median 2 results/scenarios/email_tier3.md
  hand_check (with key probe) 3/3 3/3 median 3 same
  hand_check_noprobe (idiomatic) 3/3 2/3 median 2 same
  interlock (with key probe) 3/3 3/3 median 37 same
  interlock_noprobe 3/3 1/3 median 36 same
  interlock_tier3 3/3 0/3 median 36 same
gcp_resource no_check 0/3 0/3 0.4 to 2.0 results/scenarios/gcp_resource.md
  hand_check 3/3 3/3 0.5 to 2.1 same
  interlock 3/3 3/3 30.6 to 30.9 same
connect_payout all BLOCKED BLOCKED not run results/scenarios/connect_payout.md

stripe_dispute

fault no_check hand_check interlock
crash_before_send_chargeback_during_outage STRIPE_ERROR; 0 refund object(s) (final: none), want 0; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: matches; 6.8s; proof no (checks no, outcome no, tamper-evident no) REFUSED:charged_back; 0 refund object(s) (final: none), want 0; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: matches; 6.4s; proof yes (checks yes, outcome yes, tamper-evident no) REFUSED:stale_premise_at_recovery; 0 refund object(s) (final: none), want 0; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: matches; 42.4s; proof yes (checks yes, outcome yes, tamper-evident yes)
crash_after_send_chargeback_during_outage REPLAYED_BY_STRIPE; 1 refund object(s) (final: failed (charge_for_pending_refund_disputed)), want 1; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: CONTRADICTS; 8.6s; proof no (checks no, outcome no, tamper-evident no) FOUND_BY_LOOKUP; 1 refund object(s) (final: failed (charge_for_pending_refund_disputed)), want 1; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: CONTRADICTS; 11.2s; proof yes (checks yes, outcome yes, tamper-evident no) COMMITTED_ON_QUERY; 1 refund object(s) (final: failed (charge_for_pending_refund_disputed)), want 1; created at or after the chargeback: none; money returned: no; dispute needs_response; held; answer vs refund objects: matches; answer vs final money: CONTRADICTS; 43.0s; proof yes (checks yes, outcome yes, tamper-evident yes)

shared_cap

crash point no_check (stable idempotency key, retry on restart) hand_check (read Stripe refunds, then send; no shared state) hand_lock (hand_check inside an flock on the shared dir) interlock_core (unmodified Gate, headroom premise) interlock + CapJournal (scenario subclass, cap reserved at dispatch)
after_commit held 0/20; Stripe: 20x $40 in 2; crash in 20/20; median 1.1s crash to settled; provable 0/20 held 3/20; Stripe: 3x $20 in 1, 17x $40 in 2; crash in 20/20; median 1.1s crash to settled; provable 0/20 held 20/20; Stripe: 20x $20 in 1; crash in 20/20; median 0.5s crash to settled; provable 0/20 held 5/20; Stripe: 5x $20 in 1, 15x $40 in 2; crash in 20/20; median 40.1s crash to settled; provable 20/20 held 20/20; Stripe: 20x $20 in 1; crash in 20/20; median 40.1s crash to settled; provable 20/20
before_send held 0/20; Stripe: 20x $40 in 2; crash in 20/20; median 2.3s crash to settled; provable 0/20 held 1/20; Stripe: 1x $20 in 1, 19x $40 in 2; crash in 20/20; median 2.3s crash to settled; provable 0/20 held 20/20; Stripe: 20x $20 in 1; crash in 20/20; median 1.4s crash to settled; provable 0/20 held 20/20; Stripe: 20x $20 in 1; crash in 20/20; median 41.0s crash to settled; provable 20/20 held 20/20; Stripe: 20x $20 in 1; crash in 20/20; median 41.7s crash to settled; provable 20/20

billing_credit

crash / outage no_check hand_check interlock
before_send / renewal CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof no; 18.1s crash to settled (0.8s after restart) CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 23.7s crash to settled (1.5s after restart) COMMITTED_BY_RETRY via retry-idempotent; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 41.8s crash to settled (14.0s after restart)
before_send / billing_credit CREDITED; 1 case credit(s), $10 (want 0, $0); VIOLATED; answer matches Stripe; proof no; 19.1s crash to settled (0.4s after restart) REFUSED:stale_premise; 0 case credit(s), $0 (want 0, $0); held; answer matches Stripe; proof yes; 21.4s crash to settled (1.2s after restart) REFUSED:stale_premise_at_recovery; 0 case credit(s), $0 (want 0, $0); held; answer matches Stripe; proof yes; 41.5s crash to settled (27.5s after restart)
before_send / proration CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof no; 16.6s crash to settled (0.8s after restart) CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 17.0s crash to settled (1.4s after restart) COMMITTED_BY_RETRY via retry-idempotent; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 41.0s crash to settled (24.1s after restart)
after_send / renewal REPLAYED_BY_STRIPE; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof no; 15.5s crash to settled (0.4s after restart) FOUND_BY_LOOKUP; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 15.3s crash to settled (0.3s after restart) COMMITTED_BY_RETRY via retry-idempotent; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 41.1s crash to settled (20.3s after restart)
after_send / proration REPLAYED_BY_STRIPE; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof no; 13.7s crash to settled (0.3s after restart) FOUND_BY_LOOKUP; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 14.1s crash to settled (0.8s after restart) COMMITTED_BY_RETRY via retry-idempotent; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 41.2s crash to settled (27.4s after restart)
before_send / unrelated_credit CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof no; 25.7s crash to settled (0.7s after restart) CREDITED; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 16.5s crash to settled (1.2s after restart) COMMITTED_BY_RETRY via retry-idempotent; 1 case credit(s), $10 (want 1, $10); held; answer matches Stripe; proof yes; 41.1s crash to settled (21.0s after restart)

github_merge

fault no_check hand_check interlock
before_send MERGED x3; held 0/3; answer matched 3/3; proof 0/3; median 8.5s REFUSED:head_modified x3; held 3/3; answer matched 3/3; proof 3/3; median 7.7s REFUSED:stale_premise_at_recovery x3; held 3/3; answer matched 3/3; proof 3/3; median 26.1s
after_commit MERGED x3; held 3/3; answer matched 3/3; proof 0/3; median 2.4s ALREADY_MERGED x3; held 3/3; answer matched 3/3; proof 3/3; median 2.5s COMMITTED_ON_QUERY x3; held 3/3; answer matched 3/3; proof 3/3; median 24.4s

calendar

fault no_check hand_check interlock interlock_change_only
after_commit/none ALREADY_EXISTS_409; 1 agent event (want 1); held; answer matches; 0.6s; proof: none FOUND_BY_LOOKUP; 1 agent event (want 1); held; answer matches; 0.4s; proof: none COMMITTED_BY_RETRY; 1 agent event (want 1); held; answer matches; 31.5s; proof: receipt n/a
after_commit/slot_busy ALREADY_EXISTS_409; 1 agent event (want 1); held; answer matches; 1.2s; proof: none FOUND_BY_LOOKUP; 1 agent event (want 1); held; answer matches; 1.0s; proof: none COMMITTED_ON_QUERY; 1 agent event (want 1); held; answer matches; 30.7s; proof: receipt n/a
after_commit/request_cancelled ALREADY_EXISTS_409; 1 agent event (want 1); held; answer matches; 0.6s; proof: none FOUND_BY_LOOKUP; 1 agent event (want 1); held; answer matches; 0.3s; proof: none COMMITTED_ON_QUERY; 1 agent event (want 1); held; answer matches; 31.1s; proof: receipt n/a
before_send/none BOOKED; 1 agent event (want 1); held; answer matches; 0.5s; proof: none BOOKED; 1 agent event (want 1); held; answer matches; 0.8s; proof: none COMMITTED_BY_RETRY; 1 agent event (want 1); held; answer matches; 31.2s; proof: receipt n/a
before_send/slot_busy BOOKED; 1 agent event (want 0); VIOLATED, double booked; answer matches; 1.0s; proof: none REFUSED:slot_busy; 0 agent events (want 0); held; answer matches; 1.2s; proof: none REFUSED:stale_premise_at_recovery; 0 agent events (want 0); held; answer matches; 32.1s; proof: receipt n/a
before_send/request_cancelled BOOKED; 1 agent event (want 0); VIOLATED; answer matches; 0.5s; proof: none REFUSED:request_cancelled; 0 agent events (want 0); held; answer matches; 0.4s; proof: none REFUSED:lease_at_recovery; 0 agent events (want 0); held; answer matches; 31.2s; proof: receipt n/a
before_send/pre_busy REFUSED:slot_busy; 0 agent events (want 0); held; answer matches; 0.3s; proof: none; no crash (refused before the send) REFUSED:slot_busy; 0 agent events (want 0); held; answer matches; 0.6s; proof: none; no crash (refused before the send) REFUSED:stale_premise; 0 agent events (want 0); held; answer matches; 0.8s; proof: receipt; no crash (refused before the send) COMMITTED_BY_RETRY; 1 agent event (want 0); VIOLATED, double booked; answer matches; 31.5s; proof: receipt

email_tier3

system crash_after_send refused_before_send refused_after_send
no_check SENT_REPLAYED; 1 email (want 1); held; answer matches Resend; record proves it: no; 0.2s SENT; 1 email (want 0); VIOLATED (1 email, want 0); answer matches Resend; record proves it: no; 2.0s SENT_REPLAYED; 1 email (want 1); held; answer matches Resend; record proves it: no; 2.5s
hand_check ALREADY_SENT; 1 email (want 1); held; answer matches Resend; record proves it: yes; 0.8s SKIPPED:refund_failed; 0 email (want 0); held; answer matches Resend; record proves it: yes; 4.6s ALREADY_SENT; 1 email (want 1); held; answer matches Resend; record proves it: yes; 2.6s
hand_check_noprobe SENT_REPLAYED; 1 email (want 1); held; answer matches Resend; record proves it: yes; 0.5s SKIPPED:refund_failed; 0 email (want 0); held; answer matches Resend; record proves it: yes; 1.6s SKIPPED:refund_failed; 1 email (want 1); held; answer CONTRADICTS Resend; record proves it: no; 6.1s
interlock COMMITTED_BY_RETRY; 1 email (want 1); held; answer matches Resend; record proves it: yes; 35.9s REFUSED:stale_premise_at_recovery; 0 email (want 0); held; answer matches Resend; record proves it: yes; 36.7s COMMITTED_ON_QUERY; 1 email (want 1); held; answer matches Resend; record proves it: yes; 36.8s
interlock_noprobe COMMITTED_BY_RETRY; 1 email (want 1); held; answer matches Resend; record proves it: yes; 35.8s AMBIGUOUS; 0 email (want 0); held; answer cannot know (said so); record proves it: no; 36.0s AMBIGUOUS; 1 email (want 1); held; answer cannot know (said so); record proves it: no; 35.8s
interlock_tier3 AMBIGUOUS; 1 email (want 1); held; answer cannot know (said so); record proves it: no; 35.4s AMBIGUOUS; 0 email (want 0); held; answer cannot know (said so); record proves it: no; 35.9s AMBIGUOUS; 1 email (want 1); held; answer cannot know (said so); record proves it: no; 35.9s

gcp_resource

fault want no_check hand_check interlock
crash_before_send_redeploy the human’s v43 stays live; the stale rollback never lands WRITTEN; 1 agent write(s) [‘1789338914659713’]; live generation 1789338914659713 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 2.0s PRECONDITION_FAILED; 0 agent write(s) []; live generation 1789338918615028 deployed by human-redeploy; held; answer matches GCS; record says 0 agent write(s); proof yes; 1.8s REFUSED:stale_premise_at_recovery; 0 agent write(s) []; live generation 1789338923362145 deployed by human-redeploy; held; answer matches GCS; record says 0 agent write(s); proof yes; 30.9s
crash_after_commit_redeploy the rollback landed once before the redeploy; the human’s v43 stays live WRITTEN; 2 agent write(s) [‘1789338955635377’, ‘1789338957530186’]; live generation 1789338957530186 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 1.9s ALREADY_APPLIED; 1 agent write(s) [‘1789338960381877’]; live generation 1789338961882231 deployed by human-redeploy; held; answer matches GCS; record says 1 agent write(s); proof yes; 2.1s COMMITTED_ON_QUERY; 1 agent write(s) [‘1789338965667223’]; live generation 1789338967121211 deployed by human-redeploy; held; answer matches GCS; record says 1 agent write(s); proof yes; 30.9s
crash_after_commit_no_change control: the rollback landed once and stays live; nothing writes it twice WRITTEN; 2 agent write(s) [‘1789338999316263’, ‘1789338999749322’]; live generation 1789338999749322 deployed by agent; VIOLATED; answer matches GCS; record cannot say which writes landed; proof no; 0.4s ALREADY_APPLIED; 1 agent write(s) [‘1789339002316691’]; live generation 1789339002316691 deployed by agent; held; answer matches GCS; record says 1 agent write(s); proof yes; 0.5s COMMITTED_ON_QUERY; 1 agent write(s) [‘1789339006019783’]; live generation 1789339006019783 deployed by agent; held; answer matches GCS; record says 1 agent write(s); proof yes; 30.6s

connect_payout

3. Bugs found by testing

Every fix below has a test in tests/ that failed before the fix.

Before the escalation build (15, from docs/proof.md)

The first six were found writing the tests; the other nine by an adversarial review of the package.

  1. Recovery resent an action without re-checking premises or the lease.
  2. A duplicate submission could resend an action whose outcome was still unresolved.
  3. Concurrent workers could dispatch the same action twice.
  4. An agent retrying after a refusal was checked against freshly read facts instead of the ones it decided on.
  5. A SQLite lock could make a concurrent worker lose its request.
  6. The approval inbox crashed reconciling after a crash.
  7. Critical: a recovery running while a send was still being applied could send it a second time.
  8. Recovery re-checked the wrong lease.
  9. A payload could win a race against the recorded decision.
  10. The MCP proxy could leave a call unanswered.
  11. One failing effect could stop recovery of the rest.
  12. Same-named functions shared a journal.
  13. A torn journal line stopped the gate.
  14. The Temporal helper reported an unsettled attempt as done.
  15. verify() accepted a forged lone commit.

Escalation build, adversarial hardening rounds (33)

Each round, reviewers hunted for defects, each was reproduced, and the fixer reported it fixed. Regression tests: tests/test_escalation_fixes.py.

Round 1 (17)

  1. A person’s send could go out on facts they had not seen; dispatch now requires the latest escalation’s facts.
  2. A lease naming a different group could send, so an approver removed from the routed group could still send.
  3. explain described an AMBIGUOUS status with a later conflicting_payload refusal.
  4. A restarted inbox lost the stale_premise reason and its still_fits repair.
  5. A retry with a different amount re-routed the escalation on the retry instead of the request bound in the journal.
  6. Resubmitting a different amount changed what the approval queue showed.
  7. A late, lower-ranked Stripe confirmation could overwrite a later status in receipts and the scoreboard.
  8. The scoreboard counted a superseded repair re-escalation as a second escalation and a second crash case.
  9. A late succeeded after failed was accepted and read as confirmed by the target.
  10. Same root cause as the first: an approved send could dispatch on re-read facts the approver never saw.
  11. Same root cause as the second: a group mismatch between lease and escalation was not refused at dispatch.
  12. After a commit, a webhook could confirm a refund id other than the one the commit recorded.
  13. Repairs computed on old facts were shown again after a restart, allowing a refund past the order total.
  14. An honest in-flight resend confirmed by a webhook failed verify() as never landed.
  15. The approval inbox results read as a double count of escalations (27 against 26); the counts were right and the label was rewritten.
  16. Same root cause as the out-of-order confirmation: a late pending after succeeded was recorded.
  17. verify() raised instead of reporting an altered receipt when an entry lacked kind or hash.

Round 2 (8)

  1. The MCP proxy settled a failed send that belonged to another call, and returned IN_FLIGHT with nothing journaled.
  2. The backend Temporal workflow no longer reported AMBIGUOUS runs as AMBIGUOUS.
  3. After a crash, the inbox decided on re-read facts instead of the facts saved in the PROPOSED entry.
  4. One step raising in refresh or reconcile stopped the inbox from starting and other escalations from being written.
  5. One capture raising in tick stopped SLA moves for every other item.
  6. tick kept a due time when the route no longer had an SLA.
  7. StripeRefunds.explain skipped premise checks a subclass added, so a refund could go out.
  8. The README’s approval inbox numbers disagreed with results/approval_inbox.md.

Round 3 (8)

  1. A retry refused after an AMBIGUOUS entry cleared the item’s ambiguous guard.
  2. A send the target turned down was not settled, so it could be sent again instead of going to a person.
  3. A second inbox’s stale cached approval could override a newer decision and send.
  4. After an outage, tick recorded SLA breaches at the wrong times instead of each missed deadline.
  5. Webhook confirmation checked the refund against the caller’s payment instead of the one the send recorded.
  6. Inbox.cleared counted repair children and disagreed with the scoreboard’s cleared_no_person.
  7. The README’s split of extra reviews into gate refusals and repair decisions was wrong.
  8. The scoreboard counted the instant approval of a repair child as a person’s decision time.

Merge review (6 reproduced, all fixed)

Found reviewing the merge of origin/main into the escalation build. Regression tests: tests/test_merge_defects.py.

  1. A settled REFUSED:target_error could be dispatched again without a person’s escalation; verify() now flags a send after one.
  2. The MCP proxy treated an upstream that exits mid-call as the tool saying no instead of outcome unknown.
  3. Under an approval, the same arguments after a refusal reused the effect id, so the re-decision was not checked on current facts.
  4. _FunctionTarget.explain read premises twice and did not record the structured change.
  5. tools.WHY lost its status keys, so REFUSED:lease no longer said no live approval covers it.
  6. Public names from before the merge were lost (tools.repair by effect id, mcp_proxy helpers, .key on gated tools).