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
- Inspect the Promise.allSettled rejection reasons by running the same cleanups individually or adding logging around each cleanup() to find the failing one.
- Make each cleanup idempotent and tolerant of already-aborted/destroyed resources (catch NotFound/EBADF-style errors and swallow them).
- Check filesystem permissions and disk state for the temp directory where attachments are staged.
- 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
- Write cleanups that tolerate ENOENT/already-destroyed resources (idempotent teardown).
- Never do fallible network or user-dependent work inside a close-time cleanup.
- Log each cleanup's allSettled rejection reason in development to catch failing cleanups early.
- Monitor for this error in production as a signal of leaked temp files or stuck storage sessions.
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
- paperclip_runner_attachment_staging_cleanup_failed
- Attachment exceeds the configured size limit
- attachment_not_ready
- Chat SDK endpoint runtime was retired
- Chat SDK runtime is shutting down
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)