nexu-io/open-design · error

proposal ${proposal.id} is ${proposal.status}, not reviewabl

Error message

proposal ${proposal.id} is ${proposal.status}, not reviewable

What it means

Thrown by assertReviewable() when a proposal's status is neither 'pending-review' nor 'draft'. Only those two states may be applied or rejected; proposals already applied, rejected, superseded, or failed cannot be acted on again. The guard runs in both applyAutomationProposal and rejectAutomationProposal after the proposal is fetched.

Source

Thrown at apps/daemon/src/automation-proposals.ts:123

      ? input.sourcePacketIds.filter((id): id is string => typeof id === 'string' && id.length > 0)
      : [],
    ...(typeof input.automationRunId === 'string' ? { automationRunId: input.automationRunId } : {}),
    ...(typeof input.targetRef === 'string' ? { targetRef: input.targetRef } : {}),
    patch: input.patch,
    ...(typeof input.confidence === 'number' ? { confidence: input.confidence } : {}),
    ...(input.compressionReport ? { compressionReport: input.compressionReport } : {}),
    ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
  };
  const proposals = await listAutomationProposals(dataDir, { status: 'all' });
  const next = proposals.filter((existing) => existing.id !== proposal.id);
  next.push(proposal);
  await writeProposals(dataDir, next);
  return proposal;
}

function assertReviewable(proposal: AutomationEvolutionProposal): void {
  if (proposal.status === 'pending-review' || proposal.status === 'draft') return;
  throw new Error(`proposal ${proposal.id} is ${proposal.status}, not reviewable`);
}

function safeMemoryType(value: unknown): MemoryType {
  return typeof value === 'string' && VALID_MEMORY_TYPES.has(value as MemoryType)
    ? (value as MemoryType)
    : 'project';
}

function parseJsonPatchAfter(proposal: AutomationEvolutionProposal): Record<string, unknown> {
  if (proposal.patch.format !== 'json') return {};
  const after = proposal.patch.after;
  if (typeof after !== 'string' || !after.trim()) return {};
  try {
    const parsed = JSON.parse(after);
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
  } catch {
    throw new Error('proposal patch.after is not valid JSON');
  }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Refresh the proposal list and check status before offering apply/reject actions.
  2. Disable the action in the UI once status leaves 'pending-review'/'draft'.
  3. Treat this error as a no-op in idempotent callers (log and skip) rather than retrying.

Example fix

// before — unconditional apply
await applyAutomationProposal(dataDir, id);
// after — guard on status
const p = await getAutomationProposal(dataDir, id);
if (p && (p.status === 'pending-review' || p.status === 'draft')) {
  await applyAutomationProposal(dataDir, id);
}
Defensive patterns

Strategy: validation

Validate before calling

const proposal = await getAutomationProposal(dataDir, id);
if (!proposal) throw new Error('automation proposal not found');
if (proposal.status !== 'pending-review' && proposal.status !== 'draft') {
  throw new Error(`proposal ${id} is ${proposal.status}, not reviewable`);
}

Type guard

function isReviewable(status: string): boolean {
  return status === 'pending-review' || status === 'draft';
}

Try / catch

try {
  await applyAutomationProposal(dataDir, id);
} catch (e) {
  if (/not reviewable/.test((e as Error).message)) {
    // already actioned; refresh UI, do not retry
  }
}

Prevention

When it happens

Trigger: Calling applyAutomationProposal() or rejectAutomationProposal() on a proposal whose status is 'applied', 'rejected', 'superseded', or 'failed'. Common when a UI action is double-clicked or a stale proposal id is reused.

Common situations: Reviewer double-submits (apply then apply again); two reviewers acting concurrently; client retrying after a timeout when the first call already succeeded; automated loop reprocessing an old proposal list.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/b48eaefc9f8f05d0. Report an issue: GitHub.