{"record":{"id":"7d1bb54f7324d3d7","repo":"can1357/oh-my-pi","slug":"writer-closed","errorCode":null,"errorMessage":"Writer closed","messagePattern":"Writer closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/session/indexed-session-storage.ts","lineNumber":520,"sourceCode":"\t\tthis.#onError?.(error);\n\t\treturn error;\n\t}\n\n\t#trackPromise(promise: Promise<void>): Promise<void> {\n\t\tconst next = this.#pendingChain.then(async () => {\n\t\t\tif (this.#error) throw this.#error;\n\t\t\ttry {\n\t\t\t\tawait promise;\n\t\t\t} catch (err) {\n\t\t\t\tthrow this.#recordError(err);\n\t\t\t}\n\t\t});\n\t\tthis.#pendingChain = next.catch(() => {});\n\t\treturn next;\n\t}\n\n\tappendSync(line: string): void {\n\t\tif (this.#closed) throw new Error(\"Writer closed\");\n\t\tif (this.#error) throw this.#error;\n\t\t// Local index is updated immediately; remote publish stays ordered on the\n\t\t// path queue. Callers that need remote durability still await append()/flush().\n\t\tconst mtimeMs = this.#storage._appendForWriter(this.#path, line);\n\t\tvoid this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));\n\t}\n\n\tasync append(line: string): Promise<void> {\n\t\tif (this.#closed) throw new Error(\"Writer closed\");\n\t\tif (this.#error) throw this.#error;\n\t\tconst mtimeMs = this.#storage._appendForWriter(this.#path, line);\n\t\tawait this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.#error) throw this.#error;\n\t\tawait this.#pendingChain;\n\t\tif (this.#error) throw this.#error;","sourceCodeStart":502,"sourceCodeEnd":538,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/session/indexed-session-storage.ts#L502-L538","documentation":"appendSync() on an indexed session-storage writer throws 'Writer closed' when the writer has already been closed via close()/dispose(). It also rethrows any stored #error from earlier failed remote publishes, so an append on a poisoned writer surfaces the original I/O failure.","triggerScenarios":"Calling writer.appendSync(line) after close() was called, or after a prior async append failed and set the writer's sticky #error state.","commonSituations":"A logging/session-persistence callback still firing after session close; double-close paths where two components both try to finalize; a crashed remote publisher poisoned the writer and later sync appends surface the stored error.","solutions":["Stop issuing appends once close() has been called — check a closed flag in the caller","Catch this error in shutdown/telemetry paths and drop the late write instead of crashing","Inspect the sticky #error (if rethrown) to fix the original remote-publish failure before reopening a new writer","Create a fresh writer for any post-close writes rather than reusing the closed one"],"exampleFix":"// before\nwriter.close();\nwriter.appendSync(line); // throws\n// after\nwriter.close();\nif (!closed) writer.appendSync(line); // guard with your own closed flag\n// or\ntry { writer.appendSync(line); } catch (e) { logger.debug('late append dropped', { e }); }","handlingStrategy":"try-catch","validationCode":"// track writer lifecycle in the caller:\n// let writerOpen = true;\n// writer.close(); writerOpen = false;\n// if (writerOpen) writer.appendSync(line);","typeGuard":null,"tryCatchPattern":"try {\n  writer.appendSync(line);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Writer closed') {\n    logger.debug('dropped append after close', { path });\n    return;\n  }\n  throw err; // sticky #error from a failed remote publish — surface it\n}","preventionTips":["Await all pending appends before closing the writer","Unregister logging/session callbacks before close so nothing writes afterward","If a closed writer rethrows a different stored error, fix the original publish failure before reopening","Use a single owner of the writer lifecycle to avoid double-close races"],"tags":["writer","lifecycle","closed-resource","storage"],"backgroundTag":"writer-already-closed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}