@@ -151,6 +151,8 @@ const TURN_END_METHODS = new Set([
151151interface BatchAnalysis {
152152 hasTurnEnd : boolean ;
153153 hasAwaitingUserInput : boolean ;
154+ hasTurnCompleted : boolean ;
155+ hasTurnFailed : boolean ;
154156 hasVisibleAgentOutput : boolean ;
155157 externalUserMessageCount : number ;
156158 agentMessageFinalized : boolean ;
@@ -162,6 +164,8 @@ function analyzeEntries(
162164) : BatchAnalysis {
163165 let hasTurnEnd = false ;
164166 let hasAwaitingUserInput = false ;
167+ let hasTurnCompleted = false ;
168+ let hasTurnFailed = false ;
165169 let hasVisibleAgentOutput = false ;
166170 let externalUserMessageCount = 0 ;
167171 let agentMessageFinalized = false ;
@@ -173,6 +177,15 @@ function analyzeEntries(
173177 if ( method === "_posthog/awaiting_user_input" ) {
174178 hasAwaitingUserInput = true ;
175179 }
180+ if (
181+ method === "_posthog/turn_complete" ||
182+ method === "_posthog/task_complete"
183+ ) {
184+ hasTurnCompleted = true ;
185+ }
186+ if ( method === "_posthog/error" ) {
187+ hasTurnFailed = true ;
188+ }
176189 }
177190
178191 if (
@@ -200,6 +213,8 @@ function analyzeEntries(
200213 return {
201214 hasTurnEnd,
202215 hasAwaitingUserInput,
216+ hasTurnCompleted,
217+ hasTurnFailed,
203218 hasVisibleAgentOutput,
204219 externalUserMessageCount,
205220 agentMessageFinalized,
@@ -897,21 +912,52 @@ export const useTaskSessionStore = create<TaskSessionStore>((set, get) => ({
897912 } ;
898913 } ) ;
899914
900- // Live `logs` deltas only fire pings for the "agent is blocked on the
901- // user" case. Terminal completion / failure is handled by the status
902- // block below, so we don't double-fire on every intermediate turn.
903- // Snapshots are historical replay — never ping for those.
904- const shouldPingNow =
915+ // Live `logs` deltas fire pings for three turn-boundary cases:
916+ // * agent is blocked on the user (_posthog/awaiting_user_input)
917+ // * agent finished its turn (_posthog/turn_complete / task_complete)
918+ // * agent errored out the turn (_posthog/error)
919+ // The terminal-status block below can't be relied on for these: the
920+ // turn-end log entry arrives first and clears `awaitingPing`, so by
921+ // the time status terminal fires its `preState.awaitingPing` is
922+ // already false. Status-only termination (sandbox killed without a
923+ // turn-end log) still falls through to the status block. Snapshots
924+ // are historical replay — never ping for those.
925+ const shouldPingForAwaitingInput =
905926 ! isSnapshot && wasAwaitingPing && analysis . hasAwaitingUserInput ;
927+ const shouldPingForTurnComplete =
928+ ! isSnapshot &&
929+ wasAwaitingPing &&
930+ analysis . hasTurnCompleted &&
931+ ! analysis . hasAwaitingUserInput ;
932+ const shouldPingForTurnFailed =
933+ ! isSnapshot &&
934+ wasAwaitingPing &&
935+ analysis . hasTurnFailed &&
936+ ! analysis . hasAwaitingUserInput &&
937+ ! analysis . hasTurnCompleted ;
938+ const shouldPingNow =
939+ shouldPingForAwaitingInput ||
940+ shouldPingForTurnComplete ||
941+ shouldPingForTurnFailed ;
906942 if ( shouldPingNow && usePreferencesStore . getState ( ) . pingsEnabled ) {
907943 playMeepSound ( ) . catch ( ( ) => { } ) ;
908944 Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success ) ;
909945 }
910- if ( shouldPingNow ) {
946+ if ( shouldPingForAwaitingInput ) {
911947 maybePresentLocalNotification ( {
912948 taskRunId,
913949 kind : "awaiting_user_input" ,
914950 } ) ;
951+ } else if ( shouldPingForTurnComplete ) {
952+ maybePresentLocalNotification ( {
953+ taskRunId,
954+ kind : "turn_complete" ,
955+ } ) ;
956+ } else if ( shouldPingForTurnFailed ) {
957+ maybePresentLocalNotification ( {
958+ taskRunId,
959+ kind : "task_failed" ,
960+ } ) ;
915961 }
916962 }
917963
0 commit comments