@@ -80,6 +80,8 @@ interface WorkLogEntry {
8080interface DerivedWorkLogEntry extends WorkLogEntry {
8181 activityKind : OrchestrationThreadActivity [ "kind" ] ;
8282 collapseKey ?: string ;
83+ /** Grouping key for subagent lifecycle rows (one row per agent). */
84+ taskId ?: string ;
8385}
8486
8587type RawThreadFeedEntry =
@@ -235,6 +237,63 @@ function resolvePendingUserInputAnswer(
235237 return normalizeDraftAnswer ( draft ?. selectedOptionLabel ) ;
236238}
237239
240+ /** Codex children settle via task.updated (idle/failed/interrupted), never
241+ * task.completed — these rows are mobile's only terminal signal for them. */
242+ const MOBILE_TERMINAL_UPDATE_STATUSES : ReadonlySet < string > = new Set ( [
243+ "idle" ,
244+ "completed" ,
245+ "failed" ,
246+ "cancelled" ,
247+ "interrupted" ,
248+ ] ) ;
249+
250+ function isTerminalBypassUpdate ( activity : OrchestrationThreadActivity ) : boolean {
251+ if ( activity . kind !== "task.updated" ) {
252+ return false ;
253+ }
254+ const payload =
255+ activity . payload && typeof activity . payload === "object"
256+ ? ( activity . payload as Record < string , unknown > )
257+ : null ;
258+ return (
259+ payload ?. timelineBypass === true &&
260+ typeof payload . status === "string" &&
261+ MOBILE_TERMINAL_UPDATE_STATUSES . has ( payload . status )
262+ ) ;
263+ }
264+
265+ /**
266+ * Quiet-timeline guarantee (mirrors web's session-logic): agent-internal
267+ * activity lives in the Agents sheet, not the work log. Terminal rows are
268+ * kept — with no Agents surface on mobile they are the terminal signal
269+ * (a surface that hides rows must keep its own terminal signal). That means
270+ * task.completed (Claude) AND terminal bypassed task.updated (Codex, whose
271+ * children never emit task.completed — review finding).
272+ */
273+ function isAgentInternalActivity ( activity : OrchestrationThreadActivity ) : boolean {
274+ const payload =
275+ activity . payload && typeof activity . payload === "object"
276+ ? ( activity . payload as Record < string , unknown > )
277+ : null ;
278+ if ( ! payload ) {
279+ return false ;
280+ }
281+ const isTerminalTaskRow = activity . kind === "task.completed" || isTerminalBypassUpdate ( activity ) ;
282+ if ( payload . timelineBypass === true && ! isTerminalTaskRow ) {
283+ return true ;
284+ }
285+ // agentId marks ownership, not "hide me": a NESTED AGENT's terminal row is
286+ // the only signal mobile gets (no Agents sheet), so it stays. Only an
287+ // agent's own background work (stamped "background") is internal — same
288+ // rule as web (review finding: hiding on agentId alone dropped nested
289+ // completions with no replacement UI).
290+ const ownedByAgent = typeof payload . agentId === "string" && payload . agentId . trim ( ) . length > 0 ;
291+ if ( ! ownedByAgent ) {
292+ return false ;
293+ }
294+ return ! ( isTerminalTaskRow && payload . agentKind === "agent" ) ;
295+ }
296+
238297function deriveWorkLogEntries (
239298 activities : ReadonlyArray < OrchestrationThreadActivity > ,
240299) : DerivedWorkLogEntry [ ] {
@@ -243,9 +302,13 @@ function deriveWorkLogEntries(
243302 for ( const activity of ordered ) {
244303 if ( activity . kind === "tool.started" ) continue ;
245304 if ( activity . kind === "task.started" ) continue ;
305+ // Terminal bypassed updates pass: Codex children's only terminal signal.
306+ if ( activity . kind === "task.updated" && ! isTerminalBypassUpdate ( activity ) ) continue ;
307+ if ( activity . kind === "tool.progress" ) continue ;
246308 if ( activity . kind === "context-window.updated" ) continue ;
247309 if ( activity . summary === "Checkpoint captured" ) continue ;
248310 if ( isPlanBoundaryToolActivity ( activity ) ) continue ;
311+ if ( isAgentInternalActivity ( activity ) ) continue ;
249312 entries . push ( toDerivedWorkLogEntry ( activity ) ) ;
250313 }
251314 return collapseDerivedWorkLogEntries ( entries ) ;
@@ -271,7 +334,13 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
271334 const commandPreview = extractToolCommand ( payload ) ;
272335 const changedFiles = extractChangedFiles ( payload ) ;
273336 const title = extractToolTitle ( payload ) ;
274- const isTaskActivity = activity . kind === "task.progress" || activity . kind === "task.completed" ;
337+ // task.updated included: terminal bypassed updates (Codex children's only
338+ // terminal signal) must carry task identity so they collapse per child
339+ // instead of stacking anonymous "Task idle" rows.
340+ const isTaskActivity =
341+ activity . kind === "task.progress" ||
342+ activity . kind === "task.completed" ||
343+ activity . kind === "task.updated" ;
275344 const taskSummary =
276345 isTaskActivity && typeof payload ?. summary === "string" && payload . summary . length > 0
277346 ? payload . summary
@@ -284,10 +353,15 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
284353 ? payload . detail
285354 : null ;
286355 const taskLabel = taskSummary || taskDetailAsLabel ;
356+ const taskId =
357+ isTaskActivity && typeof payload ?. taskId === "string" && payload . taskId . length > 0
358+ ? payload . taskId
359+ : undefined ;
287360 const entry : DerivedWorkLogEntry = {
288361 id : activity . id ,
289362 createdAt : activity . createdAt ,
290363 turnId : activity . turnId ,
364+ ...( taskId ? { taskId } : { } ) ,
291365 label : taskLabel || activity . summary ,
292366 tone :
293367 activity . kind === "task.progress"
@@ -352,7 +426,25 @@ function collapseDerivedWorkLogEntries(
352426 entries : ReadonlyArray < DerivedWorkLogEntry > ,
353427) : DerivedWorkLogEntry [ ] {
354428 const collapsed : DerivedWorkLogEntry [ ] = [ ] ;
429+ // Subagent rows collapse by identity, not adjacency (quiet-timeline
430+ // guarantee; mirrors web's session-logic).
431+ const taskRowIndex = new Map < string , number > ( ) ;
355432 for ( const entry of entries ) {
433+ const isTaskRow =
434+ entry . taskId !== undefined &&
435+ ( entry . activityKind === "task.progress" ||
436+ entry . activityKind === "task.completed" ||
437+ entry . activityKind === "task.updated" ) ;
438+ if ( isTaskRow && entry . taskId !== undefined ) {
439+ const existingIndex = taskRowIndex . get ( entry . taskId ) ;
440+ if ( existingIndex !== undefined ) {
441+ collapsed [ existingIndex ] = mergeDerivedWorkLogEntries ( collapsed [ existingIndex ] ! , entry ) ;
442+ continue ;
443+ }
444+ taskRowIndex . set ( entry . taskId , collapsed . length ) ;
445+ collapsed . push ( entry ) ;
446+ continue ;
447+ }
356448 const previous = collapsed . at ( - 1 ) ;
357449 if ( previous && shouldCollapseToolLifecycleEntries ( previous , entry ) ) {
358450 collapsed [ collapsed . length - 1 ] = mergeDerivedWorkLogEntries ( previous , entry ) ;
0 commit comments