Repository navigation
[Bug]: iOS Live Activity keeps showing "T3 Done" for hours when no other agent event arrives #11939
Description
Activity
Triage
Confirmed on
main@3efdcc529. This is a real relay bug, not a mobile-widget defect, and it is not a duplicate of #11938, #10864, or #11886.User docs already promise the 15-minute window: “Finished results remain visible for up to 15 minutes” (
docs/user/mobile-notifications.md). The Done card is drawn; the end push that should retire it never fires unless something else happens.What the code does today
makeAggregateStateininfra/relay/src/agentActivity/agentActivityAggregate.tskeeps a completed/failed row in the aggregate forTERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS(15 minutes), then returnsnull.chooseLiveActivityDeliveryinApnsDeliveries.tsturns thatnullintolive_activity_end(after the 2-minute freshly-armed grace).That recompute only runs on two event paths:
- An environment publishes a new awareness state (
AgentActivityPublisher.publish). - The app re-registers its activity token (
replayForLiveActivityRegistrationfromregisterLiveActivity/ foreground reconciliation).
A phone that finished a turn and then sits idle hits neither path. The 5-minute cron in
infra/relay/src/worker.tsonlypruneTerminals DB rows older than 30 minutes. It does not recompute aggregates or enqueue APNs. So the lock-screen / Dynamic Island card stays on the lastlive_activity_update(“T3 Done”) until another agent event arrives, the user foregrounds the app after the 15-minute TTL, or iOS eventually retires the activity (hours later).Tapping the island or the notification does not locally end the card. Client
activity.end("immediate")is only used on sign-out and duplicate-instance cleanup (remoteRegistration.ts). Opening the app inside the 15-minute window just replays the same Done aggregate. Opening it after the window is the documented workaround, because replay then seesnulland sends the end.The completion itself is an update, not an end.
ApnsClientputsstale-date = delivery + 10 minuteson updates and only putsdismissal-dateonendevents (5 minutes with content, 15 seconds without). iOS can dim the card; it cannot dismiss it without that later end. Ending immediately on completion would also clearactivityPushTokeninLiveActivities.markDelivery, so a follow-up turn within the 15-minute window could not update the same card. The update-then-end design is intentional; the scheduled end is what is missing.Android already honors the same 15-minute policy without a second push:
fcmPayloads.tssendsactivity_expires_at = updatedAt + 15m, and the native adapter callssetTimeoutAfter. That is why this is iOS-only.Related, not a duplicate
Issue / PR Why it does not close this #11938 / open #9081 Fresh “Done” alerts when thread.updatedAtmoves (model change / settlement). Pins terminal awareness time. Does not sendlive_activity_endafter 15 minutes of silence.#11886 / open #11914 Startup catch-up treated as new alerts. Marks reconnect as silent replay. A republished completed row would keep the Done card, not schedule its retirement. #10864 Accepted enhancement: unify iOS stale-date(delivery clock) vs Androidactivity_expires_at(state clock). Policy for which timestamp to use once a delivery happens. This bug is that iOS never gets the end delivery at all when idle. Keep #10864 open; do not merge the two.#10863 Diagnostics follow-up from the same Android audit. Not this gap. #9057 First-launch-after-update auto-settle flood. Different trigger. Open #11073 Concurrent threads hiding iOS alerts. Selection, not expiry. No open or merged PR adds a delayed / cron
live_activity_endafter the 15-minute display TTL. Searchedlive_activity_end,pruneTerminal, and Live Activity expiry PRs; nothing claims this.Suggested fix (relay only)
Do not fix this in the iOS widget, and do not send
endon the completion update (that drops the activity token).- On the existing
*/5 * * * *cron, after (or before)pruneTerminal, sweep armed iOS targets (activity_push_tokenset,ended_atnull). RecomputemakeAggregateStateand callsendForTarget. A now-null aggregate already maps tolive_activity_endand already respects the freshly-armed grace. - Optionally align the prune cutoff (30 minutes) with the 15-minute display TTL after the end is queued, so cleanup cannot race delivery.
- Regression: armed iOS target + only a completed row older than 15 minutes + no further publish → sweep queues
live_activity_end. Current tests cover “null aggregate ends the card” and “TTL drops the row from the aggregate,” but nothing asserts that the cron/time passing alone produces the end.
A per-completion delayed APNs job is an alternative to a user sweep; either is fine if it re-reads current state before sending (so new work in the window cancels the end).
Workaround
Start another turn on any linked environment, or foreground the app more than 15 minutes after the turn finished so token re-registration replays an empty aggregate. Swiping the Live Activity away also works. A mobile OTA will not fix an idle phone that never opens.
Severity: minor (card lies about being current; no lost work). Still a documented product-policy miss, and it only clears by accident.
Accepting as a bug on
infra/relay. Keep #10864 as the clock-policy follow-up. Do not attributeCloses #11939to #9081 or #11914.- An environment publishes a new awareness state (
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 15, 2026
Before submitting
Area
apps/mobile
Steps to reproduce
Expected behavior
The Done card stays for the documented 15 minute window, then goes away on its own.
Actual behavior
The Dynamic Island keeps saying "T3 Done" for hours. Tapping the notification or the island does not clear it. It only goes away when some other agent event reaches the phone, or when iOS retires the activity itself.
Impact
Minor bug or occasional failure
Version or commit
main @ 3efdcc5
Environment
iOS 26, T3 Code Mobile with T3 Connect, T3 Code Nightly 0.0.41 as the host.
Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
Start another turn on any linked environment, or open the app again more than 15 minutes after the turn finished so the token re-registration replays an empty aggregate and ends the card.