Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
9.1 KiB
TASK · 一小时窗口后三分之一被 12 簇上限静默丢掉,真实出生时间从一开始就不在候选里;用户中途说出更窄的时间段无人接(2026-09-09)
- 基线:
origin/staging@c41afe26 - 分支:
codex/rectification-window-cluster-cap-20260909,基于origin/staging - 执行方:coding agent;验收:Claude
- 涉及文件:
scripts/rectification/candidate_contrast.py(MAX_PUBLIC_CLUSTERS、select_signature_representatives)、scripts/rectification/decision_policy.py(build_candidate_decisions候选字段)、scripts/rectification/api_service.py、frontend/src/lib/rectification-agentic/v9/inference-adapter.ts/core/build-state.ts/core/credible-range.ts(簇范围)、frontend/src/app/api/rectification/agent/route.ts(自由文本分支)、frontend/src/lib/rectification-agentic/v9/turn-intent-classifier.ts、v9/answer-choice.ts(新卡片)、v9/tool-service.ts、新迁移frontend/supabase/migrations/2026091001xxxx_rectification_set_case_window.sql、frontend/src/lib/birth-time-intake-model.ts+components/birth-time-intake.tsx(自定义范围)、skills/jyotish-birth-time-rectification/SKILL.md(不得口头承认改窗口) - BUG 编号起点:BUG-623(
docs/BUG_HISTORY.md当前最大 BUG-622) - 优先级:P0(真实用户:intake 填 14:00–15:00,本人知道是 14:45–14:50,系统收到 14:04–14:43 并继续出题;用户说出真实时段后助手口头答应却什么都没改)
1. 事故实证(2026-09-09 真实用户转录 + 本机引擎复现;不写用户资料)
| 转录 | 事实 |
|---|---|
| 第一轮证据写完范围就是 14:02–14:40 | 一小时窗口的第一次 compare 就只给出到 14:40 的候选 |
| 用户:"我的出生时间是 14 点 45 到 14 点 50" → 助手(1 步)"明白了,出生时间以你说的 14:45–14:50 为准" | 助手没有任何工具能改搜索窗口(rectification-v9-tools.ts 的 15 个工具里没有);opening brief 还明写"线索仅旁白建议,不得改搜索窗口"(agent-run.ts L256)。这是口头答应、实际未改 |
| "继续吧" → "我不太确定这句是不是在回答上面的问题" | 当前焦点是点选题,自由文本进 classifyRectificationTurnIntent,枚举里没有"申报时间段"这一意图,落 unclear |
| 范围仍 14:04–14:43 | 真实时段从头到尾不在候选集里 |
本机复现(虚构盘 1997-08-08,minute_step=1):
| 窗口 | 分钟数 | 签名簇数 | 保留簇 | 保留到的最后一分钟 | 被丢掉的簇 |
|---|---|---|---|---|---|
| 14:00–15:00 | 61 | 17 | 12 | 14:39 | 14:40–14:45、14:46–14:51、14:52–14:53、14:54–14:57、14:58–15:00 |
| 14:30–15:00 | 31 | 9 | 9 | 15:00 | 无 |
| 04:45–05:15 | 31 | 9 | 9 | 05:15 | 无 |
半小时窗口刚好压在 12 以内,所以前面十几次实测(都是 ±15)从未暴露;用户一选"前后半小时"(60 分钟窗)就必然丢后段。
2. 根因
2.1 BUG-623(P0):候选簇按时间排序后取前 12 个,多出的整簇丢弃
candidate_contrast.py::select_signature_representatives:cluster_contexts_by_signature 把簇按代表分钟时间排序,循环里 if len(representatives) >= MAX_PUBLIC_CLUSTERS: break(MAX_PUBLIC_CLUSTERS = 12)。超过 12 簇时,被丢的永远是窗口尾部。这发生在任何证据评分之前,后面所有的"范围收到 …"都只在残缺候选集内进行;旁白与时间轴把 14:04–14:43 说成收敛结果,实际上 14:40 之后从未被考虑过。
2.2 BUG-624(P1):可信区间按代表分钟的跨度算,不按簇的实际覆盖算
引擎候选只带 time(簇里分数最高的那一分钟),不带簇成员;推断层 cluster_range 是用换升时刻在代表分钟之间重新聚的,多数是单分钟;credible-range.ts 取 still-valid 候选的 cluster_range 与 time 的跨度。于是即便 14:40–14:45 这一簇活着,若代表分钟是 14:40,区间右端也只写到 14:40,簇内其余五分钟被"视觉淘汰"。
2.3 BUG-625(P1):中途说出时段时助手口头答应改窗口,但什么都没改
没有"申报时间段"意图;没有把窗口改窄的 RPC(widen_agentic_rectification_case_window 要求 v_new_width > v_old_width);模型在采集焦点下收到这句话就自己"答应"了。intake 的"家人记得大概时间"只能选 ±15 / ±30 / ±60 / ±120,知道 14:45–14:50 这种小范围的用户没有地方填(customDeclaredRange 只给 period_only)。
3. 决策记录
- 去掉按时间截断。
select_signature_representatives不再break;MAX_PUBLIC_CLUSTERS改为安全上限 64(±120 窗口 241 分钟实测簇数写进度记录);若真超过 64,按分数合并相邻低分簇,不丢尾部。公开候选数变多(一小时 ≈17)不影响四选项合同:探针的candidate_ids本来就是分钟集合。 - 候选带簇成员。
build_candidate_decisions每个候选加cluster_times: [HH:MM…](连续分钟段可写cluster_start/cluster_end);inference-adapter.ts把它作为cluster_range的来源(换升聚类只作退化兜底);credible-range.ts的区间 = still-valid 簇覆盖的并集跨度。时间轴的实心/空心点仍按候选(代表分钟)画,簇范围只影响区间带。 - 中途说出时间段:不改窗口,固定回一句,不进模型。(产品 2026-09-09 否决了"确认卡改窗口"的方案:让用户口头改范围,校正就没有意义。)
route.ts自由文本分支前置确定性解析(HH:MM–HH:MM、"14 点 45 到 14 点 50"、"14:45 左右"等),命中即持久化一条固定回复:"搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。"当前焦点保持不变(点选题继续挂着)。不新增 RPC、不新增迁移。 - 模型不得口头承认改时间。 SKILL.md §4 加一句:"用户说出出生时间或时段时,不得回答『以你说的为准』或改写搜索窗口;服务端会出确认卡。"并在
agent-run的正文守卫里把"以你说的…为准"列入机器词表删除。 - intake 自定义范围(可选,等产品答复)。 这是开始前的申报,不是中途改窗口;"家人记得大概时间"下方可加"我知道一个更小的范围"(两个时钟选择器 →
customDeclaredRange,宽度 3–241 分钟,仍是minute阶段)。产品若认为申报越窄校正越无意义,此条删除。 - 不动淘汰阈值、
SCORE_DELTA、采用/确认门、_relative_support、minute_step。本单不新增迁移(决策 3 改为固定回复后不再需要收窄 RPC)。
4. 任务分解
- 4.1 BUG-623:改
select_signature_representatives;tests/test_rectification_v5_services.py加"14:00–15:00 虚构盘 17 簇全部保留、候选含 14:46–14:51 簇的代表";用本单 §1 表格做回归夹具(虚构盘,不含真实资料)。 - 4.2 BUG-624:
cluster_times贯通到credible_range;用例——簇 14:40–14:45 存活、代表 14:40 → 区间右端 14:45。 - 4.3 BUG-625:解析器 + 固定回复;用例——点选焦点下输入"我的出生时间是 14 点 45 到 14 点 50"→ 落库固定回复、无模型调用、
candidate_range不变、焦点不变;采集焦点下同样;"14:47 左右"同样命中;不含时间的普通句子不命中。 - 4.4 决策 4:Skill 10.0.20(只加一句);
agent-voice-copy-contract收禁用短语。 - 4.5 决策 5:等产品答复后再做;做则
birth-time-intake*.test.ts三栏。 - 4.6 记录:BUG-623(P0,关联 BUG-560 校准记录:以前的"分钟级≈随机"结论是在 ±15 窗口上得出,一小时窗口还叠加了截断)、BUG-624、BUG-625;
CHANGELOG.md;PROGRESS-…;docs/testing/加"±30 窗口下候选必须覆盖整窗;中途说出时段必须出确认卡;助手不得口头答应改时间"。
5. 让步顺序
4.1 当天必做并部署(一行改动止血);4.2、4.3 必做;4.4 随 4.3;4.5 等产品;4.6 不可省。
6. 给当前这位用户的临时办法(产品可直接转告)
在资料里把出生时间改成 14:47,来源选"家人记得大概时间 · 差不多准"(±15,窗口 14:32–15:02,31 分钟、9 簇,不会触发截断),重新开始校正。或者选"有出生证或医院记录"填 14:47。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-window-cluster-cap-20260909 .worktrees/rectification-window-cluster-cap-20260909 origin/staging
cd .worktrees/rectification-window-cluster-cap-20260909
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
.venv/bin/python -m pytest tests/test_rectification_v5_services.py -q
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/birth-time-intake*.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"