@@ -27,6 +27,7 @@ import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
2727import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js" ;
2828import * as authBridge from "./auth-bridge.js" ;
2929import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js" ;
30+ import { CodexAppServerRpcError } from "./client.js" ;
3031import { readCodexPluginConfig , resolveCodexAppServerRuntimeOptions } from "./config.js" ;
3132import {
3233 CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE ,
@@ -65,6 +66,7 @@ import {
6566} from "./sandbox-exec-server.js" ;
6667import { createSandboxContext } from "./sandbox-exec-server.test-helpers.js" ;
6768import { readCodexAppServerBinding , writeCodexAppServerBinding } from "./session-binding.js" ;
69+ import * as sharedClientModule from "./shared-client.js" ;
6870import { createCodexTestModel } from "./test-support.js" ;
6971import { buildTurnStartParams , startOrResumeThread } from "./thread-lifecycle.js" ;
7072
@@ -3489,6 +3491,134 @@ describe("runCodexAppServerAttempt", () => {
34893491 ] ) ;
34903492 } ) ;
34913493
3494+ it ( "does not retire the shared Codex client when a spawned helper run fails with a logical thread/start error" , async ( ) => {
3495+ const clearSpy = vi . spyOn ( sharedClientModule , "clearSharedCodexAppServerClientIfCurrent" ) ;
3496+ clearSpy . mockClear ( ) ;
3497+ let failedClient : unknown ;
3498+ setCodexAppServerClientFactoryForTest ( async ( ) => {
3499+ const c = {
3500+ request : vi . fn ( async ( method : string ) => {
3501+ if ( method === "thread/start" ) {
3502+ throw new CodexAppServerRpcError (
3503+ { message : "401 authentication_error: Invalid bearer token" } ,
3504+ "thread/start" ,
3505+ ) ;
3506+ }
3507+ return { } ;
3508+ } ) ,
3509+ addNotificationHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3510+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3511+ } ;
3512+ failedClient = c ;
3513+ return c as never ;
3514+ } ) ;
3515+ const params = createParams (
3516+ path . join ( tempDir , "session.jsonl" ) ,
3517+ path . join ( tempDir , "workspace" ) ,
3518+ ) ;
3519+ params . spawnedBy = "agent:main:session-parent" ;
3520+
3521+ await expect ( runCodexAppServerAttempt ( params ) ) . rejects . toThrow ( "Invalid bearer token" ) ;
3522+ const calledWithFailedClient = clearSpy . mock . calls . some ( ( [ arg ] ) => arg === failedClient ) ;
3523+ expect ( calledWithFailedClient ) . toBe ( false ) ;
3524+ clearSpy . mockRestore ( ) ;
3525+ } ) ;
3526+
3527+ it ( "retires the shared Codex client when a spawned helper run times out during thread/start" , async ( ) => {
3528+ const clearSpy = vi . spyOn ( sharedClientModule , "clearSharedCodexAppServerClientIfCurrent" ) ;
3529+ clearSpy . mockClear ( ) ;
3530+ let failedClient : unknown ;
3531+ setCodexAppServerClientFactoryForTest ( async ( ) => {
3532+ const c = {
3533+ request : vi . fn ( async ( method : string ) => {
3534+ if ( method === "thread/start" ) {
3535+ return await new Promise < never > ( ( ) => undefined ) ;
3536+ }
3537+ return { } ;
3538+ } ) ,
3539+ addNotificationHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3540+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3541+ } ;
3542+ failedClient = c ;
3543+ return c as never ;
3544+ } ) ;
3545+ const params = createParams (
3546+ path . join ( tempDir , "session.jsonl" ) ,
3547+ path . join ( tempDir , "workspace" ) ,
3548+ ) ;
3549+ params . spawnedBy = "agent:main:session-parent" ;
3550+ params . timeoutMs = 1 ;
3551+
3552+ await expect ( runCodexAppServerAttempt ( params , { startupTimeoutFloorMs : 1 } ) ) . rejects . toThrow (
3553+ "codex app-server startup timed out" ,
3554+ ) ;
3555+ const calledWithFailedClient = clearSpy . mock . calls . some ( ( [ arg ] ) => arg === failedClient ) ;
3556+ expect ( calledWithFailedClient ) . toBe ( true ) ;
3557+ clearSpy . mockRestore ( ) ;
3558+ } ) ;
3559+
3560+ it ( "retires the shared Codex client when a spawned helper hits a thread/start write failure" , async ( ) => {
3561+ const clearSpy = vi . spyOn ( sharedClientModule , "clearSharedCodexAppServerClientIfCurrent" ) ;
3562+ clearSpy . mockClear ( ) ;
3563+ let failedClient : unknown ;
3564+ setCodexAppServerClientFactoryForTest ( async ( ) => {
3565+ const c = {
3566+ request : vi . fn ( async ( method : string ) => {
3567+ if ( method === "thread/start" ) {
3568+ throw new Error ( "write EPIPE" ) ;
3569+ }
3570+ return { } ;
3571+ } ) ,
3572+ addNotificationHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3573+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3574+ } ;
3575+ failedClient = c ;
3576+ return c as never ;
3577+ } ) ;
3578+ const params = createParams (
3579+ path . join ( tempDir , "session.jsonl" ) ,
3580+ path . join ( tempDir , "workspace" ) ,
3581+ ) ;
3582+ params . spawnedBy = "agent:main:session-parent" ;
3583+
3584+ await expect ( runCodexAppServerAttempt ( params ) ) . rejects . toThrow ( "write EPIPE" ) ;
3585+ const calledWithFailedClient = clearSpy . mock . calls . some ( ( [ arg ] ) => arg === failedClient ) ;
3586+ expect ( calledWithFailedClient ) . toBe ( true ) ;
3587+ clearSpy . mockRestore ( ) ;
3588+ } ) ;
3589+
3590+ it ( "retires the shared Codex client when a top-level run fails with a logical thread/start error" , async ( ) => {
3591+ const clearSpy = vi . spyOn ( sharedClientModule , "clearSharedCodexAppServerClientIfCurrent" ) ;
3592+ clearSpy . mockClear ( ) ;
3593+ let failedClient : unknown ;
3594+ setCodexAppServerClientFactoryForTest ( async ( ) => {
3595+ const c = {
3596+ request : vi . fn ( async ( method : string ) => {
3597+ if ( method === "thread/start" ) {
3598+ throw new CodexAppServerRpcError (
3599+ { message : "401 authentication_error: Invalid bearer token" } ,
3600+ "thread/start" ,
3601+ ) ;
3602+ }
3603+ return { } ;
3604+ } ) ,
3605+ addNotificationHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3606+ addRequestHandler : vi . fn ( ( ) => ( ) => undefined ) ,
3607+ } ;
3608+ failedClient = c ;
3609+ return c as never ;
3610+ } ) ;
3611+ const params = createParams (
3612+ path . join ( tempDir , "session.jsonl" ) ,
3613+ path . join ( tempDir , "workspace" ) ,
3614+ ) ;
3615+
3616+ await expect ( runCodexAppServerAttempt ( params ) ) . rejects . toThrow ( "Invalid bearer token" ) ;
3617+ const calledWithFailedClient = clearSpy . mock . calls . some ( ( [ arg ] ) => arg === failedClient ) ;
3618+ expect ( calledWithFailedClient ) . toBe ( true ) ;
3619+ clearSpy . mockRestore ( ) ;
3620+ } ) ;
3621+
34923622 it ( "passes configured app-server policy, sandbox, service tier, and model on resume" , async ( ) => {
34933623 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
34943624 const workspaceDir = path . join ( tempDir , "workspace" ) ;
0 commit comments