DirectProject 流式正文被逐 delta 脱敏写坏:Markdown 表格/段落/列表整体失效(#384 引入) #532

Closed
opened 2026-09-29 21:33:23 +08:00 by suzmii · 0 comments
Member

现象

AGC 项目对话里 agent 的汇报正文渲染错位:段落并进同一行、序号项挤成一段、Markdown 表格整块不渲染(表头 | 素材 | 用途 | 路径 |、|---|---|---| 与数据行以纯文本连在同一行里);同一段文本里的路径还出现 game<absolute-path>ame.js、assets<absolute-path>anvas-generated/... 这种被切坏的占位符。重开项目读历史时同一段话渲染正常,因此很容易被当成渲染器偶发或模型写坏了 Markdown。

原因(定位到行)

#384(30c377d0f,2026-09-18)把 DirectProject 流式正文改成每个 delta 各自脱敏(本意正确:前端不得接触原始增量),但脱敏函数不满足拼接不变性,而前端是裸拼接。

  1. apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs direct_codex_thread_delta_event → .../agent/direct_thread_wire.rs direct_thread_delta_text = bounded(sanitize_detail_text(root, delta), 8000),即每个 item.delta 只拿到自己那一段。
  2. .../agent/generation/prompt_context.rs 的 sanitize_error_context 旧实现用 value.lines() 遍历、join("\n") 回填:以换行结尾的段,末尾换行被吃掉("a\n" → "a"、"\n" → "")。
  3. apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts 的 appendLiveText 直接拼接各段,于是段落空行、列表项换行、表格行换行全部消失。
  4. 同函数里 redact_absolute_path_tokens 把 index == 0 当文本边界,delta 从 / 开头时片段被误判成绝对路径:"assets" + "/c" + "anvas-generated" → assets<absolute-path>anvas-generated,而它的整段形式 assets/canvas-generated/... 不会被命中——同一段文本两种结果。

复现与证据

  • 现场:本机 %APPDATA%\world.genarrative.ai-game-creator\projects\gameagent-cfc64e7f\.agent\conversations\project.jsonl 里这条汇报原文完整((项目相对路径):\n\n| 素材 | 用途 | 路径 |\n|---|---|---|),turn-stream.jsonl 同样是完整文本 → 文本没坏,坏在展示链路。
  • 把该真实文本按「每行一个 delta」过 direct_thread_delta_text 再拼接:1445 字符 / 38 个换行 → 1435 字符 / 0 个换行;送进 ChatMarkdownMessage 渲染得 table:0 th:0 li:0 h2:0 p:1(就是截图形态)。同一文本走整段脱敏(item.completed / 历史读回)仍是 table:1 th:3 h2:4 p:8。
  • 回归点确认:git log -S direct_thread_delta_text → 30c377d0f;其父提交里这条分支直接发原始 delta(payload: {"delta": delta.clone()}),当时渲染正常。

影响

  • 任何含表格 / 多段落 / 无序列表的 agent 汇报,在流式阶段都会退化成一段正文,直到重开项目。
  • 与「更长正文优先」的合并规则叠加:<absolute-path>(15) 替换 2 字符会让流式正文比完成态更长,mergeDirectChatEntry 于是保留被搞坏的正文,item.completed 的正确快照覆盖不回界面。
  • 不受影响:project.jsonl 原文、历史读回、工具卡片。

计划

  • 本 Issue 的修复 PR:sanitize_error_context 改为 split_inclusive('\n') 逐段处理、原样保留行终止符,令「逐段脱敏 == 整段脱敏」,并补回归测试。
  • 同源但本 PR 不修:/ 开头的 delta 片段被判成绝对路径(上面第 4 点)。修它需要把「上一个字符」带进边界判定(正文增量可从 DirectProjectHistoryAccumulator::accumulated_text_for 取;思考增量目前没有累计器),属于跨 delta 状态的取舍,单独处理。
## 现象 AGC 项目对话里 agent 的汇报正文渲染错位:段落并进同一行、序号项挤成一段、Markdown 表格整块不渲染(表头 `| 素材 | 用途 | 路径 |`、`|---|---|---|` 与数据行以纯文本连在同一行里);同一段文本里的路径还出现 `game<absolute-path>ame.js`、`assets<absolute-path>anvas-generated/...` 这种被切坏的占位符。**重开项目读历史时同一段话渲染正常**,因此很容易被当成渲染器偶发或模型写坏了 Markdown。 ## 原因(定位到行) `#384`(`30c377d0f`,2026-09-18)把 DirectProject 流式正文改成**每个 delta 各自脱敏**(本意正确:前端不得接触原始增量),但脱敏函数不满足**拼接不变性**,而前端是裸拼接。 1. `apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs` `direct_codex_thread_delta_event` → `.../agent/direct_thread_wire.rs` `direct_thread_delta_text` = `bounded(sanitize_detail_text(root, delta), 8000)`,即每个 `item.delta` 只拿到自己那一段。 2. `.../agent/generation/prompt_context.rs` 的 `sanitize_error_context` 旧实现用 `value.lines()` 遍历、`join("\n")` 回填:**以换行结尾的段,末尾换行被吃掉**(`"a\n"` → `"a"`、`"\n"` → `""`)。 3. `apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts` 的 `appendLiveText` 直接拼接各段,于是段落空行、列表项换行、表格行换行全部消失。 4. 同函数里 `redact_absolute_path_tokens` 把 `index == 0` 当文本边界,delta 从 `/` 开头时片段被误判成绝对路径:`"assets" + "/c" + "anvas-generated"` → `assets<absolute-path>anvas-generated`,而它的**整段形式** `assets/canvas-generated/...` 不会被命中——同一段文本两种结果。 ## 复现与证据 - 现场:本机 `%APPDATA%\world.genarrative.ai-game-creator\projects\gameagent-cfc64e7f\.agent\conversations\project.jsonl` 里这条汇报原文完整(`(项目相对路径):\n\n| 素材 | 用途 | 路径 |\n|---|---|---|`),`turn-stream.jsonl` 同样是完整文本 → 文本没坏,坏在展示链路。 - 把该真实文本按「每行一个 delta」过 `direct_thread_delta_text` 再拼接:1445 字符 / 38 个换行 → 1435 字符 / **0 个换行**;送进 `ChatMarkdownMessage` 渲染得 `table:0 th:0 li:0 h2:0 p:1`(就是截图形态)。同一文本走整段脱敏(`item.completed` / 历史读回)仍是 `table:1 th:3 h2:4 p:8`。 - 回归点确认:`git log -S direct_thread_delta_text` → `30c377d0f`;其父提交里这条分支直接发原始 delta(`payload: {"delta": delta.clone()}`),当时渲染正常。 ## 影响 - 任何含表格 / 多段落 / 无序列表的 agent 汇报,在**流式阶段**都会退化成一段正文,直到重开项目。 - 与「更长正文优先」的合并规则叠加:`<absolute-path>`(15) 替换 2 字符会让流式正文比完成态更长,`mergeDirectChatEntry` 于是保留被搞坏的正文,`item.completed` 的正确快照覆盖不回界面。 - 不受影响:`project.jsonl` 原文、历史读回、工具卡片。 ## 计划 - 本 Issue 的修复 PR:`sanitize_error_context` 改为 `split_inclusive('\n')` 逐段处理、原样保留行终止符,令「逐段脱敏 == 整段脱敏」,并补回归测试。 - 同源但**本 PR 不修**:`/` 开头的 delta 片段被判成绝对路径(上面第 4 点)。修它需要把「上一个字符」带进边界判定(正文增量可从 `DirectProjectHistoryAccumulator::accumulated_text_for` 取;思考增量目前没有累计器),属于跨 delta 状态的取舍,单独处理。
suzmii added the Kind/Bug
Priority
High
2
labels 2026-09-29 21:33:23 +08:00
suzmii self-assigned this 2026-09-29 21:33:23 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#532