Description
Before submitting
- I searched existing issues: this looks like a regression of bug(skill-registry): /reload crashes Pi — leaked watcher debounce timer notifies through the stale session ctx #2580 / bug(skill-registry): /resume intermittently crashes Pi with a stale-ctx uncaughtException (the #2580 timer also fires on session switch) #3072 (closed), same stale-ctx family.
Problem
Idle gentle-shell sessions exit on their own with an uncaught stale-ctx error thrown from the skill-registry extension. No user action, no tool running, no background job.
Steps to reproduce
- Start several gentle-shell sessions (one per terminal pane) on Windows and leave them idle for hours.
- One of them was also relaunched with
gentle-shell -cand exited again within minutes while idle.
Observed overnight on one host: 7 idle sessions exited (several at about the same time), including 4 of 6 idle worker sessions.
Expected and actual behavior
Expected: an idle session survives session replacement/reload and the registry's background refresh; a failure in that refresh is logged, not fatal.
Actual: the process dies:
... do not use the old ctx after await ctx.reload().
at ExtensionRunner.assertActive (.../@earendil-works/pi-coding-agent/1.5.1/.../node_modules/@earendil-works/pi-coding-agent/...)
at get ui (.../pi-coding-agent/...)
at .../gentle-pi/extensions/skill-registry.ts:950:15
at .../gentle-pi/extensions/skill-registry.ts:813:11
A stack frame came from loaded extension `.../gentle-pi/extensions/skill-registry...`
The second exit printed no crash text.
gentle-pi version
4.7.0 (crashing sessions; store hash c13de932...). Upgraded since to main b6f2556540ef; not yet observed there over a full night.
Pi version
1.5.1 in the crashing sessions (others on 1.5.2).
Operating system
Windows 11 Pro (26300).
Logs
Above. Likely cause (preliminary): a timer or async callback in skill-registry.ts (around lines 813/950) keeps a ctx captured before a session replacement/reload and later reads ctx.ui.
Activity
arielbritorivera commentedon Nov 12, 2026
Reproduced on Linux too (not only Windows), and the mechanism can be pinned down more precisely than "a timer or async callback keeps a ctx captured".
Environment
Mechanism
refresh()arms its debounce through a function-localtimer(WATCH_DEBOUNCE_MS, 500 ms). That timer is not registered anywhere, so nothing outside the closure can cancel it.session_shutdownfor the old extension instance, andcloseSkillRegistryWatchers()closes the watchers and clears the poll intervals — but it cannot reach that local debounce, so an armed timer survives the shutdown.ctx. Readingctx.uiis what throws: Pi'sExtensionRunner.assertActive()rejects any access to a ctx invalidated by the replacement.So it is the escaping debounce that reaches the stale ctx, not the poll interval: the poll is cleared correctly on shutdown.
Status on main
acf6ec84already fixes this on main: the debounce is registered in awatcherTimersmap thatcloseSkillRegistryWatchers()clears, thegeneration !== startupGenerationguards stop a refresh that belongs to a replaced session, andnotifyIfLive()makes a stale ctx unable to throw out of the extension.The gap is the release, not the fix: 4.7.0 is still the latest published version on npm, so every user of the published package keeps the crashing code. "Upgraded to main" only helps source users. For released users the practical mitigation until the next release is to disable the extension (
--no-skill-registry, orGENTLE_PI_NO_SKILL_REGISTRY=1), which costs the automatic.atl/skill-registry.mdrefresh and leaves/skill-registry:refreshas the manual path.Also worth noting for triage: #2955 looks superseded by
acf6ec84— both of its changes (registering the debounce so shutdown clears it, and wrappingctx.ui.notify) are present on main now.aiorellana502 commentedon Nov 13, 2026
Data point from Windows on a
mainbuild, in case it helps close this out.Environment: Windows 11 Pro, gentle-pi
4.7.0-main.b1af8a493a4e(includesacf6ec84/ #3127), Pi 1.5.2, Node v24.13.0, sessions running inside herdr panes.What I did: left two gentle-shell sessions idle overnight, with no input, tools or background jobs, and logged both processes (launcher and Pi runtime) every 5 minutes from a separate detached process.
Result: 120 consecutive checks over ~10 h (21:36 → 07:31 local), no gaps, and every process stayed up. No exit and no stale-ctx trace. The machine went into Modern Standby afterwards, and both sessions were still alive on resume.
On 4.7.0 this is the scenario where idle sessions died, so on Windows it looks fixed on
main. This only covers the idle case; I didn't specifically time a reload against an armed debounce. As noted above, the remaining gap for most users is the npm release.JuanD-coder commentedon Nov 13, 2026
One more occurrence on the published stable release, in case it helps confirm the fix needs to ship.
Environment: Windows 11 Pro, gentle-pi
4.7.0(stable, npmlatest), Pi1.5.0, bundled self-contained install (gentle-shell upgrade, its own Node24.21.0).Observed: two entries in the agent home's
crashes.json, bothuncaught_exceptionwith the same stale-ctx message and the same frames as this issue:I did not capture what the session was doing at the time, so I can't say whether it was idle or right after a
/reload. The installed build predates #3127 (acf6ec84), which per the comment above is only onmainso far.notrepeat commentedon Nov 13, 2026
Another observed occurrence on Linux with installed gentle-pi 4.7.0, Pi 1.5.2, and Node v26.13.0. Pi exited with the same stale-context exception and extension frames:
The action immediately preceding this crash was not recorded, so this is not a claimed deterministic
/reloador/resumereproduction. Local inspection confirms that the installed extension is byte-identical to thev4.7.0Git blob65a44b4777bb4e5ac9338803d95b727edf257536. Its debounce survives shutdown and its error handler retries the notification through the same expired context.The lifecycle fix from #3127 (
acf6ec84e050e78d38e6aa12b01cbfef38ca11b1) is already present on current main (5824439001adc536bd23cdd98cf33e9b14a68ca5). This is additional evidence of the installed-release gap, not a regression against that fixed main. Please include the fix in a published release; a new main-branch implementation is not needed.JpadcTools commentedon Nov 13, 2026
A data point that closes the open question in the two comments above: the action is recorded this time.
Environment: Linux on WSL2 (kernel
6.18.58.1-microsoft-standard-WSL2), Pi1.5.2, npm-installed[email protected](not the bundled self-contained build), Nodev22.22.1.Trigger:
/new. Pi printed the stale-ctxuncaughtExceptionand exited immediately after replacing the session; frames were identical to the report (skill-registry.ts:950:15and:813:11). So the crash is reachable from session replacement, not only from/reloador an idle session.Confirmed in the installed 4.7.0 code (
extensions/skill-registry.ts, byte-identical to thev4.7.0blob already cited):refresh()arms its debounce in a function-localtimer(500 ms);closeSkillRegistryWatchers()only closes watchers and polls, so a timer armed just before the replacement still fires.startupGenerationguard, so after the replacement it reaches thenotifyclosure fromsession_start, which readsctx.uion the replaced session.catchblock, so the stale-ctx throw escapes thetryas an uncaught exception instead of a warning — which is exactly why the process dies at:813reporting a failure at:950.Nothing here contradicts the earlier comment: the lifecycle fix (
acf6ec84e0, #3127) is onmain, and 4.7.0 still ships the unguarded version. This is added evidence for shipping the fix in a published release, not a new issue and not a regression againstmain.