Skip to content

[BUG]: Task sub-session disposal invalidates the parent Pi ResourceLoader context #428

Description

@leather-s

Describe the bug

In Shannon v2.5.2, a Pi task sub-session receives the parent agent's ResourceLoader. When the child finishes, subSession.dispose() invalidates the extension runtime held by that shared loader. The parent session—and parallel sibling task sessions using the same loader—then receive Pi's deterministic stale-context error from subsequent tools:

This extension ctx is stale after session replacement or reload.

The agent can continue producing LLM turns after its tools are unusable, so the visible failure is often a later and misleading OutputValidationError. In one run, pre-recon emitted a blocked message after every tool became stale; downstream vulnerability agents later repeatedly failed output validation after doing substantial work.

This appears provider-independent: it is caused by local session/resource ownership, after a child task completes successfully.

Steps to reproduce

  1. Check out the latest release, v2.5.2, and build/run in local mode.

  2. Configure a source-available authorized target large enough for an agent to delegate work through the task tool. Parallel task calls make the failure especially easy to reproduce.

  3. Start a fresh workspace:

    ./shannon start \
      --url http://host.docker.internal:<port> \
      --repo /path/to/private-repo \
      --config /path/to/redacted-config.yaml \
      --workspace redacted-fresh-workspace \
      --follow
  4. Inspect workspaces/<workspace>/.shannon/agents/*pre-recon* or a vulnerability-agent log.

  5. Observe that child task calls complete, then later parent tools such as read, ls, todo_write, or deliverable collector tools return the stale-extension-context message.

  6. The agent eventually finishes without required deliverables, leading to output-validation retries/failure.

Expected behaviour

Each child task should own an isolated extension runtime. Disposing one child session must not invalidate its parent or parallel siblings. If a stale-runtime condition somehow occurs anyway, Shannon should stop that agent immediately with a specific execution error rather than continue spending tokens and surface a generic output-validation failure.

Actual behaviour

The parent and all task children share one loader/runtime. The first child disposal invalidates that runtime. Subsequent tools fail deterministically, but the agent continues generating turns and may retry required output calls until Temporal reports an OutputValidationError.

Pre-submission checklist (required)

  • I have searched the existing open issues and confirmed this exact bug has not already been reported.
  • I am running the latest released version of shannon (v2.5.2).

If applicable

  • I have included relevant error messages, stack traces, or failure details.
  • I have checked the workspaces folder for logs and pasted the relevant errors.
  • I have inspected the failed Temporal workflow run and included the failure reason.
  • I have included clear steps to reproduce the issue.
  • I have redacted sensitive information, including tokens, target URLs, repository names, workspace IDs, and product details.

Debugging details

Sanitized agent-log sequence after delegated tasks completed:

tool_end read       -> This extension ctx is stale after session replacement or reload. ...
tool_end todo_write -> This extension ctx is stale after session replacement or reload. ...
tool_end ls         -> This extension ctx is stale after session replacement or reload. ...
tool_end collector  -> This extension ctx is stale after session replacement or reload. ...
LLM                 -> Blocked by a Pi harness failure: all tools now return `ctx is stale ...`

The same message repeated for every parent tool call. A downstream Temporal failure surfaced only the secondary symptom:

ApplicationFailure: Agent injection-vuln failed output validation
applicationFailureInfo.type: OutputValidationError

Root cause in current source

Current main still has the same ownership chain as v2.5.2:

  1. pi-executor.ts creates one parent loader and passes it to createTaskTool.
  2. task-tool.ts passes that exact loader into every child createAgentSession.
  3. task is explicitly parallel, so sibling sessions can also share the same loader concurrently.
  4. subSession.dispose() always runs.

In @earendil-works/pi-coding-agent 0.82.1, DefaultResourceLoader caches the loaded extension runtime. Each AgentSession builds its ExtensionRunner from that runtime, and AgentSession.dispose() invalidates the runner/runtime. Therefore a shared loader is not safe across independently disposed sessions.

Tested fix

A minimal local patch on the v2.5.2 baseline fixed the ownership problem:

 interface TaskToolContext {
-  resourceLoader: ResourceLoader;
+  createResourceLoader: () => Promise<ResourceLoader>;
 }

 async execute(...) {
+  const resourceLoader = await config.createResourceLoader();
   const { session: subSession } = await createAgentSession({
     ...,
     resourceLoader,
   });
 }

The parent executor supplies a factory using the same existing buildResourceLoader(cwd, logger, agentName) path. This preserves Shannon's extension, skill, browser-agent, permission-system, and bash-timeout configuration while giving every task invocation its own loader/runtime. The factory must be called inside each execute, not once when constructing the parallel tool.

I also added a defensive check on tool_execution_end for the exact stale-context prefix. If observed, it records an AGENT_EXECUTION_FAILED, aborts the session, and preserves that specific non-retryable error rather than allowing the run to degrade into output-validation retries. Parent-session disposal was moved into finally so failure paths are cleaned up too.

Local regression coverage:

  • Two concurrent task.execute calls receive distinct loader objects and both child sessions dispose independently.
  • The stale-context detector matches the Pi text result but not ordinary text/image tool results.
  • TypeScript --noEmit check and build pass.
  • Both regression tests pass on the host and inside the rebuilt shannon-worker image.
  • A fresh patched workflow entered pre-recon attempt 1 without an immediate failure at the time of filing. Full end-to-end execution is still in progress, so this last point is preliminary rather than a completion claim.

Related: #62 describes the broad OutputValidationError symptom in older runs, with several unrelated causes. This report is separate because it has a deterministic v2.5.2 Pi lifecycle signature and a source-level ownership cause.

Screenshots

Not included; the exact sanitized agent-log and Temporal failure text are provided above.

CLI mode

Local (./shannon)

Provider

OpenAI (Codex OAuth through Pi authentication)

Full command with all flags used (with redactions)

./shannon start --url http://host.docker.internal:<port> --repo /path/to/private-repo --config /path/to/redacted-config.yaml --workspace redacted-fresh-workspace --follow

OS (with version)

macOS 26.6 (Build 25G72), Apple Silicon

Node.js version (node -v)

v22.22.2

Docker version (docker -v)

Docker version 29.4.0, build 9d7ad9f

Additional context

  • Shannon release: v2.5.2
  • Shannon baseline commit: 12d1c48a78658af9b59cd36f12d8b683a684b1d3
  • Pi coding-agent dependency: 0.82.1
  • Current main inspected at 53118c62032747b039e3abbe895096c5eb9ec8e1 still contains the shared-loader path.
  • No PR is being opened because the project README says external code contributions are not currently accepted.

Full Temporal stack trace

The initial report included the failure summary but omitted the actual stack trace. This is the sanitized trace from the failed vulnerability-analysis activity:

ApplicationFailure: Agent injection-vuln failed output validation
    at ApplicationFailure.create (/app/node_modules/.pnpm/@temporalio+common@1.15.0/node_modules/@temporalio/common/lib/failure.js:183:16)
    at runAgentActivity (file:///app/apps/worker/dist/temporal/activities.js:164:48)
    at async Activity.execute (/app/node_modules/.pnpm/@temporalio+worker@1.15.0_tslib@2.8.1/node_modules/@temporalio/worker/lib/activity.js:101:20)
    at async NativeConnection.withAbortSignal (/app/node_modules/.pnpm/@temporalio+worker@1.15.0_tslib@2.8.1/node_modules/@temporalio/worker/lib/connection.js:172:16)
    at async Client.withAbortSignal (/app/node_modules/.pnpm/@temporalio+client@1.15.0/node_modules/@temporalio/client/lib/base-client.js:65:16)
    at async /app/node_modules/.pnpm/@temporalio+worker@1.15.0_tslib@2.8.1/node_modules/@temporalio/worker/lib/activity.js:161:32
    at async /app/node_modules/.pnpm/@temporalio+worker@1.15.0_tslib@2.8.1/node_modules/@temporalio/worker/lib/worker.js:77

Associated failure details:

{
  "message": "Agent injection-vuln failed output validation",
  "source": "TypeScriptSDK",
  "applicationFailureInfo": {
    "type": "OutputValidationError",
    "details": {
      "payloads": [
        {
          "agentName": "injection-vuln",
          "attemptNumber": 1,
          "elapsed": 170931
        }
      ]
    }
  }
}

Metadata

Metadata

Assignees

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