@@ -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 )
@@ -2133,7 +2137,12 @@ describe("CodexAppServerAgent", () => {
21332137 prompt : [ { type : "text" , text : "more context" } ] ,
21342138 } as unknown as PromptRequest ) ;
21352139
2136- // The single turn/completed resolves both the original and the folded prompt.
2140+ await expect ( second ) . resolves . toMatchObject ( {
2141+ stopReason : "end_turn" ,
2142+ _meta : { steer : true } ,
2143+ } ) ;
2144+
2145+ // The original prompt remains the sole owner of turn completion.
21372146 stub . emit ( "thread/tokenUsage/updated" , {
21382147 tokenUsage : {
21392148 last : {
@@ -2145,12 +2154,11 @@ describe("CodexAppServerAgent", () => {
21452154 } ,
21462155 } ) ;
21472156 stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
2148- const [ firstResult , secondResult ] = await Promise . all ( [ first , second ] ) ;
2157+ const firstResult = await first ;
21492158 expect ( firstResult ) . toMatchObject ( {
21502159 stopReason : "end_turn" ,
21512160 usage : { totalTokens : 45 } ,
21522161 } ) ;
2153- expect ( secondResult ) . toEqual ( { stopReason : "end_turn" } ) ;
21542162
21552163 const steer = stub . requests . find ( ( r ) => r . method === "turn/steer" ) ;
21562164 expect ( steer ?. params ) . toMatchObject ( {
@@ -2164,6 +2172,118 @@ describe("CodexAppServerAgent", () => {
21642172 ) ;
21652173 } ) ;
21662174
2175+ it ( "rejects a failed turn/steer without echoing or acknowledging it" , async ( ) => {
2176+ const stub = makeStubRpc ( {
2177+ "thread/start" : { thread : { id : "t" } } ,
2178+ "turn/start" : { turn : { id : "turn_1" } } ,
2179+ "turn/steer" : new Error ( "steer transport failed" ) ,
2180+ } ) ;
2181+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2182+ const agent = new CodexAppServerAgent ( client , {
2183+ processOptions : { binaryPath : "/x/codex" } ,
2184+ rpcFactory : stub . factory ,
2185+ } ) ;
2186+
2187+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2188+ const first = agent . prompt ( {
2189+ sessionId : "t" ,
2190+ prompt : [ { type : "text" , text : "one" } ] ,
2191+ } as unknown as PromptRequest ) ;
2192+ stub . emit ( "turn/started" , { threadId : "t" , turn : { id : "turn_1" } } ) ;
2193+
2194+ await expect (
2195+ agent . prompt ( {
2196+ sessionId : "t" ,
2197+ prompt : [ { type : "text" , text : "lost steer" } ] ,
2198+ _meta : { steer : true } ,
2199+ } as unknown as PromptRequest ) ,
2200+ ) . rejects . toThrow ( "steer transport failed" ) ;
2201+ expect ( sessionUpdates ) . not . toContainEqual (
2202+ expect . objectContaining ( {
2203+ update : expect . objectContaining ( {
2204+ sessionUpdate : "user_message_chunk" ,
2205+ content : { type : "text" , text : "lost steer" } ,
2206+ } ) ,
2207+ } ) ,
2208+ ) ;
2209+
2210+ stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
2211+ await first ;
2212+ } ) ;
2213+
2214+ it ( "declines a stale turn/steer so the caller can queue it normally" , async ( ) => {
2215+ const stub = makeStubRpc ( {
2216+ "thread/start" : { thread : { id : "t" } } ,
2217+ "turn/start" : { turn : { id : "turn_1" } } ,
2218+ "turn/steer" : new AppServerRequestError (
2219+ - 32600 ,
2220+ "expected active turn id `turn_1` but found `turn_2`" ,
2221+ ) ,
2222+ } ) ;
2223+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2224+ const agent = new CodexAppServerAgent ( client , {
2225+ processOptions : { binaryPath : "/x/codex" } ,
2226+ rpcFactory : stub . factory ,
2227+ } ) ;
2228+
2229+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2230+ const first = agent . prompt ( {
2231+ sessionId : "t" ,
2232+ prompt : [ { type : "text" , text : "one" } ] ,
2233+ } as unknown as PromptRequest ) ;
2234+ stub . emit ( "turn/started" , { threadId : "t" , turn : { id : "turn_1" } } ) ;
2235+
2236+ await expect (
2237+ agent . prompt ( {
2238+ sessionId : "t" ,
2239+ prompt : [ { type : "text" , text : "queue me" } ] ,
2240+ _meta : { steer : true } ,
2241+ } as unknown as PromptRequest ) ,
2242+ ) . resolves . toMatchObject ( { _meta : { steer : false } } ) ;
2243+ expect ( sessionUpdates ) . not . toContainEqual (
2244+ expect . objectContaining ( {
2245+ update : expect . objectContaining ( {
2246+ sessionUpdate : "user_message_chunk" ,
2247+ content : { type : "text" , text : "queue me" } ,
2248+ } ) ,
2249+ } ) ,
2250+ ) ;
2251+
2252+ stub . emit ( "turn/completed" , { turn : { status : "completed" } } ) ;
2253+ await first ;
2254+ } ) ;
2255+
2256+ it ( "declines an explicit steer after the active turn has ended" , async ( ) => {
2257+ const stub = makeStubRpc ( {
2258+ "thread/start" : { thread : { id : "t" } } ,
2259+ } ) ;
2260+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2261+ const agent = new CodexAppServerAgent ( client , {
2262+ processOptions : { binaryPath : "/x/codex" } ,
2263+ rpcFactory : stub . factory ,
2264+ } ) ;
2265+
2266+ await agent . newSession ( { cwd : "/r" } as unknown as NewSessionRequest ) ;
2267+ await expect (
2268+ agent . prompt ( {
2269+ sessionId : "t" ,
2270+ prompt : [ { type : "text" , text : "too late" } ] ,
2271+ _meta : { steer : true } ,
2272+ } as unknown as PromptRequest ) ,
2273+ ) . resolves . toMatchObject ( { _meta : { steer : false } } ) ;
2274+ expect (
2275+ stub . requests . filter ( ( request ) => request . method === "turn/start" ) ,
2276+ ) . toHaveLength ( 0 ) ;
2277+ expect ( sessionUpdates ) . not . toContainEqual (
2278+ expect . objectContaining ( {
2279+ update : expect . objectContaining ( {
2280+ sessionUpdate : "user_message_chunk" ,
2281+ content : { type : "text" , text : "too late" } ,
2282+ } ) ,
2283+ } ) ,
2284+ ) ;
2285+ } ) ;
2286+
21672287 it ( "refreshes the live turnId from each turn/steer response" , async ( ) => {
21682288 const stub = makeStubRpc ( {
21692289 "thread/start" : { thread : { id : "t" } } ,
0 commit comments