dumb init

This commit is contained in:
Harivansh Rathi 2026-03-05 22:10:38 -08:00
parent 0973c1cbc5
commit 4ca2086cd4
3 changed files with 187 additions and 59 deletions

View file

@ -56,6 +56,14 @@ async function readPipedStdin(): Promise<string | undefined> {
}); });
} }
function sleep(ms: number): Promise<void> {
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 { function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
const errors = settingsManager.drainErrors(); const errors = settingsManager.drainErrors();
for (const { scope, error } of errors) { for (const { scope, error } of errors) {
@ -744,6 +752,148 @@ export async function main(args: string[]) {
authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey); 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); const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
if (!isInteractive && !session.model) { if (!isInteractive && !session.model) {
@ -792,46 +942,6 @@ export async function main(args: string[]) {
verbose: parsed.verbose, verbose: parsed.verbose,
}); });
await mode.run(); 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 { } else {
await runPrintMode(session, { await runPrintMode(session, {
mode, mode,

View file

@ -27,6 +27,10 @@ export interface DaemonModeOptions {
gateway: GatewaySettings; gateway: GatewaySettings;
} }
export interface DaemonModeResult {
reason: "shutdown";
}
function createCommandContextActions(session: AgentSession) { function createCommandContextActions(session: AgentSession) {
return { return {
waitForIdle: () => session.agent.waitForIdle(), waitForIdle: () => session.agent.waitForIdle(),
@ -70,11 +74,11 @@ function createCommandContextActions(session: AgentSession) {
* Run in daemon mode. * Run in daemon mode.
* Stays alive indefinitely unless stopped by signal or extension trigger. * Stays alive indefinitely unless stopped by signal or extension trigger.
*/ */
export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise<never> { export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise<DaemonModeResult> {
const { initialMessage, initialImages, messages = [] } = options; const { initialMessage, initialImages, messages = [] } = options;
let isShuttingDown = false; let isShuttingDown = false;
let resolveReady: () => void = () => {}; let resolveReady: (result: DaemonModeResult) => void = () => {};
const ready = new Promise<void>((resolve) => { const ready = new Promise<DaemonModeResult>((resolve) => {
resolveReady = resolve; resolveReady = resolve;
}); });
const gatewayBind = process.env.PI_GATEWAY_BIND ?? options.gateway.bind ?? "127.0.0.1"; 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(); session.dispose();
resolveReady(); resolveReady({ reason: "shutdown" });
}; };
const handleShutdownSignal = (signal: NodeJS.Signals) => { const handleShutdownSignal = (signal: NodeJS.Signals) => {
@ -126,18 +130,22 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
console.error( console.error(
`[pi-gateway] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`, `[pi-gateway] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`,
); );
process.exit(1); resolveReady({ reason: "shutdown" });
}); });
}; };
const sigintHandler = () => handleShutdownSignal("SIGINT");
process.once("SIGINT", () => handleShutdownSignal("SIGINT")); const sigtermHandler = () => handleShutdownSignal("SIGTERM");
process.once("SIGTERM", () => handleShutdownSignal("SIGTERM")); const sigquitHandler = () => handleShutdownSignal("SIGQUIT");
process.once("SIGQUIT", () => handleShutdownSignal("SIGQUIT")); const sighupHandler = () => handleShutdownSignal("SIGHUP");
process.once("SIGHUP", () => handleShutdownSignal("SIGHUP")); const unhandledRejectionHandler = (error: unknown) => {
process.on("unhandledRejection", (error) => {
console.error(`[pi-gateway] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`); 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({ await session.bindExtensions({
commandContextActions: createCommandContextActions(session), commandContextActions: createCommandContextActions(session),
@ -146,7 +154,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
console.error( console.error(
`[pi-gateway] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`, `[pi-gateway] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`,
); );
process.exit(1); resolveReady({ reason: "shutdown" });
}); });
}, },
onError: (err) => { onError: (err) => {
@ -178,9 +186,19 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
const keepAlive = setInterval(() => { const keepAlive = setInterval(() => {
// Intentionally keep the daemon event loop active. // Intentionally keep the daemon event loop active.
}, 1000); }, 1000);
ready.finally(() => {
const cleanup = () => {
clearInterval(keepAlive); clearInterval(keepAlive);
}); process.removeListener("SIGINT", sigintHandler);
await ready; process.removeListener("SIGTERM", sigtermHandler);
process.exit(0); process.removeListener("SIGQUIT", sigquitHandler);
process.removeListener("SIGHUP", sighupHandler);
process.removeListener("unhandledRejection", unhandledRejectionHandler);
};
try {
return await ready;
} finally {
cleanup();
}
} }

View file

@ -2,7 +2,7 @@
* Run modes for the coding agent. * 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 { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js";
export { type PrintModeOptions, runPrintMode } from "./print-mode.js"; export { type PrintModeOptions, runPrintMode } from "./print-mode.js";
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js"; export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js";