can1357/oh-my-pi · error

Composer is not available for transfer

Error message

Composer is not available for transfer

What it means

Composer.transfer() moves terminal ownership from the composer to InteractiveMode without tearing the composer down. It throws when the composer is not in a state that can be transferred: it was never started, it has already been stopped, or ownership was already transferred. This is a lifecycle-state guard, not a resource failure.

Source

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

			this.ui.removeChild(this.#bootstrapInputGap);
			this.ui.removeChild(this.editor);
			this.#runtimeMounted = true;
		}
		this.#runtimeChildren = children;
		for (const child of children) this.ui.addChild(child);
		this.ui.addChild(this.#statusHost);
		this.ui.requestRender();
	}

	/** Play or replay the welcome intro against the stable header render target. */
	playWelcomeIntro(): void {
		this.#welcome?.playIntro(() => this.ui.requestComponentRender(this.#header));
	}

	/** Transfer terminal ownership to InteractiveMode without stopping the composer. */
	transfer(): void {
		if (!this.#started || this.#stopped || this.#transferred) {
			throw new Error("Composer is not available for transfer");
		}
		this.#transferred = true;
	}

	/** Stop a composer that has not transferred terminal ownership. */
	stop(): void {
		if (!this.#started || this.#stopped || this.#transferred) return;
		this.#welcome?.stopIntro();
		this.ui.stop();
		this.#stopped = true;
	}

	#applyWelcomeUpdate(update: ComposerWelcomeUpdate): void {
		if (update.version !== undefined) this.#version = update.version;
		if (update.modelName !== undefined) this.#modelName = update.modelName;
		if (update.providerName !== undefined) this.#providerName = update.providerName;
		if (update.recentSessions !== undefined) this.#recentSessions = [...update.recentSessions];
		if (update.lspServers !== undefined) this.#lspServers = [...update.lspServers];

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure composer.start() has completed before calling transfer()
  2. Track transfer state in the caller and skip subsequent transfer() calls
  3. Use composer.stop() instead of transfer() if the intent is to shut the composer down rather than hand off the terminal
  4. Check the ordering of mode-switch handlers so only one path invokes transfer()

Example fix

// before
composer.transfer();
// after
if (composer.isRunning() && !composer.hasTransferred()) {
  composer.transfer();
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track lifecycle in caller state
const canTransfer = composer.isStarted && !composer.isStopped && !composer.hasTransferred;
if (!canTransfer) return;

Type guard

function isTransferable(c: Composer): boolean {
  return c.isStarted && !c.isStopped && !c.hasTransferred;
}

Try / catch

try {
  composer.transfer();
} catch (err) {
  if (err instanceof Error && err.message === 'Composer is not available for transfer') {
    // already stopped/transferred or not started; skip handoff
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling composer.transfer() before composer.start() was called; calling transfer() after composer.stop() has run; calling transfer() a second time after a previous successful transfer (#transferred already true).

Common situations: Double-dispatching a mode-switch keybinding so the transfer path runs twice; a plugin or custom mode calling transfer() during early startup before the composer finished initializing; calling transfer() from a cleanup/teardown path after the session was already stopped.

Related errors


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