• thzxx released this 2026-07-22 15:18:21 +08:00 | 0 commits to master since this release

    版本概述

    v0.3.18 是一次重要的稳定性与正确性修复版本,聚焦解决三个用户可感知的核心问题:

    1. 上下文压缩不触发:1M 上下文设置下,详情栏 token 用量到 100%、总计超 1M,但压缩从未触发
    2. 输入框永久显示"工具加载中":MCP 初始化竞态条件导致前端永久错过 tools:ready 事件
    3. Token 显示语义错误:前端用累计消耗除以单次窗口得出无意义百分比

    同时加固了记忆系统、SOUL.md 降级处理和 Token 估算准确度。


    上下文压缩机制修复(核心)

    问题现象

    用户在 LLM 设置 1M 上下文时,观察到详情栏 token 用量"上下文"到了 100%、"总计"超过了 1M,但应用从未触发上下文压缩,最终导致 API 返回 413(请求体过大)。

    根因分析

    1. effectiveContextWindow 缺少默认值engine.ts 压缩判断处 this.config.contextLength ?? this.config.contextWindow 没有 ?? 128_000 兜底。若两者都为 undefinedcompressionThreshold = 0.8 * undefined = NaNactualTokens > NaN 永远为 false压缩永远不会触发
    2. 估算值偏低estimateMessagesTokens 基于字符数估算,中文场景下估算值显著低于 LLM Provider 实际计费的 token 数。估算没到阈值不压缩,但真实 API 调用已经 413。
    3. 保留区固定条数不合理compressMessages 保留最近 10 条消息,但不同上下文窗口下 10 条消息的 token 占用差异巨大。
    4. charsPerToken 偏差:二次截断使用 charsPerToken = 2(2 字符/token),对中文偏激进,实际中文约 1 字符/token。

    修复内容

    • engine.ts 新增 lastRealInputTokens 字段,记录 LLM 返回的真实输入 token 数
    • 压缩判断取 max(估算值, 真实值) 作为实际占用,避免估算偏低导致漏压缩
    • 添加 ?? 128_000 默认值保护,防止 compressionThresholdNaN
    • compressMessages 保留区从固定 10 条改为按 token 预算动态截断(50% 上下文窗口)
    • 二次截断 charsPerToken 从 2 调整为 1.0,与 CJK_TOKEN_RATIO 一致
    • 压缩后重置 lastRealInputTokens,避免跨迭代污染
    • 每个 run 开始时重置 lastRealInputTokens

    前端 Token 显示修复

    问题现象

    详情栏 Token 用量面板的"上下文"百分比与压缩判断逻辑完全脱节——前端显示 100% 时实际并未触发压缩。

    根因

    前端 usagePercent = totalTokens(累计消耗) / contextWindow(单次窗口) 语义错误。totalTokens 是所有轮次的累加值,除以单次窗口得出无意义的百分比。而压缩判断用 estimateMessagesTokens(request.messages) 看的是单次占用,两者完全脱节。

    修复内容

    • 区分"累计消耗"和"上下文占用"两个语义
    • TokenUsage.tsx 上下文占用百分比改用 lastInputTokens(单次占用),而非累计 totalTokens
    • 新增"压缩节省"行(绿色),仅当 lastCompressedSaved > 0 时显示
    • 进度条改用 inputPercent 展示累计消耗的构成(输入/输出占比)
    • "总计"改为"累计消耗","上下文"改为"上下文占用",语义更清晰
    • agent-store.ts TokenUsage 接口新增 lastInputTokenslastCompressedSaved 字段,4 处初始值统一更新
    • useAgentStream.ts usage 事件中 lastInputTokens 替换不累加,compressed 事件通过 streamEvent 接收 savedTokens
    • handlers.ts onCompressed 同时发 toast + streamEvent,解决"压缩触发但前端 token 显示不降"缺陷
    • 旧数据兼容使用 ?? 0,保证历史会话加载不崩溃

    MCP 工具就绪竞态修复

    问题现象

    应用启动后输入框永久显示"工具加载中",用户无法输入消息。

    根因

    mcpManager.initialize() 内部的 connectServer 是异步且不 await 的,initialize() Promise 几乎立即 resolve,tools:ready 事件在主进程启动后极快就发出。而前端 ipcRenderer.on('tools:ready') 监听器要等 React 组件挂载后才注册。事件在监听器注册前已发出,导致永久错过toolsReady 永远 false,输入框永久禁用。

    修复内容

    采用"事件 + 查询"双重机制解决竞态:

    • main.ts 维护 toolsReady 标志,MCP 完成/失败时置 true 并广播事件
    • main.ts 注册 tools:isReady IPC handler,返回当前状态
    • preload.ts 暴露 tools.isReady() 查询方法
    • App.tsx 注册 onReady 监听器后立即调用 isReady() 查询当前状态
    • 无论事件是否错过,前端都能通过查询恢复正确状态

    记忆系统加固

    退出时异步固化数据丢失

    • 问题MemoryConsolidator.consolidate 是异步的,用户关闭应用时若固化未完成,记忆会丢失
    • 修复consolidator.ts 新增 runningPromise 跟踪 + waitForCompletion(35s) 方法,main.tsbefore-quit 钩子等待固化完成,shutdown 超时从 5s 延长到 40s

    双轨存储无交叉验证

    • 问题:固化到 MEMORY.md 的记忆不会写入 semantic_memories 表,两个存储源可能不一致
    • 修复consolidator.ts 注入 MemoryManager,固化时同步写入 semantic_memories

    中文分词优化

    • manager.ts tokenize 按中英文标点切分子句后再做 bigram,提升 TF-IDF 检索准确度
    • 清理正则表达式冗余括号

    SOUL.md 降级处理

    问题现象

    SOUL.md 为空或不存在时,应用静默降级到默认身份,用户自定义人格丢失但不知情。同时若每次发消息都弹 toast 提示,会造成骚扰。

    修复内容

    • context-builder.ts SOUL.md 为空或不存在时降级到默认身份,向前端发 toast 提示用户
    • 新增 fallbackRoleNotified 去重标志,仅首次降级通知,避免每次发消息都弹 toast
    • SOUL.md 恢复内容时重置标志,下次再降级会再次通知

    Token 估算调整

    • token-estimator.ts CJK_TOKEN_RATIO 从 1.5 调整为 1.0,与实际中文 token 比例更接近

    MCP 异步初始化

    • main.ts MCP 完成后广播 tools:ready 事件,前端 UI 据以控制输入框可用性
    • preload.ts + global.d.ts 暴露 tools.onReady() 监听器
    • App.tsx + ChatInput.tsx toolsReady 状态控制输入框可用性

    版本号

    • package.json + package-lock.json 从 0.3.16 升级到 0.3.18

    修改文件清单(16 个)

    主进程(electron/)

    • harness/agent-loop/engine.ts — 压缩机制核心修复
    • harness/memory/consolidator.ts — 退出保护 + 双写
    • harness/memory/manager.ts — 中文分词优化
    • harness/prompts/context-builder.ts — SOUL.md 降级
    • harness/utils/token-estimator.ts — CJK 系数调整
    • ipc/handlers.ts — onCompressed 双发
    • main.ts — toolsReady 标志 + isReady handler + 退出等待
    • preload.ts — 暴露 isReady 方法

    前端(src/)

    • App.tsx — 竞态保护查询
    • components/chat/ChatInput.tsx — toolsReady 控制输入框
    • components/trace/TokenUsage.tsx — Token 显示重构
    • hooks/useAgentStream.ts — 事件处理
    • stores/agent-store.ts — TokenUsage 接口扩展
    • types/global.d.ts — 类型声明

    配置

    • package.json — 版本号
    • package-lock.json — 版本号

    验证

    • npm run typecheck 通过(exit code 0)
    • 所有修改经过两轮代码复查,确认无引入新问题、无破坏原有功能
    Downloads