{"record":{"id":"c7633a000a0f1890","repo":"paperclipai/paperclip","slug":"capability-live-attempt-not-running","errorCode":"capability_live_attempt_not_running","errorMessage":"capability_live_attempt_not_running","messagePattern":"capability_live_attempt_not_running","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/live-session.ts","lineNumber":1436,"sourceCode":"        observedAt: undefined,\n      });\n      if (comparable(existing) !== comparable(receipt)) {\n        throw new Error(\"capability_live_usage_receipt_conflict\");\n      }\n      return \"duplicate\";\n    }\n    this.#usageLedger.push(receipt);\n    await this.#persist();\n    return \"committed\";\n  }\n\n  async completeAttempt(\n    status: Exclude<CapabilityLiveAttemptStatus, \"running\" | \"terminated\">,\n    failureCode: string | null = null,\n  ): Promise<CapabilityLiveSessionSnapshot> {\n    const attempt = this.#attempts.find((candidate) => candidate.attemptId === this.#currentAttemptId);\n    if (attempt === undefined || attempt.status !== \"running\") {\n      throw new Error(\"capability_live_attempt_not_running\");\n    }\n    if (\n      status === \"succeeded\" &&\n      (this.#activeTurnId !== null ||\n        this.#turnWaiter !== null ||\n        this.#pendingTurnAdmission !== null)\n    ) {\n      throw new Error(\"capability_live_attempt_active_turn\");\n    }\n    attempt.status = status;\n    attempt.finishedAt = this.#now().toISOString();\n    attempt.failureCode = status === \"failed\"\n      ? requireNonEmpty(failureCode ?? \"attempt_failed\", \"attempt_failure_code\")\n      : null;\n    await this.#persist();\n    return this.snapshot();\n  }\n","sourceCodeStart":1418,"sourceCodeEnd":1454,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/live-session.ts#L1418-L1454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check attempt status before calling: only invoke completeAttempt when the snapshot shows the current attempt as 'running'.","Make completion idempotent in the caller: track whether completeAttempt already succeeded and swallow the second call.","Ensure timeout/termination paths and normal completion paths are mutually exclusive (single completion owner per attempt).","Wrap in try-catch for this code and treat it as a benign race during shutdown."],"exampleFix":"// before\nawait session.completeAttempt('succeeded');\n...\nawait session.completeAttempt('failed', 'timeout'); // throws\n// after\nconst snap = session.snapshot();\nconst current = snap.attempts.find((a) => a.attemptId === snap.currentAttemptId);\nif (current?.status === 'running') await session.completeAttempt('failed', 'timeout');","handlingStrategy":"try-catch","validationCode":"const snap = session.snapshot();\nconst attempt = snap.attempts.find((a) => a.attemptId === snap.currentAttemptId);\nconst canComplete = attempt !== undefined && attempt.status === 'running';","typeGuard":"function attemptIsRunning(snap: CapabilityLiveSessionSnapshot): boolean {\n  const a = snap.attempts.find((x) => x.attemptId === snap.currentAttemptId);\n  return a !== undefined && a.status === 'running';\n}","tryCatchPattern":"try {\n  await session.completeAttempt(status, failureCode);\n} catch (err) {\n  if ((err as Error).message === 'capability_live_attempt_not_running') {\n    logger.info('attempt already terminal; completion race ignored');\n    return session.snapshot();\n  }\n  throw err;\n}","preventionTips":["Designate a single owner (timeout handler OR completion handler, not both) per attempt.","Check the attempt status in the session snapshot before completing.","Make completion idempotent: remember whether completeAttempt already ran.","Serialize completion calls through a promise/lock to avoid double delivery races."],"tags":["state-machine","race-condition","lifecycle","capability"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}