can1357/oh-my-pi · error · ToolError

No plan is awaiting approval — ${PROPOSE_DEVICE_PATH} only a

Error message

No plan is awaiting approval — ${PROPOSE_DEVICE_PATH} only accepts a plan title while plan mode is active.

What it means

dispatchResolutionDevice handles the 'xdev propose' device. The propose path only accepts a plan title while a plan proposal handler is registered (plan mode active). If session.peekPlanProposalHandler() returns undefined — no plan is awaiting approval — it throws this ToolError pointing at the PROPOSE_DEVICE_PATH usage contract.

Source

Thrown at packages/coding-agent/src/tools/resolve.ts:306

}

/**
 * Execute a resolution-device write. `text` is the raw plain-text body:
 * - `xd://resolve` / `xd://reject` → the reason; dispatches to the pending
 *   preview invoker (in-flight queue directive first).
 * - `xd://propose` → the plan title; dispatches to the plan-proposal handler
 *   installed by plan mode.
 */
export async function dispatchResolutionDevice(
	session: ToolSession,
	device: ResolutionDeviceName,
	text: string,
): Promise<{ result: AgentToolResult<unknown>; xdev: XdevDispatch }> {
	const body = text.trim();
	if (device === PROPOSE_DEVICE_NAME) {
		const handler = session.peekPlanProposalHandler?.();
		if (!handler) {
			throw new ToolError(
				`No plan is awaiting approval — ${PROPOSE_DEVICE_PATH} only accepts a plan title while plan mode is active.`,
			);
		}
		const result = await handler(body);
		return { result, xdev: { tool: device, mode: "execute", args: { title: body }, inner: result.details } };
	}

	const action: ResolveAction = device === RESOLVE_DEVICE_NAME ? "apply" : "discard";
	const xdevBase: XdevDispatch = { tool: device, mode: "execute", args: { reason: body } };
	const invoker = session.peekQueueInvoker?.() ?? session.peekPendingInvoker?.();
	if (!invoker) {
		session.clearPendingInvokers?.();
		const proposeHint = session.peekPlanProposalHandler?.()
			? ` To submit the plan for approval, write its title to ${PROPOSE_DEVICE_PATH} instead.`
			: "";
		// Rejecting is a request to reach the "no staged change" end-state, which
		// already holds when nothing is pending — honor it as a successful
		// cancellation instead of surfacing a hard error. Apply still errors.

View on GitHub (pinned to 9690622007)

Solutions

  1. Enter plan mode / create a plan proposal before using the propose device
  2. Use the resolve device (RESOLVE_DEVICE_PATH) to approve/reject an existing pending plan instead
  3. Check the session state: only one plan can await approval at a time
Defensive patterns

Strategy: validation

Validate before calling

if (!session.peekPlanProposalHandler?.()) { throw new Error("Plan mode is not active; cannot use the propose device"); }

Type guard

function canPropose(session: ToolSession): boolean { return typeof session.peekPlanProposalHandler?.() === "function"; }

Try / catch

try { await proposePlan(title); } catch (e) { if (e instanceof ToolError && e.message.includes("No plan is awaiting approval")) { /* fall back to resolve device or enable plan mode */ } else throw e; }

Prevention

When it happens

Trigger: Invoking the propose device when plan mode is not active, the plan was already approved/rejected, or the session never registered a plan proposal handler.

Common situations: Model tries to propose a plan title outside plan mode; a second propose after one was handled; stale transcript replayed into a new session without plan mode.

Related errors


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