{"record":{"id":"6981725f5fe4123f","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-scope-closed","errorCode":"paperclip_runner_chat_attachment_read_scope_closed","errorMessage":"paperclip_runner_chat_attachment_read_scope_closed","messagePattern":"paperclip_runner_chat_attachment_read_scope_closed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":65,"sourceCode":"  #pending = new Set<Promise<unknown>>();\n  #cleanups: Array<() => Promise<void>> = [];\n  #abort = new AbortController();\n  #closing: Promise<void> | null = null;\n\n  constructor(\n    readonly options: {\n      db: Db;\n      binding: ChatReuseBinding;\n      workspaceRoot: string;\n      executionTargetKind: \"local\" | \"remote\";\n      storage?: StorageService;\n      storageTimeoutMs?: number;\n    },\n  ) {}\n\n  #assertOpen() {\n    if (this.#closed)\n      throw new Error(\"paperclip_runner_chat_attachment_read_scope_closed\");\n  }\n\n  read(input: { sourceCommentId: string; attachmentId: string }) {\n    this.#assertOpen();\n    if (\n      !UUID.test(input.sourceCommentId) ||\n      !UUID.test(input.attachmentId) ||\n      Object.keys(input).some(\n        (key) => key !== \"sourceCommentId\" && key !== \"attachmentId\",\n      )\n    ) {\n      throw new Error(\n        \"paperclip_runner_chat_attachment_read_arguments_invalid\",\n      );\n    }\n    if (this.options.executionTargetKind !== \"local\") {\n      throw new Error(\n        \"paperclip_runner_chat_attachment_remote_staging_unsupported\",","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L47-L83","documentation":"NativeChatAttachmentReadScope guards all operations with a lifetime check: once close() has been called on the scope, every entry point (read, bytes, authorization) throws this error. The scope is server-owned and tied to a run's lifetime; closing aborts in-flight reads and cleans up staged workspace files. The error exists so a tool call racing with scope teardown fails loudly instead of publishing a workspace path after cleanup has started.","triggerScenarios":"Calling read() after scope.close() (or while close() is in progress); an async continuation (authorization retry, post-storage staging) resuming after close() aborted the internal AbortController; the runner invoking the read_chat_attachment tool after the run has already ended.","commonSituations":"Run completion/timeout races a slow attachment read; a retry loop re-invokes a tool with a stale scope object; tests replaying tool calls after teardown.","solutions":["Do not call read() after awaiting scope.close(); treat the scope as single-run-lifetime.","Catch this error in the tool dispatcher and surface it as a benign 'run ended' tool failure, not a bug.","If reads must outlive the current run, create a new scope with a new binding rather than reusing a closed one.","Check whether run teardown was triggered prematurely (timeout, abort) while the agent was still working."],"exampleFix":"// before\nawait scope.close();\nconst file = await scope.read({ sourceCommentId, attachmentId }); // throws\n// after\nif (!isScopeClosed) {\n  const file = await scope.read({ sourceCommentId, attachmentId });\n  await scope.close();\n}","handlingStrategy":"try-catch","validationCode":"let scopeOpen = true;\nasync function closeScope(s) { scopeOpen = false; await s.close(); }","typeGuard":"function isScopeClosedError(e: unknown): boolean {\n  return e instanceof Error && e.message === \"paperclip_runner_chat_attachment_read_scope_closed\";\n}","tryCatchPattern":"try {\n  const file = await scope.read(input);\n} catch (e) {\n  if (isScopeClosedError(e)) return { status: \"run_ended\" };\n  throw e;\n}","preventionTips":["Await scope.close() only after all tool activity for the run has finished.","Track scope lifetime alongside run lifecycle events.","Never cache the scope object across runs.","Treat scope-closed tool failures as expected teardown races, not defects."],"tags":["lifecycle","race-condition","scope-closed"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}