fix(FAR-123): dedup replayed K8s log lines at the streaming UI boundary #7

Merged
farhoodliquor-paperclip[bot] merged 3 commits from fix/far-123-duplicate-output-logs into master 2026-04-22 19:51:40 +00:00
farhoodliquor-paperclip[bot] commented 2026-04-22 19:49:54 +00:00 (Migrated from github.com)

Summary

  • Duplicate assistant text blocks showed up in the live agent UI between successive tool calls (see FAR-123 screenshot).
  • Root cause: the K8s log follow stream replays the last few seconds on every reconnect because sinceSeconds is integer-second with a +5s safety buffer. FAR-105 dedupped these replays only at the final parser (parse.ts), but the streaming UI consumes raw onLog chunks and still saw every duplicate.
  • Fix: a stateful line-level dedup filter (LogLineDedupFilter) wrapped around onLog inside streamPodLogs, shared across reconnects. Claude stream-json events are keyed by their stable structural IDs (message.id, tool_use_id, session_id); non-JSON output passes through unchanged.

Changes

  • src/server/log-dedup.ts — new filter with chunk→line buffering, structural-key dedup, and end-of-stream flush.
  • src/server/log-dedup.test.ts — 17 unit tests covering unique/replay cases, incomplete-line buffering, non-JSON passthrough, and the classic FAR-123 replay scenario.
  • src/server/execute.ts — instantiate one filter per run, thread it into streamPodLogsOnce, flush on stream end.

Test plan

  • npm run typecheck
  • npm test — all 241 tests pass (17 new)
  • Stage-deploy and confirm the duplicate Three nits to fix… / The reviewer's nits are valid… entries no longer appear between tool calls in the live agent UI.

🤖 Generated with Claude Code

## Summary - Duplicate assistant text blocks showed up in the live agent UI between successive tool calls (see FAR-123 screenshot). - Root cause: the K8s log follow stream replays the last few seconds on every reconnect because `sinceSeconds` is integer-second with a +5s safety buffer. FAR-105 dedupped these replays only at the final parser (`parse.ts`), but the streaming UI consumes raw `onLog` chunks and still saw every duplicate. - Fix: a stateful line-level dedup filter (`LogLineDedupFilter`) wrapped around `onLog` inside `streamPodLogs`, shared across reconnects. Claude stream-json events are keyed by their stable structural IDs (`message.id`, `tool_use_id`, `session_id`); non-JSON output passes through unchanged. ## Changes - `src/server/log-dedup.ts` — new filter with chunk→line buffering, structural-key dedup, and end-of-stream flush. - `src/server/log-dedup.test.ts` — 17 unit tests covering unique/replay cases, incomplete-line buffering, non-JSON passthrough, and the classic FAR-123 replay scenario. - `src/server/execute.ts` — instantiate one filter per run, thread it into `streamPodLogsOnce`, flush on stream end. ## Test plan - [x] `npm run typecheck` - [x] `npm test` — all 241 tests pass (17 new) - [ ] Stage-deploy and confirm the duplicate `Three nits to fix…` / `The reviewer's nits are valid…` entries no longer appear between tool calls in the live agent UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.