fix(riff-backend): 修复 web 终端 stdout 日志糊成墙(SSE 逐行日志丢尾换行) - #805
Merged
Conversation
riff ships each stdout log line bare (no trailing newline): the runner logger persists message verbatim and the SSE `log` event carries it as `text` unchanged. For the codex_app_server adapter that message is one JSON.stringify(event) per line (thread.started/turn.started/item.*). Since emitText only normalizes existing newlines and never adds a separator, consecutive events concatenated into one unreadable wall in the web terminal. Emit `text + '\n'` on the stdout-log path only. Non-duplicating because the stored line has no trailing newline. The output/chunk path is left raw — those chunks may be partial lines and must not get a synthetic newline. Adds 3 regression tests (separation / no double-space / chunk path raw).
deepcoldy
commented
Aug 10, 2026
deepcoldy
left a comment
Owner
Author
There was a problem hiding this comment.
Review 结论:未发现阻塞问题,代码层面 LGTM。当前账号与 PR 作者相同,GitHub 不允许 self-approve,因此以 review comment 记录结论。
核验结论:
codex_app_server上游确实按逐行、无尾换行落库:emitNormalized先生成line = JSON.stringify(event),logger.info(line, { group: 'stdout' })持久化裸行;带\n的${line}\n只送onOutput,不进入日志表。Logger.createRootLog仅 mask/truncate,不补行尾;runnerLogToTaskLogEvent将text直接设为log.message,RunnerTaskStreamPoller再原样序列化进 SSE。因此到 botmux 的event:logstdouttext没有行尾,补一个\n不会重复。- 普通 exec 路径同样由
readline的line事件去掉行尾后,以裸line+{ group }落库,和上述契约一致。 - 使用
emitText而非emitLine是正确的:前者只做\n -> \r\n归一化;后者会增加 ANSI 及前后 CRLF,确实会造成额外空行。 event:output是 chunk 语义,保持原样是必要边界;新增 chunk 拼接测试覆盖到了这一点。
独立验证:
pnpm exec vitest run --project unit test/riff-backend.test.ts:50/50 通过。pnpm exec tsc --noEmit:通过。git diff --check:通过。
当前 GitHub build 红灯来自 codex-app-runner.integration 超时和 worker-argv-reaction-status.integration 时序断言;base master 的 7cd22db CI 已以相同两组用例失败,且本 PR 单跑 codex-app-runner.integration 42/42 通过,判断与本 diff 无关。worker-argv-reaction-status.integration 在本地 base/PR 环境均可复现同样失败。
|
🚀 Released in v3.12.0 |
deepcoldy
added a commit
that referenced
this pull request
Aug 10, 2026
…inal (#812) * feat(riff-backend): render route-B display projection in the web terminal riff's feat/riff-agent-log-display attaches a per-line `display: TaskLogDisplay` to stdout log SSE events — a stateless, human-readable projection of a codex app-server event (回答 / 思路 / 命令 …). Consume it: - display present → render a timeline row via emitDisplay(): [回答]/[思路]/ [命令] <cmd> (exit N)/[工具]/[错误]/… with kind-driven color (failed command or error → red, completion → green, reasoning/usage dimmed). The Chinese label comes from riff's localized `title` when present, else a kind→label fallback. - display absent → keep the #805 raw fallback (emitText(text + '\n')). - defensive backstop: if a bare codex lifecycle event (thread.started/ item.started/…) slips through un-projected (riff normally downgrades these to channel:'raw' so a default subscription never sees them), suppress it rather than re-wall. Narrow: only single-line JSON with a known no-content `type` — never plain shell output that merely contains braces. The output/chunk (event:output) path stays untouched. Tests: +5 (display→row, command+exit+color, absent→fallback, noise suppressed, braces-not-suppressed); updated the char-wall separation test to use content- bearing lines since lifecycle markers are now suppressed. riff-backend 55/55, tsc clean. * fix(riff-backend): address codex review on route-B display rendering Codex review of #812 found 2 user-visible blockers + 4 alignment items, all confirmed against riff's committed contract (feat/riff-agent-log-display): blockers: - command projection has NO `text` (only {command,status,exitCode,summary}); the old code fell back to `summary` and printed '命令执行完成' as a fake output line under every command. Command branch now renders ONLY its header; the real captured stdout rides a SEPARATE stdout event on the verbatim passthrough path. - reusing emitLine double-spaced the timeline (leading+trailing CRLF per row) and left internal \n un-normalized (xterm stair-step). Added emitTimelineRow: no leading CRLF, exactly one trailing CRLF, all internal newlines → CRLF. alignment: - noise set now matches riff's CODEX_NOISE_EVENT_TYPES exactly (+response.completed /response.done). - added stage/trace to the label table (were falling to English default). - reasoning/usage now use a real ANSI dim (\x1b[2m) instead of no-op 'plain'. - running command (no exit code) is neutral, not green — green only for completed/exit 0, red for failure. Tests: 61/61 (+6: completed/failed/running command color, dim reasoning, no double-space, multi-line CRLF normalization, response.* noise). tsc clean. * fix(riff-backend): render captured command output below the command header Resolves the cross-end blocker from codex re-review of #812: riff surfaces only events[last] (the command header) as the single display, so the command's stdout was being dropped entirely — the timeline showed '[命令执行] echo hello (exit 0)' with no output. riff !1177 (commit a2433315) chose option A: collapseCommandOutputIntoPrimary folds the (32KB-truncated) command stdout/stderr into commandDisplay.text. This consumer now renders display.text below the header, verbatim + CRLF-normalized. Not in conflict with the earlier blocker-1 fix: the summary field ('命令执行完成') is still never rendered (we read text, not summary); text is now the real output. Tests 63/63 (+2: command output folded into text is rendered; multi-line output CRLF-normalized). tsc clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
codex_app_server 路径下,飞书 web 终端的日志正文"糊成墙"——多条 stdout 日志首尾相接、零分隔,不可读(申晗截图复现,截图里能看到
thread.started/turn.started,确认走的是 main 上的 app_server 新路径,不是老 exec)。根因:SSE 逐行日志丢尾换行
riff 端逐行、无尾换行地存/发 stdout 日志:
appServer.tsemitNormalized:logger.info(JSON.stringify(event), {group:'stdout'})存的是裸行(带\n的那份喂给另一个 sink,不进日志流)。codex_app_server 下这条 message 就是一条JSON.stringify(event)(thread.started / turn.started / item.* …)。message时只做 mask/truncate,不加换行。event: log的text=log.message原样。到本仓 relay 这端,
emitText只把已有\r?\n归一化(\n→\r\n),从不给每条日志补分隔。于是case 'log'的 stdout 分支emitText(text)把逐条裸行首尾相接 → 糊成墙。修法
src/adapters/backend/riff-backend.ts的case 'log',group === 'stdout'分支:因 riff 逐行、无尾换行存,补
\n精确不重复。两个刻意的边界
emitLine:emitLine会包 ANSI 色 + 前后\r\n空行(状态行语义),会把每条 stdout 撑成双倍行距。stdout 原始流只需要"补一个分隔",所以走emitText(text+'\n')。output/chunk 路径故意不动:event: output的 chunk 是字节流、可能半行语义,给它补\n会把一行劈两段。只有log事件是逐行语义,补换行才安全。影响面
只影响 web 终端实时日志流的渲染(把"糊成墙"降为"每行一个规整 JSON 事件"可读)。不改任何数据、不动终报路径、不动 chunk 流。
测试
新增 3 个回归测试(
test/riff-backend.test.ts):}{,各自…\r\n)hello→hello\r\n)par+tial→partial,无注入换行)riff-backend.test.ts50/50 绿,tsc --noEmit对改动文件干净。