You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I searched existing issues and did not find a duplicate.
I included enough detail to reproduce or investigate the problem.
Area
apps/server
(The fix spans apps/server (AgentAwarenessRelay) and infra/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
iPhone with a Dynamic Island, T3 Connect linked, Live Activity Updates on, environment publishing agent activity.
Start a turn from the phone so the Live Activity is armed. Optionally run more than one thread.
Let every turn finish. The island shows T3 · Done.
Within 15 minutes, open the T3 app, open each finished thread, and read the result.
Go back to the Home Screen.
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 · Done reappears 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:
HIG, Live Activities: "Always end a Live Activity immediately when the task or event ends, and consider setting a custom dismissal time." and "When a Live Activity ends, the system immediately removes it from the Dynamic Island and in CarPlay. On the Lock Screen […] it remains for up to four hours."
HIG: "Your Live Activity appears in the Dynamic Island while your app isn't in use." This is why the card seems to "come back" when you leave the app. It was never ended; iOS just hides it while T3 is in the foreground.
Starting and updating Live Activities with ActivityKit push notifications: stale-date only marks content as outdated. Only an end event with a dismissal-date removes 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 an end. 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.
1. Viewing a thread is recorded but never reaches the relay.
Mobile records a server-side visit watermark when a thread is on screen (ThreadDetailScreen.tsx:833) "so the 'Done' marker clears on every device". The orchestrator stores it as lastVisitedAt without bumping updatedAt (Orchestrator.ts:2240).
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.register treats that like a new card. It resets lastAggregateJson to null and remoteStartedAt to now (LiveActivities.ts:169-176). The replay that follows (MobileRegistrations.ts:78) has no previous aggregate to compare against. It therefore sends a fresh live_activity_update with the Done content, with no alert because it's a replay. That update sets a new stale-date 10 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 end for 2 minutes after every app open.
Because remoteStartedAt is 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 makeAggregateState and ApnsDeliveries.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:
App opened at
Aggregate
registerDevice replay (row untouched)
registerLiveActivity replay (row reset)
t+5 min
[Done]
nothing (unchanged, suppressed)
live_activity_update re-sending Done, no alert
t+20 min
null
live_activity_end
nothing (freshly-armed grace)
Both 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 is completed or failed and lastVisitedAt >= latestRunCompletedAt, publish null (a tombstone) instead of the terminal state. Remove thread.visited from 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 against latestRunCompletedAt rather than updatedAt, because metadata edits bump updatedAt ([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:
other threads still live or unseen: the seen row disappears and the card stays;
nothing left: the aggregate is null, so live_activity_end is 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 same activityPushToken re-registers for the same device, keep remoteStartedAt. Without this, step 1's end is usually swallowed by the 2-minute grace, because the user just opened the app. Whether to also keep lastAggregateJson, 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.
server: a thread.visited at or after completion publishes a tombstone; a visit before completion, or on a running thread, publishes nothing;
relay: same-token re-registration keeps remote_started_at, and an empty aggregate right afterwards ends the card;
relay: two terminal rows with one viewed leaves one row on the card, not an end.
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.md says "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.
The card isn't ended after the 15-minute window when nothing else happens. #11943 adds a sweep that ends cards with no delivery for 15 minutes (coalesce(lastLiveActivityDeliveryAt, remoteStartedAt, …)). It never looks at whether the user saw the result. And because every app open resets remoteStartedAt and 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.
Metadata changes on a finished thread bump updatedAt and re-send Done alerts. That extends this problem, but it's a separate trigger. The proposed fix compares against latestRunCompletedAt so it doesn't depend on that change.
Dismisses leftover ended activities before starting a new card. Different lifecycle step.
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.
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.
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.