{"record":{"id":"e8d25e77401bbec3","repo":"nexu-io/open-design","slug":"automation-proposal-not-found","errorCode":null,"errorMessage":"automation proposal not found","messagePattern":"automation proposal not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/daemon/src/automation-proposals.ts","lineNumber":286,"sourceCode":"  }\n  const patch = parseJsonPatchAfter(proposal);\n  const template = await upsertUserAutomationTemplate(dataDir, patch);\n  return {\n    automationTemplateId: template.id,\n    action: proposal.action,\n    path: `automation-templates/${template.id}.json`,\n  };\n}\n\nasync function updateProposalStatus(\n  dataDir: string,\n  id: string,\n  status: AutomationProposalStatus,\n  metadataPatch: Record<string, unknown> = {},\n): Promise<AutomationEvolutionProposal> {\n  const proposals = await listAutomationProposals(dataDir, { status: 'all' });\n  const index = proposals.findIndex((proposal) => proposal.id === id);\n  if (index < 0) throw new Error('automation proposal not found');\n  const current = proposals[index]!;\n  const next: AutomationEvolutionProposal = {\n    ...current,\n    status,\n    updatedAt: new Date().toISOString(),\n  };\n  if (Object.keys(metadataPatch).length > 0) {\n    const currentMetadata =\n      current.metadata && typeof current.metadata === 'object' && !Array.isArray(current.metadata)\n        ? current.metadata as Record<string, unknown>\n        : {};\n    next.metadata = {\n      ...currentMetadata,\n      ...metadataPatch,\n    } as NonNullable<AutomationEvolutionProposal['metadata']>;\n  } else if (current.metadata !== undefined) {\n    next.metadata = current.metadata as NonNullable<AutomationEvolutionProposal['metadata']>;\n  }","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/automation-proposals.ts#L268-L304","documentation":"Thrown by updateProposalStatus() when the given id is not present in the persisted proposals list. updateProposalStatus is called internally after apply/reject handlers have already fetched the proposal, so reaching this branch usually means the proposal was removed between the get and the update (a race or concurrent delete).","triggerScenarios":"Two concurrent callers: one applies/rejects and the other deletes the proposal, then the first caller's updateProposalStatus runs and cannot find the id. Also reachable if the proposals.json file was edited externally and the id no longer exists.","commonSituations":"Concurrent reviewer actions; external script rewriting proposals.json; proposal expired/rotated between fetch and status update; test reset that wiped the store mid-flow.","solutions":["Re-fetch the proposal and confirm it still exists before retrying.","Serialize reviewer actions through a single queue to avoid the get/update race.","Avoid editing proposals.json out-of-band while the daemon is serving apply/reject requests."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const proposals = await listAutomationProposals(dataDir, { status: 'all' });\nif (!proposals.some((p) => p.id === id)) {\n  throw new Error('automation proposal not found');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await updateProposalStatus(dataDir, id, status, patch);\n} catch (e) {\n  if ((e as Error).message === 'automation proposal not found') {\n    // re-fetch; the proposal was removed mid-flight — do not retry blindly\n  }\n}","preventionTips":["Serialize reviewer actions to avoid the get/update race.","Do not edit proposals.json out-of-band while the daemon is running.","Re-fetch before retrying any 'not found' from the status-update path."],"tags":["automation","proposals","not-found","concurrency","daemon"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}