Skip to content

fix(runtime): let the pi process verdict win over a failed write - #224

Merged
eddiearc merged 1 commit into
eddiearc:mainfrom
itswl:fix/pi-rpc-write-failure-masking
Sep 14, 2026
Merged

eddiearc merged 1 commit into
eddiearc:mainfrom
itswl:fix/pi-rpc-write-failure-masking

Conversation

@itswl

@itswl itswl commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

What

When a pi spawn fails (missing working directory, ENOENT on the binary), the client reported whichever error landed first — the request's write check (stdin is unavailable) or the process verdict (spawn … ENOENT) — and fail() keeps the first error. The write path often won, so the real cause was lost: users saw Pi RPC get_available_models write failed: stdin is unavailable, or the classifier's pi is not installed; neither points at the spawn failure. Filed as #223.

Change

  • src/runtime/pi-rpc-client.ts: a failed write now defers to the process verdict when the child is already gone (pid === undefined, or an exit code/signal is recorded). The write error is held as a bounded (500 ms) fallback so the error / exit handler — which carries the real reason — can fail the client first. A live process with a broken stdin still fails immediately with the write error, and the wait is always bounded so a request can never hang on a verdict that never arrives.
  • PiRpcProcess gains optional pid / exitCode / signalCode (additive; existing doubles and the ProcessLike cast in runtime-adapters stay assignable).
  • test/unit/runtime/pi-rpc-client.test.mjs: a dead-spawn double (destroyed stdin, no pid) proves the process verdict wins and stdin is unavailable never masks it; a second case proves the fallback stays bounded (< 2 s) when no verdict ever arrives.

Validation

  • Direct client repro with the real binary (pi 0.85.1, nonexistent cwd), after the change:
raw ERR in 7ms: Pi RPC process failed: spawn /home/imwl/.npm-global/bin/pi ENOENT

Before, the same setup surfaced stdin is unavailable (or, timing-dependent, the classifier's pi is not installed).

  • bun test test/unit/runtime/pi-rpc-client.test.mjs → 11 pass / 0 fail
  • bun test test/unit/runtime/ → 233 pass / 0 fail
  • bun run build ✅ · bun run typecheck ✅ · bun run licenses:check ✅ · bun run publication:check:tree
  • bun run test:unit → 897 pass / 1 skip / 1 fail. The single failure is the pre-existing environmental three-Agent live acceptance is opt-in, hermetic by default, and fixture-verifiable, which also fails on unmodified main here.

How this composes with the rest of the cluster

This fixes the masking at the client layer, so every consumer of the raw error (logs, status, session-start failures) now sees the spawn reason. The final user-facing classification is #215's job — it keeps the raw reason when the executable was already resolved and exists, which turns the message into "verify the launch environment" instead of "not installed". #212/#214 remove the setup-path trigger entirely, and #218/#219 remove the ~4 s the catalog still spends waiting on close() (the client request itself now rejects in 7 ms).

Fixes #223

🤖 Generated with Claude Code

@eddiearc eddiearc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MERGE-WITH-NITS(Fable 5.1 审合)— 请 rebase 到已合的 #219 后再请求 approve

stdin 写失败抢在 process verdict 前进入 fail() 会掩盖 spawn ENOENT;进程已不在时把 write error 作有界(500ms)fallback,活进程坏 stdin 仍立刻失败,方向对。单测覆盖「裁决优先」和「无裁决也有界」。

Nits:writeFailureTimer 单槽并发替换可先 clear;缺 version bump。请先合 #219 再合本 PR。

@eddiearc

Copy link
Copy Markdown
Owner

Maintainer follow-up(Fable vs GPT-6 Astra)

方向同意:stdin 写失败不该盖住真实的 spawn verdict。

Fable 倾向 rebase 后合;GPT-6 Astra 消融后建议先缩小实现再合(我们跟这个):

  1. 优先验证「一次 next-tick / 合并后的延迟」是否够用,而不是固定 500ms 仲裁窗口。
  2. 若必须保留 timer:只能有一个 pending fallback;fail() / 终态路径必须清掉;并发 failWrite 不能覆盖丢 timer 引用。
  3. 不要把「可选 pid 缺省」当成 processGone——活进程 double / adapter 会误走延迟路径。
  4. rebase 到已合的 #219close 终态)后再开,并补:真实 missing cwd/exe、活进程坏 stdin、并发写失败、短 request deadline。

相关:#223#219 已在 main。

@itswl
itswl force-pushed the fix/pi-rpc-write-failure-masking branch from 124561f to 10d1657 Compare September 14, 2026 01:53
@itswl

itswl commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

按四条改完了(head 10d1657;已 rebase 到含 #219 的 main;实现整体重写为更小的版本):

  1. 去掉固定仲裁窗口 → 单次 next-tick 延迟failWritesetImmediate 一次(nextTick 队列先于 check 阶段,真实 spawn/exit verdict 必然先到并胜出);无 verdict 时才报写错误。没有 500ms、没有任何 timer。
  2. 无 pending timer 可泄漏:实现里已无 timer;并发 failWritefail() 的幂等性收敛为唯一终态,fail()/终态路径无需额外清理。
  3. 已删掉"缺 pid 视为 processGone"PiRpcProcess 上的 pid/exitCode/signalCode 增补一并撤回,不对 doubles/活进程做任何进程状态推断。
  4. rebase 已完成(含 fix(runtime): settle Pi RPC shutdown on close when the spawn itself failed #219close 终态);测试补了四类:
    • 真实 missing executable 与真实 missing cwd(断言拿到 ENOENT、不被 stdin is unavailable 掩盖、且在一个 turn 内结算而非定时器);
    • 活进程坏 stdin(一次延迟即报,<1s);
    • 并发写失败(两请求、恰好一次终态失败、消息一致);
    • 短 request deadline(与延迟交互后仍只报一次)。

验证:pi-rpc-client 14 pass / 0 fail;test/unit/runtime/ 241 pass / 0 fail;build / typecheck 通过。

@eddiearc

Copy link
Copy Markdown
Owner

复看 head 10d1657:上次四条基本都对上了。

  1. ✅ 固定 500ms 仲裁已换成单次 setImmediate
  2. ✅ 无 timer / 无泄漏面
  3. ✅ 已撤回 pid/exitCode 推断
  4. ✅ rebase 含 fix(runtime): settle Pi RPC shutdown on close when the spawn itself failed #219;补测覆盖 missing exe/cwd、活进程坏 stdin、并发写失败、短 deadline

CI 全绿。maintainer 侧倾向 APPROVE合入仍等 idan 明确 yes

A spawn failure was reported as whichever error landed first: the
request's stdin check ("stdin is unavailable") or the process verdict
("spawn ... ENOENT"), and fail() keeps the first error — so the write
path could mask the real cause. Defer the write error by a single turn
(next tick runs before the check phase, so a real spawn/exit verdict
always wins) and report it only when no verdict arrives: no arbitration
timer, no pid/exit heuristics on doubles, and concurrent write failures
collapse into the client's single first-error terminal state.

Tests cover a real missing executable and missing working directory, a
live process with broken stdin, concurrent write failures, and a short
request deadline.

Bumps the package patch version (0.5.9 -> 0.5.10) per the Owner policy.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@itswl
itswl force-pushed the fix/pi-rpc-write-failure-masking branch from 10d1657 to a325714 Compare September 14, 2026 14:15
@itswl

itswl commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

package.json 已按 Owner 政策补 bump(0.5.9 → 0.5.10,head a325714),CI 已触发。

@eddiearc
eddiearc merged commit d3a506c into eddiearc:main Sep 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pi 探测在 spawn 失败时的错误被写路径掩盖("stdin is unavailable",且随竞态变成 "pi is not installed")

2 participants