11import net from "node:net" ;
2- import {
3- abortEmbeddedPiRun ,
4- getActiveEmbeddedRunCount ,
5- waitForActiveEmbeddedRuns ,
6- } from "../../agents/pi-embedded-runner/runs.js" ;
7- import { getRuntimeConfig } from "../../config/config.js" ;
82import type { startGatewayServer } from "../../gateway/server.js" ;
93import { formatErrorMessage } from "../../infra/errors.js" ;
104import { acquireGatewayLock } from "../../infra/gateway-lock.js" ;
11- import {
12- respawnGatewayProcessForUpdate ,
13- restartGatewayProcessWithFreshPid ,
14- } from "../../infra/process-respawn.js" ;
15- import { markUpdateRestartSentinelFailure } from "../../infra/restart-sentinel.js" ;
16- import {
17- consumeGatewaySigusr1RestartAuthorization ,
18- consumeGatewayRestartIntentSync ,
19- isGatewaySigusr1RestartExternallyAllowed ,
20- markGatewaySigusr1RestartHandled ,
21- peekGatewaySigusr1RestartReason ,
22- resetGatewayRestartStateForInProcessRestart ,
23- scheduleGatewaySigusr1Restart ,
24- } from "../../infra/restart.js" ;
25- import { detectRespawnSupervisor } from "../../infra/supervisor-markers.js" ;
26- import { writeDiagnosticStabilityBundleForFailureSync } from "../../logging/diagnostic-stability-bundle.js" ;
275import { createSubsystemLogger } from "../../logging/subsystem.js" ;
28- import {
29- getActiveBundledRuntimeDepsInstallCount ,
30- waitForBundledRuntimeDepsInstallIdle ,
31- } from "../../plugins/bundled-runtime-deps-activity.js" ;
32- import {
33- getActiveTaskCount ,
34- markGatewayDraining ,
35- resetAllLanes ,
36- waitForActiveTasks ,
37- } from "../../process/command-queue.js" ;
38- import { createRestartIterationHook } from "../../process/restart-recovery.js" ;
396import type { RuntimeEnv } from "../../runtime.js" ;
40- import { reloadTaskRegistryFromStore } from "../../tasks/runtime-internal.js" ;
417
428const gatewayLog = createSubsystemLogger ( "gateway" ) ;
439const LAUNCHD_SUPERVISED_RESTART_EXIT_DELAY_MS = 1500 ;
@@ -49,6 +15,61 @@ const UPDATE_RESPAWN_HEALTH_POLL_MS = 200;
4915type GatewayRunSignalAction = "stop" | "restart" ;
5016type RestartDrainTimeoutMs = number | undefined ;
5117
18+ type EmbeddedRunsModule = typeof import ( "../../agents/pi-embedded-runner/runs.js" ) ;
19+ type RuntimeConfigModule = typeof import ( "../../config/config.js" ) ;
20+ type ProcessRespawnModule = typeof import ( "../../infra/process-respawn.js" ) ;
21+ type RestartSentinelModule = typeof import ( "../../infra/restart-sentinel.js" ) ;
22+ type RestartModule = typeof import ( "../../infra/restart.js" ) ;
23+ type SupervisorMarkersModule = typeof import ( "../../infra/supervisor-markers.js" ) ;
24+ type DiagnosticStabilityBundleModule =
25+ typeof import ( "../../logging/diagnostic-stability-bundle.js" ) ;
26+ type BundledRuntimeDepsActivityModule =
27+ typeof import ( "../../plugins/bundled-runtime-deps-activity.js" ) ;
28+ type CommandQueueModule = typeof import ( "../../process/command-queue.js" ) ;
29+ type RuntimeInternalModule = typeof import ( "../../tasks/runtime-internal.js" ) ;
30+
31+ let embeddedRunsModule : Promise < EmbeddedRunsModule > | undefined ;
32+ let runtimeConfigModule : Promise < RuntimeConfigModule > | undefined ;
33+ let processRespawnModule : Promise < ProcessRespawnModule > | undefined ;
34+ let restartSentinelModule : Promise < RestartSentinelModule > | undefined ;
35+ let restartModule : Promise < RestartModule > | undefined ;
36+ let supervisorMarkersModule : Promise < SupervisorMarkersModule > | undefined ;
37+ let diagnosticStabilityBundleModule : Promise < DiagnosticStabilityBundleModule > | undefined ;
38+ let bundledRuntimeDepsActivityModule : Promise < BundledRuntimeDepsActivityModule > | undefined ;
39+ let commandQueueModule : Promise < CommandQueueModule > | undefined ;
40+ let runtimeInternalModule : Promise < RuntimeInternalModule > | undefined ;
41+
42+ const loadEmbeddedRunsModule = ( ) =>
43+ ( embeddedRunsModule ??= import ( "../../agents/pi-embedded-runner/runs.js" ) ) ;
44+ const loadRuntimeConfigModule = ( ) => ( runtimeConfigModule ??= import ( "../../config/config.js" ) ) ;
45+ const loadProcessRespawnModule = ( ) =>
46+ ( processRespawnModule ??= import ( "../../infra/process-respawn.js" ) ) ;
47+ const loadRestartSentinelModule = ( ) =>
48+ ( restartSentinelModule ??= import ( "../../infra/restart-sentinel.js" ) ) ;
49+ const loadRestartModule = ( ) => ( restartModule ??= import ( "../../infra/restart.js" ) ) ;
50+ const loadSupervisorMarkersModule = ( ) =>
51+ ( supervisorMarkersModule ??= import ( "../../infra/supervisor-markers.js" ) ) ;
52+ const loadDiagnosticStabilityBundleModule = ( ) =>
53+ ( diagnosticStabilityBundleModule ??= import ( "../../logging/diagnostic-stability-bundle.js" ) ) ;
54+ const loadBundledRuntimeDepsActivityModule = ( ) =>
55+ ( bundledRuntimeDepsActivityModule ??= import ( "../../plugins/bundled-runtime-deps-activity.js" ) ) ;
56+ const loadCommandQueueModule = ( ) =>
57+ ( commandQueueModule ??= import ( "../../process/command-queue.js" ) ) ;
58+ const loadRuntimeInternalModule = ( ) =>
59+ ( runtimeInternalModule ??= import ( "../../tasks/runtime-internal.js" ) ) ;
60+
61+ function createRestartIterationHook ( onRestart : ( ) => Promise < void > | void ) : ( ) => Promise < boolean > {
62+ let isFirstIteration = true ;
63+ return async ( ) => {
64+ if ( isFirstIteration ) {
65+ isFirstIteration = false ;
66+ return false ;
67+ }
68+ await onRestart ( ) ;
69+ return true ;
70+ } ;
71+ }
72+
5273async function waitForGatewayPortReady ( host : string , port : number ) : Promise < boolean > {
5374 return await new Promise < boolean > ( ( resolve ) => {
5475 const socket = net . createConnection ( { host, port } ) ;
@@ -114,7 +135,9 @@ export async function runGatewayLoop(params: {
114135 cleanupSignals ( ) ;
115136 params . runtime . exit ( code ) ;
116137 } ;
117- const writeStabilityBundle = ( reason : string , error ?: unknown ) => {
138+ const writeStabilityBundle = async ( reason : string , error ?: unknown ) => {
139+ const { writeDiagnosticStabilityBundleForFailureSync } =
140+ await loadDiagnosticStabilityBundleModule ( ) ;
118141 const result = writeDiagnosticStabilityBundleForFailureSync ( reason , error ) ;
119142 if ( "message" in result ) {
120143 gatewayLog . warn ( result . message ) ;
@@ -142,6 +165,10 @@ export async function runGatewayLoop(params: {
142165 const handleRestartAfterServerClose = async ( restartReason ?: string ) => {
143166 const hadLock = await releaseLockIfHeld ( ) ;
144167 const isUpdateRestart = restartReason === "update.run" ;
168+ const { respawnGatewayProcessForUpdate, restartGatewayProcessWithFreshPid } =
169+ await loadProcessRespawnModule ( ) ;
170+ const { detectRespawnSupervisor } = await loadSupervisorMarkersModule ( ) ;
171+ const { markUpdateRestartSentinelFailure } = await loadRestartSentinelModule ( ) ;
145172
146173 if ( isUpdateRestart ) {
147174 const respawn = respawnGatewayProcessForUpdate ( ) ;
@@ -228,7 +255,7 @@ export async function runGatewayLoop(params: {
228255 return ;
229256 }
230257 if ( respawn . mode === "failed" ) {
231- writeStabilityBundle ( "gateway.restart_respawn_failed" ) ;
258+ await writeStabilityBundle ( "gateway.restart_respawn_failed" ) ;
232259 gatewayLog . warn (
233260 `full process restart failed (${ respawn . detail ?? "unknown error" } ); falling back to in-process restart` ,
234261 ) ;
@@ -250,8 +277,9 @@ export async function runGatewayLoop(params: {
250277
251278 const SUPERVISOR_STOP_TIMEOUT_MS = 30_000 ;
252279 const SHUTDOWN_TIMEOUT_MS = SUPERVISOR_STOP_TIMEOUT_MS - 5_000 ;
253- const resolveRestartDrainTimeoutMs = ( ) : RestartDrainTimeoutMs => {
280+ const resolveRestartDrainTimeoutMs = async ( ) : Promise < RestartDrainTimeoutMs > => {
254281 try {
282+ const { getRuntimeConfig } = await loadRuntimeConfigModule ( ) ;
255283 const timeoutMs = getRuntimeConfig ( ) . gateway ?. reload ?. deferralTimeoutMs ;
256284 return typeof timeoutMs === "number" && Number . isFinite ( timeoutMs ) && timeoutMs > 0
257285 ? timeoutMs
@@ -268,7 +296,6 @@ export async function runGatewayLoop(params: {
268296 }
269297 shuttingDown = true ;
270298 const isRestart = action === "restart" ;
271- const restartDrainTimeoutMs = isRestart ? resolveRestartDrainTimeoutMs ( ) : 0 ;
272299 gatewayLog . info ( `received ${ signal } ; ${ isRestart ? "restarting" : "shutting down" } ` ) ;
273300
274301 let forceExitTimer : ReturnType < typeof setTimeout > | null = null ;
@@ -278,13 +305,18 @@ export async function runGatewayLoop(params: {
278305 }
279306 forceExitTimer = setTimeout ( ( ) => {
280307 gatewayLog . error ( "shutdown timed out; exiting without full cleanup" ) ;
281- writeStabilityBundle (
282- isRestart ? "gateway.restart_shutdown_timeout" : "gateway.stop_shutdown_timeout" ,
283- ) ;
284- // Keep the in-process watchdog below the supervisor stop budget so this
285- // path wins before launchd/systemd escalates to a hard kill. Exit
286- // non-zero on any timeout so supervised installs restart cleanly.
287- exitProcess ( 1 ) ;
308+ void ( async ( ) => {
309+ try {
310+ await writeStabilityBundle (
311+ isRestart ? "gateway.restart_shutdown_timeout" : "gateway.stop_shutdown_timeout" ,
312+ ) ;
313+ } finally {
314+ // Keep the in-process watchdog below the supervisor stop budget so this
315+ // path wins before launchd/systemd escalates to a hard kill. Exit
316+ // non-zero on any timeout so supervised installs restart cleanly.
317+ exitProcess ( 1 ) ;
318+ }
319+ } ) ( ) ;
288320 } , forceExitMs ) ;
289321 } ;
290322 const clearForceExitTimer = ( ) => {
@@ -295,35 +327,45 @@ export async function runGatewayLoop(params: {
295327 forceExitTimer = null ;
296328 } ;
297329
298- if ( ! isRestart ) {
299- armForceExitTimer ( SHUTDOWN_TIMEOUT_MS ) ;
300- } else if ( restartDrainTimeoutMs !== undefined ) {
301- // Allow extra time for draining active turns on explicitly capped restarts.
302- armForceExitTimer ( restartDrainTimeoutMs + SHUTDOWN_TIMEOUT_MS ) ;
303- }
304-
305- const formatRestartDrainBudget = ( ) =>
306- restartDrainTimeoutMs === undefined
307- ? "without a timeout"
308- : `with timeout ${ restartDrainTimeoutMs } ms` ;
309- const createStillPendingDrainLogger = ( ) =>
310- setInterval ( ( ) => {
311- gatewayLog . warn (
312- `still draining ${ getActiveTaskCount ( ) } active task(s), ${ getActiveEmbeddedRunCount ( ) } active embedded run(s), and ${ getActiveBundledRuntimeDepsInstallCount ( ) } runtime deps install(s) before restart` ,
313- ) ;
314- } , RESTART_DRAIN_STILL_PENDING_WARN_MS ) ;
315-
316- const armCloseForceExitTimerForIndefiniteRestart = ( ) => {
317- if ( isRestart && restartDrainTimeoutMs === undefined ) {
330+ void ( async ( ) => {
331+ const restartDrainTimeoutMs = isRestart ? await resolveRestartDrainTimeoutMs ( ) : 0 ;
332+ if ( ! isRestart ) {
318333 armForceExitTimer ( SHUTDOWN_TIMEOUT_MS ) ;
334+ } else if ( restartDrainTimeoutMs !== undefined ) {
335+ // Allow extra time for draining active turns on explicitly capped restarts.
336+ armForceExitTimer ( restartDrainTimeoutMs + SHUTDOWN_TIMEOUT_MS ) ;
319337 }
320- } ;
321338
322- void ( async ( ) => {
339+ const formatRestartDrainBudget = ( ) =>
340+ restartDrainTimeoutMs === undefined
341+ ? "without a timeout"
342+ : `with timeout ${ restartDrainTimeoutMs } ms` ;
343+ const armCloseForceExitTimerForIndefiniteRestart = ( ) => {
344+ if ( isRestart && restartDrainTimeoutMs === undefined ) {
345+ armForceExitTimer ( SHUTDOWN_TIMEOUT_MS ) ;
346+ }
347+ } ;
348+
323349 try {
324350 // On restart, wait for in-flight agent turns to finish before
325351 // tearing down the server so buffered messages are delivered.
326352 if ( isRestart ) {
353+ const [
354+ { abortEmbeddedPiRun, getActiveEmbeddedRunCount, waitForActiveEmbeddedRuns } ,
355+ { getActiveBundledRuntimeDepsInstallCount, waitForBundledRuntimeDepsInstallIdle } ,
356+ { getActiveTaskCount, markGatewayDraining, waitForActiveTasks } ,
357+ ] = await Promise . all ( [
358+ loadEmbeddedRunsModule ( ) ,
359+ loadBundledRuntimeDepsActivityModule ( ) ,
360+ loadCommandQueueModule ( ) ,
361+ ] ) ;
362+ const createStillPendingDrainLogger = ( ) =>
363+ setInterval ( ( ) => {
364+ gatewayLog . warn (
365+ `still draining ${ getActiveTaskCount ( ) } active task(s), ${ getActiveEmbeddedRunCount ( ) } active embedded run(s), and ${ getActiveBundledRuntimeDepsInstallCount ( ) } runtime deps install(s) before restart` ,
366+ ) ;
367+ } , RESTART_DRAIN_STILL_PENDING_WARN_MS ) ;
368+
327369 // Reject new enqueues immediately during the drain window so
328370 // sessions get an explicit restart error instead of silent task loss.
329371 markGatewayDraining ( ) ;
@@ -385,48 +427,69 @@ export async function runGatewayLoop(params: {
385427
386428 const onSigterm = ( ) => {
387429 gatewayLog . info ( "signal SIGTERM received" ) ;
388- request ( consumeGatewayRestartIntentSync ( ) ? "restart" : "stop" , "SIGTERM" ) ;
430+ void ( async ( ) => {
431+ const { consumeGatewayRestartIntentSync } = await loadRestartModule ( ) ;
432+ request ( consumeGatewayRestartIntentSync ( ) ? "restart" : "stop" , "SIGTERM" ) ;
433+ } ) ( ) ;
389434 } ;
390435 const onSigint = ( ) => {
391436 gatewayLog . info ( "signal SIGINT received" ) ;
392437 request ( "stop" , "SIGINT" ) ;
393438 } ;
394439 const onSigusr1 = ( ) => {
395440 gatewayLog . info ( "signal SIGUSR1 received" ) ;
396- const authorized = consumeGatewaySigusr1RestartAuthorization ( ) ;
397- if ( ! authorized ) {
398- if ( ! isGatewaySigusr1RestartExternallyAllowed ( ) ) {
399- gatewayLog . warn (
400- "SIGUSR1 restart ignored (not authorized; commands.restart=false or use gateway tool)." ,
401- ) ;
402- return ;
403- }
404- if ( shuttingDown ) {
405- gatewayLog . info ( "received SIGUSR1 during shutdown; ignoring" ) ;
441+ void ( async ( ) => {
442+ const {
443+ consumeGatewaySigusr1RestartAuthorization,
444+ isGatewaySigusr1RestartExternallyAllowed,
445+ markGatewaySigusr1RestartHandled,
446+ peekGatewaySigusr1RestartReason,
447+ scheduleGatewaySigusr1Restart,
448+ } = await loadRestartModule ( ) ;
449+ const authorized = consumeGatewaySigusr1RestartAuthorization ( ) ;
450+ if ( ! authorized ) {
451+ if ( ! isGatewaySigusr1RestartExternallyAllowed ( ) ) {
452+ gatewayLog . warn (
453+ "SIGUSR1 restart ignored (not authorized; commands.restart=false or use gateway tool)." ,
454+ ) ;
455+ return ;
456+ }
457+ if ( shuttingDown ) {
458+ gatewayLog . info ( "received SIGUSR1 during shutdown; ignoring" ) ;
459+ return ;
460+ }
461+ // External SIGUSR1 requests should still reuse the in-process restart
462+ // scheduler so idle drain and restart coalescing stay consistent.
463+ scheduleGatewaySigusr1Restart ( { delayMs : 0 , reason : "SIGUSR1" } ) ;
406464 return ;
407465 }
408- // External SIGUSR1 requests should still reuse the in-process restart
409- // scheduler so idle drain and restart coalescing stay consistent.
410- scheduleGatewaySigusr1Restart ( { delayMs : 0 , reason : "SIGUSR1" } ) ;
411- return ;
412- }
413- const restartReason = peekGatewaySigusr1RestartReason ( ) ;
414- markGatewaySigusr1RestartHandled ( ) ;
415- request ( "restart" , "SIGUSR1" , restartReason ) ;
466+ const restartReason = peekGatewaySigusr1RestartReason ( ) ;
467+ markGatewaySigusr1RestartHandled ( ) ;
468+ request ( "restart" , "SIGUSR1" , restartReason ) ;
469+ } ) ( ) ;
416470 } ;
417471
418472 process . on ( "SIGTERM" , onSigterm ) ;
419473 process . on ( "SIGINT" , onSigint ) ;
420474 process . on ( "SIGUSR1" , onSigusr1 ) ;
421475
422476 try {
423- const onIteration = createRestartIterationHook ( ( ) => {
477+ const onIteration = createRestartIterationHook ( async ( ) => {
424478 // After an in-process restart (SIGUSR1), reset command-queue lane state.
425479 // Interrupted tasks from the previous lifecycle may have left `active`
426480 // counts elevated (their finally blocks never ran), permanently blocking
427481 // new work from draining. The same boundary also discards stale restart
428482 // deferral timers and reloads the task registry from durable state so
429483 // cancelled/completed work is not kept alive by old in-memory maps.
484+ const [
485+ { resetAllLanes } ,
486+ { resetGatewayRestartStateForInProcessRestart } ,
487+ { reloadTaskRegistryFromStore } ,
488+ ] = await Promise . all ( [
489+ loadCommandQueueModule ( ) ,
490+ loadRestartModule ( ) ,
491+ loadRuntimeInternalModule ( ) ,
492+ ] ) ;
430493 resetAllLanes ( ) ;
431494 resetGatewayRestartStateForInProcessRestart ( ) ;
432495 reloadTaskRegistryFromStore ( ) ;
@@ -436,7 +499,7 @@ export async function runGatewayLoop(params: {
436499 // SIGTERM/SIGINT still exit after a graceful shutdown.
437500 let isFirstStart = true ;
438501 for ( ; ; ) {
439- onIteration ( ) ;
502+ await onIteration ( ) ;
440503 try {
441504 server = await params . start ( { startupStartedAt } ) ;
442505 isFirstStart = false ;
@@ -456,7 +519,7 @@ export async function runGatewayLoop(params: {
456519 await releaseLockIfHeld ( ) ;
457520 const errMsg = formatErrorMessage ( err ) ;
458521 const errStack = err instanceof Error && err . stack ? `\n${ err . stack } ` : "" ;
459- writeStabilityBundle ( "gateway.restart_startup_failed" , err ) ;
522+ await writeStabilityBundle ( "gateway.restart_startup_failed" , err ) ;
460523 gatewayLog . error (
461524 `gateway startup failed: ${ errMsg } . ` +
462525 `Process will stay alive; fix the issue and restart.${ errStack } ` ,
0 commit comments