feat: phase 1 — one terminal in the browser - #2
Merged
Conversation
Full terminal stack: Axum spawns a PTY (portable-pty), pipes output through a broadcast channel to WebSocket clients. Browser renders via ghostty-web through a thin JS bridge (ghostty-bridge.js) with 1:1 wasm-bindgen bindings. - server: pty.rs (spawn/read/write/resize via channels), ws.rs (bidirectional WS↔PTY pipe with scrollback replay), state.rs - client: ghostty-bridge.js, terminal.rs (FFI bindings), terminal_view.rs (Leptos component with WS status signal) - common: WsClientMessage/WsServerMessage enums, DEFAULT_COLS/ROWS - nix: fetchurl ghostty-web tarball, inject into buildTrunkPackage - UI: compact header with branding + WS status indicator - e2e: smoke (branding + canvas), terminal input, resize survival - justfile: test-dev recipe for fast testing against running dev server
- Add ResizeObserver so terminal reflows when browser viewport changes - Extract send_resize() helper to deduplicate 3x WS resize pattern - Create e2e test DSL (tests/e2e/dsl/) inspired by openspatial: scenario() entry point, TerminalView/AppView typed abstractions - Rewrite smoke + terminal tests using the DSL - Add border around terminal container - Inline ghosttyWebTgz into single nix derivation
nix build compiles Rust + WASM from scratch, which can exceed the previous 120s timeout. Bump to 600s.
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
srid
commented
Mar 19, 2026
Address PR review: split terminal_view.rs monolith into focused modules, extract named functions from closure nests, add doc comments throughout. Update code-review skill with learnings on module structure and readability.
Replace manual Closure::wrap + forget with leptos-use hooks: - use_websocket_with_options for WS connection - use_resize_observer for container resize - use_event_listener_with_options for font zoom keydown - on_cleanup for terminal dispose on unmount Extract bridge.rs for non-Leptos JS helpers (wait_animation_frame, extract_size, localStorage, build_ws_url). Simplify ws.rs to just WsStatus type with From<ConnectionReadyState>.
- measureCells() now uses actual grid dimensions instead of hardcoded 80x24, so cell size is correct after font size changes - stop_propagation() on zoom shortcuts prevents =/-/+ leaking to PTY - Add e2e tests: canvas fills container, fills after zoom, no keystroke leaks
ws_open() is non-blocking — the resize message sent immediately after was silently dropped. Now watches ready_state signal and sends resize when WS transitions to Open. Adds e2e test verifying cols > 80.
srid
marked this pull request as ready for review
March 20, 2026 00:23
- bridge::extract_size now returns Option<(u16, u16)> instead of hardcoding defaults from kolu_common - Collapse nested if in ws.rs (clippy::collapsible_if) - Update CLAUDE.md with clippy and pre-commit quality gates
This was referenced Mar 27, 2026
Closed
This was referenced Apr 8, 2026
srid
added a commit
that referenced
this pull request
Jun 30, 2026
…review F1)
restoreTargetOf paired memory.lastAgentCommand with observed.agent without checking
they name the same agent kind, so a stale-command/new-agent race (or migrated/edited
state) could build exact{ command: 'opencode …', agent: { kind: 'claude-code', … } }
— which resumeAgentCommand silently downgrades to opencode's most-recent, the
wrong-agent resume #2 exists to make unspellable, relocated inside the exact arm.
Add exactRestoreTarget(command, agent) in anyagent — the ONE constructor that builds
exact ONLY when agentKindFromCommand(command) === agent.kind, else null. Both
production sites go through it: the fold (restoreTargetOf → none on mismatch) and the
migration (backfillAwarenessCutover → legacyMostRecent on mismatch). resumeFormFor
then always takes resumeAgentCommand's same-agent path (exact-by-id or refuse), never
the most-recent downgrade. Pinned in fold.test, agent-cli.test, sessionTransfer.test.
srid
added a commit
that referenced
this pull request
Jun 30, 2026
## Awareness — the producer observes, kolu remembers (S1 + S2 → R9.0) Implements the merged plan [`awareness-derive-store.mdx`](docs/atlas/src/content/atlas/awareness-derive-store.mdx) (#1621) as **one coordinated change** — S1 (the memoryless producer) and S2 (kolu's fold) are coupled, so they ship together — then **hardened across two rounds of a 4-reviewer architecture-first-principles + perfection-review gauntlet** (`debates/pr1626-review`, `debates/pr1626-rereview`). The reviewers credited the fold spine **sound**; every residual was in the shell around it — the first round's four must-fixes (below), then two relocations codex caught on re-review (F1/F2). ### The cut The per-terminal awareness engine used to **derive** awareness by **mutating a host record it also read back as memory**, through an `AwarenessSink` two homes (kolu + `pulam`) each had to wire. This splits it: - **S1 — memoryless producer.** `startAwareness` → `startSensors`: it *emits* per-field `TerminalEvent`s and takes no seed, no sink, no record. Deleted: the `AwarenessSink` interface, **both** `makeAwarenessSink` impls (kolu's + pulam's `hooks.ts`), `AwarenessRecord`, the apply-and-publish read-back. - **S2 — kolu's fold.** A pure `fold(cur, o, ctx) → TerminalState` (`{snapshot: TerminalSnapshot, memory: AgentMemory}`). The producer's emit type **cannot spell** a memory fact, so the old "two narrowed mutators" write-fence is now the **type**. - **R9.0 — local cutover.** kolu runs the producer in-process and folds the stream, publishing the snapshot half to `snapshots` and the memory half to `kolu.authored`. ### Review-gauntlet hardening (the four must-fixes) | # | Finding | Fix | | --- | --- | --- | | **#2 / #7** | The restore target was a bare optional `resumeAgent` identity read alongside `lastAgentCommand`, so `(command set, identity absent)` meant **both** "quit, restore nothing" *and* "no id captured, resume most-recent" — and the derivation lived in the emit shell, not the fold. | The fold now produces a **discriminated `RestoreTarget`** — `none \| { exact, command, agent } \| { legacyMostRecent, command }` (`restoreTargetOf`, consumed by `resumeFormFor`). Absence can no longer be read as most-recent: **quit-to-shell → `none` → bare shell by construction** (#1492) for new records; a migrated pre-1.29 record keeps most-recent as a *named* `legacyMostRecent`. The client restore card + dormant tile require an `exact`/`legacyMostRecent` target. | | **#5** | The authored *collection* re-published on **every** observation tick (~150 ms) — only the disk arm was fenced. | The emit loop computes deltas once and gates each arm by **its own** delta: snapshot publish on a snapshot change, **authored publish on an authored-fact change**, disk autosave on a restore-relevant change. | | **#1** | Recency leaned on a 1.5 s wall-clock window; a slow agent resolution crossing it could restamp saved recency on restore. | The frame phase is decided **by value** — compare the re-resolved agent identity to the saved restore target. The timer is deleted. | | **#8** | The schema module's docstring still taught the deleted `AwarenessValue` / `AwarenessSink` world. | Rewritten to `TerminalSnapshot` / `AgentMemory` / `TerminalState` / the emit type. | `AgentIdentity` + `RestoreTarget` moved down to `anyagent/schemas` (the resume-vocabulary layer), re-exported through `terminal-workspace` — the fold *produces* the target, `resumeFormFor` *consumes* it, one owned type. The serial lens → codex → simplify → code-police pass then collapsed the vestigial `AgentSessionRef` into `AgentIdentity`, shared one `resumableCommand` projection across the client surfaces, value-gated the emit fences, and hardened the migration against corrupt input. ### Second re-review — two relocations codex caught (the panel earned its keep again) A second 4-reviewer pass on the hardened HEAD credited the four must-fixes **closed by construction** — but codex (1 of 4) found two real defects relocated *inside* the new shapes, both verified against the diff: | # | Finding | Fix | | --- | --- | --- | | **F1** | `restoreTargetOf` paired `lastAgentCommand` with `snapshot.agent` **without checking they agree on agent kind**, so a stale-command/new-agent race could build `exact{ command:"opencode …", agent:{kind:"claude-code"} }` — which `resumeAgentCommand` silently downgrades to opencode's most-recent: the wrong-agent resume #2 makes unspellable, re-expressed in the `exact` arm. | One constructor, **`exactRestoreTarget(command, agent)`** (anyagent), builds `exact` *only when* `agentKindFromCommand(command) === agent.kind`, else `null`. **Both** production sites go through it — the fold (→ `none` on mismatch) and the migration (→ `legacyMostRecent` on mismatch) — so `resumeFormFor` always takes the same-agent path (exact-by-id, or refuse), never the downgrade. | | **F2** | The funnel `guardedEmit` swallowed a thrown emit **after** the producer advanced its dedup baseline, so "emitted" ≠ "accepted": the fold never got the value while the baseline advanced → a later equal value deduped → silent permanent divergence. The guard sat at the wrong seam. | Error handling moved to where the fallible work is — the host's **publish boundary**. `commitSnapshot`/`updateMemory`/`emitTerminalsDirty` (kolu) and pulam's snapshots `upsert` each **fold-accept first, then guard their own publish** (logged, self-heals on the next change). With the publishes infallible, `emit` is infallible — and the wrong-seam funnel guard is **removed**. | ### Type-naming cleanup A follow-up **mechanical, compiler-checked rename** gives the awareness types **one root per shape** — `TerminalSnapshot` (the whole value a host emits) · `TerminalEvent` (a per-field delta) · `TerminalState` (the `{ snapshot, memory }` composite kolu folds) · `Known<T>` (resolution status). The `Terminal` prefix scopes the wire-facing family, and a field repeats its type's root (`entry.observed` → `entry.snapshot`). The served collection key bumps `awareness` → `snapshots` (`TERMINAL_WORKSPACE_CONTRACT_VERSION` `2.0` → `3.0` — breaking, the wire path changed). "Observation"/"awareness" survive only as feature-word prose; the deleted `AwarenessSink`/`AwarenessValue` keep their old names where the text describes what was removed. The schema-module docstring, the [design note](docs/atlas/src/content/atlas/awareness-derive-store.mdx) (types, prose, and the foldflow/homes diagrams), and the unit-test fixtures all move with the code. ### Honoring the design philosophy - **Fail-fast / no knobs** — the write-fence is the emit type; the restore decision is a discriminated value, not a runtime fallback. No override path. - **Volatility boundary** — producer/fold/`RestoreTarget` stay in the `@kolu/*` packages; kolu and pulam are consumers, the dependency arrow points out. - **Reuse the source of truth** — `foldSnapshot` is shared by kolu's fold and pulam's accumulator; `restoreTargetOf` / `exactRestoreTarget` are the *one* place a restore target is decided; the migration composes the existing backfill ladder. ### Deferred refinements (documented honestly, with pins) - **Active drain-at-sleep (#3).** `beginSleep` freezes the last fold-written target and does **not** drain a final settle, so a sub-second launch-then-sleep freezes a stale `none` and wakes to a **false bare shell**. The docs no longer claim the freeze "holds by construction"; under model B the consequence is fail-*safe* (a bare shell, never the wrong conversation). The active drain is an async-`beginSleep` follow-up. - **`git`/`pr` reference equality (#6).** `restoreRelevantEqual` compares `git`/`pr` by reference (correct today — upstream `gitInfoEqual`/`prResultEqual` dedup before emit). A `fold.test.ts` pin asserts a structurally-equal-but-new-ref `git`/`pr` still compares equal at the fence, so a future fold copy can't silently break it. - **R9.3 scope** — the framed `TerminalFrame` *wire* stream and cross-host keying stay R9.3; R9.0 keys by `TerminalId` and folds in-process. ### Migration `SCHEMA_VERSION` 1.28.0 → 1.29.0: `backfillSnapshotCutover` backfills `pr: { kind: "absent" }` and **synthesizes the discriminated `restoreTarget`** from what a pre-cutover record remembered — a valid `agentSession` whose kind matches the command → `exact`, a command alone (or a kind mismatch) → `legacyMostRecent`, neither → absent (`none`); corrupt `agentSession` value-types fall through safely rather than producing a parse-rejecting record. ### Tests & CI The pure `fold` / `restoreTargetOf` / `exactRestoreTarget` / `resumeFormFor` are unit-tested (the discriminated switch, the kind-mismatch refusal, the ref-stability pin, the publish-boundary infallibility). The sleep/wake, adopt, reconcile, metadata, session-transfer, and restore-card tests track the reshaped persistence; the e2e session-restore + sleeping-terminals fixtures were fixed to stamp the now-required `pr`. All **unit tests** green across the touched packages, `just check` (tsc + biome) clean, and the **Atlas sync gate idempotent** — re-confirmed after the type-naming cleanup. **Full CI green on both `x86_64-linux` and `aarch64-darwin`** (every recipe × both platforms) on the naming commit, including `ci::e2e` on each. _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 2, 2026
… padiSurface; delete surfaceCtx + reverse seal Review #2 (W1 padi seam): padi's domain modules co-wrote three foreign koluSurface cells (activityFeed, session, terminalList) and published the terminalExit event through an INJECTED kolu ctx (packages/padi/src/surfaceCtx.ts) — a reverse-direction dependency the forward seal couldn't see, and the mechanism behind the boot-recursion crash. This relocates every terminal-DERIVED wire member onto padi's OWN surface/ctx and seals the arrow. - padiSurface gains `activityFeed` + `session` cells (value forwarding policy); `session` merges with the existing `session` procedure namespace onto one wire node (no verb overlap). koluSurface shrinks to just preferences + processMemory. - The session/activityFeed conf-store STORAGE stays kolu-server's source of truth until W2.2: kolu-server builds the stores and INJECTS them into padi at boot (confStores.ts, fail-fast requireX getters mirroring requireServerProcessId). - servePadi backs both cells; the session `get` reads the injected store DIRECTLY + normalizes empty→null INLINE (never delegating to getSavedSession — the non-recursive backing that fixes the boot crash), carrying the content-dedup `equals` + the autosave-cancel `onWrite`. - session.ts / activity.ts / local.ts (terminalExit) now write padiSurfaceCtx; the client reads session/activityFeed/terminalExit off the `padi` client. - terminalList cell deleted: the client derives its list from padi's `terminals` collection keys stream; local.ts's emitTerminalListChanged is removed (the collection's upsert/remove IS the list source). No client reads TerminalInfo.pid. - getMetadata is made SOUND (no cast): it narrows padi's 3-arm PadiTerminal union to the honest 2-arm TerminalMetadata via the isParked guard (parked → undefined). - Dormant terminalWorkspace sibling retired from kolu-server's serving (zero consumers; the @kolu/terminal-workspace package + pulam's serving are untouched); the dead `workspace` client export dropped. - surfaceCtx.ts + workspaceSurfaceCtx.ts deleted; a REVERSE arm added to seal.test (d): no packages/padi/src file may import or type against koluSurface. - Parked-forfeit reconcile: a plain lifecycle.create discards lingering parked entries (session.restore still consumes them via the parked→active flip). - Restore toast restores the "Restored N terminals, resumed M agents" counts. Tests: padi contract (cells list + forwarding), reverse seal (d, verified red on a reintroduced koluSurface ref), session non-recursion + normalization, parked forfeit, and the restore toast counts — each red without its fix.
This was referenced Jul 3, 2026
srid
added a commit
that referenced
this pull request
Jul 7, 2026
… procedure ns (re-run #4 blocker) BLOCKER — a SECOND boot-breaker the mocked test surfaces hid. serveSurfaceMap's router-build loop reset `inner[member] = {}` unconditionally per member tuple from entryMemberVerbs, which emits a shared name TWICE (primitives first, procedures last). padi's `session` is BOTH a cell {get, test__set} AND a procedure namespace {restore, import, forfeit}, so the procedures pass clobbered the cell's verbs — `/surface/padi/session/get` 404'd on EVERY default boot (even single-host), firing "Saved-session subscription error" and leaving savedSession() null so the restore card never mounted: session-restore broken end-to-end. The contract half MERGES (define.ts:204) and oRPC's t.router doesn't validate completeness, so it was a SILENT contract/router divergence with no boot throw. The fix IS the pre-map guard restored: `inner[member] = inner[member] ?? {}` — exactly implementSurface's `namespaces[ns] = namespaces[ns] ?? {}` (surface/server.ts:1757) and the contract's own `?? {}` merge. This is a regression the always-map flip introduced; the pre-map implementSurface path served padi's session cell correctly. Pin closes the mocked-surface blind spot (both existing map test surfaces had ZERO procedures): a collisionSurface whose `session` is a cell {get} AND a procedure ns {ping}, served through real serveSurfaceMap → directLink → connectSurfaceMap, asserts BOTH routes resolve — the cell value (would 404 under the bug) and the procedure. Green: surface-map 17, client tsc, biome. (Re-run #4 blocker of 5; #2/#4/#1/#5 forks live.)
srid
added a commit
that referenced
this pull request
Jul 7, 2026
…(re-run #4 major) activeHost was a per-tab persistedPref whose ONLY writer was the chip onClick — no effect reconciled it against padiMap.entries membership. Removing the ACTIVE guest (user ✕, or the SERVER auto-retire on guest re-serve-pump death at index.ts's `void pool.remove(h)`) left activeHost pinned to the dead host: useEntry(activeHost) never re-keyed, the app-scope subs stayed on the departed host, every padiRpcOf(activeHost()).surface.* mutation threw MAP_KEY_UNKNOWN, and the canvas went blank with NO auto-switch and NO toast — a caught-error → silent dead-end (violating fail-loud). index.ts:307's "the canvas falls back" comment asserted a fallback that did not exist. Fix (mirror the shipped terminal auto-switch pattern, at the host level): a createEffect in wire.ts's app-lifetime hostScoped root reads padiMap.entries.use().keys() and, once a snapshot has landed, reconciles a departed active host to LOCAL_HOST with a toast.warning naming it. The decision is the pure hostReconcileTarget(keys, active, localHost) helper (wire.ts is module-init, untestable in isolation — same pinnability precedent as floorOnLiveness/pruneToMembers). Guards: no-op during the warming window (empty keys), when the active host is still a member, or when it's the local default. The entries sub dedups with the strip's via the base-client ref-count. index.ts:307's comment is corrected to name the client reconcile that now makes "falls back" true. The reconcile is trigger-agnostic — it keys off entries membership, so BOTH the user-✕ and the server auto-retire (index.ts:339) departures are covered by the one mechanism + its pin. Green: client 574 (+4 hostReconcile pins), tsc, biome. (Re-run #4 major of 5; #2/#1/#5 forks live.)
srid
added a commit
that referenced
this pull request
Jul 7, 2026
…ackages The remaining type-audit minors, batched disjoint-by-package (4 agents, no-git-ops guard, one recovered cleanly from a mid-run API error). Combined green: tsc 49 projects; surface-map 22 / surface-remote 133 / padi 221 / server 186 / client 600. surface-map+client: entries floored via the same floorOnLiveness as state() (the #1568 green-over-dead closed at the membership path); EntryConnectionState/MapRegistry parameterized over Prov (a local-only registry gets a compile error on copying, test-d pinned); Subscription.complete forwarded through delegateSubscription + wire + createPolledQuery (stays optional — non-factory sites have no typed-end to report); useIntentEditor collapsed to one clear-ability representation. server: PadiSession made generic over Prov (local arm PadiSession-of-never can't be copying, test-d pinned — the split's last consumer); ProcessStartedAt seed uses the real serverStartedAt epoch, not the in-band 0. padi: PadiIdentity union dropped the uninhabited undefined arm; HostLocation remote hostId min(1) + pin. (3 real-but-cross-package residuals documented for follow-up: PadiUrgency.awaiting, lastActivityAt-0 in terminal-workspace, SavedSession double-absence.) surface-remote: getHandler returns the entry (membership by presence, not a colliding undefined handler); ResolveDrvError.failureCause tightened off network; ExitResult now a closed exit|signal|spawn-error union (honest signal, no code-null conflation); relayStream lead boxed; ClosedInfo gained transport-failed (ssh-255) + endpoint-down (no-process death) variants. ConnectionInfo flatten REFUTED (deliberate loose wire projection, safety inherited from the SessionState sum at source — a wire discriminated union would ripple to drishti for an unreachable, un-read contradiction). drishti Phase-3 surface.md gate carries the API-facing surface-remote deltas (#2-#7). Residual: the ClosedInfo endpoint-down PRODUCER (server padiBinding) migration is a small paired follow-up. [never-defer]
srid
added a commit
that referenced
this pull request
Jul 8, 2026
…t at connecting/down, so the Skew-UX host-down card never rendered Second step-5 catch (driving a live cross-supervisor sincereintent): projectStatus now correctly reads the chip as failed, but the CANVAS still showed 'Connecting...' / the kaval-dead DegradedCanvas, never the 'Another kolu owns this host' card. Root cause: a failed host binding has no daemon-status coming (daemonPending stays true forever), and resolveCanvasMode's loading gate had 'entry === failed' in its ceiling — so a failed entry resolved to connecting/down BEFORE reaching the 'failed -> host-failed' arm. The Skew-UX host-down card was thus unreachable for any failed host. Fix: the loading gate excludes a failed entry (it has nothing to wait for), so it falls through to its cause-typed card. Pinned: canvasModeResolver.test.ts (failed + daemonPending + pendingTimedOut -> host-failed, cross-supervisor AND link-failed).
srid
added a commit
that referenced
this pull request
Jul 8, 2026
…xed; Skew-UX card verified end-to-end on live contested host
This was referenced Jul 13, 2026
srid
added a commit
that referenced
this pull request
Jul 28, 2026
…d a source that says what its silence costs The facet is the noun V2 introduces and it had no declaration: a bare `String` in Rust, spelled as ~49 literals across the two sensors. It is now a `Facet` enum, carried across to the TypeScript client by a checked-in `facets.json` that both sides pin by test. Around that, four honesty defects where the tool dropped a fact instead of reporting it — the failure mode this tool exists to refuse. - lowy F1 ≡ hickey #1 — facet names were a typed union on the consumer and a bare `String` on the producer, so a Rust typo shipped and surfaced as a consumer parse error - hickey #5 — darwin's `kern_proc_all` named one facet, but its silence costs `proc`, `start_time`, `uid` and `status`; a `--uid` consumer scoped it away and read an empty table as "no process has a uid" - lowy F7 ≡ hickey #4 — a failed host-global constant (linux page size, darwin mach timebase) degraded to N per-pid `U` rows while `CLK_TCK` emitted one `E` row; the comment at linux.rs forbids exactly that - lowy F7 — "empty means blind" carried two wire codes across the platforms; `BLIND_OR_EMPTY` is now the single code for the one condition - lowy F2 ≡ hickey #12 — `attribute_host_listeners` switched truth sources on a mode flag, so a PARTIALLY gated host table silently dropped listeners the fd walk had positively observed; it now always unions, and the caller owns the one emptiness decision - lowy F11 — a malformed `pcblist` record `break`s and returned partial rows as healthy; malformed now fails loudly, with the 24-byte closing record kept as the terminator both fixtures actually end with - lowy F6 — `Port.uid` was absent platform-wide on darwin with no honesty row, so a consumer had to know its own OS to read a field of a platform- independent contract; darwin now emits `E darwin_listeners ports_uid ENOTSUP` - hickey #3 — `push_unreadable`, `source_error` and the sort block were verbatim twins in the two sensors and the sorts had ALREADY drifted (linux `(port, pid)`, darwin `port`); row order is now `Snapshot::normalize`, called once - hickey #2 — `has_facts` re-enumerated the struct by hand; it is an exhaustive destructure, so a new field is a compile error rather than a wrong exit code - hickey #13 — `write_json` and the `E`-row loop were byte-identical in both documents The pure half of the darwin listener path (record walk + merge) moves to `osfacts::pcblist`, so the two fixes above are compiled and tested on every platform instead of only the darwin CI lane. Fixed on the way: a JSON assertion in `cli_contract` that `b8514697` had left comparing a two-key object against a three-key row — dead on linux, failing on darwin. Raised by the lowy ∥ hickey lens review. Not pushed or merged.
This was referenced Jul 28, 2026
srid
added a commit
that referenced
this pull request
Aug 1, 2026
- hickey #2 — rule 2's `seen` membership test now covers every entry reaching `overlay`, not just the top-level filter: a duplicated path makes `pathDiffOperations` emit two adds for one row, Pierre throws, and the recovery discards every hand-expanded folder. - hickey #6 — the rule-4 queue walk becomes one self-contained recursive `emit`. Fixes `lazyDirs.push` sitting above the visited check (a directory reached twice was listed twice) and `queue.unshift(...children)`, a `RangeError` on the six-figure flat level this feature targets. The loaded-but-empty directory keeps its own row exactly as before. - hickey #5 — `ignored` now names a loaded directory's own key as well. Pierre still paints that row from its children's prefixes, so dropping it un-dimmed the folder at the moment the user opened it, children dimmed below. - hickey #10 — `diffInventory` is the one constructor for the no-overlay case, so a new `BrowseInventory` field can't be forgotten at CodeTab's literal. Raised by the lowy ∥ hickey lens review. Not pushed or merged.
srid
added a commit
that referenced
this pull request
Aug 1, 2026
- hickey #3 — `openLazyDirs` carried three facts and two broke. A row that has no node right now (a search projection hid it) no longer retires the record, so a filter keystroke stops erasing the user's expansion; and a key the host no longer declares lazy is pruned, so an eye-toggle round trip reports afresh instead of showing an arbitrarily old cached level with no refetch path. - lowy #2 — a `lazyEpoch` prop clears the record when the host's loaded levels stop describing this tree (a repo / host switch), the wrapper's half of the invalidation the host already performs on its children cache. - lowy #6 — `onExpandLazyDirectory` may return a promise; on rejection the wrapper forgets the expansion, so a transient read failure no longer wedges the folder open-and-empty for the mount. - hickey #7 — the `tree.subscribe` callback runs under `safeApply`, so a throw can't escape into Pierre's emit loop and take other subscribers with it. - hickey #9 — "which directories should be open" is spelled once (`desiredExpandedPaths`) and used at both the constructor and `toOpen`. - hickey #8 — `expandPaths`' JSDoc no longer asserts an invariant `openLazyDirs` has made false. Raised by the lowy ∥ hickey lens review. Not pushed or merged.
srid
added a commit
that referenced
this pull request
Aug 1, 2026
- hickey #4 — one `AbortController` per directory replaces the hand-rolled `loadGeneration` + captured-`issuedSlot` pair, so "is this response still wanted" is ONE fact both callbacks read rather than two conditions each has to repeat. Fixes the real asymmetry where `.catch` checked only the slot and could toast a failure over a folder showing correct contents, and stops the superseded readdir server-side instead of racing it. - lowy #2 — pass `lazyEpoch={slotKey()}` beside the existing clear, so the tree's record of open lazy directories is invalidated by the same signal. - lowy #6 — the handler returns its promise and re-throws after toasting, so the tree drops its record and a retry costs one re-expand. Raised by the lowy ∥ hickey lens review. Not pushed or merged.
This was referenced Aug 1, 2026
srid
added a commit
that referenced
this pull request
Aug 5, 2026
…ng clean The deploy-#2 frozen panes (kolu#2101): under the restore stampede, attach streams ended with no `overflow` frame while their PTYs kept running. `reattachingDeltas` read that as a graceful end and returned, and every retry layer above retries FAILURES only — so the tile waited forever for a `terminalExit` that was never coming. Blank pane, live title, no verdict. kaval closes an attach fan-out in exactly one place — the PTY's own exit teardown — with no idle timeout or reaper, so a plain end while the PTY is alive never came from a healthy host. A plain end is therefore a question, not an answer: `open()` IS the liveness probe, because kaval answers a re-open for a departed PTY with `PtyNotFound`. Alive re-attaches and keeps going, gone ends the stream (the real exit), and our own abort — the consumer's teardown, which is what ended the iterator — still ends it graceful. Bounded: PLAIN_END_REOPEN_ATTEMPTS = 3 with a doubling pause off REATTACH_PAUSE_MS (150/300/600, ~1.05s), reset by any leg that delivered a frame — the same rule STREAM_RETRY uses. Exhausted THROWS loud, which hands the tile to the client's failure machinery (a strictly stronger repair: it rebuilds the whole chain) rather than to a silent clean end. `pullOnly`'s abort-swallow is left alone with a note: its signal is the stream's own, so it can never convert a producer's death into a clean end.
srid
added a commit
that referenced
this pull request
Aug 5, 2026
`runtime.done` rejecting was logged and survived (#1792's disposition), which in the deploy-#2 incident produced a zombie: padi alive, gate held, socket answering, its runtime dead, and `done` already settled so no future fault could ever be seen. The reviewer's point stands — under fail-fast a dead runtime should die, because the supervisor respawns and session-restore recovers in seconds, whereas a half-death needed an operator. After G1 that channel carries only structural wiring death (see the audit on `SurfaceRuntimeHandle.done`), so it is now fatal at both daemons: - the spine gains `DaemonSpec.faultSignal`, a second abort arm resolving `reason: "runtime-fault"` — the ONE shutdown `daemonExitCode` scores non-zero, since the exit code is a supervisor's only channel for "crash, not stop". Routing through `waitForShutdown` (never a bare `process.exit`) is the point: listener closed, gate released, last rites run first. - `armRuntimeFaultExit` is the shared observer both daemons ride: it logs the WHOLE error (the incident's line was message-only, so the failing cell was unidentifiable), runs the daemon's last rites — padi captures its final session — and aborts the arm. Last rites that throw are logged; the exit still happens. The #1792 unhandled-rejection boundary is unchanged and stays loud-not-fatal: it catches UNOWNED floats (teardown noise). The owned `done` channel is a different thing. Both files now state that line. Falsifiers: `runtimeFault.test.ts` (whole error logged, rites before abort, rites-throw still exits, clean close never trips it); the spine's fault-arm test, which on pre-fix code — the arm unwired — fails with `Test timed out in 5000ms`, i.e. the daemon serving on forever with a dead runtime.
srid
added a commit
that referenced
this pull request
Aug 5, 2026
…fact G7: the cache gap recurred at deploy #2 despite F5a's gate being green. The delta is not content, it is TIME — and a closure the deployed artifact never carries. Two receipts. First, `padi-agent` appears ZERO times in `.#default`'s closure, by construction: `padi-agent` CONTAINS `default`, so it cannot be contained by it. Deploying kolu installs the server plus the baked agent SOURCE and never the built agent CLOSURE; the binder learns that store path only at dial time and must then get the bits from a cache or build them. Second, the deploy beat its own warm job: `beb2f7418` was deployed at 21:20:43, while the nix-cache run for that exact sha was created 21:25:38 and finished 21:35:00. `nix flake update` costs seconds, a two-platform matrix build costs tens of minutes, so the operator can always outrun CI — and can deploy a branch with no open PR (nothing triggers the workflow) or a dirty tree CI will never see. On this host 5 of the 7 `padi-agent` closures ever provisioned are absent from the cache. No in-repo CI gate can produce a receipt about an artifact that exists only on the deploy host. So: * `ci/agent-substitutable` now takes a FLAKE ref (default `.`) and reads its inventory, cache list and sidecar name from the RESOLVED agent source rather than from cwd. The tree a remote resolves against is self-describing, so the script describes the artifact it was pointed at and has no opinion about where it ran. An operator can now ask the question BEFORE switching: `ci/agent-substitutable github:juspay/kolu/effect`. * `ci/agent-preflight` is the deploy-host half, and it does not ask — it PUTS the closure where provisioning reads from. `nix copy --to ssh-ng://` ships the LOCALLY-VALID closure (nixCopy.ts:493), so realising the exposed agents into the binder's own store makes remote source-realise impossible whatever any cache holds. Needs no credentials — attic is not even on the deploy host — and it is the only thing that works for an unwarmed rev or a local tree. It ends by running the gate as its own proof. Falsified: gate green against `github:juspay/kolu/master` (a remote artifact ref, all three found), preflight green end-to-end against the same, both shellcheck clean and bash-3.2 safe. The structural fix stays out of this lane and is cheap: `padi` is ALREADY in `.#default`'s closure, so having the deployed artifact carry its own agent closure costs little more than a symlink farm.
srid
added a commit
that referenced
this pull request
Aug 5, 2026
The deploy-#2 freeze produced ZERO log lines. Two rows of the mirror's fiber audit explain that exactly: - a member's upstream stream dying RESOLVED `done` clean, after one prose line on an un-leveled `log` callback — which kolu-server wires to `log.debug` and production filters away. A "clean" resolve claimed a healthy mirror that had permanently stopped mirroring that member (nothing re-subscribes it). - a per-key pump dying logged the same way and left the key silently unmirrored. Both are now LOUD and structured. `MirrorFault { label, err, scope }` is a second, separate channel from `log`'s chatter, carrying the error itself rather than a stringified message, threaded through the pump and the re-serve so kolu-server can wire it at ERROR level while keeping DEBUG for reconnect noise. A member fault also unwinds the mirror's remaining subscriptions so `done` settles PROMPTLY: previously one member could die while its siblings parked forever, so `done` never settled and no observer could act at all. Where this deviates from the brief, and why: the design said a rejecting `mirror.done` should propagate to the re-serve observer (default host → exit). The `reServeSurface` suite falsified that — a failing upstream stream IS the ordinary link-drop signal the pump's reconnect loop exists for, so propagating turns every ssh blip or padi restart into a kolu-server process exit. The pump therefore CATCHES it, names it a death (never the "mirror ended" line a clean close gets), and re-mirrors on the next spawn — which is both the reconnect path and the repair. The muteness is fixed without inventing a new way to fall over. The re-serve `.then` arm also stops guessing: a clean resolve means a clean end again, so it says that instead of "session destroyed". Falsifiers (pre-fix signatures): - `AssertionError: promise resolved "undefined" instead of rejecting` — the mute clean-resolve, at the mirror; - `AssertionError: expected [] to have a length of 1 but got +0` — the server-layer stampede: four host projections, one member's upstream dies mid-flight, and pre-fix NOTHING reaches the fault channel while the siblings carry on none the wiser.
srid
added a commit
that referenced
this pull request
Aug 5, 2026
…(G8) Deploy #2 incident #3: after a hard reload, three panes died at once with `Terminal attach failed unexpectedly: … snapshot answered 66x53, pane is now 65x53 — reopening`, the agents underneath still running. The toast was lying. The guard is correct and fires by design — a snapshot is bytes laid out for one grid, and painting it into a pane that has since resized wraps scrollback at the wrong width, damage no repaint undoes. Its documented repair, written at the throw site, was "fail the attempt, reset, reopen at the CURRENT grid". But the frame handler is `onItem`, and the loop ran it inside `Effect.sync`: a throw there is a DEFECT, and `Effect.retry` retries FAILURES only. The recovery path was unreachable from the site that promised it. The refusal is now a RETURNED `StaleSnapshotGrid`, so the channel rides in the type and the compiler routes it: `Effect.suspend` turns it into `Effect.fail`, which takes the same road as a mid-chain padi death — reset, 300ms, re-subscribe through a thunk that reads the live grid. A throw from either callback is still a defect, pinned by two tests; the thunk's measured-grid assert is unchanged. Also fixes the same shape one line away, found by this round's sweep: the loop called `onReattach()` bare inside `Effect.sync` under the retry. `xterm.reset()` on a disposed terminal throws, which would kill the loop having JUST wiped the pane, while the comment above it promises "fired ⇒ a re-subscribe follows". It is now contained and reported loudly, so the promise holds. The module header states the channel taxonomy once — defect / typed failure / classified clean end — and the rule that binds it: no message may claim a re-attach unless one follows. Both stale comment blocks and the message text are corrected. G8d pins the multi-client ping-pong (last-attach-wins on a shared pty) and, honestly, the gap it leaves: a purely FOREIGN resize is invisible to a pane today, so nothing tells the viewer why the content re-wrapped.
srid
added a commit
that referenced
this pull request
Aug 5, 2026
The mandate's falsifier — the inverse of deploy #2. Two nodes: a binder running the deployed generation, and `agenthost`, a bare sshd + nix box. Both force `nix.settings.substituters = []` and the script reads the effective value back before dialling, because a VM test is offline anyway and inheriting a property is not asserting it. A `hosts/add` then has to converge with the closure coming out of the binder's own store. Three assertions: the journal says `agent closure shipped`; a terminal really opens on the remote host's map key (the application contract, not a log line); and neither incident string from nixCopy.ts ever appears — checked again after convergence, so a late relapse cannot hide behind an early pass. It bites for free on the harness's own mechanics: a NixOS VM registers each node's nix db from its SYSTEM closure, so post-fix `padi-agent` is registered and `check-validity` says valid, while pre-fix the bits sit unregistered on the shared host store — production's "not valid locally", exactly. Two things are scaffolding and say so in the file. Evaluation inputs (npins pins) are registered on BOTH arms because I1 guarantees the closure, not the drv resolution, which stays lazy per F6. And the target sets `require-sigs = false`: measured in this nixpkgs, `nix copy --to ssh-ng://` does NOT credit the ssh user's `trusted-users` membership (the same copy over `ssh://` succeeds as root), and these paths are unsigned because the outer builder built them — signature policy is a different axis, and leaving it would make the test measure remote trust configuration instead of closure containment. Green run: 113s end to end, the terminal opening 7s after the ship. Refs #2101 (I1)
srid
added a commit
that referenced
this pull request
Aug 5, 2026
A woken tab sat on a stale card while its socket, its watchdog and its header dot were all healthy. Root cause, measured: Effect RPC registers a call's entry exactly once and never re-sends it across a re-dial, and an answer can only travel the socket its request went out on — so a run that ends with a swallowed `SocketOpenError` (the `retryTransientErrors` arm: a pre-open dial failure, or the ping timeout on a socket that died silently) orphans everything it carried, with no failure anywhere to retry on. Every fenced subscription in the tab parked forever. `websocketLink` now counts its own open EDGES — the wire epoch — off the existing status funnel, and each dispatched call records the epoch it binds to: the current one while `open`, otherwise the next (a call begun while the wire is down parks in the socket's write latch and belongs to the socket that flushes it, so the arriving open is its own and must not fail it). When the wire opens past a call's binding epoch, the link fails that call with an `RpcClientError` naming the cycle — streams and unaries alike. The fence's existing retry road does the rest; an unfenced caller gets a rejected promise instead of a dead one. Law 1 (a live socket closing, which does broadcast) coalesces structurally: the failed attempt's guard goes with the attempt, so one re-drive, never two. The link also records a 20-deep dial history — startedAt/openedAt/ endedAt/closeCode and a classification, including `"ended-without-open"`, the swallowed dial that until now left no trace on either side of the wire — exposed additively as `WebsocketLink.diagnostics`, not as a widening of `WatchableWire` (which consumers hand-build). Law 3 in `socketRedialLaws.test.ts` drives the field shape over a real served surface: socket #1 answers a subscribe, dies silently, the ping timeout ends its run swallowed, socket #2 opens. Pre-fix, verbatim: wire.status() : open socket #2 sent (subscribes) : [] stream fiber.pollUnsafe() : undefined in-flight unary promise : NOT SETTLED Post-fix: exactly one re-subscribe on #2, rendering state the server had held all along with no server-side action after the reopen. Plus the law-1 no-double-drive count, the cold-start non-regression, the unary arm, and a status pin asserting every swallowed attempt still publishes its `connecting → closed` pair — the observability the epoch rests on, now a BETA-ASSUMPTION row of its own.
srid
added a commit
that referenced
this pull request
Aug 7, 2026
…aemons on Layers (#2101) Kolu now runs end-to-end on **Effect 4.0 (beta.103)** — the wire, the schemas, the daemons, the HTTP app, the client's business logic, the reactive engine, and the CLIs. The oRPC wire is replaced by `effect/unstable/rpc` (flat, slash-tagged `RpcGroup`s over ndjson), every zod schema is Effect Schema, the surface framework's transports ride Effect sockets, the daemons boot through Layer graphs, and hono is gone — the entire HTTP app (static, PWA, artifact-sdk, health, preview, `serve()` itself, and the example servers) rides `effect/unstable/http`. Zero `zod`, `@orpc/*`, `partysocket`, `hono`, or `@preact/signals-core` dependencies remain. vazhi is the one deliberate non-Effect island (Ink rendering internals; nothing orchestral). The work ran as two planned campaigns plus a review round, all on this branch. **The campaign notes — plan of record, recon dossiers, the 26 adversarial review findings that reshaped the plan, and every per-stage report — are attached as a gist: https://gist.github.com/srid/76c5cfa5e52a21fc77196aa6237e09f4** (the `PLAN D<n>` / `review #<n>` / wave-label citations in source comments resolve there; the gist's index maps each citation form to its file). ## The wire epoch, and the hands-off upgrade The migration is a **wire epoch break**, handled as a declared flag day: `PTY_HOST_CONTRACT_VERSION` 6.0 → 7.0, `PADI_SURFACE_VERSION` 4.7 → 5.1. The daemon supervisor gained a third convergence observation — `unspeakable-protocol` — raised only for a **corroborated** peer (owned gate file, verified pid, re-attested immediately before any signal) that either sends undecodable frames or accepts and stays silent past 8s (the measured behavior of a previous-epoch daemon, pinned between Effect RPC's 5s ping and its ~10s connection kill). Disposition is each daemon's declared policy: **kaval recycles** (terminals don't survive a broken wire), **padi is taken over** — SIGTERM so its own shutdown runs (padi also gained the SIGTERM final-capture it never had), a 120s exit deadline, a 5s SIGKILL backstop. Deploying this build over a running production kolu is a plain service restart. Foreign socket-squatters keep the refuse protection untouched. Proven against a **real previous-release binary** in the upgrade-window e2e and in the six NixOS adoption VM tests (`nix/home/example/adoption/`: `adoption-adopt`, `-skew`, `-currency`, `-padi-upgrade`, `-upgrade`, `-upgrade-reboot`, all riding `ci::home-manager`). ## What the migration itself caught The typed edges and the harnesses found real bugs, several live in production paths: - Four client call sites crashed at the input-encode edge (`Cmd+T` with no cwd, every split, first scrollback backfill, session restore) — zod tolerated a present-but-`undefined` key; Effect Schema doesn't. A systematic audit of all 87 `optionalKey` conversion sites found 8 producer/schema mismatches (2 already live), each fixed at the right layer with byte fixtures. - One dead terminal tap's defect killed the **whole** padi connection (Effect RPC's `disableFatalDefects` default) — fixed centrally in `surfaceRpcServerLayer`, seam test falsified both ways. - A cell `get` served `Stream.concat(snapshot, bus)` — a zero-subscriber window that dropped writes landing on a reconnect edge. Fixed with the `subscribeBeforeSnapshot` discipline. - A session-restore lost-write race (terminals push vs. session cell echo under IO load) — the restore RPC now answers with the settled `activeTerminalId` and hydration waits on the answer; deterministic repro pinned it. - **Two silent-failure classes, structurally closed**: `await` on a now-Effect-returning call compiles and never dispatches (bit 11+ times across kolu and its consumers, including a test that had disabled the drain it exists to prove); and a `() => void` callback swallowing a built-but-never-run Effect (four tile buttons). Both are governance-banned with falsified scanners — including the alias/stored-promise dodges. ## Governance (machine-checked invariants this PR adds) - **Run edges**: every `Effect.run*` under `packages/` — production, examples, testlibs, the harness — is a named, justified allowlist row (109 sites in 54 files; test files are the awaited-face scanner's jurisdiction, stated in the header). Uncalled `Effect.run*` references (aliasing) are banned outright. - **Effect pin agreement**: all 68 effect-family pin spellings are discovered mechanically and must agree with the catalog — the 7 vendored `@kolu/surface*` manifests owe the literal (`catalog:` doesn't resolve for drishti/odu), everyone else owes `catalog:`. - **Beta-behavior assumptions**: the sites that depend on beta.103 *behavior* (not API) carry grep-able `BETA-ASSUMPTION(beta.103)` markers (eleven at HEAD, spanning the ping band, forkChild, optionalKey(Never), the three Atom batch-semantics laws, the RPC frame-cap close, and the socket-redial swallow and never-re-send pair behind the wire re-drive epoch and the attach first-frame deadline); a pin bump fails the gate until each is re-measured against its named measuring-law test. - **`Schema.optional` tolerance shims**: the four deliberate present-`undefined` tolerances are an allowlisted enumeration; a fifth can't appear silently. (`exactOptionalPropertyTypes` was probed: 940 errors across 35 packages, including npins-grafted code this repo can't edit — recorded in the gate's header as why the flag isn't the remedy.) - **`kolu-rpc` quarantine**: the harness-only wire CLI (the VM tests' probe after the HTTP `/rpc/*` arm was deleted) is scanner-pinned out of `agentToolPackages`, the home-manager module, and the app closures (its drv closure carries 0 references from anything a user installs). The widened scans immediately paid for themselves: they found a docs snippet that awaited a `kill()` Effect that never dispatched, and the typing-echo latency bench dead in three independent ways — now ported to the canonical client path and verified against a nix-built server with real PTY echoes. ## Evidence - Full local e2e: **509/509 scenarios, 4672/4672 steps, zero retries**. - Repo-wide typecheck (TS 7.0.2/tsgo), unit lanes, daemon lane (incl. the real previous-release upgrade-window run — now against **v2.2.0**, the last stable release before this switch), e2e-governance (summary line: `109 allowlisted Effect.run* edges in 54 files, 4 allowlisted Schema.optional shims in 3 files, effect@4.0.0-beta.103 agreed across 68 pin sites, 11 beta-behavior assumptions stamped`), biome `--error-on-warnings`, dev-smoke, website build: green. - The remote upgrade window is proven over **real ssh**: `just e2e-ssh-upgrade` plants a real previous-release padi daemon (v2.0.0 at the time of the run; the harness tracks the latest release tag) on a disposable box and asserts the current build produces **one takeover and one clean converge in a single campaign** (20.3s wall, vs. the incident's forever-loop). CI has no ssh lane, so this recipe is the enforced run — stated, not silent. - Two-platform CI on every push; final settle **`09700fa#1` green on both platforms, first attempt** — the e2e metrics comment tracks it. ## Consumer repos (merge order) - **drishti pair PR: srid/drishti#132** — full Effect adoption, CI green at each kolu pin. Break classes: `Surface.contract` → `{group, tagPrefix}`; async wire links `{dispatch, dispose[, wire]}`; Encoded-side client face types; `ORPCError` → `Schema.TaggedErrorClass` vocabulary; `ProcedureSpec.errors` → singular `error`; AbortSignal → `Stream`/fiber interruption; `serveOver*` take `{group, handlers}`; schemas Effect Schema throughout. - **ODU-IMPACT VERDICT: `breaks-at-bump` — adoption PR juspay/odu#74** (ledger odu#43 drained — plus one N3 line item: `serveOverUnixSocket` now takes a required `log`, at the same call site the adoption already rewrites; byte-compat proven against production data; odu's moved master merged back in at `74f922e` — its new `odu wait`/`rerun` CLI re-ported onto the Effect surface and #78's fresh zod import converted, **16/16 required contexts green** by odu's own runner, `MERGEABLE`). - **Order**: **juspay/osfacts#5 merges first** (this PR pins its branch head; re-pin the npins `osfacts` entry to the merge commit before merging here), **then this PR** — **odu#74 must not merge before it** — **then the consumer PRs** (drishti#132, odu#74), each re-pinning to the kolu merge commit and re-confirming green. ## Review rounds - **[Round 1 checklist](#2101 (comment) — all 13 items addressed; the comment carries the per-item disposition. Highlights: the campaign notes are attached (A1, the gist above); the pre-1.30 `activityAlerts` ladder bug is **fixed** with a chained-ladder test (A3); the six adoption VM tests are named above (A4, they were in-tree all along); `kolu-rpc` is ratified *and* scanner-quarantined (A5); the disclosed `sessionRestore` and `pumpRemoteSurface` holes are fixed with falsified tests (C1, C2). - **[Round 2 follow-up](#2101 (comment) — F1: both pair PRs pinned to this PR's final HEAD with receipts; F2: the restore seed's wait is **bounded** (`41d517754`) — a 10s deadline that provably cannot fire on a healthy link (the written argument sits at the site), converting the one reachable non-arrival (a stale answer after a daemon recycle) into a conservative seed plus a loud report, with the non-arrival case driven in the seam test. - **Round 4, effect 4.0.0-beta.103** — the first pin bump, taken on this branch by request: all 68 pin sites moved together (osfacts upstream first), the three `BETA-ASSUMPTION` markers re-measured (ping band 5014ms/10019ms — unchanged; `forkChild` and `optionalKey(Never)` unchanged), the [reviewer's six-surface map](#2101 (comment)) walked explicitly (all clean; RPC frame cap 16MiB vs 0.40MiB measured worst-case real frame; the five JSON-schema shim patches re-derived individually), zero removed-API hits, CI green first attempt both platforms. - **[Round 5, the production incident](#2101 (comment) — deploying an earlier head broke every remote host in a permanent connect loop: the wire-epoch machinery existed only on the local arm. Fixed at the framework level so the class is unrepresentable: a stdio wire link now **requires an un-forgeable readiness proof** (WeakSet-branded, minted only by consuming the peer's pre-splice readiness banner — no pinger before proven epoch), the `padi --stdio` / `kaval --stdio` fronts run the **full convergence kit on the remote box before relaying a byte** (unspeakable classification, gate corroboration, takeover via SIGTERM — the same kit the local arm runs), and a gate refusal is a typed `"remote"` verdict that reaches a terminal `failed` within the existing 5-attempt budget and renders as "previous protocol epoch" on the host map instead of an eternal spinner. Proven by a real-ssh upgrade-window e2e (previous release resident → one takeover, one converge — log attached to the gist) and a permanent inverted falsifier reproducing the incident's infinite loop. Secondary finding fixed along the way: the nix-cache workflow was warming the PR **merge ref** — a tree nobody deploys — so deployed closures were never on the cache; it now checks out the PR head and re-queries the cache (over plain HTTP) for every exposed agent attr after the push. - **[Round 6, deploy #2's four incidents](#2101 (comment) ([G5–G7](#2101 (comment)), [G8](#2101 (comment)), [G9](#2101 (comment))) — the second deploy surfaced four incident classes, all closed with falsified tests: **(1) the half-dead daemon** — a transient kaval-probe timeout at boot faulted padi's whole surface runtime into a zombie; poll reads are now cell-local at every tick (the same failure was fatal at T+0 and benign a second later — that timing dependence was the defect), and a genuine runtime fault now **exits the daemon** through the shutdown machinery (capture, gate release, supervisor respawn) instead of the #1792 log-and-continue; **(2) the mute server freeze** — projection-layer faults resolved *clean* at DEBUG; they are now typed, loud, and prompt, every kolu-supplied callback on the engine's writer stack is containment-bracketed, and Atom's beta.103 batch semantics carry three measured `BETA-ASSUMPTION` markers (the severed-edge hypothesis was **refuted by measurement** — pinned as a law); **(3) frozen panes** — attach streams manufactured clean ends under load (now re-opened server-side for a live PTY, re-attached client-side), and the port had flipped a documented-recoverable resize race into the defect channel (now a typed failure the loop retries; the channel taxonomy is written law, and the same sweep fixed the framework twins on `fenceStream`/`websocketLink`); **(4) the wire-killing upload** — a >16 MiB frame closed the whole multiplexed socket; uploads ride 3 MiB chunks, the cap is an owned, marked constant at every serialization site, and an oversized frame is refused client-side before send. Plus the deploy-artifact cache gap root-caused structurally (`ci/agent-substitutable <flake-ref>` + `ci/agent-preflight` on the deploy host) and a latent HTTP 500 (a `Respondable` response answered as an unhandled fault) found by the VM lane under saturation. **[Verification closed the docket](#2101 (comment) with one non-blocking residual, since landed: N1 — `connectPublishEffect`'s publish half now routes through the one shared containment implementation (`containThrow`) instead of hand-rolling it. Alongside it, `just ci::protect` now **enrols the nix-cache workflow's two `build-and-push` contexts in branch protection** (derived from the workflow file with a loud drift guard, re-appended after every `odu protect` since odu replaces the list) — the cache-warm guard that G7 showed is deploy-critical is now a required check, applied live (48 required contexts on `master`). - **[Round 7, the lid-close field test](#2101 (comment) — the wake machinery held (typed mirror death within ~15s, honest bounded probing, hands-off reconvergence adopting the surviving daemon), and the three residual defects from the wake window are closed: **(1)** a grid publish to a host the client *knows* is down no longer toasts "failed unexpectedly" — the publish gates on the client's own host-map state, and the same refusal from a host believed up stays loud (falsified both ways); **(2)** a waking client now fast-forwards every down host's scheduled retry via a new narrow `Session.nudge()` fired on websocket accept — the same attempt the backoff had scheduled, budget and terminal-verdict semantics provably unchanged (`recheck()` was measured to be the wrong verb: it refills the give-up budget); **(3)** an attach stream that opens and never delivers its first frame now hits a 10s first-frame deadline into the existing retry channel (budget 1, loud verdict on repeat) — closing the one channel the loop's taxonomy didn't cover (transport failures, clean ends, stale grids, and now *no end at all*). The mandated socket-cycle hypothesis was tested and did **not** confirm (a force-cycled socket already fails registered entries — pinned as law); the genuine hole is Effect RPC's `retryTransientErrors` swallowing `SocketOpenError` re-dials under parked subscriptions, now the ninth measured `BETA-ASSUMPTION`. - **[Round 8, I1 — the artifact carries its agents](#2101 (comment) — maintainer's call executed: a deployed kolu now guarantees every agent closure it can provision is present in the deploying host's store, by construction. The `padi-agent ⊃ default` circularity dissolves at the deployment layer — the home-manager **generation** references both without a cycle, via a new *required* `services.kolu.agentPackages` option (`nonEmptyListOf` — an omitting or empty-list consumer fails at eval, no degrade knob) defaulted by the flake wrapper from `nix/agent-packages.json`'s `expose` list and anchored on both supervisors with zero PATH pollution. Measured cost: **+1,760 bytes** (one store path) over `default`'s 740.84 MiB closure; +0.0001% on the end-to-end generation. Proven two ways in the `ci::home-manager` lane: a closure-containment check (red pre-fix at `MISSING padi-agent`), and a two-node **offline-provision VM test** — substituters forced empty and asserted, a real ssh connect ships the closure and creates a terminal with zero cache/source-realise narration, where the pre-fix run reproduces deploy #2's exact `no local copy of the agent to ship … realising from source` path down to the target compiling `padi-agent.drv`. The G7 scripts are re-scoped to one narrative (the module guarantees by construction; `agent-preflight`/`agent-substitutable` cover the raw-install and cache-warm flows outside it) and the user docs' cache-conditional provisioning language is corrected. - **[Round 9, J+K — the law-2 park and the attach-path audit](#2101 (comment) ([K audit](#2101 (comment))) — a second field incident (wake at 13:11: healthy wire, watchdog satisfied, every subscription in the tab parked on pre-sleep state) plus a three-audit deep review of the attach path, closed as one round: **(J1)** `websocketLink` now counts open edges and fails, itself, every stream *and* in-flight unary a re-dial cycle orphaned — including cycles whose intermediate failure Effect RPC swallows (`retryTransientErrors` + never-re-send, now two cross-referenced `BETA-ASSUMPTION` rows, census 9 → 11) — so the fence re-drives the whole tab with no clock and no per-subscription machinery; **(K1)** the first-frame deadline was measured to be a hard ceiling (a re-attach cancels the in-flight snapshot), re-derived to 45s above every structural repair, budgets refill per-episode, and no verdict executes a pane — exhaustion is one loud toast then a 30s cadence; **(K2)** the abort-during-reopen leak triad closed against kaval's real fan-out (including the WHATWG already-aborted-signal hole, found twice); **(K3)** the two silent unbounded lanes keep their unbounded-by-design arguments and lose their silence (derived-N structured warn server-side, derived-N verdict client-side); **(K4)** a grid suppressed while the host was down is restated once on the connected flip; **(K5–K7)** the fourth reopen lane joins the taxonomy, the inter-check PTY exit throws tagged, and the tombstone-evicted exit fails loud instead of fabricating `0` (with one genuinely-reachable caller identified and handled). **(J2)** Diagnostic Info now carries a copy-pasteable plain-text snapshot that proves a wire incident from the browser alone: dial history including the previously-invisible swallowed dials, a per-subscription liveness table with a parked verdict, host entries with client-stamped freshness, heartbeat verdicts — built lazily from client-held state only, so it works exactly when the wire is lying. - **[Round 10, M — the give-up budget counted the failures it promised to ignore](#2101 (comment) ([disposition](#2101 (comment))) — a first-overnight-sleep field incident: ~18 "host unreachable" attempts plus ONE dark-wake remote failure went instantly terminal, because the day-one shared `consecutiveFailures` counter incremented for both causes while the ceiling gate read only `remote` — a master-born latent defect this PR's own (correct) banner gate made reachable for the first time. Killed as a class: `@kolu/surface/failure-ledger` is a new leaf primitive where cross-class counting is **unrepresentable** (per-class runs, verdicts computed only from a class's own run, the interleaving rule declared as data, `attempts()` exposed strictly as display/pacing tier), with the anti-conflation law — the one no test had ever pinned — proven three ways in the framework. The session migrates to the spec `network: unbounded, resets remote` (an unreachable gap means the host *went away*; the next remote blip is fresh evidence, not accumulation), the give-up message derives from the verdict so it can only ever name the true remote run, three lying doc sites are corrected, `makeStepBudget` is ratified hand-rolled (single-class — the disease can't exist there), and the field shape is pinned by four seam falsifiers whose pre-fix red reproduced the incident's exact lying give-up line byte-for-byte. - **[Round 11, N — a comatose kaval becomes survivable by design](#2101 (comment) — a field incident (kaval alive but comatose after macOS sleep: accepts connections, answers nothing, zero error lines; padi diagnosed it in 10s and was architecturally forbidden from treating; the host dot stayed green) closed as four items: **(N1)** padi's probe verdicts feed an M1 failure ledger (`wedged`/`unreachable` at ceiling 3 ≈ 30s of coma, the auto-repair itself ledger-bounded at 3 before the card returns) and exhaustion runs *exactly what the "Restart kaval" button runs* — one shared routine, two triggers — proven by a SIGSTOP-coma falsifier against a real kaval (pre-fix red: "padi never repaired the comatose kaval", post-fix hands-off repair in 168.8s); two *pre-existing* unbounded waits reachable from the button were found and closed on the way, including a drain that parked the recycle forever on the very fault it was repairing (and the first fix attempt via fiber timeout provably couldn't work — the effect was uninterruptible; the reasoning is written at the site); **(N2)** kaval self-dials its own socket every 10s and three consecutive failures exit through the existing G2 fault arm — comatose-forever becomes dead-loudly, which everything already handles; suspension is detected (a tick firing two cadences late resets the budget), and the raw-timer-vs-Effect-Clock ruling is written at the site citing heap-diag's precedent; **(N3)** the listener telemetry the Effect port silenced is restored as a confirmed regression fix (pre-fix red: the logger was never called at all), with the `log` parameter deliberately *required*; **(N4)** the host dot composes the daemon chain — reachable-host-dead-kaval renders amber "kaval down" with the daemon's verdict, reusing the exact cell the kaval-down card already reads (no new wire field). The kernel-level double-bind itself stays out of scope as mandated: unprovable from this repo, and survivable-by-design once N1/N2 hold. - **[Round 3, the osfacts follow-up campaign](#2101 (comment) — delivered as separate PRs, per its own instruction not to reopen this one: juspay/osfacts#5 (the client ported to Effect), the kolu adoption (opened as #2103, then **folded into this branch** by maintainer decision — flips the supervisor's last two Promise seams to Effect, retires this PR's "osfacts is uneditable" premise from the governance gates), and the drishti adaptation (opened as srid/drishti#133, likewise folded into drishti#132). Dispositions with receipts on each. ## Docs & changelog, against the v2.2.0 baseline v2.2.0 was released mid-flight as the deliberate **last stable release before this switch**, and every user-facing surface was re-baselined against it: - **Changelog** (`unreleased.mdx`) rewritten **release-relative** — what a user upgrading *from 2.2.0* experiences, as one squashed release, not a diary of this branch's iterations. Every entry was then adversarially fact-checked claim-by-claim against `origin/master` (nine claims corrected, one wrongly-deleted entry restored) and audited against `.claude/rules/changelog.md` (kinds are release-relative; branch-only fixes don't narrate as user-facing leaks). A final entry-by-entry trial against the v2.2.0 tree itself asked, for each `fixed`, *can a 2.2.0 user actually hit this?* — it re-kinded one entry, deleted one whose failure mode was measured to be this branch's own engine (folded into the Surface entry as a property of the new engine), and corrected four more claims down to what the tag's code supports. - **Docs pages** synced where the changelog exposed staleness: the host-down card table's new "previous protocol epoch" row, the flag-day exception on the sessions and troubleshooting update paths, the blank-pane verdict on the tiles page, the Attention-alerts master toggle on notifications. - **Reference pages** carry the campaign's new public API (`Session.nudge()`, `@kolu/surface/subscriptions`, `StreamFenceOptions.label`, `WebsocketLink.diagnostics`, …) and all six touched package READMEs were brought to the Effect-era surface (kaval's `PtyHost` face was still fully pre-Effect prose). - The release also moved the previous-release harness's target, catching two **test-only** gaps: the gate assertion now asserts the #2011 pid-first law instead of one release's exact bytes, and the takeover arm now composes the production identity reads instead of a unit-test fake — so the e2e proves the exact 2.2.0 → this-release crossing every user will make (kaval recycled, padi taken over, against the real previous binary). The daemon machinery itself needed no change. ## Known remainders - `.claude/skills/nix-typescript` (vendored from juspay/skills) names a nonexistent `nix/modules/typescript.nix` — upstream doc fix, tracked outside this PR. - A pre-existing medium CodeQL alert on `packages/padi/src/ports/scan.live.test.ts` (named exception; untouched code). ### Try it locally ```sh nix run github:juspay/kolu/effect ``` _Generated by [`/be`](https://github.com/srid/agency) on Claude Code (model `claude-fable-5`)._
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.