Extension SessionHooks missing onPostToolUseFailure handler — failed tool results are invisible
- Lenguaje dominante
- Java
- Estrellas
- 10.5k
- Forks
- 1.5k
- Merge medio
- 1 d 11 h
- PR fusionados (30 d)
- 128
Descripción
## Summary
Extensions registering hook handlers via `joinSession({ hooks })` cannot observe non-success tool results. `onPostToolUse` only fires for `resultType === "success"` — failure / rejected / denied / timeout outcomes are silently invisible to extensions.
The underlying CLI does emit a `postToolUseFailure` hook event for failed tool calls, but the SDK never dispatches it: it is missing from both the `SessionHooks` interface and the runtime `_handleHooksInvoke` handler map.
## Repro
1. In an extension, register a session via `joinSession` with handlers for both `onPostToolUse` and (attempting) `onPostToolUseFailure`:
```js
import { joinSession } from "@github/copilot-sdk/extension";
const session = await joinSession({
hooks: {
onPostToolUse: async (input) => {
console.log("post:", input.toolName, input.toolResult?.resultType);
},
// Not declared on SessionHooks, but try anyway:
onPostToolUseFailure: async (input) => {
console.log("post-failure:", input.toolName);
},
},
});
```
2. Have Copilot run a tool that fails (e.g. a shell command that exits non-zero, then a tool that times out, then one that triggers a "rejected" / "denied" outcome).
3. Observe: `onPostToolUse` fires only for the successful tool calls. `onPostToolUseFailure` never fires for any of the failure modes.
## Expected
Extensions should be able to observe **all** terminal tool outcomes — success, failure, rejected, denied, timeout — through the hooks system. Either:
- Add `onPostToolUseFailure` to `SessionHooks` and dispatch it from `_handleHooksInvoke`, or
- Always fire `onPostToolUse` regardless of `resultType` (with the `resultType` field on the input distinguishing outcomes).
## Evidence in shipped CLI `1.0.43-0`
Path: `/copilot-sdk/extension.js` (the bundled SDK shipped with the Copilot CLI).
The runtime hook dispatcher exposes only 6 hook types:
```js
async _handleHooksInvoke(hookType, input) {
if (!this.hooks) return void 0;
const handlerMap = {
preToolUse: this.hooks.onPreToolUse,
postToolUse: this.hooks.onPostToolUse,
userPromptSubmitted: this.hooks.onUserPromptSubmitted,
sessionStart: this.hooks.onSessionStart,
sessionEnd: this.hooks.onSessionEnd,
errorOccurred: this.hooks.onErrorOccurred,
};
const handler = handlerMap[hookType];
if (!handler) return void 0; // silently dropped
...
}
```
The matching TypeScript declaration in `copilot-sdk/types.d.ts` `SessionHooks` declares the same 6 handlers, so authors using TypeScript can't even discover the missing types.
Meanwhile, the host CLI (`app.js` in the same package) builds a hook proxy per connected extension that DOES include `postToolUseFailure`:
```js
// createHooksProxy(sessionId, connection)
return {
preToolUse: [o => n("preToolUse", o)],
postToolUse: [o => n("postToolUse", o)],
postToolUseFailure: [o => n("postToolUseFailure", o)],
userPromptSubmitted: [o => n("userPromptSubmitted", o)],
sessionStart: [o => n("sessionStart", o)],
sessionEnd: [o => n("sessionEnd", o)],
errorOccurred: [o => n("errorOccurred", o)],
};
```
And the host's `processToolExecutionResult` explicitly gates `postToolUse` on success, routing failures elsewhere:
```js
async processToolExecutionResult(toolName, toolArgs, toolResult) {
let s = (toolResult.resultType === "success"
? await h1(this.getEffectiveHooks()?.postToolUse, ...)
: void 0)?.modifiedResult ?? toolResult;
let a = s.resultType === "failure"
? await this.runPostToolUseFailureHooks(toolName, toolArgs, s)
: void 0;
...
}
```
So the CLI sends `hooks.invoke` requests with `hookType: "postToolUseFailure"` to the extension over IPC, but the SDK's `_handleHooksInvoke` doesn't recognize that hook type and returns `undefined` without invoking any handler.
The full set of hook types the CLI knows about (`app.js`):
```js
new Set([
"sessionStart", "sessionEnd", "userPromptSubmitted",
"preToolUse", "postToolUse", "postToolUseFailure",
"errorOccurred",
"agentStop", "subagentStop", "subagentStart",
"preCompact", "permissionRequest", "notification",
])
```
The SDK's hooks API exposes 6 of these to extensions. Several of the missing ones (like `permissionRequest`) are intentionally exposed via different APIs (`onPermissionRequest` in the `JoinSessionConfig`), so this report intentionally focuses on the most clear-cut gap: `postToolUseFailure`.
## Impact
Extensions that want to observe or react to tool failures — for telemetry, replay buffers, fault-injection tests, or UI that visualizes tool execution — have no reliable way to do so. Since post-only-on-success silently drops the failure cases, extensions can wrongly report "no result" or, if pairing pre with post, leak resources tracking calls whose post will never come.
## Suggested fix
Add the missing handler to `_handleHooksInvoke`'s map and to the `SessionHooks` TypeScript interface:
```diff
export interface SessionHooks {
onPreToolUse?: PreToolUseHandler;
onPostToolUse?: PostToolUseHandler;
+ onPostToolUseFailure?: PostToolUseFailureHandler;
onUserPromptSubmitted?: UserPromptSubmittedHandler;
onSessionStart?: SessionStartHandler;
onSessionEnd?: SessionEndHandler;
onErrorOccurred?: ErrorOccurredHandler;
}
```
```diff
const handlerMap = {
preToolUse: this.hooks.onPreToolUse,
postToolUse: this.hooks.onPostToolUse,
+ postToolUseFailure: this.hooks.onPostToolUseFailure,
userPromptSubmitted: this.hooks.onUserPromptSubmitted,
sessionStart: this.hooks.onSessionStart,
sessionEnd: this.hooks.onSessionEnd,
errorOccurred: this.hooks.onErrorOccurred,
};
```
The input shape can mirror `PostToolUseHookInput` (`toolName` + `toolArgs` + `toolResult`), since the CLI's emit site already provides the same payload.
## Environment
- `@github/copilot` `1.0.43-0` (Windows `win32-x64` package).
- Bundled SDK at `/copilot-sdk/extension.js`.
- Confirmed by inspecting the shipped JS and the TypeScript declarations in the same package.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.