Skip to content

v0.3.14

Choose a tag to compare

@siwilizhao siwilizhao released this 24 Sep 03:04
· 227 commits to main since this release

Added

  • 中断恢复入口:未回答的提问一键续跑(优化清单"检查点/恢复"的 UX 切片):会话以
    未获回答的用户提问结尾时(工具执行中途被杀的典型残留 —— 提问已落盘、回答没有),
    面板在输入框上方显示"上一条提问还没有收到回答 · 继续回答"提示条 ✓;点击为已有
    的那条提问直接开一轮(不重复 append、不重复记历史 ✓)。桥端点
    POST /agent/resume 同能力(自动化用)✓。

  • 空回合自动重试一次(优化清单收尾):模型偶发返回空内容(上游抖动、网关抽风)时,
    不再把可见警告直接甩给用户 —— 自动再试一次,仍空才给出说清原因的警告 ✓。
    有界:至多多一次调用,不会循环 ✓;与瞬态重试、超限减半重试的既有语义并存 ✓。

    • 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
      (原发 + 自动重试)+ 1 次标题生成,最终以警告收尾、无死循环 ✓。
  • 只读工具并行执行(优化清单 P2):连续的 .readonly 工具(getPageText、快照、
    readFile、检索类……)从逐个 await 改为整段并发、按原顺序落结果 —— gate 仍逐个过
    (readonly 从不弹审批、取消即拒),toolCallId 配对不受执行顺序影响 ✓。
    多读类回合(检查多个元素、读多个文件)的墙钟直接省一半以上 ✓。

    • 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
      (位置配对)—— 串行时代恰好不出错,并行完成后结果全部张冠李戴(实测 3 个并行
      readFile 的结果互相错位)✓。现在按 toolCallId 精确认领(旧会话没有 id 的退回
      位置配对)✓;单测加了并行批的配对回归(旧算法下该用例必失败)✓。
  • Agent 评估脚本(tests/agent-eval.py)(优化清单 P1):固定 prompt 集 → 假端点 →
    断言轨迹与消息,四个确定性用例:系统提示契约(system 唯一且在开头)、失败约定
    (工具结果/模型所见均为 Error: 开头 + 机械核验触发)、脱敏(工具结果与模型所见
    均为 [redacted]、原文不出现)、超限重试(首答 context length → 自动重试并完成)✓。
    清理只删脚本记录的会话 id;档案自动恢复 ✓。

  • 上下文管理:摘要顶替 + 超限自动重试 + 预算自校准(优化清单 P1):
    ① 长对话被裁掉的轮次不再无声消失 —— 压缩时为被裁轮次生成机械摘要(每轮
    "用户目标|结论",非模型生成、有长度上限),并入开头 system 提示的
    "Earlier conversation (compacted)" 一节,模型仍知道前文聊过什么 ✓;
    ② 服务端报"上下文/输入过长"(各家措辞不一,按关键词归一识别)时,压缩预算自动减半
    并重试一次
    ,成功后记住该校准预算,后续回合沿用 —— 不再把超限当成普通失败丢给用户 ✓;
    ③ 面板的"上下文占用%"同步改用生效预算 ✓。

    • 单测:摘要包含被裁轮次的目标、预算内 digest 为 nil、压缩后逐日无断档等 ✓。
    • 实测(fixture 首次请求报 context length、重试放行):应用自动重试并正常收尾 ✓。
  • 纯逻辑单测 harness(tests/run.sh)(优化清单 P0):解析/压缩/脱敏/用量折算/轨迹派生
    这些纯 Foundation 逻辑现在有一套不依赖 Xcode 的测试 —— swiftc 直接编译受测文件 +
    用例入口,CI 在构建前跑 ✓。首批 36 项:脱敏(含多形态命中 + CJK 混排的越界回归用例、
    PEM、已知 Key、无命中原样返回)、金额格式化边界(< $0.0001 / $0)、用量汇总(混价
    不给总额)、统计派生(连续天数 / 峰值 / 分模型 / 逐日连续)、上下文压缩(裁最老整轮、
    最终块保留、工具配对不拆散、单轮超预算宁可超发)、轨迹派生(answer / 失败标记)✓。
    顺带把 compactForContext 从 AgentSessionStore 抽到 ContextCompaction.swift
    (纯 Foundation 文件才进得了这个 harness)✓。

  • 工具失败统一约定:所有工具失败一律返回 Error: 前缀(优化清单 P0):此前只有
    executeJS 写 Error:,其余失败("File not found"、"Missing path"、runCommand 非零退出、
    MCP 报错、子代理流失败…)都是普通文本 —— 机械核验看不见、轨迹的 threwError 统计低估、
    模型也难以可靠识别失败。现在约 120 处失败返回全部收口(BrowserToolProvider.fail),
    且 runCommand 的非零退出/超时与 MCP / 子代理失败一并进约定 ✓(输出原样保留,
    模型仍能看到 stdout/stderr)。查询成功但结果为空不是失败("No bookmarks" 等),
    避免把正常空答案误标成错误 ✓。

    • 实测(假端点强制 readFile 一个不存在的文件):工具结果 = Error: File not found: … ✓;
      机械核验第一次对这类失败触发了硬提示("本轮所有工具调用都被拒或报错")✓;
      轨迹 threwError = true、stats.threwError = 1 ✓;模型实际收到的工具消息同样以
      Error: 开头 ✓。
  • crew 用量记账 + 状态可见(优化清单 P1):多标签 crew 的 worker 跑在各自标签页里,
    此前用量无处记账(成本与统计都会低估,一次 crew 可能比主循环本身还贵)。现在:
    worker 的 token 按 crew 累计 ✓;落定时写成一条带 token 字段的系统备注进会话 ——
    统计与成本随之把 crew 算进去 ✓(字段不进模型请求正文)✓;crewStatus 的返回也带上
    [usage] … ✓。

    • 实测(1 子任务 crew,假端点):落定后会话出现"消耗 12.8k tokens(in 12.0k / out 800)"
      的系统备注 ✓;统计 totalTokens 精确 +12800、归入"子代理"桶 ✓;领队聚合轮的 12800
      单独计 ✓ —— 对账无重复、无遗漏 ✓。
  • Agent 面板输入框自动聚焦(优化清单 P0;用户此前反馈过"必须先点一下输入框"):
    打开面板或从子页回来时,焦点自动交给输入框(跳一帧 + 400ms 延迟 —— @FocusState
    在 onAppear 事务里直接置真走不进 AppKit 的 first responder,AGENTS 记录在案);
    停在子页(轨迹/统计等)时不抢焦点 ✓。

  • 地址栏:输入被立刻清空 / 候选闪一下 / 有网址时不出候选(用户反馈):三个症状是同一个 bug ——
    聚焦时那次"把缓冲播种成当前 URL"的写入(Toolbar.swift 的 .onChange(of: isUrlFocused) 分支)✓。
    它本来就多余(未聚焦期间 .onChange(of: displayedURL) 一直在同步),而它依赖的聚焦通知是
    晚一帧到的(地址栏是 NSViewRepresentable,同步置位会报 "Publishing changes from within view
    updates",所以当初跳了一帧)✓ —— 于是"点进去马上打字"时,这一帧的播种把刚打的字覆盖回 URL ✓;
    紧接着候选浮层因为失焦而收起 ✓(看起来就是"闪一下")✓;"有网址时输入不出候选"同样出在这里
    (缓冲被重置成 URL,模型自然不会为"你打的那串"给候选)✓。

    • 修法:聚焦不再播种 ✓;并给"失焦"通知加了真实状态校验(跳一帧后 field editor 还在 = 仍在
      编辑中,不报失焦)✓。

Fixed

  • Keychain 授权窗能把应用钉死在启动里(本次发版冒烟抓到,v0.3.13 同样中招):
    本地构建是 adhoc 签名——每次重建/换路径 cdhash 都变,Keychain 条目的 ACL 认不出
    当前构建时,SecItemCopyMatching 会向 SecurityAgent 申请授权;实测那个授权窗
    可能永远不渲染(进程和窗口都在、屏幕上什么也没有),而 AgentPreferenceStore.init
    在 applicationWillFinishLaunching 的主线程上同步等它 —— 整个应用死在启动里,
    且之后每次启动都排在同一个隐窗后面,全部挂死(桥无响应、进程活着、无崩溃报告,
    与"主 actor 僵尸"外观一致但根因不同)。

    • 修法:启动与回合中路径的 Keychain 读全部改为非交互
      (kSecUseAuthenticationUI = Fail:失配时失败成"无 Key",绝不等 UI)——
      init 的 refreshKeyState、迁移读、secretsForRedaction(桥驱动的回合没有用户在场)、
      critic 档案检查、PasswordStore.loadAll(init 全量读);设置页等用户在场的路径
      保持交互(授权窗可答,答一次"总是允许"即恢复)✓。adhoc 构建换路径后 API Key
      显示为未配置属预期,设置里重存一次即可 ✓。
  • 脱敏代码把应用打成了"僵尸":主 actor 永久卡死,界面却照旧(排查"桥端点忽然没响应"时
    挖到底):SecretRedactor 的 NSRange 在循环外算了一次 ✓,而循环里每次都改写文本
    ([redacted] 比任何命中都短 → 串必然变短 ✓)—— 下一轮 firstMatch 带着越界的旧范围
    调用 Foundation ✓,直接抛 NSRangeException ✓。异常从 Swift async 帧里穿出去(工具结果
    入会话这条路径 ✓),被 HIServices 的处理器吞掉 ✓,主 actor 的执行器就此损坏:此后所有
    @MainActor 任务只是排队、永不执行 ✓ —— 桥端点全部无响应(连 /state 都挂)✓、数据文件
    停在出事那一秒 ✓,但窗口照常渲染、AppKit 事件循环照常、甚至还能被 osascript quit
    优雅退出
    ✓,所以看起来完全不像崩了(没有崩溃报告 ✓,只有 log 里那行 NSRangeException)。

    • 修法:NSRange 每轮重算、现算现用 ✓;并审计了全仓另外两处正则
      (MarkdownRendererView / DevToolsPanel)—— 都是现算现用 ✓,只有这一处踩了。
  • 悬空 tool_calls(工具执行中途被杀 → 之后每一轮请求都被服务端拒绝)
    (优化清单"检查点/恢复"的最小切片):应用在工具还没跑完时崩溃/退出,最后一条
    assistant 的 tool_calls 没有等到结果 —— OpenAI 兼容服务会拒绝
    ("assistant message with tool_calls must be followed by tool messages"),且之后每一轮
    都拒绝,会话等于报废(模型永远答不上来)。
    现在构造请求前把末尾悬空的调用剥掉(正文保留;正文也为空则整条丢弃)✓,
    只处理崩溃形态,不猜更多 ✓;单测 4 项(剥字段/整条丢弃/正常会话不受影响)✓。


安装

未签名构建(CI 无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。

  1. 解压 Desire-v0.3.14-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.3.14-macos-arm64.zip .)。
  2. 安装:把解压出来的 Desire.app 拖进 /Applications。
  3. 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
  1. 打开 Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。

报 xattr: No such file: Desire.app 说明当前目录里没有解压好的 app——
命令要写完整路径(如 /Applications/Desire.app),或先 cd 到它所在目录。