fix: persist failed summaries and stop double-rendering memory mutations

Stamp the failed marker on the aggregated summary part server-side, mirroring
how ON_RUN_STEP_CLOSED already stamps runStepStatus. The SDK aggregator ignores
a complete event that carries no summary, so an errored round's partial deltas
were saved unmarked and a reload re-rendered the truncated text under
"Conversation summarized".

Skip memory attachments that carry a toolCallId in the message-level
MemoryArtifacts decoration. Those calls already render inline as a MemoryCall
card with the same key, value and outcome, so one mutation appeared twice.
Attachments with no originating call keep the decoration as their only surface.

Keep a reasoning-bearing group expanded when Open Thinking Dropdowns by Default
is on. The folded-in THINK part only renders inside the group body, which is
not even mounted while collapsed, so the preference had no visible effect until
the action group was opened by hand.

Give same-named place results unique keys. Several branches of one chain share
a name and often carry no identifier, so rows whose address, rating and map
target all differ collided on one React key.
This commit is contained in:
Marco Beretta 2026-08-28 22:06:38 +02:00
parent 13877d6978
commit f0f5bdd3a0
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
5 changed files with 71 additions and 6 deletions

View file

@ -785,6 +785,23 @@ function getDefaultHandlers({
handlers[GraphEvents.ON_SUMMARIZE_COMPLETE] = {
handle: async (_event, data) => {
aggregateContent({ event: GraphEvents.ON_SUMMARIZE_COMPLETE, data });
/**
* Stamped onto the aggregated part for the same reason as
* `runStepStatus` above: an errored round keeps whatever deltas it
* already streamed, and the SDK's aggregator ignores a complete event
* that carries no `summary`, so nothing records the failure. Without
* this the flag exists only on the live client message and a reload
* re-renders the truncated text under "Conversation summarized".
* Resolved through `stepMap` only, so a missing step degrades to the
* old behavior rather than marking an unrelated part.
*/
if (data?.error && contentParts) {
const index = stepMap?.get(data?.id)?.index;
const part = typeof index === 'number' ? contentParts[index] : undefined;
if (part?.type === ContentTypes.SUMMARY) {
part.failed = true;
}
}
await emitForJob({
event: GraphEvents.ON_SUMMARIZE_COMPLETE,
data,