5分钟策划agent #159
Reference in New Issue
Block a user
Delete Branch "feat/five_min_design"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A. reconcile_plan_gdd_approval_projections_at 挂在 resume 的第一行却用 `?` 强传播, 一次 Fast GDD 投影失败会掐掉全项目所有 Agent 的恢复;而它本身正是 receipt 投影失败 后的重试入口,掐掉它等于连兜底一起废掉。改成把 fail-closed 收敛到策划根 Supervisor 这个 run,其余 Agent 照常恢复;无法归属时才退回全局上抛。 B. main_loop 原来用 contains("approvalPending=awaiting_decision") 区分 plan_gdd blocker 的子状态,而三个 blocked 里只有一个含这个子串——尚未提交(下一步是 agent.delegate)和 receipt 锚点收尾都会掉进 else 被打成 needs-reconciliation,把最 正常的推进态当成故障停掉。改由构造方给出 PlanGddCompletionBlockerKind,消费方穷尽 match,判不出 kind 时保持 fail-closed。这两个推进态不再产生等待态,和 runtime.plan_update 一样让本轮循环继续。 映射抽成纯函数并建起 main_loop 至今没有的 mod tests:四个 kind × 两条映射全覆盖。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>M1C-1 把 userRevisionPending 同时加进了 StaticDelegateCompletionBarrier::detail() 的输出和 project_gates 的解析门,两边靠一个字段名字符串隔空对齐,中间没有共享 schema。 当时生产侧只有 delegation.rs 一条 detail().contains("userRevisionPending=1"),解析侧 一条测试都没有,两者之间也没有任何东西相连:字段名一改,生产侧那条照过,而三个门静默 返回 false,父 run 就会越过用户修订边界收束。 已核实这条路径是「单一生产者 → 四个解析点」:static_delegate_completion_blocker_at 原样使用 detail: Some(barrier.detail()),main_loop 的四处解析都以 tool == "runtime.delegate_receipts" 为前提。 新增用例锁的是等价关系而不是拼写:对七个计数的全部 128 种 0/1 组合,断言三个门的判定 与 barrier 自己的语义谓词逐一相等;另加多位数计数与「键之间互不为前缀」两条护栏——后者 是 strip_prefix 读对值的隐含前提,等价关系测试抓不到这层前提何时被打破。 鉴别力已用变异验证:把解析器的 userRevisionPending 改名为 userRevisionRequested, 或往 has_waiting() 里加一个解析器不认的计数,两个漂移方向都会被抓住。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>ensure_plan_gdd_approval_pending_after_acceptance_locked 先用会遍历并校验整条 祖先链的 read_..._run_profile_binding 读绑定,再检查 binding.source 是否为 project-supervisor-plan。顺序反了:任何祖先绑定的毛病都抢先变成 Err,连「这根本 不是策划根」都来不及说,于是别人的坏链变成了这道门的失败。 这正是 P4(cb5af31e7)在同文件 plan_root_completion_identity_at 修过的形状,当时 漏了这个姊妹函数。而它比完成门更容易被踩到:agent.run_status 每次 status observation 都会重跑本门(见 runtime_tools/run_status.rs 的 「Re-run the locked gate on every plan-root status observation」),那条路径上没有 任何上游守卫先把带父链的 run 挡掉,适用性完全交给门自己判。 - 识别改用 _once,只看 run 自己那条绑定记录;确认是策划根之后才走完整父链, 严格度一点没降,只是不再作用到别人身上 - 回归 broken_ancestor_binding_must_not_fail_the_acceptance_gate_for_a_non_plan_supervisor_run: 复用 P4 的构造(supervisor 的 isolated-join 子 run,删掉父绑定),断言门只能判 NotApplicable。变异验证:换回链走优先后该用例报出 Err("Agent Runtime Run Profile 父绑定缺失") Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>审批卡链路的三处项目锁错误改为脱敏后再进typed错误 阶段进度按等待状态显示真实轮次,不再透传0-indexed值 去掉reconcile错误的重复code拼接 补一条Rust脱敏回归与两条前端轮次回归 锁错误脱敏(方案 §18.3「返回值不包含绝对路径……或内部诊断」): acquire_project_write_lock 的 Err 内嵌 .agent/project.lock 真实绝对路径。补 redact_agent_runtime_project_paths 的三处是前端审批卡真正会显示的那条链—— reconcile_plan_gdd_approval_projections_at(hydrate 在取自己的锁之前调它)、 hydrate 自己的锁、decide_plan_gdd_at(其错误与 hydrate 的错误渲染在同一个 错误区)。planning 另有 14 个取锁点沿用未脱敏写法,属 M1B/M1C 既有模式, 本次不扩面。脱敏不破坏「项目正在被其他写操作占用:」前缀,project_gates.rs 与 provider_recovery.rs 两处按前缀分类的判据不受影响。 澄清轮次口径:clarificationRound 与 awaitingAnswerFor.round 都由 static_delegate_lineage_counters 派生,该函数排除目标自身,是 0-indexed 的 「已答轮数」;后端判上限用的是 current_round + 1。原样渲染成「轮次 X/3」 整体差一格——问最后一轮时显示「轮次 2/3」,字面暗示还剩一轮。只改前端 文案、不动 DTO 语义:等待回答时显示「第 N+1 轮 / 共 3 轮」(此时 latestDelegationId 就是当前 delivery,+1 恰好等于后端校验用的轮次), 其余状态退回「已完成 N/3 轮澄清」,不猜当前轮。 顺带:planning_hydrate.rs 里 reconcile 的错误原本用同一 code 把 to_string() 当 detail 重包一层,而 PlanningStorageError 的 Display 已是「{code}: {detail}」, 渲染出 CODE: CODE: detail;code 与 detail 均无变化,改为直接 ? 传播,并把 「不重复拼 code」钉进回归。 测试陷阱:写「占住项目锁」的 fixture 必须给锁 JSON 填真实 createdAt。失效锁 回收的年龄判定读的是该 JSON 字段而不是文件 mtime,填 0 会让锁显得约 1.7e9 秒 老、越过 600 秒阈值被当场回收删除,hydrate 反而成功。第一版 fixture 正是这样 自证失败的,注释已写明。 验证:Rust planning_ 组 155 passed / 0 failed(原 154 + 本次 1 条); appSurface.test.ts 383 passed / 0 failed(378 原有 + 5 条新增);三条新回归均经 变异验证,逆转对应修复即变红。cargo fmt --check、agc:typecheck、ESLint --max-warnings 0、check:encoding、git diff --check 通过。 撤回一条此前的审查发现:曾判定 hydrate 读 manifest 缺符号链接判定。复核后不 成立——read_manifest 自身在 is_symlink 处即拒,防护在另一层;.agent 目录本身 为符号链接的残差也无窗口,紧随其后的 resolve_planning_path 同样逐段判定。 未据此改动代码。 新记一条既有问题(非 M1D 引入):seedManifest.projectId 是常量 local-project-draft,App 的 5 个 init/import 调用点全传它,因此本机所有项目 projectId 相同。§18.3 第 1 步依赖的 projectId 校验因此分辨不出任意两个项目, 该门当前近乎恒真,须单独立项。 仍未修:hydrate 身份校验排在落盘投影修复之后(修它须注意 reconcile 自取项目 锁、.agent/project.lock 不可重入,不能把检查直接挪到 hydrate 取锁之后); design 组展示名剩两处硬编码,且与 taskGroupLabels 命名体系不同,需先定口径。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>3d8abac332toc2d5273873Provider 请求启动路径持有项目写锁时会追加 lifecycle "started",其中策划专属分支 要重新校验冻结的 planning session;而那道校验走的是不持锁版本,会自己再去抢同一 把项目写锁。持锁上下文里必然抢不到,错误又被包成 RECONCILIATION 前缀,主循环见 到该前缀直接静默返回:不写失败态、不发事件、不置 error。结果是策划子 Run 永远停 在 running/planning,父 Run 等一个永远不会来的委派回执。 打点实测整段 prelude 只花 300 毫秒,卡点就在这一行,锁本身 1 毫秒就能拿到——不是 锁竞争,是同一路径自锁。只有做方案会中招:只有 planning 请求带 session binding, 做游戏与做素材进不了这条分支。codex 与 provider 两种模式表现一致,因为这段在模式 分发之前。 按仓内既有 `_at_locked` 惯例补上持锁变体,校验体抽成私有函数供两者共用;生产里唯 一持锁调用点改用新变体。既有用例都在 append 之前就 drop 了锁,持锁形态从来没有被 覆盖过,而且断言的是 error.contains("reconciliation")——真踩了自锁也会因错误恰好 带这个前缀而"通过"。新增用例先断言不持锁版本在持锁上下文里必然失败且带该前缀,把 bug 的形状写进测试,再断言持锁版本成功并真的落了 lifecycle 记录。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>brief 只写「下一行给出严格 JSON 的单题问题」,模型据此输出裸的单题对象,还自创 了一个 answerFormat 字段: {"header":...,"prompt":...,"options":[...],"answerFormat":"回复 A、B..."} 解析器要的是 {"questions":[{id,header,question,options}]} 且 deny_unknown_fields, 于是父 run 收到 unknown field `answerFormat`, expected `questions`。此时子 Run 已经终止,信封没有修复通道,整条委派直接停在 needs-reconciliation。模型其余部分 (第N轮·关键决定、A/B/需要原型验证 三项合同)都严格照 brief 执行,说明它跟的就是 brief——两份 prompt 描述了两种形状时,模型跟更具体的那份。 brief 改为逐字给出外壳、四个允许字段、id 的 snake_case 规则和各字段字符上限,并 点名 answerFormat 这个真实踩过的坑。上限数字全部来自 schema 常量,新增用例用 format! 把两边钉在一起,任一边先改都会红;为此把相关常量放宽到 pub(crate)。 这一条只改立项策划自己的 role brief,其它 Agent 的 overlay 不受影响。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>原型系统几分钟就能跑完同一条链路且全程有澄清问答,生产链路 run 11/12/13 三次 澄清轮数都是 0。拓扑不是差异来源——两边都是 D11。差的是 Supervisor 读到的 东西:原型给它一份 1.4k 的专用提示词,明写「你不生产策划内容」和固定动作序列; 生产给它的是 13.5k 的通用总控合同,预设手里有 43 个工具、六个专业组、isolated child、正式任务图和视觉产物合同,而 plan 根一个都没有。 此前的做法是在通用合同上逐段做减法($visualContract、supervisorIntro、 isolatedTemplateCatalogIntro、$isolatedAgentTemplates 四个 `if plan_root` 分支)。这个形状本身在漏:模板目录那一段就是漏到第三轮才发现的,而每漏一段 都是一次已实测的偏航。现在整份换成 manifest 里的 supervisorPlan composition, plan 根的段落清单一眼可读,不再散落在两个函数的四个否定分支里。 保留的三段各有硬理由: - isolatedAgentContract —— expectedArtifacts 合同,agent.delegate 同样要用。 - supervisorRepair —— 返工必须用原 delegationId 重读 claimedDelegateContract 并逐字继承 acceptanceCriteria/expectedArtifacts。run 12 连撞两次的就是这条。 - planCommon —— common.md 的子集副本,见下。 替换的两段: - planSupervisorIdentity 取代 supervisorIdentityContract。后者的「用验收标准和 预期产物把边界清晰的任务委派给合适的专业 Agent」正是替用户预先裁定的压力源, 且 plan 根只有一个可委派目标;末句的黑板与 Agent 记忆在收窄后的工具面上是 死文本。 - planSupervisorPlaybook 取代 supervisorPlaybook。后者首段讲 spawn_isolated 批次与 manifest DAG、末段要求「manifest 正式任务图已完成」,在本链路都不可 执行。新段落照原型形状写死六步动作顺序,并补上生产此前完全没有的转述保真 规则:`[已确认] {header} → 用户答:{原文}` 逐条列出,任务接近长度上限时压缩 自己的说明而不是压缩用户答案。 删掉的三段: - common.md 有 43% 是 plan 根执行不了的内容(改码流程、git 提交、联网检索、 整段「作为被委派的专业 Agent 时」的身份错位),其中「最后一次修改后必须成功 执行 project.verify 才能 respond_to_user」还是一道 plan 根永远满足不了的假 门禁。但同一段压着这条链路唯一的原创性红线、user.input_request 协议和静态 委派协议——D11 的协议正文在这里,不在 playbook。所以 planCommon 逐字复制这 三段,并用 tripwire 断言反向钉住:planCommon 的每段都必须能在 common.md 里 逐字找到。 - supervisorClaimGate 讲 minIsolatedGroupsBeforeClaim,无 isolated group 时恒 不触发。 - $platform 整段是 command.start/exec/poll/stdin/terminate 的用法合同,plan 根 一个 command 工具都没有。GDD 里的平台事实由 Runtime 另行注入,与这段无关。 提示词头部改用上一笔提交的 allowlist 拼工具清单,与 Provider 请求实际广告的 函数目录共用同一份事实。两边各自维护一份就会退回「合同说有 43 个、请求里只有 9 个」的自相矛盾,而那正是收窄工具面本身要消灭的东西;这一致性单独钉了一条 断言。 supervisor/playbook.md 里的反预先裁定段落保留。执行层只单向拦住 plan 根, agent.delegate 的 agentId 是自由字符串,gui/cli Supervisor 委派 project-planning 并未被禁,那段在通用 lane 不是死文本。两份逐字相同,另有一条 tripwire 钉住。 plan 根实际拼到的提示词从 13531 字节降到 9049 字节,但重点不是体积——是被删的 全部执行不了、补上的固定动作序列此前完全缺失。 已变异验证四条新断言。全量 2238 passed / 10 failed,逐条单线程复跑后 7 条转绿 (并行噪声),剩余 3 条是本机缺 rg 与 CRLF,与本次改动无关。 尚未验证的是这些文字是否真的改变模型行为——单测只能证明它进了提示词。判据是 下一次 E2E 能否触发 AGC_NEEDS_USER_INPUT_V1 信封。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>run 18:Supervisor 冻结目标合同后一次也没委派,改成反复调 agent.run_status 去查 一个根本不存在的委派,24 轮里 58 次 agent.run_status、0 次 agent.delegate,烧到 超时。两道防线各自失效: · 空转闸门(AGENT_RUNTIME_PLAN_UPDATE_IDLE_LIMIT=4)只在裸 update_agent_plan 且 步骤没变化时累加,而任意非空 actions 都清零。于是「裸计划更新被 blocked → 调一次只读工具 → 计数归零」是一个结构上永远关不上的闸。 · Runtime 其实有一条精准提示「调用 agent.delegate 派出策划子 Agent」,但它在 plan.actions.is_empty() 分支里——模型一调 run_status 就有了动作,提示不再出现, 取而代之的是一份看起来像进展的「已读取 11 个 Agent 状态」。 本地原型没有这个问题,不是因为阈值,是因为它的 Supervisor 只有四个工具且每一个都 推进链路:「调了工具」和「推进了链路」在那边是同一件事,按前者计数就是准的。生产 把两者拆开了却还在按前者计数,工具也是全程可见。 三处改动: 1. 工具面按 durable 阶段开放。合同未冻结 → 只有 agent.goal_contract;合同已冻结 但本根 run 尚无委派 → 只有 agent.delegate;已有委派 → 取证/返工/审批那几个。 run 18 卡死的正是第二档,那一档模型连 run_status 都调不出来。阶段只按 durable 事实判定,不看 Provider 说了什么。 2. 砍掉 user.input_request。这条链路上它是死的:子 Agent 以信封退出后,Runtime 在 parent-wake 屏障处自己按信封原文构造 pending 且不恢复父 run,Supervisor 永远收不 到 needs-user-input observation。广告出去只会让它在别的时点调一次,撞上 Runtime 已装好的那份 pending 而硬失败。playbook 第 4 步与转达提问那条一并改写。 3. 空转判据改成看「本轮有没有能推进 durable 状态的动作」,只读工具不清零。纯只读 调查不受影响——不发裸计划更新就根本不会累加。这条对做游戏那条链路同样有效。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>run 23 那次 agent.acceptance_update 被拒的真实理由是 「Acceptance Graph evidence 缺少持久动作回执:action-2976e1b70fc2b12177c8f2d4」—— 而同一个 actionId 下一轮就通过了,回执也早在 9 秒前落盘。 原因是 evidence 引用是 {agentId, runId, actionId} 三元组, acceptance_evidence_tools_at 按三元组整体做 key 查回执,三者任一对不上都落空。 observation 只给了 sourceActionId 一个碎片,另外两个字段要模型自己回忆,它第一次 回忆错了。而错误信息只打印 identity.2,把「runId 写错」报成「这个 actionId 没有 回执」——模型据此得出"我的读取没落盘",排查的人也会先去查时序和落盘顺序。 两处都改: 1. file.read 的 observation summary 给出整份三元组 (sourceAgentId / sourceRunId / sourceActionId)。反正 validate_fast_gdd_evidence_identity 只接受当前根 run 的回执,合法取值唯一, 本来就不该让模型猜。 2. 缺回执与回执未成功两条错误都报全 agentId / runId / actionId。 取证指令与 playbook 第 5 步同步改成「三个字段原样照抄,不要自己回忆 agentId 或 runId」。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>plan 根 playbook 规定的回灌格式是 `[已确认] {header} → 用户答:{原文}`。 {header} 按信封契约恒为「第N轮·关键决定」,零信息量;而 project-planning 每轮都是全新 run(observations 为空),除了委派任务正文什么都看不到。 于是第 2 轮的子 Agent 拿到的是「[已确认] 第1轮·关键决定 → 用户答:类似B, 无关卡设计,可自由安排……」——B 指哪个选项它无从得知,问题原文和 A/B 选项全被丢掉了。 生产实测的农场经营项目里,第 1 轮问「季节订单冲刺 vs 自主农场成长」, 用户答 B;第 2 轮又拿「短周期经营目标 vs 沙盒里程碑成长」问同一条轴, 而且 B 选项几乎是用户原话的复述。一轮预算白烧。 回灌格式改成带问题原文和三个选项标签,并写明为什么不能省。压缩优先级 同步调整:长度吃紧时先压自己的说明文字和选项描述,问题原文与选项标签 和用户答案一样不许压。 子 Agent 侧加一条兜底:已确认决定关掉的轴不得重问,本轮问题必须落在 另一条还没关闭的轴上,全关闭时按出稿触发器③直接出稿。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>serde 的 from_str 要求整段输入就是一个值,尾部多一个字节就整包拒收。 而模型在长嵌套 JSON 字符串的尾部会退化:27 个历史 run 的 68 条真实澄清 信封里,有 2 条把信封写完整之后继续在同一个字符串里吐垃圾—— 「 马会」、「સwerhu рҭ. 北京赛车? тру. [ ]」。信封本体一个字节都没坏, 函数调用的 arguments JSON 也一次成型(repairAttempt 全为 0),却和真正 写坏的信封一样停在 needs-repair。 这不是截断:坏信封 445–520 字符,好信封 439–529 字符,分布完全重叠, 全场最长的那条解析正常。撞上 max_output_tokens 会把 arguments 本身切断, 那会留下格式修复痕迹,实测一次都没有。 改成从标记后第一个 `{` 起做括号配平扫描(跳过字符串与转义),只把配平的 那个对象喂给 serde,与原型 design_agent.py 的 parse_envelope 一致。信封是 终态协议载荷,配平对象之后不存在协议内容。长度上限随之改按切片计算: 退化尾巴既然不进解析器,也不该替一条合法信封把通道撑爆。 少写闭合符的那一类(同批 5 条,结尾 `}]}` 而非 `}]}]}`)仍然失败—— 那是模型真没写完,补括号只是替它猜一个没表达出来的形状,交给 run 内重取。 三条新测试用的是现场原样抓来的载荷:两条退化尾巴必须解析成功,一条缺 闭合符必须继续失败,外加一条正文里含括号字符的信封验证定界只认字符串外的 括号。delegation::tests 19/19,user_input 过滤器 21/21。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>[P2] 打开 GDD 前缺少项目权限门禁
该新增命令只校验绝对路径和 game/fast_gdd.md 是否存在,没有像 hydrate/decision 一样调用 enforce_project_permission_policy。由于 projectPath 完全由调用方传入,只要指定任意已初始化项目路径,就可以绕过该项目的读取策略打开 GDD。
绕过该项目的读取策略打开 GDD有什么后果?那不是用户自己打开的吗?
你说得对,这条 P2 的影响被我评估得过高了。
这个命令的实际行为是:
因此,如果是用户自己点击“打开文件”,用户本来就可以通过资源管理器打开该文件。绕过项目的 conversation.read 策略不会造成明显的越权读取或数据泄露;该策略主要约束 Agent/应用内操作,而不是限制用户本人使用操作系统。
最多存在两个较弱的问题:
当前 head
e5fa6438ca93af712f90da342b28f3730018a4f6的复审结论:REQUEST_CHANGES。需要先处理三条行内问题:
commands.rs:563-570的本地 GDD opener 没有项目读取权限门禁,也没有在 Rust 侧确认 approved/recovery 状态。planning_approval.rs:531-559丢弃 bounded Agent DB reader 的截断标记,长日志场景会把已落盘 terminal observation 误判为缺失并卡在 needs-reconciliation。GddApprovalCard.tsx:301-348和461-514的两个 dialog 没有 Escape 关闭、焦点陷阱和关闭后的焦点返回。验证状态:当前 head 与 master 无冲突,Gitea 四项 CI 已全部成功,
git diff --check通过。另:本地并行执行 planning Rust 测试时出现多项PLAN_CAPABILITY_DISABLED失败,而对应测试单独重跑通过;请一并确认测试配置读写隔离,避免 CI 依赖串行偶然性。修复后请补对应的权限/审批 contract test、Agent DB 截断恢复回归、dialog 键盘交互回归,并重新请求评审。
@@ -0,0 +536,4 @@// deterministically from the immutable receipt and must match exactly// before any surviving submit anchor may be cleaned up.let expected_summary = approval_observation(receipt).summary;let (records, _) = crate::project::read_agent_db_records_bounded(root, 16 * 1024 * 1024)?;[P1][恢复链] read_agent_db_records_bounded 返回的第二个值表示扫描是否被截断,但这里用
_丢弃了它。Agent DB 超过 16 MiB 或记录上限后只保留尾部,terminal observation 可能已经落在被截掉的前段;此函数随后返回 false,project_generic_submit_observation_locked 和 completion gate 会把已完成审批永久当成 observation 缺失,停在 needs-reconciliation。对照本文件的 plan_gdd_decision_audit_state_locked 已在 scan_truncated 时报错;请在这里同样 fail-closed 并触发完整扫描/显式恢复路径,补大日志后的收口回归。看完了。机制成立,但严重度和位置都反了 —— 真正会永久卡死的是评审拿来当正面对照的那个函数,不是被点名的这一行。
截断是掐头,不是掐尾
agent_db.rs:3183-3186 读的是文件尾窗:
记录数超限时也是
records.pop_front()—— 丢的是最旧的。而 terminal observation 是审批那一刻刚写进去的最新记录。所以 planning_approval.rs:539 要真的漏判,需要「observation 写下之后又追加了 16384 条记录」才会把它挤出窗口。而三个调用点里,743/749 就在写完 observation 的同一次操作里,2086 的收束闸也紧随其后。这条路径不是不可达,但要苛刻得多。
真正的永久楔子在 1602
评审说「对照
plan_gdd_decision_audit_state_locked已在 scan_truncated 时报错」—— 问题恰恰是它报得太早:if scan_truncated在遍历之前,无条件返回。也就是说:只要 agent.db 过了阈值,哪怕 audit 记录就好端端躺在窗口里,这个函数也一律 Err。agent.db 是纯 append JSONL,全仓没有任何 rotation/compaction,文件只增不减 —— 过线一次就再也回不来。闸门在 2175 把这个 Err 映射成 needs-reconciliation,文案是「decision audit identity 不一致」。这不是身份不一致,是扫描窗口不够,排查的人会被这条文案带偏。
顺带解释了一个现象:因为 539 那条不看截断位、observation 又在尾窗里,
terminal_observation会正常返回 true,于是闸门跳过 2165 走到 2175 才炸。评审预期的「停在 observation 缺失」根本到不了,实际停在下一道。阈值实测
本机 86 个真实项目的 agent.db,平均 ~700 B/条(最大的
abtest-tide2A-3:1061 条 / 704 KB)。按这个密度,16 MiB 装得下约 24000 条 —— 记录数上限先到,约 11 MiB / 16384 条。关键是写侧上限比读侧高 61 倍:追加会一路成功到 100 万条,读侧却从 1.6 万条起就开始截断。这个缺口不是「极端情况」,是设计上留出来的一段必然会踩进去的区间。最大的真实项目已经用掉 6.5%。
仓内既有约定站在 1602 的反面
其余每一处拿到截断位的地方,都是只在没找到时才拿它区分「确实不存在」和「可能没看见」:
if receipts.is_empty() { Err(if scan_truncated {…} else {…}) }scanTruncated写进 detail只有 1602 和 recovery_scan.rs:469(
if scan_truncated || …,同样把截断放在析取首位)是无条件炸。全仓扫描被截断这个字符串只出现 1 次,没有任何测试覆盖这条分支 —— 所以它一直没被发现。结论
评审这条按「照 1602 抄」去改的话,会把永久楔子从闸门第三道提前到第一道,更糟。正确形状是
command_ops.rs那种:先找,找到就通过,只有 miss 时才用截断位区分诊断文案。分两条记比较合适:
plan_gdd_decision_audit_state_locked无条件 Err,过 1.6 万条记录的项目永久停在 needs-reconciliation,且文案指向错误方向approval_terminal_observation_exists_locked丢弃截断位,把「没看见」当「不存在」;单独触发条件苛刻,但它是 P1 报错指向错位的直接原因底下还有一条结构性的:agent.db 无 rotation,读侧窗口比写侧上限小 61 倍。不解决这个,所有基于尾窗证明「记录不存在」的判据都有同样的时限。
</html> </html>看完了。**机制成立,但严重度和位置都反了** —— 真正会永久卡死的是评审拿来当正面对照的那个函数,不是被点名的这一行。截断是掐头,不是掐尾
[agent_db.rs:3183-3186](apps/ai-game-creator-shell/src-tauri/src/project/agent_db.rs:3183) 读的是文件尾窗:
记录数超限时也是
records.pop_front()—— 丢的是最旧的。而 terminal observation 是审批那一刻刚写进去的最新记录。所以 [planning_approval.rs:539](apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs:539) 要真的漏判,需要「observation 写下之后又追加了 16384 条记录」才会把它挤出窗口。而三个调用点里,743/749 就在写完 observation 的同一次操作里,2086 的收束闸也紧随其后。这条路径不是不可达,但要苛刻得多。
真正的永久楔子在 1602
评审说「对照
plan_gdd_decision_audit_state_locked已在 scan_truncated 时报错」—— 问题恰恰是它报得太早:if scan_truncated在遍历之前,无条件返回。也就是说:只要 agent.db 过了阈值,哪怕 audit 记录就好端端躺在窗口里,这个函数也一律 Err。agent.db 是纯 append JSONL,全仓没有任何 rotation/compaction,文件只增不减 —— 过线一次就再也回不来。闸门在 [2175](apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs:2175) 把这个 Err 映射成 needs-reconciliation,文案是「decision audit identity 不一致」。这不是身份不一致,是扫描窗口不够,排查的人会被这条文案带偏。
顺带解释了一个现象:因为 539 那条不看截断位、observation 又在尾窗里,
terminal_observation会正常返回 true,于是闸门跳过 [2165](apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs:2165) 走到 2175 才炸。评审预期的「停在 observation 缺失」根本到不了,实际停在下一道。阈值实测
AGENT_DB_MAX_BOUNDED_READ_BYTESAGENT_DB_MAX_BOUNDED_RECORDSAGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES/_MAX_SCAN_RECORDS本机 86 个真实项目的 agent.db,平均 ~700 B/条(最大的
abtest-tide2A-3:1061 条 / 704 KB)。按这个密度,16 MiB 装得下约 24000 条 —— 记录数上限先到,约 11 MiB / 16384 条。关键是写侧上限比读侧高 61 倍:追加会一路成功到 100 万条,读侧却从 1.6 万条起就开始截断。这个缺口不是「极端情况」,是设计上留出来的一段必然会踩进去的区间。最大的真实项目已经用掉 6.5%。
仓内既有约定站在 1602 的反面
其余每一处拿到截断位的地方,都是只在没找到时才拿它区分「确实不存在」和「可能没看见」:
if receipts.is_empty() { Err(if scan_truncated {…} else {…}) }scanTruncated写进 detail只有 1602 和 [recovery_scan.rs:469](apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs:469)(
if scan_truncated || …,同样把截断放在析取首位)是无条件炸。全仓扫描被截断这个字符串只出现 1 次,没有任何测试覆盖这条分支 —— 所以它一直没被发现。结论
评审这条按「照 1602 抄」去改的话,会把永久楔子从闸门第三道提前到第一道,更糟。正确形状是
command_ops.rs那种:先找,找到就通过,只有 miss 时才用截断位区分诊断文案。分两条记比较合适:
plan_gdd_decision_audit_state_locked无条件 Err,过 1.6 万条记录的项目永久停在 needs-reconciliation,且文案指向错误方向approval_terminal_observation_exists_locked丢弃截断位,把「没看见」当「不存在」;单独触发条件苛刻,但它是 P1 报错指向错位的直接原因底下还有一条结构性的:agent.db 无 rotation,读侧窗口比写侧上限小 61 倍。不解决这个,所有基于尾窗证明「记录不存在」的判据都有同样的时限。
@@ -537,0 +560,4 @@/// approval receipt re-renders it with the approved header. This command only/// hands that existing path to the shell; it never creates or rewrites it.#[tauri::command]pub(crate) fn open_local_project_plan_gdd_markdown([P1][权限与审批边界] 这个 Tauri command 已在 main.rs 注册,调用方可以绕过当前 UI 直接 invoke。这里仅校验项目目录和 game/fast_gdd.md 存在,没有执行 conversation.read 权限门禁,也没有读取权威 PlanGddState 确认 state=approved 且 recoveryPending=false;因此策略禁止读取的项目,或尚未批准/仍在恢复中的 GDD,都能被送进 OS opener。请在 Rust 侧复用 hydrate/decision 的权限门禁,并以审批 receipt/投影状态作为打开条件;补直接 invoke 的拒绝测试。
没有实际后果,不构成阻断。用户想打开那就打开,无所谓。本来就是用户本地的文件
@@ -0,0 +317,4 @@><sectionclassName="gdd-approval-card__dialog gdd-approval-card__details"role="dialog"[P2][键盘可访问性] 这里声明了 role=dialog/aria-modal,但弹窗没有 Escape 关闭、焦点陷阱,也没有关闭后的焦点返回;键盘用户打开详情或修改/退回弹窗后会把 Tab 焦点带到背景,Escape 也无法退出。仓库已有 useEscapeToClose/closeDialogOnEscape,请复用现有 dialog 约定,并补键盘打开、Escape 关闭和焦点恢复测试。