From 3fc5381b437701c4c79ca785d8111036381494de Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Wed, 9 Sep 2026 13:45:34 -0700 Subject: [PATCH] fix(claude): retire leftover foreground rows when the next turn starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A foreground `task_started` arriving with no turn open has no `result` coming to retire it, so it sat in the strip indefinitely — with no per-row stop, since foreground rows are not stoppable — and refused conversation commands behind an instruction nobody could follow. Settle on turn start as well as on `result`. This is cleanup only: visibility never consults `startsTurn`, so a missed one degrades to today's behaviour and can never switch the feature off. It shortens the row's life to the next turn; the case where no further turn is ever sent is filed separately. --- .../claude-background-task-tracker.test.ts | 19 +++++++++++++++++++ .../claude/claude-background-task-tracker.ts | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/claude/claude-background-task-tracker.test.ts b/src/main/claude/claude-background-task-tracker.test.ts index 040fbce29d2..4e1fae6ff8a 100644 --- a/src/main/claude/claude-background-task-tracker.test.ts +++ b/src/main/claude/claude-background-task-tracker.test.ts @@ -114,6 +114,25 @@ describe('ClaudeBackgroundTaskTracker', () => { }) }) + it('retires a phantom foreground row when the next turn starts', () => { + // A foreground `task_started` with no turn open has no `result` coming to + // retire it, so it would sit in the strip — with no stop of its own — and + // refuse a conversation command. Turn start is the same evidence `result` + // is, and settling on it is cleanup only: nothing gates visibility on it. + const tracker = new ClaudeBackgroundTaskTracker() + tracker.observe( + system('task_started', { + task_id: 'phantom', + task_type: 'local_agent', + is_backgrounded: false + }) + ) + expect(tracker.state?.tasks).toEqual([{ id: 'phantom', kind: 'agent', stoppable: false }]) + + tracker.observe({ type: 'user' }, true) + expect(tracker.state).toBeNull() + }) + it('publishes bounded display details when a running task description changes', () => { const tracker = new ClaudeBackgroundTaskTracker() expect( diff --git a/src/main/claude/claude-background-task-tracker.ts b/src/main/claude/claude-background-task-tracker.ts index 3105f0e97f3..64c285b447b 100644 --- a/src/main/claude/claude-background-task-tracker.ts +++ b/src/main/claude/claude-background-task-tracker.ts @@ -89,6 +89,15 @@ export class ClaudeBackgroundTaskTracker { } observe(message: Record, startsTurn = false): boolean { + // A new turn is the same evidence `result` is: nothing the previous turn + // left foreground is still that turn's work. This is CLEANUP ONLY — a row's + // visibility never depends on `startsTurn`, which is Orca's own + // dispatch-correlation bookkeeping and is false by design for undispatched + // turns, so a missed one degrades to the old behaviour and can never hide + // live work. + if (startsTurn) { + this.settleForegroundTasks() + } if (message.type === 'result') { this.settleForegroundTasks() } else if (message.type === 'system') {