Unify TUI runtime commands with gateway and harden gateway restart

This commit is contained in:
William Valentin
2026-02-24 13:14:53 -08:00
parent db2f697741
commit 37be391a40
24 changed files with 1253 additions and 120 deletions
+71 -11
View File
@@ -24,7 +24,7 @@ import { assembleSystemPrompt } from '../prompt/index.js';
import { join, relative, resolve, sep } from 'path';
import { homedir } from 'os';
import type { MemoryStore } from '../memory/store.js';
import type { CommandRegistry } from '../commands/index.js';
import type { CommandRegistry, RuntimeBackendMode } from '../commands/index.js';
import type { ComponentRegistry } from '../intents/index.js';
import type { RoutingPolicy } from '../routing/index.js';
import type { HookEngine } from '../hooks/index.js';
@@ -293,6 +293,8 @@ export interface GatewayDeps {
getChannelAgents: () => Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null;
memoryStore?: MemoryStore;
commandRegistry?: CommandRegistry;
getBackendMode?: () => RuntimeBackendMode;
setBackendMode?: (mode: RuntimeBackendMode) => void;
intentRegistry?: ComponentRegistry;
routingPolicy?: RoutingPolicy;
hookEngine?: HookEngine;
@@ -388,6 +390,8 @@ export function createGateway(deps: GatewayDeps): GatewayServer {
channelRegistry,
pairingManager,
memoryStore: deps.memoryStore,
getBackendMode: deps.getBackendMode,
setBackendMode: deps.setBackendMode,
restart: async () => {
console.log('Restart requested via gateway');
await lifecycle.shutdown();
@@ -472,25 +476,31 @@ export async function startServices(deps: {
memoryStore?: MemoryStore;
memoryDir: string;
dataDir: string;
gatewayStartRetry?: {
maxAttempts?: number;
retryDelayMs?: number;
sleep?: (ms: number) => Promise<void>;
};
}): Promise<void> {
const { config, lifecycle, channelRegistry, gateway, modelRouter, memoryStore, memoryDir, dataDir } = deps;
// Register shutdown handler for channels
lifecycle.onShutdown(async () => {
await channelRegistry.stopAll();
console.log('Channel adapters stopped');
});
// Start all channel adapters
await channelRegistry.startAll();
// Start gateway (HTTP + WS server)
lifecycle.onShutdown(async () => {
await gateway.stop();
console.log('Gateway server stopped');
});
await gateway.start();
const host = config.server.localhost ? '127.0.0.1' : '0.0.0.0';
await startGatewayWithRetry(gateway, host, config.server.port, deps.gatewayStartRetry);
// Register shutdown handler for channels
lifecycle.onShutdown(async () => {
await channelRegistry.stopAll();
console.log('Channel adapters stopped');
});
// Start all channel adapters after gateway bind succeeds.
await channelRegistry.startAll();
// Tailscale Serve
if (config.server.tailscale?.serve) {
@@ -589,3 +599,53 @@ export async function startServices(deps: {
console.log('Flynn daemon started');
}
function isAddressInUseError(error: unknown): error is NodeJS.ErrnoException {
return (
typeof error === 'object'
&& error !== null
&& 'code' in error
&& (error as NodeJS.ErrnoException).code === 'EADDRINUSE'
);
}
async function startGatewayWithRetry(
gateway: Pick<GatewayServer, 'start' | 'stop'>,
host: string,
port: number,
retry?: {
maxAttempts?: number;
retryDelayMs?: number;
sleep?: (ms: number) => Promise<void>;
},
): Promise<void> {
const maxAttempts = Math.max(1, retry?.maxAttempts ?? 10);
const retryDelayMs = Math.max(0, retry?.retryDelayMs ?? 500);
const sleep = retry?.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
await gateway.start();
return;
} catch (error) {
if (!isAddressInUseError(error)) {
throw error;
}
await gateway.stop().catch(() => {});
if (attempt === maxAttempts) {
throw new Error(
`Gateway bind failed: ${host}:${port} is already in use after ${maxAttempts} attempts. `
+ 'Another Flynn daemon or process is already listening on this port.',
);
}
console.warn(
`Gateway bind collision on ${host}:${port} (attempt ${attempt}/${maxAttempts}); `
+ `retrying in ${retryDelayMs}ms...`,
);
await sleep(retryDelayMs);
}
}
}