github / github/copilot-cli

preToolUse silently dropped after mid-loop extensions_reload (snapshot staleness in runAgenticLoop)

Aberta
#3,167 0 comentários 0 reações 0 responsáveis Ver no GitHub
area:plugins area:tools
Linguagem predominante
Shell
Estrelas
11.2k
Forks
1.9k
Merge médio
14h 16min
PRs com merge (30d)
6

Descrição

## 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.

Guia de contribuição

Abrir o guia de contribuição

Direção de pesquisa

Comece pelos pontos de entrada do app.js incluído mencionados no relatório: runAgenticLoop, processToolExecutionResult, createHooksProxy e addAdHocHooks. Reproduza um extensions_reload seguido de outra chamada de ferramenta no mesmo agentic loop e, em seguida, rastreie se os hooks usam a conexão atual da extensão. O trabalho está concluído quando preToolUse e postToolUse forem acionados após o reload sem usar silenciosamente o proxy descartado.

Escrita pelo modelo de indexação a partir do texto da issue.

Avaliação

Stack de tecnologia
javascript
Domínio
cli, devtools
Tipo de issue
Bug
Dificuldade
4/5
Tempo estimado
3-5 dias
Status de atividade
Pouca atividade
Clareza
Razoavelmente clara
Facilidade para iniciantes
48/100

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.