From f3a54d34e076b5916c7520e25b58ddc9587dd861 Mon Sep 17 00:00:00 2001 From: "Claude (adr9-steward)" Date: Fri, 9 Oct 2026 21:18:55 -0700 Subject: [PATCH] docs(adr): ADR 009 makes every operator edge network-transparent, over up to two hops and request/response transports (improvise-felo.4) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- context/adr/009-incremental-evaluation.md | 56 +++++++++++++++-------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/context/adr/009-incremental-evaluation.md b/context/adr/009-incremental-evaluation.md index 9bc84d21..33e43cfe 100644 --- a/context/adr/009-incremental-evaluation.md +++ b/context/adr/009-incremental-evaluation.md @@ -180,8 +180,34 @@ in their last bits, because they become correctly rounded, and a sum of `-0.0` r ### 9. Delivery -The same circuit serves the in-process TUI and remote clients. Its internal streams never -cross a wire, because aggregation stays on the host. +**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 +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, + defined in `improvise-protocol`. The adapter converts to and from it, and `zset` stays + free of serde. `CellKey` is not `Serialize`, so on the wire a cell is named by its + tuple of ids, its position or an ADR 002 surrogate. +- **Stamps compose end to end.** The host stamps each published batch (below), and an + intermediate edge forwards the stamp unchanged. So a loss anywhere on the path shows up + downstream as a gap. +- **Ordering and backpressure.** An edge delivers in order. A slow edge coalesces + exactly: deltas are summed, and notices merged. +- **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. + So each edge keeps an ordered log, every request can be retried, and a resync from a + publication stamp is idempotent. +- **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 resyncs: it refetches through the + path, and the host answers from current state. + +Aggregation stays on the model side (ADR 008), so the operators that aggregate run on +the host. Which other edges cross a wire is a deployment choice. The engine's +invalidation signal, which ADR 006's replay trigger relies on, is the change notice and +the refetch rule below, followed from a displayed cell's key back to the model. **A request** is a shape key plus a window, given as an absolute cross-product top-left and dimensions. The window is never state, and a request costs O(visible). @@ -227,11 +253,12 @@ identity, so every NaN counts as one value, because the wire spells every NaN `" state. `GridExtent` is derived by the client, and in records mode the record count is fetched. -**A connection operator** at the wire boundary adapts the circuit to a connection. It is -a sink for results and notices and a source of requests toward the circuit, and the -mirror image toward the client. It owns serialization, the stamp rules, notice -coalescing, keep-alive in both directions, and session resumption. In process it is -function calls. Its shape is open question 4. +**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: the last +stamp it applied and its liveness timer. 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. ### 10. Text column widths @@ -290,7 +317,7 @@ Step 8's required tests: - a dropped notice is detected; - a reconnect after a host restart refetches; - a refused frame consumes no publication number; -- a dropped connection forces a refetch. +- a dropped hop forces a refetch. ## Open questions @@ -299,7 +326,7 @@ Step 8's required tests: O(clients × all moved keys) per batch. 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? It could also live in the connection operator (question 4). + backend may hold? On a two-hop path the topics could instead live in a gateway. *Recommendation:* broadcast with the size cap. Add topics only if measured traffic with many clients on different shapes justifies them. @@ -318,16 +345,7 @@ Step 8's required tests: small from-scratch reference implementation, kept in the test crate, as the oracle, and a pushdown backend behind the same trait for query-only sources. -4. **What shape is the connection operator?** Either one component per connection, or - one per host process: a host half shared by all its connections, with a client half - in every front end. The second lets the host send one notice per operator rather than - per client, but holds per-connection state in the shared operator. Who owns - resumption state, and is it backend state? *Recommendation:* one per host process, - with resumption state in the host half keyed by a client-held token, treated as - delivery state rather than UI state. In the TUI and the browser worker, with one - client each, it is a function call. - -5. **Which columns get a host-maintained width?** Columns whose cells are text? What +4. **Which columns get a host-maintained width?** Columns whose cells are text? What about a column holding both text and numbers? *Recommendation:* text cells get the maintained width, and the view layer takes the larger of that and its own numeric width.