State Fork (ι)

ι is the protocol for deliberate divergence. When θ consensus cannot agree on a single continuation, rather than halting the whole network, ι creates a sibling branch with a new identity. Siblings proceed in isolation until a future epoch reconciles them.

Phase 0 reality: forks are specified but not activated. The single-writer runtime cannot diverge. The schema supports ι so a later phase can turn it on without a migration.

Authoritative spec: ../../../spec/s07-fork-protocol.md.

Fork identity

A fork has a deterministic id derived from its lineage:

fork_id = SHA-256(parent_fork_id || divergence_event_id || rule_version_hash || reason)

The four inputs:

  • parent_fork_id — the fork this one split from. The root fork has a zero parent.
  • divergence_event_id — the specific event at which θ could not agree.
  • rule_version_hash — κ’s rule set at the moment of divergence.
  • reason — one of the five ForkReason values.

Hashing over all four means two forks with the same parent and event but different rule sets are distinguishable, and a fork cannot be forged without knowing the exact divergence event.

Worked fork_id computation

Suppose the root fork is 0x00…00 (32 zero bytes), the divergence event id is evt:0x8a3c…, the active rule-version hash is rv:0xb1e2…, and the reason code is CONSENSUS_SPLIT. Canonical serialization concatenates ASCII-hex encodings with ||:

input  = "0x0000…00" || "evt:0x8a3c…" || "rv:0xb1e2…" || "CONSENSUS_SPLIT"
digest = SHA-256(input)
       = 0x4f92d3a7e1b0c82f…   ← 64-hex-char output, truncated here for display
fork_id = "fork:0x4f92d3a7e1b0c82f…"

Every input is fixed-length (hashes are 32 bytes; reasons are enum strings from a closed set of 5). No length-prefix ambiguity is possible.

The five fork reasons

Reason Meaning
CONSENSUS_SPLIT θ could not reach quorum; the vote genuinely split
PARTITION_RECOVERY A network partition ended; both sides had diverged
RULE_UPGRADE π governance approved a breaking rule change; legacy and upgraded chains run in parallel
EMERGENCY A critical vulnerability required immediate divergence; governance seals after the fact
EXPERIMENTAL An opt-in sandbox branch; governance bounded lifetime

Every fork records its reason; a fork without a valid reason is rejected at creation.

Fork-trigger examples (one per reason)

Reason Example trigger
CONSENSUS_SPLIT θ round 42 has votes {A→root_X, B→root_X, C→root_Y, D→root_Y}: 2 < quorum = 3 on both sides. ι forks X-branch and Y-branch.
PARTITION_RECOVERY Network split at t=0; branch A and branch B both advance 100 rounds independently; at t=T, reunion happens — neither side can adopt the other without a fork.
RULE_UPGRADE π enacts a breaking rule_version_hash change. Legacy chain continues under old rv; upgraded chain runs parallel under new rv until obsolete clients cut over.
INVARIANT_VIOLATION An arbiter signs a block that violates AX-02 (derived reputation); detected after the fact. Fork isolates the contaminated branch.
CONSTITUTIONAL_VIOLATION A rule change passed π but retroactive analysis shows it lowered a reputation decay below the AX-03 floor. Fork marks pre-violation history clean.
VOLUNTARY_EXIT A constituent group exercises AX-06 right-to-exit; carries own state forward under a new fork.

A fork without a valid reason is rejected at creation.

Fork status

A fork moves through four states:

  • ACTIVE — accepting new events, being voted on by its own arbiter set.
  • RECONCILING — merge attempt underway; see merge protocol below.
  • ABSORBED — merged back into a sibling; tasks complete, new events rejected.
  • ABANDONED — frozen; no merge attempted. Retained for audit only.

Isolation modes

Forks differ in how much of the world they share with siblings:

  1. Full isolation — separate DB, separate arbiter set, separate skill registry. No cross-traffic until reconciliation.
  2. Read-sharing — shared read-only view of the pre-divergence state; writes are independent.
  3. Shared identity — agent Soul Vectors and reputation carry across the fork boundary but actions are recorded independently.

Full isolation is the safest and the slowest to reconcile. Shared identity is cheaper to run but harder to audit because a single agent acts in both branches.

Isolation mode × capability matrix

Mode Read parent state Write to parent state
Full isolation No No
Read-sharing Yes No
Shared identity Yes (identity only) No (writes remain local to each branch)

No mode permits writing to parent state — that would violate AX-01 (append-only history). All merges happen via the explicit merge protocol, never by one branch reaching into another’s storage.

Checkpointing

A fork checkpoints its state periodically so a reconciliation attempt has a well-defined common ancestor. A checkpoint is valid when 7 of 10 designated arbiters sign it. The 7-of-10 parameter is the Phase 0 spec default; π governance can revise it in later phases.

Checkpoint frequency is not fixed; it is triggered whenever the fork’s Merkle root accumulates a configured event count (default: 256 events) or a time bound (default: 15 minutes) is reached.

Merge protocol

Two forks reconcile by producing a union state:

  1. Common ancestor identified — the latest checkpoint signed by both branches’ arbiter sets.
  2. Divergence set computed — the events in each branch since the common ancestor.
  3. Conflict classification — events are (a) compatible (apply cleanly to a union), (b) conflicting (same resource, different outcome), or (c) mutually-exclusive-by-design (both cannot be true).
  4. Resolution — compatible events merge; conflicts require a π governance decision; mutually-exclusive events force one branch to become ABSORBED and the other to survive.
  5. Merged root — η produces a new Merkle root over the reconciled state; θ reaches quorum on it; the merged fork replaces the two siblings.

A merge failure leaves both forks ACTIVE and emits a governance event requesting manual arbitration.

Merge pseudocode (5 steps)

fn merge(fork_source, fork_target):
    # Step 1: common ancestor
    ancestor = latest_state_root_shared_by(fork_source, fork_target)
    if ancestor is None:
        return MERGE_FAILED("no common ancestor")

    # Step 2: event diff
    events_src = events_since(fork_source, ancestor)
    events_tgt = events_since(fork_target, ancestor)
    to_apply   = symmetric_difference(events_src, events_tgt)

    # Step 3: conflict resolution
    conflicts = detect_conflicts(to_apply)  # same resource, divergent outcomes
    for conflict in conflicts:
        resolved = resolve(
            conflict,
            primary   = timestamp_ordering,        # default
            secondary = reputation_weighted_vote,  # fallback
            tertiary  = t0_manual                  # last resort
        )
        to_apply.replace(conflict, resolved)

    # Step 4: compute merged fork id
    reason   = "MERGE"
    new_fid  = SHA-256(fork_source.id || fork_target.id || ancestor.root || rule_version_hash || reason)

    # Step 5: 7-of-10 threshold-sign merge proof
    proof    = threshold_sign(arbiter_set, new_fid, quorum=7, total=10)
    emit MERGE_PROOF(new_fid, proof)
    transition both forks → ABSORBED; new fork becomes ACTIVE

Conflict scenario — two forks, same parent, divergent rules

Parent at rule version rv:A. Fork A applies π-approved delta min_stake: 500 → 600. Fork B independently applies min_stake: 500 → 700. Both advance 50 rounds.

Merge attempt:

  1. Common ancestor: last checkpoint under rv:A.
  2. Event diffs: A has 50 events applied under rv:B1 (min_stake=600); B has 50 events applied under rv:B2 (min_stake=700).
  3. Conflict: same parameter (min_stake), different final value. Not a timestamp conflict — a policy conflict.
  4. Resolution routes to π (policy conflicts cannot be resolved by timestamp ordering).
  5. π options: (a) supermajority vote picks one value — absorbed fork abandons its events; (b) both forks remain ACTIVE permanently (accepted divergence) — the union is rejected; (c) T0 declares the merge infeasible.

If (a), effects downstream of min_stake on the losing fork need replay under the winning value; in practice, many are invalidated — reputation gains from commitments that required the old threshold may be rolled forward but stake-related obligations must be re-evaluated.

Shield invariants

Three rules hold across every fork boundary, independent of reason or isolation mode:

  1. Identity keys persist. An agent’s Ed25519 keypair is valid in both branches. A fork does not revoke keys.
  2. Constitutional axioms persist. AX-01 through AX-07 (see ../constitution.md) apply in every branch. A fork cannot weaken a constitutional invariant.
  3. Fork-id ancestry is append-only. The parent_fork_id field is immutable; no fork can be re-parented. The fork DAG is a tree, not a general graph.

Exit penalty formula

AX-06 guarantees a right to exit with a stake penalty capped at 10%. The exit penalty for an agent voluntarily leaving a fork:

penalty_bps = min(1000, scar_weight * 10 * log2(epochs_since_stake))
  • scar_weight — sum of λ scar weights on the agent’s record (typically 0–5 for clean participants, 5–20 for agents with multiple equivocation flags).
  • epochs_since_stake — how long the stake has been active (older stake → larger log factor).
  • Hard cap at 1000 bps = 10% (AX-06). An agent with zero scars and any stake age pays approximately 0 — exit is free for unscarred participants.

Example: scar_weight = 2, epochs_since_stake = 82 * 10 * 3 = 60 bps = 0.6%.

What ι is not

  • Not a rollback. ι creates siblings, not ancestors. A fork does not undo history; it branches from it.
  • Not a test sandbox. A test run is an ephemeral process against a test DB (see ../../../2-plugin/modes.md); a fork is a real chain with real arbiters.
  • Not cheap. Every fork doubles the auditable surface until it reconciles. Forks are a last resort, not a feature.

Interaction with governance

π may bound a fork’s lifetime (common for EXPERIMENTAL and EMERGENCY forks), may veto an attempted merge, and may force a specific resolution in step 4 of the merge protocol. See ../enforcement/governance.md.

Phase 0 posture

No fork is ever created in Phase 0. The tool surface exposes no fork_* tool. The schema carries fork columns because the column shape is load-bearing for later phases; the fork_id field is always NULL on every row. First real ι activation target: R151+ (Phase 5) per ../../../5-time/roadmap.md.

See also


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.