nexu-io/open-design · error

proposal is , not reviewable

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.

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.

Appendix: 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)