Skip to content

fix(pi-extension): don't crash pi on stale ctx in _emitRelayState - #80

Merged
jacobaraujo7 merged 1 commit into
jacobaraujo7:mainfrom
kkunkunya:fix/emit-relay-state-stale-ctx
Aug 6, 2026
Merged

fix(pi-extension): don't crash pi on stale ctx in _emitRelayState#80
jacobaraujo7 merged 1 commit into
jacobaraujo7:mainfrom
kkunkunya:fix/emit-relay-state-stale-ctx

Conversation

@kkunkunya

Copy link
Copy Markdown
Contributor

Problem

pi exits with an uncaught exception when the relay WebSocket closes after a session replacement:

pi exiting due to uncaughtException:
Error: This extension ctx is stale after session replacement or reload.
    at Object.assertActive (.../pi-coding-agent/dist/core/extensions/loader.js:138:19)
    at Object.sendMessage (.../pi-coding-agent/dist/core/extensions/loader.js:240:21)
    at _emitRelayState (.../remote-pi/dist/index.js:1178:10)
    at RelayClient._onRelayClose (.../remote-pi/dist/index.js:1099:5)
    at WebSocket.emitClose (...)

_emitRelayState is reachable from a WebSocket 'close' callback (via _onRelayClose). After ctx.newSession() / fork() / switchSession() / reload(), the module-level _pi is stale, and sendMessage synchronously throws assertActive. With no try/catch on the WS event path, the throw becomes a process-level uncaughtException and pi exits.

The optional chain _pi?.sendMessage(...) only guards against _pi being null; it does not guard against a present-but-stale ctx.

Fix

Wrap the existing _pi?.sendMessage(...) call in try/catch. No behavior change on the happy path. When _pi is stale (or the extension runtime is not yet bound), the throw is swallowed instead of crashing the process. The next relay-state change re-emits, so connectivity stays eventually consistent.

This mirrors the stale-ctx tolerance already used by _ctxUi / _safeNotify (#55): those also swallow stale-ctx throws so relay events don't take down pi.

Scope

Minimal and intentionally narrow: only _emitRelayState is touched. Other _pi?.sendMessage(...) call sites are left as-is. If any of them is also reachable from an async/WS path after a session replacement, it can be patched the same way in a follow-up.

The root cause (captured ctx going stale after session replacement) is an SDK constraint, not something this extension can fully solve; this PR just stops one concrete crash path.

Testing

  • node --check on the built dist/index.js (equivalent change) passes.
  • Type check unaffected: the change only wraps an existing call in try/catch; no types or new APIs are introduced.

_emitRelayState can run inside a WebSocket 'close' callback (via
_onRelayClose). After a session replacement (newSession/fork/
switchSession/reload) the module-level _pi is stale, and sendMessage
synchronously throws assertActive. With no try/catch on the WS event
path, the throw becomes a process-level uncaughtException and exits pi.

Wrap the sendMessage call in try/catch. The next relay-state change
re-emits, so connectivity stays eventually consistent. Mirrors the
stale-ctx tolerance already used by _ctxUi/_safeNotify (jacobaraujo7#55).
@jacobaraujo7
jacobaraujo7 merged commit 52b6cef into jacobaraujo7:main Aug 6, 2026
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.

2 participants