{"record":{"id":"082cbbbb184d9f41","repo":"can1357/oh-my-pi","slug":"daemon-operation-name-is-record-snapshot-stat","errorCode":null,"errorMessage":"Daemon ${operation.name} is ${record.snapshot.state}","messagePattern":"Daemon (.+?) is (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/launch/broker.ts","lineNumber":1114,"sourceCode":"\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) {\n\t\t\tthrow new Error(\"send requires data or signal\");\n\t\t}\n\t\tif (operation.data !== undefined) {\n\t\t\tif (record.pty) record.pty.write(operation.data);\n\t\t\telse if (record.input) {\n\t\t\t\trecord.input.write(operation.data);\n\t\t\t\tawait record.input.flush();\n\t\t\t} else throw new Error(`Daemon ${operation.name} stdin is unavailable`);\n\t\t}\n\t\tif (operation.signal) {\n\t\t\tif (process.platform === \"win32\" && record.pty) {\n\t\t\t\tif (operation.signal === \"SIGINT\") record.pty.write(\"\\u0003\");\n\t\t\t\telse record.pty.kill();\n\t\t\t} else {\n\t\t\t\tconst processRef = record.snapshot.pid === undefined ? null : Process.fromPid(record.snapshot.pid);\n\t\t\t\tif (!processRef) throw new Error(`Daemon ${operation.name} process is unavailable`);","sourceCodeStart":1096,"sourceCodeEnd":1132,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/launch/broker.ts#L1096-L1132","documentation":"DaemonBroker's send path (#send) refreshes the daemon snapshot and refuses to deliver data or signals to a daemon that is in a terminal state (exited/failed etc.) or currently stopping. Sending would be a no-op or write into a dead pipe, so the broker throws with the daemon's current state in the message.","triggerScenarios":"Calling a send operation ({ op: \"send\", name, data|signal }) while the named daemon is exited, crashed, or in the middle of stopping.","commonSituations":"Client raced with daemon shutdown; a previous stop command was issued but the client kept sending input; daemon crashed earlier and the client's cache was stale.","solutions":["Check the daemon snapshot state (list/get) before sending; skip or queue sends for non-running daemons.","Restart the daemon if input must be delivered, then re-send.","Remove the stale stop/send sequence from the caller so it doesn't write after issuing stop."],"exampleFix":"// before\nawait broker.stop({ name: \"worker\" });\nawait broker.send({ name: \"worker\", data: \"flush\\n\" }); // throws: daemon is stopping\n// after\nawait broker.send({ name: \"worker\", data: \"flush\\n\" });\nawait broker.stop({ name: \"worker\" });","handlingStrategy":"validation","validationCode":"const snap = await broker.get(name);\nconst dead = !snap || snap.state === \"stopping\" || [\"exited\", \"failed\", \"crashed\", \"stopped\"].includes(snap.state);\nif (dead) throw new Error(`skip send: ${name} is ${snap?.state ?? \"unknown\"}`);","typeGuard":"function isSendableState(s: { state: string }): boolean {\n  return s.state === \"running\" || s.state === \"starting\";\n}","tryCatchPattern":"try {\n  await broker.send({ name, data });\n} catch (err) {\n  if (err instanceof Error && /^Daemon .* is (exited|failed|stopping|stopped)/.test(err.message)) {\n    // daemon dead/stopping: restart if more input must be delivered\n  } else throw err;\n}","preventionTips":["Order operations so all sends happen before stop.","Track daemon lifecycle in the client and stop sending once a stop was requested.","Refresh snapshots before sends in long-running supervisors to avoid stale state."],"tags":["daemon","state-conflict","ipc"],"backgroundTag":"daemon-not-running","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}