Repository navigation
Clear restored agent notifications once the agent is gone #17067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
teamleaderleo
merged 7 commits into
manaflow-ai:main
from
wowpotato:fix/stale-agent-notification-after-restore
Oct 4, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5350891
test: prune read notifications of agents that died with the previous app
wowpotato 5162bfb
fix: drop restored agent notifications once the agent does not return
wowpotato 3eb9dea
test: keep restored notifications while the resume is in flight
wowpotato fd78af5
fix: defer restored notification prune while the resume is in flight
wowpotato f144652
test: keep read non-agent notifications on a restored agent pane
wowpotato 779a72e
fix: track only agent-produced notifications on restored agent panes
wowpotato 422ccbd
Merge main (c707d8a32626) into fix/stale-agent-notification-after-res…
wowpotato File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import Foundation | ||
|
|
||
| /// Orphaned agent notifications after a session restore. | ||
| /// | ||
| /// Notifications are persisted across relaunch so an unseen agent result is not | ||
| /// lost. But an agent that was alive when cmux quit dies with the app without | ||
| /// firing its SessionEnd hook, so nothing ever clears its last "Completed in …" | ||
| /// notification, and the sidebar keeps showing it as the workspace's latest | ||
| /// summary on every subsequent launch. The live-session safety net | ||
| /// (`clearStaleAgentPIDs`) cannot catch it either: agent PIDs are not | ||
| /// restored, so there is no dead PID to sweep. | ||
| /// | ||
| /// Restore therefore records the notifications that belonged to a local agent | ||
| /// pane, and the periodic sweep drops the ones that were read once it is clear | ||
| /// the agent did not come back to that pane. Unread ones survive until read. | ||
| extension Workspace { | ||
| /// Whether a persisted panel was hosting a local agent session that died | ||
| /// with the previous app process. Remote terminals are excluded: their agent | ||
| /// can outlive the local app and keep owning its notifications. | ||
| nonisolated static func restoredPanelHostedLocalAgent(_ panelSnapshot: SessionPanelSnapshot) -> Bool { | ||
| guard let terminal = panelSnapshot.terminal else { return false } | ||
| guard terminal.isRemoteTerminal != true, terminal.remotePTYSessionID == nil else { return false } | ||
| return terminal.resumeBinding != nil || terminal.agent != nil | ||
| } | ||
|
|
||
| /// Records, per restored panel, the persisted agent notifications of panes | ||
| /// that were hosting a local agent, keyed by the panel's new id. Replaces any | ||
| /// tracking left from an earlier restore of this workspace. | ||
| func trackRestoredAgentNotifications( | ||
| from snapshot: SessionWorkspaceSnapshot, | ||
| oldToNewPanelIds: [UUID: UUID] | ||
| ) { | ||
| restoredAgentNotificationIdsByPanelId.removeAll(keepingCapacity: false) | ||
| guard snapshot.remote == nil else { return } | ||
| for panelSnapshot in snapshot.panels where Self.restoredPanelHostedLocalAgent(panelSnapshot) { | ||
| guard let newPanelId = oldToNewPanelIds[panelSnapshot.id] else { continue } | ||
| // Only agent-produced notifications are the agent's to retire; a | ||
| // `cmux notify` banner on the same pane keeps its own lifetime. | ||
| // Unknown provenance restores as agent-produced, as the store does. | ||
| let ids = Set((panelSnapshot.notifications ?? []) | ||
| .filter { $0.isAgentEvent ?? true } | ||
| .map(\.id)) | ||
| guard !ids.isEmpty else { continue } | ||
| restoredAgentNotificationIdsByPanelId[newPanelId] = ids | ||
| } | ||
| #if DEBUG | ||
| cmuxDebugLog( | ||
| "restoredAgentNotifications.track workspace=\(id.uuidString.prefix(8)) panels=\(snapshot.panels.count) tracked=\(restoredAgentNotificationIdsByPanelId.count)" | ||
| ) | ||
| #endif | ||
| } | ||
|
|
||
| /// Removes read restored agent notifications on panes the agent never | ||
| /// returned to. A pane that registered a new agent PID is handed back to the | ||
| /// agent hooks, which own its notifications from then on. A pane whose | ||
| /// restored resume is still in flight is left for a later sweep: the agent | ||
| /// may not have reported its PID yet. | ||
| /// | ||
| /// Tracked notifications are looked up by id, so a sweep that removes | ||
| /// nothing does not scan the store; each removal happens once. | ||
| @discardableResult | ||
| func pruneOrphanedRestoredAgentNotifications(store: TerminalNotificationStore) -> Bool { | ||
| guard !restoredAgentNotificationIdsByPanelId.isEmpty else { return false } | ||
| var didRemove = false | ||
| for (panelId, trackedIds) in restoredAgentNotificationIdsByPanelId { | ||
| if restoredAgentLifecycle.ownsInFlightRestoredCommand(panelId: panelId) { continue } | ||
| let hasLiveAgent = !(agentPIDKeysByPanelId[panelId] ?? []).isEmpty | ||
| #if DEBUG | ||
| cmuxDebugLog( | ||
| "restoredAgentNotifications.prune workspace=\(id.uuidString.prefix(8)) panel=\(panelId.uuidString.prefix(8)) exists=\(panels[panelId] != nil ? 1 : 0) liveAgent=\(hasLiveAgent ? 1 : 0) tracked=\(trackedIds.count)" | ||
| ) | ||
| #endif | ||
| guard panels[panelId] != nil, !hasLiveAgent else { | ||
| restoredAgentNotificationIdsByPanelId.removeValue(forKey: panelId) | ||
| continue | ||
| } | ||
| let remaining = trackedIds.compactMap { store.notification(id: $0) } | ||
| .filter { $0.matches(tabId: id, surfaceId: panelId) } | ||
| var stillTracked = Set<UUID>() | ||
| for notification in remaining { | ||
| if notification.isRead { | ||
| store.remove(id: notification.id) | ||
| didRemove = true | ||
| } else { | ||
| stillTracked.insert(notification.id) | ||
| } | ||
| } | ||
| if stillTracked.isEmpty { | ||
| restoredAgentNotificationIdsByPanelId.removeValue(forKey: panelId) | ||
| } else { | ||
| restoredAgentNotificationIdsByPanelId[panelId] = stillTracked | ||
| } | ||
| } | ||
| return didRemove | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.