Skip to content

fix: decode automation precheck output as one UTF-8 stream - #447

Open
Sma1lboy wants to merge 1 commit into
mainfrom
fix/precheck-utf8-chunk-decode
Open

fix: decode automation precheck output as one UTF-8 stream#447
Sma1lboy wants to merge 1 commit into
mainfrom
fix/precheck-utf8-chunk-decode

Conversation

@Sma1lboy

Copy link
Copy Markdown
Owner

Direction

Bugs / correctness — a self-found defect surfaced while reviewing the daemon's stateful/behavioral modules (the pure-logic helpers reviewed clean).

Problem

runAutomationPrecheck captures a precheck command's stdout/stderr as raw data-event chunks, then tail() decoded each Buffer on its own before joining:

const text = chunks.map((c) => c.toString()).join("")

Node splits a pipe's data events at arbitrary byte offsets (no setEncoding is set on the spawn), so a multi-byte UTF-8 sequence can straddle two chunks. Per-chunk .toString() decodes each half independently and emits a at the seam. A precheck like gh pr list or git log whose output carries CJK or emoji therefore came back mojibake in the recorded skip result the user reads to tell a healthy "nothing to do" from a broken command. Separately, text.slice(-MAX_OUTPUT_CHARS) capped by UTF-16 code unit, which can halve a surrogate pair at the cut and strand a lone surrogate.

The pass/skip verdict itself is driven by exit code, so the feature's decision is unaffected — this is a fidelity bug in the captured output shown to the user.

Fix

tail() now concatenates each contiguous run of Buffers and decodes it as one UTF-8 stream (Buffer.concat(...).toString("utf8")), interleaving the pre-decoded error strings we push ourselves in order, and caps the tail by whole code points so the 4000-char limit can't split a surrogate pair. The helper is exported so the decode is unit-testable directly.

Verification

  • bun run lint — clean.
  • bun run typecheck — clean.
  • New deterministic unit tests over tail() (construct Buffers split mid-character to reproduce the seam without depending on real spawn chunking): the split-🚀/CJK case now decodes without , error strings stay ordered, and the over-cap case keeps 4000 whole rockets. KOBE_INCLUDE_SOCKET=1 vitest run test/daemon/automation-precheck.test.ts → 11 passed (7 existing + 4 new).

Follow-ups (deliberately deferred — separate slice)

While verifying, I noticed the test suite is already red on main, unrelated to this change:

  • test/architecture/package-distribution.test.ts > pending changesets version the canonical package fails because .changeset/live-sidebar-tab-title.md (added by the most recent merge, fix: keep sidebar tab titles live for unselected tasks #445) targets @sma1lboy/kobe instead of the canonical @sma1lboy/rove. One-line fix to that changeset's frontmatter, but it belongs to fix: keep sidebar tab titles live for unselected tasks #445's slice — flagging rather than folding it in. (My changeset here correctly targets @sma1lboy/rove.)
  • test/state/layout-migration.test.ts > … after a partial failure and retries fails only when the suite runs as root (it simulates a write failure via chmod, which root bypasses); it passes under a normal CI user.

Neither is caused by this PR.


Generated by Claude Code

The precheck captured stdout/stderr chunk-by-chunk and decoded each Buffer on
its own, so a multi-byte UTF-8 sequence split across two `data` events became a
replacement character — a `gh pr list` or `git log` precheck with CJK/emoji came
back mojibake in the skip record. Concatenate each contiguous run of Buffers and
decode once, and cap the tail by whole code points so the 4000-char limit can't
halve a surrogate pair. Adds deterministic unit tests over the decode helper.
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
kobe-landing Ignored Ignored Aug 15, 2026 4:40pm

Request Review

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @Sma1lboy's task in 1m 49s —— View job


Reviewing PR #447

  • Read AGENTS.md / CLAUDE.md and gather PR context
  • Read changed files (automation-precheck.ts, test file, changeset)
  • Run specialized review agents (code-reviewer, silent-failure-hunter, pr-test-analyzer, type-design-analyzer, comment-analyzer, code-simplifier) — running in background
  • Check house rules (attribution, changeset, file-size cap, regression test, no implementation-mirroring tests)
  • Synthesize consolidated review and post

Sma1lboy added a commit that referenced this pull request Aug 27, 2026
#447) (#584)

* fix(api): reject malformed integer flags instead of silently coercing

parseInt stops at the first non-digit, so --id 5abc parsed as 5 and
flipped the status of a real, wrong issue while exiting 0, and --count
1e3 quietly spawned one task. A shared parsePositiveInt helper now
requires the whole value to be digits (safe integer, > 0) and replaces
all three bare parseInt sites: the spec gate, VerbArgs.int, and the
--agents count parser.

* fix(api): read-output tail keeps its last line over the byte cap

boundedTail's byte-budget loop counted the final line first, so a
single line larger than 64KB (a minified dump, a base64 blob with no
newline) pushed the window start past the end and returned a completely
empty tail — a coordinator agent reading the pane got nothing and drew
the wrong conclusion. The loop now never trims away the last line,
mirroring buildHistoryPage's never-fewer-than-one floor.

* fix(daemon): match the dashboard bind host case-insensitively

Browsers (and the WHATWG URL parser) lowercase the Origin host they
send, but allowedHost was compared with its original case, so binding
to an mDNS name like MyMac.local 403'd every LAN browser request.
Hostnames are case-insensitive (RFC 4343); the comparison now is too.

* fix(daemon): decode automation precheck output as one UTF-8 stream

Node splits pipe data events at arbitrary byte offsets, so decoding
each chunk on its own turned a multi-byte character straddling the seam
into a replacement char — a gh pr list precheck full of CJK/emoji
titles came back mojibake. Concatenate the raw bytes and decode once,
and cap the tail by whole code points so the slice can't strand a lone
surrogate at the cut.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants