can1357/oh-my-pi · error

A prepaint composer is already active

Error message

A prepaint composer is already active

What it means

beginStartupComposer creates the singleton 'prepaint' composer used to speculatively render startup state. Because only one composer instance may own the terminal at a time, the module-level pendingComposer guard throws 'A prepaint composer is already active' if one is still live. Callers must stop/finish the existing composer before beginning another.

Source

Thrown at packages/coding-agent/src/modes/startup-composer.ts:74

	/** Transfer terminal ownership exactly once. */
	adopt(): void {
		if (this.#adopted) return;
		// Safety net: startup paths that never applied resolved settings must
		// still hand InteractiveMode a raw-input terminal.
		this.composer.enableInput();
		this.composer.transfer();
		this.#adopted = true;
	}

	/** Stop an unadopted composer when startup exits before InteractiveMode. */
	dispose(): void {
		if (!this.#adopted) this.composer.stop();
	}
}

/** Start the canonical Composer with speculative cached state, then refresh recent sessions. */
export function beginStartupComposer(options: PrepaintComposerOptions = {}): void {
	if (pendingComposer) throw new Error("A prepaint composer is already active");
	const cwd = options.cwd ?? process.cwd();
	const useCache = options.cache !== false;
	const cached = useCache
		? readComposerStartupCache(cwd)
		: {
				preferences: undefined,
				theme: undefined,
				welcome: undefined,
				recentSessions: [],
				lspServers: [],
			};
	const theme = { ...cached.theme, ...options.theme };
	initThemeSync(theme.symbolPreset, theme.colorBlindMode, theme.darkTheme, theme.lightTheme);
	const preferences = { ...COMPOSER_DEFAULTS, ...cached.preferences, ...options.preferences };
	const welcome: ComposerWelcomeUpdate = {
		version: options.version ?? "",
		modelName: cached.welcome?.modelName,
		providerName: cached.welcome?.providerName,

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure the previous composer finished: call its stop() (or let adoption complete) before calling beginStartupComposer again.
  2. Audit call sites (runCli, coldLaunch) so only one startup path runs per process; guard with a lifecycle flag.
  3. Wrap in try/catch that stops the pending composer and retries once if 'A prepaint composer is already active' is thrown.

Example fix

// before
beginStartupComposer({ cwd });
beginStartupComposer({ cwd }); // throws
// after
if (firstComposer) firstComposer.stop();
beginStartupComposer({ cwd });
Defensive patterns

Strategy: try-catch

Validate before calling

// guard call sites with a module-level lifecycle flag
if (startupComposerActive) throw new Error("startup already in progress");

Try / catch

try {
  beginStartupComposer(options);
} catch (err) {
  if (err instanceof Error && err.message === "A prepaint composer is already active") {
    // stop the pending composer and retry once
  } else throw err;
}

Prevention

When it happens

Trigger: Calling beginStartupComposer twice without the first composer being stopped/adopted — e.g. runCli and coldLaunch both invoking it on overlapping code paths, re-entrant startup, or a retry path that re-enters startup after a partial initialization.

Common situations: Double CLI initialization (embedding the CLI and also calling its startup path); error during startup that leaves the composer active, followed by a retry that calls beginStartupComposer again; tests reusing a module instance across startup attempts.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a47d8c37450175c7. Report an issue: GitHub.