[Bug]: Renaming a thread fires a push notification to other devices (title-only metadata change)#25582

opened17d agobyMMdSadiqMd

Before submitting

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

Area

apps/server

Summary

Renaming a thread (changing only its title) fires a push notification to other devices. Concretely: rename a thread's title on the desktop app, and the mobile app receives a push notification (e.g. "Agent finished") for that thread. A title edit is a silent metadata change and should not notify anyone.

Steps to reproduce

  1. Have a thread whose latest run has already completed (or is in an attention phase such as waiting-for-approval / waiting-for-input), with mobile push notifications enabled for it.
  2. On the desktop app, rename that thread (edit its title in the sidebar) and commit the new title.
  3. Observe the mobile app receives a push notification for that thread.

Expected behavior

Renaming a thread's title changes only metadata and sends no push/in-app notification.

Actual behavior

A push notification is delivered (e.g. a fresh "Agent finished"/terminal or attention alert) to other devices every time the title is changed.

Root cause (investigated, against main @ 0712398)

The rename is a title-only metadata change, but it flows through the agent-activity relay as a notifiable state change. The chain:

1. Client sends title only (no notification requested). apps/web/src/components/Sidebar.tsx:1324 (commitThreadRename) calls updateThreadMetadata({ input: { threadId, title } }) — title only. packages/client-runtime/src/operations/commands.ts:605 dispatches command thread.metadata.update.

2. Server reducer applies the title and always bumps updatedAt. apps/server/src/orchestration-v2/Orchestrator.ts:3349-3421:

ts
1return {
2 ...thread,
3 ...(command.title === undefined ? {} : { title: command.title }), // 3369
4 ...
5 updatedAt: now, // 3420 — always bumped
6};

3. The command maps to a generic thread.metadata-updated event. Orchestrator.ts:3709-3711.

4. The awareness relay treats thread.metadata-updated as publish-worthy. apps/server/src/relay/AgentAwarenessRelay.ts:88-104 returns true for thread.metadata-updated, enqueuing a thread publish.

5. The published state includes the title, and the de-dup identity strips only updatedAt — so a title change is "meaningful". The projected state carries threadTitle (packages/shared/src/agentAwareness.ts:83-94), and:

ts
1// apps/server/src/relay/AgentAwarenessRelay.ts:158-164
2function agentAwarenessPublishIdentity(state: RelayAgentActivityState | null): string {
3 if (state === null) return "null";
4 const { updatedAt: _updatedAt, ...meaningfulState } = state; // only updatedAt removed; threadTitle stays
5 return JSON.stringify(meaningfulState);
6}

Because threadTitle differs, the publish-skip check does not short-circuit, and the thread's agent-activity state is re-published to the relay.

6. The notification-only delivery path re-fires because it keys off updatedAt freshness with no previous-vs-next phase comparison. infra/relay/src/agentActivity/agentActivityAlerts.ts:149-161:

ts
1export function shouldAlertForActivity(input: {...}): boolean {
2 return (
3 input.preferences?.notificationsEnabled === true &&
4 alertAllowedForPhase(input.preferences, input.phase) &&
5 ((input.phase !== "completed" && input.phase !== "failed") ||
6 isFreshTerminalNotification(input.updatedAt, input.nowMs)) // only freshness, no prev/next phase
7 );
8}

isFreshTerminalNotification (same file, lines 16-21) returns true when updatedAt is within 2 minutes of now. Since the rename bumped updatedAt to now (step 2), an already-completed thread now looks "freshly terminal," so a brand-new terminal push is sent. Attention phases (waiting_for_approval / waiting_for_input) are also permitted by alertAllowedForPhase and re-alert the same way.

Why the Live Activity path doesn't misfire but the push path does

The Live Activity delivery path (infra/relay/src/agentActivity/ApnsDeliveries.ts, chooseLiveActivityDelivery → alertForNewlyTerminal → newlyTerminalRows in agentActivityAlerts.ts) compares previous vs next phase and emits nothing when the phase was already terminal. The notification-only path (sendPushNotificationForTarget → notificationForAggregate → shouldAlertForActivity) makes no such comparison — it relies solely on isFreshTerminalNotification(updatedAt). That asymmetry is the concrete defect.

Server vs client

  • Server (root cause): the relay republishes on a title-only change because threadTitle is part of the publish identity and updatedAt is bumped. The client requests no notification.
  • Relay (amplifier): the notification-only path re-rings on a republished terminal/attention state with no phase-transition guard.
  • There is no separate client-only toast-on-rename; the mobile device receives a legitimate push from the relay.

Suggested fix direction

Primary (server, narrowest): make a title-only metadata change non-notifiable. Either —

  1. In agentAwarenessPublishIdentity (AgentAwarenessRelay.ts:158-164), strip presentation-only fields (threadTitle, projectTitle, headline, detail, modelTitle, deepLink) in addition to updatedAt, so only phase/attention-relevant fields drive a republish. (Trade-off: a notification/card's displayed title may lag until the next genuine publish.)
  2. Or, in shouldPublishAgentAwarenessEvent (AgentAwarenessRelay.ts:88), inspect the thread.metadata-updated payload and skip when the diff is title-only (no phase-affecting field changed).

Defense-in-depth (relay): bring the notification-only path in line with the Live Activity path — have shouldAlertForActivity / notificationForAggregate compare against the last-notified phase (as newlyTerminalRows does) rather than relying solely on isFreshTerminalNotification(updatedAt), so re-publishing an already-terminal/attention thread cannot re-alert.

Also consider not bumping the awareness-relevant timestamp (Orchestrator.ts:3420) for title-only changes, since that updatedAt: now is what makes a stale terminal row look "fresh."

Relevant files

  • apps/web/src/components/Sidebar.tsx:1324 (commitThreadRename), packages/client-runtime/src/operations/commands.ts:605 (updateThreadMetadata)
  • apps/server/src/orchestration-v2/Orchestrator.ts:3349-3421 (reducer, updatedAt: now), :3709-3711 (event mapping)
  • apps/server/src/relay/AgentAwarenessRelay.ts:88-104 (shouldPublishAgentAwarenessEvent), :158-164 (agentAwarenessPublishIdentity)
  • packages/shared/src/agentAwareness.ts:83-94 (projectThreadAwarenessV2, includes threadTitle)
  • infra/relay/src/agentActivity/agentActivityAlerts.ts:16-21 (isFreshTerminalNotification), :149-161 (shouldAlertForActivity); compare newlyTerminalRows / alertForNewlyTerminal
  • infra/relay/src/agentActivity/ApnsDeliveries.ts (sendPushNotificationForTarget vs chooseLiveActivityDelivery)

Impact

Minor bug or occasional failure

Version or commit

main @ 0712398

Environment

Rename performed on the desktop app; errant push received on the mobile app. Server + relay (apps/server, infra/relay).


Contributions welcome

This is open for anyone who wants to pick it up — the root cause and a suggested fix direction are laid out above. If you'd like to take it, leave a comment so we don't double up, and feel free to ask questions on the issue.