paperclipai/paperclip · error

paperclip_runner_chat_attachment_read_scope_closed

paperclip_runner_chat_attachment_read_scope_closed

Error message

paperclip_runner_chat_attachment_read_scope_closed

What it means

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.

Solutions

  1. Do not call read() after awaiting scope.close(); treat the scope as single-run-lifetime.
  2. Catch this error in the tool dispatcher and surface it as a benign 'run ended' tool failure, not a bug.
  3. If reads must outlive the current run, create a new scope with a new binding rather than reusing a closed one.
  4. Check whether run teardown was triggered prematurely (timeout, abort) while the agent was still working.

Example fix

// before
await scope.close();
const file = await scope.read({ sourceCommentId, attachmentId }); // throws
// after
if (!isScopeClosed) {
  const file = await scope.read({ sourceCommentId, attachmentId });
  await scope.close();
}
Defensive patterns

Strategy: try-catch

Validate before calling

let scopeOpen = true;
async function closeScope(s) { scopeOpen = false; await s.close(); }

Type guard

function isScopeClosedError(e: unknown): boolean {
  return e instanceof Error && e.message === "paperclip_runner_chat_attachment_read_scope_closed";
}

Try / catch

try {
  const file = await scope.read(input);
} catch (e) {
  if (isScopeClosedError(e)) return { status: "run_ended" };
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/6981725f5fe4123f. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-read.ts:65

  #pending = new Set<Promise<unknown>>();
  #cleanups: Array<() => Promise<void>> = [];
  #abort = new AbortController();
  #closing: Promise<void> | null = null;

  constructor(
    readonly options: {
      db: Db;
      binding: ChatReuseBinding;
      workspaceRoot: string;
      executionTargetKind: "local" | "remote";
      storage?: StorageService;
      storageTimeoutMs?: number;
    },
  ) {}

  #assertOpen() {
    if (this.#closed)
      throw new Error("paperclip_runner_chat_attachment_read_scope_closed");
  }

  read(input: { sourceCommentId: string; attachmentId: string }) {
    this.#assertOpen();
    if (
      !UUID.test(input.sourceCommentId) ||
      !UUID.test(input.attachmentId) ||
      Object.keys(input).some(
        (key) => key !== "sourceCommentId" && key !== "attachmentId",
      )
    ) {
      throw new Error(
        "paperclip_runner_chat_attachment_read_arguments_invalid",
      );
    }
    if (this.options.executionTargetKind !== "local") {
      throw new Error(
        "paperclip_runner_chat_attachment_remote_staging_unsupported",

View on GitHub (pinned to 3f1d897a7c)