The answered choice persists the acknowledgement, a blank line, and the rewritten delivery. The exit fill-in required equality and wrote the deterministic delivery again. Check containment instead, and pin it on the real answer path. The suspected widen staleness regression did not hold; keep a guard test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
13 KiB
PROGRESS · 校正交付一次两段回答与点采用被拒(2026-10-06)
- 任务书:
docs/tasks/TASK-rectification-delivery-dup-adopt-20261006.md(staging65b61ed5,只改文档,未部署) - 执行分支:
codex/rectification-delivery-dup-adopt-20261006 - 基线:任务书写的产品基线是
e3f3a1fc;实现从已含任务书的65b61ed5开始 - BUG:1241、1242、1243。开工时
docs/BUG_HISTORY.md最大号是 BUG-1240,没有撞号 - 修复版本:待发布。本轮不推送,不声称已部署
T0 修前复现(先于 T1–T3)
虚构出生日期 1988-03-12。开口窗口 03:00–07:00,可信范围 04:45–05:15,并列分钟 04:53 与 05:00。1998 年搬家探针(10 岁,低于搬家下限 18 岁)。四条带日期的证据。候选集合 04:45-05:15:04:53,05:00。探针的支持/冲突写的是分钟,不是候选编号。
下面两轮诊断不算复现:第一轮内存夹具把支持写成了候选编号,开口窗口又等于可信范围,采用被「范围没变窄」挡住,三处都不可采用。第一轮 PG 回执的展示许可与选择许可对不上,能力上限关掉,旁白没有改写,两条消息是同一段确定性原文。成功的那一轮才是下面的记录。
内存矩阵
当前证据指纹 f0cbba1a89013d33aaa89a7ee39fc81caf3528e5a9e9407a3b41eddfea2f2c06。
| 入参 | session_outcome | can_adopt | 探针 |
|---|---|---|---|
| 答题:出生日期 + snapshotCurrent | adopt_representative | true | 无(低于成年下限) |
| 采用:只有指纹 | discriminate_candidates | false | relocation.1998.dasha_activation |
| 只补出生日期 | adopt_representative | true | 无 |
| 只补 snapshotCurrent | discriminate_candidates | false | 探针还在 |
| 两个都补 | adopt_representative | true | 无 |
指纹已经对上时,只补出生日期就和答题一致;只补 snapshotCurrent 不行。没有未成年探针时,缺出生日期也不改变结论。
过期指纹(64 个 b)上,答题路径若强行 snapshotCurrent: true,结论也会变。生产里的重算只在重新读出的卷宗已经对上、或重算之后再从这份卷宗算出来时才标当前,不会对着过期指纹硬写成 true。所以共享入参必须带校验过的出生日期,snapshotCurrent 由卷宗自己算,答题路径不再私自传 true。
PG17
真 PostgreSQL 17。种的是答完之后的并列卷宗,没有跑星历引擎。算法 rectification-v5-matrix-scoring-8,策略 fictional-decision-policy-v1,回执的展示/选择/确认三项与列一致且上限打开。Windows 上 db/migrations 的四个符号链接被检出成普通文件,和 supabase/migrations 重名,两目录迁移会停。这一轮把 supabase 全部加上 db 里不重名的本地身份文件放进一个临时目录,设 MIGRATIONS_DIRECTORY。这不是「两目录迁移在 Windows 上通过」。
同一份已对上指纹的卷宗:
| 路径 | session_outcome | can_adopt |
|---|---|---|
| 采用那种只传指纹 | discriminate_candidates | false |
| 答题:出生日期 + snapshotCurrent | adopt_representative | true |
| 只补出生日期 | adopt_representative | true |
采用路由在 can_adopt !== true 时返回 409 adoption_not_allowed。GET 用的也是只传指纹,和答题不一致。
助手消息修前正好 2 条:
- 采用旁白改写「这一轮已经把范围收窄,可以按代表时间看盘。」写入方是答题后的
persistV9DeterministicTurn。 - 确定性兜底,写入方是
finalizeSuccessfulTurnExit(日志rectification_delivery_turn,trigger 为该函数)。正文含「分钟范围 04:45–05:15」和「现在还剩 04:45–05:15 里 2 个候选」。改写句不是这段的子串,子串防重没挡住。
收尾里的引擎刷新失败得很快(本机没有引擎),失败之后仍然写下第 2 条。选择题 JSON 没有带 x-rectification-turn-id,所以收尾看不到已经问过的轮次,补位照写。
T2 做法:不把采用旁白函数传进收尾,也不改旁白提示词。答题回复已经记下交付原文;收尾补位在「上一条助手原文去掉空白后等于这份已记原文」时不再写。子串比对留下。没有任何回复承载时,补位仍写 1 条。
实现
共享入参在 decision-inputs.ts。出生日期读不出来就抛 birth_snapshot_unavailable,不退回缺日期的判定。GET 和接续仍回到原来的中文 503。accept 在这条失败上返回 503「暂时无法采用」。快照是否当前仍由卷宗比较指纹,答题路径不再单独传 true。
收尾补位先看子串,再看已记下的交付句是否就是上一条助手原文,然后才写。先记下的那句在 60 秒内不被后一句盖掉。选择题 JSON 在有轮次编号时带 x-rectification-turn-id。真库验收没有靠这个头:收尾函数自己比对上一条。
盘型卡和分钟卡的按钮都要「结果属于采用集合,且 can_adopt 为真」。frontend/DESIGN.md 校正状态表的候选行补了「不能采用时卡片仍在、没有按钮」。
测试缝:interview-state 和 projectTurnDecision 在调用方没传出生日期时仍只传指纹,避免改掉几十条既有单测的结论。生产的 GET、accept、答题都传校验后的日期。dossierResponse 缺日期会直接抛错。
既有断言改动:
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
rectification-v9-contracts.test.ts |
step_state: stepStateFromCaseDossier(dossier) |
step_state: stepStateFromCaseDossier(dossier, decisionInputs.birthDate) |
GET 投影与答题共用出生日期(BUG-1242) |
rectification-readopt-card-20261002.test.ts 的两处盘型卡渲染补了 canAdopt: true,断言原文没改。若干 dossierResponse 测试补了虚构日期 1900-06-15,好让成年下限不滤掉夹具里的现代探针。新夹具用 1988-03-12,没有复用事故卡片上的日期。
修后真库
仍是种好的答完并列卷宗,没有跑星历。同一份已对上指纹的卷宗:
| 路径 | session_outcome | can_adopt |
|---|---|---|
| 旧的只传指纹 | discriminate_candidates | false |
| 共享出生日期 | adopt_representative | true |
| GET | adopt_representative | true |
然后走答题后的改写落库,再调收尾且不带已问轮次编号。助手交付消息恰好 1 条,就是那句改写。日志里收尾补位被调用了,没有写出第 2 条。引擎刷新仍然很快失败(本机没有引擎),失败后也没有补写。
本机测试
tsc --noEmit:0 错。npm run lint:0 error,126 条既有 warning,没有改它们。
新文件 rectification-delivery-dup-adopt-20261006.test.ts 16 项通过。相关既有文件 122 项通过,含 BUG-1153 原测试。# cancelled 都是 0。
全量 npm test(node scripts/run-tests.mjs,退出码 1):
| tests | pass | fail | cancelled | |
|---|---|---|---|---|
基线 65b61ed5 |
4848 | 4704 | 144 | 0 |
| 本分支 | 4864 | 4719 | 145 | 0 |
失败名比基线多 1 条、少 0 条。多出来的是「发出后 300 毫秒内出现第一行阶段句」(rectification-latency-20260926.test.tsx)。这次全量跑了 414 秒,这条墙钟断言没过。同一条单独再跑通过。它不读出生快照,也不写交付轮。基线全量里没有这条名字。不记成新 bug。
其余 144 条名字与基线相同。测试总数多 16,就是新文件的 16 条,都通过。
假账本原来不返回出生快照。决策入口改成缺快照就拒绝之后,第一次全量多了 60 个失败名。给没桩过的计算 RPC 补了虚构日期 1900-06-15(成年下限不滤掉夹具里的现代探针),采用路由的工具桩补了 loadV9CaseCompute,范围句那条路由测试补了同一份快照。这三处的断言原文没改,除了 persistApplied 的源码顺序:原值查 loadV9CaseCompute,新值查 loadCaseDecisionInputs,原因是出生日期改由共享入口读取。三栏写在对应测试里。
构建
next build --webpack。Turbopack 会拒绝工作树里指到主检出的 node_modules 联接,所以用 webpack。基线 65b61ed5 编译 2.9 分钟、构建自带的 TypeScript 100 秒,都通过,收集页面数据时停在 /api/rectification/agent。本分支编译 2.2 分钟、TypeScript 83 秒,都通过,收集页面数据时停在 /api/birth-time-guide。两边都是 Windows 不允许为 Skill 运行别名建符号链接(EPERM)。先碰到哪条路由取决于收集页面的工人,不是这次改动。路由表没有打出来,这台机器没能确认 / 仍是 Static,也没有首屏 gzip 数字。没有为了构建改符号链接,也没有在工作树里重装依赖。首页 page.tsx 没改;校正卡片组件有改,gzip 要等能建符号链接的环境再量。
本轮没有打开浏览器,也没有在手机上点采用。真机步骤在 docs/testing/rectification-delivery-dup-adopt-20261006.md。修复版本待发布,没有推送,没有部署。
验收修复单(Claude 直接执行,2026-10-06)
修复单:docs/tasks/TASK-rectification-delivery-dup-adopt-fix-20261006.md。在本分支 b977f482 之上追加。Linux,Node 22.14。
F1 防重改为包含判断(BUG-1241 重开后修复)
- 事实:答题回复真实落库是「答题回执 + 空行 + 改写句」。
persistApplied在交付轮skippedNextInterview=true时拼${input.narration}\n\n${hostNarration}。首版deliveryCarrierMatchesLastAssistant要求全等,事故原样仍补第二条。 - 修:
delivery-turn-guard.ts改为text.includes(needle)(去空白,不足 8 字不匹配)。 - 新测试(均在
b977f482上红、修后绿):rectification-delivery-dup-adopt-20261006.test.ts:「已记录,范围没变。」+ 改写 → 0 条;「已记录,范围没变;…领先…落后。」+ 改写 → 0 条;只有回执 → 仍 1 条。rectification-answer-choice.test.ts「a rewritten delivery reply is not followed by a second exit fill-in turn」:走applyRectificationChoice真实答题路径并注入改写旁白,取append_agentic_rectification_turn实际写入的文本(实测为「已记录,范围收到 …」+ 空行 + 改写),再调persistExhaustionGateTurn:b977f482上写入 1 条,修后 0 条。
- 真库:本轮没有做 PG17 重放(修复单要求)。替代证据是上面这条:落库文本取自生产拼接代码,而不是手写夹具,首版恰恰是手写夹具漏掉了回执前缀。改动只是内存比较,不涉及 SQL。PG17 重放记为欠项。
F2 撤回(修复单 §2.2 判断有误,BUG-1246 不成立,不入 Bug 历史)
修复单怀疑 persistApplied 删掉 snapshotCurrent: input.snapshotCurrent 后,放宽窗口失败时会按旧窗口出卡。复核结果:
- 放宽窗口(
mutateCaseForWidenWindow)与选时段(mutateCaseForBlockChoice)进persistApplied时decisionState: null。decideAfterInferenceChange在 state 为空时candidateScores: [],一律collect_evidence,snapshotCurrent 不参与。 - 唯一带 state 的调用(stop,
snapshotCurrent: rescored.snapshotCurrent)里,这个值本来就是scoreableSnapshotCurrentFromDossier对同一卷宗算出来的。stored 与 current 只差证据指纹,revision 两边抵消,与函数内自算结果相同。 - 结论:删那一行不改变行为。保留一条守护测试「answering widen A with a failed rescore never delivers or adopts on the old window's candidates」(对照组同一份存量快照按当前读是可采用的并列交付;放宽后
can_adopt=false、不出ready_to_adopt/complete_with_range),修前修后都绿。BUG-1246 号码不使用。
本机测试(对照基线 86cafe68,同机同命令)
| tests | pass | fail | cancelled | |
|---|---|---|---|---|
基线 86cafe68 |
4936 | 4871 | 24 | 0 |
b977f482 |
4952 | 4886 | 25(多 1 条首页计时 flaky,单跑 17/17) | 0 |
| 本轮 | 4957 | 4892 | 24 | 0 |
失败名与基线逐名相同(diff 为空)。新增 21 条,没有删除。tsc --noEmit 0;npm run lint 0 error,126 warning(与基线相同)。
next build:本轮只改了服务端的 delivery-turn-guard.ts 和测试,没有重建。b977f482 的构建 Claude 已验:/ 是 ○ Static,首屏 gzip 636,730 对 636,565(+0.03%)。
既有断言:未改。
变基到 staging da226db1 后复验
staging 在这期间进了普通对话的代码(da226db1),本分支已变基到它之上。CHANGELOG、BUG_HISTORY、README 的冲突都是两边各追加,按「BUG 号升序」「两条 CHANGELOG 都留」解决。复验以 da226db1 为基线,同机同命令:
| tests | fail | cancelled | |
|---|---|---|---|
基线 da226db1 |
4941 | 24 | 0 |
| 本分支(变基后) | 4962 | 24 | 0 |
失败名逐名相同,测试名无消失,新增 21 条。tsc 0;lint 0 error。next build:/ 是 ○ Static;首屏 gzip 636,129 对 635,958(+171 B,+0.03%)。tests/test_repo_privacy_markers.py 通过。