[Bug]: A Claude Monitor wakes the agent on every event and each wake fires "Thread completed" with sound (47 notifications in 16 minutes from a thread that was only waiting)#32497

opened3d agobyMMdSadiqMd

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 (ThreadNotificationCoordinator), with the wake labeling in apps/server (ClaudeAdapterV2, ProviderContinuationService)

Summary

When a Claude agent is monitoring something (a sweep, a training run, a test loop) with a native Monitor, Claude Code wakes the agent once for every line the Monitor prints. T3 Code turns each wake into a new run. The run lasts about 3 seconds and only says "Waiting on the sweep.". Every one of those runs ends as completed, so the desktop app fires a "Thread completed" notification and the completion sound each time.

Nothing has completed. The agent is waiting, and it says so. But the user gets an alert every 5โ€“20 seconds for as long as the Monitor runs, which can be hours.

What I want: while a thread is only being woken by its own monitoring, don't show a completion notification or play the sound. Notify once, when the agent actually finishes, or when it needs me.

This is the "native Monitor" part that #13625 left open. It was closed with "Native Monitor behavior was not tested. If that part still matters to you, please open a new issue for that part only." It is also the worst case of #25585: there it's one false "done" per stage, here it's one per Monitor line.

Example (what the user experiences)

User: "Run the sweep and tell me which prompts break the judge." The agent starts the sweep in tmux, arms a Monitor that prints one line per finished sample, and ends its turn.

Time (UTC)Monitor printsAgent (new run each time)Desktop
14:57:11(armed)"The sweep is running โ€ฆ I'll wait for the per-run results."โ€”
14:57:14sample 1 done"Still running; first result is in."Thread completed ๐Ÿ”” โŒ
14:57:19sample 2 done"Waiting on the sweep."Thread completed ๐Ÿ”” โŒ
14:57:24sample 3 done"Waiting on the sweep."Thread completed ๐Ÿ”” โŒ
โ€ฆโ€ฆโ€ฆ ร—70 more wake runs by 15:13Thread completed ๐Ÿ”” โŒ (47 in total)
(end)sweep done"Here are the five prompts โ€ฆ"Thread completed โœ…

Expected: the sidebar shows Waiting ยท sweep6 results per run (or Working) while the Monitor is active. No notification or sound fires for the wake runs. One "Thread completed" fires when the agent gives its final answer. Actual: a "Thread completed" notification with sound after most wake runs, and the sidebar row flips between Working and Done every few seconds.

Steps to reproduce

This is derived from the live case below. I haven't run this exact prompt separately.

  1. Settings โ†’ Notifications: Notifications with sound. Open a Claude thread, then switch to another app so desktop notifications show.

  2. Send:

    text
    1Use Bash with run_in_background=true to run:
    2 for i in $(seq 1 30); do sleep 8; echo "{\"i\": $i}" >> ./results.jsonl; done
    3Then arm a Monitor that prints one line for each new line in ./results.jsonl, and end your turn.
    4Each time you're woken, reply with one short sentence and end your turn again.
  3. Watch the desktop notifications. You get one "Thread completed" (and sound) about every 8 seconds, around 30 in total. Only the last one is real.

Evidence 1: the live thread (server side)

A Claude thread (claude-opus-5-5, effort high) on a remote SSH environment running server 0.1.7, viewed from the macOS desktop app 0.1.7. These are read-only reads of the remote statev2.sqlite and the thread's provider event log (logs/provider/events.<threadId>.log), on 2026-11-07, times UTC.

  • Run 209 (user message): the agent starts a sweep in tmux, loads Monitor through ToolSearch, and arms it on results.jsonl. The tool result says: "Monitor started (task โ€ฆ, expires in 30m โ€ฆ). You will be notified on each event." The run ends at 14:57:11 with "The sweep is running: 10 prompt variants ร— 3 samples ร— 2 effort levels. I'll wait for the per-run results."

  • Runs 210 โ†’ 282 (73 wake runs, 14:57:14 โ†’ 15:13): none of them came from me. Each one starts with this synthetic user message:

    json
    1{"notification":{"source":{"kind":"background_task"},"outcome":"updated","summary":"Background activity updated"},
    2 "createdBy":"agent","creationSource":"provider","role":"user","text":"Background task completed."}
  • Median run length: 3.2 s. Gap between one run's completion and the next run's request: 26 under 2 s, 22 between 2 and 5 s, 25 over 5 s (max 18 s).

  • What the agent said in those runs: 23ร— "Waiting on the sweep.", 11ร— "Waiting on round 2.", 8ร— "Waiting on rounds 4 and 5.", 4ร— "Waiting on rounds 3 and 4.", and so on.

  • Provider stream: in 12 minutes there were 64 system/init frames (one per wake), but only 6 task_notification frames: 5 Monitors' "stream ended" plus one background command exit. A Monitor event doesn't appear in the SDK stream at all. Claude Code feeds it to the model internally and just starts a new turn.

  • Roster (pendingBackgroundTasks on the provider thread) during the wakes: [{"kind":"monitor","description":"sweep6 results per run (cotgap)"}], without awaited. The agent re-armed a fresh Monitor after each stream ended (sweep2 โ€ฆ sweep6).

  • Cost: total_cost_usd rose by $3.66 in the first 12 minutes. Each wake re-reads about 145k cached tokens just to say "Waiting".

Evidence 2: the desktop app actually notified (client side)

From the local desktop trace (logs/desktop.trace.ndjson) over the same window, with client settings notificationMode: "notifications-and-sound":

  • 68 desktop:set-notification-badge IPC calls between 14:57:15 and 15:13:02. ThreadNotificationCoordinator calls setNotificationBadge from trackNotification right after it creates a desktop Notification.
  • 47 of the 74 run completions (run 209 plus 73 wakes) were followed by a badge call within 3 s, with a median lag of 0.39 s. The 27 that weren't were mostly the wakes where the next run was requested within ~2 s, so the client never saw the thread as settled.
  • macOS Notification Center keeps no records for com.t3tools.t3code. That's expected: each alert replaces the previous one through the per-thread tag, and it's posted silent: true because the app plays the sound itself. So the trace is the evidence, not the OS database.

The phone path is busy too: the remote server logged 247 AgentAwarenessRelay.publishThread spans in the same window. I didn't have a phone paired to this environment, so I couldn't confirm pushes. But 25 of the gaps are longer than the relay's 5 s confirmation hold (AgentAwarenessRelay.ts:565), and those would each be a fresh completed โ†’ "Agent finished".

Root cause (on main @ e455200077)

1. Every Monitor event becomes a T3 run, labeled as a completion. ClaudeAdapterV2.ts:8529 treats any root system/init frame as wake evidence (isClaudeTurnStartMessage, :3235-3237). :8601-8648 then calls continuationRequests.offer(...) with detail: null, and with no wake report, because a Monitor event never reaches the SDK as a task_notification. ProviderContinuationService.ts:183-206 then dispatches a run with the default text:

ts
1const CONTINUATION_MESSAGE_TEXT = "Background task completed."; // :11
2...
3text: request.detail ?? CONTINUATION_MESSAGE_TEXT, // :195
4notification: request.notification ?? {
5 source: { kind: "background_task" }, outcome: "updated", summary: "Background activity updated",
6},

A monitor notification source kind already exists in the contracts (orchestrationV2.test.ts:521-544), but this path never uses it.

2. A pending Monitor doesn't hold completion. packages/shared/src/orchestrationV2PendingBackgroundWork.ts:97-108:

ts
1// Commands and monitors are as often a dev server or its watchdog as a wait, so they hold
2// only when the agent marks them awaited.
3case "command":
4case "monitor":
5 return false;

So between wakes, resolveSidebarThreadStatus (Sidebar.logic.ts:868-890) returns ready and the run reads as completed. On v0.1.2 a pending monitor did hold completion. That changed with #24610 (see point 4 of #25585).

3. The notification gate fires on every newer completion, with no grace and no wake check. ThreadNotificationCoordinator.tsx:263-266:

ts
1const settled = status === "ready" && thread.latestRun?.status === "completed";

ThreadNotificationCoordinator.logic.ts:34-37 notifies "completion" whenever completedAt is newer than the last one handled. The only quiet path is latestRunStopRequested (ThreadNotificationCoordinator.tsx:283). present() plays the sound first, for every call (:143-147), then shows the toast or desktop notification. The 2 s WORKING_EXIT_GRACE_MS (packages/client-runtime/src/state/threadInbox.ts:115) only applies to inbox shelf placement, not to notifications. Even with it, most gaps here are longer than 2 s.

The client already has what it needs to tell these apart. The thread shell carries pendingBackgroundTasks (with kind: "monitor") and latestUserAuthoredMessageAt, which "excludes wakes" (packages/contracts/src/orchestrationV2.ts:2125-2126). A completed run whose latestRunRequestedAt is newer than latestUserAuthoredMessageAt, on a thread that still has a pending monitor, is a monitoring wake, not a finished task.

Suggested fix direction

The main ask is 1. The rest make it robust.

  1. Don't notify for monitoring wakes. In ThreadNotificationCoordinator, treat a completion as quiet (like latestRunStopRequested) when the run was started by a wake (latestRunRequestedAt > latestUserAuthoredMessageAt) and the thread still has a pending monitor (or any pending background task). Fire the single "Thread completed" when the roster drains, or when a wake run completes and no monitor is left. Approvals, input requests and failures should still alert immediately, as they do today.
  2. Same rule for the phone relay. In projectThreadAwarenessV2 (agentAwareness.ts:115-117), map a wake run's completed to running while a monitor is pending, so "Agent finished" doesn't fire per Monitor line.
  3. Show it in the sidebar. Let a pending monitor park the row at Waiting ยท instead of flipping between Working and Done.
  4. Label the wake honestly. For an opaque wake while a monitor is pending, send notification.source.kind: "monitor" and text like "Monitor event", not "Background task completed.". Keep "completed" for real terminal task_notifications.
  5. (Optional) Collapse back-to-back wakes. If a wake arrives while the previous wake run is still running or was requested within a few seconds, queue it into one run rather than one run per line. This cuts the token cost too.

Regression test idea: a completed user run with a pending monitor, followed by 10 wake runs (createdBy: "agent", notification.source.kind background/monitor) that each complete, then a final wake run that completes with an empty roster. That should give zero completion notifications or sounds for the 10 wakes and exactly one at the end.

Impact

Major degradation or frequent failure. While any monitored thread is running, notifications and sound fire every few seconds, so I have to mute T3 Code entirely and then miss real alerts (approvals, input, failures) from other threads. "Thread completed" stops meaning anything. Each wake also costs a full cached-context model call.

Version or commit

Observed on T3 Code (Beta) desktop 0.1.7 (macOS) with a remote SSH environment on server 0.1.7. Code references are on main @ e455200077 (2026-11-07).

Environment

macOS 27.0.1 (arm64), T3 Code (Beta) 0.1.7 with "Notifications with sound". Remote environment: Linux x64, server 0.1.7. Provider: Claude Code 2.1.319, claude-opus-5-5.

Workaround

None that keeps notifications useful. Turning notifications off silences the real alerts too. Telling the agent to call t3_background_wait on its Monitor should hold the thread, but it depends on the model doing it every time it re-arms one, and it didn't here.

  • #13625 (closed): completion sound when monitors finish. This issue is the "native Monitor" part it asked to be filed separately.
  • #25585: false Done / "Thread completed" at every stage of a multi-stage background job. This is the same rule, hit once per Monitor line instead of once per stage. Fix option 2 there would also cover this.
  • #31817: Monitor events + t3_background_wait loop inside a turn (a different symptom: a tool-call loop and a roster wipe).
  • #19183: one wake and one turn per pull request from the same pass (the same "one run per event" pattern, for PR watch).
  • #31087, #32113: completion alert fires for a parent that is about to continue (the same "completion while not done" family, for delegation).

Contributions welcome

This is open for anyone who wants to pick it up. The root cause, the live data and fix options are laid out above. If you'd like to take it, leave a comment so we don't double up, and feel free to ask questions on the issue.