fix(coding-agent): read the settled turn from agent_end for error notifications
Classifier-refusal failures end a turn with stopReason === "error" but get pruned from the active context (agent-session.ts's #removeAssistantMessageFromActiveContext) before agent_end fires. sendErrorNotification() read viewSession.getLastAssistantMessage(), which reflects that mutated context and silently missed the notification for exactly the turns it should fire on. Thread the agent_end event through #handleAgentEnd -> #finishAgentEnd so sendErrorNotification reads the turn's own outcome from agent_end.messages instead.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import * as AIError from "@oh-my-pi/pi-ai/error";
|
||||
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
||||
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
||||
@@ -1060,7 +1060,7 @@ export class EventController {
|
||||
}
|
||||
}
|
||||
}
|
||||
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
||||
async #handleAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
||||
// A superseded agent_end: the agent is already streaming a fresh turn, so
|
||||
// this event belongs to a turn that has already been replaced. The session
|
||||
// dispatches to listeners fire-and-forget across an async extension-emit hop
|
||||
@@ -1072,10 +1072,10 @@ export class EventController {
|
||||
// then). Mirrors the collab guest's !isStreaming loader reconciler.
|
||||
if (this.ctx.session.isStreaming) return;
|
||||
|
||||
await this.#finishAgentEnd();
|
||||
await this.#finishAgentEnd(event);
|
||||
}
|
||||
|
||||
async #finishAgentEnd(): Promise<void> {
|
||||
async #finishAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
||||
this.#setTerminalProgress(false);
|
||||
this.ctx.statusLine.markActivityEnd();
|
||||
this.#streamingReveal.stop();
|
||||
@@ -1120,7 +1120,7 @@ export class EventController {
|
||||
this.ctx.ui.requestRender();
|
||||
this.#scheduleIdleCompaction();
|
||||
this.#scheduleIdleRecap();
|
||||
this.sendErrorNotification();
|
||||
this.sendErrorNotification(event);
|
||||
this.sendCompletionNotification();
|
||||
}
|
||||
|
||||
@@ -1477,11 +1477,17 @@ export class EventController {
|
||||
return this.ctx.viewSession.getContextUsage()?.tokens ?? 0;
|
||||
}
|
||||
|
||||
sendErrorNotification(): void {
|
||||
sendErrorNotification(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
||||
const notify = settings.get("error.notify");
|
||||
if (notify === "off") return;
|
||||
|
||||
const last = this.ctx.viewSession.getLastAssistantMessage?.();
|
||||
// Read the turn's own outcome from `agent_end.messages`, not the mutable
|
||||
// active context: a classifier-refusal failure is final (stopReason ===
|
||||
// "error") but gets pruned from `viewSession`'s active context before this
|
||||
// handler runs (see `#removeAssistantMessageFromActiveContext` in
|
||||
// agent-session.ts), so `getLastAssistantMessage()` would see a stale or
|
||||
// absent assistant and silently drop the notification.
|
||||
const last = event.messages.findLast((message): message is AssistantMessage => message.role === "assistant");
|
||||
if (last?.stopReason !== "error") return;
|
||||
|
||||
const sessionName = this.ctx.sessionManager.getSessionName();
|
||||
|
||||
Reference in New Issue
Block a user