paperclipai/paperclip · error

capability_live_attempt_not_running

capability_live_attempt_not_running

Error message

capability_live_attempt_not_running

What it means

completeAttempt on the capability live session finalizes the current attempt with a terminal status (succeeded/failed etc.). It first finds the attempt matching #currentAttemptId and requires it to still be in 'running' state; if there is no current attempt or it has already been terminated/completed, it throws the machine-readable error 'capability_live_attempt_not_running'. This enforces that an attempt can only be completed exactly once from the running state.

Source

Thrown at packages/paperclip-runner/src/live/live-session.ts:1436

        observedAt: undefined,
      });
      if (comparable(existing) !== comparable(receipt)) {
        throw new Error("capability_live_usage_receipt_conflict");
      }
      return "duplicate";
    }
    this.#usageLedger.push(receipt);
    await this.#persist();
    return "committed";
  }

  async completeAttempt(
    status: Exclude<CapabilityLiveAttemptStatus, "running" | "terminated">,
    failureCode: string | null = null,
  ): Promise<CapabilityLiveSessionSnapshot> {
    const attempt = this.#attempts.find((candidate) => candidate.attemptId === this.#currentAttemptId);
    if (attempt === undefined || attempt.status !== "running") {
      throw new Error("capability_live_attempt_not_running");
    }
    if (
      status === "succeeded" &&
      (this.#activeTurnId !== null ||
        this.#turnWaiter !== null ||
        this.#pendingTurnAdmission !== null)
    ) {
      throw new Error("capability_live_attempt_active_turn");
    }
    attempt.status = status;
    attempt.finishedAt = this.#now().toISOString();
    attempt.failureCode = status === "failed"
      ? requireNonEmpty(failureCode ?? "attempt_failed", "attempt_failure_code")
      : null;
    await this.#persist();
    return this.snapshot();
  }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Check attempt status before calling: only invoke completeAttempt when the snapshot shows the current attempt as 'running'.
  2. Make completion idempotent in the caller: track whether completeAttempt already succeeded and swallow the second call.
  3. Ensure timeout/termination paths and normal completion paths are mutually exclusive (single completion owner per attempt).
  4. Wrap in try-catch for this code and treat it as a benign race during shutdown.

Example fix

// before
await session.completeAttempt('succeeded');
...
await session.completeAttempt('failed', 'timeout'); // throws
// after
const snap = session.snapshot();
const current = snap.attempts.find((a) => a.attemptId === snap.currentAttemptId);
if (current?.status === 'running') await session.completeAttempt('failed', 'timeout');
Defensive patterns

Strategy: try-catch

Validate before calling

const snap = session.snapshot();
const attempt = snap.attempts.find((a) => a.attemptId === snap.currentAttemptId);
const canComplete = attempt !== undefined && attempt.status === 'running';

Type guard

function attemptIsRunning(snap: CapabilityLiveSessionSnapshot): boolean {
  const a = snap.attempts.find((x) => x.attemptId === snap.currentAttemptId);
  return a !== undefined && a.status === 'running';
}

Try / catch

try {
  await session.completeAttempt(status, failureCode);
} catch (err) {
  if ((err as Error).message === 'capability_live_attempt_not_running') {
    logger.info('attempt already terminal; completion race ignored');
    return session.snapshot();
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling completeAttempt twice for the same attempt; calling it after terminateAttempt/timeout already moved the attempt out of 'running'; calling it when no attempt was ever started (#currentAttemptId is null); racing completion against a session restart that reset current-attempt tracking.

Common situations: Double callback delivery from a provider client (error + success both triggering completion); watchdog timeout firing while the model also returns; retry logic that re-completes a stale attempt handle; resuming a session from a snapshot where the attempt was already terminal.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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