{"record":{"id":"d0b67bd0e82489f0","repo":"can1357/oh-my-pi","slug":"composer-is-not-available-for-transfer","errorCode":null,"errorMessage":"Composer is not available for transfer","messagePattern":"Composer is not available for transfer","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/composer.ts","lineNumber":574,"sourceCode":"\t\t\tthis.ui.removeChild(this.#bootstrapInputGap);\n\t\t\tthis.ui.removeChild(this.editor);\n\t\t\tthis.#runtimeMounted = true;\n\t\t}\n\t\tthis.#runtimeChildren = children;\n\t\tfor (const child of children) this.ui.addChild(child);\n\t\tthis.ui.addChild(this.#statusHost);\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Play or replay the welcome intro against the stable header render target. */\n\tplayWelcomeIntro(): void {\n\t\tthis.#welcome?.playIntro(() => this.ui.requestComponentRender(this.#header));\n\t}\n\n\t/** Transfer terminal ownership to InteractiveMode without stopping the composer. */\n\ttransfer(): void {\n\t\tif (!this.#started || this.#stopped || this.#transferred) {\n\t\t\tthrow new Error(\"Composer is not available for transfer\");\n\t\t}\n\t\tthis.#transferred = true;\n\t}\n\n\t/** Stop a composer that has not transferred terminal ownership. */\n\tstop(): void {\n\t\tif (!this.#started || this.#stopped || this.#transferred) return;\n\t\tthis.#welcome?.stopIntro();\n\t\tthis.ui.stop();\n\t\tthis.#stopped = true;\n\t}\n\n\t#applyWelcomeUpdate(update: ComposerWelcomeUpdate): void {\n\t\tif (update.version !== undefined) this.#version = update.version;\n\t\tif (update.modelName !== undefined) this.#modelName = update.modelName;\n\t\tif (update.providerName !== undefined) this.#providerName = update.providerName;\n\t\tif (update.recentSessions !== undefined) this.#recentSessions = [...update.recentSessions];\n\t\tif (update.lspServers !== undefined) this.#lspServers = [...update.lspServers];","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/composer.ts#L556-L592","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Ensure composer.start() has completed before calling transfer()","Track transfer state in the caller and skip subsequent transfer() calls","Use composer.stop() instead of transfer() if the intent is to shut the composer down rather than hand off the terminal","Check the ordering of mode-switch handlers so only one path invokes transfer()"],"exampleFix":"// before\ncomposer.transfer();\n// after\nif (composer.isRunning() && !composer.hasTransferred()) {\n  composer.transfer();\n}","handlingStrategy":"try-catch","validationCode":"// Track lifecycle in caller state\nconst canTransfer = composer.isStarted && !composer.isStopped && !composer.hasTransferred;\nif (!canTransfer) return;","typeGuard":"function isTransferable(c: Composer): boolean {\n  return c.isStarted && !c.isStopped && !c.hasTransferred;\n}","tryCatchPattern":"try {\n  composer.transfer();\n} catch (err) {\n  if (err instanceof Error && err.message === 'Composer is not available for transfer') {\n    // already stopped/transferred or not started; skip handoff\n    return;\n  }\n  throw err;\n}","preventionTips":["Call transfer() exactly once per composer lifecycle and centralize the call site","Only transfer after start() has completed and before any stop()","Use a state flag in the owning mode to prevent double dispatch"],"tags":["lifecycle","tui","state-error"],"backgroundTag":"invalid-lifecycle-state","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}