mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 09:01:14 +00:00
dumb init
This commit is contained in:
parent
0973c1cbc5
commit
4ca2086cd4
3 changed files with 187 additions and 59 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue