Skip to content

feat(sessions): integrate structured Codex tool calls - #482

Open
verdenmax wants to merge 10 commits into
zszz3:mainfrom
verdenmax:feat/structured-tool-call
Open

feat(sessions): integrate structured Codex tool calls #482
verdenmax wants to merge 10 commits into
zszz3:mainfrom
verdenmax:feat/structured-tool-call

Conversation

@verdenmax

Copy link
Copy Markdown
Contributor

关联 Issue

Closes #441

变更概述

在现有 Codex ToolCall phase 1 基础上,完成 V1/V2 的统一工具调用语义层接入。Codex Legacy、Paginated 和 Code Mode Session 中分散的请求记录、完成记录、执行 metadata 与 JavaScript 调用,现在会先归一为同一种 StructuredToolCall,再供 Session Trace、Skill Usage 和 V2 Eval 消费。

用户可见结果:

  • Code Mode 外层 exec 会继续保留,同时展示其内部识别出的 exec_commandskills.readweb.run、MCP 或插件工具。
  • Skill 使用统计会区分运行时确认、仅发起请求和仅在静态代码中出现的调用,降低漏计、重复计数和误报。
  • Paginated Session 优先采用真实完成事件中的状态、cwd、耗时和退出码;Legacy Session 在缺少完成事件时可安全降级到 AST 静态证据。
  • V2 Eval 查询使用与 Session Trace、Skill Usage 相同的工具名称和执行证据。
  • 修复 V2 开发启动时 Electron runtime 跨文件系统替换触发 EXDEV、导致应用无法启动的问题。

问题根因

同一次调用存在多种持久化形态

Codex 的一次逻辑工具调用可能同时记录为:

response_item                    请求阶段
event_msg.item_completed         Paginated 运行时完成
executed_tool_calls metadata     可选运行时证据
custom_tool_call(name=exec)      Code Mode 外层 JavaScript

原有模块分别直接理解一部分 JSONL 结构,容易产生以下差异:

  • 请求与完成记录被计为两次调用;
  • Paginated 的真实完成状态没有被下游统一采用;
  • Legacy Code Mode 只能看到外层 exec
  • skills__readweb__run 和 MCP namespace 在不同功能中规范化不一致;
  • AST 中出现但没有实际执行的调用可能被误认为成功使用。

旧正则只能提供展示名称

现有 extractCodexExecToolNames() 能从 exec 源码中提取工具名,并生成:

exec · exec_command, web.run

但它不能表达 call ID、父调用、Turn、参数、运行状态、cwd、耗时和证据来源,也无法可靠关联 Paginated 完成事件。因此本次保留旧字段以兼容现有标题展示,同时由统一中间层负责完整工具语义。

Electron runtime 暂存目录位于不同设备

Electron 修复脚本原先把暂存和备份目录创建在系统 /tmp,再使用 rename() 替换工作区中的 node_modules/electron/dist。当工作区和系统临时目录属于不同文件系统时,跨设备 rename 会稳定报错 EXDEV

实现说明

统一数据流

Codex Session JSONL
        |
        v
ToolCallObservation adapters
        |
        v
call ID / parent / Turn / input 关联
        |
        v
证据升级与去重
        |
        v
StructuredToolCall
        |
        +--> Session Trace
        +--> Skill Usage
        +--> V2 Eval

ToolCallObservation 保存某一条原始记录提供的事实;StructuredToolCall 表示完成关联后的单次逻辑调用。后者统一包含:

  • canonicalName
  • callId / parentCallId / turnId
  • input / cwd
  • status
  • executionEvidence
  • pluginId / scriptPath
  • durationMs / timestamp
  • 原始 evidence 列表

输入 Adapter

Adapter 输入 目标
ResponseItem function_callcustom_tool_calllocal_shell_calltool_search_call 保存请求、call ID、namespace 和参数
ItemCompleted CommandExecutionDynamicToolCallMcpToolCall 保存运行结果、状态、cwd、耗时和退出码
Executed metadata executed_tool_calls 提供直接调用或 Code Mode 内层调用的运行时证据
Code Mode AST custom_tool_call(name=exec) 中的 JavaScript 安全提取 tools.*(...) 调用和可静态求值的参数

AST 使用 @babel/parser,不会通过 evalFunction 或其他方式执行 Session 中的 JavaScript。对象、数组和基础字面量会被静态还原;动态表达式无法安全求值时保留未知参数。

名称规范化

统一层保留 namespace,并生成稳定名称:

tools.skills__read       -> skills.read
tools.web__run           -> web.run
mcp server + tool        -> mcp__server.tool
local shell execution    -> exec_command

外层 exec 仍作为 Code Mode 容器保留,不与任意一个内层工具调用混为同一实体。

证据优先级和去重

item-completed
  > executed-tool-metadata
  > response-item
  > code-mode-ast
  • 相同 call ID 的请求和完成记录直接合并。
  • AST 内层调用使用 parentCallId + ordinal 生成稳定临时 ID。
  • metadata、AST 与完成事件根据 parent、Turn、规范化名称和输入指纹进行保守匹配。
  • 参数相同但独立执行的调用不会仅因参数一致而被折叠。
  • Paginated Session 中运行时记录覆盖 AST 推断;Legacy 缺少运行时完成记录时保留 AST 结果。

最终执行证据分为:

含义
runtime-confirmed 有完成事件或执行 metadata
recorded-request 只确认 Codex 发出了调用请求
static-only 只在 Code Mode AST 中发现

增量扫描与回滚

  • Collector 状态随 Codex incremental state 保存,增量扫描可继续关联上一次 offset 之前的请求和之后的完成事件。
  • thread_rolled_back 删除 Turn 时同步丢弃相关 call ID 和内层 observation。
  • V1 使用同步 store API,V2 使用异步 PostgreSQL API,但保持相同用户可见语义。

下游接入

Session Trace

  • 已有 trace 会补充统一的 canonical name 和 execution evidence。
  • Code Mode 内层调用可形成独立 trace。
  • 若 Paginated 完成事件与 AST 静态调用匹配,优先保留运行时调用,避免重复卡片。
  • 原有 nestedToolsexec · ... 标题继续兼容。

Skill Usage

  • shell 读取 SKILL.md、运行 Skill 脚本、顶层或 Code Mode skills.read 都消费统一调用。
  • skills.read 结合 Session 中的 Skill catalog 将 package/resource 映射到 Skill 名称。
  • Paginated Session 只把运行时确认且未失败、未拒绝的调用计为成功使用。
  • 仅静态出现、执行失败或被拒绝的调用不会误计为成功使用。

V2 Eval

PostgreSQL Skill Eval 查询从 trace attributes 读取:

tool.canonicalName
tool.executionEvidence

Eval 与解析器使用相同的名称和证据语义,不再单独推断 Code Mode 调用。

Electron runtime 修复

下载、校验和解压流程保持不变;只将暂存 runtime 和 previous runtime 放到 node_modules/electron 所在文件系统,确保安装与回滚所需的 rename 都是同盘原子操作。

修改前后

场景 修改前 修改后
Legacy Code Mode 通常只看到外层 exec AST 恢复内层调用,并标记 static-only
Paginated shell 请求和完成可能分散消费 合并为运行时确认的 exec_command
Skill 统计 多处分别解析,可能漏计或重复 统一消费 StructuredToolCall
失败/拒绝调用 可能缺少统一判定 不计为成功 Skill 使用
namespace 不同模块结果可能不一致 统一 canonical name
增量扫描 请求与后续完成可能失去关联 Collector state 跨 offset 保存
V2 Eval 独立推断工具使用 查询统一 trace evidence
Electron 修复 跨设备 rename 启动失败 同文件系统原子替换

兼容性与风险控制

  • V1 和 V2 均实现、测试相同工具语义,没有机械复制 store 调用方式。
  • 保留旧 nestedTools 字段和标题展示,避免现有 UI 回退。
  • 未知或无法解析的 Codex 记录安全跳过,不阻断整个 Session。
  • AST 只做静态语法分析,不执行 Session 代码。
  • 动态参数不会被猜测为可信输入。
  • 静态证据不会升级为成功执行。
  • 原始 Codex Session 文件不会被修改。
  • Electron 测试使用合成 archive 和临时目录,不读取或覆盖用户 runtime 数据。

数据更新与已知限制

  • 本分支没有新增数据库 schema 或一次性数据迁移。
  • 新索引和已失效后重建的 Codex Session 会使用新语义。
  • 已经完成索引且源文件未变化的历史 Session,普通增量扫描可能继续使用旧 trace;需要将 Codex Session freshness 置零后通过现有索引流程完整重建。
  • 开发验证环境已按现有第 17 号迁移的 freshness 归零逻辑临时触发重建;正式合并前需要确认是否补充新的、只执行一次的数据迁移,以自动覆盖所有已有用户。

更新说明检查

  • 本分支已新增且仅新增一个 .release-notes/<branch-slug>.md
  • 更新说明声明 release-target: both,覆盖 V1/V2 的 Session 与 Skill 结果
  • 更新说明同时包含面向用户的“新增功能”和“Bug 修复”
  • 更新说明未包含 MR、分支、CI、发布流程、内部路径或敏感信息
  • 已运行 npm run release-note:check

验证

  • V1 Session loader 与 indexer 测试通过。
  • V1 StructuredToolCall parser 测试通过。
  • V1 Skill Usage 测试通过。
  • V2 Session loader 测试通过。
  • V2 StructuredToolCall parser 测试通过。
  • V2 Skill Usage 与 PostgreSQL support repository 测试通过。
  • V1、V2 TypeScript 类型检查通过。
  • V1、V2 生产构建通过。
  • 仓库脚本测试通过:52/52。
  • Electron 跨文件系统回归测试通过。
  • npm --prefix apps/main-2.0 run predev 完成 runtime 修复,随后再次运行无需下载并立即通过。
  • npm run release-note:check 通过。
  • git diff --check 通过。

更新说明

本分支包含 .release-notes/structured-tool-call.md

  • 新增 Code Mode 内层工具展示和更准确的 Skill 使用统计。
  • 修复 V2 Electron runtime 在跨文件系统环境中无法自动修复的问题。

@LANSGANBS

Copy link
Copy Markdown
Collaborator

当前模型: anthropic/claude-opus-5-google
审查范围: b31918ac...cb6a7fa0
覆盖范围: 149/149 diff atoms · 15/15 semantic units

[P1] apps/main-1.0/src/core/session-loader.ts:2360(V2 apps/main-2.0/src/core/session-loader.ts:1536toolCallState 从未被持久化,增量扫描会留下重复的嵌套 trace
codexIncrementalState.toolCallState 只在内存中写入并从 base.loaded 读回,但两侧 store 都不保存它:V1 store/sessions.ts:1419 与 V2 postgres/session-turn-repository.ts:204getCodexIncrementalState() 只重建 historyMode / messageProvenance / activeTurnIds,而生产环境的增量基线只来自 store(apps/main-1.0/src/core/indexer.ts:257apps/main-2.0/src/core/indexer.ts:333),因此续扫时 collector 恒为空。触发路径:paginated Code Mode 会话先被索引一次,持久化了 <exec-call-id>#ast-0 这类 executionEvidence: "static-only" 子事件;随后文件追加 item_completed CommandExecution,新一轮结构化调用因缺少历史观测而 parentCallId === nullapplyStructuredCodexToolCalls 的丢弃条件要求 call.parentCallId === tool.parentCallId,旧的 static-only 事件不会被取代,于是同一次内层调用在 Session Trace 中同时留下幻影卡片和运行时卡片,并被持久化;全量重建又只剩一条,增量与全量结果长期不一致。新增的 session-loader.test.ts 用例通过 incrementalCodexSessions 直接注入内存 LoadedSession,绕过了唯一的生产路径,因此掩盖了该问题。修复方向:把 toolCallState 真正落库并在两侧 getCodexIncrementalState() 中读回,或让丢弃条件不依赖 collector 记忆(例如同 turn 内按 compatibleToolName + input 指纹取代 static-only 子调用,或从保留的父事件回填 parentCallId)。

[P2] scripts/ensure-electron-runtime.mjs:139 暂存目录改到 node_modules/electron 后没有清理残留
暂存/备份目录从 os.tmpdir() 移入 node_modules/electron/.agent-recall-electron-*,但清理只有 finally 中的 rm。下载或解压期间进程被强杀(Ctrl-C、SIGKILL、断电)时,包含上百 MB 归档和半成品 dist 的目录会永久留在包目录里;runtimeReady():64)只检查 path.txtdist/version,runtime 恢复正常后不再回访,每次异常都会再堆一份,而系统临时目录原本有回收机制。修复方向:mkdtemp 前先 readdir(electronDirectory) 清除 .agent-recall-electron-* 残留,或使用固定暂存路径并在进入时删除。

[P2] scripts/ensure-electron-runtime.mjs:139 与 open PR #477 引入完全相同的改动
PR #477 对同一行做了字节相同的修改(mkdtemp(path.join(electronDirectory, ".agent-recall-electron-"))),并各自附带描述同一修复的更新说明条目。两个 PR 都合入时该文件会产生冲突,且合并后的发布说明会出现两条描述同一 Electron 修复的用户可见条目。修复方向:确认归属后从其中一个 PR 移除该改动与对应的更新说明条目,或让 #482#477 合并后 rebase。

Show runtime results beneath their exec parent with explicit AST provenance, retain safely resolved literal arguments, and persist Code Mode relationships in both session apps.

Write V2 trace span parent links in a second phase so large sessions remain indexable across batch boundaries.
Persist the Code Mode collector state in both session stores so incremental scans retain parent relationships and replace static evidence with runtime results. Reindex existing Codex sessions once when the new storage is introduced.
Exercise two production-style index passes through the SQLite and PostgreSQL stores and assert that later runtime evidence replaces the earlier AST-only child call.
Remove the duplicate cross-filesystem Electron staging change and its regression test from this branch so PR zszz3#477 remains the single owner. Keep this branch release note focused on structured Code Mode calls.
@verdenmax

Copy link
Copy Markdown
Contributor Author

当前模型: anthropic/claude-opus-5-google
审查范围: b31918ac...cb6a7fa0
覆盖范围: 149/149 diff atoms · 15/15 semantic units

[P1] apps/main-1.0/src/core/session-loader.ts:2360(V2 apps/main-2.0/src/core/session-loader.ts:1536toolCallState 从未被持久化,增量扫描会留下重复的嵌套 trace codexIncrementalState.toolCallState 只在内存中写入并从 base.loaded 读回,但两侧 store 都不保存它:V1 store/sessions.ts:1419 与 V2 postgres/session-turn-repository.ts:204getCodexIncrementalState() 只重建 historyMode / messageProvenance / activeTurnIds,而生产环境的增量基线只来自 store(apps/main-1.0/src/core/indexer.ts:257apps/main-2.0/src/core/indexer.ts:333),因此续扫时 collector 恒为空。触发路径:paginated Code Mode 会话先被索引一次,持久化了 <exec-call-id>#ast-0 这类 executionEvidence: "static-only" 子事件;随后文件追加 item_completed CommandExecution,新一轮结构化调用因缺少历史观测而 parentCallId === nullapplyStructuredCodexToolCalls 的丢弃条件要求 call.parentCallId === tool.parentCallId,旧的 static-only 事件不会被取代,于是同一次内层调用在 Session Trace 中同时留下幻影卡片和运行时卡片,并被持久化;全量重建又只剩一条,增量与全量结果长期不一致。新增的 session-loader.test.ts 用例通过 incrementalCodexSessions 直接注入内存 LoadedSession,绕过了唯一的生产路径,因此掩盖了该问题。修复方向:把 toolCallState 真正落库并在两侧 getCodexIncrementalState() 中读回,或让丢弃条件不依赖 collector 记忆(例如同 turn 内按 compatibleToolName + input 指纹取代 static-only 子调用,或从保留的父事件回填 parentCallId)。

[P2] scripts/ensure-electron-runtime.mjs:139 暂存目录改到 node_modules/electron 后没有清理残留 暂存/备份目录从 os.tmpdir() 移入 node_modules/electron/.agent-recall-electron-*,但清理只有 finally 中的 rm。下载或解压期间进程被强杀(Ctrl-C、SIGKILL、断电)时,包含上百 MB 归档和半成品 dist 的目录会永久留在包目录里;runtimeReady():64)只检查 path.txtdist/version,runtime 恢复正常后不再回访,每次异常都会再堆一份,而系统临时目录原本有回收机制。修复方向:mkdtemp 前先 readdir(electronDirectory) 清除 .agent-recall-electron-* 残留,或使用固定暂存路径并在进入时删除。

[P2] scripts/ensure-electron-runtime.mjs:139 与 open PR #477 引入完全相同的改动 PR #477 对同一行做了字节相同的修改(mkdtemp(path.join(electronDirectory, ".agent-recall-electron-"))),并各自附带描述同一修复的更新说明条目。两个 PR 都合入时该文件会产生冲突,且合并后的发布说明会出现两条描述同一 Electron 修复的用户可见条目。修复方向:确认归属后从其中一个 PR 移除该改动与对应的更新说明条目,或让 #482#477 合并后 rebase。

修复,现在UI 展示
image

@LANSGANBS

Copy link
Copy Markdown
Collaborator

当前模型: anthropic/claude-opus-5-google
审查范围: 7322a24e...84f3179e
覆盖范围: 243/243 diff atoms · 28/28 semantic units

[P1] apps/main-1.0/src/renderer/src/features/session-detail/detail-panel.tsx:882(V2 apps/main-2.0/src/renderer/src/features/session-detail/turn-accordion.tsx:307)legacy 格式 Codex 会话中已完成和失败的工具调用全部显示为「已请求」
extractCodexResponseTrace(V1 session-loader.ts:565、V2 :465)对每个 response_item 工具请求无条件写入 tool.executionEvidence = "recorded-request"function_call_output 事件不带 tool 属性,而 dedupeCodexTraceEvents 以 output 为 primary 并按 {...secondary.attributes, ...primary.attributes} 合并,该 evidence 因此存活到最终事件上(V2 经 derive-turns.ts 的 paired 合并同理)。legacy 格式 rollout 没有 item_completed,collector 永远得不到 runtime-confirmedapplyStructuredCodexToolCalls 只会把 recorded-request 再写回一次。两侧渲染层都在 status 之前短路判断 evidence,于是这类会话里每个工具调用的状态从原先的 ✓ 已完成 变成 → 已请求failed / aborted 同样被覆盖,主时间线上不再能区分成功与失败;V2 还会把该行状态色降为 --text-faintstyles/session-detail.css)。修复方向:只有在事件尚无终态时才让 recorded-request 参与展示(例如 statusunknown / running,或事件仍为 tool_call),static-only 保留现有优先级。

[P2] .release-notes/structured-tool-call.md:11 该条修复只存在于 V2,且在任何已发布版本中都不可触发,却标为 both
「大量父子工具记录时可能无法完成索引」对应的实现是 apps/main-2.0/src/core/postgres/session-repository.ts:589trace_spans.parent_span_id 拆成第二遍 UPDATE,用于规避自引用外键跨插入批次的顺序问题;但 base 上 derive-turns.tsparentSpanId 恒为 null,该失败模式只源于本 PR 自己新引入的父子关系,历史版本不存在这个「已修复的用户可见问题」。V1 没有 trace_spans,本 PR 也没有对应改动,而 :3<!-- release-target: both --> 会让 combineReleaseNotesForTarget(notes, "v1") 把这条渲染进 V1 发布说明,与 .release-notes/README.mdboth 仅用于同一用户可见结果同时影响两个 App 的规则冲突。修复方向:删除该条,或改为 v2 并重写为确有对外可见影响的表述;其余两条在两端均成立。

[P2] apps/main-1.0/src/core/session-loaders/codex-tool-calls.ts:87(V2 同文件同行)持久化的 codex_tool_call_state 是未截断、未脱敏的全量快照,随会话单调增长
state getter 原样导出全部 observation,其 input 直接来自 parseMaybeJson(payload.arguments / payload.input),没有经过 trace 侧同一份数据必经的 sanitizeCodexTraceValue / truncateTraceDetail(12000 字符上限,并剥离 *encrypted* 字段与 base64 内容)。该集合只在 thread_rolled_back 时被 discardCallIds 局部裁剪,其余情况单调增长,而每次增量索引都会把整份 JSON 重新序列化并整列覆盖写入(V1 store/sessions.ts:343 的 TEXT 列、V2 postgres/session-repository.ts:813 的 jsonb 列)。因此包含大量 apply_patch 补丁或 Code Mode 源码的长会话,单行状态可增至数 MB 并按索引频率反复重写,同时绕过了索引产物必须截断与脱敏这一既有约束。修复方向:入库前对 observation 的 input 复用 trace 侧的净化与截断,并对 observations 的条数或序列化体积设上限。

@LANSGANBS

Copy link
Copy Markdown
Collaborator

当前模型: anthropic/claude-opus-5-google
审查范围: 7322a24e...2b485f56
覆盖范围: 262/262 diff atoms · 30/30 semantic units

[P1] apps/main-1.0/src/core/session-loaders/codex-tool-calls.ts:93(V2 同文件同行)持久化快照按调用分组重排 observation,已融合的 Code Mode 内层调用在下一次增量索引时重新裂开
state getter 用 finish() 的结果按调用分组扁平化,组内顺序是 evidence 的 push 顺序。第三遍关联(:439-457)对 mergeCall(runtime, static) 在 target 已有运行时证据时不下调 sequence,于是合并后该组是 [item-completed, code-mode-ast],持久化数组里 code-mode-ast 被排到 item-completed 之后,相对文件内的真实时序发生颠倒。下一次增量扫描由 session-loader.ts:2143(V2 :1327)用该快照重建 collector 并按数组顺序重新编号 sequence,candidate.sequence < runtime.sequence 不再成立,同一次内层调用重新裂成两条:一条 static-only 的 AST 记录,一条 parentCallId 为空的运行时记录。applyStructuredCodexToolCalls 的陈旧 static 过滤(session-loader.ts:759-773)因 callIds 重新包含该 AST callId 而短路失效;V1 的嵌套判据(detail-panel.tsx:74)要求 static-onlyparsedFromCodeMode,而重写时 parsedFromCodeMode 被置为 false,运行时结果卡片因此掉出 exec 父节点变为顶层项,exec 下又多出一条静态 AST 行;V2 经 derive-turns.ts:475 仍按 tool.parentCallId 挂父,表现为同一次调用在 exec 下出现两个子节点,并连带影响 parent_span_id 与 skill 归属。任何 Code Mode 会话在首次成功融合之后再被追加一次即触发,裂开后顺序稳定、不会自愈,只有全量重建索引才能恢复;现有增量测试只覆盖「首扫仅有 request、运行时证据下一轮才到达」,快照中尚无 item-completed,因此无法发现该路径。修复方向:把 sequence(或改用不随重载重编号的 observation timestamp)一并持久化并在构造时恢复排序,使 finish() 对 persist→reload 幂等。

[P2] apps/main-1.0/src/core/session-loader.ts:824(V2 :724)结构化融合用原始 passthrough turn_id 覆盖 rollout 已解析的 turn 归属
sourceTurnId: call.turnId ?? existing?.sourceTurnId ?? ... 让 collector 的裸值优先,而 collector 取的是 metadata.turn_id || payload.turn_idcodex-tool-calls.ts:159:225),未经 resolveAttributionTurnIdcodex-rollout.ts:1160-1173);后者的注释明确说明 Codex 会在新 task_started 之后继续用已结束 turn 的 passthrough id,必须改挂到 active turn。当 explicit turn id 非空且不在 activeTurnIds 中时两者结论不同,本 PR 让裸值胜出,受影响的 codex 工具事件(含新推入的嵌套节点)被重新归属到一个已完成或非活跃的 turn,在 V2 中直接改变 derive-turnssourceTurnId 的 span 聚合结果,表现为工具调用挂错 turn,或落到没有消息的 turn 上而从当前 turn 的工具树里消失,父事件与新增子节点还可能分裂到不同 turn。修复方向:让 collector 的 turnId 不参与覆盖(改为 existing?.sourceTurnId ?? call.turnId ?? ...),或把 rollout 解析后的 turn id 传入融合阶段统一归一。

[P2] apps/main-1.0/src/core/session-loaders/codex-tool-calls.ts:401(V2 同文件同行)直连调用合并 executed-tool-metadata 时丢失 pluginId / scriptPath
非 code-mode 的直连调用若其 function_call_outputexecuted_tool_calls 条目带 plugin_id / script_pathreadExecutedToolMetadata 会以 isOuterCall=false 生成嵌套观测,随后被这段循环并入直连调用;但该循环只回填 turnId / cwd / evidence / executionEvidence,而同一文件的 mergeObservation:504-505)与 mergeCall:519-520)都会回填这两个字段。结果直连调用的 pluginId / scriptPath 保持 nullskill-usage.ts:430-431:555 拿不到插件与脚本信息,插件类技能的 owner 与归因丢失,trace 的 attributes.tool 也不再带这两项,而 code-mode 的 exec 外层调用走 mergeObservation 时字段正常保留,同一 PR 内两条路径行为不一致。修复方向:在该循环补 direct.pluginId ??= recorded.pluginId; direct.scriptPath ??= recorded.scriptPath;(不宜直接改用 mergeCall,那会把 direct.parentCallId 设成自身 id)。

[P2] apps/main-1.0/src/core/store/sessions.ts:2081(V2 apps/main-2.0/src/core/postgres/session-records.ts:158)单行可达 256KB 的 codex_tool_call_state 被会话列表与搜索热路径无谓读出
该状态以整列形式加在 sessions 上(V1 store/schema.ts:48 的 TEXT、V2 migration 41 的 jsonb),写入体积由 MAX_PERSISTED_OBSERVATION_CHARS = 256_000codex-tool-calls.ts:66)限幅,繁忙 Codex 会话会稳定顶到该上限。而 V1 getCandidateRows 的关键词分支是 SELECT sessions.* 且没有 LIMIT,把全部命中行整行物化成 JS 对象;列表分支(:2070)与批量删除预检(:821)同样是 sessions.*;V2 的 SESSION_SELECT_SQL 也以 sessions.* 开头,被 getSession / findByRawId / listSessionsNeedingSummary 与默认 limit = 200 的会话搜索共用。唯一真正读取该列的 getCodexIncrementalState(V1 :1445)已显式选列,hydrateRow / hydrateSession 从不消费它,因此这些读取纯属放大:V1 文本搜索在数千个 Codex 会话下是百 MB 级的瞬时字符串分配且发生在 UI 输入触发的路径上,V2 单次列表最多把约 50MB jsonb 传输并解析。修复方向:把上述 sessions.* 改为排除该列的显式列清单,或把快照移到以 session_key 为键的一对一旁表。

[P2] apps/main-2.0/src/renderer/src/styles/session-detail.css:1240(V1 apps/main-1.0/src/renderer/src/styles.css:3406)新增嵌套后状态色仍用后代选择器,父子调用互相污染
.msg.tool.failed .msg-avatar / .msg.tool.failed .msg-tool-status:1240:1245)与 .msg.tool.evidence-recorded-request .msg-tool-status:1258)都是后代选择器,而子调用现在渲染在父 <details> 内部(turn-accordion.tsx:600-620)。Code Mode 的 exec 父节点通常带 evidence-recorded-request,与 failed 规则特异度相同且源码顺序在后,组内失败子调用的状态文字会从 --danger 变成 --text-faint;反向地,exec 父节点为 failed 时其规则会命中所有嵌套子节点,把已成功的子调用染成告警色。V1 .trace-event.failed .trace-symbol 有同一处泄漏。文字与符号仍正确,属工具调用树内层节点的成功/失败视觉指示不可靠。修复方向:与本 PR 已对 .trace-event > prestyles.css:3435)采取的做法一致,把这些状态/evidence 着色规则改为直接子代选择器,例如 .msg.tool.failed > .msg-tool-summary .msg-tool-status

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.

建立统一的 StructuredToolCall 提取层,解析 Codex 顶层与 Code Mode 嵌套工具调用

2 participants