From 1fa32fdfb0605fd1dbb1a332d9830b29d6767919 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Mon, 25 May 2026 06:44:03 +0300 Subject: [PATCH 1/3] fix(code): fire completion notifications for cloud tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local sessions notify on turn completion via the JSON-RPC `stopReason: "end_turn"` response, but cloud sessions never see that response — their canonical "turn done" signal is the `_posthog/turn_complete` event. The cloud branch of `updatePromptStateFromEvents` was clearing `isPromptPending` on `turn_complete` but never calling `notifyPromptComplete`, so cloud tasks silently finished with no desktop notification, dock badge, or completion sound. Fire `notifyPromptComplete` from the cloud `turn_complete` branch, gated by a new `isLive` parameter on `updatePromptStateFromEvents` so hydration and gap-fill replays of historical logs don't re-fire notifications for turns that already completed in past app runs. Also skip when the queue still has messages — those will start a new turn, mirroring the local `!hasQueuedMessages` check. Generated-By: PostHog Code Task-Id: 093f7d4f-c637-40d7-921c-74e609bdb206 --- .../features/sessions/service/service.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 930e5f0dc..451b315dc 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -1128,6 +1128,7 @@ export class SessionService { private updatePromptStateFromEvents( taskRunId: string, events: AcpMessage[], + isLive = false, ): void { for (const acpMsg of events) { const msg = acpMsg.message; @@ -1179,6 +1180,19 @@ export class SessionService { promptStartedAt: null, currentPromptId: null, }); + // Mirror local sessions, where `notifyPromptComplete` fires when + // the JSON-RPC response carries `stopReason: "end_turn"`. Gate on + // `isLive` so hydration/gap-fill replays of historical logs don't + // re-fire notifications for turns that completed in past app runs, + // and skip when the queue still has messages — those will start a + // new turn, matching the local `!hasQueuedMessages` check. + if (isLive && session.messageQueue.length === 0) { + notifyPromptComplete( + session.taskTitle, + "end_turn", + session.taskId, + ); + } } } // Lifecycle handshake from the agent — flip status to "connected" @@ -1264,7 +1278,7 @@ export class SessionService { } else { sessionStoreSetters.appendEvents(taskRunId, [acpMsg]); } - this.updatePromptStateFromEvents(taskRunId, [acpMsg]); + this.updatePromptStateFromEvents(taskRunId, [acpMsg], true); const msg = acpMsg.message; @@ -3511,7 +3525,7 @@ export class SessionService { sessionStoreSetters.clearTailOptimisticItems(taskRunId); } sessionStoreSetters.appendEvents(taskRunId, newEvents, expectedCount); - this.updatePromptStateFromEvents(taskRunId, newEvents); + this.updatePromptStateFromEvents(taskRunId, newEvents, true); } else { this.reconcileCloudLogGap({ taskId: update.taskId, From 8d84ba81d325600c53a307f0eebdecf44baab30f Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Mon, 25 May 2026 06:52:28 +0300 Subject: [PATCH 2/3] fix(code): mark task activity on cloud turn completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the local `stopReason: "end_turn"` path which calls `taskViewedApi.markActivity(taskId)` after `notifyPromptComplete`. Without this, completing a cloud turn doesn't bump `lastActivityAt`, leaving sidebar ordering stale and the unread-badge state inconsistent with local sessions. Gate the activity bump on `isLive` for the same reason as the notification — hydration / gap-fill replays must not stamp activity for turns that finished in past app runs. Also collapse the multi-line `notifyPromptComplete` call so biome ci passes. Generated-By: PostHog Code Task-Id: 093f7d4f-c637-40d7-921c-74e609bdb206 --- .../features/sessions/service/service.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 451b315dc..4c3f0b73f 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -1180,18 +1180,22 @@ export class SessionService { promptStartedAt: null, currentPromptId: null, }); - // Mirror local sessions, where `notifyPromptComplete` fires when - // the JSON-RPC response carries `stopReason: "end_turn"`. Gate on - // `isLive` so hydration/gap-fill replays of historical logs don't - // re-fire notifications for turns that completed in past app runs, - // and skip when the queue still has messages — those will start a - // new turn, matching the local `!hasQueuedMessages` check. - if (isLive && session.messageQueue.length === 0) { - notifyPromptComplete( - session.taskTitle, - "end_turn", - session.taskId, - ); + // Mirror the local `stopReason: "end_turn"` path: notify on turn + // completion and bump task activity for sidebar ordering / unread + // badges. Gate on `isLive` so hydration/gap-fill replays of + // historical logs don't re-fire notifications or stamp activity + // for turns that completed in past app runs. Skip the notification + // (but not the activity bump, matching local) when the queue still + // has messages — those will start a new turn. + if (isLive) { + if (session.messageQueue.length === 0) { + notifyPromptComplete( + session.taskTitle, + "end_turn", + session.taskId, + ); + } + taskViewedApi.markActivity(session.taskId); } } } From 6626fd58c0a6fd3b1ed2b477bbfa7defe9e4cfca Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Mon, 25 May 2026 14:51:18 +0300 Subject: [PATCH 3/3] chore(code): trim comment and use options object for isLive Generated-By: PostHog Code Task-Id: 5fb5d72d-177e-4557-94f5-82145111de52 --- .../features/sessions/service/service.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/service/service.ts b/apps/code/src/renderer/features/sessions/service/service.ts index 4c3f0b73f..7219d3730 100644 --- a/apps/code/src/renderer/features/sessions/service/service.ts +++ b/apps/code/src/renderer/features/sessions/service/service.ts @@ -1128,7 +1128,7 @@ export class SessionService { private updatePromptStateFromEvents( taskRunId: string, events: AcpMessage[], - isLive = false, + { isLive = false }: { isLive?: boolean } = {}, ): void { for (const acpMsg of events) { const msg = acpMsg.message; @@ -1180,14 +1180,8 @@ export class SessionService { promptStartedAt: null, currentPromptId: null, }); - // Mirror the local `stopReason: "end_turn"` path: notify on turn - // completion and bump task activity for sidebar ordering / unread - // badges. Gate on `isLive` so hydration/gap-fill replays of - // historical logs don't re-fire notifications or stamp activity - // for turns that completed in past app runs. Skip the notification - // (but not the activity bump, matching local) when the queue still - // has messages — those will start a new turn. if (isLive) { + // Queued messages will start a new turn — suppress the "done" notification in that case. if (session.messageQueue.length === 0) { notifyPromptComplete( session.taskTitle, @@ -1282,7 +1276,7 @@ export class SessionService { } else { sessionStoreSetters.appendEvents(taskRunId, [acpMsg]); } - this.updatePromptStateFromEvents(taskRunId, [acpMsg], true); + this.updatePromptStateFromEvents(taskRunId, [acpMsg], { isLive: true }); const msg = acpMsg.message; @@ -3529,7 +3523,9 @@ export class SessionService { sessionStoreSetters.clearTailOptimisticItems(taskRunId); } sessionStoreSetters.appendEvents(taskRunId, newEvents, expectedCount); - this.updatePromptStateFromEvents(taskRunId, newEvents, true); + this.updatePromptStateFromEvents(taskRunId, newEvents, { + isLive: true, + }); } else { this.reconcileCloudLogGap({ taskId: update.taskId,