Visitar URL original
fix(quality): keep signal excerpts on rune boundaries by MaxFreedomPollard · Pull Request #1788 · kenn-io/agentsview · GitHub
Skip to content

fix(quality): keep signal excerpts on rune boundaries - #1788

Merged
mariusvniekerk merged 3 commits into
kenn-io:mainfrom
MaxFreedomPollard:fix/excerpt-utf8
Sep 17, 2026
Merged

mariusvniekerk merged 3 commits into
kenn-io:mainfrom
MaxFreedomPollard:fix/excerpt-utf8

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

On the Quality page, a signal example whose excerpt runs past 180 bytes can render with a black diamond replacement character at the end instead of its last whole character. A message like "Fix this: 这个函数返回错误的结果..." comes back with the final character destroyed. The excerpt has one consumer, the signal sessions panel in frontend/src/lib/components/quality/QualityPage.svelte.

truncateExcerpt in internal/db/analytics.go cuts with a plain byte slice, s[:max-3] + "...". Its only caller is BuildSignalExamples, which passes 180, so the cut always lands on byte 177. When byte 177 is a continuation byte the returned string is invalid UTF-8, and encoding/json rewrites the split character into U+FFFD on the way to the API. 177 is a multiple of three, so a pure Han excerpt happens to survive; a ten-byte ASCII lead such as "Fix this: " shifts the alignment and breaks it, four-byte emoji break on a different lead, and two-byte Cyrillic on a third. The content arriving here is already valid UTF-8 through db.SanitizeUTF8, so the invalid bytes are introduced at this one line.

Both slices now back up to the nearest rune start. That walk already existed inline in ClampModel, so it moves into a runeBoundaryAtOrBefore helper in internal/db/validate.go beside the utf8RuneStart predicate it uses, and ClampModel calls it too. The excerpt cut reuses that walk instead of adding a second copy of it. ASCII excerpts keep byte-identical output and the 180-byte budget stays an upper bound.

One behavior change worth naming: on the degenerate max <= 3 branch a multibyte string now yields an empty string rather than a broken fragment. The only caller passes 180, so that branch is unreachable in production; it is kept correct rather than removed. Backend parity holds by construction, since internal/postgres/analytics.go and internal/duckdb/analytics_usage.go both build their examples by calling db.BuildSignalExamples.

Reviewer attention is best spent on the ClampModel hunk in internal/db/validate.go, the one existing caller this change reshapes. truncateLogLine in internal/insight/generate.go has the same defect against a 16 KiB stdout budget; I left it out to keep this change to one concern. I did not run the PostgreSQL or DuckDB suites locally.

@roborev-ci

roborev-ci Bot commented Sep 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (72bab7c)

Verdict: No findings at or above medium severity.


Reviewers: 2 done | Synthesis: codex | Total: 59s

@mariusvniekerk mariusvniekerk self-assigned this Sep 16, 2026
@roborev-ci

roborev-ci Bot commented Sep 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (7aa3718)

Verdict: No findings at or above medium severity.


Reviewers: 2 done | Synthesis: codex | Total: 6m33s

@roborev-ci

roborev-ci Bot commented Sep 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (d7a9e77)

Verdict: No findings at or above medium severity.


Reviewers: 2 done | Synthesis: codex | Total: 2m35s

@roborev-ci

roborev-ci Bot commented Sep 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (e2b94c6)

Verdict: Changes require fixes for 1 finding.

Medium

  • internal/stringutil/truncate.go:28: utf8string.NewString materializes rune-index data proportional to the entire input before returning a short prefix, potentially causing substantial temporary memory use or OOM for large transcripts. Implement truncation using utf8.RuneCountInString plus a range scan that stops at the requested rune limit, or otherwise avoid building a full-string index for prefix truncation.

    Reported by: codex


Reviewers: 2 done | Synthesis: codex, 5s | Total: 6m23s

@roborev-ci

roborev-ci Bot commented Sep 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (042c57d)

Verdict: No findings at or above medium severity.


Reviewers: 2 done | Synthesis: codex | Total: 6m37s

MaxFreedomPollard and others added 3 commits September 17, 2026 14:19
truncateExcerpt in internal/db/analytics.go cut its input with a plain
byte slice, so an excerpt longer than the 180-byte budget was severed at
byte 177 wherever that landed. When byte 177 falls inside a multibyte
character the result is invalid UTF-8, and encoding/json rewrites the
broken tail into U+FFFD, so a signal example came back to the Quality
page ending in one or two replacement characters instead of its last
whole character. 177 is a multiple of three, so a pure Han excerpt
happened to survive, but a ten-byte ASCII lead such as "Fix this: "
shifts the alignment and breaks it; four-byte emoji and two-byte
Cyrillic break on other leads.

Both slices now walk back to the nearest rune start. That walk already
existed inline in ClampModel, so it moves out into a
runeBoundaryAtOrBefore helper in internal/db/validate.go beside the
utf8RuneStart predicate it uses, and ClampModel calls it too. The
excerpt cut reuses that walk instead of adding a second copy of it.
ASCII excerpts keep byte-identical output and the budget stays an upper
bound. The one behavior change is on the degenerate max <= 3 branch,
where a multibyte string now yields an empty string rather than a broken
fragment; the only caller passes 180, so that branch is unreachable in
practice.

The PostgreSQL and DuckDB stores build their examples by calling
db.BuildSignalExamples, so all three backends take the fix from this
single function with no twin edits to keep in step.
@roborev-ci

roborev-ci Bot commented Sep 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (8403baf)

Verdict: No findings at or above medium severity.


Reviewers: 2 done | Synthesis: codex | Total: 2m35s

@mariusvniekerk
mariusvniekerk merged commit 193106b into kenn-io:main Sep 17, 2026
48 of 50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants