docs(adr): ADR 009 makes an edge a session rather than a two-hop path, and its clock causal (improvise-felo.4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (adr9-steward)
2026-10-09 22:22:19 -07:00
committed by Edward Langley
parent e720f7ca50
commit 63ee13289b
+33 -32
View File
@@ -157,7 +157,8 @@ changes no operator. The visible axis is a remap.
These are the only rebuilds; every other change is a delta.
- **Saving a view definition** touches no operator.
- **Live keys are a cache.** A key is evicted after a period in which no request names
it (the period is set by measurement), and every eviction is announced (clause 9).
it. The period is a local duration on the host, set by measurement, and never compared
with another machine's clock. Every eviction is announced (clause 9).
**Column totals and hidden rows.** Totals are kept over all rows, because the shape key
is shared and a hide is one client's. Whether hidden items travel in a request is ADR
@@ -193,8 +194,7 @@ in their last bits, because they become correctly rounded, and a sum of `-0.0` r
### 9. Delivery
**Every edge may cross a network.** An edge, meaning the stream from one operator to the
next, may be in-process or remote, and a path may cross up to two network hops: browser ⇄
gateway ⇄ host, or host ⇄ worker ⇄ client. No edge assumes a function call or shared
next, may be in-process or remote. No edge assumes a function call or shared
memory. An operator that sits at a wire boundary is just one implementation of an edge,
an **edge adapter**. So every edge has the same semantics:
- **A wire form.** Every element type that flows on an edge has a serializable form,
@@ -218,27 +218,30 @@ an **edge adapter**. So every edge has the same semantics:
every element type has a round-trip property test, where encoding then decoding is
the identity for every value, including `-0.0`, NaN payloads, 28-digit decimals and
`i128`-wide fractions.
- **Stamps compose end to end.** The host stamps each committed command (below), and an
intermediate edge forwards the stamp unchanged. So a loss anywhere on the path shows up
downstream as a gap.
- **One clock per process.** Each process runs its own synchronous sub-circuit. Across a
hop, the stamped commands are the clock. A delta crosses a hop only after its command
- **The clock is causal, never a wall clock.** Ordering, staleness and loss detection
use only the stamp sequence (below), a logical clock the host advances once per
committed command. Each process runs its own synchronous sub-circuit, and across an
edge the stamped commands are the clock. No decision compares wall-clock times across
machines, so clock skew cannot affect it. A delta crosses a wire only after its command
commits, so a remote operator never sees a delta that is later rolled back.
- **Ordering and backpressure.** An edge delivers in order. A slow edge coalesces
exactly: deltas are summed, and notices merged. A merged notice carries the range of
publications it covers, `(from, to]`, so a downstream side sees no gap where nothing
was lost.
- **No long-lived stream assumed.** An edge may be carried as a series of requests and
responses, for example several HTTP requests on one connection, as well as a socket.
Every request can be retried, and a resync from a watermark is idempotent. A client
publications it covers, `(from, to]`, so the client that merges notices sees no gap
where nothing was lost.
- **An edge is a session, not a connection.** An edge may be one long-lived connection,
or a sequence of requests carrying the same session token or cookie, for example
several HTTP requests. The token only lets a client's requests be recognised as one
edge: the host keeps no per-client state, so what continues across connections is the
client's own watermark and shape keys. Every request can be retried, and a resync from
a watermark is idempotent. A client
asks for "notices after my watermark", and the host answers from **one shared, bounded log
of published notices**: a single log, not one per edge or client, holding the notices
themselves (keys and flags, not values). A request older than the log's start is
answered with a correction (below).
- **Failure is local to a hop.** A hop may be a socket or a series of requests, and it is
alive while its next message or request succeeds. Each hop keeps alive in both
directions. A side that hears nothing within the timeout, or whose request fails,
treats the hop as dropped. The downstream side then asks for a correction from its
- **Failure.** An edge is alive while its next message or request succeeds, and each end
keeps alive in both directions. Timeouts are local monotonic durations, never compared
across machines. A side that hears nothing within its timeout, or whose request fails,
treats the connection as dropped. The client then asks for a correction from its
watermark.
- **Correction is Z-set arithmetic.** A correction is `target − held`, where `target`
@@ -248,8 +251,8 @@ an **edge adapter**. So every edge has the same semantics:
position is a **watermark**: the whole stamp `(host instance, document_generation,
publication)` up to which it has applied the stream, so everything at or before it is
applied. A watermark from another host instance or generation is behind every current
stamp. The client holds one per shape key it reads. The host and any
gateway keep no watermarks: a client sends its own with each request.
stamp. The client holds one per shape key it reads. The host keeps no watermarks: a
client sends its own with each request.
- where the shared log covers the watermark and the covered range holds only moved
keys, the host sends the current values of those keys, restricted to the client's
window, and the client applies `target − held` over them. The log holds keys and
@@ -266,12 +269,11 @@ an **edge adapter**. So every edge has the same semantics:
command wrote and then rolled back is never read. A correction is labelled with the
publication range it covers, so it is applied only at the watermark it starts from,
which makes it idempotent and resumable. Its elements are `(key, value)` pairs with
weights, in the lossless wire form, so a client, a gateway or a worker applies it with
the same code.
weights, in the lossless wire form, so a client and a worker apply it with the same
code.
Aggregation stays on the model side (ADR 008), so the operators that aggregate run on
the model side, which may itself span a hop. Which other edges cross a wire is a
deployment choice. The engine's invalidation signal, which ADR 006's replay trigger
the model side. Which other edges cross a wire is a deployment choice. The engine's invalidation signal, which ADR 006's replay trigger
relies on, is a correction trigger below, followed from a displayed cell's key back to
the model.
@@ -313,7 +315,7 @@ fallback, and it costs every reader a correction.
maintained key, so it maps member keys through its own remap first. It then fetches what
moved. It asks for a correction when it sees a publication gap, a new
`document_generation` or host instance, its shape in the eviction list, a notice it
cannot read, or a dropped hop. A slow client may merge pending notices: the union of
cannot read, or a dropped connection or failed request. A slow client may merge pending notices: the union of
keys, the covered range of publications, and axis changes kept in order. Because the
wire is lossless, outputs keep the circuit's bit identity end to end.
@@ -324,12 +326,10 @@ mode the record count is fetched. `Projection` also published the ADR 001 basis
claims. Whether `project` returns it instead, and what replaces the layout tier, are
ADR 008's questions.
**Edge adapters at the client.** Toward the circuit, a client's adapter is a sink for
results and notices and a source of requests. Toward the client it is the mirror image.
Each hop has an adapter at each end, and each end keeps only that hop's state: its
liveness timer, and at the client end the watermark. A gateway hop (browser ⇄ gateway ⇄
host) may serve many clients behind one upstream connection, so the host sends one notice
per gateway rather than one per client. In process, an edge is a function call.
**Edge adapters.** An edge that crosses a wire has one adapter at each end. Toward the
circuit, the client's adapter is a sink for results and notices and a source of requests.
Toward the client it is the mirror image. Each end keeps only its timeout, and the
client's end keeps the watermark. In process, an edge is a function call.
### 10. Text column widths
@@ -397,7 +397,8 @@ Step 8's required tests:
random edit sequences and losses);
- a refused command consumes no publication number, and the commands before it in its
frame stay published;
- a dropped hop triggers a correction.
- a dropped connection or a failed request triggers a correction, and a later request
with the same session resumes from the watermark.
## Open questions
@@ -406,7 +407,7 @@ Step 8's required tests:
O(clients × all moved keys) per committed command. Filtering by topic would send each connection
only its own shape's key lists. It needs one subscription per connection, which pins
its shape in the cache. Is such a subscription "model-relevant information" the
backend may hold? On a two-hop path the topics could instead live in a gateway.
backend may hold?
*Recommendation:* broadcast with the size cap. Add topics only if measured traffic
with many clients on different shapes justifies them.