paperclipai/paperclip · error

Attempt journal is closed

Error message

Attempt journal is closed

What it means

AttemptJournal is a one-shot append-only JSONL file writer (opened O_EXCL with mode 0600). append() throws 'Attempt journal is closed' when the internal file descriptor is null, i.e. after close() has been called or the fd was never successfully set. The class treats a closed journal as terminal — no further appends are allowed.

Source

Thrown at packages/paperclip-runner/src/evals/attempt-journal.ts:11

import { closeSync, constants, fsyncSync, openSync, writeSync } from "node:fs";

/** Controller-owned evidence outside disposable server storage. No credentials. */
export class AttemptJournal {
  #fd: number | null;
  #bytes = 0;
  constructor(path: string, readonly maxBytes = 64 * 1024 * 1024) {
    this.#fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
  }
  append(value: unknown): void {
    if (this.#fd === null) throw new Error("Attempt journal is closed");
    const bytes = Buffer.from(JSON.stringify(value) + "\n");
    if (this.#bytes + bytes.length > this.maxBytes) throw new Error("Attempt journal limit reached; stop provider dispatch");
    let offset = 0;
    while (offset < bytes.length) offset += writeSync(this.#fd, bytes, offset, bytes.length - offset);
    fsyncSync(this.#fd);
    this.#bytes += bytes.length;
  }
  close(): void {
    if (this.#fd !== null) closeSync(this.#fd);
    this.#fd = null;
  }
}

View on GitHub (pinned to 01ad858492)

Solutions

  1. Create a new AttemptJournal per attempt instead of reusing a closed one.
  2. Reorder cleanup so close() runs only after all dispatch/append activity completes.
  3. Guard appends with an isOpen check (journal.#fd !== null is private, so track closed state in the owning code).
  4. If you control the code, make append a no-op-or-log on closed state rather than throwing during shutdown.

Example fix

// before
finally { journal.close(); }
await dispatch(); // append() later throws: journal closed
// after
await dispatch();
finally { journal.close(); }
Defensive patterns

Strategy: try-catch

Validate before calling

let journalOpen = true;
function guardedAppend(journal: AttemptJournal, value: unknown) {
  if (!journalOpen) return; // closed after cleanup began
  journal.append(value);
}

Try / catch

try {
  journal.append(event);
} catch (err) {
  if ((err as Error).message === 'Attempt journal is closed') {
    logger.warn('append after journal close; dropping event');
    return;
  }
  throw err;
} finally {
  // close only after all appends are done
}

Prevention

When it happens

Trigger: Calling append() after close(); reusing a journal object across attempt lifecycles where close was already invoked; a shutdown/cleanup path closing the journal while provider dispatch is still running and appending.

Common situations: Finally-blocks closing the journal before async dispatch callbacks finish; error paths that close early then retry dispatch; wrapping a single journal in multiple attempt loops without recreating it (the O_EXCL constructor also fails if the file exists, so journals are per-attempt by design).

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@01ad858492 (2026-09-10). Data as JSON: /api/errors/b528f9c3a98ac8db. Report an issue: GitHub.