@@ -10,6 +10,7 @@ import type {
1010 AppServerClientHandlers ,
1111 AppServerRpc ,
1212} from "./app-server-client" ;
13+ import { AppServerRequestError } from "./app-server-client" ;
1314import { CodexAppServerAgent } from "./codex-app-server-agent" ;
1415import { sandboxPolicyFor } from "./session-config" ;
1516
@@ -48,6 +49,9 @@ function makeStubRpc(responses: Record<string, unknown>) {
4849 } ;
4950 }
5051 const response = responses [ method ] ;
52+ if ( response instanceof Error ) {
53+ throw response ;
54+ }
5155 return (
5256 typeof response === "function"
5357 ? await response ( params )
@@ -1960,7 +1964,12 @@ describe("CodexAppServerAgent", () => {
19601964 prompt : [ { type : "text" , text : "more context" } ] ,
19611965 } as unknown as PromptRequest ) ;
19621966
1963- // The single turn/completed resolves both the original and the folded prompt.
1967+ await expect ( second ) . resolves . toMatchObject ( {
1968+ stopReason : "end_turn" ,
1969+ _meta : { steer : true } ,
1970+ } ) ;
1971+
1972+ // The original prompt remains the sole owner of turn completion.
19641973 stub . emit ( "thread/tokenUsage/updated" , {
19651974 tokenUsage : {
19661975 last : {
@@ -1972,12 +1981,11 @@ describe("CodexAppServerAgent", () => {
19721981 } ,
19731982 } ) ;
19741983 stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
1975- const [ firstResult , secondResult ] = await Promise . all ( [ first , second ] ) ;
1984+ const firstResult = await first ;
19761985 expect ( firstResult ) . toMatchObject ( {
19771986 stopReason : "end_turn" ,
19781987 usage : { totalTokens : 45 } ,
19791988 } ) ;
1980- expect ( secondResult ) . toEqual ( { stopReason : "end_turn" } ) ;
19811989
19821990 const steer = stub . requests . find ( ( r ) => r . method === "turn/steer" ) ;
19831991 expect ( steer ?. params ) . toMatchObject ( {
@@ -1991,6 +1999,118 @@ describe("CodexAppServerAgent", () => {
19911999 ) ;
19922000 } ) ;
19932001
2002+ it ( "rejects a failed turn/steer without echoing or acknowledging it" , async ( ) => {
2003+ const stub = makeStubRpc ( {
2004+ "thread/start" : { thread : { id : "t" } } ,
2005+ "turn/start" : { turn : { id : "turn_1" } } ,
2006+ "turn/steer" : new Error ( "steer transport failed" ) ,
2007+ } ) ;
2008+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2009+ const agent = new CodexAppServerAgent ( client , {
2010+ processOptions : { binaryPath : "/x/codex" } ,
2011+ rpcFactory : stub . factory ,
2012+ } ) ;
2013+
2014+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2015+ const first = agent . prompt ( {
2016+ sessionId : "t" ,
2017+ prompt : [ { type : "text" , text : "one" } ] ,
2018+ } as unknown as PromptRequest ) ;
2019+ stub . emit ( "turn/started" , { threadId : "t" , turn : { id : "turn_1" } } ) ;
2020+
2021+ await expect (
2022+ agent . prompt ( {
2023+ sessionId : "t" ,
2024+ prompt : [ { type : "text" , text : "lost steer" } ] ,
2025+ _meta : { steer : true } ,
2026+ } as unknown as PromptRequest ) ,
2027+ ) . rejects . toThrow ( "steer transport failed" ) ;
2028+ expect ( sessionUpdates ) . not . toContainEqual (
2029+ expect . objectContaining ( {
2030+ update : expect . objectContaining ( {
2031+ sessionUpdate : "user_message_chunk" ,
2032+ content : { type : "text" , text : "lost steer" } ,
2033+ } ) ,
2034+ } ) ,
2035+ ) ;
2036+
2037+ stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
2038+ await first ;
2039+ } ) ;
2040+
2041+ it ( "declines a stale turn/steer so the caller can queue it normally" , async ( ) => {
2042+ const stub = makeStubRpc ( {
2043+ "thread/start" : { thread : { id : "t" } } ,
2044+ "turn/start" : { turn : { id : "turn_1" } } ,
2045+ "turn/steer" : new AppServerRequestError (
2046+ - 32600 ,
2047+ "expected active turn id `turn_1` but found `turn_2`" ,
2048+ ) ,
2049+ } ) ;
2050+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2051+ const agent = new CodexAppServerAgent ( client , {
2052+ processOptions : { binaryPath : "/x/codex" } ,
2053+ rpcFactory : stub . factory ,
2054+ } ) ;
2055+
2056+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2057+ const first = agent . prompt ( {
2058+ sessionId : "t" ,
2059+ prompt : [ { type : "text" , text : "one" } ] ,
2060+ } as unknown as PromptRequest ) ;
2061+ stub . emit ( "turn/started" , { threadId : "t" , turn : { id : "turn_1" } } ) ;
2062+
2063+ await expect (
2064+ agent . prompt ( {
2065+ sessionId : "t" ,
2066+ prompt : [ { type : "text" , text : "queue me" } ] ,
2067+ _meta : { steer : true } ,
2068+ } as unknown as PromptRequest ) ,
2069+ ) . resolves . toMatchObject ( { _meta : { steer : false } } ) ;
2070+ expect ( sessionUpdates ) . not . toContainEqual (
2071+ expect . objectContaining ( {
2072+ update : expect . objectContaining ( {
2073+ sessionUpdate : "user_message_chunk" ,
2074+ content : { type : "text" , text : "queue me" } ,
2075+ } ) ,
2076+ } ) ,
2077+ ) ;
2078+
2079+ stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
2080+ await first ;
2081+ } ) ;
2082+
2083+ it ( "declines an explicit steer after the active turn has ended" , async ( ) => {
2084+ const stub = makeStubRpc ( {
2085+ "thread/start" : { thread : { id : "t" } } ,
2086+ } ) ;
2087+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2088+ const agent = new CodexAppServerAgent ( client , {
2089+ processOptions : { binaryPath : "/x/codex" } ,
2090+ rpcFactory : stub . factory ,
2091+ } ) ;
2092+
2093+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2094+ await expect (
2095+ agent . prompt ( {
2096+ sessionId : "t" ,
2097+ prompt : [ { type : "text" , text : "too late" } ] ,
2098+ _meta : { steer : true } ,
2099+ } as unknown as PromptRequest ) ,
2100+ ) . resolves . toMatchObject ( { _meta : { steer : false } } ) ;
2101+ expect (
2102+ stub . requests . filter ( ( request ) => request . method === "turn/start" ) ,
2103+ ) . toHaveLength ( 0 ) ;
2104+ expect ( sessionUpdates ) . not . toContainEqual (
2105+ expect . objectContaining ( {
2106+ update : expect . objectContaining ( {
2107+ sessionUpdate : "user_message_chunk" ,
2108+ content : { type : "text" , text : "too late" } ,
2109+ } ) ,
2110+ } ) ,
2111+ ) ;
2112+ } ) ;
2113+
19942114 it ( "refreshes the live turnId from each turn/steer response" , async ( ) => {
19952115 const stub = makeStubRpc ( {
19962116 "thread/start" : { thread : { id : "t" } } ,
0 commit comments