can1357/oh-my-pi · error · VibeTurnError

[vibe:${record.id} cli=${record.cli} turn=${turnIndex}] turn

Error message

[vibe:${record.id} cli=${record.cli} turn=${turnIndex}] turn failed: ${reason}

What it means

The per-turn job wrapper in #registerTurnJob catches any error thrown while running a vibe worker turn (spawn failure, persistence ToolError, provider error, abort), finalizes the turn via #finishTurn, records the failure in record.lastActivity, and rethrows it as a VibeTurnError prefixed with the worker id, cli, and turn index. It is a normalizing wrapper: the original error's message becomes the reason; only VibeTurnError instances pass through untouched. The wrapped error surfaces to the caller that awaited the async job (e.g. vibe send/follow-up APIs).

Source

Thrown at packages/coding-agent/src/vibe/runtime.ts:1445

						? await runSubprocess(await this.#buildSpawnOptions(session, record, message, signal, onProgress))
						: await runSubagentFollowUpTurn({
								id: record.id,
								agent: record.agent,
								message,
								description: `vibe ${record.cli} session`,
								signal,
								onProgress,
								eventBus: session.eventBus,
								subagentEventBus: session.subagentEventBus,
								artifactsDir: session.getSessionFile()?.slice(0, -6),
							});
					return await this.#settleTurn(session, manager, record, turn, ownJobId, turnIndex, result);
				} catch (error) {
					if (error instanceof VibeTurnError) throw error;
					await this.#finishTurn(session, manager, record, ownJobId);
					const reason = error instanceof Error ? error.message : String(error);
					record.lastActivity = firstLine(`turn failed: ${reason}`);
					throw new VibeTurnError(
						`[vibe:${record.id} cli=${record.cli} turn=${turnIndex}] turn failed: ${reason}`,
					);
				}
			},
			{ id: `${record.id}-t${turnIndex}`, agentId: record.id, ownerId: record.ownerId },
		);
		turn.jobId = jobId;
		record.turn = turn;
		return jobId;
	}

	/** Post-turn bookkeeping shared by success and failure paths: clear the in-flight turn, flush the queue. */
	async #finishTurn(
		session: ToolSession,
		manager: AsyncJobManager,
		record: VibeRecord,
		settledJobId: string,
	): Promise<void> {

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the embedded reason after 'turn failed:' to identify the root cause (it is the original error message).
  2. If the reason is the scope-change ToolError, restore the original parent session scope before sending further turns.
  3. For spawn/config reasons, fix the agent definition/CLI/model credentials and start a new turn or new worker.
  4. Inspect record.lastActivity (first line of 'turn failed: ...') in the vibe status listing to triage without catching the exception.

Example fix

// before: generic handling loses context
catch (e) { log(e.message); }
// after: unwrap the VibeTurnError and act on the reason
try {
  await vibe.send(workerId, msg);
} catch (e) {
  if (e instanceof VibeTurnError && e.message.includes("changed parent scope")) {
    await session.switchTo(originalSessionFile);
    await vibe.send(workerId, msg);
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate inputs before dispatching a turn
if (!record.agent) throw new Error("vibe agent definition missing");
if (record.state === "dead") throw new Error(`vibe worker ${record.id} is dead; spawn a new one`);

Type guard

function isVibeTurnError(e: unknown): e is VibeTurnError {
  return e instanceof VibeTurnError;
}

Try / catch

try {
  await vibe.send(workerId, message);
} catch (error) {
  if (isVibeTurnError(error)) {
    const match = error.message.match(/^\[vibe:(\S+) cli=(\S+) turn=(\d+)\] turn failed: (.*)$/s);
    if (match) {
      const [, id, cli, turn, reason] = match;
      logger.warn("vibe turn failed", { id, cli, turn, reason });
      // dispatch on `reason` (scope change, spawn failure, provider error)
      return;
    }
  }
  throw error;
}

Prevention

When it happens

Trigger: Any failure inside a turn: runSubprocess/runSubagentFollowUpThrow throwing (bad agent config, missing CLI, spawn failure), the turn-started persistence ToolError (2728), provider/API errors, or cancellation — anything that is not already a VibeTurnError.

Common situations: Worker CLI binary not installed or agent name invalid; model auth expired mid-turn; parent session scope changed (see 2728); network failure during the subagent's LLM call; user aborted the job.

Related errors


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