{"record":{"id":"4556ce79368b2365","repo":"can1357/oh-my-pi","slug":"hindsight-retain-queue-is-closed","errorCode":null,"errorMessage":"Hindsight retain queue is closed.","messagePattern":"Hindsight retain queue is closed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/hindsight/state.ts","lineNumber":86,"sourceCode":" */\nexport class HindsightRetainQueue {\n\treadonly #state: HindsightSessionState;\n\t#items: PendingRetainItem[] = [];\n\t#timer?: NodeJS.Timeout;\n\t#flushing?: Promise<void>;\n\t#closed = false;\n\n\tconstructor(state: HindsightSessionState) {\n\t\tthis.#state = state;\n\t}\n\n\tget depth(): number {\n\t\treturn this.#items.length;\n\t}\n\n\tenqueue(content: string, context?: string): void {\n\t\tif (this.#closed) {\n\t\t\tthrow new Error(\"Hindsight retain queue is closed.\");\n\t\t}\n\t\tthis.#items.push({ content, context, timestamp: new Date() });\n\n\t\tif (this.#items.length >= RETAIN_FLUSH_BATCH_SIZE) {\n\t\t\tvoid this.flush();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.#timer) {\n\t\t\tthis.#timer = setTimeout(() => {\n\t\t\t\tvoid this.flush();\n\t\t\t}, RETAIN_FLUSH_INTERVAL_MS);\n\t\t\t// Don't pin the event loop alive just for a pending retain flush.\n\t\t\tthis.#timer.unref?.();\n\t\t}\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.#timer) {","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/hindsight/state.ts#L68-L104","documentation":"The Hindsight retain queue throws when enqueue is called after the queue has been closed. Closing signals shutdown (no further accepts); enqueueing afterwards is a programming error since items would never be flushed.","triggerScenarios":"Calling queue.enqueue(content) after close()/shutdown of the Hindsight state; a background hook or session-end handler firing after teardown; retaining during process exit handling.","commonSituations":"Race between an async flush/shutdown and a late retain call; session teardown ordering where a subscriber fires after the queue closes; long-running background tasks outliving the Hindsight lifecycle.","solutions":["Check/track the closed state before enqueueing (guard call sites during shutdown)","Ensure all producers stop before calling close() — cancel or await pending hooks","Catch this error in fire-and-forget retain paths and drop the item, since flush infrastructure is gone"],"exampleFix":"// before\nqueue.enqueue(memoryText); // may throw after shutdown\n// after\ntry {\n  queue.enqueue(memoryText);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"queue is closed\")) {\n    logger.debug(\"Dropped retain after Hindsight shutdown\");\n    return;\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"// track lifecycle yourself\nif (hindsightState.isClosed) return; // drop instead of enqueue","typeGuard":"function isQueueClosedError(err: unknown): err is Error {\n  return err instanceof Error && err.message === \"Hindsight retain queue is closed.\";\n}","tryCatchPattern":"try {\n  queue.enqueue(text, context);\n} catch (err) {\n  if (isQueueClosedError(err)) {\n    logger.debug(\"Retain dropped: Hindsight already shut down\");\n    return;\n  }\n  throw err;\n}","preventionTips":["Stop all retain producers before calling close()","Await pending async hooks/flushes during shutdown ordering","Make late retain calls no-ops in shutdown paths","Prefer flush() then close() in teardown code"],"tags":["lifecycle","queue","shutdown"],"backgroundTag":"queue-closed-after-shutdown","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}