v0.3.14
Added
-
中断恢复入口:未回答的提问一键续跑(优化清单"检查点/恢复"的 UX 切片):会话以
未获回答的用户提问结尾时(工具执行中途被杀的典型残留 —— 提问已落盘、回答没有),
面板在输入框上方显示"上一条提问还没有收到回答 · 继续回答"提示条 ✓;点击为已有
的那条提问直接开一轮(不重复 append、不重复记历史 ✓)。桥端点
POST /agent/resume同能力(自动化用)✓。 -
空回合自动重试一次(优化清单收尾):模型偶发返回空内容(上游抖动、网关抽风)时,
不再把可见警告直接甩给用户 —— 自动再试一次,仍空才给出说清原因的警告 ✓。
有界:至多多一次调用,不会循环 ✓;与瞬态重试、超限减半重试的既有语义并存 ✓。- 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
(原发 + 自动重试)+ 1 次标题生成,最终以警告收尾、无死循环 ✓。
- 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
-
只读工具并行执行(优化清单 P2):连续的
.readonly工具(getPageText、快照、
readFile、检索类……)从逐个 await 改为整段并发、按原顺序落结果 —— gate 仍逐个过
(readonly 从不弹审批、取消即拒),toolCallId配对不受执行顺序影响 ✓。
多读类回合(检查多个元素、读多个文件)的墙钟直接省一半以上 ✓。- 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
(位置配对)—— 串行时代恰好不出错,并行完成后结果全部张冠李戴(实测 3 个并行
readFile 的结果互相错位)✓。现在按 toolCallId 精确认领(旧会话没有 id 的退回
位置配对)✓;单测加了并行批的配对回归(旧算法下该用例必失败)✓。
- 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
-
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
单独计 ✓ —— 对账无重复、无遗漏 ✓。
- 实测(1 子任务 crew,假端点):落定后会话出现"消耗 12.8k tokens(in 12.0k / out 800)"
-
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 还在 = 仍在
编辑中,不报失焦)✓。
- 修法:聚焦不再播种 ✓;并给"失焦"通知加了真实状态校验(跳一帧后 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
显示为未配置属预期,设置里重存一次即可 ✓。
- 修法:启动与回合中路径的 Keychain 读全部改为非交互
-
脱敏代码把应用打成了"僵尸":主 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+)。
- 解压
Desire-v0.3.14-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.3.14-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。