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

147 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 生时校正 run_failed 与开场轮问题槽为空(2026-08-31)
基线:`origin/staging` @ `9492bae1`"docs: record round-1 audit and open round-2 brief")。
本轮两条必做任务来自一次线上故障的代码溯源,根因已定位到具体行,**不需要重新排查**。第三条是配置缺口,只出方案不动手。
---
## 现象
用户开始生时校正,助手正常提出第一个问题;用户回答第一条自然语言证据("2016 年 9 月上大学")后,前端立刻返回:
```json
{"type":"error","code":"run_failed","message":"生时校正暂时不可用,请稍后再试。"}
```
同时问题槽显示"当前没有可回答的问题,正在等待服务端更新。"
案例 `bf7dafa9-6c12-424f-b21f-625d5497cbf2` 快照特征:`case_revision: 0``turns` 只有 1 条、`last_activity_at` 仍等于第 1 轮的 `created_at``current_question: null``evidence: []`。说明第 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)。最近的 `3a4396a4`(stop 语义)与本次故障无关,**不要去动它**。
---
## 硬红线
1. **不得修改 `feature_pricing` 的 SQL 函数签名或校验逻辑**`supabase/migrations/20260831010000_feature_pricing.sql`)。`resolve_feature_pricing``feature_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。
让步顺序:计费正确性 > 功能与测试不回归 > 错误可诊断 > 用户文案 > 代码整洁。
## 开工前置
```bash
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 configuration`2026-08-31 03:37)在每个付费校正轮前插入了定价查询。故障发生在 05:44。完整链路:
1. `src/app/api/rectification/agent/route.ts:567``billing.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` 包装为 `FeaturePricingError``reserve()` 捕获后返回 `reason: "billing_unavailable"`route.ts:583
5. `src/lib/rectification-agentic/v9/agent-run.ts:263``throw 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`(模型没发布版本)这两种**排查动作完全不同**的情况吞掉了。`FeaturePricingError``src/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:1084``showMissingQuestion` 为真,用户看到"当前没有可回答的问题,正在等待服务端更新。"
原因:`persistNextInterviewIfIdle`route.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 prompt`jyotish-birth-time-rectification@10.0.13`)。注意 skill 包有 sha256 绑定校验(`agent-run.ts:252` `resolveExactSkillPackage`),改 prompt 意味着要发新版本并处理已有 case 的 `skill_identity_mismatch`,成本显著更高。
若选方案 1`ensureNonTerminalTurnExit` 当前与 `persistNextInterviewIfIdle` 在同一个 `action === "message"` 分支内 —— **请单独判断它是否也该放开,不要默认捆绑修改**,并把判断写进 PROGRESS。
---
## 任务 C(P1)· 定价数据缺口 —— 只出方案,不要实现
`feature_pricing` 表出厂为空。任何环境部署后若不手动发布定价行,`rectification``chat.standard``report.full` 三条付费链路**全部**会挂,且(在任务 A 修完前)都表现为语焉不详的通用错误。
请评估并**在 PR 描述里提出建议,本轮不要写实现**:
- 是否需要环境就绪检查,在定价行缺失时**早失败**并给出明确信息,而不是等用户点到第二轮才炸
- 该检查放在哪里:`scripts/db-migrate.mjs` 之后的独立脚本?还是应用启动期?
- 已发布的 `feature_key``model_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 侧仍需人工确认两件事:
```sql
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 描述里显式提醒。