@@ -1756,13 +1756,16 @@ describe("interrupt_agent unblocks wait_agents", () => {
17561756
17571757 test ( "close overlay without in-flight wait stays interrupted after followup complete" , async ( ) => {
17581758 const gate = deferred < RunSubAgentResult > ( ) ;
1759- const followupGate = deferred < string > ( ) ;
17601759 const closeHold = deferred < undefined > ( ) ;
1760+ const followupCalls : string [ ] = [ ] ;
17611761 const deps = makeDeps ( async ( params ) => {
17621762 params . onAgentReady ?.( {
17631763 close : async ( ) => closeHold . promise ,
17641764 interrupt : ( ) => undefined ,
1765- followup : async ( ) => followupGate . promise ,
1765+ followup : async ( message : string ) => {
1766+ followupCalls . push ( message ) ;
1767+ return "should never run" ;
1768+ } ,
17661769 deliver : ( ) => undefined ,
17671770 } ) ;
17681771 return gate . promise ;
@@ -1806,25 +1809,16 @@ describe("interrupt_agent unblocks wait_agents", () => {
18061809 new AbortController ( ) . signal ,
18071810 ) ;
18081811
1809- followupGate . resolve ( "followup after close overlay" ) ;
1810- await new Promise < void > ( ( resolve ) => {
1811- const done = ( ) : boolean =>
1812- deps . sessions . get ( id ) ?. lifecycle . state === "completed" ;
1813- if ( done ( ) ) {
1814- resolve ( ) ;
1815- return ;
1816- }
1817- const unsub = deps . sessions . subscribe ( ( ) => {
1818- if ( done ( ) ) {
1819- unsub ( ) ;
1820- resolve ( ) ;
1821- }
1822- } ) ;
1823- if ( done ( ) ) {
1824- unsub ( ) ;
1825- resolve ( ) ;
1826- }
1827- } ) ;
1812+ closeHold . resolve ( undefined ) ;
1813+ await closing ;
1814+ // CL-7344: close drops the stashed follow-up — it never runs against
1815+ // the closed agent, and the close overlay survives the run settling.
1816+ expect ( followupCalls ) . toEqual ( [ ] ) ;
1817+ gate . resolve ( {
1818+ report : "original interrupted" ,
1819+ interrupted : true ,
1820+ } as RunSubAgentResult ) ;
1821+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
18281822
18291823 expect ( deps . fleetRecords . peek ( id ) ?. status ) . toBe ( "interrupted" ) ;
18301824 const listed = await callTool ( list , { } ) ;
@@ -1837,13 +1831,6 @@ describe("interrupt_agent unblocks wait_agents", () => {
18371831 expect ( waited . timed_out ) . toBe ( false ) ;
18381832 const results = waited . results as { status : string } [ ] ;
18391833 expect ( defined ( results [ 0 ] ) . status ) . toBe ( "interrupted" ) ;
1840-
1841- closeHold . resolve ( undefined ) ;
1842- await closing ;
1843- gate . resolve ( {
1844- report : "original interrupted" ,
1845- interrupted : true ,
1846- } as RunSubAgentResult ) ;
18471834 } ) ;
18481835
18491836 test ( "completeAfterInterrupt does not clear a close overlay" , ( ) => {
@@ -1896,14 +1883,17 @@ describe("interrupt_agent unblocks wait_agents", () => {
18961883 message : "stop that" ,
18971884 interrupt : true ,
18981885 } ) ;
1899- followupGate . reject ( new Error ( "followup failed" ) ) ;
1900- await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1886+ // CL-7344: the follow-up is stashed until the original run settles; the
1887+ // salvage handoff launches it, and its rejection restores the
1888+ // interrupted lane so wait collects the salvage.
19011889 gate . resolve ( {
19021890 report :
19031891 "## Summary\nStopped.\n## Findings\nsalvage\n## Blockers\ninterrupted\n## Paths\n" ,
19041892 interrupted : true ,
19051893 } as RunSubAgentResult ) ;
19061894 await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1895+ followupGate . reject ( new Error ( "followup failed" ) ) ;
1896+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
19071897
19081898 const waited = await callTool ( wait , { targets : [ id ] , timeout_ms : 5000 } ) ;
19091899 expect ( waited . timed_out ) . toBe ( false ) ;
@@ -1912,21 +1902,14 @@ describe("interrupt_agent unblocks wait_agents", () => {
19121902 expect ( defined ( results [ 0 ] ) . report ) . toContain ( "salvage" ) ;
19131903 } ) ;
19141904
1915- test ( "send_input interrupt queued overlay clears when the followup is admitted" , async ( ) => {
1916- const admission = createAdmissionQueue ( { capacity : 1 } ) ;
1917- const sessions = createSubAgentSessionStore ( { admission } ) ;
1905+ test ( "send_input interrupt on a live lane stashes the followup until the run settles" , async ( ) => {
1906+ const sessions = createSubAgentSessionStore ( ) ;
19181907 const fleetRecords = createFleetMailbox ( sessions ) ;
1919- admission . enqueue ( {
1920- id : "holder" ,
1921- provider : "p" ,
1922- start : ( ) => undefined ,
1923- } ) ;
19241908 const worker = sessions . start ( {
19251909 description : "looping" ,
19261910 agentId : "explorer" ,
19271911 brief : "b" ,
19281912 retained : true ,
1929- provider : "p" ,
19301913 } ) ;
19311914 sessions . markRunning ( worker . id ) ;
19321915 fleetRecords . register ( worker . id ) ;
@@ -1944,51 +1927,44 @@ describe("interrupt_agent unblocks wait_agents", () => {
19441927 interrupt : true ,
19451928 } ) ;
19461929 expect ( sent . status ) . toBe ( "interrupted" ) ;
1947- expect ( sessions . get ( worker . id ) ?. lifecycleStatus ) . toBe ( "pending_init" ) ;
1930+ // CL-7344: the interrupt stashes the follow-up instead of starting it;
1931+ // the lane is still the live run, so the session stays running while
1932+ // wait/list stay live until the run settles and hands off.
1933+ expect ( sessions . get ( worker . id ) ?. lifecycleStatus ) . toBe ( "running" ) ;
19481934
1949- const queuedWait = await callTool ( wait , {
1935+ const liveWait = await callTool ( wait , {
19501936 targets : [ worker . id ] ,
19511937 timeout_ms : 50 ,
19521938 } ) ;
1953- expect ( queuedWait . timed_out ) . toBe ( true ) ;
1954- expect (
1955- defined ( ( queuedWait . results as { status : string } [ ] ) [ 0 ] ) . status ,
1956- ) . toBe ( "queued" ) ;
1957- const queuedList = await callTool ( list , { } ) ;
1958- const queuedEntry = (
1959- queuedList . agents as {
1939+ expect ( liveWait . timed_out ) . toBe ( true ) ;
1940+ expect ( defined ( ( liveWait . results as { status : string } [ ] ) [ 0 ] ) . status ) . toBe (
1941+ "running" ,
1942+ ) ;
1943+ const liveList = await callTool ( list , { } ) ;
1944+ const liveEntry = (
1945+ liveList . agents as {
19601946 agent_id : string ;
19611947 status : string ;
19621948 lifecycle : string ;
19631949 } [ ]
19641950 ) . find ( ( a ) => a . agent_id === worker . id ) ;
1965- expect ( queuedEntry ?. status ) . toBe ( "queued" ) ;
1966- expect ( queuedEntry ?. lifecycle ) . toBe ( "pending_init" ) ;
1967-
1968- admission . release ( "holder" ) ;
1969- await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1951+ expect ( liveEntry ?. status ) . toBe ( "running" ) ;
1952+ expect ( liveEntry ?. lifecycle ) . toBe ( "running" ) ;
19701953
1954+ // Settling the original run launches the stashed follow-up.
1955+ sessions . attachReport ( worker . id , "interrupted salvage" , {
1956+ stopReason : "interrupted" ,
1957+ } ) ;
19711958 expect ( sessions . get ( worker . id ) ?. lifecycleStatus ) . toBe ( "running" ) ;
1972- const runningWait = await callTool ( wait , {
1959+ followupGate . resolve ( "later" ) ;
1960+ const done = await callTool ( wait , {
19731961 targets : [ worker . id ] ,
1974- timeout_ms : 50 ,
1962+ timeout_ms : 5000 ,
19751963 } ) ;
1976- expect ( runningWait . timed_out ) . toBe ( true ) ;
1977- expect (
1978- defined ( ( runningWait . results as { status : string } [ ] ) [ 0 ] ) . status ,
1979- ) . toBe ( "running" ) ;
1980- const runningList = await callTool ( list , { } ) ;
1981- const runningEntry = (
1982- runningList . agents as {
1983- agent_id : string ;
1984- status : string ;
1985- lifecycle : string ;
1986- } [ ]
1987- ) . find ( ( a ) => a . agent_id === worker . id ) ;
1988- expect ( runningEntry ?. status ) . toBe ( "running" ) ;
1989- expect ( runningEntry ?. lifecycle ) . toBe ( "running" ) ;
1990-
1991- followupGate . resolve ( "later" ) ;
1964+ expect ( done . timed_out ) . toBe ( false ) ;
1965+ const results = done . results as { status : string ; report ?: string } [ ] ;
1966+ expect ( defined ( results [ 0 ] ) . status ) . toBe ( "done" ) ;
1967+ expect ( defined ( results [ 0 ] ) . report ) . toBe ( "later" ) ;
19921968 } ) ;
19931969
19941970 test ( "soft-interrupt wait path collects so omitted re-wait does not re-deliver" , async ( ) => {
@@ -2164,8 +2140,13 @@ describe("interrupt_agent unblocks wait_agents", () => {
21642140 message : "stop that" ,
21652141 interrupt : true ,
21662142 } ) ;
2143+ // CL-7344: the follow-up is stashed until the original run settles; the
2144+ // salvage handoff launches it, so the run must settle first.
2145+ gate . resolve ( {
2146+ report : "original interrupted" ,
2147+ interrupted : true ,
2148+ } as RunSubAgentResult ) ;
21672149 followupGate . resolve ( "followup report" ) ;
2168- await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
21692150 const waited = await callTool ( wait , { targets : [ id ] , timeout_ms : 5000 } ) ;
21702151 expect ( waited . timed_out ) . toBe ( false ) ;
21712152 const results = waited . results as { status : string ; report ?: string } [ ] ;
0 commit comments