The isTerminal:false early-return skipped #finishAgentEnd, the only site
that flushes a deferred plan-mode model switch, so an automatic continuation
(async wake) ran on the old model/thinking level until the terminal settle.
Flush the pending switch on the non-terminal branch before returning; the
title/loader teardown stays deferred to the terminal agent_end.
EventController.#handleAgentEnd guarded only on session.isStreaming, so a
non-terminal agent_end (isTerminal:false, emitted while an async job will
re-wake the loop) flipped the terminal title to idle and tore down the
working loader while a /vibe worker or async bash job was still running.
Early-return on event.isTerminal === false, matching the guard every other
agent_end consumer already applies; the later terminal agent_end performs
the normal teardown.
Fixes#7386
AgentSession defers and coalesces the wire-level agent_end while a
prompt is in flight (#emitSessionEvent), so a multi-attempt retry saga
often surfaces only ONE agent_end to EventController — which can be
the final settle, not an intermediate attempt. Consuming #retryPending
against whichever agent_end arrived first (previous commit) could
therefore discard the real final failure notification.
Switch to gating purely on the retry lifecycle: #retryPending is set
by auto_retry_start and cleared only by auto_retry_end (both
outcomes), never consumed by sendErrorNotification itself. Those
lifecycle events are never deferred, so they reliably bracket the
window a retry is actually outstanding regardless of how agent_end
coalescing lands.
Close the residual gap this creates: #handleRetryableError's
classifier-refusal and Fireworks-fallback-ineligible branches could
short-circuit a saga that already announced auto_retry_start without
ever emitting auto_retry_end, latching #retryPending open forever.
Both branches now emit a final auto_retry_end(false) when a prior
attempt already started the saga. #handleAgentStart also clears
#retryPending defensively so a saga that still somehow never resolves
cannot suppress a later, unrelated turn's notification.
A retryable error's agent_end fires with the failed assistant message
(stopReason === 'error') the instant #handleRetryableError schedules a
retry (auto_retry_start), before the retry has a chance to recover.
sendErrorNotification read that transient agent_end the same as a real
final settle, so error.notify=on raised a 'Stopped with error' toast
even for turns that went on to succeed on retry.
Track a #retryPending flag set on auto_retry_start and consumed
(check-then-clear) by sendErrorNotification, so exactly one mid-retry
agent_end is suppressed per attempt. #handleAutoRetryEnd also clears it
directly on both success and failure so a recovered retry never leaves
it stuck; consuming it on read (rather than only via auto_retry_end)
keeps it self-healing for the classifier-refusal short-circuit path,
which can return from #handleRetryableError without ever emitting a
fresh auto_retry_end.
sendErrorNotification now reads the settled turn from event.messages,
but sendCompletionNotification still read viewSession.getLastAssistantMessage().
For a classifier-refusal turn that stale/undefined lookup no longer
matched 'aborted'/'error', so with completion.notify=on the same
failed turn fired both the error toast and a misleading 'Complete'
toast.
Thread the same agent_end event into sendCompletionNotification so
both gates read one consistent source of truth.
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.
- Added a SessionFocusController to switch transcript and input context between main and subagent sessions.
- Added agent-hub Enter activation and double-left return behavior for focused local agents.
- Added view-session-based event and render logic to avoid stale focus-session state.
- Added status-line focused agent display with ghost icon and focused-mode border dimming.
- Removed the built-in `background` (`bg`) slash command and its `handleBackgroundCommand` path from the interactive flow.
- Deleted background event subscription and shutdown handling by removing `handleBackgroundEvent` from the event, input, and interactive controllers.
- Simplified `InteractiveModeContext` by dropping background-only fields and helpers such as `isBackgrounded`, background UI context creation, and background event callbacks.
- Updated completion and ask notifications to pass structured payloads.
- Updated completion and ask notifications to include explicit message metadata fields.
- Adjusted abort-guard and retry-capability tests for the new notification/options behavior.
Bug: Ctrl+C on the ask tool selector threw ToolAbortError, the turn
ended with stopReason === "aborted", and handleBackgroundEvent fired
sendCompletionNotification() unconditionally — producing a misleading
"Task complete" desktop toast for a turn that never actually completed.
Fix mirrors the stopReason filter already used by
#currentContextTokens, #handleMessageEnd, and the retry / TTSR /
compaction skip paths across agent-session.ts: check the most recent
assistant message via session.getLastAssistantMessage() and return
early when stopReason is "aborted" or "error".
Test coverage (event-controller-abort-guard.test.ts, 6 cases):
- aborted -> 0 sendNotification calls
- error -> 0 calls
- stop -> 1 call (normal completion)
- no last assistant message -> proceeds (defensive)
- isBackgrounded=false (foreground) -> still 0
- completion.notify=off -> still 0
Matching guard applied to the standalone desktop-notify extension
(~/.omp/agent/extensions/desktop-notify/index.ts) which is currently
the live producer of completion toasts after Phase 1 of
seed_0ca7e1143ac1.