From 6757561546add7a782e52e34ff14e59f00486fa9 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 2 Jul 2026 10:02:00 +0200 Subject: [PATCH] fix(coding-agent): abort stuck context hooks closes #6234 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/extensions/runner.ts | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d99777d7..e3391964 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- Fixed Escape aborts to clear runs stuck in extension context hooks that ignore abort signals ([#6234](https://github.com/earendil-works/pi/issues/6234)). - Fixed the question extension example to run question tool calls sequentially so multiple questions in one assistant turn remain answerable ([#6189](https://github.com/earendil-works/pi/issues/6189)). - Fixed `/login` to report auth storage persistence failures instead of claiming credentials were saved when `auth.json` is locked ([#6223](https://github.com/earendil-works/pi/issues/6223)). - Fixed split-turn compaction to serialize summary requests so single-concurrency local providers do not fail with 429 errors ([#5536](https://github.com/earendil-works/pi/issues/5536)). diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index a40625e6..a1817f7b 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -114,6 +114,25 @@ interface BeforeAgentStartCombinedResult { systemPrompt?: string; } +async function callContextHandlerAbortable(fn: () => Promise | T, signal: AbortSignal): Promise { + if (signal.aborted) { + throw new Error("Agent run aborted"); + } + + let cleanup = () => {}; + const abortPromise = new Promise((_resolve, reject) => { + const onAbort = () => reject(new Error("Agent run aborted")); + signal.addEventListener("abort", onAbort, { once: true }); + cleanup = () => signal.removeEventListener("abort", onAbort); + }); + + try { + return await Promise.race([Promise.resolve().then(fn), abortPromise]); + } finally { + cleanup(); + } +} + /** * Events handled by the generic emit() method. * Events with dedicated emitXxx() methods are excluded for stronger type safety. @@ -924,6 +943,7 @@ export class ExtensionRunner { async emitContext(messages: AgentMessage[]): Promise { const ctx = this.createContext(); + const signal = ctx.signal; let currentMessages = structuredClone(messages); for (const ext of this.extensions) { @@ -933,12 +953,17 @@ export class ExtensionRunner { for (const handler of handlers) { try { const event: ContextEvent = { type: "context", messages: currentMessages }; - const handlerResult = await handler(event, ctx); + const handlerResult = signal + ? await callContextHandlerAbortable(() => handler(event, ctx), signal) + : await handler(event, ctx); if (handlerResult && (handlerResult as ContextEventResult).messages) { currentMessages = (handlerResult as ContextEventResult).messages!; } } catch (err) { + if (signal?.aborted) { + throw err; + } const message = err instanceof Error ? err.message : String(err); const stack = err instanceof Error ? err.stack : undefined; this.emitError({