koala73/worldmonitor · critical · ConvexError
ACCOUNT_OWNER_FENCE_CONFLICT
Error message
ACCOUNT_OWNER_FENCE_CONFLICT
What it means
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.
Source
Thrown at convex/companyMonitoring/accounts.ts:122
Math.max(0, delayMs),
internal.companyMonitoring.accounts.advanceAccountPurge,
{ ownerFenceHash, purgeGeneration },
);
}
async function findAccountByOwnerFence(
ctx: MutationCtx,
ownerFence: CompanyMonitoringOwnerFenceCandidates,
): Promise<{ account: Doc<"companyMonitoringAccounts">; matchedHash: string } | null> {
let match: { account: Doc<"companyMonitoringAccounts">; matchedHash: string } | null = null;
for (const ownerFenceHash of ownerFence.all) {
const account = await ctx.db
.query("companyMonitoringAccounts")
.withIndex("by_ownerFenceHash", (q) => q.eq("ownerFenceHash", ownerFenceHash))
.unique();
if (!account) continue;
if (match && match.account._id !== account._id) {
throw new ConvexError("ACCOUNT_OWNER_FENCE_CONFLICT");
}
match = { account, matchedHash: ownerFenceHash };
}
return match;
}
async function findAccountByOwnerUserId(
ctx: MutationCtx,
ownerUserId: string,
): Promise<Doc<"companyMonitoringAccounts"> | null> {
const accounts = await ctx.db
.query("companyMonitoringAccounts")
.withIndex("by_ownerUserId", (q) => q.eq("ownerUserId", ownerUserId))
.take(2);
if (accounts.length > 1) throw new ConvexError("ACCOUNT_OWNER_FENCE_CONFLICT");
return accounts[0] ?? null;
}
View on GitHub (pinned to ffec79ac33)
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.
Defensive patterns
Strategy: try-catch
Try / catch
try {
await syncCompanyMonitoringAccountFromEntitlement(ctx, userId);
} catch (err) {
if (err instanceof ConvexError && err.message === "ACCOUNT_OWNER_FENCE_CONFLICT") {
// data-integrity incident — alert ops, do not auto-retry
throw err;
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ACCOUNT_OWNER_BINDING_MISMATCH
- COMPANY_MONITORING_ACCESS_DENIED
- COMPANY_MONITORING_ACCESS_DENIED
- COMPANY_MONITORING_ACCESS_DENIED
- ACCOUNT_NOT_FOUND
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/d4ad12d1779ddb4c.
Report an issue: GitHub.