交接:RWork

这份文件是给下一个会话看的。新会话从这里开始,每一轮都有完整余量。 状态是已经跑通并推送过的:接手时先看第二、三节,别重做已验证的东西。

远程已迁到 git@gitcode.com:CoderLife/RWork.git(旧的 CodeLife_leno/RWork 会重定向)。 2026-10-21 已推送 main、全部 exp/* 分支与 v0.3.0 / v0.4.0 标签。

最近一批(已全部推送;第 51/52 批钩子 PASSED)

提交 内容
11ab72d 第 52 批:实验运行器设置组 + dry-run 规划器(0.4.0)。先合入 harness_loop / run_identity / judge_gate 三个基础设施模块,再把实验运行器做成设置页「实验」分页的一组真实开关:启用、并行/顺序、k(1–3)、最大 rollout、输出目录。每个开关都有消费者:关闭时其余控件不渲染、dry_run 直接报错;打开后「生成预览计划」写 config.json + plan.json,并拒绝预算 <4k。不发模型请求;真实 rollout 的开关等执行器接上再加。真机验证:隔离库里打开实验分页 → 点按钮 → 显示 rwork-local-agg1-dryrun / 3 阶段 / R=4 / P=3 / 输出路径,磁盘上两个 JSON 可读回。
de61e31 第 51 批:前三个 Harness² 实验切片接线后合入(0.3.0)。① prompt-discipline:接进 build_request,设置页开关默认关;补英文块与 locale;DeepSeek A/B 源材料注入指令 3/4→0/4、擅自加列 2/4→0/4,系统提示 587→1193 字符。② harness-tree:设置页 skill 卡片加「检查当前 harness」只读诊断,逐条报 name/description/触发线/非法目录名;Surface 增加 require_scripts,live harness 不误报 skill 私有脚本。③ memory-local-refs:索引写入改为 FileSpill 收集 + Db::run 批量写;删会话清该会话索引与 .rwork/<会话>/context/,全局 pastes/ 文件保留;purge 只删索引行。worktree 统一在 experiments/worktrees/,路线图见 experiments/ROADMAP.md。
67c0e7f 第 49 批:分句召回 + 换话题按段判(用户点名的两处缺口),并修两条 GUI 偶发。① 分句召回:一句话里几件事时整句只算一个向量,它是几件事的平均——跟哪一件都不够像 ⇒ 分档整体滑向中/弱,而事实那一节只收强档 ⇒ 它空掉;关键词那条路更直白(整句是当短语查的,长消息几乎必然查不到)。新增 rwork_core::segments:for_recall 按分项边界切段(编号、带圈数字、项目符号、行首并列/递进词、分号;不把普通换行当边界——贴日志时几十行里没有一行是「一件新的事」),上限 5 段、超出并进最后一段、切不出第二段就原样返回一段(普通消息行为与从前逐字一致);recall_for_turn 逐段嵌入 + 逐段三层召回,merge_hits 同一条保留最高分、条数收在单段预算内;任何一段失败 ⇒ 整条失败(不跳过:跳过会让结果静默地不完整)。looks_multi_issue(拆解触发)同批从 turn.rs 搬进该模块——两套判据共用一份分项词表,问的问题不同(要不要拆 vs 怎么切)。② 换话题按段判:每段各自与重心比(阈值 0.62),off >= 2 直接算换(一处是口误、两处就是换了话题)、off == 1 记待定等下一轮、显式说法落在哪段就把重心置成那一段(不是整句均值——均值会把新旧话题焊在一起)。③ 修两条 GUI 偶发(同一类:等待方式,不是被测行为):pruning_orphans_from_the_behavior_page_removes_them 原先「渲染一帧就断言」(它等的是阻塞池上的读库结果)、clicking_new_chat_forgets_the_conversation_it_was_on 点击后只 run_until_parked 一次就断言状态已清——同一份代码单独跑绿、整包跑红;改成等条件成立后连跑五遍全绿。验证:cargo test --workspace 1048 条单元 + 48 条端到端全绿、clippy 零告警、convention/docs 0 违规、release 0.2.0 构建通过;反向验证两处(for_recall 原样返回整句 ⇒ 接缝用例红;漂移判据改回「连续两轮」⇒ 按段判用例红);真机冒烟:release 启动落在核心行为页、窗口 1514×938 抓图、日志无错误(没做:真机跑一轮对话看 segments= 那行日志——那需要 provider 与额度)。文档:新增 docs/04 §11「记忆召回与意图判定」(这条链此前一个字都没有)、docs/02 加 FR-9.9/9.10、REQUIREMENTS §2.7 两行、FEATURES 两段、README 版本历史;turn.rs 搬走 137 行后用「按原文重定位」脚本改了 118 处引用(含约 70 处门禁完全看不见的短式 `:行号`——脚本这次支持了它,方向记进 docs/09 F4);docs/05~08 按本次运行重跑(1048/48、清单 1128 行)。版本 0.2.0
aecc024 第 48 批:记忆取用闭环 + 定时任务失败重试 + 两处静默退化(外部两个项目 codex-rust / momo-code 逐模块读完挑出的五条)。① 记忆:memory.last_used_at 此前全仓没有生产写入者(touch_memory 一次也没被调过),召回里那句 ORDER BY last_used_at DESC 实际等价于按创建时间排序、界面上的「最近使用」显示的是写入时刻——现在每次召回落一行 memory_use(迁移 20261012000100,一行一次、只追加)并真的把它写上;核心行为页每条记忆多一行「被取用 N 次」/「还从未被取用」。成败只算「有独立结论」的那一次(共现归因,抄自 momo-code 的 experience/closure.ts):恰好命中一条才记功,多条标 coexposed,没跑完那一轮的残留先标 interrupted。顺带修掉它带出来的一处耦合:意图视图的重建指纹原本含 last_used_at,字段一开写就每轮判成「记忆变了」、缓存永不命中(the_intent_view_is_cached_until_intent_or_memory_changes 抓住)。② 定时任务:计划触发那一轮失败后再试一次(60 秒后、只一次),走既有的 next_run_at 通道(「迟到宽限」「cron 不自动补跑」一个字没改),执行历史标明「将重试 <时刻>」;execute 入口新增防重入(task_run_in_flight),到点触发 / 补做 / 接着做被中断的那一轮共用一道门。③ 两处静默退化:plan_json 从「第一个 [ 到最后一个 ]」改成括号配对(模型在数组后补一句含 [1] 的说明会让整次拆解悄悄退回无条件清单);命令与脚本输出进上下文前剥掉终端控制序列(新增 format::strip_ansi,shell/script/stream 三处接线)。验证:cargo test --workspace 1037 条单元 + 48 条端到端全绿、clippy 零告警、convention/docs 0 违规、release 构建通过;反向验证五处(去掉 last_used_at 的 UPDATE / 改成一律记赢 / 不标 interrupted / retry_at 恒 None / 守卫恒假,各自单独改回时对应用例立刻红)。真机(隔离库 + release 二进制):核心行为页 PrintWindow 抓图 1514×938;「有 3 次取用」vs「删掉取用记录」两次渲染差异 8775 像素、全部落在记忆行区域(x 296..715,y 176..270);日志 读取长期记忆 … count=2。没做到的:「将重试」那一行要手点展开执行历史才渲染(脚本没做这次点击),它由进程内窗口用例 a_run_that_will_retry_says_so 钉住。文档:FR-1.10/2.8/9.7/9.8/14.7/14.8;docs/03 表清单改成符号级引用(实测原行号改动前就偏了);docs/04~10 的 113 处引用按原文重定位(14 处本批漂移、53 处改动前就错);docs/05~08 按本次运行重跑;docs/09 新增 I1/I2。版本 0.1.1
c0fa85d 第 47 批:本机裁决模型调研 + H 组路线图(纯文档,产品代码零改动)。用户提出把 StartLux-Decision 这类「有类型的判定」模型内置进 RWork,随后限定「非商业许可不适用,自己训一个或找一个常规的」。① 许可:StartLux-Decision 的权重是 CC BY-NC 4.0(GGUF 仓 LICENSE 原文即 Attribution-NonCommercial)→ 出局;同类里 Intern-Decision(上海 AI Lab,0.8B/2B/4B)与 Mapika/decider 都是 Apache-2.0,ModelScope / HF 都有(sha256 齐全,接得上现有那套下载校验),底座 Qwen3.5 本身也是 Apache-2.0 → 自训的路同样干净。② CPU 实测(i9-13950HX / llama.cpp b11521 CPU 构建 / 16 线程):预填充 210–290 tok/s(8→24 线程 231→290,两次运行约 10% 波动),0.8B 与 2B 只差约 15% → 瓶颈是 qwen3.5 混合线性注意力的 CPU 核,不是权重体积;成本≈状态 token÷300(300 token≈0.8 s,够格每轮跑;3k≈13 s,直接排除);同一份 3k 上下文上 3 题一趟 2.8 s(前缀缓存命中)vs 3 次独立请求 3×12.8 s。③ 读数通路(这条决定能不能做):n_probs=64 的 top_logprobs → 按本题候选符号的精确 token id 过滤 → 按该类型温度缩放,已在 llama.cpp 的 HTTP 上实测跑通;<decision> 在社区 GGUF 里仍是单 token(248077)、A=32;判定过程不在权重里 → 读数与权重解耦(换权重或自训都不重做这一层)。④ 质量只有三个中文单例:0.8B 两条接近硬币、一条答错;2B 三条方向都对(0.956 / 0.660 / 0.883)——样本量 1,不能当结论。落点:docs/10-实验性质功能/本机裁决模型调研与设计.md(实验性质;附件=三个探针脚本 + 两份原始输出,含一次口径修正前后的对照);docs/09 新增 §6.5 的 H1–H6,其中 H1(50–100 条中文样本)是闸门——数据说不行就停在那里。未做:任何产品代码与模型下载入口;测量用的权重与运行时在 %TEMP%\rwork-judge(约 2.1 GB,不在仓库里)
e1bfeed 第 46 批:无人值守(G1)+ 定时任务触发侧端到端(C2)。① C2:判定与执行各有测试,而中间那一段(轮询线程、due_tasks、迟到判定)一次都没被驱动过——坏掉不报错、只表现为「该跑没跑」。轮询节拍与迟到宽限收进 Cadence(生产 30 秒一轮,测试几十毫秒 + 跑几轮收工),auto_run_is_due 收下宽限,spawn_with 交回线程句柄。两条用例起真轮询线程 + 真 HTTP + 真库:到点真跑(task_run ok、模型真被调用、next_run_at 推到将来);迟到 6 小时的 cron 零记录零调用、last_run_at 没动、missed_runs 算得出「错过」。反向验证两个方向都做过(判定恒真 ⇒ 第二条红,恒假 ⇒ 第一条红)。② G1:关窗口 ≠ 退出(QuitMode::Explicit + on_window_should_close 按当前设置决定收进托盘还是退出;托盘装不上就退回退出,绝不藏起一个回不来的窗口);托盘是自建消息窗口 + 独立线程(gpui 的窗口过程拿不到 WM_APP,拿它的窗口当宿主换不来回调);单实例锁在打开数据库之前取;开机自启写 HKCU\...\Run 的 RWork = "<exe>" --silent(注册表是唯一事实);每轮任务多写一份 run(与 task_run 同 id)+ 每步一条 run_step,启动时 resume_interrupted 把被中断的那一轮接着做完(复用原会话、提示词写清停在哪一步)。③ 验证:真机 verify-tray-and-instance.ps1 22 项 PASS(关窗后进程仍在、窗口不可见、请求打开后可见、第二个实例退出并把老窗口叫回来、--silent 不显示窗口且能退出、用户日志目录字节数前后一致);真机 verify-unattended.ps1(预置已到点的任务 + --silent,没有任何窗口)→ 日志 定时任务到点,开始执行 + 库里一条 task_run;两个脚本随 docs/08 入库。④ 文档:docs/02 新增 FR-14(六条)、docs/03 模块与流程、docs/04 新增第 10 节、docs/05–docs/08 用例表与计数(1020 通过 / 2 忽略)、docs/09 G1+C2 收口(30 项已完成 13)+ CSV、REQUIREMENTS §2.13/§3.1/§3.14/§四、FEATURES、README;改动涉及的 路径:行号 全部按偏移量换算并抽查到符号。没做:docs/10 设想的通用 job 表仍不存在(续跑只覆盖定时任务这一轮)、托盘菜单的鼠标点击(两条命令路径都用消息走通了,真人点那一下没做)、开机自启的重启那一步(项写了也删干净了)
2972dc6 第 45 批:读的边界定案 + 审批弹窗点击进自动化。① A2:核实「读的风险由什么兜住」时推翻了条目原本的假设——RWork 自己的 read/grep/glob 同样是 ToolTier::Read,三种档位下一律自动放行、路径解析也不做工作区包含检查,因此「把它的读收进管辖」只是给同一次读取多加一层形式。定案接受读不需要管辖,结论、兜底(默认关闭 / 写类动作强制回到权限链 / 每一次调用都标出来)与残余风险写进 docs/01 §8 第 3 条;并补上 docs/02 §7 一直声称、实现里却缺的对称标注:它自己执行的读与搜索在工具块上标成「由它自己执行(转述)」(decision_is_transcription + 新键 chat.tool.transcribed,与「未经 RWork 的写入」互斥)。② A3:实测「注入不了鼠标」只对从外部往真实窗口成立——进程内的测试窗口可以用 TestWindowExt::click 合成平台鼠标事件(.occlude() 的模态浮层里也能命中),因此产品代码不用改:turn::approval_dialog::clicking_allow_once_runs_the_tool_and_leaves_a_receipt 走完弹窗 → 允许 → 执行 → 回执(真窗口 + 真帧 + 真点击,调私有 run_calls,工具清单手写、不发模型请求),断言文件落盘、interaction 行 AllowOnce、账本 approved 且指回提问、弹窗收起;反向验证:点成 perm-deny 立刻红。③ 文档:docs/09 A2/A3 收口(30 项已完成 11)+ CSV;docs/05/docs/06 新增用例与 §3.5 骨架;docs/07/docs/08 计数 1011 通过 / 0 失败 / 1 忽略;REQUIREMENTS §3.0/§3.2 更正「合成鼠标进不去」的适用范围;FEATURES/README 补标注说明;改动涉及的 路径:行号 全部按偏移量换算并抽查到符号(顺带修掉 docs/05、docs/06 里几处早就偏了的引用),测试清单附件重生成。没做人眼验证:转述标注只在外部智能体真跑过一轮后才出现,本批只有帧级断言(元素存在且可见)
2aa5fe7 第 44 批:等待不再睡住界面 + 长会话重绘成本与块数脱钩。① E6:四处 std::thread::sleep(25ms) 轮询(装本机 OCR、环境探测、装运行时、手动整合)换成 rwork_app::wait::for_result(执行器定时器 + 如实收场:发送端没了/等满上限都不再静默)。A/B 实测(mock 把整合那一次拖慢 4 秒,点完 1 秒去点侧栏「设置」,看应用日志里两行时间戳谁先):旧写法切页被压到 阻塞轮询结束 之后(2.4 秒),新写法 0.2 秒内就切过去了。顺带发现这条缺陷截图抓不到——PrintWindow 是同步投递,窗口线程被睡住时截图自己也在等。② E5:Store::size_bytes(每帧 3 次 fs::metadata)改成随计数一起取的缓存值;对话正文只建视口内的块(built_range + 等高占位,page_chat.rs)。A/B(同一份二进制 + RWORK_NO_VIRTUALIZE=1):380 条消息(190 块)每次重绘 97.72 ms → 18.59 ms 进程 CPU(19.21% → 3.59% 单核,5.3 倍)。没达到 E5 原定验收线(38 条消息 <1.5% 单核):剩下的 ~18 ms 与对话长度无关,瓶颈已转到外壳整窗重绘本身——这条保留在 docs/09 E5 里,落点改过了。③ 文档:C5 那处漂移(第 39 批已完成却仍挂 P1)修掉,新增 G1/G2/G3 三条(无人值守 / 感知与操作 / 自我验证)。验证:cargo test --workspace 1009 通过 + clippy 零告警 + convention/docs 0 违规;单元测试新增 5 条(wait.rs 2 条 + page_chat::block_virtualization_tests 3 条),两条都做过反向验证
9a940bc 第 43 批:窗口失去前台后不再每 500 毫秒整窗重绘。用户报「打开之后总是定期占用 2-3% CPU,有什么自动执行的代码么」——查下来没有失控的自动任务(全仓只有调度线程 30 秒一次 SQL 与两处 5 分钟空闲卸载);烧 CPU 的是光标闪烁:BlinkCursor 每 500 毫秒 cx.notify() 一次,而 mark_view_dirty 会把祖先链全标脏、引擎不复用未变子树,于是一次翻转 = 一次整窗重绘(4.4–11.5 ms),而光标只在窗口是前台时才画。上游只实现了「激活就闪」,没有「失去激活就停」。实测(窗口全程不是前台):聊天输入框有焦点 2 帧/秒、2.44% 单核(主线程 2.30%);修后同一条生产路径(投 WM_ACTIVATE(WA_INACTIVE))0.40% 单核(主线程 0.20%),投回 WA_ACTIVE 2.80%(光标照常闪)。补丁在本地副本(第二处),门禁 vendor_patch.rs 第二条 + 副本用例 a_suspended_cursor_stops_toggling;剩下的每帧成本记进 docs/09 E5/E6
944e0ee 第 41 批:tests/omp_fake.rs / omp_session.rs 的假报错——编辑器里一直红(E0432 unresolved import,即 cargo check --workspace --all-targets 失败),而 cargo test --workspace 与 clippy 全绿。真因不是代码:那条链用的 librwork_core-<hash>.rmeta 是加 omp_client/omp_protocol 之前编出来的,而 lib.rs 的 mtime 比这份产物更早(带时间戳从 worktree 拷回主工作区 → 内容变了 mtime 没往前走),cargo 的「源文件比产物新才重编」判定于是永远 fresh。cargo clean -p rwork-core 后 check 全绿(7.9 秒),两个文件的编辑器诊断 6 → 0 条;代码一个字节没动,判别特征与修法写进 §三之二
d5362ac 第 42 批:两个本机模型统一到一处——向量模型原本在程序目录旁的 .models/(用户要求整份拷走即用),OCR 的模型与运行时在数据目录的 ocr/(第 5 批要求跟数据库一起备份)。两条要求互相顶,用户拍板统一到程序目录这一侧:现在只有一个根(rwork_embed::models_root():RWORK_MODELS → exe 旁 .models/ → 向上三层),嵌入住 <根>/<模型名>/、OCR 住 <根>/ocr/,RWORK_OCR_DIR 删掉;盘上已有的模型不搬家(用户选择),新位置没有就是「还没下载」。验证:新用例断言「两者同一个根」+ 真下载一次(临时用例,跑完删)五个文件全落 <根>/ocr/、哈希逐个核对一致、status=Ready(147 秒)+ cargo test --workspace 全绿 + clippy 零告警 + convention/docs 0 违规;真窗口截图确认设置页「本地 OCR」的存放位置是 …\RWork\.models\ocr(同批抓出并修掉文案里原样显示的反引号,a7ce0f7)
de9a16f 第 40 批:人机交互落成数据——新表 interaction(迁移 20261006000100)记「谁问的、当时问的什么、用户怎么答的」,提问先落一行、回答再补上,因此「问了没答」与「拒绝」是两件事;运行链路那一步与账本那一行挂同一个 id(run_step.interaction_id / audit_record.interaction_id,subject = 工具结果的 tool_call_id),记忆与定时任务候选的「不记 / 不建」也各落一行;界面读回(链路写「问过:总是允许」、候选卡写「这类提议你已经跳过 N 次」)。意图视图补齐:事实按相关度只收强档那几条、进视图的不在记忆节重复,视图按会话缓存(intent_view 表,换话题或记忆集合变了才重建)。外部智能体那条也带记忆与视图(新会话或换话题时作为一段话交过去——此前一个 memories 都不读)。验证:cargo test --workspace 全绿 + clippy 零告警 + convention/docs/vendor_patch;新增 13 条用例,其中 a_permission_question_and_its_answer_are_recorded_and_linked 抓出过一次 LEFT JOIN 的列名歧义。没做:外部注入那条的接线只有单测(真机跑 omp 要花额度);运行链路那行「问过」没有真窗口测试;界面观感仍未经人眼(.verify/gui 已不在工作区)
7fd50ac 第 39 批:记忆的意图视图——整合出的「决定 / 待办」带上类别与出处入库(迁移 20261005000100,老数据按正文前缀回填),召回不看相似度按类别补齐、build_request 列成「已经定下的 / 接下来要做」两节(事实不进去、也不在普通记忆那节重复),界面显示类别标签与「从第 12–18 条消息总结」。同批修 C5:memory_vec 按当时的模型维度建、换模型时 IF NOT EXISTS 是静默 no-op → 精排把两个模型算出来的向量混着算;现在索引带身份(模型标识 + 维度),不一致就丢掉重建,没装模型时什么都不做。验证:cargo test --workspace 全绿 + clippy 零告警 + convention/docs/vendor_patch;用户库副本上回填准确(12 条里 4 条决定 + 2 条待办)、库里未记身份 → 下次带模型启动走重建。没做:真机点一遍界面(.verify/gui 那套工具已不在工作区)
133a513 第 38 批:外部智能体的会话标识记在对话上(conversation.external_session,迁移 20261004000100),起进程带 --resume、握手后从 get_state 写回;接不上时去掉参数重开并如实通知(omp.session_resume_failed)。真机实测过 resume 语义(前缀/路径都能接回、messageCount 保留;没内容的会话在盘上不存在),没花模型额度
3e3650c 文档:VENDORED.md 新增「退出条件」——把 vendor/gpui-base 什么时候能整份删掉变成可查的判据(看新版闭包里的 text_style 从哪来)+ 要一起删的 8 处。此前只有一句「上游修好后删掉即可」,于是没人看
f396ef1 第 37 批:给宿主工具回路加假 omp 服务器(tests/omp_fake.rs + src/bin/mock_omp.rs),不装 omp 也能跑「请求 → 判定 → 执行 → 回执」。夹具刻意用 Rust 而不是 python:第一版写成 mock_omp.py,「通过」其实是静默跳过(这台机器没 python)
aa270bf 文档:结果报告与清单按第 35/37 批重跑(968 → 974),并改掉短式引用(:421-436 这类只写行号的)的 73 处偏移——门禁抓不到这一类的教训写进了 tests/docs.rs
8afe418 第 37 批:修掉那条「偶发失败」。它不是环境问题:夹具路径靠 100ns 粒度的时钟去重(8 线程取 160 万个值,92.6% 有重复),撞车后两个连接开同一个库,而 WAL 转换那条路径不走 busy handler(40 轮里 38 轮 database is locked)。加进程内原子序号修掉,附回归用例与反向验证
4c61901 第 36 批:文档门禁——AGENTS.md 加第 4 节「文档要跟着代码改」+ --test docs(图片引用、路径:行号、索引可达性)。同批把 A1 造成的 70 处行号漂移改回来
1d6d879 文档:交接补上第 34–36 批
1a2012e 第 35 批:外部智能体失去写入管辖时说出来(清空只读清单、或自写 --tools 含 write/edit/bash 两种形状),external-write 标记 + 对话里「未经 RWork 的写入」;顺带修两个同类缺陷(外部调用被画成红色失败、界面与库说法不同)与 main 上本就红的 5 处 clippy
e7ebb60 文档:记忆系统的设计从 REQUIREMENTS.md §3.11/§3.12 移出,独立成 docs/10-实验性质功能/记忆系统与本地小模型.md(用户要求 D4 先不推进;卡点是与 local:// 落盘引用的集成,见那份文档 §10)。同批收窄了 .gitignore 里整目录忽略 docs/ 的那条——它此前让整套文档一个字节都提交不进去,而 git status 里也看不见
024a6e8 定时任务:补上执行侧的端到端测试(真 HTTP + 真库;见第二节)。同批把 REQUIREMENTS.md §3.1 收窄成它真正剩下的部分——触发侧仍未端到端(见第三节)
928a8ea 账本:定时任务的模型调用与工具调用也进 audit_record(原来一次都没写,「这个月花了多少」在不跑对话时是假的)。抽出 audit_model_call / audit_tool_call,对话侧三个写入函数改为调它们
49a1938 同一批的收尾:裁剪会把任务指令丢掉(裁剪从最老的那条开始丢,而定时任务的指令正好是最老的)——「整段历史只有一条用户消息」时那一条不裁;新会话第一轮是同一个形状,同样受益
d905d24 第 31 批:定时任务与对话走同一套上下文——共用 turn::build_request(基座提示、记忆、skill 索引、压缩、裁剪、超长输出落盘),历史从 wire::ChatMessage 换成 core::Message;顺带修「压缩对单条用户消息完全无效」与「skill 索引是空的」
c4d8714 上下文:工具输出可读回、压缩可见;修掉 local:// 引用读不回来、run_step 步号每轮重数(把前一轮的步骤覆盖掉)两个既有缺陷
2e09f93 文档:交接补上定时任务「不自动补跑」这条(含验证证据与边界)
e4876b8 定时任务:错过的 cron 时间点不再自动补跑(用户要求「先问我」)——迟到超过一个轮询周期就只把 next_run_at 推到将来,这一次交给核心行为页的「补做 / 不补」;间隔型任务不受影响(见第二节那一行)
dd7b24b 文档:交接补上第 29 批
190bf1d 第 29 批:界面线程不再碰 IO(切会话建块搬进读库那趟、核心行为页六趟查询合成一次后台往返 + 票据、设置页 skill 扫描进后台)。同一批量清了「启动/切会话」的真实开销(我们的代码只占 ~13ms),并留下一条否定结论:拆子视图换不来帧(见 §三之二)
99c0819 修:右侧三个面板正文看不见(flex_1 在内容撑高的父元素里算成 0 → 滚动区拿到 0 高度)、开面板把下面的图标顶下去(改成绝对定位浮层)、滚轮同时滚面板与对话(浮层要 occlude())。新增 ui::capped_scroll(高度 = 内容与上限取小)
6fdd7f4 第 28 批:滚动条跟着内容跑(static_scroll 把浮层放进了滚动容器)+ 产出物按会话区分(.rwork/<会话 id>/ 与 file_provenance)
28770c4 第 28 批:用量审计的调用明细补搜索、状态筛选、看得见的分页;修一处会丢行的 keyset 翻页
a29b4f8 第 28 批:整合长期记忆被服务端拒收(素材里带了 tool 消息)
4b29e34 第 27 批:执行计划在流水线里接力(一份计划、按棒号退回重做);顺带修三个让流水线跑不起来的缺陷(内置角色进不了团队、每位都插一次用户消息、会话记着的团队被忽略)
8bf0c97 第 26 批:执行计划的两个上限进设置、孤儿向量的一键清、核心行为页按作用域列历次拆解、拆解触发认中文分项写法;顺带修「切进核心行为页不重读」
8a009fd 修:正文「字体堆叠」(上游 gpui-base 行内排版测量用错字体)——本地副本 vendor/gpui-base + [patch.crates-io],报告见 UPSTREAM_REPORT.md
47f22d2 右侧三个面板:位置/外观/关闭按钮 + 统计口径(模型用时)+ 拆解触发(带圈数字)
2d55f2b 记忆分桶召回(强/中/弱 → 全文/一行/索引)
fd7ae6a 修:老记忆搜不到也删不掉(迁移里补一次 FTS 重建)
c5c0027 修:新建对话之后第一条消息写进上一段会话
871bd6f 执行计划:带条件的 DAG + 执行器 + 审计 + 回滚
09e56e6 执行计划面板(对话右侧留白)
4acccfc 修:回滚重跑有副作用的步骤没问用户
3b83eea 对话过程日志补全(用户发送/回合开始/裁剪/审批/这一轮结束)
9463132 文档:执行计划的已知限制
f936a7e 定时任务那条路补日志(原来一行都没有)
d361ccf 日志保留改成「一天一个文件、留最近 10 天」(tracing-appender)
bb958e3 图片走本机 OCR(百度 PP-OCRv6),模型与运行时按需下载
1be52ea 换话题时自动整合长期记忆(默认关闭,三条同时成立才跑)

一、这个项目是什么

D:/Develop/workspace/gitonline/RWork —— Rust + GPUI 的桌面工作台(内嵌 SQLite), 在 main 上开发,远端 CodeLife_leno/RWork。

硬性规矩(AGENTS.md):注释、提交信息、给用户的说明一律中文;门禁四条必须全绿:

cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p rwork-integration-tests --test convention   # 注释语言:0 违规
cargo test -p rwork-integration-tests --test docs         # 文档引用:0 违规

docs 那条是本批新加的:它查 docs/ 里的图片引用、路径:行号 是否指得着、 每份文档能否从 docs/README.md 走到。它抓不住「行号漂了但仍在文件范围内」—— 为一次改动在文件中间插了几行,后面所有行号就一起偏,工具帮不了你, 只能人回头改(AGENTS.md 第 4 节)。本批实测漂了 70 处才发现这件事。

做完就提交并推送(推送会跑 pre-receive 检查,被拦下要如实报出原因)。 不要用 git -c core.autocrlf=false commit(会把 CRLF 写进 blob,diff 全是抖动)。

文档在哪:仓库带一整套软件工程文档,入口是 docs/README.md——按阶段分目录 (01 架构 → 02 需求 → 03 概要设计 → 04 详细设计 → 05/06 测试设计 → 07/08 测试结果 → 09 后续任务清单 → 10 实验性质功能)。 两条规矩:

  • docs/10-实验性质功能/ 里放尚未定案的设计(当前是记忆系统与本地小模型, 以及它与 local:// 落盘引用的集成问题)。那里的内容只能当提案读, 引用时必须写明「实验性质」;转正与搬家的规则写在那个目录的 README.md 里。
  • 改动要按影响回头改文档:新需求进 02、设计进 03/04、待办进 09。 只改代码不改文档,这套东西就会在几批之内腐烂成假的。

二、已验证到哪一步(都跑过端到端,别重做)

能力 端点 证据
本地嵌入 crates/rwork-embed(candle + bge-small-zh,纯 CPU) 加载 + 三句话语义排序,0.09s
向量库 sqlite-vec 0.1.9 + 二值量化(512 维 = 64B/条) store 90 条测试
精排 二值粗排 → 完整向量重算余弦(memory_vector 表) 关掉精排测试必失败 ✓
分桶召回 强=全文 / 中=一行(首句、60 字)/ 弱=合并成一行索引 真实实例跑两轮:日志 hits=4 full=1 one_line=2 index_only=1,请求体里强档全文、中档只剩首句、弱档正文不在、索引行只剩短 id
执行计划(带条件的 DAG) rwork_core::plan + plan/plan_node/plan_attempt 三张表 + 回合循环里的执行器 真实实例 + mock:第 2 步把第 1 步的产出清空 → 退回最早失效的那一步 target=draft invalidated=["draft","chart"] → 重做 → 全部步骤通过条件核对;库里 5 行尝试历史(passed / unsatisfied / rolled_back / passed / passed),rollbacks=1
记忆召回 会话 > 项目 > 用户 三层 + 关键词兜底 请求体里出现记忆段
老记忆的 FTS 索引 迁移 20260929000100_memory_fts_rebuild 迁移前写入的记忆现在搜得到也删得掉(之前 DELETE 报 database disk image is malformed);测试造老库复现
会话级记忆(写入 + 隔离) 「只在本会话」按钮 → memory.session_id 同会话里召回排第一给全文;新建对话后请求体里不再出现(修前会泄漏)
意图漂移 显式说法 + 向量重心(连续两轮) intent_center 落库 + 请求体里出现换话题说明
提炼 「整合长期记忆」→ 事实/决定/待办 + 前缀策略 库里出现 决定:…/待办:…,纯事实无前缀
执行计划面板 对话右侧留白「执行计划」入口(chat-plan) 截图逐行读回:✓ 1. 写出报告草稿 / 文件 report.md 非空 / 第 1 次:通过 / 退回重做:文件 report.md 是空的 / 第 2 次:通过,右上 已完成 · 共 5 次尝试 · 回滚 1 次。第 24 批起与「我的输入」「这一轮的统计」共用一个右侧栈与一层外壳(标题 + 关闭按钮),三条「点开 → 关闭」的来回由 page_chat::panel_tests 在真实帧上走通
对话过程日志 用户发出一条消息 / 回合开始(含 memories·plan·step)/ 上下文超预算 / 用户已回答审批 / 这一轮结束(轮数·调用数·用时·计划) 真机跑一轮:这一轮结束 … rounds=3 calls=2 elapsed_ms=42625 plan="needs_decision"——42 秒如实报出是停在审批浮层上等的
错过的定时任务要用户点头 scheduler::auto_run_is_due + skip_missed 真机(副本里把任务推到 45 分钟前):启动 40 秒后日志只有 定时任务错过了时间点:不自动补跑 … late_ms=2732747,一条「任务开始」都没有(没有模型调用、没有工具执行),next_run_at 推到次日 09:00;核心行为页显示「有 2 次该执行但没执行」+ 两次「补做 / 不补」。单测钉住边界:迟到 ≤30 秒照跑、>60 秒等用户、间隔型迟到 3 天照跑
定时任务路径的日志 scheduler.rs(原来一行都没有) 真机:定时任务到点,开始执行 … late_ms=128953 → 任务开始 … tools=1 → 任务:开始一轮 → 工具前后两行 → 任务结束 … elapsed_ms=103
日志保留 一天一个文件(RollingFileAppender,UTC 日期)+ 留最近 10 个 真机:预置 12 份历史日志 → 启动后目录里只剩 10 个,最老的被删;正在写的是 rwork.log.2026-09-15(UTC),启动那行打出的是目录
模型管理 .models 下载 + 官方 sha256 校验 + 进度条 从零下完 95MB,7 个文件校验一致
本机 OCR rwork-ocr(百度 PP-OCRv6 det+rec 的 ONNX + onnxruntime,ort load-dynamic)+ 按需下载 + 设置页卡片 真机:文本模型 + 剪贴板图片 → 请求体里是清单行「图片:① sample.png(…,用本机 OCR 识别成文字…)」加五行识别结果;900×320 的图 33ms 全对;清空模型目录后点发送先下载 5 个文件(哈希逐个核对一致)再提示「再发一次」(第 42 批起模型与运行时放在 .models/ocr/,与嵌入模型同一个根)
正文不再叠字 本地副本 vendor/gpui-base(InlineFlow 抓下 request_layout 那一刻的文字样式给 measured-layout 闭包) 修前:同一段文字在测量与绘制两侧的字形推进 15px / 8.8px vs 18px / 9px(汉字 1.0em vs 1.2em),字形 id 也完全不同;同一批文字「测量宽度 ÷ 绘制宽度」38 条的中位数 0.826(0.785–0.836)。修后同一批 12 条全部恰好 1.000,两侧字形逐字一致。门禁由 crates/rwork-integration-tests/tests/vendor_patch.rs 防覆盖;完整数据与给上游的报告见 UPSTREAM_REPORT.md
后台不再空转(光标闪烁) 本地副本第二处补丁:blink_cursor::suspend + 窗口激活观察的「失去激活」分支 + on_focus 判 is_window_active(vendor/gpui-base) 修前(隔离库 + 改名副本 + 帧探针 + typeperf,窗口全程不是前台):无输入框有焦点 0 帧/秒;聊天输入框有焦点 2 帧/秒、2.44% 单核(主线程 2.30%);空会话 1.22%;审计页 + 侧栏搜索框 2.24%。每次重绘 4.4–11.5 ms(空会话 4.4 / 38 条消息 11.5 / 审计页 9.8)——「一次翻转 = 一次整窗重绘」是 mark_view_dirty 把祖先链全标脏的结果。修后按同一条生产路径验证(投递 WM_ACTIVATE(WA_INACTIVE),与用户切走窗口时 Windows 发的是同一条消息):0.40% 单核(主线程 2.30% → 0.20%);投回 WA_ACTIVE 2.80%(光标照常闪)。门禁:vendor_patch.rs 的第二条检查 + 副本自带用例 a_suspended_cursor_stops_toggling。剩下的每帧成本记在 docs/09 的 E5/E6
自动整合长期记忆 换话题 + 有信号 + 开关(默认关)三条同时成立才跑;素材带 consolidated_seq 水印 真机两轮:第一轮交代「以后提交信息都用中文」→ 第二轮显式换话题 → 第 3 个请求就是整合调用(素材只有那两条用户消息、不带工具)→ 记忆入库、水印 10;反例(换话题但没交代)请求数不变、记忆与水印都没动
执行计划的两个上限进设置 AppSettings::plan_max_attempts_per_node / plan_max_rollbacks + AppSettings::plan_policy(),执行器不再写 Policy::default() 真机:设置页「模型服务」里那张「执行计划」卡片把单步上限改成 35 再保存 → 库里 plan_max_attempts_per_node=10(夹到上限)、输入框回写成 10。截图逐行读回卡片标题、说明与两行(单步重试上限(1-10)[3]、总回滚上限(0-20)[4])
按作用域列历次拆解 核心行为页「执行计划」区块 + Store::plans_visible_in(scope, 20)(一条 LEFT JOIN plan_node 数进度;可见性复用 visible_keys) 真机:库里两条不同作用域的计划 → 项目上下文只列出项目那条与全局那条,逐行读回 已完成 进度 2/2 步 全局 2026-09-16 13:20、等你确认 进度 1/3 步 回滚 2 次 项目(d:/develop/workspace/gitonline/rwork)
孤儿向量的一键清 核心行为页记忆区那一行 + Store::count_orphan_memory_vectors / prune_all_memory_vectors(按 PRUNE_BATCH 循环到干净) 真机:走一轮对话给两条记忆补上向量(日志 已给缺少向量的记忆补上向量 count=2)→ 删掉其中一条(向量行留下)→ 那一行显示 1、按钮亮色 → 点击 → 日志 已清理孤儿向量 removed=1、重读后显示 0、按钮变灰。库里还留着的那条向量没被顺手清掉
切进核心行为页会重读 Shell::open_page 里对 Page::Behavior 调 BehaviorPage::reload 这条是 GUI 验证时踩出来的缺陷:记忆向量表是第一次对话里才建的,而这一页在启动时就渲染过一次——于是「清理孤儿向量」那一行永远不出现。回归测试 shell::behavior_refresh_tests(去掉那三行就红,实测过)
拆解触发认更多中文分项写法 looks_multi_issue + ordinal_at / has_ordinal_item 单测:1、改掉…2、补测试、(1)…(2)、一、…二、、一是…二是、首先…其次 → 都判成多件事;句中 3.14、2.72、200 行 → 都不是分项(去掉「小数点后还有数字」那条守卫,3.14 是圆周率… 会误判,实测过)
执行计划在流水线里接力 一份计划整条流水线共用 + PlanNode.owner(落库一列 JSON)+ Plan::handoff_for + TurnState.plan_handoff 真机(两位内置角色 + mock,.verify/gui/scenario-team-plan.json):第 1 棒写草稿 → 第 2 棒作图时把草稿清空 → 退回给做这一步的那一棒 member=1、本棒交棒,结束这一棒的回合 member=2 → 拨回序号重做 from=2 to=1 → 第 1 棒重写草稿 → 图表那一步又交回第 2 棒 → 全部步骤通过条件核对。库里 state=done rollbacks=1,owner 分别是 {"index":1,"name":"代码实现"} / {"index":2,"name":"报告成文"},五次尝试 passed/passed/rolled_back/passed/passed
流水线能跑起来(两个阻断缺陷) agent_preset::resolve_team 改查 available_agents;ensure_conversation 只插一次用户消息 修前:内置角色进流水线报「找不到这个智能体」(settings.agents 里从来没有内置角色);第二位永远开不了跑(每位都插一次用户消息,撞 message.id 唯一索引)。修后日志里能看到 流水线:这一棒开始 member=2
会话记着的团队真的会跑 send_now / send_model 用 effective_team;打开会话时把它铺回选择器 修前 self.team.is_empty() 只看界面选择,重开应用后继续一段流水线对话会静默退化成单人。真机:重开应用后选择器显示 [1 代码实现 ×] [2 报告成文 ×],日志里两棒都跑了
整合长期记忆不再被拒 rwork-store::consolidation_material 的取料 SQL 用户报 400 Messages with role 'tool' must be a response to a preceding message with 'tool_calls'。真实库里 32 条 tool 消息、23 条助手消息里 18 条正文为空(只有工具调用那一轮)。现在滤掉这两类,limit 作用在过滤之后。离线测试造一段跑过工具的会话(去掉过滤就红,打印 [User, Assistant, Tool, Assistant]);live_provider 里一条真机测试把同一段素材发给 DeepSeek——去掉过滤时复现出用户那条 400 一字不差,修好后通过(素材 2 条:["User","Assistant"])
调用明细可搜可分页 Store::audit_page / page_audit.rs 关键字搜 model / provider_name / provider_id / error_kind / error_message(% 与 _ 按字面转义,测试里 search("%") 必须为空);状态筛选进界面;底部显示「已加载 N 条 / 加载更多 / 已经到底了」。顺带修 keyset 丢行:同一毫秒 5 行、每页 2 条,旧谓词下只翻出 2 行(实测输出 ["same-ms-3","same-ms-4"]),新游标是 (created_at_ms, id) 二元组
产出物按会话分开 workspace::session_dir + grants_repo::file_provenance_in 脚本落在 <工作区>/.rwork/<会话 id>/scripts/(测试 scripts_of_different_sessions_do_not_mix 造两段会话各跑一次);交付物清单按 file_provenance 分「本会话写出的」与「工作空间里已有的(来自别的会话)」,默认不继承别的会话的产出
滚动条不再跟着内容跑 ui::static_scroll(七处调用同源) 结构改动,没有肉眼验证到「滚动之后」的状态:鼠标注入在 GPUI 窗口上没生效(前后两张截图逐像素一致),详见 REQUIREMENTS.md §3.0。已确认的是:成因在 gpui 侧(gpui-pre 的 elements/div.rs 把滚动容器的每个子节点都整体平移),结构改成与 gpui-component 的 List 一致,程序能起、页面能画、滚动条画在内容区右缘
压缩过的工具输出可读回(第 30 批) rwork_core::context::SpillSink + turn::FileSpill 超过 1200 字的工具输出把全文存进 .rwork/<会话>/context/,摘要里留开头 + 引用。测试里用真正的 read 工具把它读回来,断言与原文逐字一致;另有两条钉住身份是内容哈希(压两次只有一份文件;历史窗口滑动之后仍是同一个文件)。存不下、或这位智能体没勾 read 时退回只压不存——绝不留下读不到的引用
local:// 真的读得回来(第 30 批,修既有缺陷) rwork-tools::spec::resolve_path 系统提示一直教模型「local:// 表示工作区里的文件,需要就用 read 读它」,而路径解析不认这个前缀——照提示去读只会得到「文件不存在」。现在前缀落地(前导斜杠去掉,因此只能指向工作区内)。回归测试实测会咬:把 strip_scheme 临时改成返回 None,三条测试全红,报错正是用户会看到的那句 无法读取 …\local:\.rwork\pastes\paste-1.md: 文件名、目录名或卷标语法不正确。 (os error 123)
运行链路的步不再被覆盖(第 30 批,修既有缺陷) TurnState::next_step run_step 主键是 (run_id, seq) 且写入是覆盖,而步号每轮从 0 重数——第二轮的工具步把第一轮的盖掉了,链路上只剩最后一轮。现在步号跨轮次单调递增,且与「上下文」步共用一个计数器(因此「先压缩再调工具」的顺序是真的)
定时任务与对话同一套上下文(第 31 批) scheduler::execute_inner 改调 turn::build_request 见右栏那条端到端用例;另有一条 app 侧单测断言「基座提示在、任务指令一字不动、超长输出落盘并被换成引用」,两条 core 侧单测钉住单条用户消息的历史也能压、且唯一的用户消息不被裁
定时任务也进账本(第 31 批) turn::audit_model_call / audit_tool_call 那条路此前一次都没写 audit_record。单测:模型调用与工具调用各落一行,且工具那条不把模型那条的汇总数字加上去(同一张表、靠 kind 分开)
定时任务执行侧端到端(第 31 批) scheduler::end_to_end::the_scheduled_path_runs_end_to_end_against_a_live_server 真 TCP + 真 SSE 的脚本化服务、真 SQLite、真文件:跑真实的 run_scheduled → execute_inner,五个工具往返 + 一个结论共六轮,中途真的执行 read 读 200 行文件。断言:六轮请求都发出;早年的工具输出换成 local:// 引用且落盘文件是全文(用末尾那一段区分摘要与全文);账本 6 条模型 + 5 条工具;task_run 收成成功、任务被标记跑过。这条是 REQUIREMENTS §3.1 记着的那个缺口

| 角色的审批档位真的生效(第 32 批,修既有缺陷) | turn::policy_from 改收 &ResolvedAgent(对话 / 流水线 / 定时任务三处同源) | ResolvedAgent::approval_mode 此前在 app 层零消费者:grep approval_mode crates/rwork-app 只命中 settings.approval_mode,而 agent_preset 那条「每位成员保留自己的审批模式」的测试只断言解析结果。回归测试:全局 Yolo + 内置「代码探查」(只读、每次都问)→ 一次写入判成 Ask,跟随全局的那一位判成 Run。顺带删掉两个同样「只写不读」的字段:ToolContext.mode(含 with_mode 与两处调用)与 ChatPage.policy | | 工具表计入上下文预算(第 32 批) | rwork_core::context::estimate_tool_table + build_request 先扣工具表再压缩/裁剪 | 工具表不在消息里,裁剪只看消息,于是它一直是在预算之外占窗口的。回归测试:额度 1000、历史约 600,只带 read 的角色一条不裁、全工具的角色必须裁(去掉扣减即红)。既有测试 a_huge_tool_output_is_saved_and_the_prompt_points_at_it 的预算假设被这条修正暴露出来(它原先按 10k 额度断言「只压不裁」),改成「把自己的工具表那一份加回去」——否则它测的就成了「工具表有多大」 | | 内置角色能当「当前智能体」(第 32 批,修既有缺陷) | agent_preset::active 与 AppSettings::sanitize 都改查 available_agents | 与第 26 批流水线那次是同一个缺陷的另一半:设置页的选择器列的是可选清单(含内置),而解析只查 settings.agents(内置从不写进去)——选中「编程」等于什么都没选,按钮还高亮着;sanitize 更狠,每次载入设置都会把它清掉。测试 a_builtin_role_can_be_the_active_agent、sanitize_keeps_a_builtin_active_agent | | 单人对话的编程档位(第 32 批) | 内置角色 builtin.coder「编程」+ i18n 中英两份 | 与「代码实现」同一套工具与档位,提示词换成单人对话的姿态。两者并存有理由:后者的提示词写着「按上一位给出的计划改代码」,单人对话里没有上一位。only_the_writing_roles_may_write 的预期清单同步加上它 | | 工作空间跟着会话走(第 32 批) | conversation.workspace 列(迁移 20261003000100)+ turn::pinned_workspace + 侧栏显示与切换 | store 往返(含定向改、整条写回不抹掉)与优先级各有测试;page_chat::open 的交付物前缀、memory_scope、发送日志也一并改成按会话取值——否则会出现「显示/记忆/日志按全局,工具按会话」。界面部分只做了逻辑验证:没有肉眼确认切换器切换后的画面 |

| 外部智能体(omp)能接管一轮(第 33 批) | rwork-core::omp_client + omp_protocol + rwork-app::external + 设置里一个开关 | 传输与协议层对着真机写:抓下来的 ready/message_update/tool_execution_*/turn_end 原文直接当夹具(omp_protocol::tests),另有真机测试 tests/omp_session.rs(传输)与 external::tests::a_real_omp_turn_becomes_messages_and_a_ledger_entry(镜像 → 真 SQLite),两条都 #[ignore]。实测抓到的两个真缺陷:①text_end 带的是整段全文,被当成增量拼进去 → 回答变成「好的好的」(修前实测,现已钉住);②等响应时把跳过的帧压回队列、下一轮又取出来再压回去——get_state 永远等不到响应(修前实测 30 秒超时)。镜像路径:真 omp 跑一句 → 正文「好的」、1 路信号、用量非零、费用 0.000073 美元级别的真实数字 | | 外部执行与本地执行不混为一谈(第 33 批) | Signal::ToolFinished 与 StreamEvent::ToolCall 分开;库里的 decision 记 external | 外部代理的工具调用是已经跑完的事实:走 StreamEvent::ToolCall 会被 RWork 的回合循环再执行一遍。测试 an_externally_run_tool_call_is_marked_as_such 钉住标记 | | 账本分得清「谁花的」(第 33 批) | mirror 用对方 turn_end 的 usage/cost,provider_id = omp | 对方报的是它按自己价格表算的 cost(实测一帧:input 186 + output 53 + cacheRead 4480 = totalTokens 4719,cost 0.00007314 美元)。按我们的价格表重算会让两个数都不对,因此原样记并归到 omp 名下 | | 写入类工具交回 RWork(第 34 批) | external::HOST_TOOLS + run_host_tool + set_host_tools;启动参数追加 --tools read,grep,glob | 真机:它请求 rwork_write {"path":"hello.txt","content":"hi"} → AlwaysAsk 下判成「先问」(asked=1)→ 我们执行 → 磁盘上内容真的是 hi → 它收到结果答 done。实测抓到的协议事实:宿主工具不能与它的内置工具重名(即使已被 --tools 关掉),set_host_tools 回 conflicts with an existing tool——因此名字加 rwork_ 前缀。判定那一步抽成 host_tool_plan(纯函数),另有单测钉住「档位决定判不判、文件级拒绝压过 Yolo」 |

注意:sqlite-vec 钉在 0.1.9(0.1.10-alpha.4 编不过); register_vector() 必须在开连接之前调用;量化列写入要包 vec_bit(?)。

三、待办

Harness² 吸收项的实验分支路线图在 experiments/ROADMAP.md(每个功能点的好在哪/坏在哪/ 改什么/怎么改);worktree 在 experiments/worktrees/<branch>/。只有「有调用方 + 有效果证据 + 门禁绿」的切片才合 main。

1. 自动提炼 [已完成](第 25 批 1be52ea)

用户选了 A 方案:复用模型提炼(而不是新写抽取式),并加了三条件。落地形状:

  • 触发:memory::auto_consolidate_due(开关, 换了话题, 信号数) —— 三条同时成立。 换话题用现成的 recall_for_turn 返回的 topic_switched(observe_intent 算出来的,显式说法立刻换、向量漂移要连续两轮);信号数用 memory::memory_signals(对这一段里的用户消息跑 detect)。 为什么非要第三条:换话题只说明「该沉淀了」,不说明「有东西可记」, 而每次触发要花一次模型调用。
  • 素材:只取上次之后的消息。水印是会话表上的 consolidated_seq (消息 seq,不是时间戳——一次提问和它的回答常常同毫秒, 用时间戳要么重发要么漏掉,而漏掉的表现是「这段内容再也不会被总结」)。 整合成功才推水印;答了但没解析出条目(格式错)不推,下次会再试。
  • 开关:AppSettings::auto_consolidate,默认关闭,位置在设置页「模型服务」 卡片里「会话交互轮次」下面(两者都决定「什么时候花一次调用」)。 跑之前会明说「换了话题,正在整理长期记忆(这一次会花一次模型调用)」。
  • 并发:一次只跑一个(MemoryRuntime 上一次比较交换的守卫), 否则用户连着换两次话题时,两次整合会各自读到没有前一份的 existing, 写下重复记忆。

验证:真机(隔离实例 + mock)两轮——第一轮交代「以后提交信息都用中文」, 第二轮显式换话题 → 第三个请求就是整合调用(素材只有那两条用户消息、 不带工具、max_tokens 4096)→ 记忆入库、水印推到 10; 再来一轮「换话题但没有交代」→ 请求数不变、记忆与水印都没动。

2. 零碎

事项 位置 说明
清理孤儿向量的界面 [已完成](第 26 批) 核心行为页记忆区 一行「向量库里有 N 条记录找不到对应的记忆…」+「清理孤儿向量」按钮;后端 count_orphan_memory_vectors / prune_all_memory_vectors(按 PRUNE_BATCH 循环,跨批也清得干净)。一条都没有时按钮禁用。真机验过(见第二节)
执行计划策略进设置 [已完成](第 26 批) AppSettings::plan_max_* + plan_policy() 设置页「模型服务」里那张「执行计划」卡片;sanitize 与输入框同区间(单步 1–10、总回滚 0–20)
按作用域列历史计划 [已完成](第 26 批) 核心行为页「执行计划」区块 Store::plans_visible_in(scope, 20):目标 + 状态 + 进度 + 回滚 + 作用域 + 最近更新。对话右侧那个面板仍只看当前这次
拆解的触发更聪明些 [已完成](第 26 批) looks_multi_issue 认了中文写法的分项编号(1、 (1) 一、 一是…二是)与几个递进词;判据仍是「只在行首/分句开头认编号」,句中数字与小数不算
流水线里的拆解 [已完成](第 26 批) run_team + rwork_core::plan 用户选的语义是 B:整条流水线一份计划,任意棒可回滚——退回到别的棒的产出上就把那一棒叫回来(含被牵连的子孙)。实现与验证见第二节。剩下一个未解的边角:ensure_conversation 的 Err 分支走 fail() 之后,那一轮会一直停在界面上的「停止」状态(fail_with 没生效);踩到时日志里现在有一行 回合失败,已交给界面提示(这一行是本批加的,此前这条路上什么都没有)
界面线程上的 25ms 轮询 [已完成](第 44 批) crates/rwork-app/src/wait.rs 四处 std::thread::sleep 换成执行器定时器;验收见「最近一批」那一行。顺带补齐了超时/发送端消失的收场(此前三处什么都不做,ocr_pending/installing 会永远停在「正在下载」)
每次重绘的固定成本 [部分完成](第 44 批) crates/rwork-app/src/page_chat.rs、shell.rs 对话流那半做完了(只建视口内的块 + 等高占位);剩下的在半路上:把对话流压下去之后,每次重绘仍有 ~18 ms,而它与对话长度无关(= 外壳整窗重绘本身)。原验收线不变(38 条消息在有焦点 + 前台时 <1.5% 单核,现测主线程 ~2.0%),落点从「对话流」改成「整窗重绘」。见 docs/09 §6.1
定时任务触发侧端到端 [待做] scheduler::spawn 执行侧第 31 批已经端到端(第二节最后一行),剩下线程本身:30 秒轮询、due_tasks 查询、auto_run_is_due 的迟到判定、skip_missed 的「错过不自动补跑」——那一段只有 auto_run_is_due 的单元测试。它的失败不会报错,只表现为「该跑没跑」或「刚打开应用就自己跑了一轮」,而前者的现场只有日志里一行。做法:把 POLL 与迟到宽限做成可注入的,起一个已到期的任务断言它跑了一次、再起一个迟到很久的 cron 任务断言它没跑而是被记成「错过」。见 REQUIREMENTS.md §3.1;已进 docs/09-后续任务清单/ 的 C2(附依据与验收标准)

2.5 「离无人值守的通用桌面代理还差什么」——四条线(第 44 批新增,详见 docs/09 §6.3)

用户问「距离桌面端通用 AGI 工具还有多远」。AGI 没有可度量的定义,而天花板由云端模型给, 所以量的是另一个具体目标:无人在场也能把活干完。九条能力轴里,对话闭环 / 工具面 / 计划自纠 / 记忆 / 权限审计都已有实测支撑,缺口集中在这四条:

  1. 无人值守(最先做):定时任务只在应用开着时跑,没有托盘/开机自启;进程被杀时 这一轮是一次性失败(task_run 标失败),没有可续跑的队列(docs/10 设想的 job 表 至今不存在)。验收:关掉窗口(或开机)之后任务照跑;跑到一半被杀,重启能接着跑并说清接着哪一步。
  2. 自我验证:交付物收尾前只有「计划里声明的文件条件」(存在/非空)在管, 没有「把产物真的跑一遍/解析一遍」的独立校验环。
  3. 感知与操作别的应用:没有截图工具、没有键鼠控制、没有浏览器驱动,只能动文件与 shell; 它牵动权限模型,因此第一步是只读的截图 + 视觉解读。
  4. 并行(E4):worktree 隔离是多智能体并行的前提。

2.6 本机裁决模型(第 47 批调研,未定案)

用户提出把 StartLux-Decision 这类「有类型的判定」模型内置进来,随后限定「非商业许可不适用, 自己训一个或找一个常规的」。第 47 批只做了调研与实测(许可核实、CPU 延迟、读数通路、 质量单例),产品代码一行没动;设计与数据在 docs/10-实验性质功能/本机裁决模型调研与设计.md(实验性质,附件里有探针脚本与原始输出), 落地项是 docs/09 §6.5 的 H1–H6。

三条最该先记住的事实:

  • 许可:StartLux-Decision 权重是 CC BY-NC 4.0(非商业)→ 出局;同类里 Intern-Decision(上海 AI Lab,0.8B/2B/4B)与 Mapika/decider 都是 Apache-2.0, ModelScope 上就有(sha256 齐全,接得上现有下载校验)。
  • 成本只看状态长度:CPU 预填充约 210–290 tok/s(i9-13950HX、llama.cpp b11521、 16 线程),≈ 状态 token ÷ 300 → 300 token ≈ 0.8 秒(够格每轮跑),3k token ≈ 13 秒(排除)。 0.8B 与 2B 只差 15%,瓶颈在混合注意力的 CPU 核,不在权重体积。
  • 读数与权重解耦:判定过程(提示词模板 + 字母/占位符读数 + 每类型温度)都在各家的 小配置与代码里,不在权重里;第 47 批已在 llama.cpp 的 HTTP 上跑通 (n_probs=64 的 top_logprobs → 按候选符号精确 token id 过滤 → 按温度缩放)。 也就是说这层是我们自己的代码,将来换权重/自训都不重做。

没有评估集就没有结论:三个中文单例里 0.8B 给不出可用信号、2B 方向对, 而候选模型自述 English only、我们的接缝全是中文——所以 H1(50–100 条中文样本)是闸门, 数据说不行就停在那里。

3. 已知限制(都写进文档了,不是漏做)

  • 拆解只在单人对话里发生、只由 looks_multi_issue 触发 → REQUIREMENTS.md §3.13 「已知限制」;
  • 计划面板按会话看,跨会话的历史列表在第 26 批补上了(核心行为页);
  • 仓库里带了一份被 patch 的第三方 crate(vendor/gpui-base,上游 gpui-base 0.6.1 的行内排版缺陷,见 UPSTREAM_REPORT.md)。它靠根 Cargo.toml 的 [patch.crates-io] 生效,靠 crates/rwork-integration-tests/tests/vendor_patch.rs 防丢。升级 gpui-kit 时要按 vendor/gpui-base/VENDORED.md 重新取一份并打补丁; 上游修好后整份删掉。
  • 滚动条那一处的「滚动之后」状态没有肉眼验证 → REQUIREMENTS.md §3.0: 鼠标注入(SetCursorPos + mouse_event 滚轮)在 GPUI 窗口上没生效,前后两张 截图逐像素一致;这与「栏位可拖动」那条记录的困难是同一个。请你在任意长页面上 滚一下确认:修好后滚动条始终贴在内容区右缘。

三之二、验证时踩到的几件事(下次会用到)

  • .verify/gui 里的脚本按 MainWindowHandle 找窗口,而在这台机器上那个句柄 指向的是另一个窗口(实测:真正的 RWork 窗口是 1514×844 的那个顶层窗口, MainWindowHandle 返回一个 89×31 的小窗)。后果是静默失效:act.ps1 / capture.ps1 / type.ps1 / wheel.ps1 / shot.ps1 的点击、截图、输入全落到 错窗口上,界面看着毫无反应,很容易误判成「功能坏了」。做法:EnumWindows 按 pid 枚举、挑宽度 >1200 的那个(.verify/perf/wins2.ps1、cap2.ps1、act2.ps1、 type2.ps1、wheel2.ps1 就是这么写的;坐标换算仍按「截图坐标 −(8,31)」)。 要根治就把那几个脚本的取窗方式改掉。

  • flex_1 在「内容撑高」的父元素里算出来是 0:面板/浮层的高度是内容撑出来的 (max_h 只在上限真的被顶到时才生效),而 flex_1 的 flex-basis 是 0——包一层 flex_1 等于把正文压成 0 高,里面那层 h_full 的滚动区跟着拿 0,整个面板只剩 标题(实测:三个面板连空状态那句提示都画不出来)。滚动区要自己承担高度: ui::capped_scroll(内容与上限取小);页面里那些有确定高度的容器继续用 ui::static_scroll(填满父元素)。

  • 浮层必须 occlude():gpui 判定滚动区该不该吃滚轮用的是 Hitbox::should_handle_scroll = 「这个命中框在光标下的命中列表里」,而列表里 默认同时含浮层与它底下的滚动区——一次滚轮,执行计划和对话正文一起滚。 .occlude() 把压在它后面的命中框整个排除掉(右侧面板栈与审批浮层都加了)。

  • 「把长列表拆成独立视图,让光标闪烁不再重排它」在 gpui-pre 上不成立(本批实测, 别再试):ViewElement 的复用条件里有一条 !window.refreshing,而 gpui-pre 在 任何一个视图渲染自己的子树期间都把 refreshing 置成 true——只要祖先重绘,子视图 必然跟着重绘;而光标闪烁会把「输入框 → 对话页 → 外壳 → 根」整条链标脏。拆完实测 仍是 2 帧/秒 × 4ms,与拆之前一样。想省帧只有一条路:让一帧本身变便宜 (虚拟化:约 0.2ms/块,63 块 7ms、221 块 55ms)。

  • 测帧不要用 render_frame:它会先 refresh() 一遍,把 GPUI 的「不脏的子视图 整棵复用」强制关掉(每个视图都重画),于是「漏通知」这类缺陷在测试里看不出来。 产品里的那一帧是 window.draw(cx).clear(cx)——用它才能观测到缓存行为。

  • 测试数据别带查询词:第一版把两条「弱档」样本写成 「……这一条与流程图毫无关系」——句子里有查询词,模型自然判它相关, 于是弱档一条都没出现。判据看日志那行 full= / one_line= / index_only=, 不成立先怀疑样本,再怀疑阈值。

  • 输入文案本身:type.ps1 投递 WM_CHAR 前必须先点中输入框。窗口 1514x844 上输入框在 y≈715..740(点 (600,728));y≈755 那一行是 「提示语 + 附件/发送」那条按钮带,点它会看不到任何反应(光标不在输入框里, WM_CHAR 全部丢掉)。判定点中了没有:点完 PrintWindow 抓一张,与点之前 按像素 diff,输入框里出现一条 2px 宽的白色竖线(光标)就是中了。

  • 别用 python 的 sqlite3 改验证库里的记忆:memory 表上挂着 FTS5 触发器, 改内容/删除会走 'delete' 命令;若那行还没进索引,SQLite 直接报 database disk image is malformed(这正是上面那个 bug 的现象)。 要在应用启动之前插入样本(那时 memory_fts 还不存在),让迁移自己去重建索引。 另外自己的重试循环里 sqlite3.connect 失败时要关连接,否则连接泄漏, 后面全变成 database is locked(我在这上面绕了十几分钟)。

  • 审批浮层会拦住脚本化回合:即使 approval_mode=yolo,write 整文件覆盖 已有文件时仍会弹「内容研判:较高」的确认框,回合就停在等点击——日志里是 等待用户确认 … reason=内容研判:较高,看着像「卡住了」。浮层按钮在窗口 y≈535 一行:拒绝(≈616,535)、允许一次(≈917,535)、总是允许(≈1075,535)。 点中了的判据是日志出现 用户已回答审批 tool=… allowed=true remember=false。 想绕开就让 mock 写新文件名(新文件不构成覆盖)。

  • 验证日志保留(按天方案):在 .verify/gui/logs/ 里预置 12 份 rwork.log.2020-01-01…-12(创建时间递增),然后启动 → 目录里应当只剩 10 个、 最老的 2020-01-01 被删;正在写的是 rwork.log.<今天 UTC 日期>。 清理在构造时就发生,因此启动一次即可验完。旧方案留下的 rwork.log.1….9 会在头几次启动里被自然清掉(它们最老),不必手动删。

  • PrintWindow 抓的帧是新的(内容区与屏幕直采逐像素一致,只差 ±1 的抗锯齿); 但它抓的是整窗(含标题栏与边框),而屏幕直采可能被别的窗口盖住—— 判断「界面变没变」用 PrintWindow + 像素 diff 最稳。

  • 新建对话按钮:浅色实心按钮,窗口里在 y 257..286、x 15..259(点中心 (137,271));用 -Post 投递而不是 mouse_event(后者在这台机器上偶尔点不动), 点中的判据是页面清空(像素 diff 三十万级)。

  • 不要相信视觉模型给的像素坐标(第 26 批实测):同一个窗口里它把「核心行为」 报成 y 197..232(实际 168)、把「设置」报成 y 297..330(实际 233)、 还把「清理孤儿向量」看成「新建任务」——误差 40~100px 且不同元素不等距, 照着点会点到别的控件,然后得出「功能坏了」的错误结论(我为此白点三次, 还把任务表单点开了,页面因此整体下移)。 可靠的办法是自己扫像素,三个够用的招:

    1. 找文字行:对每行数「深色像素个数」,得到一条条 y 带;再对每条带量 min/max x——墨迹拉到右边(x≈1480)的那几行就是「左边文字 + 右边按钮」的行。
    2. ASCII 图:把目标区域按亮度打成 #/+/./空格 的字符画(步长 2px), 能直接看出按钮的外框与图标位置,再挑中心点。
    3. 叠加网格:把裁剪图放大 + 画红线并标上原图坐标,再问视觉模型 「这个按钮跨哪两条线」——它读位置不准,但读「在两条线之间」还算可用。 判据始终是日志:点完看有没有那一行(这次是 已清理孤儿向量 removed=1), 没有就是没点中,别去改代码。
  • 侧栏的实际坐标(本次窗口 1514×844,文字带位置,点击用 x≈100): 对话 y≈122..134、核心行为 y≈157..169、用量审计 y≈192..204、 设置 y≈227..239、新建对话按钮 y 257..286、搜索框 y≈298..331。 注意 y=266 会撞到「新建对话」(点下去是开新对话,不是切页面)。

  • 切页不重读是一整类缺陷(第 26 批踩出来的真 bug):子页面在启动时就 全部渲染过一次,之后切过去只是换了个可见的实体——它在别处被改掉的数据 (这一轮才建出来的记忆向量表、对话里拆出的计划)不会重读。 判定办法:在别的页面做一件事,再切回来看看有没有出现。 修法是 Shell::open_page 里按页面调一次 reload,并留一条 渲染一帧 + try_find 断言的回归测试(shell::behavior_refresh_tests)。

  • 复用现成的 mock 现场:.verify/gui/ocr-test/rwork.db 里的 provider 指向 http://127.0.0.1:8801/v1(mock/mock-model),把它那份 providers + default_provider_id + default_model 拷进自己的验证库,就能立刻跑一轮对话 (起 python .verify/gui/mock.py 8801 scenario-plain.json)。 只声明 text 模态,因此不会走图片那条路。用完把 last_page 设成想看的页 (比点侧栏稳:behavior / settings)。

  • 要造孤儿向量,只能借道应用自己:memory_vec 是 vec0 虚拟表, python 的 sqlite3 没有这个扩展,连读都读不了。可行的顺序是 「python 插入记忆 → 起应用跑一轮(recall_for_turn 会建表并 backfill)→ 停应用 → python 删掉那行记忆 → 再起应用」;判据是日志 已给缺少向量的记忆补上向量 count=N。删记忆那一步走 FTS5 的 'delete', 只要这行是通过触发器(python INSERT 或应用写入)进过索引的就不会报 database disk image is malformed。

  • mock 的轮次计数器是全局的:这已经是第二次被它绊住——hub start 说 「already ready」时它没有重启,序号还停在上一次跑完的位置,于是这一轮 拿到的「拆解回答」其实是场景里的第 5 轮(一个工具调用),计划根本没被拆出来, 现象是日志里 plan=false、一个 计划: 都没有。规矩:每次跑场景前 hub stop 再 hub start,别信「already ready」。

  • 对话页输入框的位置随内容变:会话为空时输入框在 y≈634..776(点 (700,732))、 发送按钮在 (1301,765);有历史消息时会被内容区顶下去(第 25 批那次是 y≈715..740)。点之前先抓一张图,用「文字行 + 墨迹范围」找出输入框与 发送按钮的位置,别照抄旧坐标——照抄的后果是字打不进输入框(线程 WM_CHAR 全丢), 而界面看起来一切正常。

  • 诊断「回合卡住」时,日志可能不够用:这次卡在 ensure_conversation 的 db.run(...).await 返回之后、下一条日志之前, 而两边都是同步代码——因为 RWORK_LOG 那套日志每条直接 write、不丢行, 所以「缺行」本身就是证据。要给异步链路打点,用 RWORK_MARK 这种 「往文件里 append 一行」的临时标记最省事(tracing 的调用点若在 不同的线程上,输出顺序会与真实时序不符,别用它判断先后)。 另外:Rust 侧 std::sync::Mutex 死锁与「gpui 任务丢了唤醒」在现象上很像, 区分办法是点一下别的页面:能切页说明 UI 线程活着、store 锁也没被占住, 那就不是死锁而是任务停住了。

  • cargo check 报 unresolved import,而代码是好的(第 41 批用户看到的就是这个): tests/omp_fake.rs、tests/omp_session.rs 里 use rwork_core::omp_client::… 报 E0432 unresolved import,可 cargo test --workspace、cargo clippy --all-targets 全绿。真因不在代码:出错的那条链是 cargo check --workspace --all-targets (编辑器里就是 rust-analyzer 的 flycheck,诊断的 source 是 rustc),它用的是一套 独立的单元哈希——target/debug/deps/librwork_core-b672994876dd83c5.rmeta 是加 omp_client/omp_protocol 之前编出来的那份(文件创建时间还是 9-16),而 crates/rwork-core/src/lib.rs 的 mtime 是 20:51:54、比它(20:52:08)更早; cargo 判定「要不要重编」看的就是「源文件比产物新吗」,于是这一单元永远 fresh、 永远不重编,下游两个文件就一直读到没有那两个模块的旧元数据。 mtime 为什么会倒退:把改动从 worktree 带时间戳拷回主工作区(copy2 式写入)—— 内容变了、mtime 没往前走,cargo 看不见。 判别特征(三条同时成立就按这个处理,别去改代码):① cargo check --workspace --all-targets 红,而 cargo test --workspace 与 cargo clippy --all-targets 绿; ② 报错只出现在引用了新增模块的文件上;③ rust-analyzer 自己的分析是干净的 (textDocument/definition 能跳到 omp_client.rs、pull diagnostics 为 0), 只有 flycheck 那批 source: rustc 的诊断在报。 修法:cargo clean -p rwork-core 再重跑(本次清完 7.9 秒 check 全绿、两个文件的 诊断从 6 条变 0 条)。不要用 touch 源文件当长期手段——下次再从 worktree 拷回 同样的文件,mtime 还是会倒退。

  • 这台机器上「把窗口弄到前台」是做不到的(第 43 批实测):SetForegroundWindow 被前台锁挡住(返回后 GetForegroundWindow() 仍是那个控制台窗口), AttachThreadInput 那套经典绕法也没用,SendInput 合成点击标题栏同样没让它变前台。 后果是「RWork 在前台」这个状态没法从脚本里造出来——而从控制台启动的 RWork 是 window.active = true 的伪前台:gpui 的 is_active() 在 Windows 上是 hwnd == GetActiveWindow()(线程局部,不是系统前台),所以窗口一直自认为激活。 要验证「切走窗口」这条路径,别去抢前台,直接把 Windows 会发的那条消息投进去: PostMessage(hwnd, WM_ACTIVATE /*0x0006*/, MAKELONG(WA_INACTIVE /*0*/, 0), 0) ——gpui 的 handle_activate_msg 收到它就 window.active.set(false)、回调 observe_window_activation(与真实切走同一条链),投 WA_ACTIVE /*1*/ 再切回来。 这一招用 .verify/perf/cpu-activate-msg.ps1 跑(A 默认态 / B 投 INACTIVE / C 投 ACTIVE, 每段 typeperf 逐秒采样)。

  • 量「空闲时到底有没有在重绘」不要靠猜,插一个帧探针:临时在 Shell::render 顶部按视图名计数、每秒写一行 帧探针 view=… frames=… fps=…(RWORK_FRAME_PROBE=1 开关),跑完删掉并重新编译 debug 与 release(否则 target/ 里留着带探针的二进制)。 第 43 批用它证明了两件事:空闲无焦点时0 帧/秒(不是「少量重绘」,是一次都没有); 输入框有焦点时 2 帧/秒——frames 是每个视图自己的计数,能直接区分「整窗重绘」 与「只有某个子视图重绘」。配套脚本都在 .verify/perf/(cpu-launch.ps1 启动隔离实例、 cpu-activate-msg.ps1 / cpu-fg-test.ps1 驱动激活、cpusample.ps1 逐线程采样;跑之前 把 %APPDATA%\RWork\rwork.db 拷一份到 .verify/perf/cpudb/,测试实例用改名副本 启动,别碰用户那份)。

  • 冻结期间截图抓不到(第 44 批):PrintWindow 是同步投递——目标窗口的线程卡在 轮询循环里时,截图调用自己也会一起等,于是「冻结期间」拍到的那张其实是解冻之后拍的 (实测:旧写法等待期内的截图与新写法的参照截面逐像素相同,差异 0)。要验「界面当时是不是 死的」,判据只能用应用内部的时间戳:在日志里记下「等待开始 / 切页 / 等待结束」三行, 比较谁先发生。顺带一条:PostMessage 投的点击不会丢,只会被压到等待结束之后处理—— 所以「点了有反应」并不能证明界面没被睡住,要看它什么时候反应。

  • 量「一次改动把每次重绘压了多少」不要只测每秒 CPU(第 44 批):帧率本身在 1–5 帧/秒之间抖, WMI 的线程百分比又是 1% 粒度、每采一次要花几秒,噪声比信号大。可行的量法是 「一段窗口内进程吃了多少 CPU ÷ 这段窗口里重绘了多少帧」:Get-Process 的 TotalProcessorTime 差值 + 探针每秒记的帧数,两个数都是累加值,与帧率抖动无关。 配套脚本在 .verify/perf/(e5-frames.ps1 / e5-ab.ps1 / pngdiff.py / e6-clickthrough.ps1),场景文件 scenario-slow-consolidate.json(慢 4 秒的整合回答)。 别忘了测完把探针与临时开关删掉再重编,否则日志会被探针行刷屏。

  • docs 门禁抓不住「行号漂了但仍在范围内」——这一批又撞上两次:改代码时插了行, docs/03/docs/04 里的 路径:行号 共 20 处一起偏;而 i18n.rs 那批引用本来就偏了 30 多行(此前没人回头核过)。做法:git diff -U0 取每个文件的 hunk,按「引用行落在哪一段之前」 统一换算(落在改动区间里的手工改),再抽查被引用那一行是不是那个符号。

  • cargo check --workspace --all-targets / cargo clippy 报 can't find crate for ... 而 cargo test --workspace 全绿时,先怀疑产物,别去改代码(第 47 批踩到;与第 41 批那条假报错 是同一片区域的另一种表现)。现场:tower 找不到 futures_util、safetensors 找不到 serde、 proptest 找不到 rand,连我们自己的 rwork-core 也报 serde/chrono 找不到;而同一时刻 cargo test --workspace 与 cargo check -p rwork-core 都是绿的——因为 build 档的产物是好的, 坏的只是 check 档(fingerprint 说「新鲜」而产物已损坏/丢失,cargo 于是不重编,把不存在的 --extern 路径直接交给 rustc)。判据: find target/debug/deps -name '*.rmeta' -size 0 | wc -l → 这次是 74 个(都是工作区自己的目标, 失败的那次运行留下的)。 定向清理治不了:cargo clean -p <报错里找不到的那个依赖> 之后错误只会往下游挪一格 (清完 serde,rand_chacha 改报找不到 ppv_lite86)。 有效的是清掉整个 dev 档再重建:cargo clean --profile dev(本次移除 56152 个文件 / 42.2 GiB; 重建后四条门禁全绿,target/release 与分发物不受影响)。

  • page_behavior::page_tests::pruning_orphans_from_the_behavior_page_removes_them 会偶发红 (第 47 批实测到一次):并发跑整仓测试时它红在「核心行为页应当有『清理孤儿向量』这个入口」 ——那一行要等后台把孤儿数读回来才建,于是渲染一帧时它还没出现。单跑 0.09 秒必过,紧接着的整仓 重跑也全绿。遇到它先单跑确认,别顺着去改产品代码(那条用例的价值在「点了按钮真的清掉」)。

四、任务分解机制(已按这份规格实现,落地细节见 REQUIREMENTS.md §3.13)

下面这段是用户给的原始原理与落地建议,保留在这里当依据:实现时逐条对过, 唯一没有照做的是「回滚到有副作用的步骤前先问用户」的呈现方式——规格里 设想的是弹窗,实现改成「计划停下 + 在对话里问」,用户的下一句话按关键词 (plan::approves_resume,纯函数)决定继续还是作废:不引入新界面, 而「误跑一次推送收不回来」这条底线仍然守得住。

原理(用户原话拆解)

  1. 判断这个任务需要执行的具体步骤清单;
  2. 判断步骤之间的依赖关系,形成有向无环图;
  3. 按图的顺序执行;
  4. 下一个节点执行前,审计上一个节点的执行结果是否满足依赖(分解时就要把 每个节点的输入条件与输出条件写明确,审计拿它当判据);
  5. 若审计发现上游节点(注意:不一定是直接前置)有问题,回滚到对应节点 重新执行;
  6. 确认无问题后继续下一节点,直到全部完成。

落地时要先想清楚的几件事(我的建议,动手前确认)

  • 存哪:一张 plan(任务实例)+ 一张 plan_node(节点:序号、说明、输入条件、 输出条件、状态、尝试次数、产出文件)。不要塞进 run/run_step:那两个是 「一次回合跑了什么」的审计流水,而这是执行计划,语义不同。
  • 契约怎么表达:输入/输出条件必须是可判定的(能自动审计),例如 「文件 X 存在且非空」「这一步产出的 JSON 有字段 Y」;写成「质量好」这种 没法审。分解时写不出可判定的条件,就说明这一步拆得不够细。
  • 审计的判据:只审计声明的输出条件,不做「模型觉得行不行」——否则回滚 会变成随机行为。审计失败时要记清哪条条件不满足。
  • 回滚语义(最容易出错的地方):
    • 回滚目标 = 最早那个输出条件不满足的节点(用户强调过可能是隔了好几层的前置);
    • 回滚要重算受影响的子孙节点(它们的输入条件建立在那份失效产出上);
    • 必须防死循环:同一节点重试次数上限 + 总回滚次数上限;到顶就如实报告 「第 N 步反复不满足条件」,而不是无限重试;
    • 不要抹掉历史:重试是新增一次尝试记录,不是把上次覆盖掉——否则事后 无法解释「为什么它退回去了」。
  • 有副作用的步骤:回滚意味着重跑(写文件、跑脚本),要先想清幂等吗? 不幂等的步骤(例如已经推送到远端的提交)重跑会有后果,这类节点要标出来, 回滚到它之前先问用户。
  • 与现有回合循环的关系:分解器是模型的一次输出(结构化),执行器是 现有的工具循环;一个节点通常就是「一次工具调用 + 一次审计」。

五、验证怎么做(.verify/gui 里的现成工具)

# 起 mock(场景文件在 .verify/gui/scenario-*.json;重启会重置请求日志)
python .verify/gui/mock.py 8731 scenario-plain.json

# 隔离实例:改名副本 + 独立 DB(日志自动落到 DB 旁边)
copy target\debug\rwork-app.exe .verify\gui\rwork-app-test.exe
# 以 RWORK_DB=<.verify/gui/rwork.db> 启动副本;要看细节再加 RUST_LOG=rwork=debug

.verify/gui/act.ps1   -TargetPid <pid> -X <x> -Y <y>   # 真鼠标点击(收**截图坐标**)
.verify/gui/type.ps1  -TargetPid <pid> -Text "…"       # 中文输入(WM_CHAR)
.verify/gui/capture.ps1 -Out x.png -TargetPid <pid>    # 截图
.verify/gui/crop.ps1  -In x.png -Out y.png -X -Y -W -H # 裁剪
python .verify/gui/size.py x.png                       # 读**真实**尺寸

现成的场景(别每次现编):

场景文件 用途
scenario-plain.json 一轮普通回答(最常用的背景场景)
scenario-team-plan.json 第 26 批的流水线计划场景:拆两步 → 第 2 棒把第 1 棒的产出弄坏 → 退回第 1 棒 → 重做 → 图表那一步又交回第 2 棒 → 全部通过。需要一条带团队的会话(conversation.team 里放 builtin.build + builtin.report)
scenario-plan.json 执行计划:两步 + 第 2 步把第 1 步的产出清空 → 回滚 → 重做 → 完成
scenario-plan-ask.json 执行计划 + 有副作用的步骤:回滚到「推送到远端」→ 计划停下问用户 → 回「继续」→ 跑完

计划那两条场景的步骤是「先写草稿、再做图表」这类真实动作(write 写 report.md / chart.txt / pushed.txt / release.txt),因此每次跑之前 先删掉 ws/ 里的旧产出,否则条件一开始就成立、看不到推进。

铁律

  • 绝不影响用户正在运行的程序:只按 PID 操作、测试用改名副本、验证前后 stat 用户的日志目录(%APPDATA%\RWork\logs\,注意文件名带日期, 形如 rwork.log.2026-09-16)确认未变。
  • 坐标必须按截图的真实尺寸来(用 size.py 读)。曾经把错误的尺寸写进给 视觉模型的提问里,拿到一组编出来的坐标,白点好几次。
  • 别依赖 DB 里的 last_page(退出时会被覆盖),直接点侧栏: 对话 (74,120) / 核心行为 (74,168) / 用量审计 (~74,217) / 设置。
  • 核心行为页:右上「整合长期记忆」按钮 ≈ (1429, 60)。

五之一、验证本机 OCR(图片 → 文字)

需要三样:隔离的模型目录(否则 20MB 会写进程序目录旁的 .models/)、一个只声明 text 模态的模型(否则图片按图片块发,根本不走 OCR)、能看请求体的 mock。

# 1) 模型:从程序目录旁的 .models\ocr 复制一份(或让它按需下载)
mkdir .verify\gui\ocr-test\models\ocr      # 放 det\ rec\ onnxruntime.dll

# 2) 让服务指向 mock、模型只声明 text(一次性脚本,用完删)
#    照 `.verify` 里那次的写法:打开库 → load_settings → providers 换成
#    base_url=http://127.0.0.1:8799/v1、api_key=sk-test、model.input_modalities=["text"]

python .verify/gui/mock.py 8799 scenario-plain.json

# 3) 起改名副本(**绝不影响用户实例**)
.verify\gui\rwork-app-ocr.exe    # 环境变量:
#   RWORK_DB=…\.verify\gui\ocr-test\rwork.db
#   RWORK_MODELS=…\.verify\gui\ocr-test\models   ← 关键:覆盖模型根目录(OCR 住它下面的 ocr\)
#   RWORK_LOG_DIR=…\.verify\gui\ocr-test\logs

# 4) 图片走剪贴板(比文件对话框好自动化)
powershell -Command "Set-Clipboard -Path …\sample.png"
# 点「从剪贴板添加」(截图坐标 ≈ (1230,762))→ 点输入框(600,728) → type.ps1 打字
# → 点发送(≈ (1296,764))

# 5) 判据:mock 的 requests.log 里那一条请求的正文

请求体里应当出现两样东西:清单行 - 图片:① sample.png(…,用本机 OCR 识别成 文字,正文里是识别出的文字) 与 --- 附件:sample.png --- 后面的识别结果。 注意:requests.log 的路径是写死的(.verify/gui/requests.log),mock 启动 时会清空它——所以「重启再断言」这条同样适用。

没装模型时点发送走的是按需下载:先下载 5 个文件、提示「再发一次」, 再发才通。别把「第一次没发出去」当成 bug。

已经摆好的那套现场(第 25 批验证时留下的,.verify/ 不进版本库,可直接复用):

位置 是什么
.verify/gui/ocr-test/ocr-models/ 装好的 det/rec/onnxruntime(想验「按需下载」就把目录清空,或用 ocr-empty* 那种空目录)
.verify/gui/ocr-test/rwork.db 指向 mock 的库:provider=mock、模型只声明 text 模态,且 auto_consolidate=true(验自动整合直接用它;要验「关掉时不跑」就把它改回 false)
.verify/gui/scenario-auto.json 自动整合的三轮场景(两个普通回答 + 一个「整合」形状的回答)
.verify/gui/scenario-plain.json 普通一轮回答(OCR 验证用它)
.verify/gui/batch26/rwork.db 第 26 批的现场:指向 mock(8801)、workspace 设成仓库目录(这样「项目」作用域就是 RWork)、库里预置了两条不同作用域的计划与一条孤儿向量(已清)。日志与库都在这个目录里(RWORK_DB + RWORK_LOG_DIR)。那 91MB 的 exe 副本可以随时删(cp target/debug/rwork-app.exe 重建,记得在 cargo build 之后复制)
.verify/gui/batch26/*.png 第 26 批的截图:behavior3.png(核心行为页的执行计划区块)、settings5.png(设置页执行计划卡片)、orphan1/3.png(孤儿向量行 1→0)、memrows.png(行定位用的放大图)

端口:8731 被另一个 python 进程占着(不是本项目的 mock,别去杀它), 起 mock 换 8799 / 8801 之类;mock 启动时会清空 .verify/gui/requests.log, 所以「重启再断言」这条同样适用于它。

六、几条用血换来的教训

  1. 失败不能静默。记忆链路以前是哑的:读库失败 if let Ok 直接跳过、召回失败 被 unwrap_or_default 变成空列表——表现只是「没生效」。补日志后,用户那个 「整合长期记忆报错」当场定位到 SQL 列名写错(last_message_at_ms 不存在, 这功能从写出来就没成功过)。
  2. 测试必须是非空的:一条测试如果在被测代码完全没生效时也通过,它就等于没有。 精排那条我写了三版:前两版关掉精排照样通过(并列时恰好返回正确答案)。 写完要临时关掉被测代码跑一次,确认它会失败。
  3. 宽联动面的签名改动用编译器带路(改类型 → cargo check 列出全部出错点 → 逐条改)。用字符串锚点批量替换会误伤(我把库里的 quantize_binary 调用 一起换掉了,还写过一半就落盘,把树弄成不能编译)。
  4. std::sync::Mutex 不可重入:仓储里已经持有连接的路径不能再调 self.conn(), 会永远等自己且没有任何报错(实测 cargo test 卡满 20 分钟)。
  5. 改表名要一起改 FTS5 的特殊语法:INSERT INTO t(t, rowid, …) VALUES('delete', …) 里的第一个名字必须与虚拟表同名。
  6. 中文关键词:FTS5 默认分词按空格切词(中文整串当一个词)→ 用 trigram; 但 trigram 只索引三个字,两字查询(「约定」)根本搜不到 → 退到 LIKE。
  7. 不要为了省事另起一套:这个仓库里已经有长期记忆系统(memory 表 + 确认流程 + 注入),我一开始另建了 long_memory 一套,后来整套撤掉改成接进去。 动手前先搜一遍有没有现成的。
  8. 两个名字接近的访问器 = 测试陷阱。ChatPage::conversation_id()(打开了哪段 会话,跟着 conversation 走)与 current_conversation_id()(下一句话写到 哪段,优先用记住的那份 id)语义不同。我给「新建对话」写回归测试时断言了前者, 结果去掉修复照样通过——等于没测。断言要选「这条链路真正用的那个」。
  9. 给决策函数加分支时,每一条出口都要过同一道关。执行计划里「有副作用且已跑过 → 先问用户」只写在「找下一个待做节点」那条出口上,而回滚之后走的是另一条 (Running 分支)——于是最需要问一次的场景(重跑一次已推送的提交)一次都没问过。
  10. GPUI 测试里等异步要等条件,不要只 run_until_parked() 一次:它只保证当前 线程上没有可跑的任务,而库查询是跨线程往返,结果可能还没回来就断言了 (实测偶发失败过一次)。写成「循环 run_until_parked + sleep,直到条件成立或超上限」。
  11. 把「启动时判定」搬进「运行时判定」时,别忘了启动那条路自己也要判定。 日志轮转的大小检查整体挪进写入路径后,启动路径变成无条件轮转——开一次程序切 一段几百字节的碎片,开十次就把最早那段历史挤出去(而这正是要对比的那段)。 现在两条路各有一处判定,各有一条测试。
  12. 模型的输出维度是契约的一部分,错了不报错、只会整体错位。PP-OCRv6 的 rec 输出 6906 类,而它的 inference.yml 里只有 6904 项——多出来的是 CTC 的 blank 与 use_space_char 追加的那个空格。按「+1」解码会让每一个字都是错的那一个 (把「订单」读成别的),推理本身一句话都不说。所以解码入口就把 「类别数 = 字典 ±0/1」断言掉,对不上直接报错(ocr.dict_empty 那类文案同理)。 同一批里还有两个同源的约定:输入尺寸必须是 32 的倍数、通道顺序是 BGR ——写错都不报错,只让识别质量下降。
  13. 界面上的提示是状态的派生视图,状态变了就得重算。OCR 模型刚按需下载完, 而「当前模型不支持图片,因此无法发送」那行提示还挂在输入框上方——同一屏上 两句话互相打脸,用户不知道现在到底能不能发。修法是下载结束的回调里 重算一次提示。教训是通用的:任何「读一次状态、留一个字符串」的提示 都要问一句「这个状态会在什么时候变,变了之后谁负责刷新」。 另外 installed() 这类判断宁可每次现查(五次 stat)也别缓存:缓存下来的 后果是「刚下完却还说没装」。