Skip to content

fix(pi): a busy fixed port no longer crashes Pi when a second annotate opens - #1733

Merged
backnotprop merged 1 commit into
mainfrom
fix/pi-fixed-port-crash
Oct 6, 2026
Merged

backnotprop merged 1 commit into
mainfrom
fix/pi-fixed-port-crash

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Problem

In a local Pi session with PLANNOTATOR_PORT set (not remote mode), opening a second /plannotator-annotate while a review is open killed Pi with exit code 1:

Error: listen EADDRINUSE: address already in use 127.0.0.1:<port>
Emitted 'error' event on WebSocketServer instance at: ...

This reproduces on 0.28.4 and 0.28.5, with Pi 0.85.1 and 1.0.2.

Cause

apps/pi-extension/server/serverAnnotate.ts attached the annotate agent terminal's WebSocket server to the http server before listenOnPort ran. The ws WebSocketServer re-emits every http server error as its own. It has no error handler, and the @plannotator/webtui wrapper does not expose the instance, so nothing can add one. So the first EADDRINUSE became an unhandled error event, and it killed the process before two things could run:

  • the port-in-use retry in listenOnPort
  • startServerWithSelfPreemption, which takes the port over

Remote mode never crashed only because the agent terminal is off there.

Fix

  • The agent terminal bridge is now created after the port is bound. Before listen, the http server has no ws listener, so a busy port is an ordinary rejection.
  • A failed bind now disposes the client lease and the AI runtime. It deliberately does not close the file browser watchers, because those are shared by the whole process and belong to the sessions that are still open. The live-app failure path keeps its full disposal set through the same helper.
  • Result: a local fixed-port session now behaves like remote mode. The new command takes the port over from the stale session (Pi extension: server.close() doesn't free port — next annotate/review fails with EADDRINUSE #1159 self-preemption), and Pi stays up.
  • No change to the plannotator tool's fixed-port refusal (fixedPortBusyText).
  • Plan and review servers on Pi do not attach anything to the http server before listen. The agent terminal exists only in annotate, so they were not affected.
  • The Bun server needs no change. It uses Bun.serve upgrades rather than a ws server, and a busy fixed port fails cleanly with PortInUseError (checked with a real port: Port 19681 in use after 5 retries, and the first session keeps serving).

Tests

  • New apps/pi-extension/server/annotate-fixed-port.test.ts, which uses real ports:
    • A busy PLANNOTATOR_PORT rejects with the port-in-use error, and the open session keeps serving. On main this test fails with the unhandled emitError.
    • startServerWithSelfPreemption takes the port over for a second annotate session, with the agent terminal enabled.
  • bun test apps/pi-extension: 438 pass.
  • bun run typecheck (includes vendor.sh): clean.
  • npm pack --dry-run in apps/pi-extension: OK.
  • Live check: real Pi in RPC mode, loading the worktree extension with -e, local mode with PLANNOTATOR_PORT, two /plannotator-annotate in a row (harness /tmp/pi0281/fixport-verify.mjs):
    • On main: Pi exits 1 with the WebSocketServer EADDRINUSE trace.
    • With the fix, on Pi 0.85.1 and 1.0.2: Pi stays alive. You get "A Plannotator browser session was closed.", the second annotate opens on the same port with its agent terminal, and nothing is written to stderr.

…xed port cannot crash Pi

With PLANNOTATOR_PORT set in a local Pi session, a second
/plannotator-annotate while a review was open killed Pi with exit 1:
the agent terminal's WebSocketServer (ws) was attached to the http
server before listen, re-emits every server 'error' as its own, and
has no handler, so the first EADDRINUSE became an unhandled 'error'
before listenOnPort's retry and the self-preemption that takes the
port over could run. Remote mode never crashed only because the agent
terminal is off there.

The bridge is now created once the port is bound, and a failed bind
disposes the client lease and AI runtime (not the process-wide file
browser watchers, which belong to the open sessions). The local
fixed-port case now behaves like remote mode: the new command takes
the port over from the stale session. The plannotator tool's
fixed-port refusal is unchanged.
@backnotprop
backnotprop merged commit 098424e into main Oct 6, 2026
38 of 39 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.

1 participant