Uncaught 'stale extension ctx' in continueWhenIdle crashes host process after plan approval (embedded/multi-node hosts)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- devtools
Research direction
Start in apps/pi-extension/index.ts at the agent_end handler and the continueWhenIdle callback in the plannotator_submit_plan flow. Reproduce the embedded multi-node lifecycle where the planner session is disposed after approval, then verify that a stale ctx cannot crash the host and that the workflow advances to the next node.
Written by the indexing model from the issue text.
Description
Summary
The continueWhenIdle callback registered in the agent_end handler (apps/pi-extension/index.ts, plannotator_submit_plan flow) throws an uncaught "stale extension ctx" error inside a setTimeout after a plan is approved, which crashes the host process when the planner session is torn down before the deferred callback runs.
This does not happen in plannotator's primary single-session flow (where plan + execution stay in the same pi session). It only happens in an embedded, multi-node host (Archon) that disposes of the planner session right after the plan is approved and the node completes. The crash orphans the whole workflow run.
Version
@plannotator/pi-extension 0.23.1, loaded from source (index.ts) via bun.
Reproduction
Host: Archon running a multi-node workflow where plannotator_submit_plan is used in a dedicated "planner" node and implementation is a separate node (a fresh pi session).
- Planner node calls
plannotator_submit_plan→ plan review browser opens on127.0.0.1:19432. - User approves the plan in the browser.
plannotator_submit_planreturns the approved prompt withterminate: true;phase = "executing",justApprovedPlan = true.- The planner prints its "plan approved" message and the turn ends →
agent_endfires. - In
agent_end, becausephase === "executing" && justApprovedPlan, the code schedulessetTimeout(continueWhenIdle, 0), wherecontinueWhenIdlepolls the capturedctx.isIdle()and then callspi.sendUserMessage("Continue with the approved plan."). - The host disposes of the planner session (the node completed) — this invalidates the captured
ctx's runner (runner.staleMessageis set byrunner.invalidate(), called fromsession.dispose()). - The deferred
continueWhenIdleruns, callsctx.isIdle()→runner.assertActive()→ throws becauserunner.staleMessageis set. - The error is uncaught inside
setTimeout→ the host's Bun process crashes → the workflow run is orphaned (markedrunningin the DB, but no live process; it can never advance to the next node).
Stack trace (exact)
333 | this.runtime.invalidate(message);
334 | }
335 | }
336 | assertActive() {
337 | if (this.staleMessage) {
338 | throw new Error(this.staleMessage);
^
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(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().
at assertActive (.../@earendil-works/pi-coding-agent/dist/core/extensions/runner.js:335:46)
at isIdle (.../@earendil-works/pi-coding-agent/dist/core/extensions/runner.js:461:24)
at continueWhenIdle (.../@plannotator/pi-extension/index.ts:1160:24)
Bun v1.3.14 (Linux x64)
Root cause
In apps/pi-extension/index.ts (around the agent_end handler):
pi.on("agent_end", async (_event, ctx) => {
if (phase === "executing" && justApprovedPlan) {
justApprovedPlan = false;
let attempts = 0;
const continueWhenIdle = (): void => {
if (!ctx.isIdle()) { // <-- ctx captured here, used after session disposal
attempts += 1;
if (attempts <= 200) setTimeout(continueWhenIdle, 50);
return;
}
pi.sendUserMessage("Continue with the approved plan.");
};
setTimeout(continueWhenIdle, 0); // <-- deferred; session may be disposed by then
return;
}
...
ctx is captured in the agent_end handler and then used in a deferred setTimeout callback. pi's extension-ctx contract explicitly says: "Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload()." An embedded host that disposes of the planner session after the node completes (entirely reasonable for a multi-node architecture) invalidates that captured ctx's runner, so the deferred ctx.isIdle() throws. Because the throw happens inside setTimeout, it is uncaught and takes down the host process.
This is fine in plannotator's interactive single-session flow (the session stays alive, so the captured ctx never goes stale), but it is not safe for embedded hosts that manage the session lifecycle per-node.
Proposed fix (plannotator-side, defensive)
Make continueWhenIdle resilient to a disposed/stale ctx so an embedded host is not taken down. Minimal change:
const continueWhenIdle = (): void => {
try {
if (!ctx.isIdle()) {
attempts += 1;
if (attempts <= 200) setTimeout(continueWhenIdle, 50);
return;
}
pi.sendUserMessage("Continue with the approved plan.");
} catch {
// The planner session was replaced/disposed by the host after the plan
// was approved and the node completed. There is nothing to continue —
// stop the loop so the host can advance to its next node instead of
// crashing the process with an uncaught "stale extension ctx" error
// from a captured ctx.
}
};
Alternative (stronger): instead of capturing ctx, re-acquire a live reference inside the callback, or guard with a "session still active" check before polling.
Workaround (tested locally)
We applied the try/catch above locally and confirmed the crash no longer occurs: the workflow advances cleanly from the planner node to the next node (verify-plan → implement) after plan approval, with 0 stale-ctx errors in the run log. In our host (Archon) the "Continue with the approved plan" continuation is unwanted anyway — implementation is a separate node — so silently stopping the loop is exactly the desired behavior.
Environment
@plannotator/pi-extension0.23.1 (loaded fromindex.ts)- pi-coding-agent 0.80.7 (bun hoisted cache copy)
- Bun 1.3.14, Linux x64
- Host: Archon (multi-node workflow, planner session disposed after node completes)
Happy to provide more detail or a minimal host repro if useful. Thanks!
- Dominant language
- TypeScript
- Stars
- 8.7k
- Forks
- 649
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 113
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from backnotprop/plannotator
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
backnotprop/plannotator#1503 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
backnotprop/plannotator#1502 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
backnotprop/plannotator#1359 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
backnotprop/plannotator#1086 · 2 comments ·
-
Difficulty 5/5 Over a week Newbie friendliness 45/100
backnotprop/plannotator#1590 ·
All issues in backnotprop/plannotator
Similar issues
-
bot:ai-assisted component:compact-js status:untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
midnightntwrk/midnight-sdk#403 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
joinmarket-webui/jam#1496 · 1 comment ·
-
Tech
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
opencrvs/opencrvs-core#13865 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
linagora/twake-calendar-frontend#1377 · 1 comment ·
-
Difficulty 1/5 Under an hour Newbie friendliness 72/100
EclipseFdn/open-vsx.org#13385 · 1 comment ·