From 652657bf8efb238fe2163b9a09b741fa6fa548d2 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 27 Jul 2026 09:20:40 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=91=20fix:=20Key=20Prefix=20State=20to?= =?UTF-8?q?=20the=20Stream=20and=20Honor=20current=5Fmodel=20for=20Labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleared-prefix reset keyed on isResume, which skips exactly the case it was added for: a submission whose POST succeeded server-side but lost its response is retried, comes back resumed: true, and subscribes in resume mode even though it is a NEW generation. A previous generation's cleared state then survived into it, and incoming run steps and labels applied no offset against content that still held its retained prefix. The state is now keyed to the stream id, which changes with the generation and stays put across reconnects of one. activityModel now honors current_model. The options are documented as title-shaped and the titleModel fallback already excludes the sentinel, but the higher-precedence activity override passed the literal string through to getOptions and the provider, so an endpoint following that convention failed every label instead of using the agent model. --- client/src/hooks/SSE/useResumableSSE.ts | 18 ++++++++++++++---- packages/api/src/agents/activityLabels/host.ts | 11 ++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/client/src/hooks/SSE/useResumableSSE.ts b/client/src/hooks/SSE/useResumableSSE.ts index 6003b90c43..ce4a1ab85a 100644 --- a/client/src/hooks/SSE/useResumableSSE.ts +++ b/client/src/hooks/SSE/useResumableSSE.ts @@ -557,6 +557,9 @@ export default function useResumableSSE( * `editPrefixLength` must no longer be applied — by run steps or labels. */ const editPrefixClearedRef = useRef(false); + /** Stream the cleared-prefix state above belongs to, so it is dropped when + * the generation changes rather than when a subscribe happens to be live. */ + const prefixStateStreamIdRef = useRef(null); /** Removes the pending chip once its steer is injected (the inline content * part becomes the durable record), and records the id so a 202 ACK that @@ -699,11 +702,18 @@ export default function useResumableSSE( * A NEW generation starts with its retained prefix intact, so the * cleared-prefix state from a previous one must not carry over — the * hook outlives any single submission, and a later edited resubmission - * in the same chat would otherwise be dispatched with no offset and - * overwrite the content it kept. Reconnects pass `isResume`, so the - * state survives exactly where it should: within one generation. + * would otherwise dispatch with no offset and overwrite the content it + * kept. + * + * Keyed on the STREAM, not on `isResume`: a submission whose POST + * succeeded server-side but lost its response is retried, comes back + * `resumed: true`, and subscribes in resume mode despite being a new + * generation — so an `isResume` check skips the reset exactly when it + * is needed. The stream id changes with the generation and does not + * change across reconnects of one, which is the boundary that matters. */ - if (!isResume) { + if (prefixStateStreamIdRef.current !== currentStreamId) { + prefixStateStreamIdRef.current = currentStreamId; editPrefixClearedRef.current = false; } let { userMessage } = currentSubmission; diff --git a/packages/api/src/agents/activityLabels/host.ts b/packages/api/src/agents/activityLabels/host.ts index b03dae35bf..014a0bf1dc 100644 --- a/packages/api/src/agents/activityLabels/host.ts +++ b/packages/api/src/agents/activityLabels/host.ts @@ -180,8 +180,17 @@ export async function resolveActivityLabelModel({ providerConfig.customEndpointConfig, 'titleModel', ); + /** `current_model` means "the agent's model" for BOTH overrides. The + * activity options are documented as title-shaped, so an `activityModel` + * set to the sentinel must resolve the same way `titleModel` does — passing + * the literal through would send `model: "current_model"` to the provider + * and fail every label. */ + const activityModel = + activity.model != null && activity.model !== Constants.CURRENT_MODEL + ? activity.model + : undefined; const model = - activity.model ?? + activityModel ?? (titleModel != null && titleModel !== Constants.CURRENT_MODEL ? titleModel : (agent.model ?? agent.model_parameters?.model));