Skip to content

Commit 9b8714d

Browse files
committed
fix(tui): handle stdin EPIPE race in chat ui
1 parent 8228204 commit 9b8714d

5 files changed

Lines changed: 43 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@
9595
- in-flight wake tasks awaited with a short timeout, then aborted
9696
- MCP server stopped with graceful shutdown timeout fallback
9797

98+
## TUI Pipe Safety
99+
100+
- `crewforge-ts/src/chat/run-chat-ui.ts` must handle `child.stdin` stream errors.
101+
- Treat `EPIPE` and `ERR_STREAM_DESTROYED` as expected race conditions after core exit or stdin close.
102+
- Do not let `child.stdin.write(...)` related errors crash the launcher process.
103+
- Keep a regression test for this behavior in `crewforge-ts/src/tests/` whenever TUI command wiring changes.
104+
- Before release tags, verify local/global launcher behavior by running `npm test --prefix crewforge-ts` and checking a TTY `crewforge chat` invocation.
105+
98106
## Opencode + MCP Integration
99107

100108
- Provider command is configurable (`opencode.command`, default `opencode`).

crewforge-rs/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crewforge-rs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "crewforge"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
edition = "2024"
55

66
[dependencies]

crewforge-ts/src/chat/run-chat-ui.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ function withJsonlRpcFlag(args: string[]): string[] {
3333
return [...args, "--rpc", "jsonl"];
3434
}
3535

36+
export function shouldIgnoreChildStdinError(error: NodeJS.ErrnoException): boolean {
37+
return error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED";
38+
}
39+
3640
export async function runChatWithUi(binary: string, rawArgs: string[]): Promise<number> {
3741
const childArgs = withJsonlRpcFlag(rawArgs);
3842
const child = spawn(binary, childArgs, {
@@ -43,6 +47,15 @@ export async function runChatWithUi(binary: string, rawArgs: string[]): Promise<
4347
const state = new ChatStateStore();
4448
let forceExitTimer: NodeJS.Timeout | undefined;
4549

50+
const handleStdinError = (error: NodeJS.ErrnoException): void => {
51+
if (shouldIgnoreChildStdinError(error)) {
52+
return;
53+
}
54+
const message = error.message ?? String(error);
55+
state.appendClientNotice(`[input pipe] ${message}`);
56+
};
57+
child.stdin?.on("error", handleStdinError);
58+
4659
const sendCommand = (command: RpcCommand): void => {
4760
if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
4861
return;

crewforge-ts/src/tests/chat-ui-mode.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import assert from "node:assert";
22
import { test } from "node:test";
33

4-
import { encodeInputCommand, shouldUseChatUi } from "../chat/run-chat-ui";
4+
import {
5+
encodeInputCommand,
6+
shouldIgnoreChildStdinError,
7+
shouldUseChatUi,
8+
} from "../chat/run-chat-ui";
59

610
test("encodeInputCommand emits jsonl payload", () => {
711
assert.strictEqual(
@@ -13,3 +17,18 @@ test("encodeInputCommand emits jsonl payload", () => {
1317
test("shouldUseChatUi disables UI when --rpc is explicit", () => {
1418
assert.strictEqual(shouldUseChatUi(["chat", "--rpc", "jsonl"]), false);
1519
});
20+
21+
test("shouldIgnoreChildStdinError only suppresses expected pipe races", () => {
22+
assert.strictEqual(
23+
shouldIgnoreChildStdinError({ code: "EPIPE" } as NodeJS.ErrnoException),
24+
true,
25+
);
26+
assert.strictEqual(
27+
shouldIgnoreChildStdinError({ code: "ERR_STREAM_DESTROYED" } as NodeJS.ErrnoException),
28+
true,
29+
);
30+
assert.strictEqual(
31+
shouldIgnoreChildStdinError({ code: "EINVAL" } as NodeJS.ErrnoException),
32+
false,
33+
);
34+
});

0 commit comments

Comments
 (0)