koala73/worldmonitor · error · ConvexError
REMOVED_COMPANY_IS_TERMINAL
Error message
REMOVED_COMPANY_IS_TERMINAL
What it means
Thrown by setCompanyStateForOwner when assertLifecycleTransition('company', currentLifecycle, targetState) throws. This is the generic lifecycle-transition guard: it rejects any transition the company state machine does not permit (e.g., paused->removed is allowed, but invalid combinations are not). The catch swallows the specific reason and rethrows this terminal-styled error.
Source
Thrown at convex/companyMonitoring/companies.ts:361
export const setCompanyStateForOwner = internalMutation({
args: {
ownerUserId: v.string(),
companyId: v.string(),
state: v.union(v.literal("active"), v.literal("paused"), v.literal("removed")),
},
handler: async (ctx, args) => {
const account = await requireActiveAccount(ctx, args.ownerUserId);
const company = await ctx.db
.query("companyMonitoringCompanies")
.withIndex("by_account_companyId", (q) =>
q.eq("ownerAccountId", account.logicalAccountId).eq("companyId", args.companyId),
)
.unique();
if (!company) throw new ConvexError("NOT_FOUND");
try {
assertLifecycleTransition("company", company.lifecycle, args.state);
} catch {
throw new ConvexError("REMOVED_COMPANY_IS_TERMINAL");
}
if (company.lifecycle === "removed") {
if (args.state === "removed") return { status: "already_removed", companyId: company.companyId };
throw new ConvexError("REMOVED_COMPANY_IS_TERMINAL");
}
if (company.lifecycle === args.state) return { status: "unchanged", companyId: company.companyId };
const now = Date.now();
if (args.state !== "removed") {
if (args.state === "paused") {
await cancelCompanyScanWork(ctx, {
ownerAccountId: account.logicalAccountId,
companyId: company.companyId,
reason: "superseded",
});
}
await ctx.db.patch(company._id, {
lifecycle: args.state,View on GitHub (pinned to ffec79ac33)
Solutions
- Check assertLifecycleTransition's allowed transitions in shared/company-monitoring-contract before calling.
- Only request transitions to active, paused, or removed from their documented allowed source states.
- Re-read the company's current lifecycle and compute a legal target from it.
Example fix
// before
await setState({ ownerUserId, companyId, state: targetState }); // may be illegal
// after
const company = await fetchCompany(companyId);
if (!isLegalCompanyTransition(company.lifecycle, targetState)) {
throw new UserError(`Cannot transition ${company.lifecycle} -> ${targetState}`);
}
await setState({ ownerUserId, companyId, state: targetState }); Defensive patterns
Strategy: validation
Validate before calling
import { assertLifecycleTransition } from "shared/company-monitoring-contract";
try {
assertLifecycleTransition("company", current.lifecycle, targetState);
} catch {
throw new UserError(`Illegal transition ${current.lifecycle} -> ${targetState}`);
} Type guard
function isLegalCompanyTransition(from: string, to: "active"|"paused"|"removed"): boolean {
try { assertLifecycleTransition("company", from, to); return true; } catch { return false; }
} Prevention
- Consult the shared lifecycle transition table before requesting a state change.
- Re-read the current lifecycle and compute a legal target from it.
When it happens
Trigger: Requesting a state transition the company lifecycle machine disallows, e.g. an illegal direct jump not whitelisted by assertLifecycleTransition.
Common situations: Calling setState with a state value outside active|paused|removed (caught earlier by the validator) or a disallowed source->target pairing; a stale client sending a transition that newer contract rules forbid.
Related errors
- COMPANY_MONITORING_COMPANY_NOT_ACTIVE
- COMPANY_MONITORING_ACCESS_DENIED
- [_recordPoolFilterStats] no run ${args.runId}
- [_markPickComplete] no run ${args.runId}
- [_markPickComplete] run ${args.runId} is ${run.status}, expe
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/fbf152b153c9ee71.
Report an issue: GitHub.