{"record":{"id":"2913c8d3c126a4b3","repo":"github/copilot-sdk","slug":"cannot-log-after-the-factory-run-has-settled","errorCode":null,"errorMessage":"Cannot log after the factory run has settled","messagePattern":"Cannot log after the factory run has settled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nodejs/src/session.ts","lineNumber":253,"sourceCode":"            return previous;\n        })\n    );\n}\n\nclass FactoryProgressBuffer {\n    private nextSeq = 0;\n    private pending: FactoryLogLine[] = [];\n    private flushTimer?: ReturnType<typeof setTimeout>;\n    private flushTail: Promise<void> = Promise.resolve();\n    private flushError: unknown;\n    private flushFailed = false;\n    private closed = false;\n\n    constructor(private readonly send: (lines: FactoryLogLine[]) => Promise<void>) {}\n\n    enqueue(kind: FactoryLogLine[\"kind\"], text: string): void {\n        if (this.closed) {\n            throw new Error(\"Cannot log after the factory run has settled\");\n        }\n\n        this.pending.push({ seq: this.nextSeq++, kind, text });\n        this.scheduleFlush();\n    }\n\n    async flush(): Promise<void> {\n        this.clearFlushTimer();\n        const lines = this.pending.splice(0);\n        if (lines.length > 0) {\n            this.flushTail = this.flushTail.then(async () => {\n                try {\n                    await this.send(lines);\n                } catch (error) {\n                    if (!this.flushFailed) {\n                        this.flushFailed = true;\n                        this.flushError = error;\n                    }","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/nodejs/src/session.ts#L235-L271","documentation":"FactoryLogSink.enqueue (logger factory) queues log lines and flushes them on a 10ms delay while the factory run is live. Once the run has settled, closed is set to true and further logging is rejected — late logs could never be delivered and would silently vanish, so the library surfaces this instead.","triggerScenarios":"Invoking ctx.log (enqueue) after the factory run settled: fire-and-forget promises that resolve late, setTimeout/setInterval callbacks, or event listeners that survive past the run's completion.","commonSituations":"A thunk that starts a background fetch and logs its result after Promise.all resolved; debounced code logging on a timer that outlives the run; unawaited async work inside a factory.","solutions":["Ensure all logging happens before the factory run's promises settle — await async work inside the thunk.","Guard late log calls: capture the error and drop or buffer logs after settle.","Move long-running background work outside the factory run, with its own lifecycle."],"exampleFix":"// before\nsetTimeout(() => ctx.log(\"info\", \"done\"), 100); // run already settled\n// after\nawait new Promise((r) => setTimeout(r, 100));\nctx.log(\"info\", \"done\");","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { ctx.log('info', 'done'); } catch (e) { if (String(e.message).includes('settled')) return; throw e; }","preventionTips":["Await all async work inside factory thunks; avoid fire-and-forget promises.","Clear timers and unsubscribe event listeners when the run completes.","Add a safeLog wrapper that no-ops after settle."],"tags":["lifecycle","logging","async"],"backgroundTag":"invalid-state-transition","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}