Skip to content

gentle-engram (Pi plugin): stale ctx crash on session_compact during auto-compaction #591

Description

@ekoleszar

🐛 Bug Report

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

The gentle-engram Pi extension (plugin/pi/index.ts) crashes with a stale-ctx error when Pi performs auto-compaction after an agent run.

The session_compact event handler accesses ctx.cwd and ctx.sessionManager.getSessionId() on a context that Pi has already invalidated after replacing the session during auto-compaction. The Pi runtime's ExtensionRunner.assertActive() catches this and throws:

Extension ".../gentle-engram/index.ts" 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:331:19)
  at get cwd (runner.js:437:24)
  at gentle-engram/index.ts:778:24
  at AgentSession._runAutoCompaction
  at AgentSession._checkCompaction
  at AgentSession._handlePostAgentRun

🔄 Steps to Reproduce

  1. Install gentle-engram Pi extension (version 0.1.10)
  2. Use Pi with the extension loaded and perform enough interaction to trigger auto-compaction
  3. The extension crashes with the stale-ctx error

Auto-compaction runs automatically after agent responses when the context window is under pressure. The exact trigger is Pi-internal, but it happens consistently during normal use.

✅ Expected Behavior

The session_compact handler should handle the compaction event gracefully without crashing. The extension should continue to function normally after auto-compaction.

❌ Actual Behavior

The extension throws an unhandled error during session_compact because it accesses ctx properties on a stale context. After the crash, engram memory features (mem_save, mem_search, etc.) may become unavailable until Pi is restarted.

🖥️ Environment

Operating System

Linux (Ubuntu/Debian)

Engram Version

0.1.10 (gentle-engram npm package, plugin/pi/)

Agent / Client

OpenCode (Pi coding agent)

💡 Additional Context

Root cause: During auto-compaction, Pi replaces the session internally. The session_compact event fires after the new session is already active, so the ctx parameter passed to the handler is stale. Any access to ctx.cwd, ctx.sessionManager, or any other ctx.* property throws ExtensionRunner.assertActive.

Fix applied locally (in plugin/pi/index.ts):

Three changes:

  1. Added currentSessionId module-level variable — caches the active session ID
  2. session_compact handler — no longer accesses ctx. Uses process.cwd() instead of ctx.cwd, and reads from cached currentSessionId instead of calling getSessionId(ctx). The ctx parameter is renamed to _ctx to signal intent.
  3. before_agent_start handler — captures currentSessionId = getSessionId(ctx) right after initOnce, since this handler receives a fresh, valid ctx.

Key insight: process.cwd() is safe because the process working directory does not change during the session lifecycle.

Full diff of the fix:

+ let currentSessionId: string | undefined;

-  pi.on("session_compact", async (event, ctx) => {
-    await initOnce(ctx.cwd);
-    const sessionId = getSessionId(ctx);
+  pi.on("session_compact", async (event, _ctx) => {
+    // session_compact fires during auto-compaction, after Pi has already
+    // replaced the session — the passed ctx is stale. Do NOT access it.
+    await initOnce(process.cwd());
+    const sessionId = currentSessionId;

   pi.on("before_agent_start", async (event, ctx) => {
     await initOnce(ctx.cwd);
-    const sessionId = getSessionId(ctx);
+    currentSessionId = getSessionId(ctx);
+    const sessionId = currentSessionId;

File path in repo: plugin/pi/index.ts (published as gentle-engram on npm).

Note: other handlers (session_shutdown, before_tool_call, etc.) also access ctx — those are safe because they fire while the session is still active. Only session_compact is affected because it fires during the session replacement window.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions