{"record":{"id":"f369ee704e2e1757","repo":"can1357/oh-my-pi","slug":"daemon-operation-name-generation-boundgenerat","errorCode":null,"errorMessage":"Daemon ${operation.name} generation ${boundGeneration} exited${exit}; the wait was rejected instead of continuing against a replacement generation","messagePattern":"Daemon (.+?) generation (.+?) exited(.+?); the wait was rejected instead of continuing against a replacement generation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/launch/broker.ts","lineNumber":1098,"sourceCode":"\t\tconst generationEnded = (): boolean =>\n\t\t\trecord.generation !== boundGeneration || record.snapshot.state === \"restarting\";\n\t\tconst condition = (): boolean => {\n\t\t\tif (generationEnded()) return true;\n\t\t\tif (pattern) {\n\t\t\t\tconst match = pattern.exec(record.readinessBuffer);\n\t\t\t\tif (!match) return false;\n\t\t\t\tmatched = match[0].slice(0, 500);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (operation.for === \"exit\") return terminalState(record.snapshot.state);\n\t\t\t// Wake on observed readiness or any terminal state so the wait never\n\t\t\t// blocks for the full timeout; success is judged by readyObserved below.\n\t\t\treturn readyObserved() || terminalState(record.snapshot.state);\n\t\t};\n\t\tconst woke = condition() || (await this.#waitUntil(record, condition, operation.timeoutMs));\n\t\tif (generationEnded()) {\n\t\t\tconst exit = record.snapshot.exitCode === undefined ? \"\" : ` with exit code ${record.snapshot.exitCode}`;\n\t\t\tthrow new Error(\n\t\t\t\t`Daemon ${operation.name} generation ${boundGeneration} exited${exit}; ` +\n\t\t\t\t\t\"the wait was rejected instead of continuing against a replacement generation\",\n\t\t\t);\n\t\t}\n\t\t// A for:\"ready\" wait that woke on a terminal exit without ever observing\n\t\t// readiness is still \"not ready\" — surface it as timed out so callers and the\n\t\t// renderer don't chain work against a dead process.\n\t\tconst timedOut = operation.for === \"ready\" && !pattern ? !readyObserved() : !woke;\n\t\treturn { op: \"wait\", daemon: record.snapshot, matched, timedOut };\n\t}\n\n\tasync #send(operation: Extract<DaemonOperation, { op: \"send\" }>): Promise<DaemonRpcResult> {\n\t\tconst record = this.#record(operation.name);\n\t\tawait this.#refreshDetached(record);\n\t\tif (terminalState(record.snapshot.state) || record.snapshot.state === \"stopping\") {\n\t\t\tthrow new Error(`Daemon ${operation.name} is ${record.snapshot.state}`);\n\t\t}\n\t\tif (operation.data === undefined && operation.signal === undefined) {","sourceCodeStart":1080,"sourceCodeEnd":1116,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/launch/broker.ts#L1080-L1116","documentation":"When a wait operation is in progress and the specific daemon generation being waited on exits (crashes or terminates), the broker rejects the wait instead of silently transferring it to a replacement generation started under the same name. The message includes the generation number and, if known, the exit code. This prevents a caller from waiting on stale readiness state of a daemon process it did not launch.","triggerScenarios":"Calling wait with a bound generation while that daemon process exits before the condition is met or the timeout elapses — e.g. the daemon crashes mid-wait, or is killed externally during the wait window.","commonSituations":"Daemon crashed due to a bad startup config; supervisor or OOM killer terminated the process; someone restarted the daemon while a readiness wait was outstanding.","solutions":["Inspect daemon logs/snapshot to find why the generation exited and fix the underlying crash before retrying.","Restart the daemon and re-issue the wait against the new generation.","Check exitCode in the error/snapshot — a signal death suggests external kill (OOM, supervisor), a nonzero code suggests an app error."],"exampleFix":"// before\nawait broker.wait({ name: \"api\", for: \"ready\", generation: 3, timeoutMs: 30000 }); // generation 3 died\n// after\ntry {\n  await broker.wait({ name: \"api\", for: \"ready\", generation: 3, timeoutMs: 30000 });\n} catch (err) {\n  // generation ended: restart then wait on the fresh generation\n  await broker.start({ name: \"api\", ...spec });\n  await broker.wait({ name: \"api\", for: \"ready\", timeoutMs: 30000 });\n}","handlingStrategy":"try-catch","validationCode":"// Cannot be pre-validated: the exit happens asynchronously during the wait.\n// Optionally check current state first:\nconst snap = await broker.get(name);\nif (snap && [\"exited\", \"failed\", \"crashed\"].includes(snap.state)) throw new Error(`${name} already dead`);","typeGuard":null,"tryCatchPattern":"try {\n  await broker.wait({ name, for: \"ready\", generation, timeoutMs });\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"exited\") && err.message.includes(\"replacement generation\")) {\n    // inspect logs/exitCode, fix root cause, restart and re-wait\n  } else throw err;\n}","preventionTips":["Ensure the daemon is healthy (valid config, dependencies present) before issuing long waits.","React to the exit by restarting the daemon and waiting on the new generation, never by retrying the stale generation.","Record exitCode from the error to distinguish crashes from external kills (OOM/supervisor)."],"tags":["daemon","process-exit","wait","race"],"backgroundTag":"process-exited-during-wait","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}