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

  1. 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.
  2. 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).
  3. Fix the caller that produced the stale/mismatched runId so decisions always reference a live, company-scoped run.
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/a1260152241a449b. Report an issue: GitHub.