paperclipai/paperclip · error

paperclip_runner_chat_attachment_read_cleanup_failed

paperclip_runner_chat_attachment_read_cleanup_failed

Error message

paperclip_runner_chat_attachment_read_cleanup_failed

What it means

Thrown by the native-runtime chat attachment read handle's close() when one or more registered cleanup callbacks reject during shutdown. close() first aborts in-flight reads, waits for pending work with Promise.allSettled, then runs each cleanup with allSettled; if any cleanup rejects, close() rejects with this sentinel error so callers know teardown was not fully clean. It aggregates any failure rather than surfacing the first underlying cleanup error directly.

Solutions

  1. Inspect the Promise.allSettled rejection reasons by running the same cleanups individually or adding logging around each cleanup() to find the failing one.
  2. Make each cleanup idempotent and tolerant of already-aborted/destroyed resources (catch NotFound/EBADF-style errors and swallow them).
  3. Check filesystem permissions and disk state for the temp directory where attachments are staged.
  4. Retry close() once — the implementation memoizes the closing promise, so a retry re-awaits the same settled result; if the error persists, fix the failing cleanup rather than retrying.

Example fix

// before
cleanup: async () => {
  await fs.unlink(tempPath);
}

// after
cleanup: async () => {
  await fs.unlink(tempPath).catch((err: NodeJS.ErrnoException) => {
    if (err.code !== "ENOENT") throw err;
  });
}
Defensive patterns

Strategy: try-catch

Validate before calling

const staged = await fs.readdir(tempDir).catch(() => []);
const deletable = staged.every(f => fs.statSync(path.join(tempDir, f)).isFile());
if (!deletable) console.warn("cleanup may fail: unexpected entries in", tempDir);

Type guard

function isCleanupFailure(err: unknown): err is Error {
  return err instanceof Error && err.message === "paperclip_runner_chat_attachment_read_cleanup_failed";
}

Try / catch

try {
  await handle.close();
} catch (err) {
  if (isCleanupFailure(err)) {
    logger.warn("attachment read cleanup failed; leaking staged resources", { cause: err });
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling close() on a ChatAttachmentRead handle while at least one registered cleanup function rejects — e.g. temp-file unlink failure, storage session teardown failure, or a cleanup throwing because its resource was already destroyed by the abort signal.

Common situations: Filesystem permission issues preventing temp attachment file deletion; a cleanup racing with the close-time abort so it tries to use an aborted/destroyed stream; an environment where the storage directory was removed mid-run; bugs in a custom cleanup callback.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/29a911120f55ce9c. Report an issue: GitHub.

Appendix: source

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

        throw new Error(
          "paperclip_runner_chat_attachment_read_integrity_mismatch",
        );
      return body;
    })();
    return Promise.race([read, aborted]);
  }

  close(): Promise<void> {
    if (this.#closing) return this.#closing;
    this.#closed = true;
    this.#abort.abort();
    this.#closing = (async () => {
      await Promise.allSettled([...this.#pending]);
      const results = await Promise.allSettled(
        this.#cleanups.map((cleanup) => cleanup()),
      );
      if (results.some((result) => result.status === "rejected"))
        throw new Error("paperclip_runner_chat_attachment_read_cleanup_failed");
    })();
    return this.#closing;
  }
}

View on GitHub (pinned to 3f1d897a7c)