{"record":{"id":"e58439d7e98dd504","repo":"koala73/worldmonitor","slug":"intelhistory-retract-no-identifier-resolved-to-a","errorCode":null,"errorMessage":"intelHistory.retract: no identifier resolved to a dedupeKey — nothing was tombstoned. Unresolved ids: ${unresolvedIds.join(\", \")}. A deleted or pruned row cannot be retracted by id; re-run with --dedupe-key to suppress the identity itself.","messagePattern":"intelHistory\\.retract: no identifier resolved to a dedupeKey — nothing was tombstoned\\. Unresolved ids: (.+?)\\. A deleted or pruned row cannot be retracted by id; re-run with --dedupe-key to suppress the identity itself\\.","errorType":"exception","errorClass":"Error","httpStatus":500,"severity":"error","filePath":"convex/intelHistory.ts","lineNumber":477,"sourceCode":"    assertRetractionIdentifierBudget(args);\n    const reason = args.reason.trim();\n    if (!reason) {\n      throw new Error(\"intelHistory.retract: reason is required\");\n    }\n\n    await touchAppendLock(ctx);\n\n    const { keys, unresolvedIds } = await resolveRetractionKeys(ctx, args);\n\n    // Fail loudly rather than report a successful no-op. A document id is the\n    // handle an operator copies out of a search result, and a row that was\n    // already pruned — or already retracted — no longer resolves to one. With\n    // no key to tombstone the loop below does nothing, and a 200 saying\n    // `deleted: 0, tombstoned: 0` reads as \"already clean\" when the truth is\n    // \"nothing was suppressed and the next seed tick will re-add it\". The\n    // recovery is in the message because the operator needs it right there.\n    if (keys.length === 0) {\n      throw new Error(\n        `intelHistory.retract: no identifier resolved to a dedupeKey — nothing was tombstoned. ` +\n          `Unresolved ids: ${unresolvedIds.join(\", \")}. A deleted or pruned row cannot be ` +\n          `retracted by id; re-run with --dedupe-key to suppress the identity itself.`,\n      );\n    }\n\n    const retractedAt = Date.now();\n\n    let deleted = 0;\n    let tombstoned = 0;\n    let refreshed = 0;\n    for (const dedupeKey of keys) {\n      const [rows, existingTombstone] = await Promise.all([\n        // `collect` over the dedupe index rather than `first`: the index is\n        // meant to hold one row per key, and if an old bug ever put two there\n        // a retraction that removed only one would leave the poisoned text\n        // live while reporting success.\n        ctx.db","sourceCodeStart":459,"sourceCodeEnd":495,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/intelHistory.ts#L459-L495","documentation":"Error \"intelHistory.retract: no identifier resolved to a dedupeKey — nothing was tombstoned. Unresolved ids: ${unresolvedIds.join(\", \")}. A deleted or pruned row cannot be retracted by id; re-run with --dedupe-key to suppress the identity itself.\" thrown in koala73/worldmonitor.","triggerScenarios":"Thrown at convex/intelHistory.ts:477 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Re-run the retract with --dedupe-key to tombstone the identity itself when the row no longer resolves by document id","If the row should still exist, verify the copied id — deleted or pruned rows cannot be retracted by id","Treat the loud failure as intended: it prevents a silent no-op being mistaken for a successful retraction"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}