{"record":{"id":"b528f9c3a98ac8db","repo":"paperclipai/paperclip","slug":"attempt-journal-is-closed","errorCode":null,"errorMessage":"Attempt journal is closed","messagePattern":"Attempt journal is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/evals/attempt-journal.ts","lineNumber":11,"sourceCode":"import { closeSync, constants, fsyncSync, openSync, writeSync } from \"node:fs\";\n\n/** Controller-owned evidence outside disposable server storage. No credentials. */\nexport class AttemptJournal {\n  #fd: number | null;\n  #bytes = 0;\n  constructor(path: string, readonly maxBytes = 64 * 1024 * 1024) {\n    this.#fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);\n  }\n  append(value: unknown): void {\n    if (this.#fd === null) throw new Error(\"Attempt journal is closed\");\n    const bytes = Buffer.from(JSON.stringify(value) + \"\\n\");\n    if (this.#bytes + bytes.length > this.maxBytes) throw new Error(\"Attempt journal limit reached; stop provider dispatch\");\n    let offset = 0;\n    while (offset < bytes.length) offset += writeSync(this.#fd, bytes, offset, bytes.length - offset);\n    fsyncSync(this.#fd);\n    this.#bytes += bytes.length;\n  }\n  close(): void {\n    if (this.#fd !== null) closeSync(this.#fd);\n    this.#fd = null;\n  }\n}\n","sourceCodeStart":1,"sourceCodeEnd":24,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/evals/attempt-journal.ts#L1-L24","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Create a new AttemptJournal per attempt instead of reusing a closed one.","Reorder cleanup so close() runs only after all dispatch/append activity completes.","Guard appends with an isOpen check (journal.#fd !== null is private, so track closed state in the owning code).","If you control the code, make append a no-op-or-log on closed state rather than throwing during shutdown."],"exampleFix":"// before\nfinally { journal.close(); }\nawait dispatch(); // append() later throws: journal closed\n// after\nawait dispatch();\nfinally { journal.close(); }","handlingStrategy":"try-catch","validationCode":"let journalOpen = true;\nfunction guardedAppend(journal: AttemptJournal, value: unknown) {\n  if (!journalOpen) return; // closed after cleanup began\n  journal.append(value);\n}","typeGuard":null,"tryCatchPattern":"try {\n  journal.append(event);\n} catch (err) {\n  if ((err as Error).message === 'Attempt journal is closed') {\n    logger.warn('append after journal close; dropping event');\n    return;\n  }\n  throw err;\n} finally {\n  // close only after all appends are done\n}","preventionTips":["Scope the journal to a single attempt and close it exactly once, after all dispatch completes.","Never share one journal across attempt lifecycles (the O_EXCL constructor enforces one file per attempt).","Register close() as the very last cleanup step, not in an early finally.","Track open/closed state in the owning code to short-circuit appends during shutdown."],"tags":["file-io","state","journal","lifecycle"],"backgroundTag":"file-write-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}