@@ -1102,6 +1102,181 @@ describe("interrupt_agent unblocks wait_agents", () => {
11021102 expect ( doneResults [ 0 ] ! . report ) . toBe ( "followup report" ) ;
11031103 } ) ;
11041104
1105+ test ( "close_agent overlay survives a send_input followup completing in the close window" , async ( ) => {
1106+ const gate = deferred < RunSubAgentResult > ( ) ;
1107+ const followupGate = deferred < string > ( ) ;
1108+ const closeHold = deferred < void > ( ) ;
1109+ const deps = makeDeps ( async ( params ) => {
1110+ params . onAgentReady ?.( {
1111+ close : async ( ) => closeHold . promise ,
1112+ interrupt : ( ) => { } ,
1113+ followup : async ( ) => followupGate . promise ,
1114+ deliver : ( ) => { } ,
1115+ } ) ;
1116+ return gate . promise ;
1117+ } ) ;
1118+ const spawn = createSpawnAgentTool ( deps ) ;
1119+ const wait = createWaitAgentsTool ( {
1120+ sessions : deps . sessions ,
1121+ fleetRecords : deps . fleetRecords ,
1122+ } ) ;
1123+ const sendInput = createSendInputTool ( {
1124+ sessions : deps . sessions ,
1125+ fleetRecords : deps . fleetRecords ,
1126+ } ) ;
1127+ const close = createCloseAgentTool ( {
1128+ sessions : deps . sessions ,
1129+ fleetRecords : deps . fleetRecords ,
1130+ } ) ;
1131+ const spawned = await callTool ( spawn , {
1132+ description : "looping" ,
1133+ prompt : "do it" ,
1134+ intent : "explore" ,
1135+ } ) ;
1136+ const id = spawned . agent_id as string ;
1137+
1138+ const waiting = callTool ( wait , { targets : [ id ] , timeout_ms : 5000 } ) ;
1139+ await callTool ( sendInput , { target : id , message : "stop that" , interrupt : true } ) ;
1140+ if ( close . kind !== "full" ) throw new Error ( "expected full tool" ) ;
1141+ const closing = close . handler (
1142+ { id : "close-during-followup" , name : "close_agent" , arguments : { target : id } } ,
1143+ new AbortController ( ) . signal ,
1144+ ) ;
1145+ followupGate . resolve ( "followup during close" ) ;
1146+ gate . resolve ( { report : "original interrupted" , interrupted : true } as RunSubAgentResult ) ;
1147+
1148+ const waited = await waiting ;
1149+ expect ( waited . timed_out ) . toBe ( false ) ;
1150+ const results = waited . results as { status : string } [ ] ;
1151+ expect ( results [ 0 ] ! . status ) . toBe ( "interrupted" ) ;
1152+ expect ( deps . fleetRecords . peek ( id ) ?. status ) . toBe ( "interrupted" ) ;
1153+
1154+ closeHold . resolve ( ) ;
1155+ await closing ;
1156+ } ) ;
1157+
1158+ test ( "completeAfterInterrupt does not clear a close overlay" , ( ) => {
1159+ const sessions = createSubAgentSessionStore ( ) ;
1160+ const fleetRecords = createFleetMailbox ( sessions ) ;
1161+ const worker = sessions . start ( {
1162+ description : "looping" ,
1163+ agentId : "explorer" ,
1164+ brief : "b" ,
1165+ retained : true ,
1166+ } ) ;
1167+ sessions . markRunning ( worker . id ) ;
1168+ fleetRecords . register ( worker . id ) ;
1169+ fleetRecords . noteFollowup ( worker . id ) ;
1170+ fleetRecords . interrupt ( worker . id ) ;
1171+ expect ( fleetRecords . peek ( worker . id ) ?. status ) . toBe ( "interrupted" ) ;
1172+ fleetRecords . completeAfterInterrupt ( worker . id , "followup reply" ) ;
1173+ expect ( fleetRecords . peek ( worker . id ) ?. status ) . toBe ( "interrupted" ) ;
1174+ } ) ;
1175+
1176+ test ( "rejected send_input followup clears the lane so wait collects interrupted salvage" , async ( ) => {
1177+ const gate = deferred < RunSubAgentResult > ( ) ;
1178+ const followupGate = deferred < string > ( ) ;
1179+ const deps = makeDeps ( async ( params ) => {
1180+ params . onAgentReady ?.( {
1181+ close : async ( ) => { } ,
1182+ interrupt : ( ) => { } ,
1183+ followup : async ( ) => followupGate . promise ,
1184+ deliver : ( ) => { } ,
1185+ } ) ;
1186+ return gate . promise ;
1187+ } ) ;
1188+ const spawn = createSpawnAgentTool ( deps ) ;
1189+ const wait = createWaitAgentsTool ( {
1190+ sessions : deps . sessions ,
1191+ fleetRecords : deps . fleetRecords ,
1192+ } ) ;
1193+ const sendInput = createSendInputTool ( {
1194+ sessions : deps . sessions ,
1195+ fleetRecords : deps . fleetRecords ,
1196+ } ) ;
1197+ const spawned = await callTool ( spawn , {
1198+ description : "looping" ,
1199+ prompt : "do it" ,
1200+ intent : "explore" ,
1201+ } ) ;
1202+ const id = spawned . agent_id as string ;
1203+ await callTool ( sendInput , { target : id , message : "stop that" , interrupt : true } ) ;
1204+ followupGate . reject ( new Error ( "followup failed" ) ) ;
1205+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1206+ gate . resolve ( {
1207+ report : "## Summary\nStopped.\n## Findings\nsalvage\n## Blockers\ninterrupted\n## Paths\n" ,
1208+ interrupted : true ,
1209+ } as RunSubAgentResult ) ;
1210+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1211+
1212+ const waited = await callTool ( wait , { targets : [ id ] , timeout_ms : 5000 } ) ;
1213+ expect ( waited . timed_out ) . toBe ( false ) ;
1214+ const results = waited . results as { status : string ; report ?: string } [ ] ;
1215+ expect ( results [ 0 ] ! . status ) . toBe ( "interrupted" ) ;
1216+ expect ( results [ 0 ] ! . report ) . toContain ( "salvage" ) ;
1217+ } ) ;
1218+
1219+ test ( "send_input interrupt queued overlay clears when the followup is admitted" , async ( ) => {
1220+ const admission = createAdmissionQueue ( { capacity : 1 } ) ;
1221+ const sessions = createSubAgentSessionStore ( { admission } ) ;
1222+ const fleetRecords = createFleetMailbox ( sessions ) ;
1223+ admission . enqueue ( {
1224+ id : "holder" ,
1225+ provider : "p" ,
1226+ start : ( ) => { } ,
1227+ } ) ;
1228+ const worker = sessions . start ( {
1229+ description : "looping" ,
1230+ agentId : "explorer" ,
1231+ brief : "b" ,
1232+ retained : true ,
1233+ provider : "p" ,
1234+ } ) ;
1235+ sessions . markRunning ( worker . id ) ;
1236+ fleetRecords . register ( worker . id ) ;
1237+ const followupGate = deferred < string > ( ) ;
1238+ sessions . registerInterrupt ( worker . id , ( ) => { } ) ;
1239+ sessions . registerFollowup ( worker . id , async ( ) => followupGate . promise ) ;
1240+
1241+ const sendInput = createSendInputTool ( { sessions, fleetRecords } ) ;
1242+ const wait = createWaitAgentsTool ( { sessions, fleetRecords } ) ;
1243+ const list = createListAgentsTool ( { sessions, fleetRecords } ) ;
1244+
1245+ const sent = await callTool ( sendInput , {
1246+ target : worker . id ,
1247+ message : "stop that" ,
1248+ interrupt : true ,
1249+ } ) ;
1250+ expect ( sent . status ) . toBe ( "interrupted" ) ;
1251+ expect ( sessions . get ( worker . id ) ?. lifecycleStatus ) . toBe ( "pending_init" ) ;
1252+
1253+ const queuedWait = await callTool ( wait , { targets : [ worker . id ] , timeout_ms : 50 } ) ;
1254+ expect ( queuedWait . timed_out ) . toBe ( true ) ;
1255+ expect ( ( queuedWait . results as { status : string } [ ] ) [ 0 ] ! . status ) . toBe ( "queued" ) ;
1256+ const queuedList = await callTool ( list , { } ) ;
1257+ const queuedEntry = (
1258+ queuedList . agents as { agent_id : string ; status : string ; lifecycle : string } [ ]
1259+ ) . find ( ( a ) => a . agent_id === worker . id ) ;
1260+ expect ( queuedEntry ?. status ) . toBe ( "queued" ) ;
1261+ expect ( queuedEntry ?. lifecycle ) . toBe ( "pending_init" ) ;
1262+
1263+ admission . release ( "holder" ) ;
1264+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
1265+
1266+ expect ( sessions . get ( worker . id ) ?. lifecycleStatus ) . toBe ( "running" ) ;
1267+ const runningWait = await callTool ( wait , { targets : [ worker . id ] , timeout_ms : 50 } ) ;
1268+ expect ( runningWait . timed_out ) . toBe ( true ) ;
1269+ expect ( ( runningWait . results as { status : string } [ ] ) [ 0 ] ! . status ) . toBe ( "running" ) ;
1270+ const runningList = await callTool ( list , { } ) ;
1271+ const runningEntry = (
1272+ runningList . agents as { agent_id : string ; status : string ; lifecycle : string } [ ]
1273+ ) . find ( ( a ) => a . agent_id === worker . id ) ;
1274+ expect ( runningEntry ?. status ) . toBe ( "running" ) ;
1275+ expect ( runningEntry ?. lifecycle ) . toBe ( "running" ) ;
1276+
1277+ followupGate . resolve ( "later" ) ;
1278+ } ) ;
1279+
11051280 test ( "soft-interrupt wait path collects so omitted re-wait does not re-deliver" , async ( ) => {
11061281 const gate = deferred < RunSubAgentResult > ( ) ;
11071282 const deps = makeDeps ( async ( params ) => {
0 commit comments