{"record":{"id":"dd95ce2f7aa2f833","repo":"can1357/oh-my-pi","slug":"unexpected-broker-response-next-op","errorCode":null,"errorMessage":"Unexpected broker response ${next.op}","messagePattern":"Unexpected broker response (.+?)","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cli/ps-cli.ts","lineNumber":253,"sourceCode":"\t}\n\tconst initial = first.text.replace(/\\n$/, \"\").split(\"\\n\").slice(-lines).join(\"\\n\");\n\tif (initial) process.stdout.write(`${initial}\\n`);\n\tlet previous = first.text;\n\tlet cursor = first.cursor;\n\tlet state = first.state;\n\twhile (!TERMINAL_STATES[state]) {\n\t\tconst next = await client.request({\n\t\t\top: \"logs\",\n\t\t\tname,\n\t\t\tlines: 1_000,\n\t\t\thead: false,\n\t\t\tgrep: cmd.flags.grep,\n\t\t\tfollow: true,\n\t\t\tcursor,\n\t\t\trenderTerminalRows: false,\n\t\t\ttimeoutMs: 30_000,\n\t\t});\n\t\tif (next.op !== \"logs\") throw new Error(`Unexpected broker response ${next.op}`);\n\t\t// The broker always returns the tail window (cursor is only a wait\n\t\t// watermark), so trim the part we already printed.\n\t\tconst fresh = next.text.slice(overlapLength(previous, next.text));\n\t\tif (fresh) process.stdout.write(fresh.endsWith(\"\\n\") ? fresh : `${fresh}\\n`);\n\t\tprevious = next.text;\n\t\tcursor = next.cursor;\n\t\tstate = next.state;\n\t}\n\tconsole.log(chalk.dim(`[${name}: ${state}]`));\n}\n\n/** Longest suffix of `previous` that is a prefix of `next` — the already-printed portion of a tail window. */\nfunction overlapLength(previous: string, next: string): number {\n\tfor (let k = Math.min(previous.length, next.length); k > 0; k--) {\n\t\tconst offset = previous.length - k;\n\t\tlet match = true;\n\t\tfor (let i = 0; i < k; i++) {\n\t\t\tif (previous.charCodeAt(offset + i) !== next.charCodeAt(i)) {","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cli/ps-cli.ts#L235-L271","documentation":"In follow mode `runLogs` polls the broker with `{op:\"logs\", follow:true, cursor}` and expects each reply to be a logs response carrying new `text` and a `cursor`. This guard throws when a follow-poll reply has a different op — the loop would otherwise read `next.text`/`next.cursor` on a foreign response shape.","triggerScenarios":"A `--follow` logs session where an intermediate `client.request({op:\"logs\", follow:true, cursor, ...})` resolves with a non-\"logs\" op — typically the broker switching to an error response mid-stream (daemon removed, broker shutting down) or a protocol version change.","commonSituations":"Following logs while the daemon is stopped/removed underneath the stream; the broker dies or is upgraded mid-follow and the revived broker speaks a different protocol; long-running follow sessions crossing an omp upgrade.","solutions":["Re-run `omp ps logs --follow` after confirming the daemon still exists in `omp ps --all`.","Check the broker process is alive and from the same omp version as the CLI; restart the broker if it was upgraded mid-session.","Wrap the follow loop's request in a catch that exits the poll loop cleanly on error ops instead of treating every reply as logs."],"exampleFix":"// before\nconst next = await client.request({ op: \"logs\", name, follow: true, cursor, ... });\nif (next.op !== \"logs\") throw new Error(`Unexpected broker response ${next.op}`);\n// after\nconst next = await client.request({ op: \"logs\", name, follow: true, cursor, ... });\nif (next.op === \"error\") break; // daemon gone; end follow cleanly\nif (next.op !== \"logs\") throw new Error(`Unexpected broker response ${next.op}`);","handlingStrategy":"try-catch","validationCode":"if (previous === undefined && !(await client.request({ op: \"list\" }).then(r => r.op === \"list\" && r.daemons.some(d => d.name === name)))) {\n  throw new Error(`Daemon \"${name}\" not found; follow aborted`);\n}","typeGuard":"function isLogsResponse(r: { op: string }): r is { op: \"logs\"; text: string; cursor: string; state: string } {\n  return r.op === \"logs\";\n}","tryCatchPattern":"try {\n  while (following) {\n    const next = await client.request({ op: \"logs\", name, follow: true, cursor });\n    if (!isLogsResponse(next)) break; // end follow cleanly on foreign/error replies\n    /* print fresh text */\n    cursor = next.cursor;\n  }\n} catch (err) {\n  console.error(chalk.red(`follow ended: ${err instanceof Error ? err.message : String(err)}`));\n}","preventionTips":["Exit follow loops gracefully on non-logs replies instead of throwing mid-stream.","Re-verify the daemon still exists before long follow sessions.","Expect brokers to restart during upgrades; re-issue follow after detecting a protocol change."],"tags":["ipc","logs","streaming","protocol-mismatch"],"backgroundTag":"unexpected-response-op","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}