The K8s log follow stream replays the trailing few seconds of output on
every reconnect because `sinceSeconds` uses integer-second granularity
with a small safety buffer. FAR-105 dedupped those replays at the final
parser (parse.ts), but the streaming UI consumes raw onLog chunks and
still showed each replayed assistant/tool event as a fresh entry — which
is how the duplicate "Three nits to fix…" blocks in the screenshot
appeared between successive tool calls.
Fix: add a stateful line-level dedup filter around onLog, shared across
reconnects. Claude stream-json events are keyed by their stable
structural IDs (message.id, tool_use_id, session_id); non-JSON output
(paperclip status lines, shell output) passes through unchanged.
- New `src/server/log-dedup.ts` + tests: LogLineDedupFilter handles
chunk-to-line buffering, replay dedup, and end-of-stream flush.
- `streamPodLogs` instantiates one filter per run so dedup state persists
across reconnect attempts.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Merge both one-shot log fallbacks into a single conditional block using a
cheap string-scan guard (`stdout.includes('"type":"result"')`) to avoid
calling parseClaudeStreamJson twice and prevent double readPodLogs calls
when the first fallback already ran.
- Extract error-message logic into `buildPartialRunError(exitCode, model, stdout)`
(exported for tests) so the `!parsed` branch is a one-liner and the logic
is independently testable.
- Export `isK8s404` for tests.
- Add execute.test.ts with 15 unit tests covering:
- isK8s404: v0.x response.statusCode, v1.0+ response.status, direct
statusCode, message-based detection, non-404 codes
- buildPartialRunError: exitCode=0 path, empty stdout, init-only output
(model surfaced), first non-system content line, null exitCode (-1),
multiple consecutive system events
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add a second log fallback: if the follow stream captured partial output (init
event present but no result event), attempt a one-shot readPodLogs before the
pod is cleaned up. Fast-exiting containers (bad model, missing API key, etc.)
can cause the follow stream to return only the init line before the connection
drops; the one-shot read is more reliable for already-terminated containers.
- Improve the `!parsed` error message: skip system/init events when searching
for the first content line, so the error reads "Claude started but did not
produce a result (model: MiniMax-M2.7) — check API credentials..." instead of
"Claude exited with code -1: {"type":"system","subtype":"init",...}".
Co-Authored-By: Paperclip <noreply@paperclip.ing>