Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit b99c446

Browse files
committed
fix(sessions): re-check queue membership after cloud edit normalization
updateQueuedMessage awaited resolveCloudPrompt before writing to the store. If the message drained during that await (a turn completed and sent it), the store update became a no-op but the method still returned true, so the caller treated the edit as saved and never fell back to sending it as a fresh message -- losing the edit. Re-read fresh session state after the await and return false if the target is no longer queued. Also read fresh state for the edit-hold clear, since the pre-await snapshot may be stale. Generated-By: PostHog Code Task-Id: 5cf31c6c-1ba0-4137-88c1-3a369f3acf36
1 parent 15b9e96 commit b99c446

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

packages/core/src/sessions/sessionService.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2728,6 +2728,13 @@ export class SessionService {
27282728

27292729
if (session.isCloud) {
27302730
const normalizedPrompt = await this.resolveCloudPrompt(prompt);
2731+
// Cloud normalization awaits, during which the message may have drained
2732+
// (a turn completed and sent it). Re-check against fresh state: without
2733+
// this, the store update below is a silent no-op yet we'd still report a
2734+
// successful save, so the caller wouldn't fall back to sending the edit
2735+
// as a fresh message and the edit would be lost.
2736+
const fresh = this.d.store.getSessionByTaskId(taskId);
2737+
if (!fresh?.messageQueue.some((m) => m.id === messageId)) return false;
27312738
const transport = this.d.h.getCloudPromptTransport(normalizedPrompt);
27322739
this.d.store.updateQueuedMessage(taskId, messageId, {
27332740
content: transport.promptText,
@@ -2739,7 +2746,10 @@ export class SessionService {
27392746
});
27402747
}
27412748

2742-
if (session.editingQueuedId === messageId) {
2749+
// Read fresh: the cloud path awaited above, so the pre-await `session`
2750+
// snapshot may be stale for the edit-hold decision.
2751+
const latest = this.d.store.getSessionByTaskId(taskId);
2752+
if (latest?.editingQueuedId === messageId) {
27432753
this.d.store.clearEditingQueuedMessage(taskId);
27442754
}
27452755
this.flushQueuedMessagesIfIdle(taskId);

packages/ui/src/features/sessions/sessionServiceHost.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5223,6 +5223,75 @@ describe("SessionService", () => {
52235223
});
52245224
});
52255225

5226+
describe("updateQueuedMessage cloud normalization race", () => {
5227+
const cloudSession = (
5228+
overrides: Partial<AgentSession> = {},
5229+
): AgentSession =>
5230+
createMockSession({
5231+
isCloud: true,
5232+
cloudStatus: "in_progress",
5233+
status: "connected",
5234+
isPromptPending: true,
5235+
messageQueue: [
5236+
{
5237+
id: "q-1",
5238+
content: "old",
5239+
rawPrompt: [{ type: "text", text: "old" }],
5240+
queuedAt: 1,
5241+
},
5242+
],
5243+
editingQueuedId: "q-1",
5244+
...overrides,
5245+
});
5246+
5247+
it("returns false when the message drains while cloud normalization awaits", async () => {
5248+
const service = getSessionService();
5249+
// Present for the initial membership check, gone for the post-await
5250+
// re-check (a turn completed and drained it during normalization).
5251+
mockSessionStoreSetters.getSessionByTaskId
5252+
.mockReturnValueOnce(cloudSession())
5253+
.mockReturnValue(
5254+
cloudSession({ messageQueue: [], editingQueuedId: undefined }),
5255+
);
5256+
5257+
const updated = await service.updateQueuedMessage(
5258+
"task-123",
5259+
"q-1",
5260+
"edited",
5261+
);
5262+
5263+
// No-op store write must not be reported as a save, so the caller falls
5264+
// back to sending the edit as a fresh message instead of losing it.
5265+
expect(updated).toBe(false);
5266+
expect(
5267+
mockSessionStoreSetters.updateQueuedMessage,
5268+
).not.toHaveBeenCalled();
5269+
expect(
5270+
mockSessionStoreSetters.clearEditingQueuedMessage,
5271+
).not.toHaveBeenCalled();
5272+
});
5273+
5274+
it("updates in place when the message is still queued after normalization", async () => {
5275+
const service = getSessionService();
5276+
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(
5277+
cloudSession(),
5278+
);
5279+
5280+
const updated = await service.updateQueuedMessage(
5281+
"task-123",
5282+
"q-1",
5283+
"edited",
5284+
);
5285+
5286+
expect(updated).toBe(true);
5287+
expect(mockSessionStoreSetters.updateQueuedMessage).toHaveBeenCalledWith(
5288+
"task-123",
5289+
"q-1",
5290+
expect.objectContaining({ content: expect.any(String) }),
5291+
);
5292+
});
5293+
});
5294+
52265295
describe("cancelPrompt", () => {
52275296
it("returns false if no session exists", async () => {
52285297
const service = getSessionService();

0 commit comments

Comments
 (0)