Merge idunn/reincarnation-c2: phase-split handover, owner deadlines, forward retry, continuity yields

Cuts c2-phase-split, c2b-owner-deadlines, c2c-post-fence-forward, c2d-continuity-yields with their Soul passes; installed on Yggdrasil 2026-10-20 00:34 UTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-20 02:36:24 +02:00
commit dbbdbf12fe
4 changed files with 4370 additions and 436 deletions

View file

@ -52,12 +52,18 @@ web app and service through Idunn v2. The live evidence is on Yggdrasil:
the admitted incarnation. The proof answers an Idunn challenge and is signed
with the credential Idunn issued to that launch. Invariant 3 changes where the
proof travels, not what it proves.
5. **Every post-fencing phase ends.** Each phase after fencing has an admitted
deadline. When a deadline expires, Idunn resolves it: stateless targets abort
to the incumbent. Stateful targets get an explicit terminal record with a
named recovery. A granted lease that the process does not pick up within its
window is a named failure with an Idunn-owned recovery. It is never a silent
wait. Resumable-forever is gone.
5. **Every post-fencing phase has a deadline and is never a silent wait.** A
deployment's stateless phase that passes its deadline aborts to its
incumbent. A continuity past its deadline is left to the operator
(operator-required incident) and retries its phase forward on a capped
backoff, because aborting it would stop the only running copy of the admitted
body (ruling post-fence-forward-retry), until a deployment that the
deployment brake has released awaits the handover of its target and the
continuity's route proof has not succeeded: then it yields through its own
post-fencing abort, and the deployment fences what is admitted (rulings
continuity-yields-to-deploy, c2d-yield-only-unproven-to-movable). A
continuity that has proven its route is serving and never yields. A
stateful deployment is left to the operator.
6. **The admitted receipt describes the running incarnation.** Continuity
commits update the admitted generation atomically, so the rollback path never
meets drift between the receipt and the projection. Rollback demotes the exact

File diff suppressed because it is too large Load diff

View file

@ -475,6 +475,13 @@ pub const DEFAULT_AWAITING_READY_DEADLINE_SECONDS: u32 = 300;
pub const DEFAULT_ROUTING_DEADLINE_SECONDS: u32 = 120;
pub const DEFAULT_COMMITTING_DEADLINE_SECONDS: u32 = 120;
/// Idunn's own deadlines, not binding-declared: a new `PhaseDeadlines` field
/// would change every v3 plan id (`recomputed_plan_id` re-encodes the plan).
/// Warming is timed for every transaction, owner or not.
pub const WARMING_DEADLINE_SECONDS: u32 = 600;
/// A pre-fencing abort is timed from its beginning.
pub const PRE_FENCING_ABORT_DEADLINE_SECONDS: u32 = 300;
/// The resolved per-phase deadline durations a plan carries. Entering a
/// post-fencing phase stamps its deadline from them, and the deadline
/// resolver ends the phase once it passes.

View file

@ -32,6 +32,9 @@ pub(crate) const INCIDENT_RETENTION_MILLIS: u64 = 7 * 24 * 60 * 60 * 1000;
#[serde(rename_all = "kebab-case")]
pub(crate) enum IncidentCondition {
ContinuityExhausted,
/// A transaction past its deadline in a state Idunn will not end by
/// itself. Subject: the transaction id.
OperatorRequired,
}
impl IncidentCondition {
@ -40,6 +43,7 @@ impl IncidentCondition {
pub(crate) const fn name(self) -> &'static str {
match self {
Self::ContinuityExhausted => "continuity-exhausted",
Self::OperatorRequired => "operator-required",
}
}
@ -49,6 +53,7 @@ impl IncidentCondition {
pub(crate) const fn ends_with_admission(self) -> bool {
match self {
Self::ContinuityExhausted => true,
Self::OperatorRequired => false,
}
}
}
@ -271,6 +276,17 @@ impl IncidentStore {
Ok(true)
}
/// The subjects of every open incident of `condition`.
pub(crate) fn open_subjects(&self, condition: IncidentCondition) -> Result<Vec<String>> {
Ok(self
.snapshot()?
.records
.into_iter()
.filter(|record| record.is_open() && record.condition == condition)
.map(|record| record.subject)
.collect())
}
/// Close the open incident for `(condition, subject)`, if there is one.
/// `Ok(true)` means this call wrote the closure.
pub(crate) fn close(
@ -468,11 +484,12 @@ pub(crate) mod tests {
);
// The wire spelling readers match on is the spelling the key and
// status use.
let payload = rmp_serde::to_vec(&IncidentCondition::ContinuityExhausted).unwrap();
assert_eq!(
rmp_serde::from_slice::<String>(&payload).unwrap(),
IncidentCondition::ContinuityExhausted.name()
);
for condition in [IncidentCondition::ContinuityExhausted, IncidentCondition::OperatorRequired] {
let payload = rmp_serde::to_vec(&condition).unwrap();
assert_eq!(rmp_serde::from_slice::<String>(&payload).unwrap(), condition.name());
}
assert_eq!(IncidentCondition::OperatorRequired.name(), "operator-required");
assert!(!IncidentCondition::OperatorRequired.ends_with_admission());
let payload = rmp_serde::to_vec(&CloseReason::NoLongerAdmitted).unwrap();
assert_eq!(
rmp_serde::from_slice::<String>(&payload).unwrap(),