Summary
node-pty is a declared dependency but never lands on a fresh npm install -g comisai, because it's a native module deliberately left out of the self-contained bundle. The install-time native build is the gap, and when it doesn't happen the failure is silent: the terminal driver quietly degrades to a pipe backend and the interactive Claude Code TUI breaks, while headless claude -p keeps working.
Details
1. It's a native module the tarball can't self-contain
comisai ships as a self-contained tarball built from bundledDependencies (@comis/* are private:true + bundled; deps exact-pinned). node-pty@1.1.0 is a real, exact-pinned dependency of both @comis/skills (packages/skills/package.json) and the comis umbrella (packages/comis/package.json).
But node-pty compiles an arch-specific .node binary. You can't drop one prebuilt binary into a cross-arch (linux/amd64 + linux/arm64) tarball, so it's omitted from comisai's bundledDependencies array — even though its own pure-JS helper deps (bindings, file-uri-to-path) are force-bundled by prepack.js. The tarball therefore ships node-pty's helpers but not node-pty itself, leaving it to be fetched + built (node-gyp) at npm install -g comisai time.
2. The install-time build is the fragile step
That post-install native build is exactly what didn't complete on a real deployment (node-pty entirely absent). Same class as the recurring "every install needs a native-dep repair".
3. The failure is silent
When loadPty() throws (MODULE_NOT_FOUND), the terminal worker doesn't hard-fail — it flips to backend:"degraded" (pipe) with only a WARN and carries on:
- Interactive
claude (TUI) gets pipes, not a TTY → exits immediately before the prompt.
- Headless
claude -p needs no TTY → unaffected, so nothing looks broken.
- Nothing surfaces in
comis fleet / config_posture → discoverable only by driving a session that dies for no obvious reason.
Observed repair
cd <prefix>/lib/node_modules/comisai && npm install node-pty@1.1.0 --no-save
node -e 'require("node-pty")' # loads → backend flips from degraded to pty
(This also prunes ~100 extraneous deps but leaves the @comis/* set + better-sqlite3 intact; daemon boots clean after.)
Suggested fixes (priority order)
A. Installer builds + verifies node-pty, fails loud (highest value, lowest risk)
website/public/install.sh already has a "retry npm install after build-tools setup" path. Extend it to explicitly build and verify node-pty (node -e 'require("node-pty")' from the global comisai dir) and error out with an actionable message ("node-pty native build failed — install python3/make/gcc and re-run") instead of degrading silently.
B. Surface the degrade in observability (cheap, closes the diagnosability gap)
When the terminal driver drops to the pipe backend, raise a config_posture/fleet finding (like the other posture signals) so "node-pty missing → TUI broken" is diagnosable in one comis fleet call. Load-bearing evidence should not be WARN-only / invisible at the default log level.
C. Bundle a prebuilt node-pty per arch (cleanest, most work)
The release builds on native amd64/arm64 runners, so prepack.js could build node-pty and include the correct binary per image, making the tarball truly self-contained and removing the install-time build entirely — at the cost of per-arch complexity in the release pipeline.
Recommendation: ship A + B now (self-healing + diagnosable under the current model); consider C later to eliminate the install-time build.
Verification (what "fixed" looks like)
node -e 'require(".../comisai/node_modules/node-pty")' loads without error.
- Terminal worker reports
backend:"pty" (not degraded).
- Interactive
claude (no -p) stays alive at the ❯ prompt and is drivable via terminal_session_*.
Summary
node-ptyis a declared dependency but never lands on a freshnpm install -g comisai, because it's a native module deliberately left out of the self-contained bundle. The install-time native build is the gap, and when it doesn't happen the failure is silent: the terminal driver quietly degrades to a pipe backend and the interactive Claude Code TUI breaks, while headlessclaude -pkeeps working.Details
1. It's a native module the tarball can't self-contain
comisai ships as a self-contained tarball built from
bundledDependencies(@comis/*areprivate:true+ bundled; deps exact-pinned).node-pty@1.1.0is a real, exact-pinned dependency of both@comis/skills(packages/skills/package.json) and thecomisumbrella (packages/comis/package.json).But
node-ptycompiles an arch-specific.nodebinary. You can't drop one prebuilt binary into a cross-arch (linux/amd64+linux/arm64) tarball, so it's omitted from comisai'sbundledDependenciesarray — even though its own pure-JS helper deps (bindings,file-uri-to-path) are force-bundled byprepack.js. The tarball therefore ships node-pty's helpers but not node-pty itself, leaving it to be fetched + built (node-gyp) atnpm install -g comisaitime.2. The install-time build is the fragile step
That post-install native build is exactly what didn't complete on a real deployment (node-pty entirely absent). Same class as the recurring "every install needs a native-dep repair".
3. The failure is silent
When
loadPty()throws (MODULE_NOT_FOUND), the terminal worker doesn't hard-fail — it flips tobackend:"degraded"(pipe) with only a WARN and carries on:claude(TUI) gets pipes, not a TTY → exits immediately before the prompt.claude -pneeds no TTY → unaffected, so nothing looks broken.comis fleet/config_posture→ discoverable only by driving a session that dies for no obvious reason.Observed repair
(This also prunes ~100 extraneous deps but leaves the
@comis/*set +better-sqlite3intact; daemon boots clean after.)Suggested fixes (priority order)
A. Installer builds + verifies node-pty, fails loud (highest value, lowest risk)
website/public/install.shalready has a "retry npm install after build-tools setup" path. Extend it to explicitly build and verify node-pty (node -e 'require("node-pty")'from the global comisai dir) and error out with an actionable message ("node-pty native build failed — install python3/make/gcc and re-run") instead of degrading silently.B. Surface the degrade in observability (cheap, closes the diagnosability gap)
When the terminal driver drops to the pipe backend, raise a
config_posture/fleetfinding (like the other posture signals) so "node-pty missing → TUI broken" is diagnosable in onecomis fleetcall. Load-bearing evidence should not be WARN-only / invisible at the default log level.C. Bundle a prebuilt node-pty per arch (cleanest, most work)
The release builds on native
amd64/arm64runners, soprepack.jscould build node-pty and include the correct binary per image, making the tarball truly self-contained and removing the install-time build entirely — at the cost of per-arch complexity in the release pipeline.Recommendation: ship A + B now (self-healing + diagnosable under the current model); consider C later to eliminate the install-time build.
Verification (what "fixed" looks like)
node -e 'require(".../comisai/node_modules/node-pty")'loads without error.backend:"pty"(notdegraded).claude(no-p) stays alive at the❯prompt and is drivable viaterminal_session_*.