mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 09:08:46 +02:00
Assistant: drop mid-run MESSAGES_SNAPSHOT instead of reconciling it
Prod bug: earlier assistant replies vanished as new turns completed and only came back on refresh. Debug capture showed why: with persistence on, the run-end snapshot rebuilds history under STORE ids while the client holds it under STREAMED ids (MAF re-mints post-tool-call text ids, their #3619). ag-ui apply treats the snapshot as authoritative — matching ids get replaced (streamed text wiped by the store's empty-content toolCall entry) and the store's text copies were being filtered out by ReconcileSnapshotMiddleware as presumed duplicates, so prior answers had no surviving representation. Reconciling id-mismatched snapshots is unwinnable; skip them instead. During a normal run the client already built the turn from streamed events and the snapshot adds nothing. Hydration replays (empty input.messages) still pass through — there the snapshot IS the conversation, and every reload converges the client to store ids. ReconcileSnapshotMiddleware is replaced by the much simpler DropRunMessagesSnapshotMiddleware. Verified in dev: multi-turn history persists across runs, one card that survives follow-ups, reload replays everything, no duplicates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -250,31 +250,25 @@ class StripModelArtifactsMiddleware extends (Middleware as any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ReconcileSnapshotMiddleware ──────────────────────────────────────────────
|
// ── DropRunMessagesSnapshotMiddleware ────────────────────────────────────────
|
||||||
// MAF 1.10 reuses the streamed id for PLAIN-text snapshots, but still mints a
|
// MAF's run-end MESSAGES_SNAPSHOT re-mints ids for post-tool-call text (their
|
||||||
// fresh UUID for post-tool-call assistant text — deliberately, per their
|
// #3619) and, with thread persistence on, rebuilds history under STORE ids.
|
||||||
// issue #3619 ("separate message"). ag-ui's snapshot merge appends unknown
|
// ag-ui's apply treats the snapshot as authoritative: client messages absent
|
||||||
// ids, so tool+text turns still produce a duplicate assistant bubble without
|
// from it are dropped, matching ids are replaced. Because streamed ids and
|
||||||
// this reconciliation (re-verified live on MAF 1.10 / CK 1.62, 2026-07-04).
|
// snapshot ids never agree for tool+text turns, every mid-session snapshot
|
||||||
// Drops only the orphan text-only assistant message with no streamed
|
// either duplicates the current answer (fresh-id append) or wipes earlier
|
||||||
// counterpart. Caveat: interfered with a2ui retry runs (duplicated the user
|
// ones (store-id replace with empty content) — both observed live. During a
|
||||||
// message) — a2ui is disabled while that pipeline matures; revisit both
|
// normal run the client already built the exact turn from streamed events,
|
||||||
// together.
|
// so the snapshot adds nothing: swallow it. Hydration replays (empty
|
||||||
//
|
// input.messages) pass through untouched — there the snapshot IS the
|
||||||
// Thread persistence (2026-07-04) adds two exemptions: a hydration replay
|
// conversation, and the client converges to store ids on every reload.
|
||||||
// (empty input.messages) is passed through untouched — nothing in it was
|
|
||||||
// streamed this run — and messages whose id the client already sent in
|
|
||||||
// input.messages are history, not duplicates.
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
class ReconcileSnapshotMiddleware extends (Middleware as any) {
|
class DropRunMessagesSnapshotMiddleware extends (Middleware as any) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
run(input: any, next: any): Observable<any> {
|
run(input: any, next: any): Observable<any> {
|
||||||
const streamedTextIds = new Set<string>();
|
const isHydration =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
!Array.isArray(input?.messages) || input.messages.length === 0;
|
||||||
const inputMessages: any[] = Array.isArray(input?.messages) ? input.messages : [];
|
|
||||||
const isHydration = inputMessages.length === 0;
|
|
||||||
const clientKnownIds = new Set<string>(inputMessages.map((m) => String(m?.id ?? "")));
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return new Observable<any>((observer) => {
|
return new Observable<any>((observer) => {
|
||||||
@@ -283,38 +277,9 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
next: (eventWithState: any) => {
|
next: (eventWithState: any) => {
|
||||||
const event = eventWithState.event;
|
const event = eventWithState.event;
|
||||||
const type: string = event?.type ?? "";
|
if (event?.type === EventType.MESSAGES_SNAPSHOT && !isHydration) {
|
||||||
|
return; // client state from streaming is already correct
|
||||||
if (type === EventType.TEXT_MESSAGE_START && event?.messageId) {
|
|
||||||
streamedTextIds.add(String(event.messageId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === EventType.MESSAGES_SNAPSHOT && !isHydration) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const msgs: any[] = Array.isArray(event?.messages) ? event.messages : [];
|
|
||||||
const filtered = msgs.filter((m) => {
|
|
||||||
if (m?.role !== "assistant") return true;
|
|
||||||
const id = String(m?.id ?? "");
|
|
||||||
const hasText = typeof m?.content === "string" && m.content.length > 0;
|
|
||||||
const hasToolCalls =
|
|
||||||
(Array.isArray(m?.toolCalls) && m.toolCalls.length > 0) ||
|
|
||||||
(Array.isArray(m?.tool_calls) && m.tool_calls.length > 0);
|
|
||||||
if (
|
|
||||||
hasText &&
|
|
||||||
!hasToolCalls &&
|
|
||||||
!streamedTextIds.has(id) &&
|
|
||||||
!clientKnownIds.has(id)
|
|
||||||
) {
|
|
||||||
return false; // drop orphan text-only assistant duplicate
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (filtered.length !== msgs.length) {
|
|
||||||
observer.next({ ...event, messages: filtered });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.next(event);
|
observer.next(event);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -433,7 +398,7 @@ app.all("/api/copilotkit/*", async (c) => {
|
|||||||
if (process.env.CK_MIDDLEWARES !== "off") {
|
if (process.env.CK_MIDDLEWARES !== "off") {
|
||||||
agent.use(new SuppressRenderToolTextMiddleware() as any);
|
agent.use(new SuppressRenderToolTextMiddleware() as any);
|
||||||
agent.use(new StripModelArtifactsMiddleware() as any);
|
agent.use(new StripModelArtifactsMiddleware() as any);
|
||||||
agent.use(new ReconcileSnapshotMiddleware() as any);
|
agent.use(new DropRunMessagesSnapshotMiddleware() as any);
|
||||||
agent.use(new DeduplicateToolCallMiddleware() as any);
|
agent.use(new DeduplicateToolCallMiddleware() as any);
|
||||||
}
|
}
|
||||||
if (process.env.CK_DEBUG_EVENTS === "1" || process.env.CK_MIDDLEWARES === "off") {
|
if (process.env.CK_DEBUG_EVENTS === "1" || process.env.CK_MIDDLEWARES === "off") {
|
||||||
|
|||||||
Reference in New Issue
Block a user