01The idea in 60 seconds

Software that can
unmake itself.

What if a plugin could vanish without a restart—and every component that relied on it adjusted safely? This paper turns that wish into a programming model with two explicit guarantees.

DEPENDspatial
temporalUNDO
component Aprovidesdatabase
component Bneedsdatabase
A removed
1B deactivates before A is undone
2A’s changes are fully reversed
The whole paper, compressed into one event.

02Two orthogonal guarantees

A safe goodbye
has two jobs.

Think of a live system as a shared room. Removing one guest means cleaning up their mess—and warning everyone who borrowed their things. §1 · pp. 4–6

T

Along time

Temporal composability

Every context mutation returns an inverse. The runtime records those inverses, then composes them into the component’s teardown.

// creation and recovery stay local
ctx.effect(() => {
  bus.on("save", handler);
  return () => bus.off("save", handler);
});
Remove A ≈ A contributed nothing
S

Across components

Spatial composability

Dependencies are explicit specifications. A component starts only while they are satisfied, and reacts when the provider identity changes.

// runs only with a committed database
const reports = {
  inject: ["database"],
  apply: (ctx) => serve(ctx.database),
};
Need disappears → dependent reacts
THE PAPER IN ONE LINE

Remember how to undo every mutation, remember who supplied every dependency, and continuously reconcile the two.

03Why ordinary plugins fall short

Restarting the world
is a granularity bug.

Processes already give coarse temporal cleanup; orchestrators give coarse spatial wiring. But restarting a process discards caches, connections, and in-flight work just to remove one component. §1.2 · pp. 4–6

87/100

Top VS Code extensions contain executable code.

The paper’s June 9, 2026 marketplace snapshot says these cannot be individually unloaded from the shared extension host; a restart is required.

p. 5 · paper snapshot, not a live metric
Todaydisable plugin→ restart host → rebuild local state → interrupt work
The proposalretire one fiber→ drain dependents → undo its effects → keep serving

Self-evolving agent harnesses make this more urgent: frequent synthesized changes turn every full restart into cumulative downtime, and a bad update may disable the process needed to recover it.

04The runtime mental model

Provider first.
Consumer out first.

The clever move is to separate “stop advertising this service” from “actually destroy it.” Existing consumers retain a committed view long enough to tear down safely. §4.3.1 · pp. 34–35

Interactive trace

Retire the database without breaking its users.

1/ 7
APAPIrequires repositoryactive
RERepositoryrequires databaseactive
DADatabaseprovides databaseactive
quiet

Everything is steady

DB provides database. Repository is committed to DB; API is committed to Repository.

New discovery
DB visible
Committed consumers
DB readable
Why the two-phase exit matters

A database can be invisible to new consumers and still remain readable to the repository that is currently returning its checked-out connections.

05Three objects to keep in your head

Context, component,
fiber.

The formalism gets much easier once the nouns are fixed. A component is a reusable recipe. A fiber is one runtime instance. The context is the entire mediated world they act through. §4.1 · pp. 28–30

Γ

Context

The world the runtime can see

Shared services, component-owned tables, lifecycle registry, ambient state, and accumulated inverses. If a mutation bypasses this boundary, the theorem cannot speak about it.

C

Component

A declarative interface plus activation

(d, p, e): required keys, keys it may provide, and a witnessed effect iterator that installs its contribution.

ƒ

Fiber

One component instance

It owns an identity, parent, service table, retirement flag, committed provider view, lifecycle state, and undo accumulator.

A coder’s approximation
type Component<W> = {
  requires: Set<Key>;       // d — what it reads
  provides: Set<Key>;       // p — what it may write
  activate: Iterator<W>;    // e — effects + inverses
};

type Fiber<W> = Component<W> & {
  id: FiberId;
  parent: FiberId | "root";
  ownTable: Registry;
  retired: boolean;
  lifecycle: Lifecycle<W>;
};

The vital comparison

Committed viewWho I started withdatabase → fiber#17
Target viewWho I should use nowdatabase → fiber#24

If provider identity—not merely value—changes, the consumer unloads and may reload against the new provider.

06Programmer layer · effects

Write the action and
its undo together.

The runtime cannot infer how to reverse an arbitrary action. Its structural win is making the inverse local, composing it automatically, and ensuring it runs on every exit path. §3.1, §5.1.1 · pp. 9–17, 56–57

Separated concern

Conventional plugin lifecycle
let timer: ReturnType<typeof setInterval>;

export function activate() {
  timer = setInterval(refresh, 30_000);
  bus.on("message", onMessage);
}

export function deactivate() {
  clearInterval(timer);
  bus.off("message", onMessage);
  // Did we remember every effect?
}

Local inverse

Context-mediated lifecycle
export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(refresh, 30_000);
    return () => clearInterval(timer);
  });

  ctx.effect(() => {
    bus.on("message", onMessage);
    return () => bus.off("message", onMessage);
  });
}

Multi-step activation

A generator is a rollback tape.

Each yield is a safe boundary: the forward step has landed, its inverse is now known, and the runtime can stop before the next step if dependencies changed.

  • 1Inverses are prepended, so teardown is LIFO.
  • 2If step 3 fails, steps 2 and 1 are still recovered.
  • 3If an async step is already in flight, it lands first, then is undone.
  • !The runtime assumes the returned inverse is truthful; it does not prove it.
Cordis 4 RC · faithful executable shape
ctx.effect(async function* () {
  const socket = await connect();
  yield () => socket.close();

  await socket.subscribe("prices");
  yield () => socket.unsubscribe("prices");

  const timer = setInterval(() => socket.ping(), 5_000);
  yield () => clearInterval(timer);
});

// unload: clear timer → unsubscribe → close

The paper’s pseudocode and the current 4.0 RC API differ in a few names. This example follows the current public surface; that API is still marked unstable.

07Programmer layer · coeffects

Dependency injection,
but alive.

An effect says what a component does to its environment. A coeffect says what the environment must provide to the component. Here, dependency presence is checked again after every context change. §3.2 · pp. 17–22

A typed partial service table
interface Services {
  database: Database;
  logger: Logger;
  clock: Clock;
}

type Key = keyof Services;
type Value<K extends Key> = Services[K];

type Registry = {
  [K in Key]?: Value<K>
};

// A TypeScript approximation of V : K → Type

notifyd(before, after)

Every context transition gets classified.

unsatisfied → satisfiedactivating
satisfied → unsatisfieddeactivating
status unchangedneutral

Satisfaction means every declared key is present. A changed value with the same provider identity can remain neutral by design.

A real provider / consumer pair

The consumer can finish using the provider it committed to.

When the database is disposed, Report stops before the database closes. Its teardown may still call ctx.database.flush() because context access resolves through the committed view.

Current Cordis naming

The paper simplifies provision to ctx.set. Cordis 4 RC uses ctx.provide to establish a service and ctx.set to change a value.

TypeScript · shortened but faithful
export const DatabasePlugin = {
  name: "database-provider",
  apply(ctx: Context) {
    return ctx.effect(async function* () {
      const db = await openDatabase();
      yield () => db.close();
      yield ctx.provide("database", db);
    });
  },
};

export const ReportPlugin = {
  inject: ["database"],
  apply(ctx: Context) {
    const timer = setInterval(runReport, 60_000);
    return async () => {
      clearInterval(timer);
      await ctx.database.flush();
    };
  },
};

Isolation changes what resolves

key → realm → value
const tenantA = root.isolate("database");
const tenantB = root.isolate("database");

tenantA.plugin(DatabasePlugin, { url: "db://a" });
tenantB.plugin(DatabasePlugin, { url: "db://b" });

// Same logical key, different realm, different value.
tenantA.plugin(ReportPlugin); // receives A
tenantB.plugin(ReportPlugin); // receives B

Interception changes how it may be used

provider-enforced metadata
const constrained = root.intercept("filesystem", {
  root: "/srv/community-data",
  writable: false,
});

constrained.plugin(ThirdPartyPlugin);

// The filesystem provider must enforce this policy.
// Interception is mediation, not a security sandbox.
logical keydatabase
isolation realmtenant-Aswap only this hop
stored valuedb://tenant-a

08The theory bridge

The notation is denser
than the machinery.

The paper assumes type theory and category theory. Its core, however, mostly needs functions, composition, a monoid of state transformations, typed maps, recursive types, and observational equivalence. §2 · pp. 7–8

PaperRead it asCode intuition
Γthe context typethe type of the whole mediated world
γ, δparticular context statestwo snapshots of that world
f : Γ → Γa state transformation(world) => changedWorld
g ∘ fcompositionrun f, then g
idΓidentitya no-op transformation
A × Bproduct type[A, B] or { a, b }
A ⇀ Bpartial functionmay throw or return undefined
Maybe(A)optional valueA | undefined
Either(E, A)error or valueResult<A, E>
μX. F(X)recursive typean interface containing its own shape
σ ⊧ dcontext satisfies needsevery required key is present
observational equivalenceno allowed operation can tell apart
1

Category basics

Objects, arrows, composition

For this paper, think of types as objects and functions as arrows. Arrows compose when outputs and inputs match. Associativity lets us ignore parentheses; identity arrows do nothing.

Γ — f → Γ — g → Γ
g ∘ f means f first, then g
2

The monoid actually used

Endomorphisms form an algebra

All arrows from Γ back to Γ are closed under composition, composition is associative, and idΓ is the unit. That is a monoid—the algebra behind sequencing effects and their inverses.

(Γ → Γ, ∘, idΓ)
3

Monads and effects

A context for sequencing extra behavior

A monad wraps values with computational structure: Maybe<A> for failure, State<S,A> for state, IO<A> for interaction. The paper reviews this lineage, then reifies effects as runtime state transformations instead of adding static annotations.

A → T(A) then flatten T(T(A)) → T(A)
4

Comonads and coeffects

The dual question: what surrounds the value?

If effects describe what computation produces, coeffects describe context it consumes. The Environment comonad pairs a value with environment E × A; Cordis turns that idea into a live service table and dependency specification.

D(A) → A extract
D(A) → D(D(A)) duplicate context
5

Type families

The key determines the value type

V : K → Type means each dependency key maps to its own type: database maps to Database, clock to Clock. A dependent partial map keeps those associations type-correct.

Σ = (k : K) ⇀ V(k)
6

Witnesses and refinements

A value paired with its proof obligation

A witnessed effect is not just code returning an inverse; its type carries the condition that the inverse recovers the state where it was created. The implementation trusts authors to meet that condition.

e(γ) = (δ, g) with g(δ) ≃ γ
Useful correction

A returned g is a one-sided, state-local inverse. The paper requires g(f(γ)) ≃ γ where the effect ran—not necessarily f(g(γ)) = γ, and not necessarily a globally invertible computation.

09Formal layer · revertible effects

An undo stack becomes
a compositional law.

The effect context pairs the current state with an accumulator. Each new inverse is composed onto that accumulator in the opposite order of its forward action. §3.1 · pp. 9–17

1

Effect context

∂Γ = Γ × (Γ → Γ)

(γ, φ) stores the current world and the recovery program so far.

2

Track

track(f,g)(γ,φ)
= (f(γ), φ ∘ g)

Apply the forward map; append its inverse to recovery.

3

Recover

recover(γ,φ)
= (φ(γ), idΓ)

Run the accumulated inverse and reset the tape.

forwardopen socketsubscribestart timer
↓ recovery flips the order ↓
inverseclose socketunsubscribeclear timer
Theorem 7

One tracked witnessed effect does not change where recovery leads.

Theorem 16

A sequence can always be reversed in LIFO order when each inverse meets the state its own action produced.

Corollary 21

Any removal order works only when effects are pairwise independent.

09BThe independence test

When can A be undone
while B stays?

Equal final states are not enough. Every forward and inverse map must commute across components, and foreign work must not change which inverse or continuation the other effect returns. Def. 19 · p. 16

Independent

A adds /a. B adds /b.

After A, B{ /a: A, /b: B }
Then undo AUndo A → { /b: B }

Each operation owns a distinct key. Forward maps and removals commute; one receipt does not change when the other route exists.

Order test · TypeScript
map.set("/a", handlerA);
const undoA = () => map.delete("/a");
map.set("/b", handlerB);
undoA(); // B remains

10The context paradigm

Effects and needs meet
inside one world.

The recursive context folds effect tracking and the coeffect table into one self-similar type. Parent contexts own the disposers of child contexts, so teardown composes hierarchically. §3.3 · pp. 22–27

parent Γ
child Γ
stateΓ
accumulatorΓ → Γ
coeffectsΣ
Γ∞ = μΓ. Γ × (Γ → Γ) × Σ
  • 1Every mediated interaction is attributable to a context—and therefore to a fiber.
  • 2Unloading a parent runs the child disposers it accumulated.
  • 3Shared state can be represented as typed keys, not only “services.”

Why exact equality is too strong

Recovery means indistinguishable, not bit-for-bit identical.

free need not restore the heap’s physical layout; closing and reopening a resource may produce a fresh handle. Two states count as equal when no operation exposed by their coeffects can distinguish them.

physical state Ahandle #41
physical state Bhandle #92

If handles cannot be compared by any published operation, the renaming is observationally irrelevant.

The discipline that makes the proofs go through

  1. Mediate shared state.A location no context key represents lies outside the guarantee.
  2. Make same-key interfaces commutative.Identity-keyed listener or route registration is the friendly case.
  3. Declare genuine ordering.If operations do not commute, express a provider/consumer relationship so the lifecycle orders them.

11The calculus

A small state machine
for a messy real world.

The authors begin with atomic load/unload, then add four realities: dependent-first withdrawal, multi-step activation, asynchronous inertia, and failure. Ten rules define every legal transition. §4 · pp. 28–53

Inactiveclean or failed
L-Begin
Reloadingiterator + undo so far
L-Finish
Activecommitted view
L-Leave
Unloadingwait until not relied upon
L-Unload
L-Divert / L-Raise
O-*External orchestration 3 rules

Insert creates an inactive fiber with a fresh name. Retire irreversibly requests removal. Remove deletes only a retired, inactive, childless entry.

LoadActivation 3 rules

Begin commits a target provider view. Iter lands one effect and records its inverse. Finish lands the last iteration and exposes the fiber as Active.

ExitEarly activation exit 2 rules

Divert detects a stale target and routes partial work to teardown. Raise records an error only after accumulated work is recovered.

UnloadDeactivation 2 rules

Leave stops provider discovery without destroying the service. Unload runs the accumulator only after no committed consumer relies on the fiber.

A target changes during slow activation

t₀launch connect()

fiber is Reloading against DB #17

t₁DB #17 is replaced

target now names DB #24; promise is already in flight

t₂connect() lands

capture its close() inverse; never expose the stale fiber as Active

t₃route to Unloading

run close(), become Inactive, then reload against DB #24

The proof atlas

What is guaranteed—and the price of each guarantee.

THEOREM 59pp. 42–43

Preservation

Guarantee
Every legal step keeps the registry well formed: parents exist, provisions stay unambiguous, and committed provider pointers do not dangle.
Assumes
A well-formed start, confined effects, and the guarded lifecycle rules.
Not promised
It does not promise activation succeeds, dependencies are acyclic, or service values are correct.
THEOREM 61pp. 44–45

Recovery exactness

Guarantee
Undoing one fiber removes its contribution while preserving all independent work that other fibers interleaved around it.
Assumes
Witnessed inverses and pairwise independence of every component iterator.
Not promised
Noncommutative singleton overwrites, positional mutation, or untracked shared state.
THEOREM 63pp. 45–46

Dependency ordering

Guarantee
Providers start before consumers, finish after consumers, and their committed values remain stable throughout consumer teardown.
Assumes
Committed provider views plus the relied-upon withdrawal guard.
Not promised
A provider is not globally discoverable after Leave; only existing committed consumers retain access.
THEOREM 64pp. 46–47

Resolution coherence

Guarantee
A multi-step activation sees one provider snapshot. If the target changes, it diverts and rolls back instead of becoming active on mixed dependencies.
Assumes
Target checks at iteration boundaries; exact rollback additionally relies on independence.
Not promised
An already-launched asynchronous step cannot be cancelled; it lands, is recorded, then undone.
THEOREM 66pp. 47–48

Progress

Guarantee
Reconciliation cannot deadlock and, after orchestration stops, reaches a quiet state in finitely many lifecycle steps.
Assumes
An acyclic provider graph, bounded iterators, finitely many fibers, and lifecycle-only continuation.
Not promised
A missing dependency may remain inactive; failures are quiet; real promises still need to settle.
THEOREM 73pp. 52–53

Confluence

Guarantee
The same orchestration commands, under different valid schedules, converge to the same quiet state as a dependency-ordered from-scratch assembly.
Assumes
Independence, total provision, acyclicity, no failures, termination, and equivalence up to fresh-name renaming.
Not promised
Intermediate logs, messages, requests, and other emissions may differ. This is an endpoint theorem, not a history theorem.
Schedule ADB beginDB finishMetrics beginAPI beginMetrics finishAPI finish
independent steps transpose
Canonical formDB episodeMetrics episodeAPI episode
same orchestration
Schedule BMetrics beginDB beginMetrics finishDB finishAPI beginAPI finish

12The implementation

Cordis turns the calculus
into a meta-framework.

Cordis fixes the semantics of dynamic composition, not an application domain. A core runtime, a declarative loader, and application frameworks form three layers. §5 · pp. 54–67

3Application frameworks

Koishi server, Koishi web console, agent harnesses, plugin hosts

2Component loader

desired-state config, reconciliation, grouping, inclusion, HMR

1Core library

contexts, effect tracking, coeffect resolution, fibers, lifecycle

Effect tracking

One mutation gateway

Every context-mediated mutation reduces to ctx.effect. A disposer is prepended into the current fiber’s accumulator and, recursively, into its parent’s lifetime.

Context access

Committed, not merely live

A proxy resolves ctx.database through the accessing fiber’s committed provider map. Undeclared or inactive access throws instead of silently returning a stale global value.

Reactive notification

Refresh only real dependents

A binding change refreshes fibers that declared the key and resolve the same isolation realm. Refresh is idempotent; provider identity drives lifecycle.

Desired-state loader

Configuration says what should exist; reconciliation chooses the smallest operation.

Stable entry IDs let the loader diff a persistent tree. Missing requirements simply leave a fiber pending, so modules may load concurrently without hand-authored topological ordering.

id / urlrebuild entry and fiber
isolatereassign managed realms
interceptupdate metadata in place
configcomponent-specific keyed diff
disabledunload or reload
Paper schema · Definition 74
- id: storage
  url: ./plugins/postgres.ts
  isolate: { database: production }
  intercept: { database: { readonly: false } }
  config: { url: $DATABASE_URL }
  disabled: false

- id: reports
  url: "@cordisjs/group"
  config:
    - id: daily
      url: ./plugins/daily-report.ts
      config: { hour: 6 }

The paper uses url. Cordis 4 RC currently uses name for the module locator; the two should not be mixed as exact API documentation.

Hot module replacement

The fiber is already the acceptance boundary.

No component-specific hot-accept code is needed: dispose the old fiber, invalidate its accepted module closure, import the replacement, then instantiate it inside a clean lifecycle boundary.

  1. 1
    Classify module graph

    Propagate accepted and declined status; unresolved import cycles default to declined.

  2. 2
    Find stale entries

    Traverse imports up to declined boundaries and expand accepted cache invalidation coherently.

  3. 3
    Swap transactionally

    Back up caches, dispose stale fibers, import new code. On import error, restore caches and reconstruct the old fibers.

4,000+

Case study · Koishi

A four-year chatbot plugin ecosystem.

The paper reports more than 4,000 community plugins. Koishi’s server and browser console are separate Cordis applications, evidence that the composition model is not tied to one domain.

Evidence, not a benchmark

Koishi currently uses Cordis v3 while the paper specifies v4. The study is one ecosystem, observational, and reports no controlled overhead or productivity comparison.

§5.3 · pp. 66–67

13Where rollback ends

A closed socket is not
an unsent message.

The guarantee applies only inside the system boundary: locations the runtime exclusively mediates and can restore up to the chosen equivalence. Acquisitions can often be reversed; emissions usually cannot. §6.1 · pp. 67–68

INSIDE THE BOUNDARY

Acquisitions

  • mallocfree
  • open connectionclose
  • register listenerunregister
  • start childretire
context-mediated
ownership
OUTSIDE THE BOUNDARY

Emissions

  • send emailalready read
  • charge cardneeds refund
  • publish messagealready observed
  • write shared fileother writer exists
The disposer reverses ownership, not history
ctx.effect(async () => {
  const socket = await connect();  // reversible acquisition
  await socket.send(payload);      // irreversible emission

  return () => socket.close();
  // closes the handle; cannot unsend payload
});

Two honest strategies

Withhold until commit. Buffer an outbox and emit only after the transition is accepted.

Compensate. Register a refund, delete, or counter-message under a coarser domain equivalence. The paper is explicit that its metatheory would need to be reproved for this weaker notion.

14Useful extensions

What this model unlocks—
and what it does not.

The discussion explores service multiplexing, capability-like access, language and OS co-design, mutual-dependency refactoring, and versioned dependency typing. These are directions around the core, not all solved by it. §6 · pp. 68–74

01

Stable broker

Consumers inject one long-lived broker while backends register and drain behind it. Provider churn no longer changes the consumer’s provider identity—useful for load balancing and rolling updates.

02

Capability-shaped access

inject is an authority request and the proxy is a mediation point. But ordinary TypeScript can bypass it, so untrusted code still needs a sandboxed runtime or process.

03

Cycle decomposition

A dependency cycle stays inactive rather than deadlocking unpredictably. Split mutually dependent services into independent cores plus integration components—at a possible O(n²) authoring cost.

04

Language / OS support

A language could make contexts implicit and unforgeable; an OS could grant only declared coeffects and track file descriptors or memory per component. The current library cannot enforce either.

15The assumption ledger

The strongest reading
would be the wrong reading.

The paper’s guarantees are conditional and carefully scoped. This checklist is the shortest route to applying the model without overselling it.

01

Inverse truthfulness

The runtime records a closure; it cannot verify that the closure really restores the effect.

02

Context mediation

Untracked globals, raw Node APIs, and shared external state sit outside the formal boundary.

03

Pairwise independence

Arbitrary component removal needs commuting forward maps, inverses, and stable cleanup receipts—not just LIFO.

04

Acyclic dependencies

Progress assumes the provider relation has no cycle. Cycles remain inactive and should be reported.

05

Finite, bounded work

Termination excludes infinite self-registration and assumes each activation has bounded iterator length.

06

No failures for confluence

A schedule may fail where another succeeds. Failed fibers contribute no state, but their outcomes differ.

07

Total provision

Confluence assumes every successful provider installs every key it declared it may provide.

08

Endpoint, not trace

The final state can be canonical while logs, network messages, emails, or metrics along the way differ.

09

Local state on HMR

Cordis restarts the component from a clean slate. State survives only if externalized into a longer-lived dependency.

10

Security boundary

Isolation and interception shape resolution and policy metadata; they do not sandbox malicious code.

11

API maturity

Cordis 4.0.0 RC is marked unstable. The paper and current repository differ in some loader/API names.

12

Evidence quality

The Koishi case shows adoption and existence, not causal productivity gains or measured runtime overhead.

17Read next

A theory path for
working programmers.

These references are selected from the paper’s bibliography. Start with Pierce and Pretnar, then follow coeffects and categorical duality as far as your curiosity takes you. References · pp. 80–88

Types[21]

Types and Programming Languages

Benjamin C. Pierce

A clear route into judgments, products, sums, recursive types, and operational semantics.

Effects[16]

Notions of Computation and Monads

Eugenio Moggi

The categorical foundation the preliminaries use for computational effects.

Effects[26]

An Introduction to Algebraic Effects and Handlers

Matija Pretnar

An approachable bridge from exceptions and coroutines to effect handlers.

Coeffects[18]

Coeffects: Unified Static Analysis of Context-Dependence

Petricek, Orchard & Mycroft

The cleanest precursor for thinking about what a computation needs.

Duality[32]

Comonadic Notions of Computation

Uustalu & Vene

The categorical dual behind environment- and context-dependent computation.

Runtime[43]

Yield: Mainstream Delimited Continuations

James & Sabry

Why generators provide exactly the step boundaries used by effect iterators.

Systems[49]

Sagas

Garcia-Molina & Salem

The classic vocabulary for compensating actions when exact external rollback is impossible.

Services[50]

OSGi Core Release 8

OSGi Alliance

A mature service-availability model and the closest implementation precedent on the spatial side.

18The final mental model

Acquire with a receipt.
Depend by declaration.
Leave no residue.

When building a component: route shared mutations through the context, return a local inverse for every acquisition, declare every dependency, and design same-key operations to commute.

When building the runtime: remember provider identity, split Leave from Unload, let in-flight work land before compensating it, and never destroy a provider before committed consumers finish.

When making claims: name the boundary, independence assumptions, dependency DAG, failure model, and the emissions that no rollback can erase.

Back to the one-minute version