Skip to content

feat(protocol-core): 为成功回合推断最终回复阶段以支持 Desktop 过程折叠 - #411

Open
omsd512-W wants to merge 3 commits into
BytePioneer-AI:mainfrom
omsd512-W:feat/final-answer-phase-fallback
Open

omsd512-W wants to merge 3 commits into
BytePioneer-AI:mainfrom
omsd512-W:feat/final-answer-phase-fallback

Conversation

@omsd512-W

@omsd512-W omsd512-W commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

问题

Codex Desktop 在回合完成后,会把最终回复之前的推理、命令和工具调用折叠到"Worked for …"后面。但它只在回合最后一条 Agent 消息带 phase: "final_answer" 时才允许折叠。依据 Codex Desktop 26.924.2738.0 前端:local-conversation-turn 中的 hasFinalAssistantStarted 依赖 phase === "final_answer",conversation-blocks 只在该值为真、回合未取消、且存在可折叠条目时开启折叠。

目前只有 Kiro CLI、Qoder / Qoder CN、Kimi Code 的适配器会标这个 phase。Claude Code、DeepSeek Harness、OpenCode、Grok、Cursor、Pi、OMP、Hermes、Antigravity、CodeBuddy、WorkBuddy 都不标,所以这些 Harness 的回合完成后,过程一直展开。

方案

在 Protocol Core 的投影层统一推断,所有 Harness 一次生效:

  • 新增 packages/protocol-core/src/final-answer-phase.ts。同时满足以下条件时,把 Desktop 可见的最后一条 agentMessage 投影为 final_answer:
    • Desktop 把该回合显示为已完成:实时回合成功结束,或历史回合结果为 succeeded / unknown(即 historicalStatus(...) 为 completed);
    • 该消息自身结果为成功,且文本非空;
    • 适配器没有给出 phase。
  • 推断只改变消息阶段,不改变回合结果;failed、cancelled 回合不推断。
  • 适配器显式给出的 phase 始终优先。显式标 commentary 即可退出推断。
  • 实时推送:CodexTurnProjector 在 turn/completed 之前,重发该消息的 item/completed。原因是 Desktop 处理 turn/completed 时只读状态、错误和耗时,不读 turn.items;而 item/completed 会按 id 替换已有条目。projector 对文件变更汇总项已经在用同样的补发方式。
  • 历史回放:projectHistoricalTurn 按投影后的可见条目顺序应用同一规则。
  • 更新 HostAgentMessageItem.phase 的契约注释,写明"不标时由宿主推断,标 commentary 可退出"。
  • 新增 docs/architecture/turn-activity-folding.md,并登记到 docs/index.md。

"可见条目"按 Desktop 实际收到的顺序计算。Todo 工具、无法解析的文件写入工具和空 Reasoning 不计入。同一回合的文件变更合并为一个汇总条目,位置在首次改文件处。实时推送和历史回放两条路径的计算结果一致。

为什么放在 Protocol Core,而不是逐个改适配器

  • 已经实现的三个适配器,用的都是"成功回合的最后一条消息就是最终回复"这一条规则,没有一个是从 Harness 原生协议拿到最终回复信号的。逐个改等于把同一条规则再写 11 遍,而且它们之间已经出现了不一致(见下文)。
  • 仓库里已有先例:委派快照(host-runtime/src/delegation-snapshot.ts)在消息没有 phase 时,同样按"最后一条 Agent 消息"推断最终回复。

修正:历史回合结果为 unknown 时同样推断(commit ce067aea)

第一版只认历史结果为 succeeded 的回合。实测发现这会让 Claude Code 的旧对话全部无法折叠:

  • Claude Code 的会话记录(~/.claude/projects/*.jsonl)不保存 SDK 的 result 消息,而实时判断成功依赖其中的 subtype、is_error、terminal_reason(claude-code/src/native-message.ts:457-486)。按 openspec/specs/claude-code-text-session/spec.md 中 "Native history omits complete Result evidence" 的要求,历史回合结果必须保持 unknown,也不能凭 stop_reason 推断成功。
  • projectHistoricalTurn 一直把 unknown 显示为 completed,只有推断条件比 Desktop 更严格。

修正后,历史回合改用 historicalStatus(snapshot.outcome) === "completed" 判断,与 Desktop 显示的回合状态一致。适配器的回合结果不变,所以不违反上述规范。实时回合只有 succeeded / failed / cancelled,行为不变。

影响范围

  • Kiro CLI、Qoder / Qoder CN:在成功回合里每条消息都有显式 phase,推断不生效。历史结果不是 succeeded(包括 unknown)时,它们会把结尾消息显式标为 commentary,所以也不受这次修正影响。Qoder 在会话关闭路径上有一处不带 phase 的消息(qoder-sdk-transport.ts:1772),那时回合以失败结束,也不满足推断条件。
  • Kimi Code:不含工具调用的回合,目前最后一条消息没有 phase,现在会被推断为 final_answer。这类回合如果有推理条目,完成后会折叠,和 Kiro、Qoder、原生 Codex 的行为一致。
  • 其余 Harness:回合以回复收尾且被 Desktop 显示为已完成时,完成后会折叠。
  • 旧会话:外部 Thread 的条目不持久化,每次打开都从原生历史经过 projectHistoricalTurn 重新投影;Desktop 也不在磁盘上缓存会话。所以升级后旧会话同样会折叠。历史回合结果的来源分三类(逐个读过各适配器的判定代码):
    • 一律 unknown:Claude Code(claude-history.ts:163-184)、Hermes(gateway-history.ts:80-83、hermes-session.ts:428-435)、Cursor CLI(projection.ts:266-269)。这些 Harness 的旧对话依赖这次修正才能折叠。
    • 有终止证据时为 succeeded,缺证据时才是 unknown:Grok、CodeBuddy / WorkBuddy、Kiro CLI、Kimi Code、Qoder、Pi、OMP、OpenCode、Antigravity(从 codexhost 自己保存的附属记录读取实时结果,记录缺失时生成的 unknown 回合没有条目)。
    • 不产生 unknown:DeepSeek Harness。
    • 原生历史没有耗时数据的回合,分隔条显示"{count} previous messages",同样可以折叠。
  • 委派:推断出的 final_answer 和委派快照原有的推断结果一致,codexhost thread read 的结果不变。
  • SSH 远程工作区:远程 Harness 线程由被控机器上的 codexhost 投影,需要两端都升级到包含本 PR 的版本才会生效。

已知限制

  • 折叠发生在回合结束时。原生 Codex 在最终回复开始输出时就会折叠。多数 Harness 同样要到回合结束才知道哪句是最后一句,所以逐个改适配器也无法更早。
  • 回合以工具调用或文件变更收尾时,不做推断。
  • 最终回复被拆成多条连续消息时,前面几条会随过程一起折叠。
  • 取消或失败的回合不折叠,和原生 Codex 一致。
  • 对"仅在缺少终止证据时才给出 unknown"的 Harness,这类回合更可能是异常结束的,现在同样按已完成推断:最后一段文字会被当作最终回复,过程仍可展开查看。

与 Kiro CLI、Qoder、Kimi Code 现有实现的重复

这三个 Harness 在本 PR 之前已经自行标注 final_answer,规则和本 PR 的推断基本等价。本 PR 不修改它们:显式 phase 优先,推断对它们不生效,行为保持不变。

建议本 PR 合入、实机稳定后,另开 PR 删除下列标注 final_answer 的逻辑,统一由 Protocol Core 推断。 失败或取消时标 commentary 的分支要保留:委派快照依赖它,才不会把失败回合的部分输出当作最终结果。以下行号基于 main@4052cf49:

Kiro CLI

  • packages/adapters/kiro-cli/src/turn-output.ts:169:finish() 在回合成功时以 final_answer 结束仍在输出的消息。只删除成功分支,保留失败时的 commentary。
  • packages/adapters/kiro-cli/src/history.ts:360-367、:400-401:用 lastContent 选出最后一个"工具调用或非空回复";是回复且回合成功时标 final_answer。
  • packages/adapters/kiro-cli/src/kiro-adapter.ts:1414:斜杠命令结果消息固定标 final_answer。

Qoder(Qoder CN 通过 packages/adapters/qoder-cn/src/plugin.ts 复用同一实现)

  • packages/adapters/qoder/src/qoder-sdk-transport.ts:562-570、:582-584:本条消息不含 tool_use 的文本块,直接标 final_answer。这条规则和推断不一致,可能把回合中途的消息标成最终回复。
  • packages/adapters/qoder/src/qoder-sdk-transport.ts:859:result 成功时以 final_answer 结束仍在输出的消息。只删除成功分支,保留取消或失败时的 commentary。
  • packages/adapters/qoder/src/qoder-history.ts:232-238、:247-248:最后一条 assistant 消息(字符串内容)且回合成功时,标 final_answer。
  • packages/adapters/qoder/src/qoder-history.ts:256-261、:283-290:最后一条 assistant 消息中,所在消息不含 tool_use 的文本块且回合成功时,标 final_answer。

Kimi Code

  • packages/adapters/kimi-code/src/kimi-session.ts:560、:625、:820:用 hasHadToolCallsInTurn 跟踪本轮是否有工具调用;回合结束时只在有工具调用时标 final_answer。这条规则和推断不一致:没有工具调用的回合不标。
  • packages/adapters/kimi-code/src/history.ts:538:历史回放只在 maxToolOrder >= 0 时标 final_answer。

中间消息标 commentary 的代码(Kiro turn-output.ts:102/126/134,Qoder qoder-sdk-transport.ts:522/705,Kimi kimi-session.ts:619/628/647/732/754、history.ts:465)不影响 Desktop 主会话界面的折叠:主界面传的 commentaryDisplay 是 all。保留也没有副作用,可以由适配器维护者决定是否一并清理。

另外,Kiro CLI 与 Qoder 在历史结果为 unknown 时会把结尾消息显式标为 commentary(kiro-cli/src/history.ts:400-401、qoder-history.ts:247-248、:283-290),所以这类回合不会折叠。删除上述逻辑时如果改为不标 phase,这类回合会转由 Protocol Core 按已完成推断。

Related issues

无。相关的 #410(委派快照识别 final_answer)和本 PR 互相独立,可以分别合入。

Test plan

  • 定向测试:npx vitest run --config tests/vitest.config.js packages/protocol-core,13 个文件、135 个用例全部通过。新增的 packages/protocol-core/test/final-answer-phase.test.ts 覆盖了:

    • 实时回合会补发 item/completed;
    • 历史回合投影出同样的结果;
    • 显式 phase 优先;
    • 以工具调用收尾时不推断;
    • 取消、失败的回合在实时和历史两条路径上都不推断;
    • 历史回合结果为 unknown 时推断,且回合状态仍为 completed。
  • 反向确认:

    • 临时让推断函数总是返回 null 后,实时和历史两个正向用例失败;恢复后通过。
    • 把历史回放的判断临时还原为只认 succeeded 后,unknown 用例失败;恢复后通过。
  • 真实数据验证:用 Claude Code 适配器自己的 readClaudeTranscript 和 mapClaudeSnapshot 读取 6 个真实 Claude Code 会话(共 60 轮,全部为 unknown,每轮最后一个可见条目都是回复),再交给 projectHistoricalTurn 投影。修正前 0 轮、修正后 60 轮投影出 final_answer。

  • DeepSeek Harness 会话测试 streams v0.1.5 attempts... 收集了 projector 的全部 item/completed,现在多出一条补发的最终回复。断言已按新行为更新,这也覆盖了 DeepSeek Harness 的真实事件序列。

  • 全量 TypeScript 测试:npm run build:typescript 后执行 npx vitest run --config tests/vitest.config.js。结果为 380 个文件通过、14 个跳过;4679 个用例通过、37 个跳过、4 个失败。这 4 个在干净的 main@4052cf49 上、在同一环境里同样失败,与本 PR 无关:

    • 本机以 root 运行,影响 claude-code/test/sdk-transport.test.ts 的两个 root 安全用例;
    • workbuddy/test/history-derivation.test.ts 的 preserves the native failure when bridge cleanup also fails;
    • 本机没有官方 Codex app-server,影响 tests/release/host-bundle.test.mjs。

    此前一次全量运行中,host-runtime/test/harness-plugin-loader.test.ts 的 gives later plugins a full timeout... 出现过负载下的超时,单独运行 3 次都通过,本次全量运行也通过。

  • npx tsc -p tests/tsconfig.json --noEmit、npm run lint(eslint . 及 tools/check-boundaries.mjs)通过;改动文件的 prettier --check 通过。

  • 实机验证(Windows,Codex Desktop 26.924.2738.0,从本分支源码启动):

    • 本机 DeepSeek Harness 新对话完成后可以折叠。
    • 这次实机验证时本分支还没有包含 unknown 修正。SSH 远程工作区的 Claude Code 线程由被控机器上的 codexhost 0.10.2 投影,所以没有覆盖到;Claude Code 旧对话的效果目前只有上面的真实数据验证。
  • 未执行:Rust 测试(本 PR 没有改 Rust 代码)。

🤖 Generated with Claude Code

omsd512 and others added 2 commits September 26, 2026 15:06
Codex Desktop 只在回合最后一条 Agent 消息带 phase: "final_answer" 时,
才会把执行过程折叠到 "Worked for …" 之后。多数 Harness 适配器不标 phase,
这些回合完成后过程无法收起。

新增 inferredFinalAnswer():回合成功结束、Desktop 可见的最后一个条目是
非空 agentMessage、且适配器未给出 phase 时,将其视为 final_answer;
适配器显式给出的 phase 始终优先,标 commentary 可退出推断。

- 实时推送:在 turn/completed 之前重发该消息的 item/completed。Desktop
  处理 turn/completed 时不读取 turn.items,而 item/completed 按 id 替换条目。
- 历史回放:projectHistoricalTurn 按投影后可见条目顺序应用同一规则,
  重新打开的旧会话同样生效。
- 更新 HostAgentMessageItem.phase 的契约注释,补充定向测试,并同步
  deepseek-harness 会话测试中新增的补发消息。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
新增 docs/architecture/turn-activity-folding.md,说明 Codex Desktop 的折叠
条件、Protocol Core 的推断规则与两条投影路径、已知限制,以及与 Kiro CLI、
Qoder、Kimi Code 现有显式阶段标注的重复关系;在 docs/index.md 登记入口。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: BytePioneer-AI/codex-host/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 5f013b0f-83f4-452b-96b5-e87f6f214135

📥 Commits

Reviewing files that changed from the base of the PR and between 4052cf4 and ce067ae.

📒 Files selected for processing (7)
  • docs/architecture/turn-activity-folding.md
  • docs/index.md
  • packages/adapters/deepseek-harness/test/modern/session.test.ts
  • packages/harness-adapter/src/text-session.ts
  • packages/protocol-core/src/codex-ui-projector.ts
  • packages/protocol-core/src/final-answer-phase.ts
  • packages/protocol-core/test/final-answer-phase.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Summary

Summary by CodeRabbit

  • 文档
    • 新增回合过程条目折叠说明,介绍成功回合的最终答复识别、实时与历史记录的展示规则,以及相关限制。
  • 功能改进
    • 成功完成的回合中,若最后一条可见内容是未标注阶段的非空回复,系统会将其识别为最终答复;显式阶段标注保持不变。
    • 历史记录也支持最终答复识别;取消或失败的回合不会进行此推断。
  • 测试
    • 补充实时与历史回合的最终答复识别场景覆盖。

Walkthrough

新增最终答复阶段推断逻辑,并将其用于 Codex UI 的实时和历史回合投影。文档说明推断条件、历史回合处理方式及折叠限制。测试覆盖推断成功和不推断的情况。

Changes

最终答复阶段与投影

Layer / File(s) Summary
推断规则与适用条件
packages/protocol-core/src/final-answer-phase.ts, packages/harness-adapter/src/text-session.ts, packages/protocol-core/test/final-answer-phase.test.ts, docs/architecture/turn-activity-folding.md, docs/index.md
新增 inferredFinalAnswer。当回合已完成、最后一条可见条目是文本非空且 outcome 为 succeeded 的 agentMessage,并且未声明 phase 时,函数返回带有 final_answer 的消息。测试覆盖显式阶段、后续可见工作、取消、失败及历史回合 unknown outcome。文档说明推断条件和折叠限制。
实时与历史回合投影
packages/protocol-core/src/codex-ui-projector.ts, packages/adapters/deepseek-harness/test/modern/session.test.ts
历史投影先计算各条目的投影结果,再根据最后一条可见条目推断最终答复。实时回合完成时,投影器可更新最后一条已发送条目,并重放其完成通知。DeepSeek 测试预期包含 phase: null 和 phase: "final_answer" 两条输出。

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant CodexUiProjector
  participant inferredFinalAnswer
  participant CodexUIWire
  CodexUiProjector->>inferredFinalAnswer: 传入最后一条可见条目和回合完成状态
  inferredFinalAnswer-->>CodexUiProjector: 返回带 final_answer 的消息或 null
  CodexUiProjector->>CodexUIWire: 更新实时条目并重放完成通知
Loading

Suggested reviewers: bytepioneer-ai

Merge Risk: ⚪ Minimal · up to ce067

The final-answer projection is mergeable after normal checks; no actionable issue remains from this review.

Security Architecture Review

Security architecture risk: 🔵 Low · up to ce067

Completed turns may now fold earlier commands and tool activity behind the final reply, including historical turns without a recorded outcome. The activity remains expandable, and the change does not appear to grant new execution or permission authority.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected surface is Desktop presentation of qualifying Harness turns, not a newly reachable command, credential, or authorization sink. An adapter controlling message text and outcome can influence final-answer classification only through the stated eligibility checks.

Trust Boundaries and Controls

  • observed — Adapter-supplied phase remains authoritative. An explicit commentary phase, unsuccessful item, blank text, non-agent last item, or turn not shown as completed blocks fallback inference.

Resilience and Maintainability Implications

  • observed — The live replacement uses the existing projected item and its ID, and completion is guarded against outstanding items and interactions. Historical replacement occurs at the selected visible entry rather than adding an independently identified item.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了主要变更:在 Protocol Core 中为成功回合推断最终回复阶段,以支持 Desktop 折叠过程。标题简洁且与改动范围一致。
Description check ✅ Passed 描述与变更内容直接相关,并说明了推断条件、实时与历史投影路径、测试结果、已知限制及兼容性影响。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

原生历史缺少终止证据时,Adapter 按规范把回合结果记为 unknown(例如 Claude
Code 的会话记录不保存 SDK Result,其历史回合一律为 unknown),但
projectHistoricalTurn 仍把这类回合显示为 completed。推断条件此前只认
succeeded,导致 Claude Code、Hermes、Cursor CLI 等的旧对话无法折叠。

历史回合改为按 historicalStatus(...) === "completed" 判断,与 Desktop 显示
的回合状态一致;failed 与 cancelled 仍不推断,回合结果本身不变。实时回合
只有 succeeded / failed / cancelled,行为不变。

- 用 6 个真实 Claude Code 会话(60 轮)验证:修复前 0 轮、修复后 60 轮
  投影出 final_answer。
- 测试拆分为取消/失败不推断(补充 failed)与 unknown 历史回合推断两例。
- 文档补充 unknown 的来源分类与相应的已知限制。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@omsd512-W
omsd512-W marked this pull request as ready for review September 26, 2026 20:04
Copilot AI lite review requested due to automatic review settings September 26, 2026 20:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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