Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32b65cfaf3 | |||
| db9e7ba2a4 | |||
| b276bc8a8f | |||
| 752c8f4239 |
@@ -2899,3 +2899,34 @@
|
||||
- 防复发:任何对启用 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
|
||||
- 修复版本:待提交
|
||||
|
||||
|
||||
## BUG-170 | V9 Agent 开场未收到 Case ID 导致工具调用失败
|
||||
|
||||
- 状态:resolved(本地候选)
|
||||
- 首次发现:2026-08-11
|
||||
- 最近更新:2026-08-11
|
||||
- 影响面:`POST /api/rectification/agent` 的 opening、message、read-only Agent 消息,以及所有要求 `caseId` 的 V9 rectification 工具调用。
|
||||
- 用户现象:接口已返回 `200 application/x-ndjson`,Skill 与 Case 均已加载,但回答正文报告 `invalid_case_id`,最后事件为 `run.failed`。
|
||||
- 触发条件:Agent 按指令调用 `rectification-read-case`,但服务端构造的 Agent 消息没有提供当前 Case ID。
|
||||
- 根因:请求路由和 `runV9AgentTurn` 已验证 Case/Session 绑定,但 `buildAgentMessages` 只传时间与用户消息;模型只能猜测工具所需的 `caseId`。
|
||||
- 修复:在共享 Agent 消息构造处加入服务端已验证的唯一 Case ID,并明确所有 rectification 工具必须原样使用;不放宽 UUID、所有权或 Case/Session 绑定校验。
|
||||
- 验证:新增回归测试捕获实际传给 Agent 的 opening 消息,要求包含精确服务端 Case ID;staging 需以原请求确认流以 `run.completed` 结束。
|
||||
- 防复发:任何由模型调用、但值由服务端拥有的工具引用,都必须在 Agent 上下文中显式提供,不能要求模型猜测。
|
||||
- 相关记录:BUG-163、BUG-169
|
||||
- 修复版本:待提交
|
||||
|
||||
@@ -337,18 +337,20 @@ function buildAgentMessages(options: V9AgentRunOptions, _attempt: number): unkno
|
||||
void _attempt;
|
||||
const timeContext = options.timeContext
|
||||
?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
|
||||
const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`;
|
||||
if (options.action === "opening") {
|
||||
return [{
|
||||
role: "user",
|
||||
content: [
|
||||
timeContext,
|
||||
caseContext,
|
||||
"【服务端开场指令】这是本校正 Case 的首次开场,还没有用户输入。请先调用 skill 工具加载 jyotish-birth-time-rectification,再调用 rectification-read-case 读取服务端 Case 与证据摘要,然后用简体中文自然开场:说明你会通过已发生的人生事件来校正出生时间,并自然地提出第一个最有用的问题(只需一个问题)。",
|
||||
].join("\n"),
|
||||
}];
|
||||
}
|
||||
return [{
|
||||
role: "user",
|
||||
content: [timeContext, options.message ?? ""].join("\n"),
|
||||
content: [timeContext, caseContext, options.message ?? ""].join("\n"),
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -173,6 +173,35 @@ function runOptions(overrides: Partial<V9AgentRunOptions> = {}): {
|
||||
return { options: optionsValue, emitted, billing };
|
||||
}
|
||||
|
||||
test("agent receives the exact server-owned case id for tool calls", async () => {
|
||||
let observedMessages: unknown[] = [];
|
||||
const agent = fakeAgentStream([
|
||||
chunk("start"),
|
||||
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
|
||||
chunk("tool-result", { toolName: "skill" }),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("text-delta", { text: "你好,我是生时校正助手。" }),
|
||||
chunk("finish"),
|
||||
]);
|
||||
const { options } = runOptions({
|
||||
action: "opening",
|
||||
message: null,
|
||||
buildAgent: async () => ({
|
||||
...agent,
|
||||
stream: async (messages: unknown[]) => {
|
||||
observedMessages = messages;
|
||||
return agent.stream();
|
||||
},
|
||||
}) as never,
|
||||
});
|
||||
|
||||
const result = await runV9AgentTurn(options);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.match(JSON.stringify(observedMessages), new RegExp(CASE_ID));
|
||||
});
|
||||
|
||||
test("first turn with no real skill evidence retries once then fails without saving success", async () => {
|
||||
const { options, emitted, billing } = runOptions({
|
||||
accounting: fakeAccounting({
|
||||
|
||||
@@ -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}'`,
|
||||
|
||||
Reference in New Issue
Block a user