Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
64 kB · 1827 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828/** * Main entry point for the coding agent CLI. * * This file handles CLI argument parsing and translates them into * createAgentSession() options. The SDK does the heavy lifting. */
import { dirname, join, resolve } from "node:path";import { type Api, type ImageContent, type Model, modelsAreEqual } from "@earendil-works/pi-ai";import { registerBuiltinMcpOAuthProviders } from "@earendil-works/pi-ai/mcp";import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";import chalk from "chalk";import { type Args, type Mode, parseArgs } from "./cli/args.js";import { formatTopLevelHelp } from "./cli/command-registry.js";import { ensureInteractiveDaemonRunning, isDaemonSessionSummary, listActiveDaemonSessionSummaries, probeRunningDaemonSessions, StaleDaemonError, shutdownDaemonAndWait,} from "./cli/daemon-launch.js";import { confirmDaemonSessionLoss, type DaemonSessionLossCopy, pluralizeSessions } from "./cli/daemon-stop-confirm.js";import { processFileArguments } from "./cli/file-processor.js";import { buildInitialMessage } from "./cli/initial-message.js";import { listModels } from "./cli/list-models.js";import { installOwnedSessionRecoveryTracking, isOwnedSessionWorkerProcess } from "./cli/owned-session-worker.js";import { handlePublicCommand } from "./cli/public-command.js";import { looksLikeSessionPath, resolveSessionPath, SessionSelectorError, SessionSelectorNotFoundError,} from "./cli/session-resolver.js";import { APP_NAME, expandTildePath, getAgentDir, getSessionDirEnvOverride, VERSION } from "./config.js";import { type AgentExecutionMode, type AgentSessionRuntimeConfig, mergeAgentSessionRuntimeConfig, mergeAutonomousConfig,} from "./core/agent-session-config.js";import { type AgentSessionRuntime, type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime,} from "./core/agent-session-runtime.js";import { type AgentSessionRuntimeDiagnostic, type AgentSessionServices, createAgentSessionFromServices, createAgentSessionServices,} from "./core/agent-session-services.js";import { formatNoModelsAvailableMessage } from "./core/auth-guidance.js";import { AuthStorage } from "./core/auth-storage.js";import { exportFromFile } from "./core/export-html/index.js";import type { ExtensionFactory } from "./core/extensions/types.js";import { KeybindingsManager } from "./core/keybindings.js";import { installFileLogSink, setLogContext } from "./core/logging.js";import type { ModelRegistry } from "./core/model-registry.js";import { findInitialModel, resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";import { restoreStdout, takeOverStdout } from "./core/output-guard.js";import type { CreateAgentSessionOptions } from "./core/sdk.js";import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, type SessionCwdIssue,} from "./core/session-cwd.js";import { canonicalSessionPath, SessionAlreadyActiveError } from "./core/session-lease.js";import { findMostRecentSessionForCwd, getDefaultSessionDir, loadEntriesFromFile, loadSessionHeaderEntry, SessionManager,} from "./core/session-manager.js";import { SettingsManager } from "./core/settings-manager.js";import { isTelemetryEnabled } from "./core/telemetry.js";import { printTimings, resetTimings, time } from "./core/timings.js";import { runMigrations, showDeprecationWarnings } from "./migrations.js";import { isDaemonCatalogProcess, runDaemonCatalogProcess } from "./modes/daemon/daemon-catalog-process.js";import { DaemonSessionCreateError, deserializeDaemonCreateError, deserializeDaemonError,} from "./modes/daemon/daemon-errors.js";import { collectDaemonClientEnv, collectDaemonLaunchEnv, type DaemonResponse } from "./modes/daemon/daemon-protocol.js";import { DAEMON_WORKER_ACTIVE_SESSION_ID_ENV, daemonWorkerInstanceId, isDaemonWorkerProcess, requireDaemonWorkerAuthenticationToken, waitForDaemonWorkerStartupGate,} from "./modes/daemon/daemon-worker-protocol.js";import { type AgentConnection, type AgentsViewScopeKey, ClientPromptStashStore, createInteractiveModeLocalSessionHost, createInteractiveModeUiServicesFromServices, DaemonAgentConnection, DaemonCapabilityUnavailableError, DaemonClient, defaultDaemonSocketPath, InProcessAgentConnection, InteractiveMode, normalizeSocketPath, resolveAttachModelFallbackMessage, runAcpMode, runAcpModeWithConnection, runAgentsViewMode, runDaemonMode, runDaemonSupervisorMode, runPrintMode, runPrintModeWithConnection, runRpcMode, runRpcModeWithConnection, type SessionSummary,} from "./modes/index.js";import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";import { shouldRunOnboarding } from "./modes/interactive/onboarding.js";import { initTheme, preloadCodeHighlighter, stopThemeWatcher } from "./modes/interactive/theme/theme.js";import { handleConfigCommand } from "./package-manager-cli.js";import { isLocalPath } from "./utils/paths.js";import { readPipedStdin } from "./utils/piped-stdin.js";
function collectSettingsDiagnostics( settingsManager: SettingsManager, context: string,): AgentSessionRuntimeDiagnostic[] { return settingsManager.drainErrors().map(({ scope, error }) => ({ type: "warning", message: `(${context}, ${scope} settings) ${error.message}`, }));}
function reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void { for (const diagnostic of diagnostics) { const color = diagnostic.type === "error" ? chalk.red : diagnostic.type === "warning" ? chalk.yellow : chalk.dim; const prefix = diagnostic.type === "error" ? "Error: " : diagnostic.type === "warning" ? "Warning: " : ""; console.error(color(`${prefix}${diagnostic.message}`)); }}
function isTruthyEnvFlag(value: string | undefined): boolean { if (!value) return false; return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";}
export type ClientMode = AgentExecutionMode;/** Compatibility view of the CLI's internal daemon process entrypoint. */export type AppMode = ClientMode | "daemon";
export function shouldRejectNonInteractiveAttach(attachAgent: string | undefined, appMode: AppMode): boolean { return attachAgent !== undefined && appMode !== "interactive";}
export function shouldRejectNonInteractiveBareResume(resume: true | string | undefined, appMode: AppMode): boolean { return resume === true && appMode !== "interactive";}
function resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode { if (parsed.mode === "daemon") { return "daemon"; } if (parsed.mode === "rpc") { return "rpc"; } if (parsed.mode === "acp") { return "acp"; } if (parsed.mode === "json") { return "json"; } if (parsed.print || !stdinIsTTY) { return "print"; } return "interactive";}
function toPrintOutputMode(appMode: AppMode): Exclude<Mode, "rpc" | "acp" | "daemon"> { return appMode === "json" ? "json" : "text";}
export function isClientOwnedDaemonSession(appMode: AppMode, noSession?: boolean): boolean { return appMode !== "acp" || noSession === true;}
// `prime-agent agents` opens the agents view directly.export function parseAgentsViewCommand(args: string[]): { explicitAgentsView: boolean; args: string[] } { if (args[0] === "agents") { return { explicitAgentsView: true, args: args.slice(1) }; } return { explicitAgentsView: false, args };}
export interface DaemonClientStartupDecision { appMode: AppMode; startupBenchmark: boolean; noSession?: boolean; help?: boolean; listModels?: string | true;}
export type InteractiveDaemonStartupDecision = DaemonClientStartupDecision;
/** Retained for callers that only classify persistent interactive startup. */export function shouldUseDaemonInteractive(options: DaemonClientStartupDecision): boolean { return ( options.appMode === "interactive" && !options.startupBenchmark && !options.noSession && options.listModels === undefined );}
export function shouldUseDaemonClient(options: DaemonClientStartupDecision): boolean { return ( options.appMode !== "daemon" && !options.startupBenchmark && !options.help && options.listModels === undefined );}
export function shouldUseDaemonClientRuntime( options: DaemonClientStartupDecision & { ownedSessionWorker?: boolean; hasProcessLocalExtensionFactories?: boolean; },): boolean { return shouldUseDaemonClient(options) && !options.ownedSessionWorker && !options.hasProcessLocalExtensionFactories;}
export function shouldEnsureInteractiveDaemonForStartup( useDaemonInteractive: boolean, attachAgent: string | undefined,): boolean { return useDaemonInteractive && attachAgent === undefined;}
export interface AgentsViewStartupDecision { useDaemonInteractive: boolean; needsOnboarding: boolean; explicitAgentsView?: boolean; resume?: true | string; continue?: boolean; fork?: string;}
export function shouldOpenAgentsViewForDaemonInteractive(options: AgentsViewStartupDecision): boolean { const bareResume = options.resume === true; const requestsAgentsView = bareResume || (options.explicitAgentsView && !options.needsOnboarding); return ( options.useDaemonInteractive && // A selector, continuation, or fork must open its target directly rather than the agents view. !!requestsAgentsView && typeof options.resume !== "string" && !options.continue && !options.fork );}
export interface DaemonInteractiveSessionManagerDecision { resume?: true | string; continue?: boolean; fork?: string; hasActiveDaemonSession?: boolean;}
export function shouldUseEphemeralSessionManagerForDaemonInteractive( options: DaemonInteractiveSessionManagerDecision,): boolean { return ( !options.hasActiveDaemonSession && (options.resume === undefined || options.resume === true) && !options.continue && !options.fork );}
export interface DaemonActiveSessionLookupDecision { useDaemonInteractive: boolean; resumeSelector?: string; explicitAttach?: boolean;}
export function shouldEnsureDaemonBeforeActiveSessionLookup(options: DaemonActiveSessionLookupDecision): boolean { return ( options.useDaemonInteractive && options.resumeSelector !== undefined && (options.explicitAttach || !looksLikeSessionPath(options.resumeSelector)) );}
interface ActiveDaemonSessionSummaryLookupOptions { fallbackOnError?: boolean;}
async function findActiveDaemonSessionSummaryForInteractiveStartup( socketPath: string, selector: string, options: ActiveDaemonSessionSummaryLookupOptions = {},): Promise<SessionSummary | undefined> { try { return await findActiveDaemonSessionSummary(socketPath, selector); } catch (error) { if (options.fallbackOnError === false) { throw error; } return undefined; }}
async function prepareInitialMessage( parsed: Args, autoResizeImages: boolean, stdinContent?: string,): Promise<{ initialMessage?: string; initialImages?: ImageContent[];}> { if (parsed.fileArgs.length === 0) { return buildInitialMessage({ parsed, stdinContent }); }
const { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages }); return buildInitialMessage({ parsed, fileText: text, fileImages: images, stdinContent, });}
// Only busy sessions (streaming, compacting, or pending messages) lose work;// idle loaded sessions reload from disk on the fresh daemon.const STARTUP_SESSION_LOSS_COPY: DaemonSessionLossCopy = { busyDetail(count) { const { noun, pronoun } = pluralizeSessions(count); return `A background service from a different Prime Agent version is running with ${count} busy ${noun}. Stopping it will terminate ${pronoun}.`; }, unlistableDetail: "A background service from a different Prime Agent version is running and its sessions could not be listed. Stopping it may terminate active sessions.", question: "Stop it and continue?", nonTtyHint: 'Run "prime-agent shutdown" to stop it, then retry.',};
// The promise to keep after awaiting readiness. Wrapped in an object so it// survives `await` (which would otherwise flatten a returned Promise to void).type DaemonReadyResult = { ready: Promise<void> | undefined };
// A stale-version daemon couldn't be taken over automatically (busy or stuck).// Offer to stop it (default No) and start a fresh daemon, or exit. Returns the// fresh ready promise so callers stop re-handling the original rejection.async function takeOverStaleDaemonOrExit(socketPath: string): Promise<DaemonReadyResult> { const probe = await probeRunningDaemonSessions(socketPath); const confirmed = await confirmDaemonSessionLoss(probe, { force: false, copy: STARTUP_SESSION_LOSS_COPY }); if (!confirmed) { // Non-TTY already printed the reason; at a TTY the user declined. if (process.stdin.isTTY) { console.error(chalk.dim("Cancelled.")); } process.exit(1); } if (!(await shutdownDaemonAndWait(socketPath))) { console.error( chalk.red(`Could not stop the background service on ${socketPath}. Run "prime-agent shutdown" and retry.`), ); process.exit(1); } const ready = ensureInteractiveDaemonRunning(socketPath); try { await ready; } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Could not start the background service: ${message}`)); process.exit(1); } return { ready };}
// Resolves the daemon-ready promise, returning the promise to keep (the same// one on success, or the fresh one from a stale-daemon takeover) so repeat// calls don't re-handle the original rejection.async function awaitDaemonReady(daemonReady: Promise<void> | undefined): Promise<DaemonReadyResult> { if (!daemonReady) { return { ready: daemonReady }; } try { await daemonReady; return { ready: daemonReady }; } catch (error) { if (error instanceof StaleDaemonError) { return takeOverStaleDaemonOrExit(error.socketPath); } throw error; }}
function validateForkFlags(parsed: Args): void { if (!parsed.fork) return;
const conflictingFlags = [ parsed.continue ? "--continue" : undefined, parsed.resume ? "--resume" : undefined, parsed.noSession ? "--no-session" : undefined, ].filter((flag): flag is string => flag !== undefined);
if (conflictingFlags.length > 0) { console.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(", ")}`)); process.exit(1); }}
function forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager { try { return SessionManager.forkFrom(sourcePath, cwd, sessionDir); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${message}`)); process.exit(1); }}
function getResumeSelector(parsed: Pick<Args, "resume">): string | undefined { return typeof parsed.resume === "string" ? parsed.resume : undefined;}
function readSessionManager( path: string, sessionDir?: string, cwdOverride?: string, options?: { headerOnly?: boolean },): SessionManager { const entries = (options?.headerOnly ? loadSessionHeaderEntry(path) : undefined) ?? loadEntriesFromFile(path); const header = entries.find((entry) => entry.type === "session"); const manager = SessionManager.inMemory( cwdOverride ?? header?.cwd ?? process.cwd(), sessionDir ?? dirname(resolve(path)), ); manager.setSessionFile(path, entries); return manager;}
export async function createSessionManager( parsed: Args, cwd: string, sessionDir: string | undefined, readOnly = false,): Promise<SessionManager> { const explicitCwdOverride = parsed.cwd ? cwd : undefined;
if (parsed.noSession) { return SessionManager.inMemory(); }
if (parsed.fork) { const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
switch (resolved.type) { case "path": case "local": case "global": return forkSessionOrExit(resolved.path, cwd, sessionDir); } }
const resumeSelector = getResumeSelector(parsed); if (resumeSelector) { const resolved = await resolveSessionPath(resumeSelector, cwd, sessionDir);
// A session belongs to the directory it recorded, so resuming means opening it there // (SessionManager.open takes the header cwd), wherever this shell happens to stand. // Inheriting that history *here* is the other intent, and it is spelled --fork. const openResolved = (): SessionManager => readOnly ? readSessionManager(resolved.path, sessionDir, explicitCwdOverride) : SessionManager.open(resolved.path, sessionDir, explicitCwdOverride);
switch (resolved.type) { case "path": case "local": return openResolved();
case "global": // Another project's session still resumes here; it just runs where it belongs, // so say where that is before the first tool call demonstrates it. console.log( chalk.dim( `Resuming session from ${resolved.cwd}; its tools run there. Pass --fork to continue it in this project instead.`, ), ); return openResolved(); } }
if (parsed.continue) { if (readOnly) { const dir = sessionDir ?? getDefaultSessionDir(cwd); const path = findMostRecentSessionForCwd(dir, cwd); return path ? readSessionManager(path, dir, cwd) : SessionManager.inMemory(cwd, dir); } return SessionManager.continueRecent(cwd, sessionDir); }
return readOnly ? SessionManager.inMemory(cwd, sessionDir) : SessionManager.create(cwd, sessionDir);}
function buildSessionOptions( config: AgentSessionRuntimeConfig, scopedModels: ScopedModel[], hasExistingSession: boolean, modelRegistry: ModelRegistry, settingsManager: SettingsManager,): { options: CreateAgentSessionOptions; cliThinkingFromModel: boolean; diagnostics: AgentSessionRuntimeDiagnostic[];} { const options: CreateAgentSessionOptions = {}; const diagnostics: AgentSessionRuntimeDiagnostic[] = []; let cliThinkingFromModel = false;
// Model from CLI // - supports --provider <name> --model <pattern> // - supports --model <provider>/<pattern> if (config.model) { const resolved = resolveCliModel({ cliProvider: config.provider, cliModel: config.model, modelRegistry, }); if (resolved.warning) { diagnostics.push({ type: "warning", message: resolved.warning }); } if (resolved.error) { diagnostics.push({ type: "error", message: resolved.error }); } if (resolved.model) { options.model = resolved.model; // Allow "--model <pattern>:<thinking>" as a shorthand. // Explicit --thinking still takes precedence (applied later). if (!config.thinking && resolved.thinkingLevel) { options.thinkingLevel = resolved.thinkingLevel; cliThinkingFromModel = true; } } }
if (!options.model && scopedModels.length > 0 && !hasExistingSession) { // Check if saved default is in scoped models - use it if so, otherwise first scoped model const savedProvider = settingsManager.getDefaultProvider(); const savedModelId = settingsManager.getDefaultModel(); const savedModel = savedProvider && savedModelId ? modelRegistry.find(savedProvider, savedModelId) : undefined; const savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;
if (savedInScope) { options.model = savedInScope.model; // Use thinking level from scoped model config if explicitly set if (!config.thinking && savedInScope.thinkingLevel) { options.thinkingLevel = savedInScope.thinkingLevel; } } else { options.model = scopedModels[0].model; // Use thinking level from first scoped model if explicitly set if (!config.thinking && scopedModels[0].thinkingLevel) { options.thinkingLevel = scopedModels[0].thinkingLevel; } } }
// Thinking level from CLI (takes precedence over scoped model thinking levels set above) if (config.thinking) { options.thinkingLevel = config.thinking; }
// Scoped models for Alt+M cycling // Keep thinking level undefined when not explicitly set in the model pattern. // Undefined means "inherit current session thinking level" during cycling. if (scopedModels.length > 0) { options.scopedModels = scopedModels.map((sm) => ({ model: sm.model, thinkingLevel: sm.thinkingLevel, })); }
// API key from CLI - set in authStorage // (handled by caller before createAgentSession)
// Tools if (config.noTools) { options.noTools = "all"; } else if (config.noBuiltinTools) { options.noTools = "builtin"; } if (config.tools) { options.tools = [...config.tools]; } if (config.autonomous) { options.autonomous = mergeAutonomousConfig(undefined, config.autonomous); } if (config.remoteHost) { options.remoteHost = config.remoteHost; }
return { options, cliThinkingFromModel, diagnostics };}
function resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined { return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));}
function runtimeAutonomousConfigFromArgs(parsed: Args): AgentSessionRuntimeConfig["autonomous"] { const hasAutonomousOptions = parsed.autonomous === true || parsed.autonomousGates !== undefined || parsed.autonomousGateRetries !== undefined || parsed.autonomousGateTimeoutMs !== undefined || parsed.autonomousMaxContinuations !== undefined || parsed.autonomousMaxTurns !== undefined || parsed.autonomousMaxTokens !== undefined || parsed.autonomousTimeoutMs !== undefined; if (!hasAutonomousOptions) { return undefined; } const hasGateOptions = parsed.autonomousGates !== undefined || parsed.autonomousGateRetries !== undefined || parsed.autonomousGateTimeoutMs !== undefined; return { enabled: true, maxContinuations: parsed.autonomousMaxContinuations, maxTurns: parsed.autonomousMaxTurns, maxTokens: parsed.autonomousMaxTokens, timeoutMs: parsed.autonomousTimeoutMs, gates: hasGateOptions ? { commands: parsed.autonomousGates, maxRetries: parsed.autonomousGateRetries, timeoutMs: parsed.autonomousGateTimeoutMs, } : undefined, };}
function runtimeConfigFromArgs( parsed: Args, cwd: string, agentDir: string, sessionDir: string | undefined, appMode: AppMode, telemetryDisabled?: true,): AgentSessionRuntimeConfig { return { cwd, agentDir, sessionDir, provider: parsed.provider, model: parsed.model, apiKey: parsed.apiKey, systemPrompt: parsed.systemPrompt, appendSystemPrompt: parsed.appendSystemPrompt, thinking: parsed.thinking, models: parsed.models, tools: parsed.tools, noTools: parsed.noTools, noBuiltinTools: parsed.noBuiltinTools, extensions: resolveCliPaths(cwd, parsed.extensions), noExtensions: parsed.noExtensions, skills: resolveCliPaths(cwd, parsed.skills), noSkills: parsed.noSkills, promptTemplates: resolveCliPaths(cwd, parsed.promptTemplates), noPromptTemplates: parsed.noPromptTemplates, themes: resolveCliPaths(cwd, parsed.themes), noThemes: parsed.noThemes, noContextFiles: parsed.noContextFiles, autonomous: runtimeAutonomousConfigFromArgs(parsed), extensionFlagValues: parsed.unknownFlags.size > 0 ? Object.fromEntries(parsed.unknownFlags.entries()) : undefined, executionMode: appMode === "daemon" ? undefined : appMode, telemetryDisabled, // Serialized refine for print/json/rpc: the client's appMode is NOT // "daemon" here — it's "print", "json", or "rpc". The daemon worker // receives this flag via AgentSessionRuntimeConfig and uses it // instead of its own appMode="daemon". serializedRefine: appMode !== "interactive" && appMode !== "daemon", initialGoal: parsed.goal ? { objective: parsed.goal, tokenBudget: parsed.goalTokenBudget } : undefined, remoteHost: parsed.remotePythonHost, };}
interface PreparedRuntimeServices { services: AgentSessionServices; scopedModels: ScopedModel[]; sessionOptions: CreateAgentSessionOptions; cliThinkingFromModel: boolean; diagnostics: AgentSessionRuntimeDiagnostic[]; /** Set when the extension reload was deferred; see deferResourceReload. */ resourceReload?: Promise<void>;}
export function daemonServerDefaultSessionConfig(config: AgentSessionRuntimeConfig): AgentSessionRuntimeConfig { return { ...config, initialGoal: undefined };}
export function resolveRuntimeSessionOptions( sessionOptions: CreateAgentSessionOptions, runtimeSessionOptions?: CreateAgentSessionOptions,): CreateAgentSessionOptions { return { model: runtimeSessionOptions?.model ?? sessionOptions.model, thinkingLevel: runtimeSessionOptions?.thinkingLevel ?? sessionOptions.thinkingLevel, serviceTier: runtimeSessionOptions?.serviceTier ?? sessionOptions.serviceTier, scopedModels: runtimeSessionOptions?.scopedModels ?? sessionOptions.scopedModels, tools: runtimeSessionOptions?.tools ?? sessionOptions.tools, noTools: runtimeSessionOptions?.noTools ?? sessionOptions.noTools, customTools: runtimeSessionOptions?.customTools ?? sessionOptions.customTools, initialActiveToolNames: runtimeSessionOptions?.initialActiveToolNames, allowedToolNames: runtimeSessionOptions?.allowedToolNames, includeGoals: runtimeSessionOptions?.includeGoals, includeCompactSkill: runtimeSessionOptions?.includeCompactSkill, rlmHeartbeatController: runtimeSessionOptions?.rlmHeartbeatController, agentMessageController: runtimeSessionOptions?.agentMessageController, agentObserveController: runtimeSessionOptions?.agentObserveController, agentSessionController: runtimeSessionOptions?.agentSessionController, autonomous: (runtimeSessionOptions?.rlmDepth ?? 0) > 0 ? mergeAutonomousConfig(sessionOptions.autonomous, { ...runtimeSessionOptions?.autonomous, enabled: false }) : mergeAutonomousConfig(sessionOptions.autonomous, runtimeSessionOptions?.autonomous), rlmDepth: runtimeSessionOptions?.rlmDepth, rlmMaxDepth: runtimeSessionOptions?.rlmMaxDepth, rlmSessionDir: runtimeSessionOptions?.rlmSessionDir, rlmParentNodeId: runtimeSessionOptions?.rlmParentNodeId, rlmParentAgent: runtimeSessionOptions?.rlmParentAgent, semanticParentSessionId: runtimeSessionOptions?.semanticParentSessionId, semanticSpawnedByRequestId: runtimeSessionOptions?.semanticSpawnedByRequestId, subagentRuntimeHost: runtimeSessionOptions?.subagentRuntimeHost, remoteHost: runtimeSessionOptions?.remoteHost ?? sessionOptions.remoteHost, };}
/** The production runtime factory: every daemon-hosted and runtime-hosted session is created through this path. */export function createDefaultRuntimeFactory( runtimeDefaultSessionConfig: AgentSessionRuntimeConfig, extensionFactories?: ExtensionFactory[],): CreateAgentSessionRuntimeFactory { return async ({ cwd, agentDir, sessionManager, sessionStartEvent, sessionConfig, sessionOptions: runtimeSessionOptions, }) => { const config = mergeAgentSessionRuntimeConfig(runtimeDefaultSessionConfig, sessionConfig); const prepared = await prepareRuntimeServices({ config, cwd, agentDir, sessionManager, extensionFactories, sessionOptionsOverride: runtimeSessionOptions, }); const { services, sessionOptions, diagnostics } = prepared; const resolvedSessionOptions = resolveRuntimeSessionOptions(sessionOptions, runtimeSessionOptions);
const created = await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, ...resolvedSessionOptions, // Main agents boot their kernel in the background at session creation; // subagent sessions (rlmDepth > 0) keep the lazy first-call start. prewarmIpythonKernel: true, // Read serializedRefine from the merged runtime config (passed // from the JSON/print client through AgentSessionRuntimeConfig) // so it survives the daemon worker's appMode="daemon" context. serializedRefine: config.serializedRefine ?? false, executionMode: config.executionMode, telemetryDisabled: config.telemetryDisabled, // Only seed initial goal for top-level sessions (rlmDepth 0). initialGoal: (runtimeSessionOptions?.rlmDepth ?? 0) === 0 ? config.initialGoal : undefined, }); const cliThinkingOverride = config.thinking !== undefined || prepared.cliThinkingFromModel; if (created.session.model && cliThinkingOverride) { created.session.setThinkingLevel(created.session.thinkingLevel); }
return { ...created, services, diagnostics, }; };}
async function prepareRuntimeServices(options: { config: AgentSessionRuntimeConfig; cwd: string; agentDir: string; sessionManager: SessionManager; extensionFactories?: ExtensionFactory[]; sessionOptionsOverride?: CreateAgentSessionOptions; /** * Session facts a caller already knows (a daemon attach summary). Supplying * them keeps a client that holds only the session header from parsing the * whole transcript to answer "resuming?" and "which model?". */ existingSession?: ExistingSessionFacts; /** See CreateAgentSessionServicesOptions.deferResourceReload. */ deferResourceReload?: boolean;}): Promise<PreparedRuntimeServices> { const { config, sessionManager } = options; const effectiveAgentDir = config.agentDir ?? options.agentDir; const authStorage = AuthStorage.create(join(effectiveAgentDir, "auth.json"), { usePrimeCliConfig: effectiveAgentDir === options.agentDir, }); const services = await createAgentSessionServices({ cwd: options.cwd, agentDir: effectiveAgentDir, authStorage, extensionFlagValues: new Map(Object.entries(config.extensionFlagValues ?? {})), // Subagents share the parent's Herdr pane; their own reporter would race // the parent's and a subagent quit would release the still-active pane. noBuiltinHerdrReporter: (options.sessionOptionsOverride?.rlmDepth ?? 0) > 0, telemetryDisabled: config.telemetryDisabled, // Interactive launches hold the notice back for onboarding, which marks // itself shown; every other mode discloses immediately. Deriving it from // the session config covers the daemon-hosted path too, which creates the // session the TUI actually talks to. deferTelemetryNoticeForOnboarding: config.executionMode === "interactive", resourceLoaderOptions: { additionalExtensionPaths: config.extensions, additionalSkillPaths: config.skills, additionalPromptTemplatePaths: config.promptTemplates, additionalThemePaths: config.themes, noExtensions: config.noExtensions, noSkills: config.noSkills, noPromptTemplates: config.noPromptTemplates, noThemes: config.noThemes, noContextFiles: config.noContextFiles, systemPrompt: config.systemPrompt, appendSystemPrompt: config.appendSystemPrompt, extensionFactories: options.extensionFactories, }, deferResourceReload: options.deferResourceReload, }); const { settingsManager, modelRegistry, resourceLoader } = services; const diagnostics: AgentSessionRuntimeDiagnostic[] = [ ...services.diagnostics, ...collectSettingsDiagnostics(settingsManager, "runtime creation"), ...resourceLoader.getExtensions().errors.map(({ path, error }) => ({ type: "error" as const, message: `Failed to load extension "${path}": ${error}`, })), ];
const effectiveConfig = { ...config, remoteHost: config.remoteHost ?? settingsManager.getRemotePythonHost(), }; const modelPatterns = effectiveConfig.models ?? settingsManager.getEnabledModels(); const scopedModels = modelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : []; const { options: sessionOptions, cliThinkingFromModel, diagnostics: sessionOptionDiagnostics, } = buildSessionOptions( effectiveConfig, scopedModels, options.existingSession?.hasMessages ?? sessionManager.buildSessionContext().messages.length > 0, modelRegistry, settingsManager, ); diagnostics.push(...sessionOptionDiagnostics);
const effectiveSessionModel = options.sessionOptionsOverride?.model ?? sessionOptions.model; if (config.apiKey) { if (!effectiveSessionModel) { diagnostics.push({ type: "error", message: "--api-key requires a model to be specified via --model, --provider/--model, or --models", }); } else { authStorage.setRuntimeApiKey(effectiveSessionModel.provider, config.apiKey); } }
return { services, scopedModels, sessionOptions, cliThinkingFromModel, diagnostics, resourceReload: services.resourceReload, };}
/** Session facts a caller already knows, so the transcript need not be parsed. */interface ExistingSessionFacts { hasMessages: boolean; model: { provider: string; modelId: string } | null;}
async function resolvePreparedStartupModel(options: { prepared: PreparedRuntimeServices; sessionManager: SessionManager; existingSession?: ExistingSessionFacts;}): Promise<{ model: Model<Api> | undefined; modelFallbackMessage: string | undefined }> { const { prepared, sessionManager } = options; const { modelRegistry, settingsManager } = prepared.services; const sessionContext = options.existingSession ? undefined : sessionManager.buildSessionContext(); const hasExistingSession = options.existingSession?.hasMessages ?? sessionContext!.messages.length > 0; const sessionModel = options.existingSession ? options.existingSession.model : (sessionContext!.model ?? null);
let model = prepared.sessionOptions.model; let modelFallbackMessage: string | undefined;
if (!model && hasExistingSession && sessionModel) { const restoredModel = modelRegistry.find(sessionModel.provider, sessionModel.modelId); if (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) { model = restoredModel; } if (!model) { modelFallbackMessage = `Could not restore model ${sessionModel.provider}/${sessionModel.modelId}`; } }
if (!model) { const result = await findInitialModel({ scopedModels: prepared.scopedModels, isContinuing: hasExistingSession, defaultProvider: settingsManager.getDefaultProvider(), defaultModelId: settingsManager.getDefaultModel(), defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(), modelRegistry, }); model = result.model; if (!model) { modelFallbackMessage = formatNoModelsAvailableMessage(); } else if (modelFallbackMessage) { modelFallbackMessage += `. Using ${model.provider}/${model.id}`; } }
return { model, modelFallbackMessage };}
async function promptForMissingSessionCwd( issue: SessionCwdIssue, settingsManager: SettingsManager,): Promise<string | undefined> { initTheme(settingsManager.getTheme()); setKeybindings(KeybindingsManager.create());
return new Promise((resolve) => { const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); ui.setClearOnShrink(settingsManager.getClearOnShrink());
let settled = false; const finish = (result: string | undefined) => { if (settled) { return; } settled = true; ui.stop(); resolve(result); };
const selector = new ExtensionSelectorComponent( formatMissingSessionCwdPrompt(issue), ["Continue", "Cancel"], (option) => finish(option === "Continue" ? issue.fallbackCwd : undefined), () => finish(undefined), { tui: ui }, ); ui.addChild(selector); ui.setFocus(selector); ui.start(); });}
function getDaemonSummaryActiveSessionId(summary: SessionSummary): string { return summary.activeSessionId ?? summary.id;}
function isUnknownActiveSessionError(message: string): boolean { return message.startsWith("Unknown active session:");}
/** * Unknown falls back to the saved-session path (whose create/open route retries recovery); recovering * throws typed so an explicit --attach-agent surfaces the retryable state, not "No active agent found". */export function resolveActiveSessionLookupFailure( response: Extract<DaemonResponse, { success: false }>,): Error | undefined { if (response.errorInfo?.code === "session_recovering") { return deserializeDaemonError(response); } if (isUnknownActiveSessionError(response.error)) { return undefined; } return new Error(response.error);}
async function findActiveDaemonSessionSummary( socketPath: string, selector: string,): Promise<SessionSummary | undefined> { const client = new DaemonClient(socketPath); await client.connect(250);
try { const response = await client.request({ type: "get_state", activeSessionId: selector }, 3000); if (!response.success) { const failure = resolveActiveSessionLookupFailure(response); if (failure) { throw failure; } return undefined; } if (!isDaemonSessionSummary(response.data)) { throw new Error("Daemon returned an invalid active session summary"); } return response.data; } finally { client.close(); }}
function createSessionManagerForActiveDaemonSummary(summary: SessionSummary, fallbackCwd: string): SessionManager { const cwd = summary.cwd || fallbackCwd; if (summary.sessionFile) { try { // The summary already carries the cwd, id and model, so the client only // needs the header: parsing a multi-megabyte transcript here would cost // tens of milliseconds before the first frame. return readSessionManager(summary.sessionFile, undefined, cwd, { headerOnly: true }); } catch { return SessionManager.inMemory(cwd); } } return SessionManager.inMemory(cwd);}
function getInteractiveDaemonSessionPath(parsed: Args, sessionManager: SessionManager): string | undefined { if (!parsed.resume && !parsed.continue && !parsed.fork) { return undefined; } return sessionManager.getSessionFile();}
export function findActiveDaemonSessionSummaryForSessionFile( summaries: readonly SessionSummary[], sessionPath: string,): SessionSummary | undefined { const resolvedSessionPath = canonicalSessionPath(sessionPath); return summaries.find( (summary) => summary.activeSessionId !== undefined && summary.sessionFile !== undefined && canonicalSessionPath(summary.sessionFile) === resolvedSessionPath, );}
async function createDaemonClientConnection(options: { socketPath: string; config: AgentSessionRuntimeConfig; sessionPath?: string; continueRecent?: boolean; activeSessionId?: string; clientOwned?: boolean; noSession?: boolean; supportsExtensionUi?: boolean;}): Promise<{ connection: DaemonAgentConnection; summary: SessionSummary }> { // Caller must have awaited ensureInteractiveDaemonRunning for this socket. const client = new DaemonClient(options.socketPath); await client.connect();
try { const attach = async (summary: SessionSummary) => { const connection = await DaemonAgentConnection.attach(client, getDaemonSummaryActiveSessionId(summary), { closeClientOnDispose: true, deferSessionEvents: options.config.executionMode === "interactive", sendClientEnv: true, ownedSession: options.clientOwned, ownedSessionRecoveryConfig: options.clientOwned ? options.config : undefined, supportsExtensionUi: options.supportsExtensionUi, recoverDaemon: () => ensureInteractiveDaemonRunning(options.socketPath), telemetryDisabled: options.config.telemetryDisabled, }); return { connection, summary }; };
if (options.activeSessionId) { const summary = await findAttachedDaemonSessionSummary(client, options.activeSessionId); return await attach(summary); }
if (options.sessionPath && !options.clientOwned) { const activeSummary = findActiveDaemonSessionSummaryForSessionFile( await listActiveDaemonSessionSummaries(client), options.sessionPath, ); if (activeSummary && activeSummary.workerState !== "failed") { return await attach(activeSummary); } } if (options.clientOwned) { await client.waitForHello(); if (!client.supportsServerCapability("client_owned_sessions")) { throw new DaemonCapabilityUnavailableError("create", "client_owned_sessions"); } }
const response = await client.request({ type: "create", config: options.config, sessionPath: options.sessionPath, continueRecent: options.continueRecent, noSession: options.noSession, env: collectDaemonClientEnv(), lifecycle: options.clientOwned ? "client_owned" : "resident", launchEnv: collectDaemonLaunchEnv(), }); if (!response.success) { throw deserializeDaemonCreateError(response); } if (!isDaemonSessionSummary(response.data)) { throw new Error("Daemon returned an invalid create response"); } const summary = response.data; return await attach(summary); } catch (error) { client.close(); throw error; }}
async function findAttachedDaemonSessionSummary( client: DaemonClient, activeSessionId: string,): Promise<SessionSummary> { const response = await client.request({ type: "get_state", activeSessionId }); if (!response.success) { throw new Error(response.error); } if (!isDaemonSessionSummary(response.data)) { throw new Error("Daemon returned an invalid active session summary"); } return response.data;}
export interface MainOptions { extensionFactories?: ExtensionFactory[];}
export async function main(args: string[], options?: MainOptions) { resetTimings(); if (isDaemonWorkerProcess()) { waitForDaemonWorkerStartupGate(); } installFileLogSink(); if (isDaemonCatalogProcess()) { await runDaemonCatalogProcess(); return; } // Client and daemon are separate processes; both need these in their registry. registerBuiltinMcpOAuthProviders(); const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE); if (offlineMode) { process.env.PI_OFFLINE = "1"; process.env.PI_SKIP_VERSION_CHECK = "1"; }
const publicCommand = await handlePublicCommand(args); if (publicCommand.handled) { return; } args = publicCommand.args;
if (await handleConfigCommand(args)) { return; }
const explicitAgentsView = publicCommand.explicitAgentsView;
const parsed = parseArgs(args); if (parsed.diagnostics.length > 0) { for (const d of parsed.diagnostics) { const color = d.type === "error" ? chalk.red : chalk.yellow; console.error(color(`${d.type === "error" ? "Error" : "Warning"}: ${d.message}`)); } if (parsed.diagnostics.some((d) => d.type === "error")) { process.exit(1); } } time("parseArgs"); const appMode = resolveAppMode(parsed, process.stdin.isTTY);
if (shouldRejectNonInteractiveAttach(publicCommand.attachAgent, appMode)) { console.error(chalk.red("Error: attach requires an interactive terminal")); process.exit(1); } if (shouldRejectNonInteractiveBareResume(parsed.resume, appMode)) { console.error(chalk.red("Error: --resume without a session selector requires an interactive terminal")); process.exit(1); } setLogContext({ mode: appMode }); const shouldTakeOverStdout = appMode !== "interactive"; if (shouldTakeOverStdout) { takeOverStdout(); }
if (parsed.version) { console.log(VERSION); process.exit(0); } if (parsed.help) { console.log(formatTopLevelHelp()); process.exit(0); }
if (parsed.export) { let result: string; try { const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined; result = await exportFromFile(parsed.export, outputPath); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Failed to export session"; console.error(chalk.red(`Error: ${message}`)); process.exit(1); } console.log(`Exported to: ${result}`); process.exit(0); }
if ((parsed.mode === "rpc" || parsed.mode === "daemon") && parsed.fileArgs.length > 0) { console.error(chalk.red("Error: @file arguments are not supported in RPC or daemon mode")); process.exit(1); }
validateForkFlags(parsed);
const cwd = parsed.cwd ? resolve(expandTildePath(parsed.cwd)) : process.cwd(); if (parsed.cwd) { try { process.chdir(cwd); } catch (error) { if (!parsed.remotePythonHost) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: Cannot use cwd ${cwd}: ${message}`)); process.exit(1); } } } if (parsed.daemonSocket) { // After --cwd so a relative socket path resolves against the requested directory. parsed.daemonSocket = normalizeSocketPath(parsed.daemonSocket); }
// Run migrations (pass cwd for project-local migrations) const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd); time("runMigrations");
const agentDir = getAgentDir(); const startupSettingsManager = SettingsManager.create(cwd, agentDir); reportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, "startup session lookup")); const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK); if (startupBenchmark && appMode !== "interactive") { console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode")); process.exit(1); } // Programmatic factories are process-local functions and cannot be serialized to a daemon worker. const hasProcessLocalExtensionFactories = (options?.extensionFactories?.length ?? 0) > 0; const useDaemonClient = shouldUseDaemonClientRuntime({ appMode, startupBenchmark, noSession: parsed.noSession, listModels: parsed.listModels, ownedSessionWorker: isOwnedSessionWorkerProcess(), hasProcessLocalExtensionFactories, }); const useDaemonInteractive = useDaemonClient && appMode === "interactive";
// Decide the final runtime cwd before creating cwd-bound runtime services. // --resume may select a session from another project, so project-local // settings, resources, provider registrations, and models must be resolved only after // the target session cwd is known. The startup-cwd settings manager is used only for // sessionDir lookup during session selection. const sessionDir = (parsed.sessionDir ? expandTildePath(parsed.sessionDir) : undefined) ?? getSessionDirEnvOverride() ?? startupSettingsManager.getSessionDir(); const daemonSocketPath = parsed.daemonSocket ?? defaultDaemonSocketPath(); // Kick off daemon spawn/readiness immediately so it overlaps session-manager // and runtime-services preparation; attach only connects to an existing daemon. let daemonReady = shouldEnsureInteractiveDaemonForStartup(useDaemonClient, publicCommand.attachAgent) ? ensureInteractiveDaemonRunning(daemonSocketPath) : undefined; // Errors are rethrown at the await sites below; this only avoids an unhandled // rejection if startup exits before reaching them. daemonReady?.catch(() => {}); const resumeSelector = getResumeSelector(parsed); const shouldLookupDaemonActiveSession = shouldEnsureDaemonBeforeActiveSessionLookup({ useDaemonInteractive, resumeSelector, explicitAttach: publicCommand.attachAgent !== undefined, }); if (shouldLookupDaemonActiveSession && daemonReady) { daemonReady = (await awaitDaemonReady(daemonReady)).ready; } let activeDaemonSessionSummary: SessionSummary | undefined; if (shouldLookupDaemonActiveSession && resumeSelector) { try { activeDaemonSessionSummary = await findActiveDaemonSessionSummaryForInteractiveStartup( daemonSocketPath, resumeSelector, { fallbackOnError: !publicCommand.attachAgent }, ); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: Could not look up active agent '${resumeSelector}': ${message}`)); process.exit(1); } } if (publicCommand.attachAgent && !activeDaemonSessionSummary) { console.error(chalk.red(`Error: No active agent found matching '${publicCommand.attachAgent}'`)); process.exit(1); } let sessionManager: SessionManager; if (activeDaemonSessionSummary) { sessionManager = createSessionManagerForActiveDaemonSummary(activeDaemonSessionSummary, cwd); } else if ( useDaemonInteractive && shouldUseEphemeralSessionManagerForDaemonInteractive({ resume: parsed.resume, continue: parsed.continue, fork: parsed.fork, }) ) { sessionManager = SessionManager.inMemory(cwd); } else { try { sessionManager = await createSessionManager(parsed, cwd, sessionDir, useDaemonClient); } catch (error) { if (!(error instanceof SessionSelectorError)) { throw error; } const suggestion = error instanceof SessionSelectorNotFoundError && error.suggestion ? ` Did you mean '${error.suggestion}'?` : ""; console.error(chalk.red(`Error: ${error.message}.${suggestion}`)); console.error(chalk.dim(`Open ${APP_NAME} and press left-arrow to browse sessions.`)); process.exit(1); } } const missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd); if (missingSessionCwdIssue) { if (appMode === "interactive") { const selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager); if (!selectedCwd) { process.exit(0); } sessionManager = useDaemonClient ? readSessionManager(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd) : SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd); } else { console.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message)); process.exit(1); } } time("createSessionManager");
const telemetrySettingsManager = sessionManager.getCwd() === cwd ? startupSettingsManager : SettingsManager.create(sessionManager.getCwd(), agentDir); const telemetryDisabled = isTelemetryEnabled(telemetrySettingsManager) ? undefined : true; const defaultSessionConfig = runtimeConfigFromArgs( parsed, sessionManager.getCwd(), agentDir, sessionDir, appMode, telemetryDisabled, ); // Verifier/headless clients pass initialGoal in each create request. The long-lived // daemon fallback must not seed that goal into unrelated future sessions. const daemonDefaultSessionConfig = daemonServerDefaultSessionConfig(defaultSessionConfig); const runtimeDefaultSessionConfig = appMode === "daemon" ? daemonDefaultSessionConfig : defaultSessionConfig; const createRuntime = createDefaultRuntimeFactory(runtimeDefaultSessionConfig, options?.extensionFactories); time("createRuntime"); // Daemon mode never uses the bootstrap runtime, so skip the heavy // createAgentSessionRuntime below and start listening immediately; sessions // are created on demand through the daemon protocol via createRuntime. // --list-models still takes the full path to print and exit. if (appMode === "daemon" && parsed.listModels === undefined) { printTimings(); if (isDaemonWorkerProcess()) { await runDaemonMode({ socketPath: parsed.daemonSocket, defaultSessionConfig: daemonDefaultSessionConfig, createRuntime, worker: { authenticationToken: requireDaemonWorkerAuthenticationToken(), workerInstanceId: daemonWorkerInstanceId(), restoreActiveSessionId: process.env[DAEMON_WORKER_ACTIVE_SESSION_ID_ENV], }, }); } else { await runDaemonSupervisorMode({ socketPath: parsed.daemonSocket, defaultSessionConfig: daemonDefaultSessionConfig, }); } return; } if (useDaemonInteractive) { // The daemon owns the sessions it attaches, so its summary is the authority // on whether the session has messages and which model it runs. Using it // keeps the client from parsing the transcript to re-derive both. const daemonSessionFacts: ExistingSessionFacts | undefined = activeDaemonSessionSummary ? { hasMessages: (activeDaemonSessionSummary.messageCount ?? 0) > 0, model: activeDaemonSessionSummary.model ? { provider: activeDaemonSessionSummary.model.provider, modelId: activeDaemonSessionSummary.model.id, } : null, } : undefined; const prepared = await prepareRuntimeServices({ config: defaultSessionConfig, cwd: sessionManager.getCwd(), agentDir, sessionManager, extensionFactories: options?.extensionFactories, existingSession: daemonSessionFacts, // The daemon owns this session's extensions; waiting for this client's // own extension load would put ~130ms of TypeScript loading in front of // the first frame. deferResourceReload: true, }); const { services, scopedModels } = prepared; const { settingsManager } = services;
const startupModel = await resolvePreparedStartupModel({ prepared, sessionManager, existingSession: daemonSessionFacts, });
const stdinContent = await readPipedStdin(); time("readPipedStdin");
const { initialMessage, initialImages } = await prepareInitialMessage( parsed, settingsManager.getImageAutoResize(), stdinContent, ); time("prepareInitialMessage"); initTheme(settingsManager.getTheme(), true); time("initTheme");
if (deprecationWarnings.length > 0) { await showDeprecationWarnings(deprecationWarnings); }
reportDiagnostics(prepared.diagnostics); if (prepared.diagnostics.some((diagnostic) => diagnostic.type === "error")) { process.exit(1); } time("prepareInteractiveServices");
if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) { const modelList = scopedModels .map((sm) => { const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; return `${sm.model.id}${thinkingStr}`; }) .join(", "); console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+M to cycle)")}`)); }
const promptStashStore = new ClientPromptStashStore(); const daemonUiServices = createInteractiveModeUiServicesFromServices({ services, sessionManager, }); const launchAgentsView = async (initialSession?: SessionSummary, initialScopeKey?: AgentsViewScopeKey) => { await runAgentsViewMode({ socketPath: daemonSocketPath, config: defaultSessionConfig, uiServices: daemonUiServices, recoverDaemon: () => ensureInteractiveDaemonRunning(daemonSocketPath), createUiServicesForSession: async (summary) => { // The daemon runs the attached session, so the UI client only needs // the startup services aimed at the session's cwd and name; a full // prepareRuntimeServices here would reload extensions per switch. return { settingsManager: services.settingsManager, modelRegistry: services.modelRegistry, getInitialCwd: () => summary.cwd || sessionManager.getCwd(), getInitialSessionName: () => summary.sessionName, getThemes: () => services.resourceLoader.getThemes().themes, refreshMcpProviders: () => services.mcpManager.refresh(), }; }, migratedProviders, modelFallbackMessage: startupModel.modelFallbackMessage, promptStashStore, initialSession, initialScopeKey, verbose: parsed.verbose, }); }; if ( shouldOpenAgentsViewForDaemonInteractive({ useDaemonInteractive: useDaemonInteractive && !parsed.noSession, explicitAgentsView, needsOnboarding: shouldRunOnboarding({ settingsManager, modelRegistry: services.modelRegistry, model: startupModel.model, }), resume: parsed.resume, continue: parsed.continue, fork: parsed.fork, }) ) { daemonReady = (await awaitDaemonReady(daemonReady)).ready; await preloadCodeHighlighter(); printTimings(); await launchAgentsView(); return; }
daemonReady = (await awaitDaemonReady(daemonReady)).ready; time("awaitDaemonReady"); // A fresh default chat opens a real but message-less session; the lifecycle // axis treats it as a draft (hidden, discarded on detach if never used), so // no DeferredAgentConnection is needed to avoid creating it up front. const isFreshDefaultSession = !activeDaemonSessionSummary && !getInteractiveDaemonSessionPath(parsed, sessionManager); let connection: DaemonAgentConnection; let summary: SessionSummary; try { ({ connection, summary } = await createDaemonClientConnection({ socketPath: daemonSocketPath, config: defaultSessionConfig, activeSessionId: activeDaemonSessionSummary ? getDaemonSummaryActiveSessionId(activeDaemonSessionSummary) : undefined, sessionPath: getInteractiveDaemonSessionPath(parsed, sessionManager), clientOwned: parsed.noSession, noSession: parsed.noSession, supportsExtensionUi: true, })); } catch (error) { if (error instanceof DaemonSessionCreateError) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } throw error; } time("createDaemonClientConnection"); const agentConnection: AgentConnection = connection; const attachModelFallbackMessage = isFreshDefaultSession ? startupModel.modelFallbackMessage : resolveAttachModelFallbackMessage(summary, startupModel.modelFallbackMessage);
const deferredResources = prepared.resourceReload?.then(() => ({ themes: services.resourceLoader.getThemes().themes, warnings: [ ...collectSettingsDiagnostics(services.settingsManager, "resource reload").map((d) => d.message), ...services.resourceLoader .getExtensions() .errors.map(({ path, error }) => `Failed to load extension "${path}": ${error}`), ], }));
const interactiveMode = new InteractiveMode({ agentConnection, daemonSocketPath, uiServices: daemonUiServices, deferredResources, promptStashStore, promptStashSessionId: summary.sessionId, bindLocalSessionExtensions: false, migratedProviders, modelFallbackMessage: attachModelFallbackMessage, initialMessage, initialImages, initialMessages: parsed.messages, verbose: parsed.verbose, // Resumed/attached daemon sessions are part of the same fleet; left // arrow takes them to the agents view like any other session. The agents // view was not rendered here, so we intentionally leave // agentsViewOwnsStartupNotices unset and let the in-session fallback run. returnToAgentsView: !parsed.noSession, sessionDepth: summary.rlmDepth, // Direct launch has only the attached summary; the live+passive+saved // unified catalog index is not built until the agents view opens. Retained // child snapshots augment this running-child fallback inside chat. sessionHasChildren: summary.hasRunningRlmChildren === true, }); time("interactiveMode.ctor");
await preloadCodeHighlighter(); time("preloadCodeHighlighter"); printTimings(); const interactiveResult = await interactiveMode.run(); if (parsed.noSession) { return; } const returnedSummary = { ...summary, ...interactiveResult.source, id: interactiveResult.source.activeSessionId ?? summary.id, }; const initialScopeKey = interactiveResult.type === "scoped_agents_view" ? { sessionId: interactiveResult.source.sessionId, activeSessionId: interactiveResult.source.activeSessionId, } : undefined; await launchAgentsView(returnedSummary, initialScopeKey); return; } if (useDaemonClient) { const settingsManager = SettingsManager.create(sessionManager.getCwd(), agentDir); let stdinContent: string | undefined; if (appMode !== "rpc" && appMode !== "acp") { stdinContent = await readPipedStdin(); } time("readPipedStdin"); const { initialMessage, initialImages } = await prepareInitialMessage( parsed, settingsManager.getImageAutoResize(), stdinContent, ); time("prepareInitialMessage"); initTheme(settingsManager.getTheme(), false); time("initTheme");
daemonReady = (await awaitDaemonReady(daemonReady)).ready; let connection: DaemonAgentConnection; let summary: SessionSummary; try { ({ connection, summary } = await createDaemonClientConnection({ socketPath: daemonSocketPath, config: defaultSessionConfig, sessionPath: parsed.noSession ? undefined : sessionManager.getSessionFile(), continueRecent: parsed.continue, clientOwned: isClientOwnedDaemonSession(appMode, parsed.noSession), noSession: parsed.noSession, supportsExtensionUi: appMode === "rpc", })); } catch (error) { if (error instanceof SessionAlreadyActiveError || error instanceof DaemonSessionCreateError) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } throw error; } const diagnostics = summary.diagnostics ?? []; reportDiagnostics(diagnostics); if (diagnostics.some((diagnostic) => diagnostic.type === "error")) { await connection.dispose(); process.exit(1); } if (!summary.model) { console.error(chalk.red(summary.modelFallbackMessage ?? formatNoModelsAvailableMessage())); await connection.dispose(); process.exit(1); }
printTimings(); if (appMode === "rpc") { return await runRpcModeWithConnection(connection); } if (appMode === "acp") { return await runAcpModeWithConnection(connection); } const exitCode = await runPrintModeWithConnection(connection, { mode: toPrintOutputMode(appMode), messages: parsed.messages, initialMessage, initialImages, }); stopThemeWatcher(); restoreStdout(); if (exitCode !== 0) { process.exitCode = exitCode; } return; }
let runtime: AgentSessionRuntime; try { runtime = await createAgentSessionRuntime(createRuntime, { cwd: sessionManager.getCwd(), agentDir, sessionManager, sessionConfig: defaultSessionConfig, }); } catch (error) { if (error instanceof SessionAlreadyActiveError) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } throw error; } const { services, session, modelFallbackMessage } = runtime; installOwnedSessionRecoveryTracking(runtime); const { settingsManager, modelRegistry } = services;
if (parsed.listModels !== undefined) { const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined; await listModels(modelRegistry, searchPattern); process.exit(0); }
// Read piped stdin content (if any) - skip for RPC/daemon modes which use other transports let stdinContent: string | undefined; if (appMode !== "rpc" && appMode !== "acp" && appMode !== "daemon") { stdinContent = await readPipedStdin(); } time("readPipedStdin");
const { initialMessage, initialImages } = await prepareInitialMessage( parsed, settingsManager.getImageAutoResize(), stdinContent, ); time("prepareInitialMessage"); initTheme(settingsManager.getTheme(), appMode === "interactive"); time("initTheme");
// Show deprecation warnings in interactive mode if (appMode === "interactive" && deprecationWarnings.length > 0) { await showDeprecationWarnings(deprecationWarnings); }
const scopedModels = [...session.scopedModels]; time("resolveModelScope"); reportDiagnostics(runtime.diagnostics); if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) { process.exit(1); } time("createAgentSession");
if (appMode !== "interactive" && appMode !== "daemon" && !session.model) { console.error(chalk.red(formatNoModelsAvailableMessage())); process.exit(1); }
if (appMode === "rpc") { printTimings(); await runRpcMode(runtime); } else if (appMode === "acp") { printTimings(); await runAcpMode(runtime); } else if (appMode === "interactive") { if (explicitAgentsView || parsed.resume === true) { console.error(chalk.yellow("Warning: the agents view needs the daemon; opening a normal chat instead")); } if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) { const modelList = scopedModels .map((sm) => { const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; return `${sm.model.id}${thinkingStr}`; }) .join(", "); console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+M to cycle)")}`)); }
const interactiveMode = new InteractiveMode({ agentConnection: new InProcessAgentConnection(runtime), localSessionHost: createInteractiveModeLocalSessionHost(runtime), promptStashStore: new ClientPromptStashStore(), promptStashSessionId: session.sessionId, bindLocalSessionExtensions: true, migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages: parsed.messages, verbose: parsed.verbose, }); if (startupBenchmark) { await interactiveMode.init(); time("interactiveMode.init"); printTimings(); interactiveMode.stop(); stopThemeWatcher(); if (process.stdout.writableLength > 0) { await new Promise<void>((resolve) => process.stdout.once("drain", resolve)); } if (process.stderr.writableLength > 0) { await new Promise<void>((resolve) => process.stderr.once("drain", resolve)); } return; }
await preloadCodeHighlighter(); printTimings(); await interactiveMode.run(); } else { printTimings(); const exitCode = await runPrintMode(runtime, { mode: toPrintOutputMode(appMode), messages: parsed.messages, initialMessage, initialImages, }); stopThemeWatcher(); restoreStdout(); if (exitCode !== 0) { process.exitCode = exitCode; } return; }}