@@ -1838,123 +1838,182 @@ it.layer(layerTest)("delegated tasks across a server restart", (it) => {
18381838 ) ;
18391839} ) ;
18401840
1841- it . layer ( layerTest ) ( "delegated follow-ups promoted to Steer" , ( it ) => {
1842- it . effect ( "settles a promoted follow-up with the turn it steered, not as cancelled" , ( ) =>
1843- Effect . gen ( function * ( ) {
1844- const orchestrator = yield * Orchestrator . OrchestratorV2 ;
1845- const eventSink = yield * EventSink . EventSinkV2 ;
1846- const now = yield * DateTime . now ;
1847- const threadId = ThreadId . make ( "thread:steer-parent" ) ;
1848- const projectId = ProjectId . make ( "project:steer-parent" ) ;
1849- const runId = RunId . make ( "run:steer-parent" ) ;
1850- const rootNodeId = NodeId . make ( "node:steer-parent-root" ) ;
1851- yield * seedParentWithTerminalTask ( {
1852- threadId,
1853- projectId,
1854- runId,
1855- rootNodeId,
1856- taskId : NodeId . make ( "node:steer-parent-settled" ) ,
1857- deliveryState : "delivered" ,
1858- now,
1859- } ) ;
1860- const child = yield * seedRestartCancelledChild ( {
1861- parentThreadId : threadId ,
1862- projectId,
1863- parentRunId : runId ,
1864- rootNodeId,
1865- name : "steer-child" ,
1866- completionWake : "always" ,
1867- continuationPending : false ,
1868- runStatus : "completed" ,
1869- now,
1870- } ) ;
1871- const followUpTask = ( taskId : NodeId , prompt : string ) => ( {
1872- id : EventId . make ( `event:${ taskId } ` ) ,
1873- type : "subagent.updated" as const ,
1841+ /**
1842+ * A child whose second follow-up was queued as its own task, then promoted to
1843+ * Steer into the first follow-up's running turn.
1844+ */
1845+ const seedPromotedFollowUp = ( name : string ) =>
1846+ Effect . gen ( function * ( ) {
1847+ const eventSink = yield * EventSink . EventSinkV2 ;
1848+ const now = yield * DateTime . now ;
1849+ const threadId = ThreadId . make ( `thread:${ name } -parent` ) ;
1850+ const runId = RunId . make ( `run:${ name } -parent` ) ;
1851+ const rootNodeId = NodeId . make ( `node:${ name } -parent-root` ) ;
1852+ const projectId = ProjectId . make ( `project:${ name } -parent` ) ;
1853+ yield * seedParentWithTerminalTask ( {
1854+ threadId,
1855+ projectId,
1856+ runId,
1857+ rootNodeId,
1858+ taskId : NodeId . make ( `node:${ name } -parent-settled` ) ,
1859+ deliveryState : "delivered" ,
1860+ now,
1861+ } ) ;
1862+ const child = yield * seedRestartCancelledChild ( {
1863+ parentThreadId : threadId ,
1864+ projectId,
1865+ parentRunId : runId ,
1866+ rootNodeId,
1867+ name,
1868+ completionWake : "always" ,
1869+ continuationPending : false ,
1870+ runStatus : "completed" ,
1871+ now,
1872+ } ) ;
1873+ const followUpTask = ( taskId : NodeId , prompt : string ) => ( {
1874+ id : EventId . make ( `event:${ taskId } ` ) ,
1875+ type : "subagent.updated" as const ,
1876+ threadId,
1877+ runId,
1878+ nodeId : taskId ,
1879+ driver,
1880+ providerInstanceId : modelSelection . instanceId ,
1881+ occurredAt : now ,
1882+ payload : {
1883+ id : taskId ,
18741884 threadId,
18751885 runId,
1876- nodeId : taskId ,
1886+ parentNodeId : rootNodeId ,
1887+ origin : "app_owned" as const ,
1888+ createdBy : "agent" as const ,
18771889 driver,
18781890 providerInstanceId : modelSelection . instanceId ,
1879- occurredAt : now ,
1891+ providerThreadId : null ,
1892+ childThreadId : child . childThreadId ,
1893+ nativeTaskRef : null ,
1894+ prompt,
1895+ title : null ,
1896+ model : null ,
1897+ completionWake : "always" as const ,
1898+ status : "pending" as const ,
1899+ result : null ,
1900+ startedAt : now ,
1901+ completedAt : null ,
1902+ updatedAt : now ,
1903+ } ,
1904+ } ) ;
1905+ const steeredRunId = RunId . make ( `run:${ name } :2` ) ;
1906+ const steeredTaskId = NodeId . make ( `node:${ name } -steered` ) ;
1907+ const promotedTaskId = NodeId . make ( `node:${ name } -promoted` ) ;
1908+ const promotedMessageId = MessageId . make ( `message:${ name } -promoted` ) ;
1909+ const promotedRun = runEvent ( {
1910+ threadId : child . childThreadId ,
1911+ runId : RunId . make ( `run:${ name } :3` ) ,
1912+ ordinal : 3 ,
1913+ status : "cancelled" ,
1914+ userMessageId : promotedMessageId ,
1915+ now,
1916+ } ) ;
1917+ const steeredRun = (
1918+ status : OrchestrationV2Run [ "status" ] ,
1919+ id = steeredRunId ,
1920+ ordinal = 2 ,
1921+ restartContinuationOfRunId ?: RunId ,
1922+ ) => {
1923+ const event = runEvent ( { threadId : child . childThreadId , runId : id , ordinal, status, now } ) ;
1924+ return {
1925+ ...event ,
18801926 payload : {
1881- id : taskId ,
1882- threadId,
1883- runId,
1884- parentNodeId : rootNodeId ,
1885- origin : "app_owned" as const ,
1886- createdBy : "agent" as const ,
1887- driver,
1888- providerInstanceId : modelSelection . instanceId ,
1889- providerThreadId : null ,
1890- childThreadId : child . childThreadId ,
1891- nativeTaskRef : null ,
1892- prompt,
1893- title : null ,
1894- model : null ,
1895- completionWake : "always" as const ,
1896- status : "pending" as const ,
1897- result : null ,
1898- startedAt : now ,
1899- completedAt : null ,
1900- updatedAt : now ,
1927+ ...event . payload ,
1928+ delegatedTaskId : steeredTaskId ,
1929+ ...( restartContinuationOfRunId === undefined ? { } : { restartContinuationOfRunId } ) ,
19011930 } ,
1902- } ) ;
1903- const steeredRunId = RunId . make ( "run:steer-child:2" ) ;
1904- const steeredTaskId = NodeId . make ( "node:steer-child-steered" ) ;
1905- const promotedTaskId = NodeId . make ( "node:steer-child-promoted" ) ;
1906- const promotedMessageId = MessageId . make ( "message:steer-child-promoted" ) ;
1907- const steeredRun = runEvent ( {
1908- threadId : child . childThreadId ,
1909- runId : steeredRunId ,
1910- ordinal : 2 ,
1911- status : "running" ,
1912- now,
1913- } ) ;
1914- const promotedRun = runEvent ( {
1915- threadId : child . childThreadId ,
1916- runId : RunId . make ( "run:steer-child:3" ) ,
1917- ordinal : 3 ,
1918- status : "cancelled" ,
1919- userMessageId : promotedMessageId ,
1920- now,
1921- } ) ;
1922- // Both follow-ups were queued as their own tasks. The second was promoted into the first's turn.
1923- yield * eventSink . write ( {
1924- commandId : CommandId . make ( "command:steer-child:promoted" ) ,
1925- events : [
1926- followUpTask ( steeredTaskId , "Merge the fixture branch." ) ,
1927- followUpTask ( promotedTaskId , "Also merge the pricing branch." ) ,
1928- { ...steeredRun , payload : { ...steeredRun . payload , delegatedTaskId : steeredTaskId } } ,
1929- {
1930- ...promotedRun ,
1931- payload : { ...promotedRun . payload , startedAt : null , delegatedTaskId : promotedTaskId } ,
1931+ } ;
1932+ } ;
1933+ yield * eventSink . write ( {
1934+ commandId : CommandId . make ( `command:${ name } :promoted` ) ,
1935+ events : [
1936+ followUpTask ( steeredTaskId , "Merge the fixture branch." ) ,
1937+ followUpTask ( promotedTaskId , "Also merge the pricing branch." ) ,
1938+ steeredRun ( "running" ) ,
1939+ {
1940+ ...promotedRun ,
1941+ payload : { ...promotedRun . payload , startedAt : null , delegatedTaskId : promotedTaskId } ,
1942+ } ,
1943+ {
1944+ id : EventId . make ( `event:${ name } :promoted-message` ) ,
1945+ type : "message.updated" ,
1946+ threadId : child . childThreadId ,
1947+ runId : steeredRunId ,
1948+ occurredAt : now ,
1949+ payload : {
1950+ id : promotedMessageId ,
1951+ threadId : child . childThreadId ,
1952+ runId : steeredRunId ,
1953+ nodeId : null ,
1954+ role : "user" ,
1955+ text : "Also merge the pricing branch." ,
1956+ attachments : [ ] ,
1957+ streaming : false ,
1958+ createdBy : "agent" ,
1959+ creationSource : "mcp" ,
1960+ createdAt : now ,
1961+ updatedAt : now ,
19321962 } ,
1963+ } ,
1964+ ] ,
1965+ } ) ;
1966+ /** Ends `resultRunId` with an assistant reply; `run` is that run's terminal event. */
1967+ const finish = ( resultRunId : RunId , run : ReturnType < typeof steeredRun > ) =>
1968+ eventSink . write ( {
1969+ commandId : CommandId . make ( `command:${ resultRunId } :completed` ) ,
1970+ events : [
19331971 {
1934- id : EventId . make ( " event:steer-child:promoted-message" ) ,
1972+ id : EventId . make ( ` event:${ resultRunId } :result` ) ,
19351973 type : "message.updated" ,
19361974 threadId : child . childThreadId ,
1937- runId : steeredRunId ,
1975+ runId : resultRunId ,
19381976 occurredAt : now ,
19391977 payload : {
1940- id : promotedMessageId ,
1978+ id : MessageId . make ( `message: ${ resultRunId } :result` ) ,
19411979 threadId : child . childThreadId ,
1942- runId : steeredRunId ,
1980+ runId : resultRunId ,
19431981 nodeId : null ,
1944- role : "user " ,
1945- text : "Also merge the pricing branch ." ,
1982+ role : "assistant " ,
1983+ text : "Merged both branches ." ,
19461984 attachments : [ ] ,
19471985 streaming : false ,
19481986 createdBy : "agent" ,
1949- creationSource : "mcp " ,
1987+ creationSource : "server " ,
19501988 createdAt : now ,
19511989 updatedAt : now ,
19521990 } ,
19531991 } ,
1992+ { ...run , id : EventId . make ( `event:${ resultRunId } :finished` ) } ,
19541993 ] ,
19551994 } ) ;
1995+ return {
1996+ threadId,
1997+ child,
1998+ steeredRunId,
1999+ steeredRun,
2000+ steeredTaskId,
2001+ promotedTaskId,
2002+ promotedRunId : promotedRun . payload . id ,
2003+ finish,
2004+ } ;
2005+ } ) ;
2006+
2007+ it . layer ( layerTest ) ( "delegated follow-ups promoted to Steer" , ( it ) => {
2008+ it . effect ( "settles a promoted follow-up with the turn it steered, not as cancelled" , ( ) =>
2009+ Effect . gen ( function * ( ) {
2010+ const orchestrator = yield * Orchestrator . OrchestratorV2 ;
2011+ const eventSink = yield * EventSink . EventSinkV2 ;
2012+ const now = yield * DateTime . now ;
2013+ const seeded = yield * seedPromotedFollowUp ( "steer-child" ) ;
2014+ const { threadId, child, promotedTaskId, steeredTaskId } = seeded ;
19562015
1957- yield * orchestrator . recoverDelegatedTask ( child . childThreadId , promotedRun . payload . id ) ;
2016+ yield * orchestrator . recoverDelegatedTask ( child . childThreadId , seeded . promotedRunId ) ;
19582017
19592018 const steering = yield * orchestrator . getThreadProjection ( threadId ) ;
19602019 assert . equal ( steering . subagents . find ( ( row ) => row . id === promotedTaskId ) ?. status , "pending" ) ;
@@ -1991,43 +2050,8 @@ it.layer(layerTest)("delegated follow-ups promoted to Steer", (it) => {
19912050 } ,
19922051 ] ,
19932052 } ) ;
1994- yield * eventSink . write ( {
1995- commandId : CommandId . make ( "command:steer-child:completed" ) ,
1996- events : [
1997- {
1998- id : EventId . make ( "event:steer-child:result" ) ,
1999- type : "message.updated" ,
2000- threadId : child . childThreadId ,
2001- runId : steeredRunId ,
2002- occurredAt : now ,
2003- payload : {
2004- id : MessageId . make ( "message:steer-child:result" ) ,
2005- threadId : child . childThreadId ,
2006- runId : steeredRunId ,
2007- nodeId : null ,
2008- role : "assistant" ,
2009- text : "Merged both branches." ,
2010- attachments : [ ] ,
2011- streaming : false ,
2012- createdBy : "agent" ,
2013- creationSource : "server" ,
2014- createdAt : now ,
2015- updatedAt : now ,
2016- } ,
2017- } ,
2018- {
2019- ...steeredRun ,
2020- id : EventId . make ( "event:steer-child:steered-completed" ) ,
2021- payload : {
2022- ...steeredRun . payload ,
2023- delegatedTaskId : steeredTaskId ,
2024- status : "completed" as const ,
2025- completedAt : now ,
2026- } ,
2027- } ,
2028- ] ,
2029- } ) ;
2030- yield * orchestrator . recoverDelegatedTask ( child . childThreadId , steeredRunId ) ;
2053+ yield * seeded . finish ( seeded . steeredRunId , seeded . steeredRun ( "completed" ) ) ;
2054+ yield * orchestrator . recoverDelegatedTask ( child . childThreadId , seeded . steeredRunId ) ;
20312055
20322056 const settled = yield * orchestrator . getThreadProjection ( threadId ) ;
20332057 const task = ( id : NodeId ) => settled . subagents . find ( ( row ) => row . id === id ) ;
@@ -2039,4 +2063,45 @@ it.layer(layerTest)("delegated follow-ups promoted to Steer", (it) => {
20392063 ) ;
20402064 } ) ,
20412065 ) ;
2066+
2067+ it . effect ( "keeps a promoted follow-up open while a restart continues its turn" , ( ) =>
2068+ Effect . gen ( function * ( ) {
2069+ const orchestrator = yield * Orchestrator . OrchestratorV2 ;
2070+ const eventSink = yield * EventSink . EventSinkV2 ;
2071+ const seeded = yield * seedPromotedFollowUp ( "steer-restart-child" ) ;
2072+ const { threadId, child, promotedTaskId, steeredRunId } = seeded ;
2073+ const continuationRunId = RunId . make ( "run:steer-restart-child:4" ) ;
2074+ // A restart cut the steered turn, and its continuation resumed it.
2075+ yield * eventSink . write ( {
2076+ commandId : reconcileCommandId ( "steer-restart-child-cut" ) ,
2077+ events : [ seeded . steeredRun ( "cancelled" ) ] ,
2078+ } ) ;
2079+ yield * eventSink . write ( {
2080+ commandId : CommandId . make ( "command:steer-restart-child:continued" ) ,
2081+ events : [ seeded . steeredRun ( "running" , continuationRunId , 4 , steeredRunId ) ] ,
2082+ } ) ;
2083+
2084+ yield * orchestrator . recoverDelegatedTask ( child . childThreadId , steeredRunId ) ;
2085+
2086+ const continuing = yield * orchestrator . getThreadProjection ( threadId ) ;
2087+ assert . equal (
2088+ continuing . subagents . find ( ( row ) => row . id === promotedTaskId ) ?. status ,
2089+ "pending" ,
2090+ ) ;
2091+ assert . isTrue (
2092+ yield * orchestrator . delegatedTaskResultPending ( child . childThreadId , promotedTaskId ) ,
2093+ ) ;
2094+
2095+ yield * seeded . finish (
2096+ continuationRunId ,
2097+ seeded . steeredRun ( "completed" , continuationRunId , 4 , steeredRunId ) ,
2098+ ) ;
2099+ yield * orchestrator . recoverDelegatedTask ( child . childThreadId , continuationRunId ) ;
2100+
2101+ const settled = yield * orchestrator . getThreadProjection ( threadId ) ;
2102+ const promoted = settled . subagents . find ( ( row ) => row . id === promotedTaskId ) ;
2103+ assert . equal ( promoted ?. status , "completed" ) ;
2104+ assert . equal ( promoted ?. result , "Merged both branches." ) ;
2105+ } ) ,
2106+ ) ;
20422107} ) ;
0 commit comments