From 4716fe46b3588d6943ec471a991afbb7de9c1d2e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 7 Sep 2026 06:14:21 +0000 Subject: [PATCH] docs(tasks): P0 stale-compare fix brief (BUG-575~578) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P --- docs/tasks/README.md | 1 + ...ectification-stale-compare-fix-20260907.md | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docs/tasks/TASK-rectification-stale-compare-fix-20260907.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 8179bc6d..b0b65ae1 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -68,6 +68,7 @@ | `TASK-rectification-unknown-time-20260906.md` | `PROGRESS-rectification-unknown-time-20260906.md` | 完全不知道出生时间的两段式路线:`stage=block_scan` 以 10 分钟步长扫 24 小时只做事件计分、出五时段四选卡(不写账本不采用),选定后进现有分钟流程;引擎加 `minute_step`;开场读 `birth_time_clue`;删 intake 劝退文案 | 已验收(带修复单:时段支持度按段长偏置,下午段先天 25%,P1) | `814c924e`;修复单 `TASK-rectification-capability-fix-20260907.md`(BUG-570) | | `TASK-rectification-capability-fix-20260907.md` | `PROGRESS-rectification-capability-fix-20260907.md` | 能力补齐修复单:答后旁白把 `range_start/range_end`(搜索窗口)当范围比较,每题都说「范围没变」;`block_scan` 五段支持度按段内原始分求和,长时段先天占优(24/24/36/30/30 个候选);`TRACK_LABEL` 音译与产品 Vimshottari/Narayana 口径不一;BLK-001 写到 `docs/BLOCKED.md` 应回根目录 | 已验收通过(P2:block_scan 15 s 壁钟断言在门禁里可能间歇红) | `517df002`(BUG-569~570);staging 未部署,需先 Migrate Staging Database | | `TASK-rectification-declared-uncertainty-20260907.md` | `PROGRESS-rectification-declared-uncertainty-20260907.md` | 出生时间「有多确定」只在 intake 问一次:三档(医院记录 / 家人大概 ±15·30·60·120 / 时段或未知),校正窗口读档案(现在有钟点一律 ±15,声明值被忽略,BUG-571);吻合率 <60% 且代表分钟贴窗口边缘时出服务端一键放宽卡,放宽后重算并写回档案(BUG-572);窗口 >120 分钟先切三子段迭代到 ≤120 再进分钟(BUG-573);日级事件问一次可靠度;四个脚本化手测场景进 docs/testing | 已验收通过(2 P3 建议:可靠度正则去掉「记得」、档案写回改走 account-profile-patch) | `8e31680b`(BUG-571~573);迁移 `20260907020000` 待应用,部署前先 Migrate Staging Database | +| `TASK-rectification-stale-compare-fix-20260907.md` | `PROGRESS-rectification-stale-compare-fix-20260907.md` | **P0**:BUG-559 把 128 字符的 `candidate_split_hash` 塞进 `asked_probe_keys`,引擎限 120 直接 400,答完任何性格/边界卡后每次候选比较都静默失败(receipt 只有 tool_failed),快照永远过期;`decideRectification` 让快照过期压过用户停止,点「先这样」后无卡、「没有拿到下一个问题」;holdout 题用过期 oos 领域被模型改写成「再说一件事」;采集轮正文双写 | 待领取(当天须合入部署) | `codex/rectification-stale-compare-fix-20260907`(BUG-575~578) | | `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 已验收 | `5483649b`(BUG-542);2 条子进程测试留 CI Node 22 复核 | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | diff --git a/docs/tasks/TASK-rectification-stale-compare-fix-20260907.md b/docs/tasks/TASK-rectification-stale-compare-fix-20260907.md new file mode 100644 index 00000000..551d3078 --- /dev/null +++ b/docs/tasks/TASK-rectification-stale-compare-fix-20260907.md @@ -0,0 +1,92 @@ +# TASK · 生时校正修复单:答完性格卡后每次候选比较都静默失败,快照过期把对话拖进死胡同(2026-09-07) + +- 基线:`origin/staging` @ `7cf7705a`(代码头 `8e31680b`,staging 已部署该版本,事故就是这版跑出来的) +- 分支:`codex/rectification-stale-compare-fix-20260907` +- 执行方:coding agent;验收:Claude +- 涉及文件:`frontend/src/lib/rectification-agentic/v9/inference-adapter.ts`、`engine-client.ts`、`frontend/src/mastra/rectification-v9-tools.ts`、`frontend/src/lib/rectification-agentic/core/rectification-decision.ts`、`v9/answer-choice.ts`、`v9/method-followup.ts`(`holdoutFollowupFor`)、`v9/agent-run.ts`、`scripts/rectification/contracts.py`、`frontend/src/lib/rectification-agentic/user-copy.ts` +- BUG 编号起点:**BUG-575**(BUG-574 已被报告列表修复占用;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核) +- 优先级:**P0**,先于 `docs/testing/rectification-scenarios-20260907.md` 的走查;不修这条,走查全部作废 + +## 1. 事故实证(staging,2026-09-07,产品负责人实测;Case JSON 已由产品负责人提供,本单只写结构,不写年份与经历) + +对话走向:开场 → 三条口述采集(学业、感情、工作,共 5 件带年月事件)→ 引擎比较成功(21.6 s)→ 两张性格卡(D9 相处方式、D10 做事风格)→ 三张年月存在卡 → 家人拒答 → 财务采集(1 件)→ 搬家采集(1 件)→ 健康拒答 → 职业口述 → 助手说"也再说一件你能记得大致时间的事吧" → 用户点「先这样,先看当前范围」→ 助手写了一段"再问下去也分不开 X 和 Y…下面是这次的结果" → **没有候选卡**,界面只有"目前范围 X–Y,还在收窄"和"没有拿到下一个问题"。 + +Case JSON 里的关键事实: + +| # | 事实 | 位置 | +| --- | --- | --- | +| 1 | `latest_result.createdAt` 停在第 3 条口述之后;财务与搬家两轮的 `rectification-compare-candidates` 都是 `status: failed, detail: {error: "tool_failed"}`,耗时 28 ms / 37 ms(不是引擎超时,是请求被拒) | `turns[].receipt.tool_activities` | +| 2 | 财务轮之后 `askedProbeKeys` 里带上了两张性格卡的 `candidate_split_hash`,形如 `04:45-05:15:<9 个分钟>:varga.d9.<分组>`,**长 128 字符**;nakshatra 边界探针的 hash 126 字符 | `inference_state.answered_probes[].candidate_split_hash` | +| 3 | 引擎 `normalize_rectification_request` 对 `asked_probe_keys` 每条限 **120 字符**,超限直接 `ValueError` → `BadRequest` → 工具抛错 | `scripts/rectification/contracts.py` L339 | +| 4 | TS 把 `probe_id`、`semantic_key`、`candidate_split_hash` 三种键都塞进 `asked_probe_keys` | `inference-adapter.ts::askedProbeKeysFromReceipt` L53–55(BUG-559 引入) | +| 5 | 于是:**任何 varga_style / nakshatra 卡答过之后,后续每一次比较都失败**;账本已有 7 件事,`latest_result` 只算了 5 件,`evidenceLedgerFingerprint` 与当前账本不等 → `snapshotCurrent=false` | `decision-from-dossier.ts::scoreableSnapshotCurrentFromDossier` | +| 6 | `decideRectification` 的 `snapshotCurrent === false` 分支排在 `userStopped` 之前:用户点了停止、`case.status=paused`,决策仍是 `collect`(`type: ask_fact_collection`、`session_outcome: collect_evidence`、`can_adopt: false`) → 卡片条件 `ADOPT_OUTCOMES` 不满足 → 无卡;`current_question: null` → 客户端显示"没有拿到下一个问题" | `rectification-decision.ts` L285–290;`rectification-surface-state.ts` L31 | +| 7 | 工具失败被 Agent 吞掉:助手正文仍写"这条记下了…对缩小候选范围很有帮助",用户不知道比较根本没跑 | `rectification-v9-tools.ts` L2073 只记 receipt 后 `throw`,agent-run 继续作答 | +| 8 | "也再说一件你能记得大致时间的事吧"来自 `oos_blind:holdout` 焦点(`kind: reverse_verify`):`holdoutFollowupFor` 从过期 `oos_blind_prompts` 里取了"财务这条线还没用过"(账本其实已有财务事件),模型把它改写成泛泛的"再说一件事" | `method-followup.ts` L1684–1694;`next_user_action.user_meaning` | +| 9 | 采集轮正文重复:第 1、2 轮助手文字各出现两段近似复述;工作采集题在正文出现一次、问题块又出现一次 | `turns[2].text`、`turns[4].text` | + +## 2. 根因 + +1. **BUG-575(P0)**:`asked_probe_keys` 把 `candidate_split_hash` 也传给引擎,varga/nakshatra 的 hash 超过引擎 120 字符上限,比较请求被 400 拒绝。BUG-559 只在 `test_rectification_event_probes.py` 用短键测过,没有用真实 split hash 测过整条 compare。 +2. **BUG-576(P1)**:比较失败没有任何用户可见后果:receipt 只有 `tool_failed`,没有错误码;Agent 继续写"记下了、很有帮助";下一轮也不重试。 +3. **BUG-577(P1)**:`decideRectification` 让"快照过期"压过"用户已停止",停止后交付不出来;`persistNextInterviewIfIdle` 的停止路径也不先重算。 +4. **BUG-578(P1)**:`holdoutFollowupFor` 只排除拒答领域,不排除账本里已有事件的领域,而且用的是可能过期的 `oos_blind_prompts`;焦点是 `reverse_verify`,题干交给模型自由改写,成了"再说一件事"。 +5. **P2**:采集轮正文双写;题干进正文。 + +## 3. 决策记录 + +1. `asked_probe_keys` 只传 `semantic_key`(去重、≤120),不传 `probe_id` 与 `candidate_split_hash`;引擎侧上限提到 200 并对超长键**跳过而不是拒绝**(记 `dropped_asked_probe_keys` 计数进 receipt)。TS 侧再加一层保险:超过 200 的键在 `engineRequestBody` 里过滤并 `console.warn`。 +2. 比较失败必须可见:receipt `detail` 带 `safe_error_code` 与引擎返回的错误文本前 120 字;agent-run 在工具失败后给正文追加服务端固定句"候选比较这次没跑成,下一句话时会自动再试";下一轮 `persistNextInterviewIfIdle` 见 `snapshotCurrent=false` 且账本有可评分事件时先重算一次(复用 `rescoreMinuteAfterWindowChange`),再决策。 +3. 用户停止优先于快照过期:`decideRectification` 把 `userStopped && ranked.length>0` 提到 `snapshotCurrent` 判断之前;停止路径(route 的 `stop_and_review` 与 `applyRectificationChoice` STOP)在决策前先尝试重算,重算失败则按上一次有效结果交付,旁白加一句"这是按上一次成功比较给出的范围"。 +4. `holdoutFollowupFor`:候选领域 = `oos_blind_prompts` 中 **既未拒答、账本里也没有该领域带年月事件** 的领域;没有则 holdout 记为 `unavailable`,直接进交付;题干用服务端固定句(`USER_COLLECT_QUESTION[domain]` 同源,不给模型改写),焦点 kind 改为 `collect_spoken` 而不是 `reverse_verify`。 +5. 正文双写:`composeCollectSpokenAssistantText` 与模型正文若都包含采集题,只保留问题块;模型正文里与 `spokenPrompt` 相同或高度相似(前 12 字相同)的句子删除;两段复述的根因要查(`phases` 里 `intent.classified` 出现两次),进度记录写明。 + +## 4. 硬红线 + +- 不动采用门、确认门、`MIN_SEPARATION_LEAD`、`_relative_support`。 +- `minute_step=1` 路径字节不变(指纹用例)。 +- BUG-559 的去重用例照常通过(去重靠 `semantic_key`,不靠 hash)。 +- 测试总数 ≥ 1899;tsc 0 错;lint 0 error。 +- 任务书 / 进度 / Bug 历史不得写入产品负责人 Case JSON 里的年份、经历、Case ID。 + +## 5. 任务分解 + +### 5.1 BUG-575 +- `askedDiscriminatorKeys` 拆成两份:`askedSemanticKeys`(给引擎)与现有全量(给 TS 去重);`engineRequestBody` 只用前者并过滤 >200;`contracts.py` 上限 200、超长跳过、计数进 `decision_receipt.dropped_asked_probe_keys`。 +- 验收:`tests/test_rectification_input_contract.py`——128 字符键不再报错、被跳过并计数;TS `rectification-v9-engine-contract.test.ts`——用真实 varga split hash(≥128 字符)构造 receipt,`engineRequestBody.asked_probe_keys` 全部 ≤120 且不含 `:varga.`;**端到端**:`rectification-probe-year-dedupe` 加一例——答过 D9 卡后 compare 请求体合法。 + +### 5.2 BUG-576 +- receipt failed detail 加 `safe_error_code` + `engine_message`(截 120 字,不含用户资料);agent-run 工具失败 → 正文追加固定句;`persistNextInterviewIfIdle` 与 `inspectNonTerminalTurnExit` 在 `snapshotCurrent=false` 时先重算一次(有节流:同一证据指纹只重试一次,记 `rescore_attempted_fingerprint`)。 +- 验收:`rectification-exhaustion-exit` 或新文件——比较抛错的回合正文含固定句;下一回合看到过期快照会调用一次 `runV9CandidateScore`,成功后决策不再是 `collect`。 + +### 5.3 BUG-577 +- `decideRectification` 顺序调整(决策 3);stop 路径先重算。 +- 验收:`rectification-decision-authority.test.ts` 三栏——`snapshotCurrent=false && userStopped && ranked>0` → `completeWithRange(user_stopped)`,`session_outcome ∈ ADOPT_OUTCOMES`;`rectification-window-widen` 或 answer-choice 测试——STOP 在过期快照上先重算再交付。 + +### 5.4 BUG-578 +- `holdoutFollowupFor` 过滤已有事件领域;无可问领域 → `holdoutValidation="unavailable"`;焦点改 `collect_spoken` + 服务端题干。 +- 验收:`rectification-choice-card` 或 `collect-direction` 测试——账本已有财务事件且家人/健康拒答 → 不出 holdout 题,决策直接交付;有可问领域时题干等于 `USER_COLLECT_QUESTION[domain]`,不是"再说一件事"。 + +### 5.5 P2 正文双写 +- 定位 `intent.classified` 双触发与正文拼接;去重规则见决策 5。 +- 验收:`rectification-spoken-collect.test.ts`——采集轮最终正文不含与 `spokenPrompt` 重复的句子;同一段复述不出现两次。 + +### 5.6 记录 +- `docs/BUG_HISTORY.md` BUG-575~578(575 标"复发自 BUG-559");`CHANGELOG.md`;`PROGRESS-rectification-stale-compare-fix-20260907.md`;`docs/testing/rectification-scenarios-20260907.md` 加第 0 条"答完性格卡后再说一件事,顶部范围必须变化或旁白说明比较结果"。 + +## 6. 让步顺序 + +5.1 单独就能解封线上,**当天必须合入并部署**;5.2 + 5.3 其次;5.4 必做;5.5 可后置到同分支第二次提交;5.6 不可省。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-stale-compare-fix-20260907 .worktrees/rectification-stale-compare-fix-20260907 origin/staging +cd .worktrees/rectification-stale-compare-fix-20260907 +ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules +ln -s /workspace/Jyotisha/.venv .venv +.venv/bin/python -c "print(len('04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:06,05:08,05:13,05:15:varga.d9.04:47|04:51/05:00|05:06|04:59|04:53/05:08|05:13|05:15'))" # 128 +.venv/bin/python -m pytest tests/test_rectification_input_contract.py tests/test_rectification_v5_services.py -q +cd frontend && ls tests/rectification-*.test.ts tests/consultation-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)" +```