Skip to content

fix: cancel widget auto-clear timers on session_shutdown (stale ctx crash) - #121

Open
JohannesBertens wants to merge 1 commit into
weauratech:mainfrom
JohannesBertens:fix/widget-clear-stale-ctx
Open

JohannesBertens wants to merge 1 commit into
weauratech:mainfrom
JohannesBertens:fix/widget-clear-stale-ctx

Conversation

@JohannesBertens

Copy link
Copy Markdown

Fixes #120

Problem

pi exits with an uncaughtException when a transient widget's auto-clear timer fires after the session has been replaced:

pi exiting due to uncaughtException:
Error: This extension ctx is stale after session replacement or reload. ...
    at ExtensionRunner.assertActive (...)
    at get hasUI (...)
    at Timeout._onTimeout (index.ts:5405:28)

All three widget auto-clear timers (memctx-tool-failure, memctx-learned, memctx-learn) capture ctx and dereference it up to 30s later:

setTimeout(() => ctx.hasUI && ctx.ui.setWidget("memctx-learn", []), 30000);

pi invalidates the extension ctx on session replacement (newSession, fork, switchSession, reload) and process exit — every property access then throws from assertActive(). The ctx.hasUI && guard cannot help because the getter itself is the crash site, and since the throw happens in a bare timer callback (no surrounding event dispatch or try/catch), it surfaces as a process-level uncaughtException and kills pi.

Repro: queue a memory candidate at end of turn (the memctx-learn widget appears), start a new session within 30s, wait for the timer.

Fix

  • Pending widget clears are tracked in a module-level Set and all cancelled in the existing session_shutdown handler. pi emits session_shutdown for the old extension instance on every replacement path and before exit, so a pending clear can never outlive its session. This also stops the 20–30s timers from delaying process exit.
  • The callback itself is stale-ctx-safe as a second line of defense: hasUI is captured while the ctx is guaranteed live (headless -p/JSON sessions still never touch ui), and setWidget is wrapped in try/catch in case a clear ever races the shutdown event.
  • _resetState also cancels pending clears for test isolation.

Scoped deliberately to the timers: the other deferred ctx touches in the agent_end curation IIFE already sit inside its try/catch, so they cannot crash the process.

Tests

New widget auto-clear timers section in test/unit.test.ts:

  • clears the widget via setWidget after the delay
  • session_shutdown cancels pending widget clears (driven through the registered handler)
  • firing on a stale ctx does not throw — regression test for the uncaughtException exit (ctx whose hasUI/setWidget throw like pi's assertActive)
  • headless ctx (hasUI === false) never touches setWidget

npm run typecheck and npm run lint pass; bun test test/unit.test.ts → 118 pass / 0 fail (4 new).

Checklist

  • Change is generic and safe for a public repository
  • No secrets or sensitive data added
  • Extension behavior change includes tests
  • CHANGELOG updated under [Unreleased]

Related: #114 (same stale-ctx family, headless /memctx-init).

pi invalidates the extension ctx on session replacement (newSession,
fork, switchSession, reload) and process exit. The widget auto-clear
setTimeout callbacks captured ctx and read `ctx.hasUI`, which throws
from assertActive() on a stale ctx; inside a bare timer callback that
throw became an uncaughtException that killed the whole pi process.

Track pending widget clears in a module-level Set, cancel them all in
session_shutdown (and in _resetState for test isolation), and make the
callback itself stale-ctx-safe: hasUI is captured while the ctx is
guaranteed live and setWidget is wrapped in try/catch as a second line
of defense.

Fixes weauratech#120

This branch has not been deployed

No deployments
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.

pi exits with uncaughtException: widget auto-clear setTimeout reads stale ctx after session replacement

1 participant