Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions docs/vrs/.delta/DELTA-006-delivery-state-v1-arm.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,40 @@ point, the ownership filter, the legacy filename), `codex_v1.rs`, and
labels — Codex's `accepted` was a typed in-turn receipt and grades to
`consumed`, OpenCode's was a storage read-back and grades to `persisted`).

Canonical code gains one version-free concept, `Attestation{Observed,
Asserted}` on `Entry`: a phase this build graded versus a phase another
authority asserted. An assertion bounds what already happened, so it suppresses
a duplicate; it is not an observation, so it authorizes no transport until this
build sees something itself. That distinction is permanent and would be needed
by any future asserting authority, so it stays when the arm goes.

Deletion is `git rm -r src/migrations` plus replacing the seam arm with
`Ok(())`, which is byte-for-byte a first run on a fresh seat. Measured cost of
that deletion: one compile error, at the seam.
Canonical code gains one version-free field, `Attestation{Observed, Asserted}`
on `Entry`: whether this build graded the phase or another authority asserted
it. It changes no decision — no `Retention`, `RetryDecision`, or transport
arm reads it. What holds a carried-forward attempt is its `Phase` measured
against the harness `Profile`: no profile proves `Attempted`, and `Ledger::seed`
refuses an asserted phase its profile cannot prove at all, so the safety
property is delivered by the phase, not by the label. The field is there so the
fleet can *see* an unobserved phase — it is clause 2 of the Resolution Signal
below — which makes it trigger instrumentation for this arm, not a permanent
concept. It is deleted **with** the arm.

Deletion is `git rm -r src/migrations`, replacing the seam arm with `Ok(())`,
and removing the instrumentation the trigger needed: `Entry.attestation` and
`Attestation`, `delivery_ledger::asserted_entries`,
`migrations::delivery_state::resolution_signal`, and the `st2 doctor` row that
prints it. What remains is byte-for-byte a first run on a fresh seat. Measured
cost of the boundary deletion itself: one compile error, at the seam.

## Direction

update implementation

## Resolution Signal

Both commands below print nothing, on every admitted host, for seven
consecutive days:
`st2 doctor` prints, per seat, a `pre-ledger delivery state (DELTA-006)`
advisory carrying `preLedgerRecords` and `assertedEntries`, and prints nothing
when both are zero. The trigger resolves when that line is absent from
`st2 doctor` on every admitted host for seven consecutive days. Producer:
`migrations::delivery_state::resolution_signal`, pinned by
`migrations::delivery_state::tests::the_resolution_signal_counts_each_clause_without_consuming_it`;
measuring is read-only, so a diagnostic cannot make the record it counts
disappear.

The two clauses, and the equivalent commands for a host with no `st2` on PATH:

```sh
state="${XDG_STATE_HOME:-$HOME/.local/state}/st2"
Expand All @@ -74,10 +89,14 @@ find "$state/codex" "$state/opencode" -maxdepth 2 -name delivery-ledger.json -pr
| xargs -0 -r jq -r 'select([.entries[].attestation] | any(. == "asserted")) | input_filename'
```

Clause 1 also requires that rollback to a pre-ledger release has stopped being
supported: while it is supported, a rolled-back binary can write a new
`delivery-state.json`, and the roll-forward window it opens is pinned by
Clause 1 additionally requires a named artifact, not a recollection: a ratified
decision under `docs/vrs/.decisions/` recording that the pre-ledger release line
is no longer a supported rollback target. While no such record exists clause 1
stays open, because a rolled-back binary can write a new `delivery-state.json`
and the roll-forward window it opens is pinned by
`migrations::delivery_state::tests::a_rollback_then_roll_forward_does_not_see_the_record_written_in_between`.
That record's existence is what closes the clause; cite it by number here when
it lands.

The local half of the trigger — that with no old record present the module
contributes nothing and writes nothing, so removing it cannot change observed
Expand Down
76 changes: 51 additions & 25 deletions src/delivery_ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
//! authority and is reconciled through [`Ledger::prune`].
//!
//! One phase can reach this ledger without this build observing anything: an attempt an earlier
//! release made and left behind. [`Attestation`] is the whole vocabulary for that — an asserted
//! phase bounds what already happened, so it suppresses a duplicate, and it is not evidence, so
//! it authorizes no transport. The translation itself lives outside this module, behind the one
//! seam in [`Ledger::open`].
//! release made and left behind. Such a phase holds exactly as far as a phase holds — no harness
//! profile proves `Attempted`, so nothing is re-sent — and [`Attestation`] records that this
//! build never watched it, which is what makes the leftover countable and therefore removable.
//! The translation itself lives outside this module, behind the one seam in [`Ledger::open`].

use std::fs;
use std::io::Write as _;
Expand All @@ -20,8 +20,8 @@ use serde::{Deserialize, Serialize};

use crate::message;

pub const LEDGER_SCHEMA: &str = "st2.delivery-ledger.v1";
pub const LEDGER_FILE: &str = "delivery-ledger.json";
pub(crate) const LEDGER_SCHEMA: &str = "st2.delivery-ledger.v1";
pub(crate) const LEDGER_FILE: &str = "delivery-ledger.json";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Harness {
Expand Down Expand Up @@ -141,16 +141,20 @@ pub enum Evidence {

/// Whether this build observed the evidence behind a phase, or another authority asserted it.
///
/// An assertion is a true lower bound on what already happened, so it may suppress a duplicate;
/// it is not an observation, so it authorizes no transport until fresh evidence arrives. Nothing
/// here names the authority: any party that can bound an attempt this build never watched
/// asserts, and the record boundary in `crate::migrations::delivery_state` is one such party.
/// This is provenance, not authority: no [`Retention`], [`RetryDecision`] or transport decision
/// reads it. What holds a carried-forward attempt is its [`Phase`] measured against the harness
/// [`Profile`] — `Profile::releases` releases only a phase that harness can actually prove, and
/// [`Ledger::seed`] refuses an asserted phase the profile cannot prove at all. The field exists
/// so the fleet can *see* an unobserved phase: it is clause 2 of DELTA-006's Resolution Signal
/// (`st2 doctor` counts it per seat), and it is deleted with the record boundary that produces
/// it. Nothing here names the authority: any party that can bound an attempt this build never
/// watched asserts, and the boundary in `crate::migrations::delivery_state` is one such party.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Attestation {
/// This build graded the evidence that set the phase through [`Profile::graded`].
Observed,
/// Another authority asserted the phase: enough to hold a delivery, never enough to send one.
/// Another authority asserted the phase: this build never watched the attempt it describes.
Asserted,
}

Expand Down Expand Up @@ -192,9 +196,6 @@ pub enum HoldReason {
AmbiguousAttempt,
UnreadReceipt,
NegativeReceipt,
/// The phase was asserted, not observed: enough to suppress a duplicate, never enough to
/// authorize a transport. Only fresh evidence about the world clears it.
UnattestedClaim,
Quarantined,
Settled,
}
Expand Down Expand Up @@ -495,9 +496,6 @@ impl Ledger {
if entry.phase >= Phase::Persisted {
return Retention::Hold(HoldReason::UnreadReceipt);
}
if entry.attestation == Attestation::Asserted {
return Retention::Hold(HoldReason::UnattestedClaim);
}
Retention::Hold(HoldReason::AmbiguousAttempt)
}

Expand Down Expand Up @@ -562,8 +560,9 @@ impl Ledger {
///
/// The only constructor the recovery seam may use. The phase it carries is still checked against
/// the harness [`Profile`] by [`Ledger::seed`], so a claim of evidence the harness cannot produce
/// fails closed instead of being written, and [`Attestation::Asserted`] keeps the entry holding
/// until this build observes something: it can suppress a duplicate, it can authorize nothing.
/// fails closed instead of being written, and the phase itself is what keeps the entry holding:
/// an [`Attestation::Asserted`] entry suppresses a duplicate exactly as far as its phase does,
/// and authorizes a transport only where the profile proves that phase.
pub fn asserted(
filename: String,
binding: String,
Expand All @@ -582,6 +581,31 @@ pub fn asserted(
}
}

/// How many entries in the ledger at `path` carry a phase this build never observed.
///
/// Clause 2 of DELTA-006's Resolution Signal, and the reason [`Attestation`] is serialized at
/// all. Read-only on purpose: [`Ledger::open`] runs the record boundary and may write, and a
/// diagnostic must not change what it measures. A missing ledger counts zero; bytes that will
/// not parse are an error, because "I hold a delivery record I cannot read" is exactly what an
/// operator needs told.
pub(crate) fn asserted_entries(path: &Path) -> Result<usize> {
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
Err(error) => {
return Err(error)
.with_context(|| format!("reading delivery ledger {}", path.display()));
}
};
let record: Record = serde_json::from_slice(&bytes)
.with_context(|| format!("parsing delivery ledger {}", path.display()))?;
Ok(record
.entries
.iter()
.filter(|entry| entry.attestation == Attestation::Asserted)
.count())
}

/// Durable replacement: file bytes reach disk before rename, then the directory entry is synced.
///
/// The temp file is created exclusively at `0600` under a name unique to this process and write,
Expand Down Expand Up @@ -685,9 +709,11 @@ mod tests {
);
}

/// The safety property the whole record boundary rests on: an asserted phase is a bound on
/// what already happened, so it holds the delivery, and it is not evidence, so it authorizes
/// no transport. This build's own observation is what clears it.
/// The safety property the whole record boundary rests on, and where it comes from: a
/// carried-forward `Attempted` phase is a bound on what already happened, so it holds the
/// delivery, and no harness profile proves `Attempted`, so it authorizes no transport. The
/// phase does that work; the attestation only records who saw it. This build's own
/// observation — here an authoritative absence — is what clears the hold.
#[test]
fn an_asserted_phase_suppresses_a_duplicate_and_authorizes_no_transport() {
let tmp = tempfile::tempdir().unwrap();
Expand All @@ -706,11 +732,11 @@ mod tests {
.unwrap();
assert_eq!(
ledger.retention(FILE_A),
Retention::Hold(HoldReason::UnattestedClaim)
Retention::Hold(HoldReason::AmbiguousAttempt)
);
assert_eq!(
ledger.retry(FILE_A),
RetryDecision::Hold(HoldReason::UnattestedClaim)
RetryDecision::Hold(HoldReason::AmbiguousAttempt)
);

// The seed is durable, so a restart still holds instead of re-sending.
Expand All @@ -721,7 +747,7 @@ mod tests {
);
assert_eq!(
reopened.retry(FILE_A),
RetryDecision::Hold(HoldReason::UnattestedClaim)
RetryDecision::Hold(HoldReason::AmbiguousAttempt)
);

// An authoritative absence is an observation: it clears the claim and re-authorizes one
Expand Down
7 changes: 1 addition & 6 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,7 @@ fn resolve_stream(
let every_subject = discovered
.specs
.iter()
.map(|spec| crate::identity::AddressBookEntry {
id: spec.effective_id(this_host),
bus_identity: spec.bus_id(this_host),
host: spec.resolved_host(this_host).to_owned(),
address: spec.effective_address().to_owned(),
})
.map(|spec| crate::identity::AddressBookEntry::of(spec, this_host))
.collect::<Vec<_>>();
crate::identity::resolve_local_first(&every_subject, recipient, this_host)
.map(|entry| (entry.id.clone(), entry.bus_identity.clone()))
Expand Down
24 changes: 16 additions & 8 deletions src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ pub struct AddressBookEntry {
}

impl AddressBookEntry {
/// The one mapping from a declaration to its routable subject.
///
/// Every caller that hand-built this struct was re-deriving the same four fields from the
/// same spec, so the derivation lives here and the address book itself uses it too.
pub fn of(spec: &agent_spec::AgentSpec, this_host: &str) -> Self {
Self {
id: spec.effective_id(this_host),
bus_identity: spec.bus_id(this_host),
host: spec.resolved_host(this_host).to_owned(),
address: spec.effective_address().to_owned(),
}
}

/// The human-routable bus address `<host>.<address>`.
pub fn bus_address(&self) -> String {
format!("{}.{}", self.host, self.address)
Expand Down Expand Up @@ -146,7 +159,7 @@ pub fn resolve_id<'a>(
/// try every dotted split whose prefix is an admitted logical host and whose suffix is an
/// effective address in that host.
/// 3. Deduplicate by agent ID and succeed only when exactly one subject remains.
pub fn resolve_address<'a>(
fn resolve_address<'a>(
entries: &'a [AddressBookEntry],
reference: &str,
pinned_host: Option<&str>,
Expand Down Expand Up @@ -200,7 +213,7 @@ pub fn resolve_address<'a>(
}

/// Resolve either selector form against one coherent address book.
pub fn resolve<'a>(
fn resolve<'a>(
entries: &'a [AddressBookEntry],
selector: &AgentSelector,
pinned_host: Option<&str>,
Expand Down Expand Up @@ -237,12 +250,7 @@ pub fn address_book(specs: &[agent_spec::AgentSpec], this_host: &str) -> Vec<Add
specs
.iter()
.filter(|spec| !spec.desired_state.is_retired())
.map(|spec| AddressBookEntry {
id: spec.effective_id(this_host),
bus_identity: spec.bus_id(this_host),
host: spec.resolved_host(this_host).to_owned(),
address: spec.effective_address().to_owned(),
})
.map(|spec| AddressBookEntry::of(spec, this_host))
.collect()
}

Expand Down
31 changes: 25 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2229,6 +2229,30 @@ fn doctor_cmd(root: &Path, host: Option<String>, require_supervisor: bool) -> Re
),
}
}
// DELTA-006's Resolution Signal, per seat. Silence means both clauses are clear here, so
// the `delivery-state.json` boundary arm is removable once every admitted host is silent
// for the record's window — a trigger nobody produces resolves on memory instead.
// Advisory, not a problem: a carried-forward attempt is correct behaviour today.
let delivery_state_dirs = [
st2::codex_app_server::state_dir(&catalog, &bus_id),
st2::opencode_session::state_dir(&catalog, &bus_id),
];
match st2::migrations::delivery_state::resolution_signal(&delivery_state_dirs) {
Ok(signal) if signal.is_clear() => {}
Ok(signal) => report_advisory(
&format!("{bus_id} pre-ledger delivery state (DELTA-006)"),
&format!(
"preLedgerRecords={} assertedEntries={}",
signal.pre_ledger_records, signal.asserted_entries
),
),
Err(error) => report_check(
&mut problems,
false,
&format!("{bus_id} delivery ledger readable"),
&format!("{error:#}"),
),
}
if spec.desired_state.is_retired() {
let still_present = spec
.tasks
Expand Down Expand Up @@ -3094,12 +3118,7 @@ fn resolve_selected(
let entries = found
.specs
.iter()
.map(|spec| st2::identity::AddressBookEntry {
id: spec.effective_id(host),
bus_identity: spec.bus_id(host),
host: spec.resolved_host(host).to_owned(),
address: spec.effective_address().to_owned(),
})
.map(|spec| st2::identity::AddressBookEntry::of(spec, host))
.collect::<Vec<_>>();
let resolved = &st2::identity::resolve_id(&entries, &id)?.id;
let spec = found
Expand Down
7 changes: 1 addition & 6 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,12 +1247,7 @@ fn select_spec<'a>(
specs,
selector,
this_host,
|spec| crate::identity::AddressBookEntry {
id: spec.effective_id(this_host),
bus_identity: spec.bus_id(this_host),
host: spec.resolved_host(this_host).to_owned(),
address: spec.effective_address().to_owned(),
},
|spec| crate::identity::AddressBookEntry::of(spec, this_host),
|spec| spec.desired_state.is_retired(),
)?;
Ok(&specs[index])
Expand Down
Loading
Loading