@@ -67,6 +67,7 @@ import { createAgentToolset, type AgentToolset, type OperatorResult } from "../a
6767import { createAgentWithLiveToolDispatch } from "../agent/live-tool-dispatch.js" ;
6868import { liveTelemetry } from "../telemetry/singleton.js" ;
6969import { createTurnObserver } from "../telemetry/ai-observability.js" ;
70+ import { terminalProviderFailureMessage } from "../inference-error-message.js" ;
7071import { collectToolPlugins , resolveToolPlugins } from "../plugins/tool-plugins.js" ;
7172import {
7273 expandExistingPluginMembers ,
@@ -120,6 +121,32 @@ export function formatCaughtError(err: unknown): string {
120121 return err instanceof Error ? err . message : String ( err ) ;
121122}
122123
124+ const SELECTED_PROVIDER_FAILURE = "SelectedProviderFailure" ;
125+
126+ export async function refreshSelectedProviderCredential < T > ( refresh : ( ) => Promise < T > ) : Promise < T > {
127+ try {
128+ return await refresh ( ) ;
129+ } catch ( cause ) {
130+ const error = new Error ( formatCaughtError ( cause ) , { cause } ) ;
131+ error . name = SELECTED_PROVIDER_FAILURE ;
132+ throw error ;
133+ }
134+ }
135+
136+ export function execUserFailureMessage (
137+ config : Config ,
138+ err : unknown ,
139+ inferenceStarted : boolean ,
140+ ) : string {
141+ if ( inferenceStarted || ( err instanceof Error && err . name === SELECTED_PROVIDER_FAILURE ) ) {
142+ return terminalProviderFailureMessage (
143+ config . providerName ,
144+ config . settings ?. providers [ config . providerName ] ?. name ,
145+ ) ;
146+ }
147+ return formatCaughtError ( err ) ;
148+ }
149+
123150/**
124151 * Headless analogue of TUI `runtime-shutdown`: abort live workers, then close
125152 * the primary agent and dispose the toolset. `cancelAll` is fire-and-forget —
@@ -297,6 +324,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
297324 let finalized = false ;
298325 let turnsUsed = 0 ;
299326 let runSink : RunSink | null = null ;
327+ let inferenceStarted = false ;
300328
301329 const persist = async (
302330 status : "running" | "done" | "failed" | "cancelled" ,
@@ -651,15 +679,19 @@ export async function runExec(config: Config): Promise<ExecResult> {
651679
652680 // Refresh OAuth tokens before first inference when starting on codex/xai.
653681 if ( initialCodexProfile !== undefined ) {
654- const { access } = await getValidCodexToken ( initialCodexProfile ) ;
682+ const { access } = await refreshSelectedProviderCredential ( ( ) =>
683+ getValidCodexToken ( initialCodexProfile ) ,
684+ ) ;
655685 liveSource = { ...liveSource , apiKey : access } ;
656686 liveSubAgentProvider . current = {
657687 ...liveSubAgentProvider . current ,
658688 apiKey : access ,
659689 } ;
660690 }
661691 if ( initialXaiProfile !== undefined ) {
662- const { access } = await getValidXaiToken ( initialXaiProfile ) ;
692+ const { access } = await refreshSelectedProviderCredential ( ( ) =>
693+ getValidXaiToken ( initialXaiProfile ) ,
694+ ) ;
663695 liveSource = { ...liveSource , apiKey : access } ;
664696 liveSubAgentProvider . current = {
665697 ...liveSubAgentProvider . current ,
@@ -785,6 +817,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
785817 let runError : string | undefined ;
786818 let sinkStatus : ReturnType < typeof liveSink . getStatus > = "cancelled" ;
787819 try {
820+ inferenceStarted = true ;
788821 // Final OAuth refresh immediately before send (token may have aged during MCP).
789822 if ( initialCodexProfile !== undefined ) {
790823 const { access } = await getValidCodexToken ( initialCodexProfile ) ;
@@ -881,17 +914,24 @@ export async function runExec(config: Config): Promise<ExecResult> {
881914 } ) ;
882915
883916 if ( ! sendCompleted || runError !== undefined || summaryStatus === "failed" ) {
884- const message =
917+ const diagnosticMessage =
885918 runError ??
886919 ( summaryStatus === "cancelled" ? "run cancelled before completion" : "run failed" ) ;
887- stderr . write ( `Error: ${ message } \n` ) ;
920+ const userMessage =
921+ summaryStatus === "failed"
922+ ? terminalProviderFailureMessage (
923+ config . providerName ,
924+ config . settings ?. providers [ config . providerName ] ?. name ,
925+ )
926+ : diagnosticMessage ;
927+ stderr . write ( `Error: ${ userMessage } \n` ) ;
888928 const persistStatus = summaryStatus === "cancelled" ? "cancelled" : "failed" ;
889- await persist ( persistStatus , { error : message } ) ;
929+ await persist ( persistStatus , { error : diagnosticMessage } ) ;
890930 return {
891931 exitCode : 1 ,
892932 sessionId,
893933 text : textOut ,
894- error : message ,
934+ error : userMessage ,
895935 status : summaryStatus ,
896936 durationMs : finishedAt - startedAt ,
897937 turnsUsed : runSink . getTurnCount ( ) ,
@@ -916,15 +956,16 @@ export async function runExec(config: Config): Promise<ExecResult> {
916956 model : config . model ,
917957 } ;
918958 } catch ( err ) {
919- const message = err instanceof Error ? err . message : String ( err ) ;
920- logger . error ( "exec failed: {error}" , { error : message } ) ;
921- stderr . write ( `Error: ${ message } \n` ) ;
922- await persist ( "failed" , { error : message } ) ;
959+ const diagnosticMessage = formatCaughtError ( err ) ;
960+ logger . error ( "exec failed: {error}" , { error : diagnosticMessage } ) ;
961+ const userMessage = execUserFailureMessage ( config , err , inferenceStarted ) ;
962+ stderr . write ( `Error: ${ userMessage } \n` ) ;
963+ await persist ( "failed" , { error : diagnosticMessage } ) ;
923964 return {
924965 exitCode : 1 ,
925966 sessionId,
926967 text : textOut ,
927- error : message ,
968+ error : userMessage ,
928969 status : "failed" ,
929970 durationMs : Date . now ( ) - startedAt ,
930971 turnsUsed : runSink ?. getTurnCount ( ) ?? turnsUsed ,
0 commit comments