@@ -618,13 +618,13 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
618618 this . emit ( 'tool_active' , toolName , inputStr )
619619 // Detect task/todo tool calls and emit todo_update
620620 if ( part . state ?. input && this . handleTaskTool ( toolName , part . state . input ) ) {
621- this . emit ( 'todo_update' , Array . from ( this . tasks . values ( ) ) )
621+ this . emit ( 'todo_update' , Array . from ( this . tasks . values ( ) , t => ( { ... t } ) ) )
622622 }
623623 } else if ( status === 'completed' ) {
624624 // Also check for task tools at completion (some providers only
625625 // populate input at this stage, not during 'running')
626626 if ( part . state ?. input && this . handleTaskTool ( toolName , part . state . input ) ) {
627- this . emit ( 'todo_update' , Array . from ( this . tasks . values ( ) ) )
627+ this . emit ( 'todo_update' , Array . from ( this . tasks . values ( ) , t => ( { ... t } ) ) )
628628 }
629629 const output = part . state ?. output
630630 const summary = output ? output . slice ( 0 , 200 ) : undefined
@@ -796,13 +796,16 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
796796 private handleTaskTool ( toolName : string , input : Record < string , unknown > ) : boolean {
797797 // Normalize tool name — OpenCode may report as 'todowrite', 'TodoWrite', 'todo_write', etc.
798798 const normalized = toolName . toLowerCase ( ) . replace ( / _ / g, '' )
799+ // Note: taskSeq is intentionally NOT reset on TodoWrite — keeping ids
800+ // monotonic across list generations lets the frontend detect a brand-new
801+ // list by id and avoids collisions with later TaskCreate/TaskUpdate calls.
799802 if ( normalized === 'todowrite' ) {
800803 const todos = input . todos as Array < Record < string , unknown > > | undefined
801804 if ( ! Array . isArray ( todos ) ) return false
802805 this . tasks . clear ( )
803- this . taskSeq = 0
804806 for ( const item of todos ) {
805807 const id = String ( item . id || ++ this . taskSeq )
808+ this . syncTaskSeq ( id )
806809 const status = item . status as string
807810 if ( status !== 'pending' && status !== 'in_progress' && status !== 'completed' ) continue
808811 this . tasks . set ( id , {
@@ -815,7 +818,8 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
815818 return true
816819 }
817820 if ( normalized === 'taskcreate' ) {
818- const id = String ( ++ this . taskSeq )
821+ const id = String ( input . taskId || input . id || ++ this . taskSeq )
822+ this . syncTaskSeq ( id )
819823 this . tasks . set ( id , {
820824 id,
821825 subject : String ( input . subject || '' ) ,
@@ -826,8 +830,17 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
826830 }
827831 if ( normalized === 'taskupdate' ) {
828832 const id = String ( input . taskId || '' )
829- const task = this . tasks . get ( id )
830- if ( ! task ) return false
833+ if ( ! id ) return false
834+ let task = this . tasks . get ( id )
835+ if ( ! task ) {
836+ // Unknown id — our in-memory map can diverge from the provider's real
837+ // task list (e.g. after a process restart mid-session). Upsert instead
838+ // of dropping the update, otherwise the UI shows a stale list forever.
839+ if ( input . status === 'deleted' ) return false
840+ task = { id, subject : String ( input . subject || `Task ${ id } ` ) , status : 'pending' }
841+ this . tasks . set ( id , task )
842+ this . syncTaskSeq ( id )
843+ }
831844 const status = input . status as string | undefined
832845 if ( status === 'deleted' ) {
833846 this . tasks . delete ( id )
@@ -843,6 +856,25 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
843856 return false
844857 }
845858
859+ /** Keep taskSeq ahead of any numeric id we have seen, so generated ids never collide. */
860+ private syncTaskSeq ( id : string ) : void {
861+ const n = Number ( id )
862+ if ( Number . isInteger ( n ) && n > this . taskSeq ) this . taskSeq = n
863+ }
864+
865+ /**
866+ * Seed task state from a previous process's last known list (session restore).
867+ * Without this, a restarted process starts with an empty map and TaskUpdate
868+ * calls referencing pre-restart task ids would otherwise be lost.
869+ */
870+ seedTasks ( tasks : TaskItem [ ] ) : void {
871+ this . tasks . clear ( )
872+ for ( const t of tasks ) {
873+ this . tasks . set ( t . id , { ...t } )
874+ this . syncTaskSeq ( t . id )
875+ }
876+ }
877+
846878 /** Send a user message to the OpenCode session. */
847879 sendMessage ( content : string ) : void {
848880 if ( ! this . alive || ! this . opencodeSessionId ) {
0 commit comments