1- import { spawn , type ChildProcessWithoutNullStreams } from "node:child_process" ;
1+ import { execFile , spawn , type ChildProcessWithoutNullStreams } from "node:child_process" ;
22import * as path from "node:path" ;
33import readline from "node:readline" ;
4+ import { promisify } from "node:util" ;
45import WebSocket from "ws" ;
56import type { PluginLogger } from "openclaw/plugin-sdk" ;
67import { createPendingInputState , parseCodexUserInput } from "./pending-input.js" ;
@@ -72,6 +73,17 @@ const DEFAULT_PROTOCOL_VERSION = "1.0";
7273const TRAILING_NOTIFICATION_SETTLE_MS = 250 ;
7374const TURN_STEER_METHODS = [ "turn/steer" ] as const ;
7475const TURN_INTERRUPT_METHODS = [ "turn/interrupt" ] as const ;
76+ const execFileAsync = promisify ( execFile ) ;
77+
78+ type StartupProbeInfo = {
79+ transport : PluginSettings [ "transport" ] ;
80+ command ?: string ;
81+ args ?: string [ ] ;
82+ resolvedCommandPath ?: string ;
83+ cliVersion ?: string ;
84+ serverName ?: string ;
85+ serverVersion ?: string ;
86+ } ;
7587
7688function isTransportClosedError ( error : unknown ) : boolean {
7789 const text = error instanceof Error ? error . message : String ( error ) ;
@@ -741,8 +753,8 @@ async function initializeClient(params: {
741753 client : JsonRpcClient ;
742754 settings : PluginSettings ;
743755 sessionKey ?: string ;
744- } ) : Promise < void > {
745- await params . client . request ( "initialize" , {
756+ } ) : Promise < unknown > {
757+ const initializeResult = await params . client . request ( "initialize" , {
746758 protocolVersion : DEFAULT_PROTOCOL_VERSION ,
747759 clientInfo : { name : "openclaw-codex-app-server" , version : "0.0.0" } ,
748760 capabilities : { experimentalApi : true } ,
@@ -760,6 +772,82 @@ async function initializeClient(params: {
760772 }
761773 } ) ;
762774 }
775+ return initializeResult ;
776+ }
777+
778+ function extractStartupProbeInfo (
779+ initializeResult : unknown ,
780+ base : StartupProbeInfo ,
781+ ) : StartupProbeInfo {
782+ const record = asRecord ( initializeResult ) ?? { } ;
783+ const serverInfo = asRecord ( record . serverInfo ) ?? asRecord ( record . server_info ) ?? record ;
784+ return {
785+ ...base ,
786+ serverName :
787+ pickString ( serverInfo , [ "name" , "serverName" , "server_name" ] ) ?? base . serverName ,
788+ serverVersion :
789+ pickString ( serverInfo , [ "version" , "serverVersion" , "server_version" ] ) ?? base . serverVersion ,
790+ } ;
791+ }
792+
793+ async function resolveCommandPath ( command : string ) : Promise < string | undefined > {
794+ const trimmed = command . trim ( ) ;
795+ if ( ! trimmed ) {
796+ return undefined ;
797+ }
798+ if ( trimmed . includes ( path . sep ) ) {
799+ return path . resolve ( trimmed ) ;
800+ }
801+ const locator = process . platform === "win32" ? "where" : "which" ;
802+ try {
803+ const { stdout } = await execFileAsync ( locator , [ trimmed ] , { timeout : 5_000 } ) ;
804+ const first = stdout
805+ . split ( / \r ? \n / )
806+ . map ( ( line ) => line . trim ( ) )
807+ . find ( Boolean ) ;
808+ return first || undefined ;
809+ } catch {
810+ return undefined ;
811+ }
812+ }
813+
814+ async function probeStdioVersion ( settings : PluginSettings ) : Promise < {
815+ resolvedCommandPath ?: string ;
816+ cliVersion ?: string ;
817+ } > {
818+ const resolvedCommandPath = await resolveCommandPath ( settings . command ) ;
819+ const commandPath = resolvedCommandPath ?? settings . command ;
820+ try {
821+ const { stdout, stderr } = await execFileAsync (
822+ commandPath ,
823+ [ ...settings . args , "--version" ] ,
824+ { timeout : Math . min ( settings . requestTimeoutMs , 10_000 ) } ,
825+ ) ;
826+ const combined = `${ stdout } \n${ stderr } `
827+ . split ( / \r ? \n / )
828+ . map ( ( line ) => line . trim ( ) )
829+ . find ( Boolean ) ;
830+ return {
831+ resolvedCommandPath,
832+ cliVersion : combined || undefined ,
833+ } ;
834+ } catch {
835+ return { resolvedCommandPath } ;
836+ }
837+ }
838+
839+ function formatStartupProbeLog ( info : StartupProbeInfo ) : string {
840+ return [
841+ `transport=${ info . transport } ` ,
842+ info . command ? `command=${ info . command } ` : undefined ,
843+ info . args ? `args=${ JSON . stringify ( info . args ) } ` : undefined ,
844+ info . resolvedCommandPath ? `resolved=${ info . resolvedCommandPath } ` : undefined ,
845+ info . cliVersion ? `cliVersion=${ info . cliVersion } ` : undefined ,
846+ info . serverName ? `serverName=${ info . serverName } ` : undefined ,
847+ info . serverVersion ? `serverVersion=${ info . serverVersion } ` : undefined ,
848+ ]
849+ . filter ( Boolean )
850+ . join ( " " ) ;
763851}
764852
765853async function requestWithFallbacks ( params : {
@@ -828,21 +916,8 @@ function buildThreadResumePayloads(params: {
828916 } ) ;
829917}
830918
831- function buildTurnInput (
832- prompt : string ,
833- options ?: { includeLegacyMessageVariant ?: boolean } ,
834- ) : unknown [ ] {
835- const variants : unknown [ ] = [ [ { type : "text" , text : prompt } ] ] ;
836- if ( options ?. includeLegacyMessageVariant !== false ) {
837- variants . push ( [
838- {
839- type : "message" ,
840- role : "user" ,
841- content : [ { type : "input_text" , text : prompt } ] ,
842- } ,
843- ] ) ;
844- }
845- return variants ;
919+ function buildTurnInput ( prompt : string ) : unknown [ ] {
920+ return [ [ { type : "text" , text : prompt } ] ] ;
846921}
847922
848923function buildCollaborationModeVariants (
@@ -912,9 +987,7 @@ function buildTurnStartPayloads(params: {
912987 model ?: string ;
913988 collaborationMode ?: CollaborationMode ;
914989} ) : unknown [ ] {
915- const payloads = buildTurnInput ( params . prompt , {
916- includeLegacyMessageVariant : ! params . collaborationMode ,
917- } ) . flatMap ( ( input ) => {
990+ const payloads = buildTurnInput ( params . prompt ) . flatMap ( ( input ) => {
918991 const camel : Record < string , unknown > = {
919992 threadId : params . threadId ,
920993 input,
@@ -1908,17 +1981,21 @@ async function withInitializedClient<T>(
19081981 settings : PluginSettings ;
19091982 sessionKey ?: string ;
19101983 } ,
1911- callback : ( args : { client : JsonRpcClient ; settings : PluginSettings } ) => Promise < T > ,
1984+ callback : ( args : {
1985+ client : JsonRpcClient ;
1986+ settings : PluginSettings ;
1987+ initializeResult : unknown ;
1988+ } ) => Promise < T > ,
19121989) : Promise < T > {
19131990 const client = createJsonRpcClient ( params . settings ) ;
19141991 try {
19151992 await client . connect ( ) ;
1916- await initializeClient ( {
1993+ const initializeResult = await initializeClient ( {
19171994 client,
19181995 settings : params . settings ,
19191996 sessionKey : params . sessionKey ,
19201997 } ) ;
1921- return await callback ( { client, settings : params . settings } ) ;
1998+ return await callback ( { client, settings : params . settings , initializeResult } ) ;
19221999 } finally {
19232000 await client . close ( ) . catch ( ( ) => undefined ) ;
19242001 }
@@ -1941,6 +2018,31 @@ export class CodexAppServerClient {
19412018 private readonly logger : PluginLogger ,
19422019 ) { }
19432020
2021+ async logStartupProbe ( params : { sessionKey ?: string } = { } ) : Promise < void > {
2022+ const base : StartupProbeInfo = {
2023+ transport : this . settings . transport ,
2024+ command : this . settings . transport === "stdio" ? this . settings . command : undefined ,
2025+ args : this . settings . transport === "stdio" ? this . settings . args : undefined ,
2026+ } ;
2027+ const stdioProbe =
2028+ this . settings . transport === "stdio" ? await probeStdioVersion ( this . settings ) : { } ;
2029+ await withInitializedClient (
2030+ { settings : this . settings , sessionKey : params . sessionKey } ,
2031+ async ( { initializeResult } ) => {
2032+ const info = extractStartupProbeInfo ( initializeResult , {
2033+ ...base ,
2034+ ...stdioProbe ,
2035+ } ) ;
2036+ this . logger . info ( `codex startup probe ${ formatStartupProbeLog ( info ) } ` ) ;
2037+ } ,
2038+ ) . catch ( ( error ) => {
2039+ const message = error instanceof Error ? error . message : String ( error ) ;
2040+ this . logger . warn (
2041+ `codex startup probe failed transport=${ this . settings . transport } ${ this . settings . transport === "stdio" ? ` command=${ this . settings . command } ` : "" } : ${ message } ` ,
2042+ ) ;
2043+ } ) ;
2044+ }
2045+
19442046 async listThreads ( params : {
19452047 sessionKey ?: string ;
19462048 workspaceDir ?: string ;
@@ -2965,6 +3067,7 @@ export const __testing = {
29653067 buildTurnStartPayloads,
29663068 createPendingInputCoordinator,
29673069 extractFileChangePathsFromReadResult,
3070+ extractStartupProbeInfo,
29683071 extractThreadTokenUsageSnapshot,
29693072 extractRateLimitSummaries,
29703073} ;
0 commit comments