@@ -22,6 +22,7 @@ import {
2222import {
2323 IS_WIN ,
2424 waitForHealth ,
25+ quickHealthCheck ,
2526 quickRecallPrecheck ,
2627 withTimeout ,
2728 resolvePythonCommand ,
@@ -707,7 +708,11 @@ const contextEnginePlugin = {
707708 async execute ( _toolCallId : string , params : Record < string , unknown > ) {
708709 rememberSessionAgentId ( ctx ) ;
709710 const archiveId = String ( ( params as { archiveId ?: string } ) . archiveId ?? "" ) . trim ( ) ;
711+ const sessionId = ctx . sessionId ?? "" ;
712+ api . logger . info ?.( `openviking: ov_archive_expand invoked (archiveId=${ archiveId || "(empty)" } , sessionId=${ sessionId || "(empty)" } )` ) ;
713+
710714 if ( ! archiveId ) {
715+ api . logger . warn ?.( `openviking: ov_archive_expand missing archiveId` ) ;
711716 return {
712717 content : [ { type : "text" , text : "Error: archiveId is required." } ] ,
713718 details : { error : "missing_param" , param : "archiveId" } ,
@@ -747,6 +752,7 @@ const contextEnginePlugin = {
747752 . map ( ( m : OVMessage ) => formatMessageFaithful ( m ) )
748753 . join ( "\n\n" ) ;
749754
755+ api . logger . info ?.( `openviking: ov_archive_expand expanded ${ detail . archive_id } , messages=${ detail . messages . length } , chars=${ body . length } , sessionId=${ sessionId } ` ) ;
750756 return {
751757 content : [ { type : "text" , text : `${ header } \n${ body } ` } ] ,
752758 details : {
@@ -759,6 +765,7 @@ const contextEnginePlugin = {
759765 } ;
760766 } catch ( err ) {
761767 const msg = err instanceof Error ? err . message : String ( err ) ;
768+ api . logger . warn ?.( `openviking: ov_archive_expand failed (archiveId=${ archiveId } , sessionId=${ sessionId } ): ${ msg } ` ) ;
762769 return {
763770 content : [ { type : "text" , text : `Failed to expand ${ archiveId } : ${ msg } ` } ] ,
764771 details : { error : msg , archiveId, sessionId, ovSessionId } ,
@@ -1072,10 +1079,8 @@ const contextEnginePlugin = {
10721079 localProcess = null ;
10731080 localClientCache . delete ( localCacheKey ) ;
10741081 }
1075- if ( code != null && code !== 0 || signal ) {
1076- const out = formatStderrOutput ( ) ;
1077- api . logger . warn ( `openviking: subprocess exited (code=${ code } , signal=${ signal } )${ out } ` ) ;
1078- }
1082+ const out = formatStderrOutput ( ) ;
1083+ api . logger . warn ( `openviking: subprocess exited (code=${ code } , signal=${ signal } )${ out } ` ) ;
10791084 } ) ;
10801085 try {
10811086 await waitForHealth ( baseUrl , timeoutMs , intervalMs ) ;
@@ -1105,6 +1110,82 @@ const contextEnginePlugin = {
11051110 }
11061111 throw err ;
11071112 }
1113+ } else if ( cfg . mode === "local" ) {
1114+ // Defensive re-spawn: if we're not the designated spawner but there's
1115+ // no valid local process, trigger a fresh spawn to recover from
1116+ // scenarios like Gateway force-restart where the child process was
1117+ // orphaned or exited silently.
1118+ const cached = localClientCache . get ( localCacheKey ) ;
1119+ const processAlive = cached ?. process && cached . process . exitCode === null && ! cached . process . killed ;
1120+ if ( ! processAlive ) {
1121+ const healthOk = await quickHealthCheck ( `http://127.0.0.1:${ cfg . port } ` , 2000 ) ;
1122+ if ( ! healthOk ) {
1123+ api . logger . warn (
1124+ `openviking: no valid local process detected (isSpawner=false), triggering defensive re-spawn` ,
1125+ ) ;
1126+ const timeoutMs = 60_000 ;
1127+ const intervalMs = 500 ;
1128+ const actualPort = await prepareLocalPort ( cfg . port , api . logger ) ;
1129+ const baseUrl = `http://127.0.0.1:${ actualPort } ` ;
1130+ const pythonCmd = resolvePythonCommand ( api . logger ) ;
1131+ const pathSep = IS_WIN ? ";" : ":" ;
1132+ const env = {
1133+ ...process . env ,
1134+ PYTHONUNBUFFERED : "1" ,
1135+ PYTHONWARNINGS : "ignore::RuntimeWarning" ,
1136+ OPENVIKING_CONFIG_FILE : cfg . configPath ,
1137+ OPENVIKING_START_CONFIG : cfg . configPath ,
1138+ OPENVIKING_START_HOST : "127.0.0.1" ,
1139+ OPENVIKING_START_PORT : String ( actualPort ) ,
1140+ ...( process . env . OPENVIKING_GO_PATH && { PATH : `${ process . env . OPENVIKING_GO_PATH } ${ pathSep } ${ process . env . PATH || "" } ` } ) ,
1141+ ...( process . env . OPENVIKING_GOPATH && { GOPATH : process . env . OPENVIKING_GOPATH } ) ,
1142+ ...( process . env . OPENVIKING_GOPROXY && { GOPROXY : process . env . OPENVIKING_GOPROXY } ) ,
1143+ } ;
1144+ const runpyCode = `import sys,os,warnings; warnings.filterwarnings('ignore', category=RuntimeWarning, message='.*sys.modules.*'); sys.argv=['openviking.server.bootstrap','--config',os.environ['OPENVIKING_START_CONFIG'],'--host',os.environ.get('OPENVIKING_START_HOST','127.0.0.1'),'--port',os.environ['OPENVIKING_START_PORT']]; import runpy, importlib.util; spec=importlib.util.find_spec('openviking.server.bootstrap'); (runpy.run_path(spec.origin, run_name='__main__') if spec and getattr(spec,'origin',None) else runpy.run_module('openviking.server.bootstrap', run_name='__main__', alter_sys=True))` ;
1145+ const child = spawn (
1146+ pythonCmd ,
1147+ [ "-c" , runpyCode ] ,
1148+ { env, cwd : IS_WIN ? tmpdir ( ) : "/tmp" , stdio : [ "ignore" , "pipe" , "pipe" ] } ,
1149+ ) ;
1150+ localProcess = child ;
1151+ child . on ( "error" , ( err : Error ) => api . logger . warn ( `openviking: local server error (re-spawn): ${ String ( err ) } ` ) ) ;
1152+ child . stderr ?. on ( "data" , ( chunk : Buffer ) => {
1153+ api . logger . debug ?.( `[openviking-respawn] ${ String ( chunk ) . trim ( ) } ` ) ;
1154+ } ) ;
1155+ child . on ( "exit" , ( code : number | null , signal : string | null ) => {
1156+ if ( localProcess === child ) {
1157+ localProcess = null ;
1158+ localClientCache . delete ( localCacheKey ) ;
1159+ }
1160+ api . logger . warn ( `openviking: re-spawned subprocess exited (code=${ code } , signal=${ signal } )` ) ;
1161+ } ) ;
1162+ try {
1163+ await waitForHealth ( baseUrl , timeoutMs , intervalMs ) ;
1164+ const client = new OpenVikingClient ( baseUrl , cfg . apiKey , cfg . agentId , cfg . timeoutMs ) ;
1165+ localClientCache . set ( localCacheKey , { client, process : child } ) ;
1166+ if ( resolveLocalClient ) {
1167+ resolveLocalClient ( client ) ;
1168+ rejectLocalClient = null ;
1169+ }
1170+ api . logger . info (
1171+ `openviking: local server re-spawned successfully (${ baseUrl } , config: ${ cfg . configPath } )` ,
1172+ ) ;
1173+ } catch ( err ) {
1174+ localProcess = null ;
1175+ child . kill ( "SIGTERM" ) ;
1176+ markLocalUnavailable ( "re-spawn failed" , err ) ;
1177+ api . logger . warn ( `openviking: defensive re-spawn failed: ${ String ( err ) } ` ) ;
1178+ throw err ;
1179+ }
1180+ } else {
1181+ api . logger . info ( `openviking: local process healthy on port ${ cfg . port } (isSpawner=false)` ) ;
1182+ }
1183+ } else {
1184+ await ( await getClient ( ) ) . healthCheck ( ) . catch ( ( ) => { } ) ;
1185+ api . logger . info (
1186+ `openviking: initialized via cache (url: ${ cfg . baseUrl } , targetUri: ${ cfg . targetUri } )` ,
1187+ ) ;
1188+ }
11081189 } else {
11091190 await ( await getClient ( ) ) . healthCheck ( ) . catch ( ( ) => { } ) ;
11101191 api . logger . info (
0 commit comments