Skip to content

[Bug]: iOS Live Activity keeps showing "T3 Done" for hours when no other agent event arrives #11939

Description

@ishaanko

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/mobile

Steps to reproduce

  1. Link an environment to T3 Connect, enable Live Activity Updates on an iPhone, and start a turn from the phone so a Live Activity is armed.
  2. Let the turn finish. The Dynamic Island and lock screen card switch to "T3 Done".
  3. Tap the notification or the Dynamic Island. The thread opens in the app.
  4. Leave the phone alone. Do not start any other agent work on any linked environment.
  5. Check the Dynamic Island 30 minutes later, or hours later.

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

Root cause, from reading the relay code:

infra/relay/src/agentActivity/agentActivityAggregate.ts keeps finished rows in the aggregate
for 15 minutes and returns null after that, and ApnsDeliveries turns a null aggregate into a
live_activity_end. But the aggregate is only recomputed when an environment publishes a new
state or the app re-registers its activity token. A user with no further agent activity never
triggers either, so the end push is never sent.

Opening the app does re-register the token, but inside the 15 minute window that replay just
re-sends the same Done aggregate. After the window, nothing runs at all. The 5 minute cron in
infra/relay/src/worker.ts prunes the terminal rows from the database but does not end the
cards that were showing them.

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.

Activity

  1. juliusmarminge commented on Sep 15, 2026

    @juliusmarminge
    Member

    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

    makeAggregateState in infra/relay/src/agentActivity/agentActivityAggregate.ts keeps a completed/failed row in the aggregate for TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS (15 minutes), then returns null. chooseLiveActivityDelivery in ApnsDeliveries.ts turns that null into live_activity_end (after the 2-minute freshly-armed grace).

    That recompute only runs on two event paths:

    1. An environment publishes a new awareness state (AgentActivityPublisher.publish).
    2. The app re-registers its activity token (replayForLiveActivityRegistration from registerLiveActivity / foreground reconciliation).

    A phone that finished a turn and then sits idle hits neither path. The 5-minute cron in infra/relay/src/worker.ts only pruneTerminals DB rows older than 30 minutes. It does not recompute aggregates or enqueue APNs. So the lock-screen / Dynamic Island card stays on the last live_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 sees null and sends the end.

    The completion itself is an update, not an end. ApnsClient puts stale-date = delivery + 10 minutes on updates and only puts dismissal-date on end events (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 clear activityPushToken in LiveActivities.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.ts sends activity_expires_at = updatedAt + 15m, and the native adapter calls setTimeoutAfter. 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.updatedAt moves (model change / settlement). Pins terminal awareness time. Does not send live_activity_end after 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 Android activity_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_end after the 15-minute display TTL. Searched live_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 end on the completion update (that drops the activity token).

    1. On the existing */5 * * * * cron, after (or before) pruneTerminal, sweep armed iOS targets (activity_push_token set, ended_at null). Recompute makeAggregateState and call sendForTarget. A now-null aggregate already maps to live_activity_end and already respects the freshly-armed grace.
    2. Optionally align the prune cutoff (30 minutes) with the 15-minute display TTL after the end is queued, so cleanup cannot race delivery.
    3. 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 attribute Closes #11939 to #9081 or #11914.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions