- F: Store::flush(now) with a no-op default; Node::flush(now) replaces Node::store_mut. The caller's now anchors ages (fixes LogStore's stale clock marker, F18). - G: age_ms is documented as a lower bound. Robots are assumed to have a trusted wall clock (RTC or GPS); LogStore adds power-off time on open from a wall-clock flush marker. Unknown downtime expires held addressed messages and floors held broadcasts' age at unknown_downtime_age_ms. Hosts set observed_at. - AGENTS.md: specs and plans change only to record decisions Kyle has discussed and agreed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,10 @@ architecture in the **spec first**:
|
|||||||
- **Implementation plans:** `docs/superpowers/plans/`
|
- **Implementation plans:** `docs/superpowers/plans/`
|
||||||
|
|
||||||
The plans argue from the specs. Where a plan is silent, the **spec rules**.
|
The plans argue from the specs. Where a plan is silent, the **spec rules**.
|
||||||
Do not edit the specs or the plans — they are design history, not source.
|
Do not edit the specs or the plans on your own initiative — they are design
|
||||||
|
history, not source. Change one only to record something Kyle has discussed
|
||||||
|
and agreed, and record it as a dated decision in that document. Anything
|
||||||
|
else that needs changing in a spec goes to Kyle first.
|
||||||
`README.md` is the user-facing overview and should track the code.
|
`README.md` is the user-facing overview and should track the code.
|
||||||
`THREAT_MODEL.md` is the living security assessment: when a change fixes or
|
`THREAT_MODEL.md` is the living security assessment: when a change fixes or
|
||||||
changes a finding, update its status there (§1 and §10) with the commit.
|
changes a finding, update its status there (§1 and §10) with the commit.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Status: approved by Kyle (2026-10-20). Kyle decided D1–D5 and Q1–Q3
|
|||||||
(2026-10-18) and the review decisions R1–R3 and the `observed_at`
|
(2026-10-18) and the review decisions R1–R3 and the `observed_at`
|
||||||
definition (2026-10-19); review fixes applied. v6 added §10 (core hardening
|
definition (2026-10-19); review fixes applied. v6 added §10 (core hardening
|
||||||
from `THREAT_MODEL.md`) and the adversarial fuzz mode; Kyle reviewed and
|
from `THREAT_MODEL.md`) and the adversarial fuzz mode; Kyle reviewed and
|
||||||
approved both (2026-10-20).
|
approved both (2026-10-20). Decisions F (`flush`) and G (age across downtime) added 2026-10-20.
|
||||||
Builds on: `2026-10-16-tines-design.md`, `2026-10-18-tines-addressed-delivery.md`,
|
Builds on: `2026-10-16-tines-design.md`, `2026-10-18-tines-addressed-delivery.md`,
|
||||||
`2026-10-18-tines-fleet-deployment-design.md` (assumed fully implemented)
|
`2026-10-18-tines-fleet-deployment-design.md` (assumed fully implemented)
|
||||||
|
|
||||||
@@ -220,6 +220,33 @@ pub struct Dest { pub to: NodeId, pub lifetime_ms: u64 }
|
|||||||
monotonic milliseconds and never crosses the wire.
|
monotonic milliseconds and never crosses the wire.
|
||||||
|
|
||||||
The rustdoc on the field says all of this.
|
The rustdoc on the field says all of this.
|
||||||
|
- **`age_ms` is a lower bound on the true age (decision G).** It counts the
|
||||||
|
time the observation spent on running clocks, and not counted are:
|
||||||
|
- time a holder was powered off, since `Tick` restarts at 0 on every boot;
|
||||||
|
- time between a holder's last `flush` and a crash (§4);
|
||||||
|
- time in flight, since a frame carries the age it had when sent.
|
||||||
|
|
||||||
|
So errors only ever make an observation look younger, which is the
|
||||||
|
dangerous direction:
|
||||||
|
- a stale fact wins `LatestWins`;
|
||||||
|
- a young age spreads, and first-copy-wins dedupe keeps it;
|
||||||
|
- an addressed message outlives its `lifetime_ms`;
|
||||||
|
- `trim` by `max_age_ms` keeps what it should drop.
|
||||||
|
|
||||||
|
1.0 handles each gap:
|
||||||
|
- **Power-off time** is recovered by the store, from a wall clock, when it
|
||||||
|
opens (§9). The fleet assumes every robot has a clock it can trust
|
||||||
|
across a sleep, an RTC or GPS (decision G). It's cheap, and robots sleep
|
||||||
|
through charge cycles.
|
||||||
|
- **Crash time** is bounded by how often the host flushes (§4; host work,
|
||||||
|
`THREAT_MODEL.md` F18).
|
||||||
|
- **In-flight time** is negligible on the links tines targets, and is
|
||||||
|
accepted.
|
||||||
|
|
||||||
|
The rustdoc on `age_ms` says it's a lower bound and points applications
|
||||||
|
that need absolute time to `observed_at`. With a trusted clock assumed,
|
||||||
|
hosts set `observed_at` on every observation they make (R4). Today both
|
||||||
|
hosts pass `None`.
|
||||||
- `reject()` keeps only the expiry check; the malformed-pair branch and its
|
- `reject()` keeps only the expiry check; the malformed-pair branch and its
|
||||||
tests go away (the pairing is now enforced by the type, including on
|
tests go away (the pairing is now enforced by the type, including on
|
||||||
decode).
|
decode).
|
||||||
@@ -386,7 +413,11 @@ is a doc-comment link in `tines-sim/src/world.rs`, which is updated.
|
|||||||
(`TrimPolicy::default()` + field assignment still works).
|
(`TrimPolicy::default()` + field assignment still works).
|
||||||
- `Node` keeps its current methods: `new`, `id`, `next_seq`, `observe`,
|
- `Node` keeps its current methods: `new`, `id`, `next_seq`, `observe`,
|
||||||
`observe_to`, `poll`, `drain_events`, `belief`, `trim`, `store`,
|
`observe_to`, `poll`, `drain_events`, `belief`, `trim`, `store`,
|
||||||
`transport_mut`. `drain_events` returns `NodeEvent<T::Addr>`s: queued
|
`transport_mut`.
|
||||||
|
- It gains `flush(now) -> Result<(), StoreError>`, which delegates to
|
||||||
|
`Store::flush` (§4; decision F).
|
||||||
|
- `store_mut`, added by the fleet fixes so the service could reach
|
||||||
|
`LogStore::flush`, is removed. Read access through `store()` stays. `drain_events` returns `NodeEvent<T::Addr>`s: queued
|
||||||
`Delivered` events, then one `Dropped` per nonzero drop counter (§1).
|
`Delivered` events, then one `Dropped` per nonzero drop counter (§1).
|
||||||
- Every remaining public item is documented; `#![deny(missing_docs)]` goes in
|
- Every remaining public item is documented; `#![deny(missing_docs)]` goes in
|
||||||
`tines-core/src/lib.rs` (26 items are missing docs as of the fleet
|
`tines-core/src/lib.rs` (26 items are missing docs as of the fleet
|
||||||
@@ -401,6 +432,23 @@ changes:
|
|||||||
|
|
||||||
- **`is_empty()`** gains a default (`len() == 0`), dropping the
|
- **`is_empty()`** gains a default (`len() == 0`), dropping the
|
||||||
`len_without_is_empty` allow.
|
`len_without_is_empty` allow.
|
||||||
|
- **`flush(&mut self, now: Tick) -> Result<(), StoreError>`** is added,
|
||||||
|
with a default that does nothing and returns `Ok(())` (decision F). It's
|
||||||
|
the store's durability point, and it does two things:
|
||||||
|
- everything inserted, released or trimmed so far is durable;
|
||||||
|
- `now` becomes the time anchor for ages after a restart.
|
||||||
|
|
||||||
|
More of the contract:
|
||||||
|
- Flushing twice is harmless.
|
||||||
|
- A `now` earlier than the last anchor is clamped to it.
|
||||||
|
- A failure returns `Err(StoreError::Backend)` and leaves the store
|
||||||
|
usable.
|
||||||
|
- The application decides when to flush; core never calls `flush`
|
||||||
|
itself. On flash, every flush costs wear, so how often is the
|
||||||
|
application's call, like `poll` and `trim`.
|
||||||
|
- `MemStore` keeps the default. `LogStore` implements it, and its clock
|
||||||
|
marker uses the caller's `now`; today it uses the tick of the last new
|
||||||
|
insert (`THREAT_MODEL.md` F18).
|
||||||
- **Release semantics are documented as best-effort.** `release` stays
|
- **Release semantics are documented as best-effort.** `release` stays
|
||||||
infallible (`()`): if a persistent backend fails to record a release, the
|
infallible (`()`): if a persistent backend fails to record a release, the
|
||||||
worst case after reboot is a resumed copy, i.e. a duplicate custodian.
|
worst case after reboot is a resumed copy, i.e. a duplicate custodian.
|
||||||
@@ -426,7 +474,8 @@ changes:
|
|||||||
- release surviving `get`,
|
- release surviving `get`,
|
||||||
- `sig` round-tripping,
|
- `sig` round-tripping,
|
||||||
- trim order,
|
- trim order,
|
||||||
- no tombstones.
|
- no tombstones,
|
||||||
|
- `flush` being idempotent and leaving the store usable.
|
||||||
|
|
||||||
`MemStore` and `tines-store` both run it. This is new public API, called
|
`MemStore` and `tines-store` both run it. This is new public API, called
|
||||||
out per AGENTS.md: it's what makes a third-party `Store` trustworthy.
|
out per AGENTS.md: it's what makes a third-party `Store` trustworthy.
|
||||||
@@ -566,6 +615,35 @@ the new `Observation` encoding. There is no migration:
|
|||||||
`Observation` encoding. Opening a log with an older version is an error
|
`Observation` encoding. Opening a log with an older version is an error
|
||||||
that names the fix (delete the log). Silently misparsing an old log is
|
that names the fix (delete the log). Silently misparsing an old log is
|
||||||
not allowed.
|
not allowed.
|
||||||
|
- **Ages across downtime (decision G).** The same format bump adds a
|
||||||
|
wall-clock reading to the clock marker `flush` writes: the host's trusted
|
||||||
|
wall time in milliseconds since the Unix epoch, or none.
|
||||||
|
- `LogStore::open` takes the host's current trusted wall time,
|
||||||
|
`Option<u64>`. The host passes `None` if its clock isn't trusted, for
|
||||||
|
example a dead RTC battery with no GPS fix yet.
|
||||||
|
- **Downtime known:** both readings are present and now ≥ the marker.
|
||||||
|
`open` adds `now − marker` to every held observation's age,
|
||||||
|
saturating, on top of the tick fold it already does.
|
||||||
|
- **Downtime unknown:** either reading is missing, or the clock went
|
||||||
|
backwards. `open` treats every held **addressed** observation as
|
||||||
|
expired: its age is set past `lifetime_ms`, so core never delivers or
|
||||||
|
forwards it, and `open` reports the count once. A custodian that can't
|
||||||
|
tell how long it slept must not deliver a directive that may have
|
||||||
|
expired. Losing a possibly valid one is the lesser harm, because
|
||||||
|
sharing is opportunistic and the originator can re-issue.
|
||||||
|
- **Broadcasts when downtime is unknown:** they're kept, but their age
|
||||||
|
isn't trusted. `open` raises every held broadcast's age to at least
|
||||||
|
`unknown_downtime_age_ms`, an open option the host configures.
|
||||||
|
- They lose `LatestWins` to any fresher fact and age out with the rest.
|
||||||
|
- They stay available as a last resort for topics nobody else has
|
||||||
|
reported.
|
||||||
|
- The floor must be below the host's trim `max_age_ms`; otherwise the
|
||||||
|
next trim drops them, and that's option 2 (drop) by another name.
|
||||||
|
The default is left to the builder (host hardening, F10, sets the
|
||||||
|
trim defaults).
|
||||||
|
- Every `open` that runs a fold reports the downtime it applied, or that
|
||||||
|
it was unknown, once (stderr plus a counter, like the skipped-record
|
||||||
|
count).
|
||||||
- Hosts wipe pre-1.0 logs on upgrade. That's acceptable because no robot
|
- Hosts wipe pre-1.0 logs on upgrade. That's acceptable because no robot
|
||||||
runs tines yet, and none will be deployed with a pre-1.0 `tines-core`.
|
runs tines yet, and none will be deployed with a pre-1.0 `tines-core`.
|
||||||
- The header is what makes a later migration possible if one is ever
|
- The header is what makes a later migration possible if one is ever
|
||||||
@@ -676,6 +754,34 @@ The review's other fixes are applied too:
|
|||||||
milliseconds since the Unix epoch (UTC), or `None` if the observer has no
|
milliseconds since the Unix epoch (UTC), or `None` if the observer has no
|
||||||
trusted clock. Relays never change it, and it's unrelated to `Tick` (§2).
|
trusted clock. Relays never change it, and it's unrelated to `Tick` (§2).
|
||||||
|
|
||||||
|
## Decisions (Kyle, 2026-10-20)
|
||||||
|
|
||||||
|
- **F. `flush` goes on the `Store` trait, and `Node::flush(now)` replaces
|
||||||
|
`Node::store_mut`** (§3, §4).
|
||||||
|
- Every persistent backend needs a durability point and a time anchor,
|
||||||
|
so it belongs in the trait contract, not in one backend's own method.
|
||||||
|
- The default does nothing, so core stays I/O-free and `MemStore`
|
||||||
|
doesn't change.
|
||||||
|
- The application controls when flushes happen.
|
||||||
|
- Removing `store_mut` stops applications from changing the store behind
|
||||||
|
the exchange's back.
|
||||||
|
- Taking `now` also fixes `LogStore`'s stale clock marker (F18).
|
||||||
|
- The hosts' timed and shutdown flushes stay host work (F18).
|
||||||
|
|
||||||
|
- **G. Age across downtime** (§2, §9).
|
||||||
|
- `age_ms` is documented as a lower bound on the true age.
|
||||||
|
- Robots are assumed to have a trusted wall clock (RTC or GPS), cheap
|
||||||
|
for robots that sleep through charge cycles.
|
||||||
|
- Stores add power-off time on open, using a wall-clock reading in the
|
||||||
|
flush marker.
|
||||||
|
- When downtime can't be known, held addressed messages are expired:
|
||||||
|
possibly delivering an expired directive is worse than possibly
|
||||||
|
dropping a valid one, since sharing is opportunistic.
|
||||||
|
- Hosts set `observed_at`.
|
||||||
|
- When downtime can't be known, held broadcasts are kept but their age
|
||||||
|
is floored at `unknown_downtime_age_ms` (Kyle, 2026-10-20): assume the
|
||||||
|
worst about freshness without discarding data.
|
||||||
|
|
||||||
## Left to the builder
|
## Left to the builder
|
||||||
|
|
||||||
- The exact `WIRE_VERSION` value, within the rules in §1.
|
- The exact `WIRE_VERSION` value, within the rules in §1.
|
||||||
|
|||||||
Reference in New Issue
Block a user