diff --git a/CHANGELOG.md b/CHANGELOG.md index f1380307..cfaf9eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 生时校正开场先播出的打招呼会留在同一条消息里。`rectification-set-focus` 只负责题干,不再发空的 `replace` 把正文抹掉。Skill 版本未变。 +## 2026-09-04 — 报告写到一半时阅读页不再误报失败 + +分章生成会把进度写成 `section:主题`。后台读回这一行时校验过严,心跳中断,打开报告会看到失败。现在按写入时同一套规则读回进度。Skill 版本未变。 + ## 2026-09-04 — 未确认生时的个人报告不再被候选窗读取打死 出生时间尚未 confirmed 时,报告会去读校正候选窗。那张表已经收回运行时表权限,读失败却被当成计算不可用,整份报告在排盘前就失败。现在改走只读 RPC;读不到窗口就按无窗口继续生成。Skill 版本未变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 1e2fe35b..dfcafb10 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -8125,9 +8125,9 @@ - 触发条件:资料 `birth_time_status` 为 `accepted`(不是 `confirmed`),worker 与 create route 因此去查 `agentic_rectification_cases` 的 `candidate_accepted` 行。 - 根因:`20260814010000_immutable_skill_registry.sql` 已从该表收回 `service_role` 的表级权限,只许走 security definer RPC。报告链路仍 `from("agentic_rectification_cases").select(...)`。Postgres 三次 `permission denied for table agentic_rectification_cases`(与 job 三次 attempt、默认 5s/10s 重试对齐)。查询失败被映射成可重试 `calculation_unavailable`,报告从未调用引擎。第二读者:create route 同样 `if (error) throw error`,会把创建请求打成 `calculation_unavailable`。 - 修复:新增只读 RPC `read_report_candidate_range`(不 re-grant 表权限)。worker 与 create route 经共享 helper 调用;任何读失败降级为无窗口并继续生成。`calculation_unavailable` 只保留给引擎调用失败。legacy 表终态以库约束为准:`confirmed` 与 `completed`(`20260720` 已纳入枚举;前端不再 `.in("status", ["confirmed", "completed"])` 直查)。 -- 验证:`tests/report-candidate-range.test.ts` 三态降级;`tests/personal-report-api.test.ts` null 窗口仍 201;`tests/database-report-candidate-range.test.ts` 权限矩阵与列白名单。产品侧真实 personal_full 待本修复部署后补。 +- 验证:`tests/report-candidate-range.test.ts` 三态降级;`tests/personal-report-api.test.ts` null 窗口仍 201;`tests/database-report-candidate-range.test.ts` 权限矩阵与列白名单。staging @ `be6cf145`:accepted 档案 `personal_full` 五次 `POST /api/consultation_workflow` 均 200,career 章已落库;随后被 BUG-534 打断,完整 ready 待 534 部署后补。 - 防复发:报告 worker 与 create route 不得再直接 SELECT 已收回 `service_role` 权限的校正表;候选窗读失败不得再变成 `calculation_unavailable`。 -- 相关记录:BUG-524 +- 相关记录:BUG-524、BUG-534 - 复发自:无 - 修复版本:待发布 - 编号说明:rebase 到 `origin/staging` 时 BUG-525 已被采集拒答占用,本条落在 BUG-526。 @@ -8244,3 +8244,19 @@ - 相关记录:无 - 复发自:无 - 修复版本:待发布 + +## BUG-534 | 分章进度 `section:` 让 job 行无法 round-trip,心跳中断、阅读页误报失败 + +- 状态:resolved +- 首次发现:2026-09-04 +- 最近更新:2026-09-04 +- 影响面:`personal-report-job-service-core` `recordFromRow`;心跳 RPC 返回解析;`GET /api/reports/:id` 读 generating 行 +- 用户现象:standard personal_full 已经调到引擎并写出至少一章后,阅读页变成 `report_generation_failed` /「报告暂时无法读取」。后台 job 停在 `section:theme-*`,心跳不再刷新,直到租约过期才重试。 +- 触发条件:worker `onProgress` 把 `progress_phase` 写成 `section:`(SQL 与 `isValidPersonalReportJobProgressPhase` 允许冒号)。随后心跳或 GET 把该行再读进 `recordFromRow`。 +- 根因:`recordFromRow` 用 `operationalCodePattern`(`^[a-z][a-z0-9_]{0,63}$`)校验 `progress_phase`,不接受冒号。`updateProgress` 写入成功后解析 RETURNING 行抛 `storage_invalid`;`onProgress` 吞掉非租约错误,生成继续。下一次心跳解析同一行再抛,worker 中止。GET 同一路径变成 HTTP 500。 +- 修复:`progress_phase` 改走已有的 `isValidPersonalReportJobProgressPhase`(与 SQL check、写入校验同一套,允许 `section:`)。 +- 验证:`personal-report-job-service`:写入 `section:theme-health_pressure` 后心跳与 `getOwnedByRequestId` 仍成功。 +- 防复发:job 行 round-trip 不得比写入校验更严。分章进度相位必须能读回。 +- 相关记录:BUG-526 +- 复发自:无 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-report-candidate-range-20260904.md b/docs/tasks/PROGRESS-report-candidate-range-20260904.md index ee7a923c..3b4a2f42 100644 --- a/docs/tasks/PROGRESS-report-candidate-range-20260904.md +++ b/docs/tasks/PROGRESS-report-candidate-range-20260904.md @@ -10,7 +10,7 @@ | --- | --- | --- | | 1 两处读取降级 + 语义修正 | 完成 | BUG-526 | | 2 只读 RPC + `test:db` | 完成(本文件定向套件已绿;全量 `test:db` 见下方门禁输出) | BUG-526 | -| 3 部署后真实 personal_full | 待部署 | BUG-526 | +| 3 部署后真实 personal_full | 进行中(引擎已通;被 BUG-534 打断) | BUG-526 / BUG-534 | | 4 BUG-526 收口 | 完成(修复版本待 staging SHA) | BUG-526 | ## 实现要点 @@ -41,3 +41,19 @@ - 定向:`report-candidate-range` + `personal-report-api` + `personal-report-worker` + migration 源码锁 **75 pass** - `npm run test:db`(`tsx --test --test-concurrency=1 tests/database-*.test.ts`):**35 pass, 0 fail**(含新 RPC 套件)。第一次全量 34/35,`database-admin-identity` 迁移在套件开头失败,单跑即过,属 Docker 争用。 - 非数据库 `tsx --test tests/*.test.ts`:2626 pass;当时 2 fail 为 CSS 合同(已由 `0be51e65` 修)与 `identity-auth-integration`(与并行 `test:db` 抢 fixture,单跑已过)。 + +## 任务 3 · staging 实测(`be6cf145`) + +health:`gitCommit=be6cf145`,`latestMigration=20260904010000_read_report_candidate_range.sql`。RPC 对 `candidate_accepted` 行返回窗口。无 `report_candidate_range_unavailable`。 + +request `3c4d8311-d222-494b-8174-b8e731204fd2`(standard 五主题): + +| 项 | 结果 | +| --- | --- | +| 创建 | generating,未在 16 秒内 `calculation_unavailable` | +| 引擎 | 5× `POST /api/consultation_workflow` 200(11:48:17–11:50:03Z);租约恢复后又 5× | +| 章节 | `theme-career` ready 1776 chars;`theme-health_pressure` ready 1581 chars(attempt 2) | +| telemetry | career `finishReason=stop` `inputTokens=5866` `elapsedMs=10765`;health 首次 `tripwire`×2(无 length) | +| 阅读页 | GET 500 `report_generation_failed`(假失败) | + +随后 worker 心跳停在 `section:theme-health_pressure`。根因是 BUG-534:`recordFromRow` 不接受 `section:`。为避免第三次租约耗尽把报告标成 `calculation_unavailable`,将该 job 租约延长到 15:06Z,等 534 部署后再过期恢复。 diff --git a/frontend/src/lib/personal-report-job-service-core.ts b/frontend/src/lib/personal-report-job-service-core.ts index 46d4cc77..14318162 100644 --- a/frontend/src/lib/personal-report-job-service-core.ts +++ b/frontend/src/lib/personal-report-job-service-core.ts @@ -352,7 +352,7 @@ function recordFromRow(value: unknown): PersonalReportJobRecord { } const status = statusValue as PersonalReportJobStatus; if (attemptCount > maxAttempts) storageInvalid("invalid personal report job retry budget"); - if (!operationalCodePattern.test(progressPhase)) storageInvalid("invalid progress phase"); + if (!isValidPersonalReportJobProgressPhase(progressPhase)) storageInvalid("invalid progress phase"); if (lastErrorCode !== null && !operationalCodePattern.test(lastErrorCode)) { storageInvalid("invalid last error code"); } diff --git a/frontend/tests/personal-report-job-service.test.ts b/frontend/tests/personal-report-job-service.test.ts index 3c459e5c..63776a7e 100644 --- a/frontend/tests/personal-report-job-service.test.ts +++ b/frontend/tests/personal-report-job-service.test.ts @@ -479,6 +479,23 @@ test("claim and heartbeat require the exact live lease token", async () => { }); assert.equal(progressed.progressPhase, "building_evidence"); assert.equal(progressed.progressPercent, 40); + + const sectionProgress = await service.updateProgress({ + jobId: claimed.id, + leaseToken: claimed.leaseToken, + phase: "section:theme-health_pressure", + percent: 52, + }); + assert.equal(sectionProgress.progressPhase, "section:theme-health_pressure"); + backend.now = new Date("2026-08-14T03:00:20.000Z"); + const afterSection = await service.heartbeatLease({ + jobId: claimed.id, + leaseToken: claimed.leaseToken, + leaseSeconds: 60, + }); + assert.equal(afterSection.progressPhase, "section:theme-health_pressure"); + const owned = await service.getOwnedByRequestId(USER_A, REQUEST_A); + assert.equal(owned?.progressPhase, "section:theme-health_pressure"); }); test("atomic completion binds the canonical v2 document to the exact live lease", async () => {