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));