fix: preserve queued chat ownership

This commit is contained in:
Shakker
2026-08-02 21:46:12 +01:00
parent a9393395d5
commit 53a3f6d902
4 changed files with 68 additions and 8 deletions
+1 -1
View File
@@ -683,7 +683,7 @@ describe("createSessionCapability", () => {
{ model: "openai/gpt-new" },
{ waitFor: priorPatch.promise },
);
expect(sessions.state.modelOverrides[key]).toBe("openai/gpt-new");
expect(sessions.state.modelOverrides[key]).toBe("openai/gpt-old");
expect(request).not.toHaveBeenCalledWith("sessions.patch", expect.anything());
publish(false);
+16 -6
View File
@@ -163,20 +163,29 @@ export function createSessionMutations(host: SessionMutationsHost) {
const hasModelPatch = Object.hasOwn(patchParams, "model");
const managesModelOverride = hasModelPatch && options.deferModelOverride !== true;
const normalizedKey = key.trim();
const pendingModelPatch = pendingModelPatches.get(normalizedKey);
const previousModelOverride = pendingModelPatch
? pendingModelPatch.previous
: host.readState().modelOverrides[normalizedKey];
let previousModelOverride: string | null | undefined;
let modelPatchStarted = false;
const modelPatchToken = Symbol();
if (managesModelOverride) {
const startModelPatch = () => {
if (!managesModelOverride || modelPatchStarted) {
return;
}
const pendingModelPatch = pendingModelPatches.get(normalizedKey);
previousModelOverride = pendingModelPatch
? pendingModelPatch.previous
: host.readState().modelOverrides[normalizedKey];
modelPatchStarted = true;
pendingModelPatches.set(normalizedKey, {
token: modelPatchToken,
previous: previousModelOverride,
});
setModelOverride(key, patchParams.model);
};
if (!options.waitFor) {
startModelPatch();
}
const restoreModelOverride = () => {
if (pendingModelPatches.get(normalizedKey)?.token === modelPatchToken) {
if (modelPatchStarted && pendingModelPatches.get(normalizedKey)?.token === modelPatchToken) {
pendingModelPatches.delete(normalizedKey);
setModelOverride(key, previousModelOverride);
}
@@ -189,6 +198,7 @@ export function createSessionMutations(host: SessionMutationsHost) {
return null;
}
}
startModelPatch();
const result = await requestSessionPatch(scope.client, key, patchParams, options);
if (!host.connection.isCurrent(scope)) {
restoreModelOverride();
+10 -1
View File
@@ -579,6 +579,10 @@ export async function deliverChatQueueItem(
const sessionKey = item.sessionKey ?? host.sessionKey;
const storageMode = options.storageMode ?? "durable";
const routingSessionKey = options.routingSessionKey ?? sessionKey;
const deliveryClient = host.client;
const deliveryConnectionEpoch = host.connectionEpoch;
const deliveryAgentId =
item.agentId ?? scopedAgentIdForSession(host, routingSessionKey) ?? undefined;
const sendOptions = { ...options, routingSessionKey, storageMode };
let result: QueuedChatSendResult;
if (storageMode === "memory") {
@@ -678,7 +682,12 @@ export async function deliverChatQueueItem(
host.chatAttachments = options.previousAttachments;
}
}
if (host.sessionKey === sessionKey) {
if (
host.client === deliveryClient &&
host.connectionEpoch === deliveryConnectionEpoch &&
host.sessionKey === routingSessionKey &&
visibleSessionMatches(host, routingSessionKey, deliveryAgentId)
) {
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
}
if (result === "sent" && host.sessionKey === sessionKey && !host.chatRunId) {
+41
View File
@@ -1835,6 +1835,45 @@ describe("handleSendChat", () => {
expect(patchCount).toBe(2);
});
it("rolls a failed queued picker back to the preceding slash model value", async () => {
const firstPatch = createDeferred<unknown>();
let patchCount = 0;
const host = makeHost({
requestHandlers: {
"sessions.patch": () => {
patchCount += 1;
if (patchCount === 1) {
return firstPatch.promise;
}
throw new Error("picker rejected");
},
},
});
host.sessions.setModelOverride(host.sessionKey, "openai/gpt-old");
const slash = patchChatSessionSettings(
host,
host.sessionKey,
{ model: "openai/gpt-5-mini" },
{
deferModelOverride: true,
reconcile: () => {
host.sessions.setModelOverride(host.sessionKey, "openai/gpt-5-mini");
},
},
);
await waitForFast(() => expect(patchCount).toBe(1));
const picker = patchChatSessionSettings(host, host.sessionKey, {
model: "openai/gpt-5",
});
expect(host.sessions.state.modelOverrides[host.sessionKey]).toBe("openai/gpt-old");
firstPatch.resolve(createResolvedModelPatch("gpt-5-mini", "openai"));
await expect(slash).resolves.toBeTruthy();
await expect(picker).rejects.toThrow("picker rejected");
expect(host.sessions.state.modelOverrides[host.sessionKey]).toBe("openai/gpt-5-mini");
});
it("keeps waiting when a late picker barrier cannot be persisted", async () => {
const queuedText = "do not bypass the late picker";
const history = createDeferred<unknown>();
@@ -7513,6 +7552,7 @@ describe("handleSendChat", () => {
limit: 100,
}),
);
const scrollGeneration = host.chatScrollGeneration;
if (change === "route") {
host.sessionKey = "agent:main:replacement";
@@ -7527,6 +7567,7 @@ describe("handleSendChat", () => {
expect(host.lastError).toBe("Replacement session error");
expect(host.chatError).toBe("Replacement session error");
expect(host.chatScrollGeneration).toBe(scrollGeneration);
},
);