fix: recover expired rectification handoffs

This commit is contained in:
Jesse_Chen
2026-07-22 12:28:55 +08:00
parent 753210747a
commit 31d6ab1ee3
3 changed files with 65 additions and 0 deletions
+16
View File
@@ -329,3 +329,19 @@
- 相关记录:BUG-009、BUG-017
- 复发自:无
- 修复版本:待提交
## BUG-019 | 原问题交接租约过期后永久显示处理中
- 状态:resolved
- 首次发现:2026-07-22
- 最近更新:2026-07-22
- 影响面:生时校正范围终态、跨设备/失败重试、durable 原问题 handoff
- 用户现象:一次继续咨询在输出前失败后,刷新仍无法重试,接口返回“原问题状态已经变化”。
- 触发条件:handoff 处于 `claimed``executing`,两分钟租约已经过期,客户端重新加载交接。
- 根因:数据库 projection 只看持久化 state,把过期租约仍投影为 `in_progress`;客户端因此不会发起 claim,而执行 RPC 又正确拒绝过期租约,形成永久卡死。
- 修复:projection 将已过期的 `claimed`/`executing` 租约投影为 `pending`,复用既有加锁 claim RPC 原子接管;未过期租约仍保持 `in_progress`,双设备隔离不变。
- 验证:迁移契约测试锁定过期租约恢复语义;生产制造过期 claim 后重新加载、claim、执行模型咨询和 settle。
- 防复发:所有租约型状态的读取投影必须同时解释 lease deadline,不允许只依赖枚举 state。
- 相关记录:BUG-017、BUG-018
- 复发自:无
- 修复版本:待提交
@@ -0,0 +1,36 @@
begin;
create or replace function public.conversational_rectification_handoff_projection(
p_user_id uuid,
p_case_id uuid
)
returns jsonb
language sql
stable
security definer
set search_path = ''
as $$
select pg_catalog.jsonb_build_object(
'caseId', h.case_id,
'turnVersion', c.turn_version,
'question', h.question,
'questionFingerprint', h.question_fingerprint,
'requestId', h.request_id,
'status', case
when h.state = 'pending' then 'pending'
when h.state in ('claimed', 'executing')
and h.lease_expires_at <= pg_catalog.now() then 'pending'
when h.state in ('claimed', 'executing') then 'in_progress'
else 'consumed'
end,
'turn', public.conversational_rectification_case_projection(
p_user_id, p_case_id
) -> 'latest_turn'
)
from public.birth_time_rectification_question_handoffs h
join public.birth_time_rectification_cases c
on c.id = h.case_id and c.user_id = h.user_id
where h.case_id = p_case_id and h.user_id = p_user_id;
$$;
commit;
@@ -201,3 +201,16 @@ test("migration keeps claim, billing recovery, settlement and ACL inside locked
assert.match(sql, new RegExp(`grant execute on function public\\.${functionName}\\([\\s\\S]*?to service_role`, "i"));
}
});
test("expired handoff leases project as pending so another device can reclaim them", () => {
const sql = readFileSync(new URL(
"../supabase/migrations/20260722120000_recover_expired_rectification_handoff.sql",
import.meta.url,
), "utf8");
assert.match(
sql,
/h\.state in \('claimed', 'executing'\)[\s\S]*h\.lease_expires_at <= pg_catalog\.now\(\)[\s\S]*then 'pending'/i,
);
assert.match(sql, /create or replace function public\.conversational_rectification_handoff_projection/i);
});