← the Atlas

Bug — remote host: kaval didn't start, Restart → Internal server error (contract skew)

Bugs·seedling·implemented·

A newer local kolu binds a stale remote host whose kaval speaks an older pty-host contract. The skew is well-typed at its source but flattens to a generic error, the Restart button is a dead end, and the remote log bridge crashes per line. Fix in two tiers — the instances, and the surface-framework fortification that makes the class inexpressible.

What was seen

Remote-hosting a newer local kolu (pureintent) to sincereintent — a 104-day-uptime box carrying kaval daemons up to 21 days old — produced a dead workspace, and every recovery attempt re-surfaced the same failure. Full-res: imgur album.

kaval didn't start card

Clicking Restart kaval → a confirm → then a generic error toast:

Restart kaval confirm dialog Couldn't restart kaval: Internal server error toast

Root cause — a pty-host contract skew, well-typed then squandered

The remote padi couldn’t connect to its kaval:

DaemonContractSkewError: pty-host contract skew: kaval speaks 5.0, server needs 5.2
  at connectKaval        packages/padi/src/ptyHost/connect.ts:97
  at recycleKaval        packages/padi/src/servePadi.ts:446

The local kolu needs pty-host contract 5.2; the kaval on the box speaks 5.0. sincereintent was a daemon graveyard — 65 orphan /tmp/padi-* state-roots and 21-day-old kavals from weeks of CI at many kolu versions — so the binder adopted an ancient, wrong-contract kaval. (The box hygiene that was the day’s darwin-flake substrate, biting the remote-host feature directly; the box wants a reboot regardless.)

The three defects (a-f-p + perfection, 12 findings, 0 refuted)

A · The skew flattens to INTERNAL_SERVER_ERROR. surface-map’s unary handler awaits the leaf with no translation (server.ts:534) — correctly, a keyed proxy can’t know a handler’s domain — so a plain DaemonContractSkewError (not an ORPCError) is serialized by oRPC as a generic error and the toast shows “Internal server error.” The precise cause is lost between padi’s throw and the browser.P5 / end-to-end: the actionability guarantee was placed at surface-map, a layer that cannot see what the error means. It belongs at padi’s recycleKaval handler, the knowing endpoint.

B · Restart kaval is a dead end. recycleKaval is kill-then-respawn of the same on-disk binary (a build constant; no re-fetch), then the same connectKaval handshake — so it re-throws the identical skew forever. On a remote host the kaval binary comes from padi’s own nix closure, so respawn can never yield a compatible one. The user is offered a recovery action the server has already proven cannot succeed.

C · The remote log bridge crashes per line. Not an old-log-shape problem — a plain unbound pino method call: (severity === "error" ? log.error : … : log.info)({host, line}, "…") extracts a bare function reference, so pino runs with this === undefined and throws Cannot read properties of undefined (reading 'Symbol(pino.msgPrefix)') on every line. It exists in both remotePadiBinding.ts:697 and padiBinding.ts:605; a try/catch one layer up (session.ts:562) swallows it, spams stderr per line, and drops every remote log. The version skew merely exercised this remote path hard for the first time.

The fix — two tiers

Tier 1 — the instances

Tier 2 — fortify the surface framework (make the class inexpressible)

The three defects are one class each; the framework moves that kill the class:

Phase tree (the full arc, final)

Phase Outcome
This note — root cause + two-tier fix design plan of record
#1876 — the skew card: name it, show both versions, offer the working recovery shipped
#1881 — isolation-default (per-client private estates) abandoned — orphaned every existing remote terminal on deploy
#1883 — renew re-point + honest client toast closed, zero product code — the product’s incompatible → renewDaemon was already correct; every client-side toast shape was falsified by the gauntlet
one-time stray reap (25494) ✓ done — the field box converges via existing code
#1887 — gate-less socket-squatter recovery (three-attestation identity) shipped — kolu heals its own rendezvous; the class fix
issue #1885 — server-authored honest “didn’t converge” toast ○ filed, awaiting a lane

Field addendum — the recovery was correct but insufficient (RENEW1, #1881)

Deployed and driven against the real graveyard box, the renew recovery converged the binaries but not the box: a competing old padi supervisor re-spawned its 5.0 kaval at the shared rendezvous after every recycle — mutual recycle, forever (two padis at one rendezvous each classify the other’s kaval as skew and recycle it — the livelock, endpoint.ts adoptAt skew arm). The card’s paths are individually correct; nothing arbitrated estate ownership. Two graveyard strata clarified the field: the digest-keyed kaval scheme is only ~14 days old (W2.2 cutover 590f49e48), so the 21–26-day survivors are pre-W2.2 port-keyed (a separate, irrelevant estate) — the live fight was the 5–6-day post-W2.2 padis sharing the default state-root digest.

The follow-up fix (#1881) is isolation, not arbitration — eviction was adversarially falsified (a live client re-dials and re-provisions its old closure; an old binary can’t honor a gate that postdates it): each kolu client gets a private remote estate keyed by a persisted client UUID (--client-id; the host spells the path). The fix kills nothing: a competitor’s 5.0 kaval simply lives in an estate the browser’s binder never touches.

A seamless auto-migration was designed and then dropped, in the gauntlet, for cause. The plan was a one-time --legacy-state-root adopt bridge (modeled on the W2.2 one) so a client’s existing terminals moved to its new isolated estate instead of being left behind. The codex review proved it re-created the very disease: two isolated clients (distinct UUIDs, distinct isolated gates) could both adopt the same live default estate — arbitrated by nothing, because the gate each holds is its own estate’s, not a shared migration claim — resurrecting the livelock; and it wasn’t even one-time (every no-override dial re-probed). A migration that can bring back the disease is worse than a loud one-time cutover, so #1881 ships clean isolation with an honest cutover: the pre-isolation default-estate daemons are named plainly as abandoned-running (their terminals are not migrated; the daemons sit idle until reboot or manual cleanup), and the binder emits a loud warning when it boots isolated and detects a live default-estate daemon it is leaving behind — never adopting it, never killing it.

Seamless migration done right is a recorded follow-up, two candidate shapes: (A) an atomic default-estate migration-claim (one acquirePidGate in the default estate so exactly one client adopts, once, plus a persisted cutover-done marker so reboots never re-probe — needs a two-client race test); or (B, recommended) a user-mediated explicit adopt affordance — one user action gives natural exclusivity with no race and no new shared rendezvous, and it matches the design’s own doctrine that sharing is an explicit choice, not an accident.

Lens artifact: workflow wf_97914292-c4b — 15 agents, a-f-p (P4/P5/P3) + perfection over the fix space, every recommendation adversarially verified against the real code (0 refuted).