Fix duplicate initial messages after agent creation#1222
Conversation
Reconcile the optimistic first message with the first authoritative user message when providers replace the client-generated message id during new-agent handoff. This prevents duplicate prompt rows and stale pending turn UI in both native and gateway-backed Codex sessions. Preserve pending image and attachment merging across provider-assigned message ids, and cover the reconciliation boundary with focused tests that avoid matching later repeated prompts.
|
| Filename | Overview |
|---|---|
| packages/app/src/utils/pending-create-images.ts | New utility providing ID-then-text-fallback message matching and idempotent image/attachment merge; reference-equality short-circuits are correct throughout. |
| packages/app/src/utils/pending-create-images.test.ts | Comprehensive unit tests covering ID match, text-based fallback, no-overwrite guards, and the repeated-later-message edge case for findPendingCreateUserMessageIndex. |
| packages/app/src/panels/agent-panel.tsx | Adds optimistic stream display and image merge effect; streamItems is sourced directly from agentStreamTail so the setAgentStreamTail updater is consistent. The redundant streamItems entry in the useEffect dependency array causes extra no-op effect invocations but no incorrect behavior. |
| packages/app/e2e/bottom-sheet-reopen.spec.ts | Retry loop (up to 3 × 10s) with caught intermediate assertions; final hard assertion outside the loop still catches the regression case. Acceptable flakiness fix. |
| packages/app/src/screens/settings/providers-section.test.tsx | Trivial fix: adds missing modelGateways: {} field to the test helper to satisfy the updated MutableDaemonConfig type. |
Sequence Diagram
sequenceDiagram
participant User
participant AgentStreamSection
participant CreateFlowStore
participant SessionStore
User->>CreateFlowStore: "setPending (text, images, lifecycle=active)"
CreateFlowStore-->>AgentStreamSection: pendingCreate (via useCreateFlowStore)
Note over AgentStreamSection: shouldUseOptimisticStream = true
AgentStreamSection->>AgentStreamSection: "optimisticStreamItems = [user_message w/ images]"
AgentStreamSection->>AgentStreamSection: "mergedStreamItems = [optimistic, ...streamItems]"
SessionStore-->>AgentStreamSection: agentStreamTail updated (authoritative msg arrives)
AgentStreamSection->>AgentStreamSection: "pendingCreateUserMessageIndex >= 0"
AgentStreamSection->>AgentStreamSection: "mergedStreamItems = streamItems (optimistic dropped)"
Note over AgentStreamSection: useEffect fires (canFinalizePendingCreate=true)
AgentStreamSection->>SessionStore: setAgentStreamTail (merge images into tail)
AgentStreamSection->>CreateFlowStore: markLifecycle(sent)
AgentStreamSection->>CreateFlowStore: clearPendingCreate(draftId)
CreateFlowStore-->>AgentStreamSection: "pendingCreate = null"
Note over AgentStreamSection: shouldUseOptimisticStream = false, streamItems now has merged images
Reviews (7): Last reviewed commit: "Harden bottom sheet reopen E2E close" | Re-trigger Greptile
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
| if (agentId && (hasPendingImages || hasPendingAttachments)) { | ||
| setAgentStreamTail(serverId, (previous) => { | ||
| const current = previous.get(agentId); | ||
| if (!current) { | ||
| return previous; | ||
| } | ||
|
|
||
| const merged = mergePendingCreateImages({ | ||
| streamItems: current, | ||
| clientMessageId: pendingCreate.clientMessageId, | ||
| text: pendingCreate.text, | ||
| images: pendingImages, | ||
| attachments: pendingAttachments, | ||
| }); | ||
| if (merged === current) { | ||
| return previous; | ||
| } | ||
|
|
||
| const next = new Map(previous); | ||
| next.set(agentId, merged); | ||
| return next; | ||
| }); | ||
| } | ||
| markPendingCreateLifecycle({ | ||
| draftId: pendingCreate.draftId, | ||
| lifecycle: "sent", | ||
| }); | ||
| clearPendingCreate({ draftId: pendingCreate.draftId }); |
There was a problem hiding this comment.
Image merge can silently fail before
clearPendingCreate is called
markPendingCreateLifecycle and clearPendingCreate run unconditionally after the setAgentStreamTail block. Inside that block, the updater returns early without mutating state when previous.get(agentId) is undefined. If that entry is absent at the moment the effect fires (e.g. the stream-tail map hasn't been populated yet even though streamItems already contains the authoritative message via a different selector path), images/attachments are silently dropped and the pending create is permanently cleared with no retry path. Guarding the markPendingCreateLifecycle / clearPendingCreate calls on a confirmed successful merge (or at least checking that merged !== current) would make the behavior safe-by-construction.
Summary
Test