Desktop app: artifact watch re-delivers the same artifact-changed notification on every app restart, firing an unrequested turn
Summary
An artifact watch re-delivers an already-delivered artifact-changed notification on the first
warm of the watching session after every app restart, even when the artifact has not changed since.
Each re-delivery fires a real, unrequested turn in that session.
Practical effect: focusing an old, inactive session that has an artifact panel open silently spends
a turn - in my case on Opus at high effort - to be told about a republish it was already told about,
and this repeats indefinitely, once per app restart.
Evidence
The two notification records in the session transcript are byte-identical, including the version:
<task-notification>
<task-type>artifact-changed</task-type>
<summary>Artifact changed: <redacted url></summary>
Artifact <redacted url> appears to have been republished elsewhere (by another session, or by
someone saving from the page itself) - it is now version <same version string in both>. Your copy
is stale; re-read before editing or republishing (read it with the Artifact tool
(action: "read", url)).
</task-notification>
No republish happened between the two. The version string embeds a unix timestamp that decodes to
nine days before the first of the two notifications, so the artifact had not been touched in that
window at all.
Delivery correlates strictly with app runs, not with artifact changes:
| App run | Warm of the watching session | Notification delivered? |
| --- | --- | --- |
| run A | first warm | yes, ~17s after warm |
| run A | second warm, same run | no |
| run B (after restart) | first warm | yes, ~17s after warm |
So the watch appears to dedup in memory within an app run, but that dedup is not persisted, and the
session's own "known version" pointer is never advanced - so every restart re-delivers.
Why it never converges
The notification instructs the reader to re-read the artifact. In practice the woken turn often
should not re-read it: if the artifact is legitimately owned by another session, the correct
response is "noted, my copy is stale, I will not touch it" - which advances nothing. The loop then
repeats forever. Interrupting the woken turn, or rewinding it away, guarantees recurrence for the
same reason.
There is no way to tell from the sidebar that a session holds a live watch, so the cause is quite
hard to attribute from the user side - the visible symptom is just "this old session keeps showing
as running when I focus it".
Steps to reproduce
- In a session, publish an artifact (this starts a watch) and leave the artifact panel open.
- Have the artifact republished from somewhere else, so the session's copy goes stale.
- Focus the session. It warms, the watch reconnects, an
artifact-changednotification is
delivered, and a real turn runs. Let the turn conclude without calling Artifact action: "read".
- Restart the app, then focus the session again. The identical notification is delivered again and
another turn runs.
- Repeat step 4 indefinitely.
Expected
Either the notification is delivered at most once per actual artifact version change (persisted
across restarts), or an unchanged version does not re-trigger on reconnect.
Actual
Re-delivered on the first warm after each app restart, indefinitely, for a version that has not
changed.
Suggested fixes
- Persist the last-delivered version per (session, artifact) so the dedup survives a restart.
- Let the woken turn acknowledge a version without being required to re-read the whole artifact,
so "not mine, leaving it alone" is a terminal state rather than a no-op.
- Surface live watches in the session UI, and make them stoppable without having to know the
unwatch incantation.
Environment
- Claude desktop app
1.37937.1.0 - Claude Code CLI
2.1.246 - Windows 11 Pro 10.0.22631, session over RDP
- Relevant log lines:
[CCD] Warming session,[WarmLifecycle:preview] Warming up session,
[Stop hook] Query completed in %LOCALAPPDATA%\Claude\Logs\main.log
Related
Distinct from the isRunning latch in #90081 - that one sticks on an idle session and needs an
interrupt to clear; this one is a genuinely-running turn that should never have been triggered. They
share a symptom (a session showing "running" when you did not ask for anything), which makes the two
easy to conflate.