HOMELAB-1881: fix(nats-intercom): return Text components from TUI renderers #64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1881-intercom-renderers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
nats-intercom'srenderCall/renderResultreturned raw strings. pi'sToolDefinitionrenderer contract (since 0.63.0; enforced fatally in 0.87.1) requires returning a Component — 0.87.1 wraps renderer output in aMouseRegionwhoserender()callschild.render(width)unguarded.Impact
Any interactive session that executed the
intercomtool crashed the entire pi process with:Three crashes recorded in
~/.pi/agent/crashes.json(2026-10-02 04:44, 2026-10-03 04:47 + 04:49) — including a session that resumed and re-crashed on the sameintercom {action:"list"}batch. HOMELAB-1881.Fix
Wrap returns in
new Text(...)from@earendil-works/pi-tui— the same pattern thetodoandpi-swarm-suiterenderers use.@earendil-works/pi-tui@0.87.1added as devDependency (matches pi's bundled copy) sotsc --noEmitresolves types.Verification
tsc --noEmitcleanintercomtool renders in interactive TUI without crashing pi; no new entries incrashes.jsonLanding review — verdict: CLEAN
Reviewed in a fresh context against primary sources only: the PR diff at head
dfb147980f57f86822d2bfa57a27b8c16ae0a2fe, pi 0.87.1's installed renderer contract,~/.pi/agent/crashes.json, and the sibling renderers intodo/andpi-swarm-suite/. No blockers. 2 non-blocking concerns below.1. Correctness vs the crash signature — verified, not just read
Every renderer return path is now a
Component; there are exactly three and all three were exercised:renderCall(single return)new Text(text, 0, 0)renderResultisPartialearly returnnew Text(...)renderResultfinal returnnew Text(...)No unwrapped path remains (no
return text;in either hook), and repo-widerenderCall(/renderResult(exists in only three files — this one pluspi-swarm-suite/src/core/index.tsandtodo/todo.ts, which already returnnew Text(...). No other extension carries the same latent defect.I reproduced the contract both ways in a throwaway probe (fake
picapturingregisterTool, wrapping output exactly as pi does):main: all 5 probe cases return raw strings, andnew MouseRegion("a raw string").render(80)throwsTypeError: this.child.render is not a function— byte-identical to the message in all threecrashes.jsonentries (pi 0.87.1,MouseRegion.render→ToolExecutionComponent.render). The crash attribution is confirmed, not assumed.Textinstances and render cleanly throughMouseRegion.render(80), including theisPartialearly-return path that a naive fix would miss.2. Crash evidence claim — accurate
All three
crashes.jsonentries arethis.child.render is not a functionon version0.87.1, timestamped 2026-10-02T04:44:57Z and 2026-10-03T04:47:57Z / 04:49:37Z — matching the PR description's three crashes. The 04:49 session's last recorded line is an assistant message at 04:49:37.917Z, i.e. 47 ms before the 04:49:37.964Z crash, consistent with the "resumed and re-crashed" claim.3. Typecheck / tests — reproduced locally
npm ci+npm run typecheck(tsc --noEmit) clean andnpm test48/48 passing at the head sha in a scratch clone, so the added devDependency genuinely resolvesText's types. The claim "@earendil-works/pi-tui@0.87.1 matches pi's bundled copy" checks out:pi-coding-agent0.87.1 declares"@earendil-works/pi-tui": "^0.87.1"and carries it nested in its ownnode_modules; the new lock entry resolves0.87.1.4. Claim honesty / scope — accurate
Three files changed, all in-scope (
index.ts,package.json,package-lock.json). The lockfile additionally drops stalenats/nkeys.js/tweetnaclentries; that is npm reconciling a lockfile that was already inconsistent withpackage.json(nonatsdependency there since the@nats-io/*migration), so it is correct hygiene rather than scope creep.npm ci(which hard-fails on lock/package mismatch) succeeds both locally and in CI, proving the lock is coherent.Concerns (non-blocking)
Textat runtime declare@earendil-works/pi-tuiindependencies—todo/package.json(^0.80.2) andpi-swarm-suite/package.json(^0.80.2) — whereas this PR places it indevDependencies. Runtime is unaffected either way: pi's extension loader resolves@earendil-works/pi-tuithrough its virtual-module map to the bundled copy (dist/core/extensions/virtual-modules.js), so the package is only needed fortsc.pi's owndocs/packages.mdrecommendspeerDependencies: "*"for supplied Pi packages. Worth aligning in a follow-up so a futurenpm ci --omit=devor virtual-map change cannot silently break the import.pi, then assert each hook's result isinstanceof Text(and optionally survives aMouseRegionwrap). Suggested follow-up, not a merge gate for a 3-line fix.CI
Run #124 on head
dfb147980f— pass, all 13 jobs green (nats-intercomtypecheck + vitest, pluspr-titleformat gate).Landing (squash merge + rollout + smoke test) proceeding on this verdict.