docs(tasks): rectification explicit open brief (agent loop, issue #103)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dd5ae8d032
commit
d72adb5754
@@ -37,6 +37,7 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-rectification-explicit-open-20261023.md` | — | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表 | 待执行(循环) | 分支 `codex/rectification-explicit-open-20261023` |
|
||||
| `TASK-chart-table-span-20261023.md` | — | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发 | 待执行(循环) | 分支 `codex/chart-table-span-20261023` |
|
||||
| `TASK-ui-kit-migration-20261023.md` | — | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | 待领取(循环) | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
|
||||
| `TASK-onboarding-journey-20261022.md` | — | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 起 | 待领取(循环) | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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、真库测试都不可让。
|
||||
|
||||
## 开工前置命令
|
||||
|
||||
```bash
|
||||
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 清理时)。
|
||||
Reference in New Issue
Block a user