{"record":{"id":"d4ad12d1779ddb4c","repo":"koala73/worldmonitor","slug":"account-owner-fence-conflict","errorCode":null,"errorMessage":"ACCOUNT_OWNER_FENCE_CONFLICT","messagePattern":"ACCOUNT_OWNER_FENCE_CONFLICT","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"critical","filePath":"convex/companyMonitoring/accounts.ts","lineNumber":122,"sourceCode":"    Math.max(0, delayMs),\n    internal.companyMonitoring.accounts.advanceAccountPurge,\n    { ownerFenceHash, purgeGeneration },\n  );\n}\n\nasync function findAccountByOwnerFence(\n  ctx: MutationCtx,\n  ownerFence: CompanyMonitoringOwnerFenceCandidates,\n): Promise<{ account: Doc<\"companyMonitoringAccounts\">; matchedHash: string } | null> {\n  let match: { account: Doc<\"companyMonitoringAccounts\">; matchedHash: string } | null = null;\n  for (const ownerFenceHash of ownerFence.all) {\n    const account = await ctx.db\n      .query(\"companyMonitoringAccounts\")\n      .withIndex(\"by_ownerFenceHash\", (q) => q.eq(\"ownerFenceHash\", ownerFenceHash))\n      .unique();\n    if (!account) continue;\n    if (match && match.account._id !== account._id) {\n      throw new ConvexError(\"ACCOUNT_OWNER_FENCE_CONFLICT\");\n    }\n    match = { account, matchedHash: ownerFenceHash };\n  }\n  return match;\n}\n\nasync function findAccountByOwnerUserId(\n  ctx: MutationCtx,\n  ownerUserId: string,\n): Promise<Doc<\"companyMonitoringAccounts\"> | null> {\n  const accounts = await ctx.db\n    .query(\"companyMonitoringAccounts\")\n    .withIndex(\"by_ownerUserId\", (q) => q.eq(\"ownerUserId\", ownerUserId))\n    .take(2);\n  if (accounts.length > 1) throw new ConvexError(\"ACCOUNT_OWNER_FENCE_CONFLICT\");\n  return accounts[0] ?? null;\n}\n","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/companyMonitoring/accounts.ts#L104-L140","documentation":"Thrown by findAccountByOwnerFence (accounts.ts:121-122) when two or more different owner fence hash candidates in ownerFence.all resolve to different companyMonitoringAccounts rows via the by_ownerFenceHash index. This indicates a data-integrity violation: multiple account roots exist for what should be a single owner. The fence keyring is designed so all current+historical hashes map to exactly one account.","triggerScenarios":"A fence key was added or removed from the keyring configuration, causing two historical hashes to point at two distinct account rows. A bug in account provisioning created a duplicate root before the owner-binding reconciliation was added. Manual DB edits inserted a second row with a colliding fence hash.","commonSituations":"Operator rotated or dropped a fence key in the keyring config without running the migration that reconciles roots. Two deployments with different keyring configs wrote to the same table. A prior version of the code (before the owner-binding guard) created duplicate roots that now collide under the current keyring.","solutions":["Inspect the companyMonitoringAccounts table for rows sharing any ownerFenceHash in the user's fence candidate set — identify the duplicate roots.","Determine which row is canonical (correct ownerUserId, non-terminal, latest lifecycleSequence) and delete or terminalize the other(s).","Audit the fence keyring configuration (companyMonitoringOwnerFenceCandidates) for a misconfigured or dropped key that caused the split.","Run a reconciliation script via syncCompanyMonitoringAccountFromEntitlement after dedup to confirm the conflict is resolved."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await syncCompanyMonitoringAccountFromEntitlement(ctx, userId);\n} catch (err) {\n  if (err instanceof ConvexError && err.message === \"ACCOUNT_OWNER_FENCE_CONFLICT\") {\n    // data-integrity incident — alert ops, do not auto-retry\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Never manually insert or edit companyMonitoringAccounts rows outside the provisioning code path.","When rotating fence keys, run the reconciliation migration that maps old hashes to the canonical root before deploying the new keyring.","Monitor for ACCOUNT_OWNER_FENCE_CONFLICT in production — it indicates a split root that needs manual repair."],"tags":["convex","data-integrity","company-monitoring","fence-keyring","duplicate-root"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}