- README.md is now the product/repo front door (architecture, repo map, local dev, test tiers, delivery flow, doc map). Engine positioning, VedAstro/Codex setup and the oracle/benchmark command reference move verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and docs/benchmark/README.md. Capability badges realigned with the registry (91/78/8/0); tests/test_readme_badges.py was red on staging. - AGENTS.md: Part A (environment truth, delivery, worktrees, record placement, bug workflow, growth freeze, frontend red lines, privacy, pre-work check, test tiers) and Part B (reading-rigor constraints). GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only mirror. All strings locked by tests/ are preserved. - CLAUDE.md added: roles, three working modes, task-brief sections, acceptance criteria, session discipline; imports AGENTS.md. - 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to docs/tasks/ with an index; REPO_LAYOUT.md merged into README. Docs-only change (no gated path touched). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
13 KiB
PROGRESS · 个人报告写作阶段 report_schema_invalid(2026-09-02)
任务书:TASK-report-section-writer-failure-20260902.md(838d9e76)
分支:codex/report-writer-failure-20260902
基线:origin/staging @ 838d9e76
开工前置:已读 docs/research/pre_work_error_ledger.md、frontend/AGENTS.md;docs/BUG_HISTORY.md 检索 BUG-352 / 451 / 486–489 / 495。按任务书从 origin/staging 建独立 worktree,不碰原仓脏树。
隐私:下文只有错误码、token 计数、finishReason、时间戳与 request UUID(任务书已公开)。无 prompt、bundle、模型原文、姓名、出生资料或账号标识。
任务 0 · Staging 实证(门控)
取证主机:staging 118.26.111.127,路径 /opt/jyotisha-staging。Postgres 容器 jyotisha-staging-postgres-1。
日志窗口丢失
取证时公开 staging health 仍指向 35688015(838d9e76 的父提交)。Web 容器在 2026-09-02T14:00:21Z(22:00 +08)被 recreate,与任务书提交 838d9e76(14:00:43Z)几乎同时。事故窗口(13:19–13:21 UTC)的 docker logs 已不在。[personal-report-agent] 与 [personal-report] generation_failed 无法回收。usage_ledger 对该 requestId 为 0 行。
因此 不能 用本事故的 finishReason / outputTokens 分布来确认「主嫌 · 截断」。截断判定只能写 blocked。
事故行(requestId 77b79067-bd70-465e-ac23-adc4500d4738)
| 面 | 结果 |
|---|---|
personal_reports |
failed / report_schema_invalid / created 13:19:23 UTC / updated 13:21:38 UTC |
personal_report_jobs |
failed / last_error_code=report_schema_invalid / phase failed / progress 43% / attempt 1 of 3 / finished 13:21:38 UTC |
theme-career |
blocked,attempts 2/2,last_error_code=section_evidence_insufficient,updated 13:21:38.398 UTC |
theme-marriage |
pending,0 attempts |
theme-timing |
pending,0 attempts |
theme-wealth |
pending,0 attempts |
43% = 30 + floor((1/4)*55):career 被 block 后第一次 onProgress。Job 在 career block 后约 39ms 结束,其余三章从未 start。
历史 report_schema_invalid(08-30 起,与任务书 5 个小时一致):跨这些小时的 section 行里,本事故是唯一一条 blocked + section_evidence_insufficient。其余为 pending 或 ready。
定案
- 主嫌(CJK
/2截断)未证实。 落库码不是section_output_truncated。截断在 writer 路径通常会变成PersonalReportAgentOutputError("report_schema_invalid")→section_output_invalid。本事故没有 telemetry,不得写成finishReason=length。 - 次嫌(evidenceRefs 回显)是 career 章的最佳解释。 旧
classifySectionErrorCode把任何含"evidence"的消息打成section_evidence_insufficient。report_writer_evidence_refs_mismatch含子串evidence,会被误分类。career 2/2 次、约 29s,符合 assert 失败 + 一次 repair,而不是四章长输出截断。 - 第三层:首章 blocked 后没有继续写其余章。 产品测试已要求其他章仍应交付。Job 以不可重试的
report_schema_invalid失败(attempt 1/3),说明 worker 收到了{status:"failed"}或invalid_request/storage_invalid,而不是 retryable 抛错。无日志无法闭合「是 filter / onProgress / 其它未捕获抛错」。本轮仍把每章失败隔离开,避免一章 throw 中止整份。
all_sections_blocked 原先裸 return、不走 failSchema,因此即使日志还在,也可能看不到 generation_failed。本轮改为走 failSchema。
任务 1 仍按任务书授权落地中文口径公式,校准用的是 08-30 任务 0 的整份调用观测(outputTokens=3069,finishReason=stop),不是 本事故。research / deep 的 3072 cap 保持不变;若以后实测需要更高,另开任务。
任务 1 · CJK 输出预算
禁止 targetCharacters.max / 2。
口径:1 中文字符 ≈ 1.3 token(CJK_OUTPUT_TOKENS_PER_CHAR)。
章节结构余量 568(narrative 之外的 title / actions / caveats / refs / JSON 外壳):
| 部分 | 估算 |
|---|---|
| title ≈ 80 字 | ceil(80 × 1.3) = 104 |
| 3 条 actions ≈ 120 字 | 156 |
| caveats ≈ 80 字 | 104 |
| evidenceRefs JSON | 80 |
| JSON 键与括号 | 124 |
| 合计 | 568 |
摘要结构余量 580 已经包含 headline / priorities / JSON,不再把 headline 算第二次:
ceil(2000 × 1.3) + 580 = 2600 + 580 = 3180,再与章节同一 cap 3072 取 min(08-30 整份 outputTokens=3069 / stop)。落地 3072。
章节:min(3072, max(1024, ceil(maxChars × 1.3) + 568))
| 深度 | max 字 | 未封顶 | 落地预算 |
|---|---|---|---|
| concise | 500 | 1218 | 1218 |
| standard | 1200 | 2128 | 2128 |
| deep | 2000 | 3168 | 3072(cap,缺口 96 token) |
| research | 2800 | 4208 | 3072(cap;约合 narrative 2504 字,低于 2800 上限) |
| summary | 2000 字段 | 3180 | 3072(与章节同一 cap) |
08-30 整份 writer 一次调用 outputTokens=3069 且 stop,说明 3072 能装下一份大 JSON。本事故未证实截断,因此不把 cap 自行抬高。length 修复重试:同一 outputBudget(不得低于首次),提示词要求压到 targetCharacters.min 并闭合 JSON。
任务 2 · evidenceRefs 回显
sectionPrompt增加:evidenceRefs 必须逐字复制 plan.evidenceRefs 数组,不得增删。assertWriterOutput仍是 id/theme 全等 + refs 集合相等,语义未放宽。- Repair 只拼类别词:
identity mismatch/refs mismatch/schema invalid/output truncated。refs 类再次重申逐字复制。不把 ref id、正文或 bundle 拼进 repair。 classifySectionErrorCode先匹配 identity / refs,再匹配泛化"evidence",避免再把 refs mismatch 写成section_evidence_insufficient。
任务 3 · 失败可观测
不改表。应用层聚合已有 personal_report_sections 行。
- 详情 API:
failureSummary、innerReason(all_sections_blocked或section_generation_incomplete)、sectionErrorCodes。 - 列表 API:失败行多查
request_id, status, last_error_code(无正文),写入failureSummary。 - 报告中心:
failureSummary ?? failureCode。详情页摘要为主文案,等宽错误码为辅。
本事故若用新摘要,应接近:「4 个主题中 1 个写作失败:证据引用未对齐,3 个未开始」。四章都写成但整份失败时,应接近:「4 个主题已写成,整份报告未完成装配」,不得再显示成空的「结构校验」。旧码仍会显示「证据不足」,因为历史行的 last_error_code 不会回写。
任务 4 · 真实验证
GET https://staging.jyotisha.chat/api/health → .deployment.gitCommit = eda37c1523341898850e6d71d63c8dd40563973b。Web 容器 2026-09-02T17:31:48Z recreate。未提升 main。
Web docker logs 在 Ready 之后几乎不再收录 console.info。本轮拿不到 [personal-report-agent] 的 finishReason / 每章 token。usage_ledger 对未 ready 的请求无 complete 行。2 倍线的真实 inputTokens 仍缺测;墙钟如下,不自行裁决。
Run A · requestId 54be49f1-a657-43fa-88ce-7d7e8d575831
standard personal_full,主题 career / marriage / wealth / timing。started 17:44:07 UTC,终态 17:54:30 UTC(约 10.4 分钟)。
| 章 | status | attempts | narrative 字数 | last_error_code |
|---|---|---|---|---|
| theme-career | ready | 1 | 1557 | (空) |
| theme-marriage | ready | 2 | 1395 | (空) |
| theme-timing | ready | 2 | 842 | (空) |
| theme-wealth | ready | 1 | 470 | (空) |
事故那一章(career)这次 1 次成功,没有 section_output_truncated / section_refs_mismatch。四章都有正文。
Job 在写作过程中 lease_expired 两次(默认租约 120s,单章可超过 2 分钟;heartbeat 未能保住租约),第三次 attempt 写完 wealth 后约 39ms 整份 report_schema_invalid。进度停在 71%(timing 完成后的值),wealth 的 85% onProgress 未落库。四个 section 行仍是 ready。摘要/装配没有产出 report_document。reservation released。
默认 leaseSeconds=120 不够串行四章写作。心跳 setInterval 被 unref 后,Next.js instrumentation 在没有 HTTP 请求时会跳过定时器,单章 4 分钟写作期间租约无法续上。补丁:600s 租约、20s 心跳、心跳定时器保持 ref、章节 onProgress 先 heartbeatLease 再写进度。摘要预算与章节同一 cap 3072(原公式 3180)。
回溯抽查(只记是否对得上 ref,不摘正文)
| # | 章 | 叙事层(布尔) | 对回的 evidenceRef |
|---|---|---|---|
| 1 | wealth | 含 yoga 词 | ev-tech-yoga |
| 2 | 四章皆含 | 含 大运/主运/副运 | ev-tech-vimshottari |
| 3 | marriage | 含 yoga 词 + 大运词 | ev-tech-d9 + ev-tech-dk |
| 4 | career | 含 yoga 词 + 大运词 | ev-tech-amk + ev-tech-vimshottari |
| 5 | wealth | 含 yoga 词 + 大运词 | ev-tech-d2 + ev-tech-d11 |
未在四章叙事中扫到强弱/shadbala 用词,不记为回溯命中。
Run B · requestId 8e75299b-58ac-43c2-8bf4-59f9bd394734
为区分租约与摘要失败另开一次。四次 POST /api/consultation_workflow 均为 200,随后 worker 在 buildReportEvidenceBundleV2 处非重试失败,calculation_unavailable,无 section 行。未再消耗第三次生成。
相对 2 倍线
无每章 inputTokens/outputTokens。Run A 墙钟约 10.4 分钟(含两次租约回收与 marriage/timing 的 section repair)。数据摆出,不裁决。
Run C · requestId 89c04491-81ca-457e-b09d-dc81c3af36b3
GET /api/health .deployment.gitCommit = 5c0bec0c9419e12e2c1fcab8e75fd35d535cb50e。Web 容器 2026-09-02T18:24:50Z。未提升 main。
standard personal_full,主题 career / marriage / wealth / timing。started 01:31:16 UTC,终态 01:45:53 UTC(约 14.6 分钟)。reservation released。
| 章 | status | attempts | narrative 字数 | last_error_code |
|---|---|---|---|---|
| theme-career | ready | 1 | 170 | (空) |
| theme-marriage | ready | 2 | 1338 | (空) |
| theme-timing | ready | 1 | 926 | (空) |
| theme-wealth | ready | 1 | 745 | (空) |
Telemetry([personal-report-agent],本轮 docker logs 可读):
| 调用 | outcome | elapsedMs | inputTokens | outputTokens | finishReason | repair |
|---|---|---|---|---|---|---|
| career | resolved | 5721 | 5215 | 560 | stop | false |
| marriage | failed | 9430 | null | null | tripwire | false |
| marriage | failed | 9447 | null | null | tripwire | true |
| marriage | resolved | 13618 | 5076 | 1240 | stop | false |
| timing | resolved | 8052 | 4474 | 816 | stop | false |
| wealth | resolved | 7446 | 5228 | 759 | stop | false |
| summary | (无日志) |
无 finishReason=length。四章 stop 合计 input 19993 / output 3375。无 summary telemetry。
Job attempt 1 在 marriage 写作中心跳停更(heartbeat 与 lease 同步老化,最长观测 584s),lease_expired 后 attempt 2。四章随后都写成 ready。wealth 落库 01:45:53.549,整份失败 01:45:53.583(约 34ms)。进度停在 57%(marriage 完成后的 2/4),timing/wealth 的 onProgress 未落库。无 [personal-report] generation_failed 行 → 不是 failSchema 的摘要/装配返回,而是 onProgress/续租抛错被 worker 映射成 report_schema_invalid。
Run C 回溯抽查(只记是否对得上 ref)
| # | 章 | 叙事层(布尔) | 对回的 evidenceRef |
|---|---|---|---|
| 1 | 四章皆含 yoga 词 | true | wealth 另有 ev-tech-yoga |
| 2 | marriage / timing / wealth | 大运/主运/副运 true;career false | 四章皆有 ev-tech-vimshottari |
| 3 | marriage | yoga + 大运 | ev-tech-d9 + ev-tech-dk |
| 4 | career | yoga;无大运用词 | ev-tech-amk + ev-tech-vimshottari |
| 5 | wealth | yoga + 大运 | ev-tech-d2 + ev-tech-d11 |
未扫到强弱/shadbala 用词。
相对 2 倍线
四章 writer 墙钟合计约 35s 模型时间;整份 14.6 分钟含一次租约回收与 marriage tripwire。inputTokens 已测(上表)。数据摆出,不裁决。
任务 4 结论
Writer 修正在章节层成立。整份 ready 被两件事挡住:Next.js instrumentation 里章节写作期间心跳定时器仍不续租;最后一章 complete 之后 onProgress 抛错被标成 schema 失败,摘要从未开始。跟进补丁:进度投影失败不得中止已落库章节的摘要/装配。
质量门
| 门 | 结果 |
|---|---|
frontend/node_modules/.bin/tsc --noEmit |
通过(exit 0,无输出) |
eslint(改动的 TS/TSX 文件) |
exit 0,无 error 输出 |
聚焦 tsx --test:plan / writer-failure / generation-v2 / api / view / agent-telemetry |
104 pass / 0 fail |
| 复跑 api + writer-failure | 53 pass / 0 fail |
personal-report-generation.test.ts + personal-report-worker.test.ts |
51 pass / 0 fail |
未跑全量 tests/*.test.ts,不声称全套绿。未改 .gitea/workflows/**,未提升 main。
任务 4 补丁(租约 600s、摘要 cap 3072、心跳保持 ref + 进度续租 + onProgress 不中止装配):tsc --noEmit 通过;generation-v2 / worker / writer-failure 聚焦测试通过。