docs(rectification): BUG-1047 records, progress, device checklist, ledger ERR-110/111

- BUG_HISTORY BUG-1047 (investigating), PROGRESS with timing estimates, A/B
  evidence, diagnostics guide, test diffs and env gaps.
- CHANGELOG (Skill version not bumped), docs/testing checklist + headless
  screenshots, tasks README row -> 待验收, BLOCKED entry.
- pre-work ledger: ERR-110 (byte-identical engine perf changes still trip the
  frozen-scoring integrity gate), ERR-111 (Shadbala fingerprint depends on
  PYTHONHASHSEED).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 11:53:27 +08:00
co-authored by Claude Opus 5.5
parent ab0f01a9cf
commit 86ff9a40d1
13 changed files with 192 additions and 1 deletions
+7
View File
@@ -1,5 +1,12 @@
# BLOCKED
## TASK-rectification-latency:真实模型耗时、登录态真机与引擎探针复用待验(2026-09-26)
- 无模型凭据与受控登录账号:进度句与开流顺序只在本地 `next start` + Chrome 151 无头、CDP 虚构接口 + 本地延迟 NDJSON 流上验过(脚本在 scratchpad,未提交)。真实模型下每段耗时、分类 10 s 上限是否触发,按 `docs/testing/rectification-latency-20260926.md` 部署后用 `RectificationRunDiagnostic` / `RectificationTurnDiagnostic` / `RectificationClassifierDiagnostic` 日志复核。
- 本机 Node v20.19.2 跑不了 `mock.module`:新路由测试 `rectification-latency-route-20260926.test.ts` 2 条在 Node 20 属既有环境缺口;已用本机 `/exec-daemon/node` v22.14.0 跑全量:基线 3981 / 24 失败(均为 Docker/DB),本单 4002 / 24 失败,名单逐条一致。无 Docker,`npm run test:db` 未跑(本单不动表)。
- D5 第一项(refresh 分支探针只算一遍)未提交:字面做法会改变输出;输出逐字节一致的替代方案(探针复用候选静态上下文里的大运表)触碰冻结评分身份 `scripts/rectification/event_probes.py`,校正验证完整性门禁 4 条失败。需产品决定是否另开单按研究协议重新冻结 sealed holdout / reported offset 记录后再合入(补丁与同机 A/B 脚本见进度记录)。
- 未部署;BUG-1047 保持 investigating。
## TASK-home-warm-return:登录态真机、Node 22 全量与部署待验(2026-09-26)
- 无受控登录账号:暖返回只在本地 `next start` + Chrome 151 无头上验过(CDP 拦截 `/api/*` 返回虚构账户与会话、所有接口人为延迟 1.5 s;脚本在 scratchpad,未提交)。iPhone Safari 真机与真实 staging 数据按 `docs/testing/home-warm-return-20260926.md` 待产品走。
+7
View File
@@ -1,5 +1,12 @@
# 印度占星 Skill 更新日志
## 2026-09-26 — 生时校正:打字回答发出后立刻有进度句,并按阶段变化(待验收)
- 打字回答一按发送,活动行就写「收到,正在对照你的档案…」,随后按服务端真实在做的事换成「正在记下这件事…」「正在重新对照盘面…」「正在准备下一个问题…」;不再一直停在「正在处理… / 正在分析…」。这几句只在等待时出现,不进回复正文和历史(BUG-1047)。
- 服务端先把回复流打开、第一行就推这句进度,再做意图分类;分类每次最多等 10 秒,超时按原来的「重试一次,再不行就请你再发一次」处理。分类模型和思考方式不变,收尾两步(下一问的写法与最后正文)不变。
- 线上诊断日志补上每一步的起止时间、推理 token(供应商给时)、分类耗时(成功也记)和每次引擎调用耗时,不含任何用户原文或出生资料;另减少每轮对引擎版本号和空闲问题的重复读取。
- Skill 版本不 bump(Skill 原文与口吻未改,只在 VOICE 增加这四句进度句)。不改数据库;真实模型下的总耗时需部署后用新日志复核。
## 2026-09-26 — 刷新后回到对话「跳到最新」不再乱出现,生时校正长回答停在开头(待验收)
- 刷新页面、用链接直接打开或一打开就落到一个已有的普通对话时:先落到最新一条;再问一个短问题,回答下面不再冒出「跳到最新」;长回答照常出现这个按钮,点了之后回答继续变长也会一直跟到最后(BUG-1043)。
+16
View File
@@ -14020,3 +14020,19 @@
- 相关记录:BUG-675、BUG-678、BUG-685、BUG-635、BUG-917、BUG-540、BUG-559、BUG-592。
- 复发自:BUG-678(兜底块「只在没有助手消息时」没有闸门);D4 同型于 BUG-685。
- 修复版本:`codex/rectification-dup-question-20260926`,未部署。
## BUG-1047 | 生时校正打字回答后要等很久才有动静:开流前无反馈、分类无超时、线上看不出时间花在哪
- 状态:investigating(D2/D3/D4 与 D5 的两项前端优化已实现并有回归测试,本地 `next start` + 无头 Chrome 虚构数据验证;未部署,真实模型下的单轮耗时需部署后用新埋点复核,不写 resolved)
- 首次发现 / 最近更新:2026-09-26 / 2026-09-26
- 影响面:`POST /api/rectification/agent`(`route.ts` 的 message 分支)、`turn-intent-classifier.ts` `classifyTurnIntentWithRetry`、`agent-run.ts` `streamAttempt` 的 `RectificationRunDiagnostic`、`turn-exit.ts` `finalizeSuccessfulTurnExit`、`engine-client.ts` `readV9EngineScoringIdentity`、校正面 `rectification-agentic-chat.tsx` 的 live 行。
- 用户现象:产品 09-26 staging 真机:打字回答后,活动区一直是「正在分析 / 正在处理…」,要等很久(估计 25–90 s)才出正文,期间没有任何变化。
- 触发条件:`action=message`。一轮串行 5 次开思考的模型调用:开流前的意图分类(`classifyTurnIntentWithRetry`,每次尝试无超时、最多 2 次;discriminator 分支之后还会再分类一次)+ Agent 四步。分类在 `new ReadableStream` 之前,所以分类期间浏览器收不到任何字节。
- 根因:串行多步 + 每步深度思考 + 开流前无超时空等 + 无进度反馈;埋点里 `inputTokens` / `reasoningTokens` 恒为 null、`stepCount` 实为"用过几种工具"、分类耗时只在失败时记录,线上无法核实各段耗时。收尾 `persistNextInterviewIfIdle` 在 `agent-run.ts` 与路由出口各调一次;`/v5/versions` 每轮被读多次。不是回归:09-15 性能审计(BUG-721~726)处理的是引擎与档案读取,没有覆盖模型链路与开流顺序。
- 修复(产品 09-26 决策 D1–D5):D1 收尾两步(set-focus 改写与最终正文)不动。D2 分类保持会话模型与思考,只给每次尝试加 10 s 上限(`RECTIFICATION_CLASSIFIER_ATTEMPT_TIMEOUT_MS`),超时走既有重试 → `classifier_unavailable` 路径。D3 message 先建流,第一行推 `turn.progress received`(「收到,正在对照你的档案…」,客户端发送同一帧就显示),分类与确定性回复都在流内;写经历 →「正在记下这件事…」、引擎重算 →「正在重新对照盘面…」、经历写完 / 定下一问 →「正在准备下一个问题…」,由既有工具事件与引擎调用推动(AsyncLocalStorage 回合作用域),只往前走;进度句只在 live 行,不进 `assistant_message`、不进 phase 回执。流内的预检拒绝改为 `turn.rejected`(原状态码、code、文案),客户端按原非 2xx 处理(撤回打字标记、402 / 资料不完整分支不变)。D4 `RectificationRunDiagnostic` 增加每步起止、供应商给出的 token(含推理)、分类耗时(成功也记,另有 `RectificationClassifierDiagnostic`)、每次引擎调用路径 / 耗时 / 结果;确定性回复另记 `RectificationTurnDiagnostic`;均不含用户原文、出生资料或模型文本。D5 `/v5/versions` 进程内 30 s 记忆(只存完整身份、按 fetch 实现 + 引擎地址分域);Agent 自己的收尾调用已发现下一题处于 active 时,路由出口跳过第二次 `persistNextInterviewIfIdle`(第二次只会重读同一焦点并重复同一幂等链接)。
- 未做:D5 第一项「refresh 分支探针只算一遍」。两遍用的候选集不同(refresh 列 / 全网格),合成一遍会改变 `candidate_contrast_opportunities` 输出,违反"逐位不变";改为复用候选静态上下文里已算好的 Vimshottari / Narayana 表的替代方案同机 A/B 7 个场景逐字节一致、refresh 61 候选 2.33 → 1.84 s,但 `event_probes.py` 属于冻结评分身份(`scripts/research/sealed_holdout_rerun.py` `PRODUCTION_FILES`),改动会让校正验证完整性门禁 4 条失败,需按研究协议重新冻结 sealed holdout / reported offset 记录,本单未提交该改动(补丁与 A/B 脚本在进度记录列明)。
- 验证:`frontend/tests/rectification-latency-20260926.test.ts`(17 条:挂起分类 10 s 中止、只重试一次、结果 `classifier_unavailable`;首试超时次试成功;成功耗时日志不含原文;阶段只前进、`ReadableStream` start 继承作用域;`turn.progress` / `turn.rejected` 过白名单;工具事件映射;Agent 一轮的阶段序列、完整诊断、进度句不落库;`/v5/versions` TTL 内一次、失败与残缺不记忆;第二次空闲调用是纯重复、出口跳过后写操作集合不变)、`rectification-latency-20260926.test.tsx`(真实组件:发送后服务端零字节时已显示第一句、按 `turn.progress` 换句、工具名与「正在分析…」不再顶替、结算后无进度句;`turn.rejected` 与旧 HTTP 拒绝同效)、`rectification-latency-route-20260926.test.ts`(真实路由 + 模块替身:分类未返回时首字节即 `turn.progress`,8 ms;回复在出口门之后;预检 409 走 `turn.rejected`;Node 22 通过,Node 20 为既有 `mock.module` 环境缺口)。浏览器(`next start` + Chrome 151,CDP 虚构接口 + 本地延迟流):第一句在回车后 13–17 ms 可见,阶段切换与服务端事件同步(±35 ms),全程未出现「正在处理… / 正在分析…」live 行,结算后无进度句;截图 `docs/testing/rectification-latency-20260926/`。
- 防复发:会产生模型或引擎等待的请求必须先建流、首字节是确定性进度;开流前只做鉴权与绑定校验。任何模型调用都要有单次上限。耗时埋点必须覆盖分类、每步与引擎调用,且不得含用户原文。改冻结评分文件(`PRODUCTION_FILES` 与 dataset `frozen_scoring.files`)的性能优化,即便输出不变,也要把重新冻结验证记录列入任务书。
- 相关记录:BUG-721、BUG-722、BUG-723、BUG-724、BUG-725、BUG-726、BUG-388、BUG-1046。
- 复发自:无。
- 修复版本:`codex/rectification-latency-20260926`,未部署。
+17
View File
@@ -2,6 +2,11 @@
Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility.
## 2026-09-26 · 校正时延单预检(BUG-1047)
- `python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 在 Linux / Python 3.13 上整体 exit 0:远端 verified,碎片扫描、外部引擎适配器诊断与 5 个 focused 目标全部通过(本机没有复现 09-24/09-25 记录的 `.workbuddy` 镜像断言失败)。
- 新发现两条,记为 ERR-110 / ERR-111。
## 2026-09-25 · 三份修复单最终验证环境保真
- Windows普通文件形式的migration symlink导致duplicate filename;被错误设为可执行的Skill文件改变hash framing。改用保留Git mode/symlink的archive,不能改迁移duplicate detector或registry hash迎合污染环境。
@@ -402,3 +407,15 @@ Prevention: 前台同步路径不得进入无上限排队,超预算即 fail-fa
本仓引擎同步停在 `f2241463`(2026-09-03)。上游 `f3b5196f`(2026-09-09)才引入 `reader_main`,`origin/main` 在 2026-09-25 fetch 后是 `23b9609e`,不是任务书写的 `19195f6e`。漏掉的还有 `calc_solar_return_chart` 返回值里的 `birth_asc_sign_idx`(上游 solar_return 成功字典会写这个键)。网页全读用 `type('Args', (), fields)()`,上游同样用 `SimpleNamespace(**vars(args))`,实例字典为空时 KP / transit 读不到 `year`;上游空结果不写进读者正文,所以这个缺陷在上游也潜伏。
Prevention: 引擎/上游同步类任务必须 fetch 后核对 `origin/main`,以实际 SHA 为准,并在进度和本台账写明与任务书 tip 的差异。读者版整段引入要带来源提交。不要把 `.workbuddy` 当主仓。同步太阳返照时核对返回键,而不只核对函数还在。关联 BUG-1026、BUG-1027、BUG-1028。
## ERR-110 | 输出逐字节不变的引擎性能改动也会打红校正验证完整性门禁 | observed 2026-09-26
`scripts/rectification/event_probes.py`、`refinement_packet.py` 等 10 个文件在 `scripts/research/sealed_holdout_rerun.py` `PRODUCTION_FILES` 里,另有 dataset `frozen_scoring.files` 12 个遗留文件;快速门的 `tests/test_rectification_validation_integrity_gate.py` 按文件字节哈希核对冻结记录。BUG-1047 的探针大运表复用(同机 A/B 7 个场景逐字节一致,refresh 61 候选 2.33 → 1.84 s)只改了 `event_probes.py`,就让该门禁 4 条失败。
Prevention: 引擎/打分路径的性能任务书开工前先查这两份清单;动到其中任何文件,即使输出不变,也要把「按研究协议重新冻结 sealed holdout / reported offset 记录」写进任务分解,否则不要合入。同机 A/B 是必要证据,但不能代替重新冻结。关联 BUG-1047、BUG-985。
## ERR-111 | Shadbala 特征指纹随 PYTHONHASHSEED 变化,同一输入跨进程 `feature_hash` 不同 | observed 2026-09-26
同一台机器、同一份代码、同一虚构请求连跑两次 `score_candidates`:`candidate_feature_snapshot.features[*].fingerprints.shadbala` / `static` 与 `feature_hash` 每次都不同;固定 `PYTHONHASHSEED=0` 后两次逐字节一致。说明 Shadbala 结果里有依赖集合 / 哈希顺序的结构进入了指纹。打分分数本身未见变化(未系统核对)。
Prevention: 跨进程比较引擎输出(A/B、golden、缓存键)时固定 `PYTHONHASHSEED`,或排除这三类指纹;若有缓存或复用判断依赖 `feature_hash`,需另开单核对是否因此每次都判为变化。本条只记观察,未修。关联 BUG-985、BUG-733。
@@ -0,0 +1,118 @@
# PROGRESS · 生时校正每轮等待过长:埋点 + 分类超时 + 发出后立即反馈(2026-09-26)
- 任务书:`TASK-rectification-latency-20260926.md`
- 执行:Claude 子代理(直接执行模式,产品授权),分支 `codex/rectification-latency-20260926`,worktree `.worktrees/rectification-latency-20260926`
- 基线:开工 `origin/staging` `f1d16405`(已含 BUG-1045/1046,任务书要求的串行前置已满足);提交前变基到 `fe76d547`(仅 `docs/BUG_HISTORY.md` / `docs/tasks/README.md` 两处文档差异)。本单未推送。
- BUG:BUG-1047(开工时最大号 BUG-1046,核对无冲突),状态 investigating(未部署、真实模型耗时未复核)。
- 开工预检(AGENTS §9):`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` exit 0,远端 verified、适配器与碎片扫描、5 个 focused 目标全过;新发现 ERR-110 / ERR-111 已写入 `docs/research/pre_work_error_ledger.md`。
## 结论
| 决策 | 结果 |
|---|---|
| D1 收尾两步不动 | 未改 Agent 步数、set-focus、最终正文、Skill 文本与各步 thinking |
| D2 分类加 10 s 超时 | 已做。每次尝试 10 s,超时走既有重试 → `classifier_unavailable`;模型与思考不变 |
| D3 发出后立即反馈 | 已做。客户端发送同一帧显示第一句;服务端先建流、首行 `turn.progress`;四句阶段进度;不落库 |
| D4 补埋点 | 已做。每步起止、推理 token、分类耗时(成功也记)、每次引擎调用耗时 |
| D5 ① refresh 探针只算一遍 | **未提交**(见「偏离」1):字面做法改变输出;输出一致的替代方案 A/B 逐字节一致,但触碰冻结评分身份 |
| D5 ② 第二次 `persistNextInterviewIfIdle` 跳过 | 已做(只在第一次命中「下一题已 active」时跳过,可证明等价) |
| D5 ③ `/v5/versions` 进程内缓存 | 已做(30 s,只存完整身份) |
## 做了什么
| 项 | 实现 | 位置 |
|---|---|---|
| T1 / D4 埋点 | `RectificationRunDiagnostic` 新增 `steps[]`(每步 `startMs`/`endMs`/token/公开工具名,相对 attempt 起点)、`inputTokens`/`outputTokens`/`reasoningTokens`(各步供应商用量之和,未报告为 null)、`stepCount` 改为真实模型步数(原为"用过几种工具",另留 `distinctToolCount`)、`toolCallCount`(含重复)、`attemptNumber`/`attemptStartMs`/`attemptElapsedMs`、`classifier`、`engineCalls[]`。分类每次都打 `RectificationClassifierDiagnostic`(成功也打);确定性回复轮打 `RectificationTurnDiagnostic`。引擎耗时由 `turn-instrumentation.ts` 的 AsyncLocalStorage 回合作用域收集,只记路径、毫秒、结果档(ok/busy/http_error/timeout/bad_payload) | `v9/run-diagnostic.ts`、`v9/agent-run.ts`、`v9/turn-instrumentation.ts`、`v9/engine-client.ts`、`route.ts` |
| T2 / D2 分类超时 | `classifyWithinTimeout`:每次尝试独立 `AbortController` + 10 s 计时,`Promise.race` 保证供应商不理会 signal 也会结束;超时计入 `timedOutAttempts`。仍是 `for (attempt < 2)`、仍是会话模型、未加任何 thinking/providerOptions | `v9/turn-intent-classifier.ts` |
| T2 / D3 分类移入流内 | message 分支:原 IIFE 改为 `computeImmediateResponse`,`action === "message"` 时不在开流前执行;`new ReadableStream` 在 `instrumentation.run()` 内构建,`start()` 第一件事 `advance("received")`,再跑预检(分类 + 确定性回复)与无焦点分类。预检返回的 200 回复先过同一个 `awaitTurnExitBeforeResponse(finalizeSuccessfulTurnExit)` 再逐行推给流;非 2xx 变成 `turn.rejected`(原状态码、code、文案)。点选题、开场、只读续轮的请求/响应形态不变 | `route.ts` |
| T3 / D3 阶段进度句 | 服务端:`record-evidence-batch` 等写入工具开始 → recording;`/v5/score` / `/v5/block_scan` 引擎调用开始或 compare 工具 → rescoring;写入工具完成、set-focus / 出口门 / Agent 收尾空闲调用 → preparing_question;只前进,重试 attempt 重置后重推 received。`turn.progress` / `turn.rejected` 进公开白名单。客户端:发送即「收到,正在对照你的档案…」;`turn.progress` 换句并改写正在进行的工具步骤;工具开始 / 完成 / `activity.changed` 不再用工具名或「正在分析…」顶替阶段句;结算时 live 行与 activity 一并清掉。时间线适配器把四句识别为进度句 | `v9/stream-mapping.ts`、`v9/turn-progress.ts`、`rectification-agentic-chat.tsx`、`rectification-activity-labels.ts`、`rectification-timeline-adapter.ts`、`rectification-surface-state.ts` |
| VOICE / DESIGN | VOICE 新节「生时校正打字回答的阶段进度句」+ 好/坏对照一行;DESIGN 新节 + 状态表 `typed-pending` 行 + §9 一句 | `frontend/docs/VOICE.md`、`frontend/DESIGN.md` |
| T4 / D5 | `/v5/versions` 记忆:`WeakMap<fetch 实现, Map<引擎地址, {identity, expiresAt}>>`,TTL 30 s,只存 algorithm+policy 两项齐全的身份,失败与残缺不存,返回副本。第二次空闲调用:`persistNextInterviewIfIdle` 在「焦点已 active」出口返回 `focusActive: true`;Agent 收尾调用命中它时 `interviewSettled = true`,路由出口 `finalizeSuccessfulTurnExit({ interviewSettled })` 直接进入 `ensureNonTerminalTurnExit` | `v9/engine-client.ts`、`v9/answer-choice.ts`、`v9/agent-run.ts`、`v9/turn-exit.ts` |
| T5 记录 | BUG-1047、CHANGELOG、真机清单、状态板、BLOCKED、错误台账 ERR-110/111 | `docs/` |
## 偏离与说明
1. **D5 ① 未提交。** refresh 分支两遍探针用的候选集不同:第一遍是 refresh 列(出 `discriminating_event_probes`),第二遍是全网格(出 `candidate_contrast_opportunities`),按网格截然不同的代表盘各算各的(实测两遍 `_score_year` 键零重合,950 vs 13345 次)。合成一遍必然改变第二项输出,违反"逐位不变"。热点其实在每个探针格子里重建 Vimshottari 时间线与 Narayana 大运表(`_score_year` 调 `_active_*` 时不传表)。输出一致的替代方案是复用候选静态上下文里已按同样输入算好的两张表(Vimshottari 只在候选日期等于 `birth_date` 且月亮经度相同时复用,跨午夜候选照旧重建)。同机 A/B(`PYTHONHASHSEED=0`,基线 = `origin/staging` scripts 原样导出,新 = 补丁后,逐字节 `cmp`):
| 场景(1990-01-01 北京公开烟测盘 + 6 条虚构事件) | 基线最快 s | 新最快 s | 输出 |
|---|---:|---:|---|
| 05:00–06:00(61 候选)默认 | 1.332 | 1.263 | 逐字节一致 |
| 同上 + refresh 10 列 | 2.329 | 1.837 | 逐字节一致 |
| 05:10–05:30(21 候选)默认 | 0.366 | 0.334 | 逐字节一致 |
| 同上 + refresh 4 列 | 0.764 | 0.624 | 逐字节一致 |
| 61 候选 3 条事件 | 1.063 | 1.044 | 逐字节一致 |
| 23:40–00:20 跨午夜 | 0.808 | 0.801 | 逐字节一致 |
| 跨午夜 + refresh 5 列 | 2.332 | 1.883 | 逐字节一致 |
另写了同进程 A/B pytest(两臂各跑一次 `score_candidates`、整份输出规范化 JSON 相等;突变校验:把复用的大运表改坏后输出确实变化,说明测试敏感)。**但** `event_probes.py` 在冻结评分身份 `PRODUCTION_FILES` 里,快速门的 `test_rectification_validation_integrity_gate.py` 4 条随即失败(按文件字节哈希核对 sealed holdout / reported offset 冻结记录)。重新冻结是研究协议动作,不在本单授权内,所以补丁未提交:`scratchpad/lat2/d5-python-static-tables.patch`(含测试),A/B 脚本 `scratchpad/lat2/ab.sh` + `bench.py`。建议另开单:合入补丁 + 按协议重新冻结记录。
2. **stepCount 语义修正**:原字段实为 `toolsUsed.size`,本单改为真实模型步数,旧含义留在 `distinctToolCount`。`mapModelFinishToErrorCode` 的输入仍用 `toolsUsed.size`(业务判断不动)。
3. **「正在分析」时间线标题仍在**:时间线折叠标题 live 时写「正在分析」(与普通对话共用的 `ConsultationRunTimeline` 摘要),本单没动;变化的是下面那一行 live 步骤(截图可见)。
4. **开场轮也会带 `turn.progress preparing_question`**:Agent 收尾空闲调用处统一上报;开场 live 行在收尾时显示「正在准备下一个问题…」,语义属实,未单独屏蔽。
5. **阶段信号来源**:除工具事件外,「正在重新对照盘面…」也由引擎打分调用本身触发(`record-evidence-batch` 在工具内部重算,没有独立工具事件);不新增模型调用。
6. **跳过第二次空闲调用的条件收窄**:只在第一次落在「焦点已 active」出口时跳过。其它出口(新写焦点、交付收尾、穷尽门)第二次调用可能读到第一次的写入而走不同分支,无法证明等价,保持原样。
7. **路由拒绝的形态**:开流前的鉴权、绑定、终态、声明时段拦截仍是原 HTTP 状态码;只有原先发生在分类/确定性回复里的拒绝改走 `turn.rejected`,客户端复用同一个 `rejectTurn`(撤回打字标记、402 / 401 / 资料不完整分支与原来一致)。
## 各段耗时估计(改前 / 改后)
| 段 | 改前 | 改后 | 依据 |
|---|---|---|---|
| 发送 → 屏幕上第一句确定性文字 | 立刻出现通用「正在处理…」,之后长时间不变 | 13–17 ms 出「收到,正在对照你的档案…」 | 无头 Chrome 实测(rAF 轮询可见文本) |
| 发送 → 服务端第一个字节 | 分类结束后(分类开思考,典型数秒,无上限) | 鉴权 + Case/会话读取后立即(路由测试 8 ms,替身环境) | `rectification-latency-route-20260926.test.ts` |
| 意图分类 | 每次无上限,最多 2 次;discriminator 分支再分类 1 次 | 每次 ≤ 10 s,最坏 20 s 后走既有兜底 | 单元测试(模拟计时器) |
| Agent 四步 | 不变(D1) | 不变,现可逐步看到起止与推理 token | 部署后看 `steps[]` |
| 引擎重算 | 20 候选 0.4–0.5 s / 61 候选 2.3–2.5 s / refresh 5.8–6.1 s(任务书实测) | 本分支不变(D5 ① 未提交);替代方案 refresh 约 −21% | 上表 A/B |
| `/v5/versions` | 每轮按调用点 4–7 次 GET | 每进程每 30 s 1 次 | 单元测试 |
| 出口第二次空闲调用 | 1 次档案读 + 版本读 + 幂等链接写 | Agent 轮常见路径跳过 | 单元测试(写集合不变、调用数减少) |
单轮总时长仍由模型步数决定(D1 不动),本单主要改善的是"开流前空等"和"无反馈";真实分布待部署后用新埋点复核。
## A/B 与等价证据(前端 D5)
- `/v5/versions`:TTL 内两次读取只发 1 次请求、返回副本;TTL 过后重读;残缺身份与异常不记忆;换 fetch 实现互不串用(`rectification-latency-20260926.test.ts` D5 三条)。
- 第二次空闲调用:同一 fixture 连调两次 `persistNextInterviewIfIdle`,第二次返回值与第一次深相等,唯一写操作是同参数的幂等链接;出口 `interviewSettled: true / false` 两臂(都先做 Agent 的那次调用)写操作集合相同、调用数更少。
## 新埋点字段与部署后怎么看
在 staging 主机上:
```bash
docker compose --env-file .env.staging -f deploy/docker-compose.server.yml logs --since 30m web \
| grep -E 'RectificationRunDiagnostic|RectificationTurnDiagnostic|RectificationClassifierDiagnostic|rectification_classifier_unavailable'
```
- `RectificationTurnDiagnostic`(每个 message 轮一行):`path`(`deterministic` 确定性回复 / `agent`)、`status`、`totalMs`(路由收到请求到流结束)、`streamMs`(流内耗时)、`classifier`(`outcome`/`attempts`/`timedOutAttempts`/`elapsedMs`)、`engineCalls[]`(`path`/`ms`/`outcome`)。
- `RectificationRunDiagnostic`(每次 Agent attempt 一行):`attemptNumber`、`attemptStartMs`、`attemptElapsedMs`、`elapsedMs`(整轮)、`stepCount`、`steps[]`(每步 `startMs`→`endMs`、`reasoningTokens`、`tools`)、`inputTokens`/`outputTokens`/`reasoningTokens` 合计、`toolCallCount`/`distinctToolCount`、`classifier`、`engineCalls[]`(本 attempt 内)、`finishReason`、`expectedWrite`。
- 读法:`totalMs − attemptElapsedMs 之和` ≈ 分类 + 预检 + 出口门;某步 `endMs − startMs` 大而 `reasoningTokens` 高 = 思考耗时;`engineCalls` 里 `/api/rectification/v5/score` 的 `ms` 就是重算耗时;`classifier.timedOutAttempts > 0` 说明 10 s 上限触发过。
- 这些行不含用户原文、出生资料或模型文本(测试断言)。
## 改动的既有断言
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
| `frontend/tests/rectification-surface-state.test.ts`「live-row labels follow the action that started the turn」 | `rectificationInitialLiveLabel("message") === "正在处理…"` | `=== "收到,正在对照你的档案…"` | D3:打字回答发出即显示第一句阶段进度,替代一直不变的「正在处理…」(断言内已写三栏注释) |
其余既有断言未改。三条会被字面源码合同卡住的写法,按原文保留(例如 `if (response.status === 402)`、`rememberLiveActivity(RECTIFICATION_ANALYZING_LIVE_LABEL, null)`、计费块缩进),没有去改测试。
## 验证
| 项 | 结果 |
|---|---|
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error;126 warnings,与基线 126 逐条一致(按文件+规则比对) |
| 新测试 | `rectification-latency-20260926.test.ts` 17/17;`.test.tsx` 2/2;`rectification-latency-route-20260926.test.ts` 2/2(Node 22.14;Node 20 为 `mock.module` 环境缺口) |
| 全量 `npm test`(Node 20.19.2) | 4002 条:3912 通过 / 63 失败 / 27 跳过。基线 `test14.log` 3981 / 61 失败;失败名单差异只有本单 2 条路由测试(`mock.module`,Node 20 既有缺口,Node 22 通过);基线测试名无一消失 |
| 全量 `npm test`(Node 22.14.0,本机 `/exec-daemon/node`) | 4002 条:3951 通过 / 24 失败 / 27 跳过。同机 Node 22 基线(`origin/staging` 独立 worktree)3981 / 24 失败;24 条全是 Docker/DB 套件,名单逐条一致,新增失败 0,消失测试名 0 |
| `npm run build -- --webpack` | 通过;`┌ ○ /` Static;构建产生的 `frontend/frontend/` 已删除 |
| 首屏 gzip | `rootMainFiles` 4 个文件 130933 B → 130933 B(0%) |
| Python 快速门 pytest 集合(门禁同款 69 个文件,本机 python3.13) | 948 通过 / 1 跳过 / 0 失败,与基线一致(本分支最终不含 Python 改动;带 D5 ① 补丁时为 4 条验证完整性门禁失败,见偏离 1) |
| 浏览器(`next start` + Chrome 151 无头;CDP 拦截 `/api/*` 返回虚构账户/会话/Case,`/api/rectification/agent` 改道到本地延迟 NDJSON 服务) | 1280 与 390 宽:回车后 17 / 13 ms 显示「收到,正在对照你的档案…」;3.03 / 5.03 / 7.03 s 分别换成「正在记下这件事…」「正在重新对照盘面…」「正在准备下一个问题…」(与服务端事件同步);全程未见「正在处理… / 正在分析…」live 行;结算后无进度句,下一题卡可点。截图 `docs/testing/rectification-latency-20260926/`(`1280-1-sent`、`1280-2-recording`、`1280-3-rescoring`、`1280-4-preparing`、`1280-5-settled`、`390-1-sent`) |
## 环境缺口
- 无模型凭据、无受控登录账号:真实模型下各段耗时、10 s 上限是否触发,按 `docs/testing/rectification-latency-20260926.md` 部署后复核(BLOCKED.md 已记)。
- 无 Docker:`npm run test:db` 未跑(本单不动表)。
- Node 20 下新路由测试 2 条失败属既有 `mock.module` 缺口;Node 22 已全量跑过。
## 观察项(未修)
- ERR-111:同一请求跨进程 `candidate_feature_snapshot` 的 Shadbala / static 指纹与 `feature_hash` 随 `PYTHONHASHSEED` 变化;A/B 因此固定哈希种子。
+1 -1
View File
@@ -36,7 +36,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-dup-question-20260926.md` | `PROGRESS-rectification-dup-question-20260926.md` | **同一轮问题出现两次(BUG-1045,复发自 BUG-585,BUG-969 拼回题干、去重只在刷新路径)+ 同一道选择题连画两张(BUG-1046:提交失败不回滚本地已答 + 兜底问题块条件过宽;H2 漏收回合)**。先于 latency 单 | 已验收(Claude 09-26 直接执行:子代理复现 A 与 B-H1(选择题提交 409 / 网络错误不回滚本地已答);Claude 变基到含 BUG-1043/1044 的 staging 后独立复验 tsc/lint 0、全量 3981 条失败名单与基线逐条一致、四路由 ○、gzip 不变) | `e4c1c7a3`(已部署 `f1d16405`,health 一致) |
| `TASK-rectification-latency-20260926.md` | — | **每轮等待过长(BUG-1047)**:一轮打字回答串行 5 次开思考的模型调用,分类在开流前且无超时。产品定:收尾两步不动、分类保持思考只加 10 秒超时、发出后立即出确定性进度句并按阶段更新;补埋点;服务端无口吻优化须 A/B 逐位一致。排在 dup-question 之后 | 待领取 | — |
| `TASK-rectification-latency-20260926.md` | `PROGRESS-rectification-latency-20260926.md` | **每轮等待过长(BUG-1047)**:一轮打字回答串行 5 次开思考的模型调用,分类在开流前且无超时。产品定:收尾两步不动、分类保持思考只加 10 秒超时、发出后立即出确定性进度句并按阶段更新;补埋点;服务端无口吻优化须 A/B 逐位一致。排在 dup-question 之后 | 待验收(Claude 09-26 直接执行,子代理实现:先建流 + 首行进度 13–17 ms 可见、分类 10 s 上限、四句阶段进度、每步/分类/引擎埋点、`/v5/versions` 记忆 + 跳过第二次空闲调用;D5 引擎探针项未提交——输出一致的替代方案 A/B 逐字节一致但触碰冻结评分身份,需重新冻结验证记录,待产品定) | `codex/rectification-latency-20260926`(未推送) |
| `TASK-rectification-session-title-result-20260922.md` | `PROGRESS-rectification-session-title-result-20260922.md` | **校正会话标题改写结果(BUG-1001)**:BUG-988 把副标题改成最后活动时间后,标题里的日期成了重复;而 BUG-929 删掉 `uniquifySessionTitle` 之后,`resolveSessionTitle` 对校正入口只返回 `生时校正 · M月D日`,**同一天多条标题完全相同**(真机截图:9/17 三条同名、9/16 两条同名、9/14 一天 5 条);旧标题的钟点后缀是创建时间、副标题是最后活动时间,同一行两个对不上的时间。**产品 09-22 拍板**:D1 标题改为承载结果——有 `accepted_time`/`confirmed_time` 写 `生时校正 · HH:MM`,否则写 `candidate_range` 的 `生时校正 · HH:MM–HH:MM`(推翻 BUG-929 的日期口径,但「不得再加墙钟去重后缀」保留);D2 存量批量重算(推翻 BUG-929「旧标题不批量改」,先例 `20260916020000_rectification_session_title_repair.sql`);D3 今日节奏保留日期(我判定的例外);D4 只在结果变化时改 title,`open` 路径不动(对 BUG-699 边界的有限扩展)。**红线**:任何写标题的路径都不得 bump `updated_at`(否则 13 条历史会话集体跳顶、毁掉 BUG-988);标题算式必须只有一处实现(一个 SQL 函数,触发器与回填共用)——BUG-987/992 都栽在「两层规则各写一份」。T4 跨层对断不得让步。已 grep 出真正受影响的只有 7 个文件,Python 侧只查裸字符串、无需改。BUG 段 1001 起 | 已验收(Claude 09-25:单一 SQL 函数、不动 updated_at、手改名不覆盖、回填幂等) | `88fe67df`/`a94a1d67`(已部署) |
| `TASK-rectification-convergence-20260830.md` | `PROGRESS-rectification-convergence-20260830.md` | 收敛重构 v2 | 已验收 | `3a4396a4`、`86ba17ee` |
| `TASK-rectification-decision-authority-20260831.md` | `PROGRESS-rectification-decision-authority-20260831.md` | 决策权威归一与停止语义 | 已合入 | `9f011194` |
@@ -0,0 +1,26 @@
# 真机清单 · 生时校正打字回答的进度句与耗时(BUG-1047,2026-09-26)
部署含 `codex/rectification-latency-20260926` 的 staging 后,用本人账号在一段进行中的生时校正里照做。截图与本地无头浏览器结果见 `docs/testing/rectification-latency-20260926/`(虚构数据)。
## A. 发出后立刻有字
1. 在输入框打一句带年月的经历(例如「2019 年 5 月换了工作」),按发送。
- 期望:发送的同一瞬间,回复位置出现一行转圈的「收到,正在对照你的档案…」;不出现「正在处理…」。
2. 盯着这一行,不要刷新。
- 期望:依次换成「正在记下这件事…」→「正在重新对照盘面…」→「正在准备下一个问题…」(某一句可能一闪而过,但顺序不会倒回去);已完成的步骤仍列在上面(如「读取校正记录」)。
- 期望:步骤之间不再落回「正在分析…」。
3. 正文出现后。
- 期望:进度句全部消失;正文里没有任何一句进度句;刷新后也没有。
4. 打字回答一道点选题(例如「没有」)。
- 期望:同样先出「收到,正在对照你的档案…」,随后直接出回复与下一题。
## B. 总耗时(记下来,和改前对比)
5. 对 A.1 的那一轮用手机秒表记三个时间:按发送 → 第一句出现;按发送 → 正文出现;正文出现 → 下一题可点。
- 期望:第一句 ≤ 0.3 秒。正文出现的总时长本单不承诺(收尾两步仍由模型生成),记录即可。
6. 把三个时间和当时的钟点发给 Claude,便于在服务端日志里按时间找到这一轮的 `RectificationTurnDiagnostic` / `RectificationRunDiagnostic`,核对各段耗时。
## C. 出错时不留残影
7. 在一道题刚被换掉时(另开一个标签页先答掉同一题)回到原标签页打字回答。
- 期望:进度句消失,出现一行说明(如「这道题已经过期,请回答当前问题」),刚才那张卡仍可点,不会多出一张卡。
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB