22462519
docs: link and serve join the table; spec table becomes the diffstat
a73x 2026-08-31 22:01
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -59,7 +59,7 @@ a symbol by its FILE stem (`wall_pump.askOn`) — a file, not a module. | |||
| 59 | | `src/client/` | `client` — `client_core` `hosts` `handoff` `layout` `keymap` `askpass` · `webhub` · `wasm_core` `client_core_wasm_check` (wasm roots the build wires outside the table) | | 59 | | `src/client/` | `client` — `client_core` `hosts` `handoff` `layout` `keymap` `askpass` · `webhub` · `wasm_core` `client_core_wasm_check` (wasm roots the build wires outside the table) | |
| 60 | | `src/tui/` | `wall`(`wallview.zig`) — `interact` `paint` `select` `predict` `wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*` | | 60 | | `src/tui/` | `wall`(`wallview.zig`) — `interact` `paint` `select` `predict` `wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*` | |
| 61 | | `src/cli/` | `mux`(dispatch) — `main`(daemon) `mux_main`(client) `webhub_main`(hub) · `agent`(`muxa.zig`) · `cliflags`(`flags.zig`) · `spawn` | | 61 | | `src/cli/` | `mux`(dispatch) — `main`(daemon) `mux_main`(client) `webhub_main`(hub) · `agent`(`muxa.zig`) · `cliflags`(`flags.zig`) · `spawn` | |
| 62 | | `src/` | `xdg` `sockpath` `dial` `proxy` `quic` `testtmp` — what both sides link; `dial` is the client side of a daemon socket, `term` and nothing else under it | | 62 | | `src/` | `xdg` `sockpath` `dial` `link` `serve` `proxy` `quic` `testtmp` — what both sides link; `dial` is the client side of a daemon socket and `link` the live connection under it whatever reached it (fd, handoff stdio, QUIC), `serve` the right to bind a socket path and the duty to unlink it, `term` and nothing else under them | |
| 63 | 63 | ||
| 64 | The grouping itself is a convention now, not a gate: the table wires the | 64 | The grouping itself is a convention now, not a gate: the table wires the |
| 65 | build and says who owns what, and an import edit is an ordinary diff. What | 65 | build and says who owns what, and an import edit is an ordinary diff. What |
docs/superpowers/specs/2026-08-31-connection-primitives-design.md
| Old | New | ||
|---|---|---|---|
| @@ -139,7 +139,7 @@ pub const Policy = enum { | |||
| 139 | pub fn bind(path: []const u8, opts: struct { | 139 | pub fn bind(path: []const u8, opts: struct { |
| 140 | policy: Policy, | 140 | policy: Policy, |
| 141 | backlog: u31 = 128, | 141 | backlog: u31 = 128, |
| 142 | cloexec: bool = false, | 142 | cloexec: bool = true, |
| 143 | }) !Bound; | 143 | }) !Bound; |
| 144 | pub const Bound = struct { | 144 | pub const Bound = struct { |
| 145 | fd: std.posix.fd_t, | 145 | fd: std.posix.fd_t, |
| @@ -157,10 +157,19 @@ Decisions inside: | |||
| 157 | worries about. Cost: one fstatat. | 157 | worries about. Cost: one fstatat. |
| 158 | - **`clobber_own` does the pre-bind `deleteFile`** both current sites | 158 | - **`clobber_own` does the pre-bind `deleteFile`** both current sites |
| 159 | hand-roll, written once next to the rationale. | 159 | hand-roll, written once next to the rationale. |
| 160 | - **`cloexec` defaults false and the default is load-bearing:** `mux d | 160 | - **`cloexec` defaults TRUE.** This spec originally wrote the opposite — |
| 161 | upgrade` execs the candidate over the running daemon and the listener fd | 161 | "defaults false and the default is load-bearing", on the premise that `mux |
| 162 | (and the agent sockets) must survive the exec. askpass passes true. The | 162 | d upgrade` needs the listener and the agent sockets to survive the exec of |
| 163 | field's comment names the upgrade path so nobody hardens the default. | 163 | the candidate. Implementation refuted the premise (Task 5): `std.net` |
| 164 | already set CLOEXEC on every listener this change converted, so a false | ||
| 165 | default would have been a silent loosening, not a preserved status quo, | ||
| 166 | and the upgrade path never depended on it. What the upgrade actually does | ||
| 167 | is clear the flag per fd immediately before the exec | ||
| 168 | (`Server.execUpgrade`) and put it back on the far side | ||
| 169 | (`Server.sealAdoptedFds`), so the fds that must cross the exec say so one | ||
| 170 | by one rather than standing open to every forked shell. The default is | ||
| 171 | therefore true, `serve.zig`'s field comment records why, and a unit test | ||
| 172 | pins both settings. | ||
| 164 | - **Length refusal stays split:** `sockpath.tooLong` remains `mux d`'s | 173 | - **Length refusal stays split:** `sockpath.tooLong` remains `mux d`'s |
| 165 | parse-time refusal on the asking client's stderr; everyone else lets | 174 | parse-time refusal on the asking client's stderr; everyone else lets |
| 166 | `initUnix` refuse at bind (the agent relay already documents relying on | 175 | `initUnix` refuse at bind (the agent relay already documents relying on |
| @@ -229,38 +238,68 @@ Risks named: | |||
| 229 | 238 | ||
| 230 | ## Line count: before and after | 239 | ## Line count: before and after |
| 231 | 240 | ||
| 232 | Measured 2026-08-31 (`wc -l`, whole tree 55,066 Zig lines). Deletion | 241 | Measured at delivery (`git diff --numstat 1aaa22fe..HEAD`; whole tree now |
| 233 | candidates, counted by symbol span: | 242 | 55,943 Zig lines). This replaces the pre-implementation estimate table: |
| 234 | 243 | ||
| 235 | | Region | Lines | | 244 | | Region | Lines | |
| 236 | |---|---| | 245 | |---|---| |
| 237 | | `dial.askOn` + doc | 42 | | 246 | | `src/link.zig` (new; 184 of it its own tests) | +490 −0 | |
| 238 | | muxa `sendFrameQuic` + `awaitFrameFd` + `awaitFrameQuic` + `waitReady` | 133 | | 247 | | `src/serve.zig` (new; 102 of it its own tests) | +197 −0 | |
| 239 | | `client.awaitFrames` | 58 | | 248 | | `src/dial.zig` | +20 −120 | |
| 240 | | `Transport` per-arm write/service/timeout/flush/read + `close` | 115 | | 249 | | `src/cli/muxa.zig` | +111 −175 | |
| 241 | | harness `firstStateFrame` + `awaitFrame` + `awaitFrameOn` | 105 | | 250 | | `src/client/client.zig` | +115 −179 | |
| 242 | | test-private `awaitSelectionReply` / `awaitSnapshotSize` / `awaitMarkerWithoutSnapshot` | 182 | | 251 | | `src/client/askpass.zig` | +65 −18 | |
| 243 | | inline poll+readFrame test loops (attach/session/modes/deliver/await) | ~250 | | 252 | | `src/server/server.zig` | +94 −58 | |
| 244 | | spin loops + `deadline_ms` countdowns | ~60 | | 253 | | `src/server/server_agent.zig` | +26 −19 | |
| 245 | | three binder sites (bind + teardown blocks) | ~50 | | 254 | | `src/server/server_sessions.zig` | +3 −5 | |
| 246 | | **total removed or moved** | **~995** | | 255 | | `build.zig` | +20 −11 | |
| 247 | 256 | | `src/server/server_test_harness.zig` | +288 −48 | | |
| 248 | Estimated additions: `link.zig` ~350 (of which ~120 its own tests), | 257 | | `src/server/server_test_attach.zig` | +239 −366 | |
| 249 | `serve.zig` ~180 (of which ~60 tests), `pumpUntil` ~25, harness wrappers | 258 | | `src/server/server_test_agent.zig` | +155 −87 | |
| 250 | ~40, wrapper glue in `Transport` / `AgentConnection` ~60 → **~655**. | 259 | | `src/server/server_test_upgrade.zig` | +80 −13 | |
| 251 | 260 | | `src/server/server_test_modes.zig` | +78 −32 | | |
| 252 | Net: **roughly −350 lines**, almost all of it test code. Production is | 261 | | `src/server/server_test_await.zig` | +69 −29 | |
| 253 | approximately flat by design — that code *moves* to one owner rather than | 262 | | `src/server/server_test_session.zig` | +49 −74 | |
| 254 | shrinking. The value is one copy of each loop, not fewer bytes; the test tree | 263 | | `src/server/server_test_deliver.zig` | +19 −4 | |
| 255 | is where consolidation actually deletes. | 264 | | `src/server/server_test_clipboard.zig` | +13 −15 | |
| 256 | 265 | | `src/tui/wall_test_pump.zig` | +9 −8 | | |
| 257 | These are estimates against measured spans; re-measure at delivery and | 266 | | `src/server/server_test_quic.zig` | +7 −0 | |
| 258 | replace this table with the real diffstat. | 267 | | **total (21 files)** | **+2147 −1261, net +886** | |
| 268 | |||
| 269 | The estimate was wrong in sign, and the reason is worth recording. It | ||
| 270 | predicted roughly −350 by counting the call sites a shared owner would | ||
| 271 | delete, and it undercounted what the owner itself costs: `link.zig` and | ||
| 272 | `serve.zig` together are +687, of which 286 are their own tests — a unit | ||
| 273 | suite the scattered loops never had, because a loop inlined in a test body | ||
| 274 | is not a thing another test can call. The call sites did shrink about as | ||
| 275 | predicted (`dial` −100 net, `muxa` −64, `client` −64, and | ||
| 276 | `server_test_attach` −127), but the harness grew +240 net: `pumpUntil` and | ||
| 277 | the Link-backed awaits are shared code that used to be copied per test file, | ||
| 278 | so it moved rather than vanished, and it moved into the file that now owns | ||
| 279 | it. | ||
| 280 | |||
| 281 | So: production +556 net, tests +330 net. The value delivered is the one | ||
| 282 | that was claimed all along — one copy of each await loop and one binder, | ||
| 283 | both now directly testable — and not a smaller tree. A consolidation that | ||
| 284 | adds a test suite for the thing it consolidates should be expected to grow | ||
| 285 | the line count; the estimate's mistake was counting only the deletions it | ||
| 286 | could see spans for. | ||
| 259 | 287 | ||
| 260 | ## Success criteria | 288 | ## Success criteria |
| 261 | 289 | ||
| 262 | - `grep -rn "posix.poll" src/ | xargs grep -l readFrame` finds only | 290 | - `grep -rln "posix.poll" src/ --include="*.zig" | xargs grep -ln readFrame` |
| 263 | `link.zig` (and the wall's event loop). | 291 | finds only `link.zig` and files that poll for a reason a single-fd frame |
| 292 | await cannot express. Measured at delivery, that set is: `link.zig` itself; | ||
| 293 | the wall's event loop (`wallview.zig`, `wall_pump.zig`) and the hub's | ||
| 294 | (`webhub.zig`), each waiting on a transport, a UI fd and ssh's stderr at | ||
| 295 | once; `client.zig`, whose waits watch the abort fd and ssh's stderr | ||
| 296 | alongside the socket — `Transport.open`'s dial wait, the ErrPipe drain and | ||
| 297 | the handoff announce read; `protocol.zig`, whose two are `POLL.OUT` write | ||
| 298 | backpressure inside `writeFrameBounded`, not reads at all; and the server | ||
| 299 | test files, whose 23 remaining `posix.poll(` call sites are silence | ||
| 300 | assertions and multi-client waits that each carry their reason in-file. | ||
| 301 | What the criterion actually forbids — a hand-rolled "await one frame type | ||
| 302 | on one fd" loop outside `link.zig` — has none left. | ||
| 264 | - One `Link` union in the tree; muxa still does not import `client`. | 303 | - One `Link` union in the tree; muxa still does not import `client`. |
| 265 | - All three production binders reach the socket through `serve.bind` and the | 304 | - All three production binders reach the socket through `serve.bind` and the |
| 266 | guarded unlink. | 305 | guarded unlink. |
try.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,59 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # Demo for the connection-primitives change: src/link.zig (the fd|pipe|quic | ||
| 3 | # connection under dial) and src/serve.zig (the right to bind a socket path and | ||
| 4 | # the duty to unlink it). Everything below goes through them — the daemon's | ||
| 5 | # listener is a serve.bind, and every client ask below is a Link await. | ||
| 6 | # | ||
| 7 | # Run from the repo root: ./try.sh | ||
| 8 | set -u | ||
| 9 | |||
| 10 | cd "$(dirname "$0")" || exit 1 | ||
| 11 | ZIG=deps/zig/zig | ||
| 12 | MUX=$PWD/zig-out/bin/mux | ||
| 13 | |||
| 14 | SCRATCH=$(mktemp -d /tmp/mux-try.XXXXXX) | ||
| 15 | SOCK=$SCRATCH/d.sock | ||
| 16 | |||
| 17 | # Isolated state: a bare `mux` records its daemon in $XDG_STATE_HOME/mux/hosts, | ||
| 18 | # and an un-isolated rig leaves a dead --sock line on the user's real wall. | ||
| 19 | export XDG_STATE_HOME=$SCRATCH/state | ||
| 20 | export XDG_RUNTIME_DIR=$SCRATCH/run | ||
| 21 | mkdir -p "$XDG_STATE_HOME" "$XDG_RUNTIME_DIR" | ||
| 22 | chmod 700 "$XDG_RUNTIME_DIR" | ||
| 23 | |||
| 24 | cleanup() { | ||
| 25 | "$MUX" d stop --sock "$SOCK" >/dev/null 2>&1 | ||
| 26 | rm -rf "$SCRATCH" | ||
| 27 | } | ||
| 28 | trap cleanup EXIT | ||
| 29 | |||
| 30 | step() { printf '\n=== %s\n' "$*"; } | ||
| 31 | |||
| 32 | step "build (zig-out is not refreshed by make ci)" | ||
| 33 | $ZIG build || exit 1 | ||
| 34 | echo "ok: $MUX" | ||
| 35 | |||
| 36 | step "serve.bind: mux d start -d --sock \$SCRATCH/d.sock" | ||
| 37 | "$MUX" d start -d --sock "$SOCK" || exit 1 | ||
| 38 | ls -l "$SOCK" | ||
| 39 | |||
| 40 | step "mux d endpoint --sock ... (announce, then it proxies; we take line 1)" | ||
| 41 | timeout 3 "$MUX" d endpoint --sock "$SOCK" 2>&1 | head -1 | ||
| 42 | |||
| 43 | step "mux a status --sock ... (a Link ask: one frame out, one frame awaited)" | ||
| 44 | "$MUX" a status --sock "$SOCK" | ||
| 45 | |||
| 46 | step "mux d stats --sock ... (a second ask on a fresh connection)" | ||
| 47 | "$MUX" d stats --sock "$SOCK" | ||
| 48 | |||
| 49 | step "mux d stop --sock ..." | ||
| 50 | "$MUX" d stop --sock "$SOCK" | ||
| 51 | |||
| 52 | step "serve's duty: the path is unlinked on the way out" | ||
| 53 | if [ -e "$SOCK" ]; then echo "FAIL: $SOCK survived the stop"; else echo "ok: socket path gone"; fi | ||
| 54 | |||
| 55 | step "no mux process left behind" | ||
| 56 | pgrep -a -f "$SOCK" || echo "ok: none" | ||
| 57 | |||
| 58 | echo | ||
| 59 | echo "done." | ||