Merge pull request #30: fix(rectification): skip settlement for free turns
Free opening/read_only turns no longer invoke complete_usage without a reservation.
This commit was merged in pull request #30.
This commit is contained in:
@@ -2930,3 +2930,18 @@
|
||||
- 防复发:任何由模型调用、但值由服务端拥有的工具引用,都必须在 Agent 上下文中显式提供,不能要求模型猜测。
|
||||
- 相关记录:BUG-163、BUG-169
|
||||
- 修复版本:待提交
|
||||
|
||||
## BUG-171 | V9 免费开场生成成功后仍执行 usage settlement 导致 run.failed
|
||||
|
||||
- 状态:resolved(本地候选)
|
||||
- 首次发现:2026-08-11
|
||||
- 最近更新:2026-08-11
|
||||
- 影响面:`POST /api/rectification/agent` 的 `opening` 与 `read_only` 免费 Turn,以及回答完成后的 Turn 最终状态与 assistant message 持久化。
|
||||
- 用户现象:Agent 已加载 Skill 与正确 Case,并输出完整回答,但 NDJSON 最终事件仍为 `run.failed`;数据库 Turn 为 `retryable` 且没有持久化 assistant message。
|
||||
- 触发条件:免费 `opening` 或 `read_only` Turn 正常生成回答并进入成功收尾。
|
||||
- 根因:路由的 `billing.reserve()` 对免费 Turn 不创建 `usage_reservations`,但 `billing.complete()` 仍调用 `complete_usage`;数据库因找不到 reservation 返回 `request_missing`,Agent runner 将已成功回答降级为 `usage_settlement_failed`。
|
||||
- 修复:免费 Turn 在 settlement adapter 中直接成功返回;只有 `message` Turn 才执行 reservation 与 settlement,保留原有付费消息的计费、幂等与失败保护。
|
||||
- 验证:staging 数据库确认目标 Case 没有 usage reservation,直接调用同一结算函数稳定返回 `request_missing`;新增合同回归要求免费 Turn 同时绕过 reservation 与 settlement。部署后需以真实 opening 确认最终 `run.completed`、Turn `completed` 且 assistant message 已持久化。
|
||||
- 防复发:任何声明为免费的 Agent action 必须在授权和结算两个阶段保持同一策略,不能只跳过预授权而继续结算。
|
||||
- 相关记录:BUG-163、BUG-168、BUG-169、BUG-170
|
||||
- 修复版本:待提交
|
||||
|
||||
@@ -209,7 +209,7 @@ export async function POST(request: Request) {
|
||||
async reserve() {
|
||||
// opening / read-only turns are free; the first substantive run
|
||||
// reserves once, and resume/retry reuse the same case-bound request.
|
||||
if (action === "opening" || action === "read_only") {
|
||||
if (action !== "message") {
|
||||
return { success: true, status: 200 };
|
||||
}
|
||||
try {
|
||||
@@ -233,6 +233,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
},
|
||||
async complete(usage) {
|
||||
if (action !== "message") return true;
|
||||
try {
|
||||
const billingRequestId = await rectificationBillingRequestId(accounting, userId, caseId);
|
||||
const settlement = await completeUsage(accounting, userId, billingRequestId, {
|
||||
|
||||
@@ -21,6 +21,11 @@ test("Agentic rectification reuses one case-level usage authorization and the se
|
||||
assert.match(rectificationRoute, /releaseUsage\(accounting, userId, billingRequestId,/);
|
||||
});
|
||||
|
||||
test("free Agentic rectification turns bypass both usage reservation and settlement", () => {
|
||||
assert.equal(rectificationRoute.match(/if \(action !== "message"\)/g)?.length, 2);
|
||||
assert.match(rectificationRoute, /async complete\(usage\) \{\s*if \(action !== "message"\) return true;/);
|
||||
});
|
||||
|
||||
|
||||
test("standard consultation resolves and settles the session-pinned model version", () => {
|
||||
assert.match(consultRoute, /sessionId: z\.string\(\)\.uuid\(\)/);
|
||||
|
||||
Reference in New Issue
Block a user