Repository navigation
[Bug]: iOS Live Activity keeps showing "Done" after you open the finished thread #15728
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks for the detailed report, @jlipworth. Your code pointers made this much easier to trace.
What I found
- The 15-minute Done window.
docs/user/mobile-notifications.mdsays finished results stay visible for up to 15 minutes. Here's how that works today:- The completion is sent as an update, not an end.
thread.visitedisn't published to agent awareness (shouldPublishAgentAwarenessEventinapps/server/src/relay/AgentAwarenessRelay.ts).- The terminal row is kept for
TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MSbased onupdatedAt(infra/relay/src/agentActivity/agentActivityAggregate.ts). - So opening the thread doesn't retire the card on its own.
- Ending the card when the thread is viewed would be a product change. The visit watermark is also shared across devices, so a desktop read would clear the phone card too.
- Foreground re-registration resets the existing card. On foreground,
remoteRegistration.tsre-registers the current activity token. ThenLiveActivities.registernulls and rewrites the row even when the token hasn't changed:- It sets
remoteStartedAt = now. - It clears
lastAggregateJsonandlastLiveActivityDeliveryAt.
- It sets
- What that reset causes. The replay that follows has no previous aggregate, so it sends a fresh
live_activity_update, and an unchanged Done card gets repainted with a new stale date. After the TTL has passed, the freshly-armed grace ininfra/relay/src/agentActivity/ApnsDeliveries.ts(FRESHLY_ARMED_GRACE_MS) can drop thelive_activity_end, and nothing retries it. That would explain why foregrounding the app after 15 minutes doesn't reliably end the card. - Related, not duplicates:
- [Bug]: iOS Live Activity keeps showing "T3 Done" for hours when no other agent event arrives #11939, with open PR fix(relay): end idle Live Activities after the display window #11943: the idle card is never ended. The idle clock in fix(relay): end idle Live Activities after the display window #11943 would also be reset by this registration path.
- Define shared iOS and Android activity expiry policy #10864: the expiry clock policy.
- [Bug]: Changing the model on a finished thread sends a new "Done" alert to the phone #11938 and fix(server): stop stale settlement notifications #9081: metadata bumps re-alert Done.
- fix(relay): an iPad no longer gets a Done alert for every unrelated publish #15015: duplicate Done alerts.
- Merged fix(mobile): an expired Live Activity no longer leaves a second card #15254: dismissing leftover ended activities.
Likely fix area
- Option 1: keep the existing card on same-token re-registration. When the same device re-registers the same
activityPushTokenfor a card that hasn't ended,LiveActivities.registercould keepremoteStartedAt,lastAggregateJson, andlastLiveActivityDeliveryAt. It would need to snapshot those values before the preliminaryUPDATEnulls them. A changed token or an already-ended row would still start fresh. - Option 2, a separate product question: end the card once the thread is viewed. This would shorten the documented window and would need a decision on cross-device scope.
- Workaround for now: swipe the Live Activity away on the Lock Screen, or turn off Live Activity Updates.
A maintainer will decide on the fix direction.
- The 15-minute Done window.
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 4, 2026 Field confirmation of root cause 3, on main @
c138163c8c(DB timestamps, UTC):- 22:10:10: last run completes; the card shows Done.
- 22:10:55: thread viewed and settled on iOS.
- About 22:40: opened the app (30+ min after completion, past the 15-minute TTL), opened the thread, went back, locked the phone.
The card still shows Done. Foregrounding past the TTL is the documented workaround for #11939, but the same-token re-registration resets
remoteStartedAt, and the empty-aggregate end falls insideFRESHLY_ARMED_GRACE_MSand is never retried. So on a real device none of these clear the card: viewing, settling, waiting, or opening the app after the window. Only archiving, a new turn, or swiping it away does. This supports option 1 from the triage (keepremoteStartedAton same-token re-registration). Settling not clearing the card is filed separately as #16229.Correction to my comment above: the card I observed turned out to be an ended activity frozen on the Lock Screen. It was missing from the Dynamic Island when unlocked, and the relay had already fallen back to plain push notifications. Swiping it away restored normal behavior. That's consistent with iOS's ~8-hour activity limit plus #15254 only dismissing ended cards inside
start(). So it isn't clean field evidence for the grace-window root cause. That cause is still visible in the code, but I can't attribute this observation to it.
Before submitting
Area
apps/server
(The fix spans
apps/server(AgentAwarenessRelay) andinfra/relay. No mobile widget change needed.)Summary
The iOS Live Activity keeps showing "T3 / Done" in the Dynamic Island after the user has opened the app and read the finished thread. Opening the thread never reaches the Live Activity pipeline. Opening the app actually re-sends the Done card and makes it look fresh again.
This is not #11939. #11939 is about the card failing to end once the 15-minute display window passes. This issue is about the card staying during that window (and longer) even though the user already saw the result. Fixing #11939 (for example with #11943) does not fix it. See "Relationship to existing issues" below.
Steps to reproduce
T3 · Done.Expected behavior
Once every finished thread on the card has been viewed, the Live Activity ends and leaves the Dynamic Island. If one finished thread hasn't been viewed yet, only that row stays on the card. A new completion arriving afterwards still shows normally.
Actual behavior
T3 · Donereappears in the Dynamic Island as soon as the app goes to the background. It stays at least until 15 minutes after the completion. Because of #11939, it often stays for hours. Viewing the threads has no effect.How iOS expects this to work
From Apple's documentation:
stale-dateonly marks content as outdated. Only anendevent with adismissal-dateremoves the activity. "The system ignores an ActivityKit push notification if it arrives after the Live Activity ended."T3 deliberately sends Done as an
update, not anend. Ending would retire the activity's push token, so the next turn could not reuse the card. That trade-off is reasonable while the result is unseen. Once the user has seen it, the activity is showing a finished task the user already knows about, which Apple's guidance says to end. Today nothing ever tells the relay that the user has seen it.Root cause (main @ 4ee6bfd)
1. Viewing a thread is recorded but never reaches the relay.
ThreadDetailScreen.tsx:833) "so the 'Done' marker clears on every device". The orchestrator stores it aslastVisitedAtwithout bumpingupdatedAt(Orchestrator.ts:2240).shouldPublishAgentAwarenessEventreturnsfalseforthread.visited(AgentAwarenessRelay.ts:118falls through to the turn-item check on line 131). A test asserts this (AgentAwarenessRelay.test.ts:286).projectThreadAwarenessV2doesn't readlastVisitedAt(agentAwareness.ts:43). A seen completion and an unseen one publish the same state.agentActivityAggregate.ts:62).2. Opening the app re-sends Done instead of clearing it.
On every foreground, at most once per 60 seconds per token (
remoteRegistration.ts:846), the app re-registers its unchanged activity token.LiveActivities.registertreats that like a new card. It resetslastAggregateJsontonullandremoteStartedAtto now (LiveActivities.ts:169-176). The replay that follows (MobileRegistrations.ts:78) has no previous aggregate to compare against. It therefore sends a freshlive_activity_updatewith the Done content, with no alert because it's a replay. That update sets a newstale-date10 minutes out (ApnsClient.ts:165). The result: opening the app is the action that makes the Done card look fresh again.3. The reset also suppresses an
endfor 2 minutes after every app open.Because
remoteStartedAtis reset to now,FRESHLY_ARMED_GRACE_MS(ApnsDeliveries.ts:63,:268) treats an existing card as just created. For 2 minutes, any empty aggregate is dropped instead of ending the card. The grace exists for a genuinely new card whose first publish hasn't arrived yet. It shouldn't apply to a token the relay has been delivering to for minutes. This matters for the fix below: the user opens the app and then views the thread, which happens inside that 2-minute window.Verified with a temporary scenario test against the real
makeAggregateStateandApnsDeliveries.sendForTarget(not committed). The card already received Done at t=0, and the row is shown both as stored and as it looks after same-token re-registration:registerDevicereplay (row untouched)registerLiveActivityreplay (row reset)[Done]live_activity_updatere-sending Done, no alertnulllive_activity_endBoth replays fire from the same foreground event, and the client doesn't order them. Past the 15-minute window, whether opening the app ends the card therefore depends on which request the relay handles first. That explains part of why this feels inconsistent from one open to the next.
Proposed fix
Treat a viewed terminal row as gone, using the deletion path that already exists. This needs no contract or widget change.
Server (
AgentAwarenessRelay). When a thread's projected state iscompletedorfailedandlastVisitedAt >= latestRunCompletedAt, publishnull(a tombstone) instead of the terminal state. Removethread.visitedfrom the list of events that don't trigger a publish. The existing publish-identity dedupe keeps visits to running threads free. The existing 5-second tombstone confirmation still applies. Compare againstlatestRunCompletedAtrather thanupdatedAt, because metadata edits bumpupdatedAt([Bug]: Changing the model on a finished thread sends a new "Done" alert to the phone #11938).Relay. Nothing new for content. The tombstone already does
rows.remove, and the aggregate is recomputed:null, solive_activity_endis sent with the 15-second contentless dismissal. Per Apple, that removes it from the Dynamic Island immediately.Android FCM gets the same aggregate.
Relay (
LiveActivities.register). When the sameactivityPushTokenre-registers for the same device, keepremoteStartedAt. Without this, step 1's end is usually swallowed by the 2-minute grace, because the user just opened the app. Whether to also keeplastAggregateJson, which would stop re-sending unchanged Done content on every open, is the maintainers' call. Once seen rows are removed, the replay is correct either way.Tests.
thread.visitedat or after completion publishes a tombstone; a visit before completion, or on a running thread, publishes nothing;remote_started_at, and an empty aggregate right afterwards ends the card;Arming stays as it is. On open, the app only creates a card when
activeCount > 0(remoteRegistration.ts:1138), so ending a seen Done-only card won't be recreated on the next open. A new completion after the end behaves as it does today after any end.Open product question. The visit watermark is shared across devices, so reading a thread on desktop would also end the phone's card. That matches how the Done marker already clears "on every device".
docs/user/mobile-notifications.mdsays "Viewing a thread on another device does not silence your phone's alerts", but that sentence is about alerts, which have already fired by this point, not about the card. If maintainers want the card to clear only when viewed on the phone itself, the visit needs to carry its origin. That's a larger change.Relationship to existing issues
coalesce(lastLiveActivityDeliveryAt, remoteStartedAt, …)). It never looks at whether the user saw the result. And because every app open resetsremoteStartedAtand delivers a replay (root cause 2), each open restarts that 15-minute idle clock. Keep both issues; this one is about the content of the card, #11939 about its expiry.updatedAtand re-send Done alerts. That extends this problem, but it's a separate trigger. The proposed fix compares againstlatestRunCompletedAtso it doesn't depend on that change.Impact
Minor bug or occasional failure. The Dynamic Island keeps saying "Done" for work the user has already reviewed, so it stops signalling anything. Users learn to ignore it, which defeats the point of the card.
Version or commit
main @ 4ee6bfd
Environment
iPhone with Dynamic Island, T3 Code Mobile with T3 Connect and Live Activity Updates on. Root cause established by code reading plus a scenario test against the relay delivery code at the commit above.
Workaround
Swipe the Live Activity away on the Lock Screen, or turn off Live Activity Updates.