Skip to content

fix: await session_start to prevent stale ctx access - #16

Merged
tintinweb merged 1 commit into
tintinweb:masterfrom
zm2231:fix/session-start-stale-ctx
Jun 23, 2026
Merged

fix: await session_start to prevent stale ctx access#16
tintinweb merged 1 commit into
tintinweb:masterfrom
zm2231:fix/session-start-stale-ctx

Conversation

@zm2231

@zm2231 zm2231 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Problem

pi-gitnexus@0.6.3 crashes during boot when used with @earendil-works/pi-coding-agent >=0.74 if a session replacement happens during init (resume, fork, switch, reload). Hit this on pi-coding-agent@0.79.6.

Error: This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload().
    at ExtensionRunner.assertActive (.../runner.js:334:19)
    at get cwd (.../runner.js:431:24)
    at onSession (.../pi-gitnexus/src/index.ts:193:47)

Root cause

src/index.ts:273-277 registers the session_start handler as fire-and-forget:

pi.on('session_start', (_event: unknown, ctx: ExtensionContext) => {
  void onSession(ctx).catch(err => {
    ctx.ui.notify(`GitNexus session init failed: ${err.message}`, 'error');
  });
});

onSession(ctx) does several awaits: resolveShellPath(), probeGitNexusBinary() (spawn), findGitNexusIndex(ctx.cwd). The synchronous handler returns immediately after kicking off the promise. The agent loop sees session_start as done, may run a session replacement next turn (or even concurrently during boot, depending on launcher), and then onSession's async tail reads ctx.cwd / calls ctx.ui.notify on a now-stale ctx. Upstream's ExtensionRunner.assertActive throws on any access.

Fix

Make the handler async and await onSession. The runner serially awaits each session_start handler, so init completes before the loop continues and ctx replacement can happen.

Also switched the catch from ctx.ui.notify to console.error so the error path doesn't trip the same staleness check if the throw happens after handler resumption.

Diff

-  pi.on('session_start', (_event: unknown, ctx: ExtensionContext) => {
-    void onSession(ctx).catch(err => {
-      ctx.ui.notify(`GitNexus session init failed: ${err.message}`, 'error');
-    });
+  pi.on('session_start', async (_event: unknown, ctx: ExtensionContext) => {
+    try {
+      await onSession(ctx);
+    } catch (err) {
+      // Don't touch ctx here — it may be stale if onSession ran past replacement.
+      console.error('[pi-gitnexus] session init failed:', err instanceof Error ? err.message : String(err));
+    }
   });

Repro

Launch pi with pi-gitnexus enabled, then fork/switch/reload a session before GitNexus init completes (onSession is awaiting resolveShellPath() and probeGitNexusBinary()). On >=0.74 the next ctx access in the deferred chain throws via assertActive.

In my case it surfaced on cold-start of a fresh pi-ult-code launch with ~30 other extensions also firing session_start in parallel, which pushed the agent loop's session replacement window past gitnexus's spawn time.

Notes

  • Side effect: session_start is now awaited, so a slow probeGitNexusBinary() will delay pi boot. In practice the probe is fast (single gitnexus --version spawn), but trySpawn at src/index.ts:80 has no timeout. If anyone hits a hanging custom gitnexus-cmd, boot blocks until the upstream runner watchdog skips it (or indefinitely on older runners without that watchdog). Filing this as a follow-up rather than scope-creeping this PR; happy to add the timeout here if you want.
  • No tests added; pi-gitnexus has no extension-runtime test surface today.
  • Verification: git diff --check clean. Verified at runtime against @earendil-works/pi-coding-agent@0.79.6 via a pi-ult-code launch with the full extension set loaded; pre-fix the launch crashed on the trace above, post-fix it boots clean and /gitnexus status works.

Fire-and-forget onSession() could resolve after a session was replaced, then
read ctx.cwd / call ctx.ui.notify on the now-stale ctx and trigger
ExtensionRunner.assertActive throw. Await the handler so the runner serializes
init before any replacement; log via console.error in the catch so the error
path doesn't access ctx either.

Surfaces on @earendil-works/pi-coding-agent >=0.74. Tested at 0.79.6.
@tintinweb
tintinweb merged commit 9af9e1e into tintinweb:master Jun 23, 2026
1 check passed
tintinweb added a commit that referenced this pull request Jun 23, 2026
  session_start is now awaited (#16), so a hung `gitnexus --version`
  would stall pi boot. trySpawn had no timeout (unlike resolveShellPath).
  Add a default 5s ceiling so a slow/broken gitnexus-cmd can't block init.
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