@@ -969,6 +969,7 @@ test("nested spawn live action tracks tool execution events", () => {
969969 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
970970 const { session, emit } = createSubscribableSession ( [ ] ) ;
971971 state . childSessions . set ( "tool-call-1" , session ) ;
972+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
972973
973974 // Mock console.warn to suppress any expected-but-harmless warnings
974975 // (e.g., streaming component errors in headless test env).
@@ -1012,6 +1013,7 @@ test("nested spawn handleEvent recovers from malformed events", () => {
10121013 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
10131014 const { session, emit } = createSubscribableSession ( [ ] ) ;
10141015 state . childSessions . set ( "tool-call-1" , session ) ;
1016+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
10151017
10161018 const warnings : any [ ] = [ ] ;
10171019 const originalWarn = console . warn ;
@@ -1044,6 +1046,7 @@ test("nested spawn message_end with aborted stopReason clears pending tools", ()
10441046 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
10451047 const { session, emit } = createSubscribableSession ( [ ] ) ;
10461048 state . childSessions . set ( "tool-call-1" , session ) ;
1049+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
10471050
10481051 const component = childSpawnTool . renderResult (
10491052 { content : [ { type : "text" , text : "ignored" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
@@ -1066,6 +1069,7 @@ test("nested spawn dispose stops event processing", () => {
10661069 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
10671070 const { session, emit } = createSubscribableSession ( [ ] ) ;
10681071 state . childSessions . set ( "tool-call-1" , session ) ;
1072+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
10691073
10701074 const component = childSpawnTool . renderResult (
10711075 { content : [ { type : "text" , text : "ignored" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
@@ -1670,6 +1674,7 @@ test("nested spawn rapid events collapse to last state", () => {
16701674 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
16711675 const { session, emit } = createSubscribableSession ( [ ] ) ;
16721676 state . childSessions . set ( "tool-call-1" , session ) ;
1677+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
16731678
16741679 const component = childSpawnTool . renderResult (
16751680 { content : [ ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
@@ -1697,30 +1702,107 @@ test("nested spawn rapid events collapse to last state", () => {
16971702 assert . ok ( finalLines . some ( ( l : string ) => l . includes ( "✓" ) ) ) ;
16981703} ) ;
16991704
1700- test ( "nested spawn isDetachedFromLiveState drops events when session is replaced " , ( ) => {
1705+ test ( "nested spawn drops late events after live registry deletion " , ( ) => {
17011706 const state = createState ( ) ;
17021707 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
17031708 const { session, emit } = createSubscribableSession ( [ ] ) ;
17041709 state . childSessions . set ( "tool-call-1" , session ) ;
17051710 state . liveChildSessions . set ( "tool-call-1" , session ) ;
1711+ let invalidateCalls = 0 ;
17061712
17071713 const component = childSpawnTool . renderResult (
17081714 { content : [ { type : "text" , text : "initial" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
17091715 { expanded : false } ,
17101716 theme ,
1711- createRenderContext ( ) ,
1717+ createRenderContext ( { invalidate : ( ) => { invalidateCalls ++ ; } } ) ,
1718+ ) as any ;
1719+ const before = component . render ( 120 ) ;
1720+
1721+ state . liveChildSessions . delete ( "tool-call-1" ) ;
1722+ emit ( { type : "message_start" , message : { role : "assistant" , content : [ ] } } ) ;
1723+
1724+ const after = component . render ( 120 ) ;
1725+ assert . equal ( invalidateCalls , 0 , "completed-session deletion should stop rerenders from late events" ) ;
1726+ assert . deepEqual ( after , before , "completed-session deletion should freeze the rendered state" ) ;
1727+ } ) ;
1728+
1729+ test ( "nested spawn drops events after resetState bumps child epoch" , ( ) => {
1730+ const state = createState ( ) ;
1731+ const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
1732+ const { session, emit } = createSubscribableSession ( [ ] ) ;
1733+ state . childSessions . set ( "tool-call-1" , session ) ;
1734+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
1735+ let invalidateCalls = 0 ;
1736+
1737+ const component = childSpawnTool . renderResult (
1738+ { content : [ { type : "text" , text : "initial" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
1739+ { expanded : false } ,
1740+ theme ,
1741+ createRenderContext ( { invalidate : ( ) => { invalidateCalls ++ ; } } ) ,
1742+ ) as any ;
1743+ const before = component . render ( 120 ) ;
1744+
1745+ resetState ( state ) ;
1746+ emit ( { type : "message_start" , message : { role : "assistant" , content : [ ] } } ) ;
1747+
1748+ const after = component . render ( 120 ) ;
1749+ assert . equal ( invalidateCalls , 0 , "stale events should not request rerender after reset" ) ;
1750+ assert . deepEqual ( after , before , "stale events should not change rendered state after reset" ) ;
1751+ } ) ;
1752+
1753+ test ( "nested spawn drops events when session is replaced in live state" , ( ) => {
1754+ const state = createState ( ) ;
1755+ const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
1756+ const { session, emit } = createSubscribableSession ( [ ] ) ;
1757+ state . childSessions . set ( "tool-call-1" , session ) ;
1758+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
1759+ let invalidateCalls = 0 ;
1760+
1761+ const component = childSpawnTool . renderResult (
1762+ { content : [ { type : "text" , text : "initial" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
1763+ { expanded : false } ,
1764+ theme ,
1765+ createRenderContext ( { invalidate : ( ) => { invalidateCalls ++ ; } } ) ,
17121766 ) as any ;
1767+ const before = component . render ( 120 ) ;
17131768
1714- // Replace the session in liveChildSessions with a different object
17151769 const replacementSession = createSubscribableSession ( [ ] ) . session ;
17161770 state . liveChildSessions . set ( "tool-call-1" , replacementSession ) ;
1771+ emit ( { type : "message_start" , message : { role : "assistant" , content : [ ] } } ) ;
1772+
1773+ const after = component . render ( 120 ) ;
1774+ assert . equal ( invalidateCalls , 0 , "replaced sessions should not request rerender" ) ;
1775+ assert . deepEqual ( after , before , "replaced sessions should not change rendered state" ) ;
1776+ } ) ;
17171777
1718- // Emit a message_start event — should be silently dropped
1778+ test ( "nested spawn completed-session deletion stays stale even if the toolCallId is later reused" , ( ) => {
1779+ const state = createState ( ) ;
1780+ const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
1781+ const { session, emit } = createSubscribableSession ( [ ] ) ;
1782+ state . childSessions . set ( "tool-call-1" , session ) ;
1783+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
1784+ let invalidateCalls = 0 ;
1785+
1786+ const component = childSpawnTool . renderResult (
1787+ { content : [ { type : "text" , text : "initial" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
1788+ { expanded : false } ,
1789+ theme ,
1790+ createRenderContext ( { invalidate : ( ) => { invalidateCalls ++ ; } } ) ,
1791+ ) as any ;
1792+ const before = component . render ( 120 ) ;
1793+
1794+ state . liveChildSessions . delete ( "tool-call-1" ) ;
17191795 emit ( { type : "message_start" , message : { role : "assistant" , content : [ ] } } ) ;
1796+ const afterDeletion = component . render ( 120 ) ;
1797+ assert . equal ( invalidateCalls , 0 , "completed-session deletion should immediately stale the old session" ) ;
1798+ assert . deepEqual ( afterDeletion , before , "completed-session deletion should freeze the rendered state before reuse" ) ;
17201799
1721- const lines = component . render ( 120 ) ;
1722- // Should NOT contain "thinking" because the event was dropped
1723- assert . ok ( lines . every ( ( l : string ) => ! l . includes ( "thinking" ) ) , `expected no thinking after detach, got: ${ lines . join ( "\n" ) } ` ) ;
1800+ state . liveChildSessions . set ( "tool-call-1" , createSubscribableSession ( [ ] ) . session ) ;
1801+ emit ( { type : "message_update" , message : { role : "assistant" , content : [ { type : "text" , text : "should be dropped" } ] } } ) ;
1802+ const afterReuse = component . render ( 120 ) ;
1803+ assert . equal ( invalidateCalls , 0 , "toolCallId reuse should not revive a completed stale session" ) ;
1804+ assert . deepEqual ( afterReuse , before , "toolCallId reuse should keep the old rendered state frozen" ) ;
1805+ assert . ok ( afterReuse . every ( ( l : string ) => ! l . includes ( "should be dropped" ) ) , "toolCallId reuse should not admit stale text updates" ) ;
17241806} ) ;
17251807
17261808test ( "concurrent spawn executions produce independent results" , async ( ) => {
@@ -1792,6 +1874,7 @@ test("nested spawn render cache preserves stable output for identical params", (
17921874 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
17931875 const { session } = createSubscribableSession ( [ ] ) ;
17941876 state . childSessions . set ( "tool-call-1" , session ) ;
1877+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
17951878
17961879 const component = childSpawnTool . renderResult (
17971880 { content : [ { type : "text" , text : "hello" } ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
@@ -1944,6 +2027,7 @@ test("handleEvent gracefully degrades with null message events", () => {
19442027 const childSpawnTool = createChildTools ( new MockPi ( ) as any , state , "medium" , 0 ) . find ( t => t . name === "spawn" ) ! ;
19452028 const { session, emit } = createSubscribableSession ( [ ] ) ;
19462029 state . childSessions . set ( "tool-call-1" , session ) ;
2030+ state . liveChildSessions . set ( "tool-call-1" , session ) ;
19472031
19482032 const component = childSpawnTool . renderResult (
19492033 { content : [ ] , details : { depth : 1 , model : "m" , thinking : "low" , truncated : false } } ,
0 commit comments