Skip to content

Split pool.remove into user-remove vs internal-retire (W11) - #1775

Merged
srid merged 9 commits into
masterfrom
w11-retire-split
Jul 12, 2026
Merged

Split pool.remove into user-remove vs internal-retire (W11)#1775
srid merged 9 commits into
masterfrom
w11-retire-split

Conversation

@srid

@srid srid commented Jul 12, 2026

Copy link
Copy Markdown
Member

buildRemotePool's remove carried two different facts under one verb — a user removing a host (which should persist the departure) and the pool shedding a dead session on its own initiative (#1708's guest re-serve pump-death). Once remove is wired to a persist hook, that conflation means an internal shed permanently un-remembers a host the user never removed — the defect the W10 host-persistence PR surfaced and is now blocked on. This splits the verb so persistence follows intent.

Two verbs, not a persist flag

verb teardown persists? for
remove(host) evict sockets → drop membership → destroy session yes (the persist hook fires first) the user's explicit removal
retire(host) identical no an internal shed — a dead session the pool sheds itself; the host stays in the persisted set and a membership store re-seeds it next boot

The destructive teardown is factored into one shared tearDownEntry; the two verbs differ only in whether they persist before it. Because retire has no persist step and swallows its own teardown fault, it can't reject — so a fire-and-forget void pool.retire(h) needs no .catch (unlike remove, whose persist step can). kolu's #1708 guest pump-death path now calls pool.retire.

On master this is behaviour-neutral — kolu passes no persist hook, so retire and remove are identical today. It lands first so W10 can rebase onto a correct contract, per srid's W11 ruling (the padi note, #1770). This is the sanctioned "framework change whose consumer lands right after it" shape.

Drishti gate

Additive API change — a new retire method on RemotePool. Drishti consumes buildRemotePool/RemotePool (hostRegistry.ts, admin-router.ts) but calls only existing verbs (add/remove/reconnect/recheckAll/has/subscribe/destroyAll) — never retire. Drishti's app typechecks green against this new surface-remote (verified locally by hydrating the change into drishti's tree). No paired drishti PR is forced — no drishti code changes are needed.

Carried finding (from W10's review)

F3: if retire's teardown fails, a dead re-serve mirror could be left in kolu's reServes cache and the pool's entries. The teardown ordering here (drop membership + notify before destroy, destroy swallowed) is the pool half; kolu's per-host reServeSurface cache eviction is keyed to pool.subscribe membership, so a retired host's mirror is pruned by pruneToMembers on the membership drop. Tracked so it isn't lost.

Tests: retire runs the same teardown as remove but doesn't persist; a no-op on an unknown host; swallows a destroy fault and resolves (proving the fire-and-forget is safe).

Generated by /be on Claude Code (model claude-opus-4-8).

srid added 8 commits July 12, 2026 00:07
…retire (W11)

buildRemotePool's `remove` carried two distinct facts: a USER removing a host
(which should persist the departure) and the pool shedding a dead session on its
OWN initiative (#1708's guest re-serve pump-death, which must NOT persist — only
an explicit user remove should forget a host). With one verb wired to `persist`,
an internal shed permanently un-remembers a host the user never removed.

Split them — two verbs, not a `persist` flag:
- `remove(host)` — user intent: persist first (the `persist` hook fires), then
  tear down. Unchanged behaviour.
- `retire(host)` — internal shed: the SAME teardown, but NO persist, so the host
  leaves the live pool yet stays in the persisted set and a membership store
  re-seeds it next boot. With no persist step it can't reject, so a
  fire-and-forget `void pool.retire(h)` needs no `.catch`.

The destructive teardown is factored into one shared `tearDownEntry` (the two
verbs differ only in the persist step before it). kolu's #1708 guest pump-death
path now calls `pool.retire`, not `pool.remove`.

On master this is behaviour-neutral (kolu passes no `persist` hook, so retire and
remove are identical) — it lands first so W10 (host-membership persistence, #1772)
can rebase onto a correct contract, per srid's W11 ruling (padi.mdx, #1770).

Additive API change (a new method); drishti consumes buildRemotePool and calls
only existing verbs, and its app typechecks green against this — no paired drishti
change forced. Reference docs updated (ref-surface-remote.mdx).
… entries

Architecture review (C6 state-and-time) caught that retire's "stays remembered"
contract was not actually delivered: persist derived its set from live
`entries.keys()`, which retire deliberately desyncs, so the FIRST add/remove after
a retire recomputed the file from live keys and silently dropped the retired host
BEFORE any reboot — meaning W10 would rebase onto a broken contract (F2 not really
fixed).

Track the intended-persisted membership as its OWN ordered Set: add inserts,
remove deletes, retire LEAVES IT UNTOUCHED, and persist writes from THAT set (not
live keys). A retired host now provably survives every later add/remove until a
reboot re-seeds it. Also dedup the add path so re-adding a retired host (still in
the persisted set) doesn't write a duplicate. Two new tests pin both.

Behaviour-neutral for a no-persist consumer (drishti): persistedMembership tracks
entries exactly when retire is never called.
…ve membership

Reword the persist hook docstring so it states its argument is the intended persisted membership, which after a retire can include a shed-but-remembered host absent from hosts()/has(); persist must not cross-check or prune against live membership.

Agreed by the lowy ⇄ hickey lens debate (finding lowy-2, raised by lowy). Not pushed or merged.
Thread verb into the WebSocket close reason (`host ${verb}`) so a retired host closes with reason "host retired", matching the adjacent log line instead of the hardcoded "host removed".

Agreed by the lowy ⇄ hickey lens debate (finding lowy-3, raised by lowy). Not pushed or merged.
Address codex's five findings on the W11 retire/remove verb split.

- F1 (major, fixed): remove() early-returned when no live entry existed, so a
  host already retired (gone from entries, still in persistedMembership) could
  never be forgotten by the user's explicit remove — it re-seeded next boot.
  remove() now no-ops only when the host is absent from the REMEMBERED set,
  always persists + drops the remembered claim, and tears down only when a live
  entry remains. Regression test added.
- F2 (major, fixed): "retire can't reject" was false — notifyMembership fanned
  out listeners unguarded, so a throwing subscriber (serveHostMap reconcile/fire)
  skipped session destroy AND rejected the queued task → void pool.retire() goes
  fatal. Each listener is now isolated (loud log, fan-out continues), making the
  premise structurally true; kolu's no-.catch call site stands. Regression test
  added.
- F3 (minor, fixed): module header, subscribe() docstring, and the fleet-safety
  guide now document retire and separate live vs remembered membership.
- F4 (minor, fixed): tearDownEntry comment + ref-surface-remote.mdx corrected —
  the verb labels the socket close reason too (not "only the log line"), the two
  verbs aren't "identical" without persist, and links evict on either verb.
- F5 (nit, fixed): ISOLATION test comment reframed around the shared teardown
  guard and kolu's new void pool.retire() shed path.

check + fmt green; 24 surface-remote hostFanout tests pass.
Close the two remaining doc findings from codex's round-2 verdict (F1, F2,
F5 already resolved in round 1; F3, F4 were held open).

- F3 (partial→fixed): registerConnection JSDoc now names retire alongside
  remove (both close the socket via the shared tearDownEntry); the fleet
  guide no longer claims a nonexistent `{ reason: "removed" }` /
  `{ reason: "retired" }` stream payload — it describes the real wire facts:
  a clean typed stream completion plus a 1000 WebSocket close whose reason
  string names the verb (`host removed` / `host retired`).
- F4 (fixed): RemotePool.retire JSDoc no longer says the two verbs are
  "identical" without a persist hook — they are the same on disk but differ
  on the wire (socket close reason + log line name the verb), matching the
  already-corrected website reference.

Doc/comment-only; no runtime change. just check green.
@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

⚖️ Lowy ⇄ Hickey lens debate

Consensus after 1 round(s) · lowy + hickey · base 10070a6629c0

Independent findings: lowy=4, hickey=3

Applied (3)

  • lowy-2 persist hook's contract silently broadened, but its docstring still implies persisted == live membership — commit 5a56b6b0e
  • lowy-3 Socket-close reason 'host removed' is now emitted for retire too — a diagnostic that lies — commit 86737639e
  • hickey-2 verb threaded through tearDownEntry for the log line, but socket close reason stays hardcoded — (uncommitted)

Agreed — no change (4)

  • lowy-1 Electricity test — receptacle is RemotePool; the verb split is the correct Lowy shape, not a flag (packages/surface-remote/src/hostFanout.ts:326-376)
  • lowy-4 Next-persisted-set is derived twice per mutation (the persist arg and the Set mutation are computed independently) (packages/surface-remote/src/hostFanout.ts:534-546)
  • hickey-1 remove() cannot forget a retired (non-live) host from persistedMembership (packages/surface-remote/src/hostFanout.ts:557-568)
  • hickey-3 retire's persist/divergence rationale duplicated across ~5 sites (packages/surface-remote/src/hostFanout.ts:571-581)

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Codex ⇄ Claude debate

Consensus after 3 round(s) · codex reviewed at xhigh reasoning effort · base 10070a6629c0

Round 1

codex — approved: false

The two-verb design, separate persisted membership, framework placement, and behavior-neutral landing are defensible. However, two major correctness defects remain: a later user removal can be discarded after retirement, and retire() can reject through membership callbacks despite the production caller treating it as non-rejecting. Several lifecycle comments and public docs also became inaccurate.

Findings:

  • F1 · major · open — remove() decides that a host is unknown solely from live entries. After retire() removes the live entry but preserves persistedMembership, a subsequent or concurrently queued user remove() returns without forgetting the host. The later authoritative user intent is silently lost, so the host returns after reboot. (packages/surface-remote/src/hostFanout.ts:562)
  • F2 · major · open — The claim that retire() cannot reject is false. tearDownEntry() invokes unguarded membership subscribers before session.destroy(). A subscriber throw rejects the queued promise, skips session teardown, and the bare void pool.retire(enc) reaches the server's fatal unhandled-rejection handler. The production serveHostMap subscriber executes projection and downstream callback code that can throw; the current test covers only a throwing session.destroy(). (packages/surface-remote/src/hostFanout.ts:509; packages/server/src/index.ts:426)
  • F3 · minor · open — The public lifecycle documentation still describes membership notifications, socket tracking, and the module lifecycle in terms of add/remove/destroyAll only, even though retire() now drops membership, closes sockets, and notifies subscribers. The fleet-safety guide likewise does not distinguish live membership from the required remembered-membership fact. (packages/surface-remote/src/hostFanout.ts:375)
  • F4 · minor · open — The reference says remove() and retire() differ only in persistence and become identical without a persist hook, but they expose different WebSocket close reasons and log messages. The implementation comment at hostFanout.ts:480 similarly says the verb only labels the log line even though it also labels the socket close reason. (website/src/content/surface/ref-surface-remote.mdx:78)
  • F5 · nit · open — The test comment still says Kolu fire-and-forgets remove(), but this branch changes that production path to retire(). It now explains a call pattern that no longer exists. (packages/surface-remote/src/hostFanout.test.ts:186)

claude — Fixed the two correctness gaps (retire→remove losing the departure; retire able to reject through a throwing membership listener) and tightened the four doc/comment inaccuracies; added two regression tests.

  • F1 fixed — Real contract bug, agreed. remove() early-returned on entries.get(host) === undefined, so a host already retired (gone from live entries but still in persistedMembership) could never be forgotten by the user's explicit remove — it stayed in the remembered set and re-seeded next boot, silently discarding the one authoritative "forget this host" intent. Fixed at packages/surface-remote/src/hostFanout.ts:562 (remove): the no-op test is now !persistedMembership.has(host) (remembered OR live), the persist+persistedMembership.delete always run when the host is remembered, and tearDownEntry runs only if (entry !== undefined) (a retired host has no live entry left to tear down). entries is always a subset of persistedMembership, so has on the remembered set is the whole test. Regression test added: hostFanout.test.ts "a user remove() AFTER a retire() still forgets the host" — persist last-called with the shrunk set and a later add does not re-introduce the forgotten host.

  • F2 fixed — Agreed the "retire provably can't reject" premise was incomplete. tearDownEntry calls notifyMembership() before session.destroy(), and notifyMembership fanned out listeners unguarded (for (const l of …) l()), so a throwing subscriber (production: serveHostMap's reconcile/fire) would both skip the session destroy AND reject the queued task → void pool.retire(enc) floats an unhandledRejection into the server's fatal handler. Fixed at hostFanout.ts:436 (notifyMembership): each listener is now wrapped in try/catch that logs the fault loudly (never silent — same teardown-fault treatment session.destroy() already gets) and continues the fan-out. This makes the premise true structurally for retire (and hardens add/remove/destroyAll too). Kept the author's ratified decision (no .catch at kolu's call site) intact by making its premise hold rather than adding a catch. Updated the retire docstring at hostFanout.ts:606 to cite the isolated fan-out. Regression test added: "retire() with a THROWING membership listener still RESOLVES and still destroys the session".

  • F3 fixed — Agreed the lifecycle docs omitted retire. Updated the module header (hostFanout.ts:18, "add / remove / retire"), the subscribe docstring (hostFanout.ts:375, now "LIVE-MEMBERSHIP changes (add / remove / retire / destroyAll)" plus the listener-isolation note), and the fleet-safety guide (website/src/content/surface/operate-a-fleet-safely.mdx): the membership bullets now name retire and its typed { reason: "retired" } end, and a new Aside separates live membership (the map's single-writer truth) from the remembered set (survives a retire).

  • F4 fixed — Agreed. The tearDownEntry comment claimed verb "only labels the log line"; corrected at hostFanout.ts:478 to say it labels BOTH the WebSocket close reason (host removed/host retired) and the log line. The reference (ref-surface-remote.mdx:77) no longer says the two verbs are "identical" without a persist hook — it now says they behave the same on disk (neither writes) while the close reason and log still name the verb, and that linkFor is evicted on either removal or retirement (verified: serveHostMap's reconciledetach deletes the link on any live-membership drop, serveHostMap.ts:190,196).

  • F5 fixed — Agreed the comment named a call pattern this branch removed. Updated hostFanout.test.ts:185: the ISOLATION test's comment now frames the guard as shared by remove/retire via tearDownEntry, notes kolu now sheds a guest via void pool.retire(h), and points at the retire-specific twin above; the test itself still exercises the shared guard through remove.

commit: eec8c24f54062be9920ea5f2a700d09cc85dce99

Round 2

codex — approved: false

The two correctness fixes are sound and their regression tests cover the reported failures. F5 is also fully corrected. The documentation cleanup remains incomplete: one public socket-lifecycle comment and the RemotePool.retire JSDoc still omit or contradict retirement behavior, and the fleet guide now claims typed reason payloads that the stream implementation does not emit.

Findings:

  • F1 · major · resolved — The original retire→remove lost-intent bug is fixed. remove() now consults remembered membership, persists its deletion, and conditionally tears down a live entry. (packages/surface-remote/src/hostFanout.ts:578)
  • F2 · major · resolved — The reported throwing-subscriber path is fixed. Membership listeners are isolated individually, fan-out continues, and session teardown proceeds without rejecting retire(). (packages/surface-remote/src/hostFanout.ts:445)
  • F3 · minor · open — The lifecycle documentation fix is incomplete. registerConnection still says sockets are tracked so only remove(host) can close them, omitting retire(host). The guide also newly claims retirement produces { reason: "retired" }, but forwardStream ends by returning cleanly and emits no such payload. (packages/surface-remote/src/hostFanout.ts:369; website/src/content/surface/operate-a-fleet-safely.mdx:39)
  • F4 · minor · open — The website reference and teardown comment are corrected, but the public RemotePool.retire JSDoc still says a pool without persist makes retire and remove behave identically. They retain distinct WebSocket close reasons and log messages. (packages/surface-remote/src/hostFanout.ts:366)
  • F5 · nit · resolved — The obsolete fire-and-forget remove() comment is corrected and now accurately describes the shared teardown path and production retire() caller. (packages/surface-remote/src/hostFanout.test.ts:217)

codex on the rebuttal: F1: accepted; the remembered-membership check and conditional teardown resolve the race. F2: accepted; per-listener isolation plus the regression test addresses the reported rejection path. F3: held open because the socket JSDoc was missed and the updated guide introduces a nonexistent { reason: "retired" } payload. F4: held open because the same inaccurate "identical" claim remains in the exported interface JSDoc. F5: accepted; the comment now matches production behavior.

claude — Closed the two remaining doc findings: named retire alongside remove in the registerConnection JSDoc, corrected the RemotePool.retire JSDoc's "identical" claim to "same on disk, distinct on the wire", and rewrote the fleet-guide bullet to describe a clean typed stream completion + a 1000 socket close (not a nonexistent { reason: "…" } JSON payload).

  • F1 fixed (already resolved round 1) — codex marks resolved; no further change.
  • F2 fixed (already resolved round 1) — codex marks resolved; no further change.
  • F3 fixed — Both parts agreed and corrected. Part 1 (socket JSDoc, hostFanout.ts:369): registerConnection said sockets are tracked "so remove(host) can close it", omitting retire. Since the shared tearDownEntry (invoked by BOTH remove and retire, hostFanout.ts:507-533) is what closes the socket via ws.close(1000, "host " + verb), retire closes it too — the JSDoc now names both verbs and points at the shared teardown. Part 2 (guide, operate-a-fleet-safely.mdx:39): codex is right — nothing emits a { reason: "removed" } / { reason: "retired" } JSON value on the stream. The wire facts are (a) the subscription/stream completes cleanly (a typed END, the map's error→typed-end guard, not a value carrying a reason) and (b) the WebSocket closes with code 1000 and a close-frame REASON STRING host removed / host retired (hostFanout.ts:516). Rewrote the bullet to state exactly those two facts and dropped the invented object payloads.
  • F4 fixed — Agreed. The exported RemotePool.retire JSDoc (hostFanout.ts:366-367) still said a no-persist registry makes retire and remove "behave identically (neither writes anything)". They are identical ON DISK but differ on the wire — the socket close reason (host retired vs host removed, hostFanout.ts:516) and the log line (hostFanout.ts:524) both name the verb. Reworded the JSDoc to "same on disk … but they still differ on the wire", matching the already-fixed website reference (ref-surface-remote.mdx).

Gate: just check green (pnpm typecheck + biome lint, 1277 files, no fixes); biome format clean on the touched .ts.

commit: 007ef9e66794f3ca50b100931397ad1e7c8ff12f

Round 3

codex — approved: true

All five findings are resolved. The correctness fixes remain intact, and commit 007ef9e accurately closes the remaining documentation gaps: socket tracking names both teardown verbs, the guide describes clean completion rather than nonexistent payloads, and the exported JSDoc distinguishes disk behavior from wire-visible close reasons and logging.

Findings:

  • F1 · major · resolved — The retire→remove lost-intent bug is fixed. (packages/surface-remote/src/hostFanout.ts:578)
  • F2 · major · resolved — Throwing membership listeners are isolated and no longer reject retirement or skip teardown. (packages/surface-remote/src/hostFanout.ts:445)
  • F3 · minor · resolved — The lifecycle documentation now names both teardown verbs and accurately describes clean stream completion plus the normal WebSocket close. (packages/surface-remote/src/hostFanout.ts:371; website/src/content/surface/operate-a-fleet-safely.mdx:39)
  • F4 · minor · resolved — The exported JSDoc now correctly states that no-persist behavior is identical only on disk while wire close reasons and logs remain distinct. (packages/surface-remote/src/hostFanout.ts:366)
  • F5 · nit · resolved — The test commentary accurately reflects the production retirement path. (packages/surface-remote/src/hostFanout.test.ts:217)

codex on the rebuttal: F1, F2, and F5 remain resolved. F3: accepted; both the missed socket JSDoc and invented reason payloads are corrected. F4: accepted; the public interface now precisely distinguishes disk behavior from wire-visible behavior. No disputes remain.

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

👮 Code-police

Two passes (rules + fact-check) over git diff 10070a662...HEAD.

Rules pass — no violations (24 rules checked). Called out as exemplary on callback-fanout-guarded-at-funnel: the new per-listener try/catch sits inside notifyMembership — the single funnel every emission path (add/remove/retire/destroyAll) passes through — which is exactly what closes the void pool.retire(h)unhandledRejection vector.

Fact-check pass — sound, all five load-bearing claims verified against the code:

  1. persistedMembership stays a correct superset of live entries across add/remove/retire/re-add.
  2. remove after retire truly forgets the host (guards on persistedMembership.has, persists the shrunk set, deletes it — even with no live entry).
  3. retire genuinely cannot reject (no persist step; tearDownEntry swallows the destroy fault; notifyMembership isolates listener throws), so kolu's bare void pool.retire(enc) is safe.
  4. persist-before-commit + enqueueMutation serialization preserved.
  5. the feat(padi): W4 — the switch (warm host pool + live per-tab host switching) #1708 typed-end ordering (drop membership + notify before destroy) is byte-identical to the pre-diff remove.

24/24 unit tests green. One pre-existing non-defect noted (a throwing opts.log would escape — a trusted diagnostic sink unchanged by this diff). No fixes required.

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Darwin e2e — retuned sample (srid-ratified, in progress)

Per srid's ruling, the two prior ci::e2e@aarch64-darwin reds were the run's own concurrent load tripping the local-padi liveness watchdog (#1776), on a verified-clean, cool box — not a #1775 defect. Retune: darwin e2e worker cap 6 → 3 (#1778).

Exact tree under sample (honesty over tidiness):

This is a diagnostic sample of the retune, not #1775's official check. Result appended when it settles.

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Retune sample result: FAILED — wedged even at PAR=3. ci::e2e@aarch64-darwin on w1775-retune-sample@afe882fd6 (= #1775 @ 98517e4 + retune @ c466a4f) failed at 4m57s. PAR=3 confirmed applied (workers=3 cores=24 load=3 cap=3 — cool box), yet 2 of 3 workers still hit the #1776 killAll-500 liveness cascade (~994 scenarios churned). So the collision is not load-marginal — it reproduces at half parallelism on a cool box, which disproves the retune's premise and shows the darwin e2e can't go green here until the #1776 product fix (local-arm liveness tolerance) lands. Disposition (accept on green-elsewhere + tracked-#1776 vs hold-for-fix) is with srid, now backed by three wedged samples (PAR=6 ×2, PAR=3 ×1). #1775's code stays off the failing path.

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Control run (srid-ratified, in progress). To prove #1775's innocence dynamically, a control sample runs ci::e2e@aarch64-darwin at PAR=3 on #1775's exact merge-base + the retune and nothing else — none of #1775's code. Tree: control-mergebase-retune @ e34365a75 = merge-base 10070a662 (= W9/#1764, exactly what #1775 forked from) + retune c466a4f9d. Same box (rasam), same lane. Reading: control wedges → the defect is master-resident/environmental, #1775 innocent dynamically too (disposition = clean policy call). Control greens → the split tree is implicated despite the static analysis → stop everything. Result appended when it settles.

@srid

srid commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

CI on the final HEAD dd43e1c64

Ran because the earlier 26/27 predates this HEAD (provenance below).

✅ Linux — full ci::default GREEN on the final tree

x86_64-linux on naiveintent, SHA dd43e1c64: 14 ok · 0 failed · 0 errored — OK.

node
ci::e2e@x86_64-linux ✔ 5m37s
ci::unit@x86_64-linux ✔ 1m54s
ci::smoke · ci::biome · ci::fmt · ci::nix · ci::flake-check · ci::home-manager · ci::pnpm-hash-fresh · ci::atlas-sync · surface-example builds ✔ all

⚠️ Darwin — NOT claimed on this HEAD

Provenance (straight record)

  • dd43e1c64 (this HEAD) = 98517e415 + master-merge (#1771 boundToPid + #1774 statepip).
  • The earlier 26/27 ran on 98517e415, which is post-gauntletpersistedMembership (ac11544cb) and both codex fixes (eec8c24f5, 007ef9e66) are ancestors of it. So the untested delta on dd43e1c64 is the master-merge, not the gauntlet fixes.

Note: #1771 (test daemons die with their run) is now in this tree — the bare-host daemon-leak cure — so a fresh darwin sample is worth it once rasam's transport is stable.

@srid
srid marked this pull request as ready for review July 12, 2026 14:04
@srid
srid merged commit 7f28115 into master Jul 12, 2026
22 checks passed
@srid
srid deleted the w11-retire-split branch July 12, 2026 14:04
srid added a commit that referenced this pull request Jul 12, 2026
…-cycle (#1776) (#1777)

Adds a Flaky Test Tracker row for the `aarch64-darwin` e2e flake
diagnosed as [#1776](#1776): the
local padi link's **liveness probe times out under load** and
force-cycles an alive-but-slow padi, so the per-scenario `Before`-hook
`killAll` 500s during the down-window and the worker queue-drains.

Surfaced while diagnosing #1775's darwin e2e (which is otherwise green
on linux and every non-e2e darwin node). Verified the flake is **not**
#1775's code (behaviour-neutral, changed paths unreachable by the
single-host suite), **not** the startup readiness gate (already correct
— `waitForPadiLive` gates every server start), and **not** the teardown
race #1719.

Docs-only; dist regenerated.

_Generated by [`/be`](https://github.com/srid/agency) on Claude Code
(model `claude-opus-4-8`)._
srid added a commit that referenced this pull request Jul 12, 2026
> ## 📚 Stacked on #1775 (the retire-verb split)
>
> This now sits on **#1775**, which delivers the framework fix W10
needed. The root cause — one verb (`pool.remove`) carrying both
*user-remove* (persist) and *system-retire* (#1708 pump-death, must not
persist) — is fixed there: kolu's guest pump-death path now calls
**`pool.retire`**, which tears the host out of the live pool **without**
persisting, so a transient guest fault no longer un-remembers a host.
W10's earlier `.catch` guard on `pool.remove` was dropped in the rebase
(retire provably can't reject).
>
> **Carried findings, resolved:** F2 (a guest pump-death permanently
forgot a host) — fixed by the split's `retire`. F3 (a dead re-serve
mirror stranded on retire) — kolu's per-host `reServeSurface` cache is
evicted by `pruneToMembers` wired to `pool.subscribe`, which fires on
`retire`'s membership drop, so the mirror is pruned.
>
> **Merge order:** split **#1775** first, then this. Base is
`w11-retire-split`; it retargets to `master` automatically once #1775
merges. Tracked as **W11** in the padi note (#1770).
>
> _CI + evidence run on the final (rebased) tree — pending a free pu box
(shared-pool saturation)._

---

**The remote hosts you add from the selector strip now survive a kolu
restart.** Today a restart forgets everything but the local default and
you re-add your fleet by hand; after this, every remembered host
reappears in the strip and reconnects through the normal connect
pipeline — a host that has since gone away shows its honest **failed**
chip with the cause rather than silently vanishing. The only way a host
leaves the strip is your explicit remove.

Membership is a **server** fact — kolu-server's pool (`buildRemotePool`)
is its one writer — so its memory lives beside that authority, never in
the browser, where localStorage would fork the one list into per-device
copies. The persisted artifact is a value replaced whole: a zod-schemed
`{ version: 1, hosts: string[] }` JSON of **encoded host keys only**,
beside `conf`'s `config.json` under `KOLU_STATE_DIR`, written atomically
(tmp + rename, async so it never blocks the serving loop).

### The seam this wires (srid-approved over the brief's sketch)

The brief sketched a hand-rolled write *inside the add/removeHost
handlers*. But the pool already ships a purpose-built, transactional
`persist` hook (from #1714) that kolu's own `buildRemotePool` call never
wired. Hand-rolling would duplicate it — the "parallel hand-rolled
mechanism" the *reuse the existing source of truth* rule names as a
defect — so we **wired the existing seam** instead:

| | Handler-side write (brief's sketch) | Pool `persist` hook (this PR)
|
| --- | --- | --- |
| Ordering | writes *after* `pool.add` commits → disk/memory can diverge
on failure | write ordered **before** the in-memory commit |
| Concurrency | two rapid strip-adds race the file | serialized through
the pool's one mutation queue |
| Failure | host live but unpersisted | just-built session **rolled
back** |

### Boot: seed, don't replay

```
parseKoluPadiHostSeed()  ─┐
                          ├─▶ dedup ─▶ initialHosts ─▶ buildEntry ─▶ W6 connect pipeline
loadPersistedHosts(file) ─┘
```

Persisted hosts are merged into `initialHosts` (deduped against the env
seed via order-preserving `new Set`) rather than re-added via a
post-build `pool.add` loop. Both flow the identical connect pipeline,
but seeding at construction *doesn't re-fire `persist`* — so the file is
only ever rewritten by a genuine runtime add/remove, and **an
interrupted boot can't truncate it**.

### Design philosophy

- **Fail fast** — a file that exists but fails the schema *crashes the
boot* naming the path (delete it to recover). Never
start-with-empty-fleet, which would silently eat the user's hosts
(`caught-error-must-not-collapse-to-empty`). This is why it's a
**separate** file, not the `conf` store: `conf` data is reconstructible
so a corrupt file resets to defaults; a fleet is not. Only `ENOENT`
reads as "fresh install"; a permission error surfaces.
- **Electricity** — persistence policy is app-level (`packages/server`
only); the durability *mechanism* is the framework's `persist` contract.
No framework change in *this* PR — that's exactly the split PR's job.
- **Reuse the source of truth** — the pool's `persist` seam,
`KOLU_STATE_DIR` (via a narrowed export from `state.ts`), the pool's own
`encodeHostKey` vocabulary, and a shared `isEncodedHostKey` promoted
into `kolu-common` (de-duping a copy the client held).

### Review gauntlet outcome

- **architecture-first-principles** — 1 confirmed defect **fixed** (W10
made `pool.remove` rejectable; guarded the fire-and-forget guest-retire
so a disk-write failure can't crash the whole server) + 3 minors folded
in. It also surfaced the deeper verb-conflation that became the block
above.
- **lens-debate (lowy ⇄ hickey)** — consensus, 1 round, 1 fix (key the
persist-exclusion off `LOCAL_HOST`, not the boot-default).
- **codex-debate** — *stopped as non-converging* after 4 rounds
(findings grew 13→16, never approved) when its author spiralled into an
out-of-scope retry/overlay mechanism that generated its own blocking
bugs. Reset to the lens-approved tree and triaged codex's **round-1**
review (of the clean diff) by hand — the legit in-scope findings are
applied (fail-fast read vs `existsSync` masking `EACCES`, `0600` mode on
the ssh-target file, APM-source sync, atlas doc-sync). The full
round-1–4 codex trail is posted as a comment for audit.
- **simplify** — efficiency + altitude clean; applied a reuse de-dup and
dropped dead test state.
- **code-police** — 1 fix (async fs on the persist path). Two findings
**recorded, not actioned** because they contradict the ratified plan:
*prefer-focused-library* (use `conf`) — the plan mandates a zod JSON
tmp+rename kept outside conf's ladder; *persisted-schema-stays-tolerant*
(filter, don't crash) — the plan mandates a fail-fast crash-with-path,
with a dedicated test. Surfaced for srid.

Plan of record: the W10 section of
`docs/atlas/src/content/atlas/padi.mdx` (#1770); the block resolution is
minted there as **W11**.

_Generated by [`/be`](https://github.com/srid/agency) on Claude Code
(model `claude-opus-4-8`)._


---

### Carried finding → resolved in the W11 split

**F3 (codex round-1, residual):** if the internal guest-retire's
teardown fails, a **dead re-serve mirror can be left stranded in both
cache layers** (`reServes` in `index.ts` and the pool's `entries`). This
is legitimately the **W11 split's** world — the retire path is being
rebuilt there (the non-persisting internal retire), so the
mirror-eviction ordering is fixed as part of that rebuild, not patched
here. Named here so it isn't dropped; tracked on the W11 PR.

_Disposition table for the full codex trail is in the coordinator
report; the round-1 in-scope fixes are applied on this branch (fail-fast
read, 0600 + fsync durability, env-seed provenance, strict schema,
doc-sync)._
srid added a commit that referenced this pull request Jul 13, 2026
**Status: ratified (2026-07-13).** srid delegated ratification to the
coordinator under /perfection-review; the note now carries `status:
accepted` and reads as the current snapshot only — no revision history
in the note body (git and this PR hold the journey).

**This note is now the campaign plan of record for the surface-framework
consolidation.** The original audit (Codex, `gpt-5`) was written
deliberately blind to the existing Atlas plans — that was methodology,
an independent read of the three working trees. This revision is the
reconciliation it deferred: the coordinator's reviewed ruling on the
proposal, ratified by srid, merged in as one plan.

### What merged in

- **The ratified [reactive
bridge](https://kolu.dev/atlas/surface-reactive-bridge.html)** — phase 0
shipped in W5 (#1759); its phases 1–4 are now PRs 7–10 of this plan's
sequence (one numbering scheme; the bridge note carries a
superseded-pointer).
- **The [consolidation
ledger](https://kolu.dev/atlas/padi-cleanup.html)'s campaign 1** — L28
rung 2 → PR 3, L20·L21 → PR 5, L1 → PR 8, L8's hygiene residue as the
tail commit; L22 and L24 dispositioned (L24 already shipped as #1749).
- **A staleness sweep** — the audit's pins predate six merged PRs (W9
#1764, W10 #1772, W11 #1775, W12 #1784, #1783, #1791). Every code claim
was re-grounded against current kolu/drishti/odu tips or cut; e.g. the
"six polled-query state machines" claim died against W9's per-host
ownership shape, and odu already adopted `implementSurface` since the
audit.

### The dispositions

| proposed | ruling |
| --- | --- |
| supervised `SurfaceRuntime` (final router · `done` · idempotent
`close`) | **kernel — PR 1** |
| bound procedures as first-class client members | **kernel — PR 2** |
| opaque `membershipId` per map add (+ typed connection key, `clockNow`
at admit) | **kernel — PR 3** |
| total, schema-valid `failureOf` (no fabricated `"other"`) | **kernel —
PR 4** |
| mirror consumes collection `deltas` (deletes drishti's parallel
stream) | **kernel — PR 5** |
| `firstFrameOrThrow`-style adoptions | **kernel — PR 6** |
| `Feed` member kind | **dropped** — it is the bridge's `scan` wearing a
wire protocol; the append-heavy wire concern (terminal bytes, logs)
becomes a named bridge design question, not a fourth member kind |
| `@kolu/surface-suite` | **deferred** — a thin composition leaf earns
no receptacle; its two real moves (final router, typed key) are in the
kernel; revival condition: the surface-app dependency direction |
| Odu lease stack (`SurfaceLease` … `SurfacePort`) | **Odu-local** —
five concepts for one caller fails prove-then-extract; graduates at a
second consumer; `SurfacePort` dies with it |
| `createLiveQuery` | **obsolete** — W9's `hostCodeTab.ts` ownership
shape supersedes the string-keyed singleton; the createResource instinct
may refine inside it |
| `notificationSurfaceApp` / `bootSurfaceApp` | **needs grounding**
against W5's shipped `createNotify`/SW seam before any PR is cut |

The note is restructured as user-facing / architecture / implementation,
with the sequenced ten-PR list (kernel first, every framework PR
drishti-paired per `.claude/rules/surface.md`) and grep-able
done-criteria. Status stays `proposed` — srid merges and flips.

### Validation

- `just atlas::build` (foreground, exit 0)
- `just atlas::check-sync` (sync + idempotency both green)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant