From 4ca2086cd460ccb495cfe2b195658eb5117bf4d0 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Thu, 5 Mar 2026 22:10:38 -0800 Subject: [PATCH 1/2] dumb init --- packages/coding-agent/src/main.ts | 190 ++++++++++++++---- .../coding-agent/src/modes/daemon-mode.ts | 54 +++-- packages/coding-agent/src/modes/index.ts | 2 +- 3 files changed, 187 insertions(+), 59 deletions(-) diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 735cad15..4600a96d 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -56,6 +56,14 @@ async function readPipedStdin(): Promise { }); } +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const GATEWAY_RESTART_DELAY_MS = 2000; +const GATEWAY_MIN_RUNTIME_MS = 10000; +const GATEWAY_MAX_CONSECUTIVE_FAILURES = 10; + function reportSettingsErrors(settingsManager: SettingsManager, context: string): void { const errors = settingsManager.drainErrors(); for (const { scope, error } of errors) { @@ -744,6 +752,148 @@ export async function main(args: string[]) { authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey); } + if (isGatewayCommand) { + const gatewayLoaderOptions = { + additionalExtensionPaths: firstPass.extensions, + additionalSkillPaths: firstPass.skills, + additionalPromptTemplatePaths: firstPass.promptTemplates, + additionalThemePaths: firstPass.themes, + noExtensions: firstPass.noExtensions, + noSkills: firstPass.noSkills, + noPromptTemplates: firstPass.noPromptTemplates, + noThemes: firstPass.noThemes, + systemPrompt: firstPass.systemPrompt, + appendSystemPrompt: firstPass.appendSystemPrompt, + }; + const gatewaySessionRoot = join(agentDir, "gateway-sessions"); + let consecutiveFailures = 0; + let primarySessionFile = sessionManager?.getSessionFile(); + const persistPrimarySession = sessionManager ? sessionManager.isPersisted() : !parsed.noSession; + + const createPrimarySessionManager = (): SessionManager => { + if (!persistPrimarySession) { + return SessionManager.inMemory(cwd); + } + if (primarySessionFile) { + return SessionManager.open(primarySessionFile, parsed.sessionDir); + } + return SessionManager.create(cwd, parsed.sessionDir); + }; + + const createGatewaySession = async (sessionManagerForRun: SessionManager) => { + const gatewayResourceLoader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager, + ...gatewayLoaderOptions, + }); + await gatewayResourceLoader.reload(); + + const result = await createAgentSession({ + ...sessionOptions, + authStorage, + modelRegistry, + settingsManager, + resourceLoader: gatewayResourceLoader, + sessionManager: sessionManagerForRun, + }); + + primarySessionFile = result.session.sessionManager.getSessionFile(); + return result; + }; + + while (true) { + const primarySessionManager = createPrimarySessionManager(); + const { session, modelFallbackMessage } = await createGatewaySession(primarySessionManager); + + if (!session.model) { + console.error(chalk.red("No models available.")); + console.error(chalk.yellow("\nSet an API key environment variable:")); + console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc."); + console.error(chalk.yellow(`\nOr create ${getModelsPath()}`)); + if (modelFallbackMessage) { + console.error(chalk.dim(modelFallbackMessage)); + } + process.exit(1); + } + + if (cliThinkingOverride) { + let effectiveThinking = session.thinkingLevel; + if (!session.model.reasoning) { + effectiveThinking = "off"; + } else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) { + effectiveThinking = "high"; + } + if (effectiveThinking !== session.thinkingLevel) { + session.setThinkingLevel(effectiveThinking); + } + } + + const daemonOptions: DaemonModeOptions = { + initialMessage, + initialImages, + messages: parsed.messages, + gateway: settingsManager.getGatewaySettings(), + createSession: async (sessionKey) => { + const gatewayResourceLoader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager, + ...gatewayLoaderOptions, + }); + await gatewayResourceLoader.reload(); + const gatewaySessionOptions: CreateAgentSessionOptions = { + ...sessionOptions, + authStorage, + modelRegistry, + settingsManager, + resourceLoader: gatewayResourceLoader, + sessionManager: createGatewaySessionManager(cwd, sessionKey, gatewaySessionRoot), + }; + const { session: gatewaySession } = await createAgentSession(gatewaySessionOptions); + return gatewaySession; + }, + }; + + const startedAt = Date.now(); + try { + const result = await runDaemonMode(session, daemonOptions); + if (result.reason === "shutdown") { + stopThemeWatcher(); + process.exit(0); + } + } catch (error) { + const message = error instanceof Error ? error.stack || error.message : String(error); + console.error(`[pi-gateway] daemon crashed: ${message}`); + try { + session.dispose(); + } catch { + // Ignore disposal errors during crash handling. + } + } + + const runtimeMs = Date.now() - startedAt; + if (runtimeMs < GATEWAY_MIN_RUNTIME_MS) { + consecutiveFailures += 1; + console.error( + `[pi-gateway] exited quickly (${runtimeMs}ms), failure ${consecutiveFailures}/${GATEWAY_MAX_CONSECUTIVE_FAILURES}`, + ); + if (consecutiveFailures >= GATEWAY_MAX_CONSECUTIVE_FAILURES) { + console.error("[pi-gateway] crash loop detected, exiting"); + process.exit(1); + } + } else { + consecutiveFailures = 0; + console.error(`[pi-gateway] exited after ${runtimeMs}ms, restarting`); + } + + if (GATEWAY_RESTART_DELAY_MS > 0) { + console.error(`[pi-gateway] restarting in ${GATEWAY_RESTART_DELAY_MS}ms`); + await sleep(GATEWAY_RESTART_DELAY_MS); + } + } + } + const { session, modelFallbackMessage } = await createAgentSession(sessionOptions); if (!isInteractive && !session.model) { @@ -792,46 +942,6 @@ export async function main(args: string[]) { verbose: parsed.verbose, }); await mode.run(); - } else if (isGatewayCommand) { - const gatewayLoaderOptions = { - additionalExtensionPaths: firstPass.extensions, - additionalSkillPaths: firstPass.skills, - additionalPromptTemplatePaths: firstPass.promptTemplates, - additionalThemePaths: firstPass.themes, - noExtensions: firstPass.noExtensions, - noSkills: firstPass.noSkills, - noPromptTemplates: firstPass.noPromptTemplates, - noThemes: firstPass.noThemes, - systemPrompt: firstPass.systemPrompt, - appendSystemPrompt: firstPass.appendSystemPrompt, - }; - const gatewaySessionRoot = join(agentDir, "gateway-sessions"); - const daemonOptions: DaemonModeOptions = { - initialMessage, - initialImages, - messages: parsed.messages, - gateway: settingsManager.getGatewaySettings(), - createSession: async (sessionKey) => { - const gatewayResourceLoader = new DefaultResourceLoader({ - cwd, - agentDir, - settingsManager, - ...gatewayLoaderOptions, - }); - await gatewayResourceLoader.reload(); - const gatewaySessionOptions: CreateAgentSessionOptions = { - ...sessionOptions, - authStorage, - modelRegistry, - settingsManager, - resourceLoader: gatewayResourceLoader, - sessionManager: createGatewaySessionManager(cwd, sessionKey, gatewaySessionRoot), - }; - const { session: gatewaySession } = await createAgentSession(gatewaySessionOptions); - return gatewaySession; - }, - }; - await runDaemonMode(session, daemonOptions); } else { await runPrintMode(session, { mode, diff --git a/packages/coding-agent/src/modes/daemon-mode.ts b/packages/coding-agent/src/modes/daemon-mode.ts index 2829a5d5..73c03358 100644 --- a/packages/coding-agent/src/modes/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon-mode.ts @@ -27,6 +27,10 @@ export interface DaemonModeOptions { gateway: GatewaySettings; } +export interface DaemonModeResult { + reason: "shutdown"; +} + function createCommandContextActions(session: AgentSession) { return { waitForIdle: () => session.agent.waitForIdle(), @@ -70,11 +74,11 @@ function createCommandContextActions(session: AgentSession) { * Run in daemon mode. * Stays alive indefinitely unless stopped by signal or extension trigger. */ -export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise { +export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise { const { initialMessage, initialImages, messages = [] } = options; let isShuttingDown = false; - let resolveReady: () => void = () => {}; - const ready = new Promise((resolve) => { + let resolveReady: (result: DaemonModeResult) => void = () => {}; + const ready = new Promise((resolve) => { resolveReady = resolve; }); const gatewayBind = process.env.PI_GATEWAY_BIND ?? options.gateway.bind ?? "127.0.0.1"; @@ -118,7 +122,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp } session.dispose(); - resolveReady(); + resolveReady({ reason: "shutdown" }); }; const handleShutdownSignal = (signal: NodeJS.Signals) => { @@ -126,18 +130,22 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp console.error( `[pi-gateway] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`, ); - process.exit(1); + resolveReady({ reason: "shutdown" }); }); }; - - process.once("SIGINT", () => handleShutdownSignal("SIGINT")); - process.once("SIGTERM", () => handleShutdownSignal("SIGTERM")); - process.once("SIGQUIT", () => handleShutdownSignal("SIGQUIT")); - process.once("SIGHUP", () => handleShutdownSignal("SIGHUP")); - - process.on("unhandledRejection", (error) => { + const sigintHandler = () => handleShutdownSignal("SIGINT"); + const sigtermHandler = () => handleShutdownSignal("SIGTERM"); + const sigquitHandler = () => handleShutdownSignal("SIGQUIT"); + const sighupHandler = () => handleShutdownSignal("SIGHUP"); + const unhandledRejectionHandler = (error: unknown) => { console.error(`[pi-gateway] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`); - }); + }; + + process.once("SIGINT", sigintHandler); + process.once("SIGTERM", sigtermHandler); + process.once("SIGQUIT", sigquitHandler); + process.once("SIGHUP", sighupHandler); + process.on("unhandledRejection", unhandledRejectionHandler); await session.bindExtensions({ commandContextActions: createCommandContextActions(session), @@ -146,7 +154,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp console.error( `[pi-gateway] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`, ); - process.exit(1); + resolveReady({ reason: "shutdown" }); }); }, onError: (err) => { @@ -178,9 +186,19 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp const keepAlive = setInterval(() => { // Intentionally keep the daemon event loop active. }, 1000); - ready.finally(() => { + + const cleanup = () => { clearInterval(keepAlive); - }); - await ready; - process.exit(0); + process.removeListener("SIGINT", sigintHandler); + process.removeListener("SIGTERM", sigtermHandler); + process.removeListener("SIGQUIT", sigquitHandler); + process.removeListener("SIGHUP", sighupHandler); + process.removeListener("unhandledRejection", unhandledRejectionHandler); + }; + + try { + return await ready; + } finally { + cleanup(); + } } diff --git a/packages/coding-agent/src/modes/index.ts b/packages/coding-agent/src/modes/index.ts index a2237403..dfd5421f 100644 --- a/packages/coding-agent/src/modes/index.ts +++ b/packages/coding-agent/src/modes/index.ts @@ -2,7 +2,7 @@ * Run modes for the coding agent. */ -export { type DaemonModeOptions, runDaemonMode } from "./daemon-mode.js"; +export { type DaemonModeOptions, type DaemonModeResult, runDaemonMode } from "./daemon-mode.js"; export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js"; export { type PrintModeOptions, runPrintMode } from "./print-mode.js"; export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js"; From fcb434bd57514e58fb97a9c4b18971ded945a8ce Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Thu, 5 Mar 2026 22:24:46 -0800 Subject: [PATCH 2/2] fix --- packages/coding-agent/src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 4600a96d..5e4d8428 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -752,6 +752,8 @@ export async function main(args: string[]) { authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey); } + const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel; + if (isGatewayCommand) { const gatewayLoaderOptions = { additionalExtensionPaths: firstPass.extensions, @@ -906,7 +908,6 @@ export async function main(args: string[]) { // Clamp thinking level to model capabilities for CLI-provided thinking levels. // This covers both --thinking and --model :. - const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel; if (session.model && cliThinkingOverride) { let effectiveThinking = session.thinkingLevel; if (!session.model.reasoning) {