[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
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 prints | Agent (new run each time) | Desktop |
|---|---|---|---|
| 14:57:11 | (armed) | "The sweep is running โฆ I'll wait for the per-run results." | โ |
| 14:57:14 | sample 1 done | "Still running; first result is in." | Thread completed ๐ โ |
| 14:57:19 | sample 2 done | "Waiting on the sweep." | Thread completed ๐ โ |
| 14:57:24 | sample 3 done | "Waiting on the sweep." | Thread completed ๐ โ |
| โฆ | โฆ | โฆ ร70 more wake runs by 15:13 | Thread 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.
Settings โ Notifications: Notifications with sound. Open a Claude thread, then switch to another app so desktop notifications show.
Send:
text1 Use Bash with run_in_background=true to run: 2 for i in $(seq 1 30); do sleep 8; echo "{\"i\": $i}" >> ./results.jsonl; done 3 Then arm a Monitor that prints one line for each new line in ./results.jsonl, and end your turn. 4 Each time you're woken, reply with one short sentence and end your turn again. 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
Monitorthrough ToolSearch, and arms it onresults.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:
json1 {"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/initframes (one per wake), but only 6task_notificationframes: 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 (
pendingBackgroundTaskson the provider thread) during the wakes:[{"kind":"monitor","description":"sweep6 results per run (cotgap)"}], withoutawaited. The agent re-armed a fresh Monitor after each stream ended (sweep2โฆsweep6).Cost:
total_cost_usdrose 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-badgeIPC calls between 14:57:15 and 15:13:02.ThreadNotificationCoordinatorcallssetNotificationBadgefromtrackNotificationright after it creates a desktopNotification. - 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-threadtag, and it's postedsilent: truebecause 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:
| 1 | const CONTINUATION_MESSAGE_TEXT = "Background task completed."; // :11 |
| 2 | ... |
| 3 | text: request.detail ?? CONTINUATION_MESSAGE_TEXT, // :195 |
| 4 | notification: 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:
| 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. |
| 3 | case "command": |
| 4 | case "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:
| 1 | const 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.
- Don't notify for monitoring wakes. In
ThreadNotificationCoordinator, treat a completion asquiet(likelatestRunStopRequested) when the run was started by a wake (latestRunRequestedAt > latestUserAuthoredMessageAt) and the thread still has a pendingmonitor(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. - Same rule for the phone relay. In
projectThreadAwarenessV2(agentAwareness.ts:115-117), map a wake run'scompletedtorunningwhile amonitoris pending, so "Agent finished" doesn't fire per Monitor line. - Show it in the sidebar. Let a pending
monitorpark the row at Waiting ยท instead of flipping between Working and Done. - Label the wake honestly. For an opaque wake while a
monitoris pending, sendnotification.source.kind: "monitor"and text like "Monitor event", not "Background task completed.". Keep "completed" for real terminaltask_notifications. - (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.
Related
- #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_waitloop 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.