Skip to content

bug(skill-registry): idle sessions still crash with stale ctx after reload on Windows (regression of #2580/#3072) #3220

Open

Description

@ardelperal

Before submitting

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

  1. Start several gentle-shell sessions (one per terminal pane) on Windows and leave them idle for hours.
  2. One of them was also relaunched with gentle-shell -c and 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

  1. arielbritorivera commented on Nov 12, 2026

    @arielbritorivera

    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

    • gentle-pi 4.7.0 (the latest published release), Pi 1.5.0, Linux.
    • No user action is needed to trigger it: the crash landed 0.46 s after the replacement session's file was created, which is consistent with the 500 ms debounce of a refresh that had been armed just before the replacement.

    Mechanism

    1. refresh() arms its debounce through a function-local timer (WATCH_DEBOUNCE_MS, 500 ms). That timer is not registered anywhere, so nothing outside the closure can cancel it.
    2. On a session replacement Pi emits session_shutdown for the old extension instance, and closeSkillRegistryWatchers() closes the watchers and clears the poll intervals — but it cannot reach that local debounce, so an armed timer survives the shutdown.
    3. About 500 ms later the escaped timer runs the refresh and reaches the notice closure that still holds the replaced session's ctx. Reading ctx.ui is what throws: Pi's ExtensionRunner.assertActive() rejects any access to a ctx invalidated by the replacement.
    4. The throw leaves an unawaited async callback, so it surfaces as an unhandled rejection / uncaught exception and takes the process down.

    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

    acf6ec84 already fixes this on main: the debounce is registered in a watcherTimers map that closeSkillRegistryWatchers() clears, the generation !== startupGeneration guards stop a refresh that belongs to a replaced session, and notifyIfLive() 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, or GENTLE_PI_NO_SKILL_REGISTRY=1), which costs the automatic .atl/skill-registry.md refresh and leaves /skill-registry:refresh as 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 wrapping ctx.ui.notify) are present on main now.

  2. aiorellana502 commented on Nov 13, 2026

    @aiorellana502
    Contributor

    Data point from Windows on a main build, in case it helps close this out.

    Environment: Windows 11 Pro, gentle-pi 4.7.0-main.b1af8a493a4e (includes acf6ec84 / #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.

  3. JuanD-coder commented on Nov 13, 2026

    @JuanD-coder

    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, npm latest), Pi 1.5.0, bundled self-contained install (gentle-shell upgrade, its own Node 24.21.0).

    Observed: two entries in the agent home's crashes.json, both uncaught_exception with the same stale-ctx message and the same frames as this issue:

    2026-11-09T13:41:12.648Z  v1.5.0  uncaught_exception
    2026-11-10T18:07:56Z      v1.5.0  uncaught_exception
      at ExtensionRunner.assertActive (pi-coding-agent/dist/bundle/chunks/...)
      at get ui (pi-coding-agent/dist/bundle/chunks/...)
      at gentle-pi/extensions/skill-registry.ts:950:15
      at gentle-pi/extensions/skill-registry.ts:813:11
    

    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 on main so far.

  4. notrepeat commented on Nov 13, 2026

    @notrepeat

    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:

    Error: This extension ctx is stale after session replacement or reload.
        at ExtensionRunner.assertActive (<npm-global>/gentle-pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/...)
        at get ui (<npm-global>/gentle-pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/...)
        at extensions/skill-registry.ts:950:15
        at extensions/skill-registry.ts:813:11
    

    The action immediately preceding this crash was not recorded, so this is not a claimed deterministic /reload or /resume reproduction. Local inspection confirms that the installed extension is byte-identical to the v4.7.0 Git blob 65a44b4777bb4e5ac9338803d95b727edf257536. 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.

  5. JpadcTools commented on Nov 13, 2026

    @JpadcTools

    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), Pi 1.5.2, npm-installed [email protected] (not the bundled self-contained build), Node v22.22.1.

    Trigger: /new. Pi printed the stale-ctx uncaughtException and exited immediately after replacing the session; frames were identical to the report (skill-registry.ts:950:15 and :813:11). So the crash is reachable from session replacement, not only from /reload or an idle session.

    Confirmed in the installed 4.7.0 code (extensions/skill-registry.ts, byte-identical to the v4.7.0 blob already cited):

    1. refresh() arms its debounce in a function-local timer (500 ms); closeSkillRegistryWatchers() only closes watchers and polls, so a timer armed just before the replacement still fires.
    2. That callback has no startupGeneration guard, so after the replacement it reaches the notify closure from session_start, which reads ctx.ui on the replaced session.
    3. The failure inside the callback is reported through the same closure in the catch block, so the stale-ctx throw escapes the try as an uncaught exception instead of a warning — which is exactly why the process dies at :813 reporting a failure at :950.

    Nothing here contradicts the earlier comment: the lifecycle fix (acf6ec84e0, #3127) is on main, 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 against main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions