Skip to content

[Bug]: Completion sound fires when subagents or monitors finish, even though the agent resumes working #13625

Description

@Jardo-51

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. Enable notification sounds (or desktop notifications).
  2. Start a Claude thread where the agent launches subagents, or starts a background monitor, and ends its turn to wait for them.
  3. Wait for the subagents or the monitor to finish. The provider then wakes the agent and it continues working in a follow-up turn.

Expected behavior

No completion sound, toast, or desktop notification while the run is still going. The agent picks the work back up as soon as the background work finishes, so nothing is ready for the user yet. The completion alert should fire once, when the follow-up turn actually settles.

Actual behavior

The completion sound (and toast / desktop notification) fires the moment the subagents or the monitor finish, even though the agent immediately resumes working. The alert is a false alarm, and the user gets a second, real one when the follow-up turn ends.

Cause, as far as I can tell: ThreadNotificationCoordinator resolves the thread through resolveSidebarThreadStatus, which reports working / monitoring while backgroundLiveness is set, so nothing fires during the wait. When the background work ends, backgroundLiveness clears before the provider starts the wake-up turn. For that short gap the thread reads as ready with a completed latestTurn. That turn's completedAt (from when the wait began) is newer than the stored completion, so the coordinator plays the completion alert. The new turn starts right after.

Related but different: #5518 / #12903 cover the alert at the start of the wait. #12903 holds completion back while background work is live, but its web test expects the alert to fire right when background work settles, which is exactly the moment described here. Relay / mobile awareness looks like it has the same gap, since it keys off the same state.

A possible fix is for the server to keep the thread in a working state through the handoff when finished background work is going to wake the agent (it knows when a background result is delivered back to the provider), rather than a client-side debounce.

Impact

Minor bug or occasional failure

Version or commit

main @ e3e7cc3

Environment

Linux, Claude Code provider (subagents and background monitors)

Workaround

None, other than turning notification sounds off.

Activity

  1. juliusmarminge commented on Sep 25, 2026

    @juliusmarminge
    Member

    Triage

    Verdict: Bug, still present on main (e3e7cc3fc2, package 0.0.42). The report matches the code. The completion sound, toast, and desktop notification share one trigger, and that trigger treats the gap after background work ends as a finished run. This is not #5518, and #12903 does not fix it.

    What the report gets right

    Web alerts come from ThreadNotificationCoordinator. It asks resolveSidebarThreadStatus for the thread, and it only records a completion timestamp while that status is ready and latestTurn is completed. A newer timestamp than the one it stored last time is a completion alert. Sound plays for that alert before the toast and desktop-notification checks, so all three are the same event.

          let status = resolveSidebarThreadStatus(thread);
          if (status === "ready" && thread.latestTurn?.state === "error") status = "failed";
          const prior = previous.current.get(thread.id);
          // ...
          const completedAt = Date.parse(thread.latestTurn?.completedAt ?? "");
          const completion =
            status === "ready" &&
            thread.latestTurn?.state === "completed" &&
            Number.isFinite(completedAt)
              ? completedAt
              : (prior?.completion ?? null);
          next.set(thread.id, { attention, completion });
          if (!prior || thread.archivedAt !== null) continue;
          const kind =
            attention && attention !== prior.attention
              ? "input"
              : completion !== null && (prior.completion === null || completion > prior.completion)
                ? "completion"
                : null;

    resolveSidebarThreadStatus already reports working or monitoring while backgroundLiveness is set, after the session itself has left running. So nothing fires during the wait. While that status holds, the coordinator also does not store the waiting turn's completedAt. It keeps the previous marker.

      if (thread.session?.status === "running" || thread.session?.status === "starting") {
        return "working";
      }
      if (thread.session?.status === "error") {
        return "failed";
      }
      if (thread.backgroundLiveness === "working") {
        return "working";
      }
      if (thread.backgroundLiveness === "monitoring") {
        return "monitoring";
      }
      return "ready";

    Claude opens the gap the report describes. The parent result completes the turn and sets the session back to ready (ClaudeAdapter, around the turn.completed emission). Live tasks stay in the in-memory liveness set. When a subagent or monitor finishes, task_notification becomes task.completed, and ingestion drops that task immediately:

          // Sidebar background liveness: fed from the same lifecycle stream,
          // read by the shell query at mapping time (no persistence).
          switch (event.type) {
            case "task.started":
            case "task.progress":
            case "task.updated":
            case "task.completed": {
              // ...
              threadBackgroundLiveness.recordTaskLiveness({
                threadId: thread.id,
                taskId: payload.taskId,
                taskType: payload.taskType,
                status: payload.status,
                agentId: payload.agentId,
                kind:
                  event.type === "task.started"
                    ? "started"
                    : event.type === "task.progress"
                      ? "progress"
                      : event.type === "task.updated"
                        ? "updated"
                        : "completed",
              });

    kind: "completed" always removes the task. The same event appends thread activity, so the shell is rebuilt with backgroundLiveness cleared and the session still ready. The waiting turn's completedAt is now newer than the stored marker, and the alert fires. The wake-up turn does not exist yet. The next parent assistant message is what auto-starts it (claude/synthetic-turn-start in handleAssistantMessage). That message arrives only after the model starts responding, so the ready snapshot is a real shell update, not a single-frame race. When that follow-up turn later completes, its newer completedAt fires the second, real alert.

    Why #5518 and #12903 are a different moment

    #5518 is the alert at the start of the wait, when the parent turn settles and subagents are still running. Web already suppresses that, because liveness keeps the status off ready.

    #12903 is open and conflicting. It holds unread state (hasUnseenCompletion) and relay/mobile awareness in a running phase while backgroundLiveness is set. Its web test then expects the toast as soon as backgroundLiveness becomes null, with no follow-up turn. That is the snapshot this report is about. Landing that test would lock the false alarm in as the expected web behavior.

    On current main, relay and mobile do not have this same gap yet. resolveThreadAwarenessPhase never reads backgroundLiveness, and resolveThreadListV2Status only looks at session status. A running-to-completed awareness publish is not deferred, so T3 Connect and mobile still alert when the parent turn completes, which is #5518. #12903 would move that alert to liveness clear, and then they would share this gap. apps/mobile/src/features/threads/threadPresentation.ts in that PR is not a file on main.

    hasUnseenCompletion also ignores liveness, so the sidebar can already mark the thread unseen when the parent turn completes. That is the unread half of #5518, not this sound.

    What a fix has to change

    The alert has to wait for the follow-up turn when a finished task is delivered back into the provider and the provider is going to resume. Remembering the waiting turn's completedAt on the client without playing a sound would hide this gap, and it would also hide a real completion when background work ends and nothing resumes. The signal is the handoff, not a debounce. Claude already has it: task_notification is the delivery, and turn.started from the next assistant message is the resume. The shell must not read as ready between those two.

    A run that actually ends when the last background task ends should still alert once. Approvals, questions, and failures should still alert immediately.

    Disposition

    Leave #13625 open. Add bug and accepted. Do not close it as a duplicate of #5518. Do not treat #12903 as the fix.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 25, 2026
  3. added a commit that references this issue on Sep 28, 2026
    b19e48e
  4. tris203 commented on Oct 5, 2026

    @tris203
    Contributor

    Note

    Opus 5.5 responding on behalf of @tris203

    This also happens with T3's own delegate_task on orchestrator V2, through a path the triage above does not cover. Checked on main @ e22c880434, from source; not reproduced under a debugger.

    Repro: enable notification sound, have an agent call delegate_task in async mode (a review loop is an easy way) and end its turn. When the child finishes, the completion sound plays and the parent immediately starts its wake run. Each review round repeats it.

    Cause: finalizeAppOwnedSubagent commits the child's result and marks the parent's subagent turn item terminal, which empties the pending-background roster. The parent wake is offered afterwards, as a separate step (offerDelegatedCompletionDelivery → continuationRequests.offer). Between the two, the parent shell is ready with a completed latest run, and ThreadNotificationCoordinator treats that as a new completion.

    Unlike the Claude-native case, the server already knows at commit time that a wake is coming (completionPlan.offer is true), so the parent could stay non-ready across the handoff without any provider signal. A fix scoped to ClaudeAdapterV2 would miss this path.

  5. PrivateVictories-Main commented on Oct 6, 2026

    @PrivateVictories-Main

    Same false alert with T3-owned delegated tasks (delegate_task), not just Claude subagents and monitors. Data from 0.0.46-nightly.20261004 (macOS, Claude parent delegating to Codex):

    • When the last delegated child finishes and the parent is idle, the parent's subagent turn item completes before its wake run exists. The gap was 54 of 54 cases since Oct 1: average 79 ms, 24 over 50 ms, max 720 ms. Gaps over the shell stream's 50 ms coalescing window can show the thread as ready, with a Done badge and completion alert, before the wake turn starts.
    • The alert is not retracted when the thread resumes. ThreadNotificationCoordinator clears notifications on focus, removal or settings changes only (ThreadNotificationCoordinator.tsx:36-73), so the stale "completed" notification outlives the brief ready state.

    Possible server-side fix, matching the issue's suggestion: derivePendingBackgroundWork could hold while an app-owned subagent's completionDelivery is pending/claimed. delegatedTaskProgress already treats that state as owed work (SubagentProjection.ts:235-242). Separately, the coordinator could retract a thread's completion notification when that thread goes back to working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions