Files
Jyotisha/docs/tasks/TASK-rectification-explicit-open-20261023.md
T

12 KiB
Raw Blame History

TASK · 说「帮我做生时纠正」只回一句「可在历史对话里接着做」(issue #103,2026-10-23)

基线

  • 写本单时 origin/staging 为 dd5ae8d03。开工时以当时的 origin/staging 为准。
  • 执行方:Grok(Windows + Docker),分支 codex/rectification-explicit-open-20261023,只推这个分支。
  • BUG 编号:开工时用 grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 核对,从下一个号起。写本单时的最大号是 BUG-1420。
  • 本单不需要动表(理由见决策记录 D3)。

事故实证

现象:产品负责人在 staging 对话里说「请你帮我做一下生时纠正」,只收到一句「可在历史对话里接着做。」,没有卡片、链接或按钮,校正进不去。该账户的出生时间显示「已采用」。

链路(文件、行号以开工时为准,按符号定位):

  1. 识别:模型调用 offer-birth-time-rectification 并传 explicitRequest: true(frontend/src/mastra/consultation-tools.ts)。
  2. 判定:decideRectificationOffer(src/lib/rectification-in-chat-server.ts)读取 getRectificationEntrySummary(...).hasResumableCase,再进入 decideRectificationOfferWith(src/lib/rectification-in-chat.ts),得到 case_in_progress。
  3. 「可续做」的状态集合:RESUMABLE_CASE_STATUSES(src/lib/contracts/rectification-entry.ts)与 SQL 函数 agentic_rectification_resumable_statuses()(20260812010000_agentic_rectification_v9_runtime.sql)都包含 candidate_accepted。
    • 但确认门实际上永远不开(Bug 历史:confirmation_allowed=false,「本会话以代表性时间收口」),candidate_accepted 在现实里就是终点。
    • 结果:每个采用过时间的用户都会被永久判为「有进行中的校正」。明说要校正进不去;普通对话里的校正提议卡也再不会出现。
  4. 找那次校正的会话:finishExplicitRectificationRequest(consultation-tools.ts)调用 loadResumableSessionId,实现在 src/app/api/consult/route.ts。它直接 from("agentic_rectification_cases").select("session_id") 读表。
    • 而 20260814010000_immutable_skill_registry.sql 写了 revoke all on table public.agentic_rectification_cases from service_role;,之后没有任何迁移把权限授回。20260904010000_read_report_candidate_range.sql 的注释也写明:直接读表的权限保持撤销,只能走 RPC。
    • 自托管环境下,admin 客户端以 set local role service_role 运行,所以这次查询必然报 permission denied。if (error || !data) return null 把错误吞掉,也没有日志。
    • 结果:即使是真正没做完的校正,也永远续不上,必然落到那一句话。
    • 单测没拦住的原因:测试里的 loadResumableSessionId 是注入的假函数,从来没有碰过真库。
  5. 兜底:拿到 null 后写成 kind: "refused" + RECTIFICATION_DIRECT_RESUME_ELSEWHERE_COPY。stream-agent-response.ts 只在 new / resume 时下发 rectificationDirectOpen,所以前端只剩一句话。

根因

  1. 找会话的方式错了:绕开 RPC 直接读一张对服务角色撤销了权限的表,失败又被静默吞掉。
  2. 「进行中」的口径错了:把一个实际上的终点状态(candidate_accepted)算作「进行中」,这个口径同时挡住了明说入口和提议卡。
  3. 兜底设计错了:失败时只给一句话,没有任何能操作的出口。

决策记录

# 决定 来源
D1 已采用(candidate_accepted)的校正不算「进行中」。用户明说要校正时,直接开一次新的;普通对话里的提议卡也按同一口径恢复出现 产品 2026-10-22 选 C
D2 删掉「可在历史对话里接着做」这个兜底:说了要校正,就一定打开一个能操作的校正 产品 2026-10-22
D3 只改「入口判定」的口径,不改共享的 RESUMABLE_CASE_STATUSES 和 SQL 函数。理由是那个集合还被采用后补证据、重新出卡、暂停恢复、Skill 升级(不在集合内会被当成 case_terminal)、改资料触发器(置 needs_rebaseline)、以 session 打开时的 resumed / readonly 判定共同使用;从里面删掉 candidate_accepted 会让旧会话里的采用后核对题变成只读、Skill 升级报错。按 AGENTS.md §5「修在根因上」,根因是入口口径,不是状态机。因此本单不动表 Claude 规划
D4 真正没做完的校正(draft / collecting_evidence / candidate_ready / needs_rebaseline / paused)仍然续做,但改为经 RPC 拿到会话号(现成的 getRectificationCase(accounting, userId, caseId),src/lib/rectification-agentic/v9/case-service.ts,返回值带 sessionId)。RPC 失败时打日志,并改为直接新开一次。数据库早已允许同一用户并行多个校正(BUG-177 删了唯一索引,rectification-v9-database.test.ts 已验证) Claude 规划

推翻的旧决策:

  • BUG-1306 修复段原话「进行中的 case 不新开:查到 agentic_rectification_cases.session_id 就打开那次,查不到就说可以在历史对话里接着做」:本单推翻「查不到就说去历史接着做」,并改为经 RPC 查找。
  • BUG-1306 10-12 决策日志「服务端本人 / 开关 / 进行中 case / 价格判定不变」中的「进行中 case」:口径改为不含 candidate_accepted。
  • BUG-1248 提议卡条件「无进行中 case」:沿用,但口径同上。
  • 与 BUG-177「不得用存在 resumable Case 阻止新 Case」一致,本单是在对话入口上重申这一点。

硬红线

  1. 不改 RESUMABLE_CASE_STATUSES、agentic_rectification_resumable_statuses() 及其它 SQL;不新增迁移。
  2. 非本人、开关关闭、没有价格这三种拒绝,行为不变。
  3. 扣点规则不变:开始答第一题时才扣。
  4. 日志不得含任何个人资料,只记 requestId、错误类别和 caseId 是否存在。
  5. 旧会话里已采用的那次校正,采用后核对题在新校正还没采用前必须照常能答。新校正采用了不同的时间之后,旧那次的核对题不得报错或弹出错误提示,按只读处理即可(segmentSelection 为 null 时的界面状态)。
  6. 改既有断言必须写「原值 / 新值 / 原因」三栏。按 AGENTS.md §7 第 8 条,先 grep 源码文本合同测试,同一轮里跟着改。

任务

T1 入口口径:已采用不算进行中

  • 新增一个只给入口用的判定,名称执行方定,例如 blocksNewRectification(status):命中 D4 列的五种状态返回 true,candidate_accepted 返回 false。
  • getRectificationEntrySummary 返回的 latestResumable.status 如果是 candidate_accepted,入口就视为「没有进行中的校正」。
  • decideRectificationOffer(明说入口和普通提议卡共用)改用这个判定。
  • 已知边界:summary 只返回最近活动的一次可续做校正。如果最近一次是已采用的、更早还有一次没做完的,本单按「没有进行中的」处理,直接新开。数据库允许并行,用户在历史对话里仍能找到那次。在进度记录里写明这一点。

验收

  • 单测:最近一次是 candidate_accepted → 明说要校正得到 new,提议卡可以出现;最近一次是 collecting_evidence → 得到 resume。

T2 经 RPC 找会话,删掉兜底

  • loadResumableSessionId 改为调用 getRectificationCase(accounting, userId, caseId),取 sessionId。不再直接读表。
  • finishExplicitRectificationRequest 的分支:
    • 有进行中的校正且拿到 sessionId → kind: "resume";
    • 有进行中的校正,但 RPC 失败或拿不到 sessionId → 打日志(红线 4),然后 kind: "new";
    • 没有进行中的校正 → kind: "new"。
  • 删除常量 RECTIFICATION_DIRECT_RESUME_ELSEWHERE_COPY,以及 consultation-tools.ts 里给模型的「去历史对话接着做」说明;同步删掉 frontend/docs/VOICE.md 里的对应口径。
  • 清理 case-status.ts 里已经过期的 MAX_RESUMABLE_CASES_PER_USER = 1:注释说它「镜像 DB 部分唯一索引」,但索引已被 BUG-177 删除。先 grep 引用;删除前确认没有业务逻辑依赖,测试里的断言按三栏改。

验收

  • 单测:RPC 抛错 → new,并且有一条日志调用(不含个人资料)。
  • 真库测试(test:db):用服务角色调 loadResumableSessionId 的新实现,能拿到一个进行中校正的 sessionId。旧实现在同样条件下会 permission denied,这条测试就是当初缺的防线。

T3 旧会话不报错

  • 准备一个已采用的旧校正,再开一个新校正,并让新校正采用不同的时间。然后打开旧会话:
    • 采用后核对题显示为只读;
    • 不弹错误;
    • 控制台没有未处理的异常。
  • 用交互测试或组件测试覆盖。

T4 文档

  • docs/BUG_HISTORY.md 新增一条:关联 BUG-1306、BUG-1248、BUG-177。根因写三条(读表权限、口径、兜底);防复发写 T2 的真库测试和 T1 的口径单测。
  • CHANGELOG.md 写一行用户可感知的变化。
  • docs/testing/rectification-explicit-open-20261023.md 真机清单:
    1. 已采用过时间的账户,在对话里说「帮我做生时纠正」,应直接进入新的校正;
    2. 一个答到一半的校正,从对话里说要校正,应接回那次;
    3. 普通对话问到时机类问题时,提议卡会出现(已采用的账户)。

让步顺序

  1. 先砍 T3 的自动化测试,改为写进真机清单。
  2. T1、T2、真库测试都不可让。

开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-explicit-open-20261023 .worktrees/rectification-explicit-open-20261023 origin/staging
cd .worktrees/rectification-explicit-open-20261023 && git status -sb
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1
cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint
./node_modules/.bin/tsx --test tests/rectification-explicit-entry-20261012.test.ts tests/rectification-in-chat-20261007.test.ts tests/rectification-v9-contracts.test.ts   # 记基线

验收分工

谁 跑什么
Grok 自跑 tsc --noEmit;npm run lint;上面三个测试文件加新增测试;npm run test:db,与 tools/agent-loop/windows-known-failures.txt 逐条比对;npm run build
Claude 验收时跑 前端全量 npm test,与基线逐条对比;复核没有新迁移、RESUMABLE_CASE_STATUSES 和 SQL 函数未改;复核日志不含个人资料
环境缺口 真实模型会不会在用户明说时调用工具,Claude 这边没有模型凭据,留给真机清单第 1 步

现有会受影响的断言(改时按三栏写):

  • rectification-explicit-entry-20261012.test.ts
    • 断言 RESUME_ELSEWHERE_COPY 原文(约第 129–132 行);
    • 「an in-progress case resumes that session, or says to continue in history」(约第 243 行),其中 loader 返回 null 或抛错时期望 refused;
    • 普通调用 note 匹配 /历史对话/;
    • 约第 695–698 行的源码文本合同(openRectificationFromChat(...) 恰好出现 1 次)。
  • rectification-in-chat-20261007.test.ts:hasResumableCase 为 yes 时期望 case_in_progress(约第 107–115 行),需要补「最近一次是已采用」的分支。
  • rectification-v9-contracts.test.ts:MAX_RESUMABLE_CASES_PER_USER === 1(T2 清理时)。