@@ -313,6 +313,75 @@ describe("History resume delegation - parent metadata transitions", () => {
313313 )
314314 } )
315315
316+ it ( "preserves an unrelated child pending action when completion has no action owner" , async ( ) => {
317+ const parentHistoryItem = {
318+ id : "parent-unowned-action" ,
319+ status : "delegated" ,
320+ awaitingChildId : "child-unowned-action" ,
321+ childIds : [ "child-unowned-action" ] ,
322+ ts : 1 ,
323+ task : "Parent" ,
324+ tokensIn : 0 ,
325+ tokensOut : 0 ,
326+ totalCost : 0 ,
327+ }
328+ const pendingAction = {
329+ kind : "finish_subtask" as const ,
330+ actionId : "other-action" ,
331+ approvalText : "{}" ,
332+ parentTaskId : "parent-unowned-action" ,
333+ result : "Other result" ,
334+ }
335+ const childHistoryItem = {
336+ id : "child-unowned-action" ,
337+ status : "active" ,
338+ pendingAction,
339+ }
340+ let updatedChild : HistoryItem | undefined
341+ const taskHistoryStore = makeTaskHistoryStoreStub ( childHistoryItem , parentHistoryItem , {
342+ atomicUpdatePair : vi . fn (
343+ async (
344+ _firstId : string ,
345+ _secondId : string ,
346+ firstUpdater : ( item : HistoryItem ) => HistoryItem ,
347+ secondUpdater : ( item : HistoryItem ) => HistoryItem ,
348+ options ?: { whileFirstFileLocked ?: ( ) => Promise < void > } ,
349+ ) => {
350+ firstUpdater ( parentHistoryItem as HistoryItem )
351+ updatedChild = secondUpdater ( childHistoryItem as HistoryItem )
352+ await options ?. whileFirstFileLocked ?.( )
353+ return [ ]
354+ } ,
355+ ) ,
356+ } )
357+ const provider = makeProviderStub ( {
358+ contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
359+ getTaskWithId : vi . fn ( ) . mockResolvedValue ( { historyItem : parentHistoryItem } ) ,
360+ emit : vi . fn ( ) ,
361+ getCurrentTask : vi . fn ( ( ) => ( { taskId : "different-task" } ) ) ,
362+ removeClineFromStack : vi . fn ( ) ,
363+ createTaskWithHistoryItem : vi . fn ( ) . mockResolvedValue ( {
364+ resumeAfterDelegation : vi . fn ( ) . mockResolvedValue ( undefined ) ,
365+ overwriteClineMessages : vi . fn ( ) . mockResolvedValue ( undefined ) ,
366+ overwriteApiConversationHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
367+ } ) ,
368+ taskHistoryStore,
369+ } )
370+
371+ vi . mocked ( readTaskMessages ) . mockResolvedValue ( [ ] )
372+ vi . mocked ( readApiMessages ) . mockResolvedValue ( [ ] )
373+ vi . mocked ( saveTaskMessages ) . mockResolvedValue ( undefined )
374+ vi . mocked ( saveApiMessages ) . mockResolvedValue ( undefined )
375+
376+ await ClineProvider . prototype . reopenParentFromDelegation . call ( provider , {
377+ parentTaskId : "parent-unowned-action" ,
378+ childTaskId : "child-unowned-action" ,
379+ completionResultSummary : "Done" ,
380+ } )
381+
382+ expect ( updatedChild ?. pendingAction ) . toEqual ( pendingAction )
383+ } )
384+
316385 it ( "reopenParentFromDelegation injects subtask_result into both UI and API histories" , async ( ) => {
317386 const parentItem = {
318387 id : "p1" ,
@@ -517,11 +586,17 @@ describe("History resume delegation - parent metadata transitions", () => {
517586 const existingApiMessages = [
518587 {
519588 role : "assistant" as const ,
520- content : [ { type : "tool_use" as const , name : "new_task" , id : "tool-existing" , input : { } } ] ,
589+ content : [
590+ { type : "tool_use" as const , name : "read_file" , id : "tool-unrelated" , input : { } } ,
591+ { type : "tool_use" as const , name : "new_task" , id : "tool-existing" , input : { } } ,
592+ ] ,
521593 } ,
522594 {
523595 role : "user" as const ,
524- content : [ { type : "tool_result" as const , tool_use_id : "tool-existing" , content : "old result" } ] ,
596+ content : [
597+ { type : "tool_result" as const , tool_use_id : "tool-unrelated" , content : "read result" } ,
598+ { type : "tool_result" as const , tool_use_id : "tool-existing" , content : "old result" } ,
599+ ] ,
525600 } ,
526601 ]
527602
@@ -540,13 +615,13 @@ describe("History resume delegation - parent metadata transitions", () => {
540615 expect ( persistedApiMessages ) . toHaveLength ( 2 )
541616 expect ( persistedApiMessages [ 1 ] ) . toMatchObject ( {
542617 role : "user" ,
543- content : [
618+ content : expect . arrayContaining ( [
544619 {
545620 type : "tool_result" ,
546621 tool_use_id : "tool-existing" ,
547622 content : "Subtask c-existing-result completed.\n\nResult:\nreplacement result" ,
548623 } ,
549- ] ,
624+ ] ) ,
550625 } )
551626 } )
552627
@@ -600,6 +675,61 @@ describe("History resume delegation - parent metadata transitions", () => {
600675 expect ( ( injected . content [ 0 ] as any ) . text ) . toContain ( "Subtask c-no-tool completed" )
601676 } )
602677
678+ it ( "keeps already-injected UI and fallback API completion records idempotent" , async ( ) => {
679+ const parentItem = {
680+ id : "p-existing-fallback" ,
681+ status : "delegated" ,
682+ awaitingChildId : "c-existing-fallback" ,
683+ childIds : [ "c-existing-fallback" ] ,
684+ ts : 100 ,
685+ task : "Parent with existing fallback" ,
686+ tokensIn : 0 ,
687+ tokensOut : 0 ,
688+ totalCost : 0 ,
689+ }
690+ const completionResultSummary = "Already recorded"
691+ const fallbackText = `Subtask c-existing-fallback completed.\n\nResult:\n${ completionResultSummary } `
692+ const existingUiMessages = [
693+ {
694+ type : "say" as const ,
695+ say : "subtask_result" as const ,
696+ text : completionResultSummary ,
697+ ts : 50 ,
698+ } ,
699+ ]
700+ const existingApiMessages = [
701+ { role : "user" as const , content : [ { type : "text" as const , text : fallbackText } ] , ts : 50 } ,
702+ ]
703+ const taskHistoryStore = makeTaskHistoryStoreStub ( { id : "c-existing-fallback" , status : "active" } , parentItem )
704+ const provider = makeProviderStub ( {
705+ contextProxy : { globalStorageUri : { fsPath : "/storage" } } ,
706+ getTaskWithId : vi . fn ( ) . mockResolvedValue ( { historyItem : parentItem } ) ,
707+ emit : vi . fn ( ) ,
708+ getCurrentTask : vi . fn ( ( ) => ( { taskId : "c-existing-fallback" } ) ) ,
709+ removeClineFromStack : vi . fn ( ) . mockResolvedValue ( undefined ) ,
710+ createTaskWithHistoryItem : vi . fn ( ) . mockResolvedValue ( {
711+ resumeAfterDelegation : vi . fn ( ) . mockResolvedValue ( undefined ) ,
712+ overwriteClineMessages : vi . fn ( ) . mockResolvedValue ( undefined ) ,
713+ overwriteApiConversationHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
714+ } ) ,
715+ taskHistoryStore,
716+ } )
717+
718+ vi . mocked ( readTaskMessages ) . mockResolvedValue ( existingUiMessages )
719+ vi . mocked ( readApiMessages ) . mockResolvedValue ( existingApiMessages )
720+ vi . mocked ( saveTaskMessages ) . mockResolvedValue ( undefined )
721+ vi . mocked ( saveApiMessages ) . mockResolvedValue ( undefined )
722+
723+ await ClineProvider . prototype . reopenParentFromDelegation . call ( provider , {
724+ parentTaskId : "p-existing-fallback" ,
725+ childTaskId : "c-existing-fallback" ,
726+ completionResultSummary,
727+ } )
728+
729+ expect ( vi . mocked ( saveTaskMessages ) . mock . calls [ 0 ] [ 0 ] . messages ) . toEqual ( existingUiMessages )
730+ expect ( vi . mocked ( saveApiMessages ) . mock . calls [ 0 ] [ 0 ] . messages ) . toEqual ( existingApiMessages )
731+ } )
732+
603733 it ( "reopenParentFromDelegation sets skipPrevResponseIdOnce via resumeAfterDelegation" , async ( ) => {
604734 const parentInstance : any = {
605735 skipPrevResponseIdOnce : false ,
0 commit comments