fix(chat): a model switch on an unsaved new chat stays local (BUG-1259)
Independent Staging Quality Gate / validate (push) Successful in 14m48s
Independent Staging Quality Gate / publish (push) Successful in 3m46s

Since BUG-989 a new chat is saved on its first send, and that create already
carries the session's model. selectSessionModel still PATCHed the session
right away; with no row yet the route answered 404 and the composer showed
「云端同步失败」 although the local choice had taken effect. The selection now
returns before any PATCH for an unsaved empty consultation (same predicate as
the send path). Saved chats sync as before.

Full suite 4988 / fail 24, identical to 11333460; build keeps / Static.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-07 15:04:37 +08:00
co-authored by Claude Opus 5.5
parent b339b8aec3
commit 0ed45a4b3a
4 changed files with 36 additions and 3 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-10-07 — 新对话里换模型不再报「云端同步失败」(未上线)
- 新建对话还没发第一句时换模型,以前会报「云端同步失败」,其实模型已经换好。现在只在本机记下所选模型,发第一句时一起保存,不再报错(BUG-1259)。
## 2026-10-07 — 生时校正改从对话里开始,做完回到原来的对话(未上线)
- 首页不再有「生时校正」按钮(BUG-1247)。在对话里说「你说的那年其实没发生」「我出生时间是我妈估的」这类话时,agent 会在回答下面放一张卡,写着大约几题、几点;只是不认同某个解释时不会提。给家人档案问时会说暂不支持(BUG-1248)。
+5 -3
View File
@@ -16904,7 +16904,7 @@
## BUG-1259 | 新对话里还没发第一句就换模型,提示「云端同步失败」
- 状态:investigating(根因已确认,未修)
- 状态:fixed-pending-verify(真机复核前不标 resolved)
- 首次发现 / 最近更新:2026-10-07 / 2026-10-07
- 来源:产品 iPhone 真机截图(staging 首页,新对话未发消息时切到 gpt-5.6-luna)。
- 影响面:`use-session-management.ts` 的 `selectSessionModel` → `patchSessionModel` → `PATCH /api/sessions/{id}`;首页 / 新建的空对话。
@@ -16912,6 +16912,8 @@
- 触发条件:会话还没发过第一句(本地未落库的空咨询)时切换模型。
- 根因:BUG-989(2026-09-21)把「新建对话」改成只在本地创建、第一问时才 `POST /api/sessions` 落库;`selectSessionModel` 没跟着改,仍对这个还不存在于服务器的会话发 `PATCH`,路由查不到行返回 404「聊天记录不存在或已被删除」,前端包装成「云端同步失败」。本地选择本身已生效(第一问落库时 `model_id` 取自本地会话),报错是多余的,但会让用户以为没切成功。
- 复发自:无;BUG-989 漏改的调用点。BUG-989 的源码合同只锁了「新建无 POST / send 时 create」,没覆盖换模型。
- 修复:待定(候选:未落库会话换模型只改本地,不发 PATCH;第一问落库时带上所选模型——现有 create 已带)。
- 修复:`selectSessionModel` 在本地更新后,若会话仍是未落库的空咨询(与发送路径同一判据 `isUnsavedEmptyConsultation` 且不在 `cloudCreatedIds`),清掉该会话的同步失败标记后直接返回,不发 `PATCH`。第一问落库的 create 本来就写 `model_id: session.modelId`,所选模型随落库生效。已落库会话照旧同步。启动 / 回访时的模型下线回退(`applyWarmCatalog`)早已跳过未落库会话,未改。
- 验证:`session-list-filter.test.ts` 新增 1 条源码合同(守卫在任何 PATCH 之前、清失败标记并 return、create 写入 `model_id`);去掉修复后该测试失败、恢复后通过。全量 4988 项 fail 24,与 `11333460` 逐条一致;tsc 0、lint 0 error;build `/` Static,首屏 gzip 568,343 → 568,362 B。
- 防复发:上述测试;以后凡是对会话发 PATCH 的入口,先判断会话是否已落库(BUG-989 的「第一问才落库」)。
- 相关记录:BUG-989、BUG-928。
- 修复版本:—
- 修复版本:待发布
@@ -506,6 +506,14 @@ export function useSessionManagement(params: SessionManagementParams) {
if (!retryingFailedSync) updateSession(activeSession.id, () => nextSession);
setRequestError(null);
setComposerNotice("");
// BUG-1259: a new chat is saved on its first send (BUG-989) and that create
// already carries the session's model; there is no row to patch yet, so
// the choice stays local instead of reporting a sync failure.
if (isUnsavedEmptyConsultation(nextSession) && !cloudCreatedIds.current.has(nextSession.id)) {
modelSelectionVersions.current.delete(nextSession.id);
modelSyncFailures.current.delete(nextSession.id);
return;
}
try {
await modelPersistence.current.enqueue(nextSession.id, () => persistSessionModelSelection(
@@ -131,3 +131,22 @@ test("new chat stays local until the first send and does not reuse a draft row",
assert.match(persistBlock, /setRequestError/);
assert.doesNotMatch(persistBlock, /setDraft\(/);
});
test("switching the model on a chat not saved yet stays local and never patches (BUG-1259)", () => {
// BUG-989 saves a new chat on its first send; a PATCH before that hits no
// row (404) and was shown as 「云端同步失败」.
const start = management.indexOf("async function selectSessionModel");
const end = management.indexOf("\n }\n", start);
assert.ok(start >= 0 && end > start);
const body = management.slice(start, end);
const guard = body.indexOf("if (isUnsavedEmptyConsultation(nextSession) && !cloudCreatedIds.current.has(nextSession.id))");
const patch = body.indexOf("patchSessionModel(");
assert.ok(guard >= 0, "the unsaved-chat guard exists");
assert.ok(patch > guard, "the guard runs before any PATCH");
const guarded = body.slice(guard, body.indexOf("}", guard));
assert.match(guarded, /modelSyncFailures\.current\.delete\(nextSession\.id\)/);
assert.match(guarded, /return;/);
// The local choice is what the first-send create writes.
const create = management.slice(management.indexOf("const values = mode === \"create\""), management.indexOf(": {", management.indexOf("const values = mode === \"create\"") + 40));
assert.match(create, /model_id: session\.modelId/);
});