@@ -1470,6 +1470,96 @@ describe("interrupt_agent unblocks wait_agents", () => {
14701470 expect ( fleetRecords . peek ( worker . id ) ?. status ) . toBe ( "interrupted" ) ;
14711471 } ) ;
14721472
1473+ test ( "completeAfterInterrupt attaches the followup reply to the interrupted lane" , async ( ) => {
1474+ const sessions = createSubAgentSessionStore ( ) ;
1475+ const fleetRecords = createFleetMailbox ( sessions ) ;
1476+ const worker = sessions . start ( {
1477+ description : "looping" ,
1478+ agentId : "explorer" ,
1479+ brief : "b" ,
1480+ retained : true ,
1481+ } ) ;
1482+ sessions . markRunning ( worker . id ) ;
1483+ fleetRecords . register ( worker . id ) ;
1484+ fleetRecords . noteFollowup ( worker . id ) ;
1485+ fleetRecords . interrupt ( worker . id ) ;
1486+ fleetRecords . completeAfterInterrupt ( worker . id , "followup reply" ) ;
1487+ // The overlay stays put, but the reply the send_input followup handoff
1488+ // passes in must surface to the parent through peek/wait.
1489+ expect ( fleetRecords . peek ( worker . id ) ?. status ) . toBe ( "interrupted" ) ;
1490+ expect ( fleetRecords . peek ( worker . id ) ?. report ) . toBe ( "followup reply" ) ;
1491+ expect ( fleetRecords . peek ( worker . id ) ?. collected ) . toBeUndefined ( ) ;
1492+
1493+ // wait_agents delivers the reply exactly once; the bodyHanded guard
1494+ // omits it on redelivery so a repeat wait cannot hand the same reply
1495+ // out twice.
1496+ const wait = createWaitAgentsTool ( { sessions, fleetRecords } ) ;
1497+ const first = await callFleetTool ( wait , {
1498+ targets : [ worker . id ] ,
1499+ timeout_ms : 1000 ,
1500+ } ) ;
1501+ expect ( first . timed_out ) . toBe ( false ) ;
1502+ const firstResults = first . results as {
1503+ agent_id : string ;
1504+ status : string ;
1505+ report ?: string ;
1506+ } [ ] ;
1507+ expect ( defined ( firstResults [ 0 ] ) ) . toEqual ( {
1508+ agent_id : worker . id ,
1509+ status : "interrupted" ,
1510+ report : "followup reply" ,
1511+ } ) ;
1512+ expect ( fleetRecords . peek ( worker . id ) ?. bodyHanded ) . toBe ( true ) ;
1513+
1514+ const second = await callFleetTool ( wait , {
1515+ targets : [ worker . id ] ,
1516+ timeout_ms : 1000 ,
1517+ } ) ;
1518+ expect ( second . timed_out ) . toBe ( false ) ;
1519+ const secondResults = second . results as {
1520+ agent_id : string ;
1521+ status : string ;
1522+ report ?: string ;
1523+ } [ ] ;
1524+ expect ( defined ( secondResults [ 0 ] ) ) . toEqual ( {
1525+ agent_id : worker . id ,
1526+ status : "interrupted" ,
1527+ } ) ;
1528+ } ) ;
1529+
1530+ test ( "tombstone releases a stored followup reply from the lane" , ( ) => {
1531+ const sessions = createSubAgentSessionStore ( ) ;
1532+ const fleetRecords = createFleetMailbox ( sessions ) ;
1533+ const worker = sessions . start ( {
1534+ description : "looping" ,
1535+ agentId : "explorer" ,
1536+ brief : "b" ,
1537+ retained : true ,
1538+ } ) ;
1539+ sessions . markRunning ( worker . id ) ;
1540+ fleetRecords . register ( worker . id ) ;
1541+ fleetRecords . noteFollowup ( worker . id ) ;
1542+ fleetRecords . interrupt ( worker . id ) ;
1543+ fleetRecords . completeAfterInterrupt ( worker . id , "followup reply" ) ;
1544+ // Dropping the session makes the lane tombstone; tombstone is permanent
1545+ // and the record is kept, so the reply must be released from the
1546+ // overlay instead of lingering behind its hidden projection.
1547+ sessions . clear ( ) ;
1548+ expect ( sessions . get ( worker . id ) ) . toBeUndefined ( ) ;
1549+ const snap = fleetRecords . peek ( worker . id ) ;
1550+ expect ( snap ?. status ) . toBe ( "interrupted" ) ;
1551+ expect ( snap ?. tombstoned ) . toBe ( true ) ;
1552+ expect ( snap ?. report ) . toBeUndefined ( ) ;
1553+ // Heap-truth check: the projection gate alone would hide a retained
1554+ // reply; the overlay itself must carry nothing.
1555+ const overlay = (
1556+ fleetRecords as unknown as {
1557+ records : Map < string , { report ?: string } > ;
1558+ }
1559+ ) . records . get ( worker . id ) ;
1560+ expect ( overlay ?. report ) . toBeUndefined ( ) ;
1561+ } ) ;
1562+
14731563 test ( "rejected send_input followup clears the lane so wait collects interrupted salvage" , async ( ) => {
14741564 const gate = deferred < RunSubAgentResult > ( ) ;
14751565 const followupGate = deferred < string > ( ) ;
0 commit comments