Files
Jyotisha/TASK-rectification-billing-20260831.md
T
Jesse_Chen 29e331bc0d
Independent Staging Quality Gate / validate (push) Successful in 9m55s
Independent Staging Quality Gate / publish (push) Successful in 11m16s
docs(rectification): add billing failure and question-slot task brief
Traces the staging run_failed on the first user message to a missing
feature_pricing row: the opening turn is free and skips reserve(), so
the first `message` action is the first request to hit
resolve_feature_pricing, which raises feature_pricing_missing and maps
to the generic run_failed fallback. Also records the independent
opening-turn defect that leaves current_question null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-31 06:05:50 +00:00

11 KiB
Raw Blame History

任务书 · 生时校正 run_failed 与开场轮问题槽为空(2026-08-31)

基线:origin/staging @ 9492bae1"docs: record round-1 audit and open round-2 brief")。

本轮两条必做任务来自一次线上故障的代码溯源,根因已定位到具体行,不需要重新排查。第三条是配置缺口,只出方案不动手。


现象

用户开始生时校正,助手正常提出第一个问题;用户回答第一条自然语言证据("2016 年 9 月上大学")后,前端立刻返回:

{"type":"error","code":"run_failed","message":"生时校正暂时不可用,请稍后再试。"}

同时问题槽显示"当前没有可回答的问题,正在等待服务端更新。"

案例 bf7dafa9-6c12-424f-b21f-625d5497cbf2 快照特征:case_revision: 0turns 只有 1 条、last_activity_at 仍等于第 1 轮的 created_atcurrent_question: nullevidence: []。说明第 2 轮在 append_agentic_rectification_turn 落库之前就抛错了。

这不是逻辑回归

已实测:./node_modules/.bin/tsc --noEmit exit 0TypeScript 5.9.3);npx tsx --test tests/rectification-*.test.ts 705 条全绿(696 pass / 9 skipped / 0 fail / 0 error)。最近的 3a4396a4stop 语义)与本次故障无关,不要去动它


硬红线

  1. 不得修改 feature_pricing 的 SQL 函数签名或校验逻辑supabase/migrations/20260831010000_feature_pricing.sql)。resolve_feature_pricingfeature_pricing_missing正确行为 —— 缺定价就该拒绝计费,不能默默按 0 或默认价放行。本轮修的是错误如何被映射和呈现,不是要绕过这个闸门。
  2. 不得在 migration 里硬编码价格 seed。 定价是 admin 可配置的业务数据(admin_save_feature_pricing_draft / admin_publish_feature_pricing),写死会和发布流冲突,并且会让"生产环境用了测试价"这类事故无声发生。
  3. 不得修改 src/lib/rectification-agentic/core/rectification-decision.ts 及其 stop 语义相关逻辑。那是 3a4396a4 刚落地的,与本次故障无关。
  4. 不得把内部错误原文透给用户。 consult-safety.ts 的既有约束继续生效。任务 A 要让错误可诊断(服务端日志)且可行动(用户文案),这是两件事,不要混为一谈。
  5. 不得修改既有测试断言 —— 除非该断言锁住的正是本轮要修的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 里单列。
  6. 不得手写 useCallback / useMemo
  7. 推 staging 前必须 ./node_modules/.bin/tsc --noEmit 通过。不要用 npx tsc —— 新建 worktree 里 node_modules 未安装时 npx 会去装空包 tsc@2.0.4(本仓库既有教训)。
  8. tests/rectification-*.test.ts 不得低于基线 705,且 fail=0。全量 npm test 需要 Docker,本机无 Docker 时会有既有的数据库/部署类失败 —— 必须逐条比对失败清单,确认没有新增
  9. 不得改 .gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。

让步顺序:计费正确性 > 功能与测试不回归 > 错误可诊断 > 用户文案 > 代码整洁。

开工前置

git fetch origin --prune
git worktree add -b codex/rectification-billing-20260831 \
  ../.worktrees/rectification-billing-20260831 origin/staging

基线必须是 origin/staging,不是任何本地 ref。读 docs/research/pre_work_error_ledger.md,跑 scripts/pre_work_check.py,读 frontend/AGENTS.md(Next.js 版本与训练数据不同,写代码前先看 node_modules/next/dist/docs/)。改前先在 docs/BUG_HISTORY.md 检索同类记录。

下面所有行号只是线索,请按符号名定位origin/staging 上的行号可能有偏移。


任务 AP0)· billing_unavailable 没有用户可见映射

事实(已逐行确认)

今天的提交 aaaa5fb7 feat(billing): add feature pricing configuration2026-08-31 03:37)在每个付费校正轮前插入了定价查询。故障发生在 05:44。完整链路:

  1. src/app/api/rectification/agent/route.ts:567billing.reserve()resolveFeaturePricing(accounting, "rectification", selectedModel.id)
  2. supabase/migrations/20260831010000_feature_pricing.sql:69 — 无 ('rectification', <model_tier>)status='published' and enabled 的行时抛 feature_pricing_missing
  3. 该 migration 建的是空表,全仓库无任何 seed insert,行只能由 admin 发布流产生 → staging 上就是没有
  4. src/lib/feature-pricing.ts:46 包装为 FeaturePricingErrorreserve() 捕获后返回 reason: "billing_unavailable"route.ts:583
  5. src/lib/rectification-agentic/v9/agent-run.ts:263throw new RectificationToolServiceError(reserve.reason ?? "billing_denied")
  6. route.ts:702-721 的 catch 映射链没有 billing_unavailable 分支,逐个 else-if 落空后进兜底 run_failed

第 1 轮能成功、第 2 轮才炸,是因为 reserve() 开头有 if (action !== "message") return { success: true, status: 200 } —— opening 轮免费、根本不查定价。用户的第一条消息是第一个 message 动作,也就是第一个撞上定价查询的请求。这与"turns 只有 1 条 + case_revision: 0"完全吻合。

要做的

A-1 补映射。 在 route.ts 的 catch 映射块里为 billing_unavailable 加独立分支,与既有 billing_denied 并列。配置缺失不可自愈,文案不能是"请稍后再试"(用户重试一万次也不会好),要引导联系支持 —— 参考同一块里 skill_identity_missing 分支的语气。

A-2 让日志可诊断。 reserve() 的 catchroute.ts:581-584)当前只记 error.name,日志里只能看到 FeaturePricingError,把 feature_pricing_missing(没配价)和 feature_pricing_model_unavailable(模型没发布版本)这两种排查动作完全不同的情况吞掉了。FeaturePricingErrorsrc/lib/feature-pricing.ts:19 已带 code 字段,改成记录它,并让返回的 reason 能区分这两种。用户侧文案不必区分,服务端日志必须区分。

A-3 同类暴露面。 已确认以下两处有同样问题,一并按一致方式修,但不要顺手重构这两个路由的其他部分

位置 现状
src/app/api/consult/route.ts:375 直接调 resolveFeaturePricing(accounting, "chat.standard", model.id),外层只判断 error_code === "insufficient_credits"FeaturePricingError 会穿透到通用 catch
src/app/api/reports/route.ts:202 billing.reserve 里直接调 resolveFeaturePricing(admin, "report.full", modelId),无任何针对性映射

任务 B(P0)· 开场轮不持久化问题槽

事实

current_question 为 null,导致 src/components/rectification-agentic-chat.tsx:1084showMissingQuestion 为真,用户看到"当前没有可回答的问题,正在等待服务端更新。"

原因:persistNextInterviewIfIdleroute.ts:677)被包在 if (action === "message") 里,opening 轮不会执行。开场轮的工具回执佐证了这点 —— 只跑了 rectification-read-case,没有任何能生成 collect_spoken focus 的工具,助手的问题只以散文形式出现在回复正文里。

action 枚举见 route.ts:69["opening", "message", "read_only", "answer_choice", "stop_and_review"]

这是独立于计费的缺陷。任务 A 修完、定价行发布之后,它依然存在。

决策点 —— 动手前先在 PROGRESS 里写明你选哪个及理由

  • 方案 1(建议):让 opening 轮成功后也走 persistNextInterviewIfIdle。理由:这条路径是服务端确定性的,不依赖模型是否想起来调工具;且 ensureNonTerminalTurnExit 已是同一分支里的同类先例。
  • 方案 2:让 agent 在开场轮自行调用持久化 focus 的工具,需改 skill promptjyotish-birth-time-rectification@10.0.13)。注意 skill 包有 sha256 绑定校验(agent-run.ts:252 resolveExactSkillPackage),改 prompt 意味着要发新版本并处理已有 case 的 skill_identity_mismatch,成本显著更高。

若选方案 1ensureNonTerminalTurnExit 当前与 persistNextInterviewIfIdle 在同一个 action === "message" 分支内 —— 请单独判断它是否也该放开,不要默认捆绑修改,并把判断写进 PROGRESS。


任务 C(P1)· 定价数据缺口 —— 只出方案,不要实现

feature_pricing 表出厂为空。任何环境部署后若不手动发布定价行,rectificationchat.standardreport.full 三条付费链路全部会挂,且(在任务 A 修完前)都表现为语焉不详的通用错误。

请评估并在 PR 描述里提出建议,本轮不要写实现

  • 是否需要环境就绪检查,在定价行缺失时早失败并给出明确信息,而不是等用户点到第二轮才炸
  • 该检查放在哪里:scripts/db-migrate.mjs 之后的独立脚本?还是应用启动期?
  • 已发布的 feature_keymodel_tier 组合应如何做覆盖度校验(三个 feature × 该环境所有已发布模型的 tier)

注意红线 2:不要用 seed migration 解决这个问题。


验收标准

  1. cd frontend && ./node_modules/.bin/tsc --noEmit exit 0
  2. cd frontend && npx tsx --test tests/rectification-*.test.ts 不低于 705 条且 fail=0
  3. 任务 A 补测试:断言 billing_unavailable 不再落到 run_failed 兜底,且 feature_pricing_missing / feature_pricing_model_unavailable 在服务端日志里可区分
  4. 任务 B 补测试:断言 opening 轮结束后 current_question 非空
  5. 任务 C 的方案写在 PR 描述里,仓库内无对应实现改动

交付前必须说明

本任务书的根因链是静态溯源得出的,作者没有 staging 凭据(仓库内无 .env),未在真实环境验证。你若同样无凭据,不要声称已验证。staging 侧仍需人工确认两件事:

select feature_key, model_tier, credit_cost, version, status, enabled
from public.feature_pricing where feature_key = 'rectification';
-- 预期:零行

日志中应能找到:

[rectification-v9] reserve failed case=bf7dafa9-6c12-424f-b21f-625d5497cbf2 reason=FeaturePricingError

若这两条都不成立,说明根因判断有误 —— 停下来登记 BLOCKED.md,不要硬改。

修复上线后,运维侧还需通过 admin 流为 rectification 发布对应 tier 的定价行(src/app/api/admin/feature-pricing/route.ts)。这是代码改动之外的必要动作,缺了它用户依然用不了 —— 请在 PR 描述里显式提醒。