{"record":{"id":"f3ee018770b94a9b","repo":"paperclipai/paperclip","slug":"opencode-request-input-requestid-is-no-longer-p","errorCode":null,"errorMessage":"OpenCode request ${input.requestId} is no longer pending","messagePattern":"OpenCode request (.+?) is no longer pending","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts","lineNumber":626,"sourceCode":"      `/session/${encodeURIComponent(this.#providerSessionId)}/abort`,\n      { method: \"POST\" },\n    );\n  }\n\n  pendingRuntimeRequests(): HarnessRuntimeRequest[] {\n    return [...this.#pendingRuntimeRequests.values()].map(({ request }) =>\n      structuredClone(request),\n    );\n  }\n\n  async resolveRuntimeRequest(input: {\n    requestId: string;\n    turnId: string;\n    resolution: HarnessRuntimeRequestResolution;\n  }): Promise<void> {\n    const pending = this.#pendingRuntimeRequests.get(input.requestId);\n    if (!pending)\n      throw new Error(\n        `OpenCode request ${input.requestId} is no longer pending`,\n      );\n    if (\n      pending.request.turnId !== input.turnId ||\n      this.#activeTurnId !== input.turnId\n    ) {\n      throw new Error(\n        `OpenCode request ${input.requestId} belongs to a stale turn`,\n      );\n    }\n    const resolution = parseHarnessRuntimeRequestResolution(\n      pending.request.requestKind,\n      input.resolution,\n      pending.request.input,\n    );\n    if (pending.settling)\n      throw new Error(\n        `OpenCode request ${input.requestId} is already settling`,","sourceCodeStart":608,"sourceCodeEnd":644,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts#L608-L644","documentation":"Runtime requests (permission approvals, native questions) are tracked in `#pendingRuntimeRequests`. `resolveRuntimeRequest` throws when the given `requestId` is no longer in that map — it was already resolved, rejected, handed off, expired, or dropped when the session closed. This prevents double-submission of an answer to OpenCode's permission/question reply APIs.","triggerScenarios":"Calling `resolveRuntimeRequest({ requestId, turnId, resolution })` for a request that was already resolved earlier in this run, auto-expired via `handoffRuntimeRequest`, cancelled by `close()`, or that never existed on this session (e.g. after a restart the in-memory map is empty).","commonSituations":"A UI queues a permission answer and a durable handoff resolves the request first, then the queued answer is submitted; a process restart loses the pending map while the caller retries; the user double-clicks Approve and both clicks hit `resolveRuntimeRequest`.","solutions":["List currently answerable requests with `session.pendingRuntimeRequests()` and only call `resolveRuntimeRequest` for ids present there.","Treat the error as idempotent success: if the request was already resolved, the intended effect likely happened — log and continue.","Do not persist requestId resolution across process restarts without replaying `pendingRuntimeRequests()`; recover state before resolving.","Debounce/deduplicate resolution calls (e.g. disable the button once resolution starts) to avoid double submission."],"exampleFix":"// before\nawait session.resolveRuntimeRequest({ requestId, turnId, resolution }); // throws if gone\n\n// after\nconst pending = session.pendingRuntimeRequests().find(r => r.requestId === requestId);\nif (pending) {\n  await session.resolveRuntimeRequest({ requestId, turnId, resolution });\n}","handlingStrategy":"validation","validationCode":"const exists = session.pendingRuntimeRequests().some(r => r.requestId === requestId);\nif (!exists) return; // already resolved or expired","typeGuard":"function isPending(session, requestId) { return session.pendingRuntimeRequests().some(r => r.requestId === requestId); }","tryCatchPattern":"try {\n  await session.resolveRuntimeRequest({ requestId, turnId, resolution });\n} catch (e) {\n  if (e.message.includes('is no longer pending')) {\n    logger.info({ requestId }, 'runtime request already resolved; treating as no-op');\n  } else throw e;\n}","preventionTips":["Check `pendingRuntimeRequests()` before resolving.","Deduplicate resolution calls (single-flight per requestId).","Treat this error as idempotent success in reconcilers.","Recover pending state after restarts before replaying resolutions."],"tags":["runtime-request","double-submit","opencode","idempotency"],"backgroundTag":"record-not-found","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}