paperclipai/paperclip · error
native_continuation_cancellation_run_missing
native_continuation_cancellation_run_missing
Error message
native_continuation_cancellation_run_missing
What it means
In materializeDecisionEffect (server/src/services/native-runtime/status-decision-committer.ts:705), the `cancel_continuations` effect attempts to cancel a heartbeat run with `UPDATE heartbeat_runs SET status='cancelled' ... WHERE id = input.runId AND company_id = input.companyId RETURNING *`. If no row is returned, the run either does not exist or belongs to a different company, and this sentinel error is thrown to abort the commit transaction. It is a company-scoped entity-not-found guard: the committer refuses to fabricate a cancellation for a run it cannot see.
Source
Thrown at server/src/services/native-runtime/status-decision-committer.ts:705
eq(issueThreadInteractions.issueId, input.issue.id),
eq(issueThreadInteractions.status, "pending"),
));
if (interactionRows.length > 0) {
await input.tx.update(issueThreadInteractions).set({
status: "cancelled",
resolvedAt: new Date(),
updatedAt: new Date(),
}).where(inArray(issueThreadInteractions.id, interactionRows.map((row) => row.id)));
}
const [run] = await input.tx.update(heartbeatRuns).set({
status: "cancelled",
finishedAt: new Date(),
updatedAt: new Date(),
}).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
)).returning();
if (!run) throw new Error("native_continuation_cancellation_run_missing");
input.terminalRunsToEmit?.push(run);
return {
effectKind: effect.kind,
targetType: "heartbeat_run",
targetId: run.id,
payload: {
cancelledWakeIds: wakeRows.map((row) => row.id),
cancelledInteractionIds: interactionRows.map((row) => row.id),
},
};
}
if (effect.kind === "append_superseding_assessment") {
const lineage = await input.tx.select({
currentAssessmentId: statusDecisions.assessmentId,
}).from(statusDecisions).where(and(
eq(statusDecisions.id, input.decisionId),
eq(statusDecisions.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null);View on GitHub (pinned to 01ad858492)
Solutions
- Verify input.runId is a valid heartbeat_runs id for the same company before materializing the cancel_continuations effect; log and inspect the decision payload.
- Query heartbeat_runs by id/companyId first; if the run is already terminal or missing, treat the cancellation as a no-op instead of throwing (add an idempotency guard).
- Fix the caller that produced the stale/mismatched runId so decisions always reference a live, company-scoped run.
- If rows were intentionally deleted, relax the update's WHERE or use a softer lookup that tolerates missing runs for already-terminal decisions.
Example fix
// before
const [run] = await tx.update(heartbeatRuns).set({...}).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
)).returning();
if (!run) throw new Error("native_continuation_cancellation_run_missing");
// after
const [run] = await tx.update(heartbeatRuns).set({...}).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
)).returning();
if (!run) {
const existing = await tx.select({ id: heartbeatRuns.id }).from(heartbeatRuns)
.where(and(eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.companyId)));
if (existing.length === 0) return { effectKind: effect.kind, targetType: "heartbeat_run", targetId: input.runId, payload: { alreadyGone: true } };
throw new Error("native_continuation_cancellation_run_missing"); // exists but not terminal-able
} Defensive patterns
Strategy: validation
Validate before calling
const existing = await db.select({ id: heartbeatRuns.id, companyId: heartbeatRuns.companyId })
.from(heartbeatRuns)
.where(and(eq(heartbeatRuns.id, runId), eq(heartbeatRuns.companyId, companyId)));
if (existing.length === 0) {
// skip cancellation: run missing or cross-company — don't materialize the effect
} Type guard
function isCancelableRun(
run: { id: string; companyId: string; status: string } | undefined,
expectedCompanyId: string,
): run is { id: string; companyId: string; status: string } {
return !!run && run.companyId === expectedCompanyId;
} Try / catch
try {
await committer.commit(decision);
} catch (err) {
if (err instanceof Error && err.message === "native_continuation_cancellation_run_missing") {
logger.warn({ runId }, "cancel_continuations target run missing; treating as no-op");
return;
}
throw err;
} Prevention
- Always source runId from the same company-scoped query/decision that produced it; never pass ids across company contexts.
- Pre-validate run existence and terminal status before enqueuing a cancel_continuations effect.
- Make the cancellation idempotent: a missing or already-terminal run should be a no-op, not a hard failure.
- Add an assertion/log when a decision references a runId that no longer resolves, to catch purge/migration bugs.
When it happens
Trigger: Committing a status decision whose effect kind is `cancel_continuations` while input.runId references a heartbeat_runs row that is absent, already hard-deleted, or scoped to a different companyId than input.companyId.
Common situations: Stale runId captured before the run was purged/migrated; passing the wrong run id (e.g. issue id or taskId) into the committer; cross-company data leak attempt or mis-scoped caller context; idempotent re-commit after the run row was already finalized and removed.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- Environment variable ${envName} is empty or not set.
- --payload must be a JSON object
- ${label} had drifted migration history; repaired migration j
- ${label} has existing tables but no migration journal. Run m
- Database mode is postgres but no connection string was set;
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/a1260152241a449b.
Report an issue: GitHub.