preToolUse silently dropped after mid-loop extensions_reload (snapshot staleness in runAgenticLoop)
- Lenguaje dominante
- Shell
- Estrellas
- 11.2k
- Forks
- 1.9k
- Merge medio
- 14 h 16 min
- PR fusionados (30 d)
- 6
Descripción
## Summary
The agent's main reaction loop captures the session's hooks **once** at loop start and then uses that snapshot for every subsequent iteration. After `extensions_reload` is called from inside the loop, the snapshot still references the now-dead old extension's IPC proxy. The proxy's calls fail with `EPIPE` / `"connection reset"` / `"stream was destroyed"`, which are caught and silently swallowed by the proxy's own error handler.
The result: `preToolUse`, `userPromptSubmitted`, and `sessionStart` invocations registered via that proxy are silently dropped for the rest of the loop. Meanwhile `postToolUse` (and other paths that call `getEffectiveHooks()` live each time) continue to fire on the freshly-launched extension instance.
For an extension that observes both pre and post hooks, every tool call that happens after a mid-loop `extensions_reload` becomes a `postToolUse`-only event with no matching `preToolUse`. The behavior persists until the user submits the next prompt and a fresh `runAgenticLoop` retakes the snapshot.
## Repro
Minimal extension that registers `preToolUse` and `postToolUse` and logs both:
```js
import { joinSession } from "@github/copilot-sdk/extension";
const session = await joinSession({
hooks: {
onPreToolUse: async (input) => { console.error("PRE", input.toolName); },
onPostToolUse: async (input) => { console.error("POST", input.toolName); },
},
});
```
Then in a Copilot CLI session with the extension installed:
1. Send a prompt that triggers a tool call. Both `PRE` and `POST` fire.
2. Send a prompt that asks the agent to call `extensions_reload` and then run another tool in the same response (e.g. *"reload extensions, then list the files in the current directory"*).
3. Observe extension logs (or the CLI's own debug logs).
**Expected:** `PRE` and `POST` both fire for the post-reload tool call.
**Actual:** Only `POST` fires for the post-reload tool call. `PRE` is silently dropped. Every subsequent tool call in the same agentic loop has the same behavior. The next user-submitted prompt restarts the loop and the behavior self-heals.
## Root cause (reading shipped CLI `1.0.43-0`)
Path: `/app.js`.
Inside `runAgenticLoop` (one call per user prompt; iterates internally through many LLM-tool cycles):
```js
async runAgenticLoop(...) {
...
let _ = this.getEffectiveHooks(); // ← SNAPSHOT taken once
try {
let T = await h1(_?.userPromptSubmitted, ...); // uses snapshot
...
new XIr(_, this.workingDir, this.sessionId, ...) // PreToolUseHooksProcessor uses snapshot
...
}
}
```
Vs. `processToolExecutionResult` (one call per tool result, anywhere in the same loop):
```js
async processToolExecutionResult(toolName, toolArgs, toolResult) {
let s = (toolResult.resultType === "success"
? await h1(this.getEffectiveHooks()?.postToolUse, ...) // ← LIVE per call
: void 0);
...
}
```
Hook proxies for connected extensions are built via `createHooksProxy(sessionId, connection)`. Each proxy closes over the IPC `connection` to that specific extension instance:
```js
createHooksProxy(sessionId, connection) {
let n = async (hookType, input) => {
...
try {
return (await connection.sendRequest(c0.HOOKS_INVOKE, { sessionId, hookType, input })).output;
} catch (a) {
let l = q(a), c = l.toLowerCase();
// EPIPE / broken pipe / EOF / closed / connection reset / write after end /
// stream was destroyed / shutting down → just log; otherwise warn.
...
return; // ← swallowed
}
};
return {
preToolUse: [o => n("preToolUse", o)],
postToolUse: [o => n("postToolUse", o)],
...
};
}
```
When `extensions_reload` runs `stopAllExtensions()`, the old child process is killed (`SIGTERM` then `SIGKILL` after 5 s) and its connection is disposed. The new extension is launched and registers fresh hooks via `addAdHocHooks(connectionId, createHooksProxy(sessionId, newConnection))`.
But the snapshot `_` captured at the top of `runAgenticLoop` still holds the **old** proxy in its hook arrays. `preToolUse` calls on the snapshot route through the dead proxy → `sendRequest` rejects → catch swallows → returns `undefined` → no extension sees the hook.
## Suggested fix (one of)
1. **Move the snapshot to live look-up.** Replace `_=this.getEffectiveHooks()` with live calls at each hook invocation site (mirroring `processToolExecutionResult`). This is the smallest semantic change.
2. **Refresh the snapshot reactively.** When an extension connection drops or a new one is registered (`addAdHocHooks` / `removeAdHocHooks`), invalidate any in-flight `runAgenticLoop` snapshot.
3. **Refresh the snapshot after `extensions_reload` specifically.** Smaller scope, but doesn't cover other ways an extension might disconnect mid-loop (crashes, network issues for remote sessions, etc.).
Of these, (1) is the most consistent with how `postToolUse` already works.
## Impact
- Extensions that observe `preToolUse` for tool tracking, telemetry, audit logs, fault injection, or UI live-update have visible gaps after any mid-loop `extensions_reload`.
- `userPromptSubmitted` is also affected (same snapshot), so any extension that relies on it for memory / state could miss prompts after a mid-loop reload (though the reload itself is rarely between submission and tool execution, so this is less observable).
- `sessionStart` is fired through the same captured snapshot earlier in the loop, but is gated by `!this._sessionStartHooksFired`, so its impact is bounded to the very first iteration of the very first loop.
- Symptoms are silent — no warning is surfaced to the user, the agent, or the extension. Extensions just stop receiving certain hooks.
## Environment
- `@github/copilot` `1.0.43-0` (Windows `win32-x64` package).
- Reproduced on Windows 11.
- Source confirmed by reading the bundled `app.js` shipped in the CLI package.
Guía de contribución
Línea de trabajo
Start with the bundled app.js entry points named in the report: runAgenticLoop, processToolExecutionResult, createHooksProxy, and addAdHocHooks. Reproduce an extensions_reload followed by another tool call in the same agentic loop, then trace whether hooks use the current extension connection. Done means both preToolUse and postToolUse fire after reload without silently using the disposed proxy.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- javascript
- Área
- cli, devtools
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Tranquilo
- Claridad
- Bastante claro
- Aptitud para principiantes
- 48/100