github / github/copilot-cli

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

Đang mở
#3,167 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
area:plugins area:tools
Ngôn ngữ chính
Shell
Star
11.2k
Fork
1.9k
Merge trung bình
14 giờ 16 phút
Pull request đã merge (30 ngày)
6

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.