diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index cfbdd290b1..26cb8c4587 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -27,7 +27,7 @@ snapshots: archive-archivedtasksview--many-tasks--dark: hash: v1.k4693efd2.40d24f07a24c6400c32ae68b39fd908a056beb5dc6df8bcb237ec2ab2b494f4a.l2vjfSQDalCPRtDNV7pbfTKGlMsQoI81jI-UdJfLHWE archive-archivedtasksview--many-tasks--light: - hash: v1.k4693efd2.52ec9963bfe6f248ffcdaa4c26945d2b7e303115825b65d978aa5aefd4af1007.ZLtOfTZUrznhYeB-N2SIHzQWRzF6TDSaipNkfd8uzjM + hash: v1.k4693efd2.74c25b303262f2d4c0f22890d351e76f482827548548f7c023bc309327c927b8.DP9VVrvBz1naIJMwHORkfqm69BlO785ulOt4o6zFs94 archive-archivedtasksview--mixed-modes--dark: hash: v1.k4693efd2.d94039b8cc17a4ad1b720364f41fee58a4843aa9a901443997ba62602f698794.441LWZhT-2WQZJHni4LoOgQsOSr2O103NZOk1pF8ME4 archive-archivedtasksview--mixed-modes--light: diff --git a/packages/core/src/sessions/cloudTaskUpdateNotifications.test.ts b/packages/core/src/sessions/cloudTaskUpdateNotifications.test.ts index 4fd5c1bc70..5ad145a0c0 100644 --- a/packages/core/src/sessions/cloudTaskUpdateNotifications.test.ts +++ b/packages/core/src/sessions/cloudTaskUpdateNotifications.test.ts @@ -206,11 +206,14 @@ describe("cloud task update notifications", () => { }); // Each case applies a sequence of updates to a fresh harness; `expected` is - // the resulting notify count. Snapshots never ring; each live turn_complete - // rings once. Re-delivered stream entries are dropped upstream in - // CloudTaskService by their event id (see cloud-task.test.ts), so a replay - // never reaches this layer. + // the resulting notify count. Snapshots never ring; each armed turn rings at + // most once even if the producer writes duplicate completion entries. it.each([ + { + label: "a completion event without an armed turn", + updates: [logsUpdate([turnComplete()], 1)], + expected: 0, + }, { label: "a live turn that starts and completes", updates: [logsUpdate([sessionPrompt(1), turnComplete()], 2)], @@ -224,6 +227,14 @@ describe("cloud task update notifications", () => { ], expected: 2, }, + { + label: "duplicate completion events for one turn", + updates: [ + logsUpdate([sessionPrompt(1), turnComplete()], 2), + logsUpdate([turnComplete()], 3), + ], + expected: 1, + }, { // Opening a task mid-turn: its session/prompt is already in history and // only the turn_complete arrives live. The completion must still ring. diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index 096826e4fb..3846d2164f 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -1947,6 +1947,9 @@ export class SessionService { // above. Cloud sessions never see that response. const session = this.getSessionByRunId(taskRunId); if (session?.isCloud) { + const completedActiveTurn = + session.currentPromptId !== null && + session.currentPromptId !== undefined; const turnStartedAtTs = this.liveTurnContent.get(taskRunId)?.startedAtTs ?? session.promptStartedAt; @@ -1957,7 +1960,7 @@ export class SessionService { }); if (isLive) { // Queued messages will start a new turn — suppress the "done" notification in that case. - if (session.messageQueue.length === 0) { + if (completedActiveTurn && session.messageQueue.length === 0) { this.d.notifyPromptComplete( session.taskTitle, "end_turn",