From b276bc8a8f27c86cbb9bccbda8950ecbe0f38692 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 12 Aug 2026 01:05:36 +0800 Subject: [PATCH] fix(rectification): bind default session model --- docs/BUG_HISTORY.md | 15 ++++++ ...00_rectification_session_default_model.sql | 49 +++++++++++++++++++ .../tests/rectification-v9-database.test.ts | 21 ++++++++ 3 files changed, 85 insertions(+) create mode 100644 frontend/supabase/migrations/20260811040000_rectification_session_default_model.sql diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index a7198eb2..51e9ce80 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2899,3 +2899,18 @@ - 防复发:任何对启用 RLS 的表新增 runtime grant 时,必须同时测试对应运行时角色的真实可见行,而不能只断言 `has_table_privilege=true`;feature flag 种子测试必须以 Web 实际使用的 `admin_runtime` 读取。 - 相关记录:BUG-151、BUG-166 - 修复版本:待提交 + +## BUG-169 | V9 新建生时校正 Session 未绑定模型导致 Agent 立即返回模型不可用 + +- 状态:resolved(本地候选) +- 首次发现:2026-08-11 +- 最近更新:2026-08-11 +- 影响面:V9 `open_agentic_rectification_case` 新建会话、既有 `model_id is null` 的生时校正会话,以及 `POST /api/rectification/agent` 的模型解析。 +- 用户现象:运行时开关恢复后,原请求继续返回 `409 模型暂不可用`;请求体包含有效 `modelId`,管理端模型及供应商也均为 published/enabled。 +- 触发条件:V9 Open Case RPC 原子创建 `birth_time_rectification` Session 后立即发送 opening。 +- 根因:RPC 插入 `chat_sessions` 时没有写入 `model_id`;前端仅在内存中把目录默认模型显示为当前选择,而 Agent 路由按安全合同只解析服务端持久化的 `chatSession.model_id/model_config_version`,不会信任请求体覆盖会话模型。 +- 修复:新增向前迁移 `20260811040000_rectification_session_default_model.sql`;数据库触发器为缺少模型的生时校正 Session 绑定当前 published/enabled 默认模型,由既有 pin trigger 固定配置版本,并一次性回填同类历史 Session。普通咨询 Session 与已有明确模型选择均不改变。 +- 验证:V9 PostgreSQL fixture 在创建 Case 前播种默认模型,要求 RPC 新建 Session 后持久化为 `v9-default-model:1`;staging 还需核对迁移账本、原 Session 回填结果和真实 opening 请求。 +- 防复发:任何服务器端创建 `birth_time_rectification` Session 的路径都必须在同一事务内得到可解析的持久化模型与版本;前端显示的默认模型不能替代数据库绑定。 +- 相关记录:BUG-060、BUG-163、BUG-168 +- 修复版本:待提交 diff --git a/frontend/supabase/migrations/20260811040000_rectification_session_default_model.sql b/frontend/supabase/migrations/20260811040000_rectification_session_default_model.sql new file mode 100644 index 00000000..6da1ece2 --- /dev/null +++ b/frontend/supabase/migrations/20260811040000_rectification_session_default_model.sql @@ -0,0 +1,49 @@ +begin; + +create or replace function public.default_rectification_chat_session_model() +returns trigger +language plpgsql +security definer +set search_path = '' +as $$ +begin + if new.session_type = 'birth_time_rectification' and new.model_id is null then + select c.model_id into new.model_id + from public.model_config_versions v + join public.model_configs c on c.id = v.config_id + join public.model_providers p on p.id = v.provider_id + where v.status = 'published' + and v.enabled + and v.is_default + and p.enabled + limit 1; + end if; + return new; +end +$$; + +revoke all on function public.default_rectification_chat_session_model() from public, anon, authenticated; + +drop trigger if exists chat_sessions_default_rectification_model on public.chat_sessions; +create trigger chat_sessions_default_rectification_model +before insert or update of session_type, model_id on public.chat_sessions +for each row execute function public.default_rectification_chat_session_model(); + +with default_model as ( + select c.model_id + from public.model_config_versions v + join public.model_configs c on c.id = v.config_id + join public.model_providers p on p.id = v.provider_id + where v.status = 'published' + and v.enabled + and v.is_default + and p.enabled + limit 1 +) +update public.chat_sessions s +set model_id = default_model.model_id +from default_model +where s.session_type = 'birth_time_rectification' + and s.model_id is null; + +commit; diff --git a/frontend/tests/rectification-v9-database.test.ts b/frontend/tests/rectification-v9-database.test.ts index f831f0a9..19fc3c85 100644 --- a/frontend/tests/rectification-v9-database.test.ts +++ b/frontend/tests/rectification-v9-database.test.ts @@ -101,6 +101,23 @@ test("v9 open is atomic, idempotent and resumes instead of duplicating", { skip: where id = '${userId}'; `); + fixture.psql(` + with provider as ( + insert into public.model_providers (code, name, provider_type, encrypted_api_key, enabled) + values ('v9-test', 'V9 Test', 'openai', 'test-ciphertext', true) + returning id + ), config as ( + insert into public.model_configs (model_id) + values ('v9-default-model') + returning id + ) + insert into public.model_config_versions ( + config_id, version, provider_id, label, provider_model, enabled, is_default, status, published_at + ) + select config.id, 1, provider.id, 'V9 Default', 'gpt-test', true, true, 'published', now() + from config cross join provider; + `); + const service = createLocalPostgresDataClient( fixture.connectionUrl("service_runtime", "service-runtime-test-password"), null, @@ -134,6 +151,10 @@ test("v9 open is atomic, idempotent and resumes instead of duplicating", { skip: fixture.psql(`select count(*) from public.chat_sessions where user_id = '${userId}'`), "1", ); + assert.equal( + fixture.psql(`select model_id || ':' || model_config_version from public.chat_sessions where id = '${sessionId}'`), + "v9-default-model:1", + ); assert.equal( fixture.psql( `select count(*) from public.agentic_rectification_open_ledger where user_id = '${userId}'`,