diff --git a/Packages/macOS/CmuxAgentJournal/Sources/CmuxAgentJournal/AgentNotificationReconciler.swift b/Packages/macOS/CmuxAgentJournal/Sources/CmuxAgentJournal/AgentNotificationReconciler.swift index ec3116767905..5a21e1e34bbc 100644 --- a/Packages/macOS/CmuxAgentJournal/Sources/CmuxAgentJournal/AgentNotificationReconciler.swift +++ b/Packages/macOS/CmuxAgentJournal/Sources/CmuxAgentJournal/AgentNotificationReconciler.swift @@ -99,9 +99,12 @@ public struct AgentNotificationReconciler: Sendable { } if !session.attentionIdentities.isEmpty { session.phase = .needsInput - } else if let declaredPhase = draft.declaredPhase { - session.phase = declaredPhase - session.rootStopped = declaredPhase == .idle + } else if draft.declaredPhase == .idle { + // Only an idle declaration (a dismissed idle dialog) settles the + // turn. Feed declares every tool result `.running`, which must not + // reopen a turn that already completed. + session.phase = .idle + session.rootStopped = true } else if session.rootStopped && session.children.isEmpty { session.phase = .idle } else if session.phase == .needsInput { diff --git a/Packages/macOS/CmuxAgentJournal/Tests/CmuxAgentJournalTests/AgentNotificationReconcilerTests.swift b/Packages/macOS/CmuxAgentJournal/Tests/CmuxAgentJournalTests/AgentNotificationReconcilerTests.swift index 5bbeb5d8a349..abe2d1e7b869 100644 --- a/Packages/macOS/CmuxAgentJournal/Tests/CmuxAgentJournalTests/AgentNotificationReconcilerTests.swift +++ b/Packages/macOS/CmuxAgentJournal/Tests/CmuxAgentJournalTests/AgentNotificationReconcilerTests.swift @@ -315,6 +315,17 @@ struct AgentNotificationReconcilerTests { #expect(reconciler.lifecycleEvent(response).draft.declaredPhase == .idle) } + @Test(arguments: ["claude", "codex"]) + func runningToolResultDoesNotReopenSettledTurn(source: String) { + // Feed declares every tool result `.running`; only an idle declaration settles. + var reconciler = AgentNotificationReconciler() + _ = reconciler.apply(event(1, .turnCompleted, source: source)) + let result = event(2, .attentionResolved, source: source, request: "ordinary-tool", + notify: false, declaredPhase: .running) + #expect(reconciler.apply(result).invalidatedCorrelationKeys.isEmpty) + #expect(reconciler.lifecycleEvent(result).draft.declaredPhase == .idle) + } + @Test(arguments: ["claude", "codex"]) func ambiguousOrOlderIdentitylessResponsePreservesAttention(source: String) { for responseTurn in [nil, "previous", "turn-1"] as [String?] {