fix(coding-agent): abort stuck context hooks

closes #6234
This commit is contained in:
Vegard Stikbakke
2026-07-02 10:02:00 +02:00
parent 9f91da2145
commit 6757561546
2 changed files with 27 additions and 1 deletions
+1
View File
@@ -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)).
@@ -114,6 +114,25 @@ interface BeforeAgentStartCombinedResult {
systemPrompt?: string;
}
async function callContextHandlerAbortable<T>(fn: () => Promise<T> | T, signal: AbortSignal): Promise<T> {
if (signal.aborted) {
throw new Error("Agent run aborted");
}
let cleanup = () => {};
const abortPromise = new Promise<never>((_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<AgentMessage[]> {
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({