@@ -1774,6 +1774,348 @@ describe("runTurn: real approval park + respondPermission resume", () => {
17741774 await env . destroy ( ) ;
17751775 } ) ;
17761776
1777+ it ( "replays the concurrent-approval incident with deferred closure and exactly-once real results" , async ( ) => {
1778+ const { calls, deps, captured } = pausableHarness ( ) ;
1779+ deps . createOtel = createSandboxAgentOtel as any ;
1780+ const incidentRequest : AgentRunRequest = {
1781+ ...engineReq ,
1782+ permissions : { default : "ask" } ,
1783+ customTools : [
1784+ { name : "tool-a" , permission : "ask" } ,
1785+ { name : "tool-b" , permission : "ask" } ,
1786+ ] ,
1787+ messages : [ { role : "user" , content : "run both writes in parallel" } ] ,
1788+ } ;
1789+ const acquired = await acquireEnvironment ( incidentRequest , deps ) ;
1790+ assert . equal ( acquired . ok , true ) ;
1791+ if ( ! acquired . ok ) return ;
1792+ const env = acquired . env ;
1793+
1794+ const firstTurn = runTurn (
1795+ env ,
1796+ incidentRequest ,
1797+ undefined ,
1798+ undefined ,
1799+ { approvalParkMode : true } ,
1800+ ) ;
1801+ await flush ( ) ;
1802+ captured . onEvent ! (
1803+ updateEvent ( {
1804+ sessionUpdate : "tool_call" ,
1805+ toolCallId : "tool-a" ,
1806+ title : "tool-a" ,
1807+ rawInput : { target : "a" } ,
1808+ } ) ,
1809+ ) ;
1810+ captured . onEvent ! (
1811+ updateEvent ( {
1812+ sessionUpdate : "tool_call" ,
1813+ toolCallId : "tool-b" ,
1814+ title : "tool-b" ,
1815+ rawInput : { target : "b" } ,
1816+ } ) ,
1817+ ) ;
1818+ captured . onPermissionRequest ! ( {
1819+ id : "permission-a" ,
1820+ availableReplies : [ "once" , "reject" ] ,
1821+ toolCall : {
1822+ toolCallId : "tool-a" ,
1823+ name : "tool-a" ,
1824+ rawInput : { target : "a" } ,
1825+ } ,
1826+ } ) ;
1827+ for ( let i = 0 ; i < 5 && ! env . currentTurn ?. pause . active ; i += 1 ) {
1828+ await Promise . resolve ( ) ;
1829+ }
1830+ assert . equal ( env . currentTurn ?. pause . active , true ) ;
1831+ captured . onEvent ! (
1832+ updateEvent ( {
1833+ sessionUpdate : "tool_call_update" ,
1834+ toolCallId : "tool-b" ,
1835+ status : "completed" ,
1836+ content : "tool-b cancellation closure" ,
1837+ } ) ,
1838+ ) ;
1839+ const firstResult = await firstTurn ;
1840+
1841+ assert . equal ( firstResult . ok , true ) ;
1842+ assert . equal ( firstResult . stopReason , "paused" ) ;
1843+ assert . equal ( env . parkedApprovals . size , 1 ) ;
1844+ assert . deepEqual ( [ ...env . parkedApprovals . keys ( ) ] , [ "tool-a" ] ) ;
1845+ const gateA = env . parkedApprovals . get ( "tool-a" ) ! ;
1846+ const firstEvents = firstResult . events ?? [ ] ;
1847+ assert . equal (
1848+ firstEvents . filter (
1849+ ( event ) =>
1850+ event . type === "interaction_request" &&
1851+ ( event . payload as { toolCallId ?: string } ) . toolCallId === "tool-a" ,
1852+ ) . length ,
1853+ 1 ,
1854+ ) ;
1855+ assert . deepEqual (
1856+ firstEvents . filter (
1857+ ( event ) => event . type === "tool_result" && event . id === "tool-b" ,
1858+ ) ,
1859+ [
1860+ {
1861+ type : "tool_result" ,
1862+ id : "tool-b" ,
1863+ output : TOOL_NOT_EXECUTED_PAUSED ,
1864+ isError : true ,
1865+ } ,
1866+ ] ,
1867+ ) ;
1868+ assert . equal (
1869+ firstEvents . some (
1870+ ( event ) => event . type === "tool_result" && event . isError === false ,
1871+ ) ,
1872+ false ,
1873+ "the never-started first-turn states have no success record" ,
1874+ ) ;
1875+
1876+ env . clearTurn ( ) ;
1877+ const secondTurn = runTurn (
1878+ env ,
1879+ incidentRequest ,
1880+ undefined ,
1881+ undefined ,
1882+ {
1883+ approvalParkMode : true ,
1884+ resume : {
1885+ decisions : [
1886+ {
1887+ permissionId : gateA . permissionId ,
1888+ reply : "once" ,
1889+ toolCallId : gateA . toolCallId ,
1890+ toolName : gateA . toolName ,
1891+ args : gateA . args ,
1892+ interactionToken : gateA . interactionToken ,
1893+ promptPromise : gateA . promptPromise ,
1894+ } ,
1895+ ] ,
1896+ carriedForward : [ ] ,
1897+ } ,
1898+ } ,
1899+ ) ;
1900+ for (
1901+ let i = 0 ;
1902+ i < 5 &&
1903+ ! calls . permissionReplies . some ( ( reply ) => reply . id === gateA . permissionId ) ;
1904+ i += 1
1905+ ) {
1906+ await Promise . resolve ( ) ;
1907+ }
1908+ assert . equal (
1909+ calls . permissionReplies . filter ( ( reply ) => reply . id === gateA . permissionId )
1910+ . length ,
1911+ 1 ,
1912+ ) ;
1913+ captured . onEvent ! (
1914+ updateEvent ( {
1915+ sessionUpdate : "tool_call_update" ,
1916+ toolCallId : "tool-a" ,
1917+ status : "in_progress" ,
1918+ } ) ,
1919+ ) ;
1920+ captured . onPermissionRequest ! ( {
1921+ id : "permission-b" ,
1922+ availableReplies : [ "once" , "reject" ] ,
1923+ toolCall : {
1924+ toolCallId : "tool-b" ,
1925+ name : "tool-b" ,
1926+ rawInput : { target : "b" } ,
1927+ } ,
1928+ } ) ;
1929+ for ( let i = 0 ; i < 5 && ! env . currentTurn ?. pause . active ; i += 1 ) {
1930+ await Promise . resolve ( ) ;
1931+ }
1932+ assert . equal ( env . currentTurn ?. pause . active , true ) ;
1933+ captured . onEvent ! (
1934+ updateEvent ( {
1935+ sessionUpdate : "tool_call_update" ,
1936+ toolCallId : "tool-a" ,
1937+ status : "completed" ,
1938+ content : "tool-a real output" ,
1939+ } ) ,
1940+ ) ;
1941+ const secondResult = await secondTurn ;
1942+
1943+ assert . equal ( secondResult . ok , true ) ;
1944+ assert . equal ( secondResult . stopReason , "paused" ) ;
1945+ assert . equal ( env . parkedApprovals . size , 1 ) ;
1946+ assert . deepEqual ( [ ...env . parkedApprovals . keys ( ) ] , [ "tool-b" ] ) ;
1947+ const gateB = env . parkedApprovals . get ( "tool-b" ) ! ;
1948+ const secondEvents = secondResult . events ?? [ ] ;
1949+ assert . deepEqual (
1950+ secondEvents . filter (
1951+ ( event ) => event . type === "tool_result" && event . id === "tool-a" ,
1952+ ) ,
1953+ [
1954+ {
1955+ type : "tool_result" ,
1956+ id : "tool-a" ,
1957+ output : "tool-a real output" ,
1958+ isError : false ,
1959+ } ,
1960+ ] ,
1961+ ) ;
1962+ assert . equal (
1963+ secondEvents . filter (
1964+ ( event ) =>
1965+ event . type === "interaction_request" &&
1966+ ( event . payload as { toolCallId ?: string } ) . toolCallId === "tool-b" ,
1967+ ) . length ,
1968+ 1 ,
1969+ ) ;
1970+ assert . deepEqual (
1971+ secondEvents . filter (
1972+ ( event ) =>
1973+ event . type === "interaction_response" &&
1974+ ( event . payload as { toolCallId ?: string } ) . toolCallId === "tool-a" ,
1975+ ) ,
1976+ [
1977+ {
1978+ type : "interaction_response" ,
1979+ id : gateA . interactionToken ,
1980+ kind : "user_approval" ,
1981+ payload : { toolCallId : "tool-a" , approved : true } ,
1982+ } ,
1983+ ] ,
1984+ ) ;
1985+
1986+ env . clearTurn ( ) ;
1987+ const thirdTurn = runTurn (
1988+ env ,
1989+ incidentRequest ,
1990+ undefined ,
1991+ undefined ,
1992+ {
1993+ approvalParkMode : true ,
1994+ resume : {
1995+ decisions : [
1996+ {
1997+ permissionId : gateB . permissionId ,
1998+ reply : "once" ,
1999+ toolCallId : gateB . toolCallId ,
2000+ toolName : gateB . toolName ,
2001+ args : gateB . args ,
2002+ interactionToken : gateB . interactionToken ,
2003+ promptPromise : gateB . promptPromise ,
2004+ } ,
2005+ ] ,
2006+ carriedForward : [ ] ,
2007+ } ,
2008+ } ,
2009+ ) ;
2010+ for (
2011+ let i = 0 ;
2012+ i < 5 &&
2013+ ! calls . permissionReplies . some ( ( reply ) => reply . id === gateB . permissionId ) ;
2014+ i += 1
2015+ ) {
2016+ await Promise . resolve ( ) ;
2017+ }
2018+ assert . equal (
2019+ calls . permissionReplies . filter ( ( reply ) => reply . id === gateB . permissionId )
2020+ . length ,
2021+ 1 ,
2022+ ) ;
2023+ captured . onEvent ! (
2024+ updateEvent ( {
2025+ sessionUpdate : "tool_call_update" ,
2026+ toolCallId : "tool-b" ,
2027+ status : "completed" ,
2028+ content : "tool-b real output" ,
2029+ } ) ,
2030+ ) ;
2031+ calls . resolvePrompt ! ( {
2032+ stopReason : "complete" ,
2033+ usage : { inputTokens : 1 , outputTokens : 1 } ,
2034+ } ) ;
2035+ const thirdResult = await thirdTurn ;
2036+
2037+ assert . equal ( thirdResult . ok , true ) ;
2038+ assert . equal ( thirdResult . stopReason , "complete" ) ;
2039+ const thirdEvents = thirdResult . events ?? [ ] ;
2040+ assert . deepEqual (
2041+ thirdEvents . filter (
2042+ ( event ) => event . type === "tool_result" && event . id === "tool-b" ,
2043+ ) ,
2044+ [
2045+ {
2046+ type : "tool_result" ,
2047+ id : "tool-b" ,
2048+ output : "tool-b real output" ,
2049+ isError : false ,
2050+ } ,
2051+ ] ,
2052+ ) ;
2053+ assert . deepEqual (
2054+ thirdEvents . filter (
2055+ ( event ) =>
2056+ event . type === "interaction_response" &&
2057+ ( event . payload as { toolCallId ?: string } ) . toolCallId === "tool-b" ,
2058+ ) ,
2059+ [
2060+ {
2061+ type : "interaction_response" ,
2062+ id : gateB . interactionToken ,
2063+ kind : "user_approval" ,
2064+ payload : { toolCallId : "tool-b" , approved : true } ,
2065+ } ,
2066+ ] ,
2067+ ) ;
2068+
2069+ const allEvents = [ ...firstEvents , ...secondEvents , ...thirdEvents ] ;
2070+ const realResults = allEvents . filter (
2071+ (
2072+ event ,
2073+ ) : event is Extract < AgentEvent , { type : "tool_result" } > =>
2074+ event . type === "tool_result" && event . isError === false ,
2075+ ) ;
2076+ assert . deepEqual (
2077+ realResults . map ( ( event ) => event . id ) . sort ( ) ,
2078+ [ "tool-a" , "tool-b" ] ,
2079+ "each call records exactly one real result" ,
2080+ ) ;
2081+ assert . equal (
2082+ allEvents . some (
2083+ ( event ) =>
2084+ event . type === "tool_result" &&
2085+ event . id === "tool-a" &&
2086+ ( event . output === TOOL_NOT_EXECUTED_PAUSED ||
2087+ event . output === APPROVED_EXECUTION_RESULT_UNKNOWN ) ,
2088+ ) ,
2089+ false ,
2090+ "the approved tool-a result is never replaced by a sentinel" ,
2091+ ) ;
2092+ assert . equal (
2093+ allEvents . some (
2094+ ( event ) =>
2095+ event . type === "tool_result" &&
2096+ event . output === "tool-b cancellation closure" ,
2097+ ) ,
2098+ false ,
2099+ "the cancellation closure is never recorded as success" ,
2100+ ) ;
2101+ const permissionReplyCounts = new Map < string , number > ( ) ;
2102+ for ( const reply of calls . permissionReplies ) {
2103+ permissionReplyCounts . set (
2104+ reply . id ,
2105+ ( permissionReplyCounts . get ( reply . id ) ?? 0 ) + 1 ,
2106+ ) ;
2107+ }
2108+ assert . deepEqual (
2109+ permissionReplyCounts ,
2110+ new Map ( [
2111+ [ gateA . permissionId , 1 ] ,
2112+ [ gateB . permissionId , 1 ] ,
2113+ ] ) ,
2114+ ) ;
2115+
2116+ await env . destroy ( ) ;
2117+ } ) ;
2118+
17772119 it ( "records the non-retry sentinel when an approved result misses the bound" , async ( ) => {
17782120 const { calls, deps, captured } = pausableHarness ( ) ;
17792121 deps . resolveRunLimits = ( ) => ( {
0 commit comments