Added in-progress label to GitHub issue.
EditReviewed code and verified root cause independently.
UnderstandCreated small test to reproduce the bug.
BuildDrafted and committed fix for the issue.
EditPushed changes and confirmed issue closure.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/2791
For each issue:
Add the inprogress label to the issue via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue.
Read the issue in full, including all comments and linked issues/PRs.
Do not trust analysis written in the issue. Independently verify behavior and derive your own analysis from the code and execution path.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
Let me start by reading the GitHub issue and adding the inprogress label.
>cd /Users/badlogic/workspaces/pi-mono && gh issue view 2791 --json title,body,comments,labels,state{"body":"### What happened?\n\n`startThemeWatcher()` creates an `fs.watch()` handle on the custom themes directory.\nThe watcher has a `try/catch` around the `fs.watch()` call to suppress startup errors,\nbut **no `.on('error', ...)` handler is attached to the returned `FSWatcher`**.\n\nIn Node.js, `FSWatcher` is an `EventEmitter`. If it emits an `error` event after\nstarting — which happens on Windows when a git rebase or similar fs operation\ninvalidates the `ReadDirectoryChangesW` handle — Node.js treats it as an\n**uncaught exception and terminates the process with exit code 1**.\n\nSince every interactive pi session starts its own watcher, a single handle\ninvalidation event kills **all open sessions simultaneously**.\n\n## Root Cause\n\n```typescript\n// packages/coding-agent/src/modes/interactive/theme/theme.ts\n\ntry {\n themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => {\n // ...handler...\n });\n // ← BUG: no themeWatcher.on('error', ...) here\n}\ncatch (_error) {\n // Ignore errors starting watcher ← only catches synchronous startup errors\n}\n```\n\n`fs.watch()` only throws synchronously on invalid arguments. All runtime errors\n(directory renamed/deleted, OS handle limit reached, ReadDirectoryChangesW failure)\nare emitted asynchronously as `error` events on the returned `FSWatcher` object.\nWithout a listener, Node.js re-throws them as uncaught exceptions.\n\n---\n\n## Fix (one line)\n\n```typescript\ntry {\n themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => {\n // ...handler...\n });\n // ADD: handle async watcher errors — prevents process crash\n themeWatcher.on('error', (_err) => {\n try { themeWatcher?.close(); } catch { /* ignore */ }\n themeWatcher = undefined;\n // Watcher silently stops. Theme hot-reload is unavailable until\n // next session, but pi stays alive.\n });\n}\ncatch (_error) {\n // Ignore errors starting watcher\n}\n```\n\nThe same pattern already exists in many Node.js file-watching libraries\n(chokidar, nodemon, etc.) as standard defensive practice.\n\n---\n\n## Impact\n\n- All interactive pi sessions die simultaneously — User loses all work in progress\n- New sessions crash instantly on restart (watcher starts again into the same error state)\n- Recovery requires waiting for the OS to release the invalid handle or restarting the machine\n- Only affects custom themes; built-in themes (`dark`, `light`) skip the watcher entirely\n\n---\n\n**##** Workaround (applied locally)\n\nPatched `dist/modes/interactive/theme/theme.js` in place with the `.on('error', ...)`\nhandler above. Fix survives until next `npm update` of `@mariozechner/pi-coding-agent`.\n\n\n\n### Steps to reproduce\n\n1. Running pi with a custom theme (any theme other than `dark`/`light`)\n2. Multiple concurrent pi sessions open (each holds its own `fs.watch` handle)\n3. A git rebase, worktree operation, or branch switch in the agent directory\n (`~/.pi/agent/`) modifies the custom themes directory\n4. Windows `ReadDirectoryChangesW` invalidates one or more handles\n5. All pi sessions crash simultaneously with `[process exited with code 1]`\n\n### Expected behavior\n\n_No response_\n\n### Version\n\n_No response_","comments":[],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"OPEN","title":"`fs.watch` error event crashes all pi sessions simultaneously"}>cd /Users/badlogic/workspaces/pi-mono && gh issue edit 2791 --add-label "inprogress" 2>&1https://github.com/badlogic/pi-mono/issues/2791Let me read the theme.ts file to verify the issue independently.
Label added. Now let me read the actual source file to verify the issue independently.
Now I've read the full file. Let me verify the issue independently.
Looking at startThemeWatcher() (around line 420-470):
try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { if (currentThemeName !== watchedThemeName) { return; } if (!filename) { scheduleReload(); return; } const changedFile = String(filename); if (changedFile !== watchedFileName) { return; } scheduleReload(); });} catch (_error) { // Ignore errors starting watcher}The problem is clear: fs.watch() returns an FSWatcher that's an EventEmitter, but the try/catch only handles synchronous errors during the initial call. If the watcher later encounters an error—like the directory being deleted or an OS handle becoming invalid—it emits an error event. Without a handler for that event, Node.js crashes the process with an unhandled exception.
The fix is straightforward: add an error event listener...
Now I should scan the rest of the codebase to see if there are other fs.watch calls that might have the same vulnerability.
Let me check if there are other fs.watch usages with the same pattern in the codebase.
>cd /Users/badlogic/workspaces/pi-mono && rg 'fs\.watch\(' --type ts -lpackages/coding-agent/src/modes/interactive/theme/theme.ts
packages/coding-agent/examples/extensions/file-trigger.ts>cd /Users/badlogic/workspaces/pi-mono && rg "fs\.watch\(" --type ts -A 10 packages/coding-agent/examples/extensions/file-trigger.tsfs.watch(triggerFile, () => {
try {
const content = fs.readFileSync(triggerFile, "utf-8").trim();
if (content) {
pi.sendMessage(
{
customType: "file-trigger",
content: `External trigger: ${content}`,
display: true,
},
{ triggerTurn: true }, // triggerTurn - get LLM to respondThe example file has the same pattern (no .on('error') handler), but it's just an example, not production code.
Issue: #2791 — fs.watch error event crashes all pi sessions simultaneously
Type: Bug
Independently verified: Yes. The issue is real and the root cause analysis in the issue is correct.
In packages/coding-agent/src/modes/interactive/theme/theme.ts, the startThemeWatcher() function (lines ~420-470) creates an fs.watch() handle on the custom themes directory. The code wraps the call in try/catch, but this only catches synchronous errors from fs.watch() itself.
fs.watch() returns an FSWatcher (an EventEmitter). Runtime errors (directory renamed/deleted, OS handle invalidation, handle limit exhaustion) are emitted asynchronously as error events. Without an .on('error', ...) handler, Node.js treats these as uncaught exceptions and terminates the process.
The specific code:
try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { // ... callback ... }); // No .on('error', ...) handler} catch (_error) { // Only catches synchronous errors}Add an error event handler to themeWatcher immediately after creation, inside the try block. On error, close the watcher and set it to undefined (theme hot-reload becomes unavailable, but pi stays alive):
try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { // ... existing callback ... }); themeWatcher.on("error", () => { try { themeWatcher?.close(); } catch { /* ignore */ } themeWatcher = undefined; });} catch (_error) { // Ignore errors starting watcher}packages/coding-agent/src/modes/interactive/theme/theme.ts — single change in startThemeWatcher()initTheme()/setTheme() call, but all sessions remain alivecan you reproduce this in a small test?
The user wants me to reproduce the bug in a small test. Let me think about how to do this.
The core issue is that fs.watch() returns an FSWatcher EventEmitter, and if it emits an error event without a handler, Node.js crashes the process.
I can write a test that:
startThemeWatcher() (or the relevant code path)Actually, the simplest approach would be to directly test the behavior: create an fs.watch on a directory, then programmatically emit an error on it, and see what happens.
But I need to consider the test infrastructure for this codebase. The themeWatcher variable is module-private, so I can't directly access it from a test. I could either write a standalone reproduction script that demonstrates the bug, or create a test that exercises the theme initialization code path with a custom theme and then triggers a directory error. Let me explore the second approach by setting up a test scenario that exercises the relevant code paths. Looking at the test structure in AGENTS.md, I need to put regression tests under packages/coding-agent/test/suite/regressions/ with the naming convention <issue-number>-<short-slug>.test.ts. The challenge is that themeWatcher isn't exported, so I'll need to either mock getCustomThemesDir and trigger the directory deletion to test the error handling, or find another way to access the watcher instance for verification.
The simplest approach is to spy on fs.watch to capture the FSWatcher instance, then manually emit an error event on it to verify the process handles it gracefully without crashing.
Let me look at the test infrastructure to understand how to write this properly.
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && ls test/suite/regressions/ 2>/dev/null | head -202023-queued-slash-command-followup.test.ts
2753-reload-stale-resource-settings.test.ts>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && ls test/ | head -20agent-session-auto-compaction-queue.test.ts
agent-session-branching.test.ts
agent-session-compaction.test.ts
agent-session-concurrent.test.ts
agent-session-dynamic-provider.test.ts
agent-session-dynamic-tools.test.ts
agent-session-model-switch-thinking.test.ts
agent-session-retry.test.ts
agent-session-runtime-events.test.ts
agent-session-stats.test.ts
agent-session-tree-navigation.test.ts
args.test.ts
auth-storage.test.ts
bash-close-hang-windows.test.ts
bash-execution-width.test.ts
block-images.test.ts
clipboard-image-bmp-conversion.test.ts
clipboard-image.test.ts
compaction-extensions-example.test.ts
compaction-extensions.test.ts>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && head -30 test/suite/regressions/2753-reload-stale-resource-settings.test.tsimport { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { registerFauxProvider } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import {
type CreateAgentSessionRuntimeFactory,
createAgentSessionFromServices,
createAgentSessionRuntime,
createAgentSessionServices,
} from "../../../src/core/agent-session-runtime.js";
import { AuthStorage } from "../../../src/core/auth-storage.js";
import { SessionManager } from "../../../src/core/session-manager.js";
describe("issue #2753 reload stale resource settings", () => {
const cleanups: Array<() => void> = [];
afterEach(() => {
while (cleanups.length > 0) {
cleanups.pop()?.();
}
});
it("applies updated top-level prompt settings on reload after startup", async () => {
const tempDir = join(tmpdir(), `pi-2753-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const agentDir = join(tempDir, "agent");
const promptsDir = join(agentDir, "prompts");
mkdirSync(promptsDir, { recursive: true });
writeFileSync(join(promptsDir, "test.md"), "Echo test prompt\n");>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && ls test/theme* 2>/dev/null; rg -l "theme" test/ --max-depth=1 2>/dev/nulltest/theme-export.test.ts
test/utilities.ts
test/test-theme-colors.ts
test/session-info-modified-timestamp.test.ts
test/session-selector-path-delete.test.ts
test/sdk-codex-cache-probe-tool-loop.ts
test/tool-execution-component.test.ts
test/extensions-runner.test.ts
test/extensions-discovery.test.ts
test/settings-manager-bug.test.ts
test/streaming-render-debug.ts
test/sdk-skills.test.ts
test/interactive-mode-status.test.ts
test/theme-export.test.ts
test/edit-tool-no-full-redraw.test.ts
test/footer-width.test.ts
test/settings-manager.test.ts
test/resource-loader.test.ts
test/bash-execution-width.test.ts
test/tree-selector.test.ts
test/args.test.ts
test/package-manager.test.ts
test/session-selector-rename.test.tsGood, now I understand the test patterns. Let me also check how getCustomThemesDir works to know how to set up the environment.
>cd /Users/badlogic/workspaces/pi-mono && rg "getCustomThemesDir" packages/coding-agent/src/config.tsexport function getCustomThemesDir(): string {>cd /Users/badlogic/workspaces/pi-mono && rg -A 5 "export function getCustomThemesDir" packages/coding-agent/src/config.tsexport function getCustomThemesDir(): string {
return join(getAgentDir(), "themes");
}
/** Get path to models.json */
export function getModelsPath(): string {>cd /Users/badlogic/workspaces/pi-mono && rg -A 5 "export function getAgentDir" packages/coding-agent/src/config.tsexport function getAgentDir(): string {
const envDir = process.env[ENV_AGENT_DIR];
if (envDir) {
// Expand tilde to home directory
if (envDir === "~") return homedir();
if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);Now let me look at a dark.json theme to use as a template for the test:
>cd /Users/badlogic/workspaces/pi-mono && rg "ENV_AGENT_DIR" packages/coding-agent/src/config.ts | head -3export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
const envDir = process.env[ENV_AGENT_DIR];Now let me write the regression test:
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/suite/regressions/2791-fswatch-error-crash.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts (1 test | 1 failed) 5ms
× issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher 4ms
→ Cannot spy on export "watch". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/browser/#limitations
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL test/suite/regressions/2791-fswatch-error-crash.test.ts > issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher
TypeError: Cannot spy on export "watch". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/browser/#limitations
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts:44:17
42| // Spy on fs.watch to capture the returned FSWatcher
43| capturedWatcher = undefined;
44| watchSpy = vi.spyOn(fs, "watch").mockImplementation((...args: unknow…
| ^
45| // Call the real fs.watch
46| const realWatcher = (vi.mocked(fs.watch) as unknown as { getMockImp…
Caused by: TypeError: Cannot redefine property: watch
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts:44:17
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed (1)
Start at 20:43:25
Duration 395ms (transform 44ms, setup 0ms, collect 140ms, tests 5ms, environment 0ms, prepare 44ms)
Command exited with code 1Can't spy on ESM modules. Let me take a different approach. Instead of spying on fs.watch, I can:
Actually, the simplest approach is to not try to capture the watcher at all. Instead:
Set up the custom theme and enable the watcher
Delete the watched directory (which should trigger an error on the FSWatcher on most platforms)
Listen for uncaughtException events on the process to detect if the error crashes it
A subprocess approach would be more reliable—spawn a child process that sets up the theme, creates the watcher, deletes the directory, and then check its exit code to determine if the bug is present.
Actually, I'm overcomplicating this. The real issue is whether the themeWatcher FSWatcher has an error handler attached, so I can verify that directly by checking the listeners on the process or by examining the watcher's error handlers after setup. I can access the active handles in the process using process._getActiveHandles() to locate the FSWatcher, then emit an error directly on it to test if the error handling works. But actually, the child process approach is more reliable for testing whether the process crashes or survives an error.
Can't spy on ESM namespace. Let me use a child process approach instead, which is actually more faithful to the bug (it tests whether the process actually crashes).
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/suite/regressions/2791-fswatch-error-crash.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts (1 test | 1 failed) 680ms
× issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher 679ms
→ Child process crashed (exit 3). stdout: : expected 3 to be +0 // Object.is equality
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL test/suite/regressions/2791-fswatch-error-crash.test.ts > issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher
AssertionError: Child process crashed (exit 3). stdout: : expected 3 to be +0 // Object.is equality
[32m- Expected[39m
[31m+ Received[39m
[32m- 0[39m
[31m+ 3[39m
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts:109:83
107|
108| // If exit code is non-zero, the unhandled error event crashed the p…
109| expect(exitCode, `Child process crashed (exit ${exitCode}). stdout: …
| ^
110| expect(stdout.trim()).toBe("OK");
111| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed (1)
Start at 20:44:10
Duration 959ms (transform 18ms, setup 0ms, collect 16ms, tests 680ms, environment 0ms, prepare 44ms)
Command exited with code 1Exit code 3 means ERROR_THROWN - the error was thrown when fsWatcher.emit("error", ...) was called. This confirms the bug: there's no error handler, so emit("error") throws.
Actually wait, the emit("error") in EventEmitter: when there are no error listeners, EventEmitter.emit("error") throws the error. The try/catch in the script caught it. So exit code 3 and "ERROR_THROWN" means: confirmed, no error handler is registered.
The test is working correctly - it's demonstrating the bug.
Now I want to make the test output clearer and verify that stderr also shows the expected error message. I'll refine the test slightly so it's ready to pass once the fix is applied, and I should capture stderr to confirm the error details are being logged correctly.
The test confirms the bug: exit code 3 means emit("error") threw because there's no error handler. The test is working correctly: it fails with the current code (bug present) and will pass once the fix is applied.
Let me clean up the test slightly to make the failure output clearer:
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/suite/regressions/2791-fswatch-error-crash.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts (1 test | 1 failed) 712ms
× issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher 712ms
→ Child crashed (exit 1). stderr: BUG: FSWatcher has no error handler (issue #2791)
error event was unhandled and threw: expected 1 to be +0 // Object.is equality
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL test/suite/regressions/2791-fswatch-error-crash.test.ts > issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher
AssertionError: Child crashed (exit 1). stderr: BUG: FSWatcher has no error handler (issue #2791)
error event was unhandled and threw: expected 1 to be +0 // Object.is equality
[32m- Expected[39m
[31m+ Received[39m
[32m- 0[39m
[31m+ 1[39m
❯ test/suite/regressions/2791-fswatch-error-crash.test.ts:104:82
102| }
103|
104| expect(exitCode, `Child crashed (exit ${exitCode}). stderr: ${stderr…
| ^
105| });
106| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed (1)
Start at 20:44:38
Duration 1000ms (transform 19ms, setup 0ms, collect 17ms, tests 712ms, environment 0ms, prepare 45ms)
Command exited with code 1Test works correctly. The failure message is clear:
BUG: FSWatcher has no error handler (issue #2791)error event was unhandled and threwThis confirms the bug: the FSWatcher from fs.watch() has zero error listeners, so any async error event crashes the process.
Summary:
packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.tsFSWatcher handle, and emits a synthetic error event on it. Without an error handler, EventEmitter.emit('error') throws/crashes. With a handler, the child exits cleanly.The fix remains the same one-liner addition in startThemeWatcher() in theme.ts:
themeWatcher.on("error", () => { try { themeWatcher?.close(); } catch { /* ignore */ } themeWatcher = undefined;});ok fix it
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/suite/regressions/2791-fswatch-error-crash.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/suite/regressions/2791-fswatch-error-crash.test.ts (1 test) 654ms
✓ issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher 653ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 20:49:47
Duration 910ms (transform 24ms, setup 0ms, collect 17ms, tests 654ms, environment 0ms, prepare 47ms)Test passes. Now run the full check:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check
packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts:86:7 lint/correctness/noUnusedVariables FIXABLE ━━━━━━━━━━
! This variable stdout is unused.
84 │ );
85 │
> 86 │ let stdout = "";
│ ^^^^^^
87 │ let stderr = "";
88 │ let exitCode: number;
i Unused variables are often the result of an incomplete refactoring, typos, or other sources of bugs.
i Unsafe fix: If this is intentional, prepend stdout with an underscore.
84 84 │ );
85 85 │
86 │ - → → let·stdout·=·"";
86 │ + → → let·_stdout·=·"";
87 87 │ let stderr = "";
88 88 │ let exitCode: number;
89 89 │ try {
90 │ - → → → stdout·=·execFileSync("npx",·["tsx",·scriptPath],·{
90 │ + → → → _stdout·=·execFileSync("npx",·["tsx",·scriptPath],·{
91 91 │ timeout: 10000,
92 92 │ encoding: "utf-8",
······· │
97 97 │ } catch (err: unknown) {
98 98 │ const e = err as { status: number; stdout: string; stderr: string };
99 │ - → → → stdout·=·e.stdout·??·"";
99 │ + → → → _stdout·=·e.stdout·??·"";
100 100 │ stderr = e.stderr ?? "";
101 101 │ exitCode = e.status ?? 1;
Skipped 1 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 561 files in 406ms. No fixes applied.
Found 1 warning.
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× Some warnings were emitted while running checks.
Command exited with code 1>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check
Checked 561 files in 413ms. No fixes applied.
> pi-monorepo@0.0.3 check:browser-smoke
> node scripts/check-browser-smoke.mjs
> @mariozechner/pi-web-ui@0.65.0 check
> biome check --write --error-on-warnings . && tsc --noEmit && cd example && biome check --write --error-on-warnings . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Now update the changelog:
Run the test one more time to confirm everything is solid:
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/suite/regressions/2791-fswatch-error-crash.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/suite/regressions/2791-fswatch-error-crash.test.ts (1 test) 669ms
✓ issue #2791 fs.watch error event crashes process > process should survive an error event on the theme FSWatcher 668ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 20:50:27
Duration 933ms (transform 19ms, setup 0ms, collect 16ms, tests 669ms, environment 0ms, prepare 43ms)All done. Here's a summary of the changes:
Files modified:
packages/coding-agent/src/modes/interactive/theme/theme.ts — Added .on("error", ...) handler to the FSWatcher in startThemeWatcher(). On error, the watcher is closed and set to undefined. Theme hot-reload stops, but pi stays alive.
packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts — New regression test. Spawns a child process that sets up the theme watcher, finds the FSWatcher handle, and emits a synthetic error event. Verifies the process survives instead of crashing.
packages/coding-agent/CHANGELOG.md — Added entry under [Unreleased] > Fixed.
Wrap it.
Additional instructions:
Determine context from the conversation history first.
Rules for context detection:
/is or /pr, assume the issue or PR context is already known from the conversation and from the analysis work already done.Unless I explicitly override something in this request, do the following in order:
## [Unreleased] using the repo changelog rules.closes #<issue> in the commit message. If it is tied to multiple issues, stop and ask which one to use. If it is not tied to any issue, do not include closes # or fixes # in the commit message.main, stop and ask what to do. Do not push from another branch unless I explicitly say so.Constraints:
git add . or git add -A.Let me review what happened in this session:
inprogress labelThe changelog is already updated. I need to:
closes #2791 in the commitLet me check the branch first, then draft the comment.
>cd /Users/badlogic/workspaces/pi-mono && git branch --show-currentmain>cd /Users/badlogic/workspaces/pi-mono && git status --shortM packages/coding-agent/CHANGELOG.md
M packages/coding-agent/src/core/agent-session.ts
M packages/coding-agent/src/modes/interactive/theme/theme.ts
D packages/coding-agent/test/suite/agent-session-runtime-host.test.ts
?? nohup.out
?? out.txt
?? packages/coding-agent/test/suite/agent-session-runtime.test.ts
?? packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts
?? packages/coding-agent/test/suite/regressions/2796-tooluse-branch.test.ts
?? test.txtOn main. My files are: theme.ts, CHANGELOG.md, and the new test file. The other changes are from other work.
Now draft the issue comment:
>cat > /tmp/2791-comment.md << 'EOF'
Fixed. Added `.on("error", ...)` to the `FSWatcher` in `startThemeWatcher()`. On error, the watcher closes itself and sets the handle to `undefined`. Theme hot-reload stops but pi stays alive.
Regression test in `test/suite/regressions/2791-fswatch-error-crash.test.ts` spawns a child process, finds the `FSWatcher` handle, and emits a synthetic error event to verify the process survives.
EOF
cat /tmp/2791-comment.mdFixed. Added `.on("error", ...)` to the `FSWatcher` in `startThemeWatcher()`. On error, the watcher closes itself and sets the handle to `undefined`. Theme hot-reload stops but pi stays alive.
Regression test in `test/suite/regressions/2791-fswatch-error-crash.test.ts` spawns a child process, finds the `FSWatcher` handle, and emits a synthetic error event to verify the process survives.Looks good. Posting and committing:
>cd /Users/badlogic/workspaces/pi-mono && gh issue comment 2791 --body-file /tmp/2791-comment.mdhttps://github.com/badlogic/pi-mono/issues/2791#issuecomment-4187575543>cd /Users/badlogic/workspaces/pi-mono && git add \
packages/coding-agent/src/modes/interactive/theme/theme.ts \
packages/coding-agent/CHANGELOG.md \
packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts(no output)>cd /Users/badlogic/workspaces/pi-mono && git diff --cached --statpackages/coding-agent/CHANGELOG.md | 1 +
.../src/modes/interactive/theme/theme.ts | 8 ++
.../regressions/2791-fswatch-error-crash.test.ts | 106 +++++++++++++++++++++
3 files changed, 115 insertions(+)Only my 3 files staged.
>cd /Users/badlogic/workspaces/pi-mono && git commit -m "fix(coding-agent): handle async fs.watch error events in theme watcher
closes #2791"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check
Checked 561 files in 408ms. No fixes applied.
> pi-monorepo@0.0.3 check:browser-smoke
> node scripts/check-browser-smoke.mjs
> @mariozechner/pi-web-ui@0.65.0 check
> biome check --write --error-on-warnings . && tsc --noEmit && cd example && biome check --write --error-on-warnings . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.
✅ All pre-commit checks passed!
[main 5e5eeb96] fix(coding-agent): handle async fs.watch error events in theme watcher
3 files changed, 115 insertions(+)
create mode 100644 packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
72a43dc0..5e5eeb96 main -> mainPushed. Issue #2791 will be auto-closed by the commit.
try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { if (currentThemeName !== watchedThemeName) { return; } if (!filename) { scheduleReload(); return; } const changedFile = String(filename); if (changedFile !== watchedFileName) { return; } scheduleReload(); });} catch (_error) { // Ignore errors starting watcher}try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { // ... callback ... }); // No .on('error', ...) handler} catch (_error) { // Only catches synchronous errors}try { themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { // ... existing callback ... }); themeWatcher.on("error", () => { try { themeWatcher?.close(); } catch { /* ignore */ } themeWatcher = undefined; });} catch (_error) { // Ignore errors starting watcher}BUG: FSWatcher has no error handler (issue #2791)error event was unhandled and threwthemeWatcher.on("error", () => { try { themeWatcher?.close(); } catch { /* ignore */ } themeWatcher = undefined;});