Sync upstream v0.8.8-rc1 into apro-deploy - #73
Open
busla wants to merge 358 commits into
Open
Conversation
…la#14232) * 💭 fix: Default GPT-5.6 Reasoning Requests to Responses API GPT-5.6 models reject function tools combined with `reasoning_effort` in /v1/chat/completions (400: "Function tools with reasoning_effort are not supported ... To use function tools, use /v1/responses or set reasoning_effort to 'none'"). Default `useResponsesApi` to `true` for GPT-5.6 models on the OpenAI endpoint when a reasoning effort other than 'none' is requested, unless the user explicitly set `useResponsesApi`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 💭 fix: Address GPT-5.6 Responses API guard review feedback - Skip the auto-switch for OpenRouter-backed OpenAI endpoint configs; OpenRouter keeps its own reasoning path - Respect `dropParams: ['useResponsesApi']` as an explicit opt-out so Chat Completions requests never carry a nested reasoning payload - Stop treating `dropParams: ['reasoning']` as dropping reasoning: `deleteConfigParam` only removes the nested object, so skipping the switch there would resend flat `reasoning_effort` and re-trigger the 400 - Reword docstring: reasoning without tools still works on Chat Completions; the default exists because tools bind after config time - Reconcile with danny-avila#14233: the Chat Completions mode/context omission test now opts out explicitly, and a new test locks in that the Responses API default carries reasoning mode/context in the nested reasoning object Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 💭 fix: Scope GPT-5.6 Responses API default to first-party OpenAI - Skip the auto-switch for custom gateway base URLs (reverseProxyUrl/ directEndpoint may expose only /v1/chat/completions); the Responses default now applies to canonical api.openai.com only - Skip when reasoningFormat is 'disabled': no reasoning payload is sent, so there is no tool incompatibility to avoid and the request stays on the configured Chat Completions path - Add coverage for custom gateway base URL, canonical base URL, and disabled reasoning format --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Danny Avila <danny@librechat.ai>
…tion Resume (danny-avila#14254) * fix: durable ask_user_question resume for ephemeral agents * 🤖 refactor: Drop chat.js resume hunks in favor of shared packages/api helpers * 🤖 fix: Normalize resume thinking param and replay modelLabel (danny-avila#14253 Bugs 1&2) * 🤖 fix: Preserve adaptive thinking display and effort across HITL resume * 🔤 style: Sort load.spec.ts imports (repo import-order) * 🤖 fix: Replay paused request body params on HITL resume (UI-form source of truth) --------- Co-authored-by: Danny Avila <danny@librechat.ai>
…y-avila#14260) * 💬 feat: Explain Steering & Queuing at Every User-Facing Surface Adds localized info affordances so users understand what steer / queue / interrupt do wherever the feature appears, using the app's existing norms (InfoHoverCard in Settings, TooltipAnchor in the composer, inline descriptions in menus). - Settings → Chat: an InfoHoverCard "?" beside the during-run action toggle explaining steer vs queue (matches every neighboring setting) - During-run send button: the hover action list gains a header and a one-line description under each action (steer / queue / interrupt), turning the menu into a self-explaining card - "Turn on steering / queueing" overflow entry: a description sub-line explaining the mode the user would switch to (the label alone doesn't say what steering/queueing means) - Steered in-thread message: a subtle "Steered" badge with a tooltip clarifying why a user message appears inside the assistant response - New English keys only (others automated) * 🔤 style: Sort SteerPart imports (repo import-order) * 🎐 refactor: Subtle "?" Info Affordances for Steering (Feedback) Reworked the info UI toward the app's "?" InfoHoverCard norm and away from always-on text / a loud badge: - Steered message: replaced the amber "Steered" pill with a subtle "?" InfoHoverCard in the header (the norm; muted, hover-reveals why a user message appears inside the response) - During-run send button: reverted the per-action description lines — the hovercard is already a hover-reveal affordance, so it stays a clean action list (labels + shortcuts) - "Turn on steering/queueing" overflow entry: reverted the description sub-line back to a clean menu item - Settings → Chat "While generating, Enter will…": keeps its "?" InfoHoverCard (the canonical, discoverable explanation) - Pruned the 5 now-unused i18n keys; kept com_nav_info_during_run_action and com_ui_steered_info
…ds` (danny-avila#14246) * 🐛 fix: Strip duplicate `system` from Bedrock additionalModelRequestFields Bedrock Anthropic presets bind the system prompt to the `system` model param. bedrockInputParser routes `system` into additionalModelRequestFields, then bedrockOutputParser promotes it back to the root as a known key without removing the copy. Bedrock Converse then sees `system` in both places and rejects the request ("The additional field system conflicts with an existing field"), which surfaces once context compression/summarization runs. Delete `system` from additionalModelRequestFields after promoting it to the root. `system` is the only leaked field that collides with a reserved top-level Converse field, so the fix is scoped to it and leaves other passthrough fields untouched. Clones before mutating to avoid touching the caller's input. Closes danny-avila#14029 * 🛡️ fix: Guard scalar additionalModelRequestFields before `in` check DocumentType permits scalar values (boolean/number/string), so a saved Bedrock preset/agent can carry a non-object additionalModelRequestFields. The new `system` cleanup used `'system' in amrf`, which throws TypeError on a truthy scalar. Guard with a typeof-object check to keep the prior tolerant behavior; the empty-check is left unchanged. * 🛡️ fix: Strip all reserved Converse fields from additionalModelRequestFields
…ouch (danny-avila#14272) The pin/options buttons on these items were hidden-until-hover on ALL pointers (invisible / opacity-0), making each item's rendering hover-dependent. On touch that triggers the iOS "first tap reveals the hover state, second tap activates" behavior, so selecting a model spec, model, or pinned/favorite agent took two taps. Gate the hover-reveal on hover CAPABILITY (the same fix danny-avila#13712 applied to message hover buttons): the control is visible/tappable by default and only hidden-until-hover on hover-capable pointers via [@media(hover:hover)]. On touch the item is no longer hover-dependent, so the first tap selects. - ModelSpecItem / EndpointModelItem: pin button reveal - FavoriteItem (sidebar pinned agents): options button + wrapper reveal
…4271) * 👀 fix: Reveal the Steered "?" on Message Hover/Focus The steered-message "?" InfoHoverCard sat on every steered message at rest. Wrap it like the message hover buttons so it stays transparent until the message is hovered (group-hover) or the trigger is focused (focus-within), keeping the thread clean. * 📱 fix: Keep the Steered "?" Visible on Touch (Codex) Plain opacity-0 hid the info affordance on touch entirely, with no hover path to reveal it. Gate the hidden-at-rest state on hover capability ([@media(hover:hover)]:opacity-0), matching the message hover controls: visible on touch, revealed on hover/focus on hover-capable pointers.
* fix: clear deleted conversation message caches * test: cover deleted chat cache cleanup * test: clarify deleted cache scenarios
…nny-avila#14279) * fix(agents): clarify ask question validation errors * fix(agents): narrow question failure detection * fix(agents): persist question validation failures * fix(agents): track question validation failures
…-avila#14289) * 🧹 fix: Drop Pending Composer Draft When Steering or Queuing A during-run submit takes the composer text into a steer or a queued item and clears the composer via the form's `reset()`. That clear is programmatic, so it never fires the `input` event `useAutoSave` listens on, leaving the autosaved draft (keyed under `PENDING_CONVO` for the duration of the run) behind. When the run ends, `useAutoSave` migrates a surviving pending draft onto the real conversation id and restores it into the textarea. The result: a queued message that was successfully auto-sent by the run-end drain immediately resurfaced as the composer draft, and persisted there under the conversation key across reloads. Consume the pending draft at the three composer-origin entry points (steer, queue, interrupt & send), mirroring the existing takeComposerFiles/takeComposerContext consumption helpers. Only a consumed submit clears it — a refused one (empty text, uploads in flight) leaves the draft intact. * 🔒 fix: Flush The Live Composer Value On Debounced Autosave Codex round 1: the 25ms debounced autosave captured the textarea value at event time, so a write scheduled just before a during-run steer/queue could land after the composer was consumed and cleared — rewriting the just-sent text back into the PENDING_CONVO draft and defeating the clear. Read the value at flush time instead. When the composer was cleared in the debounce window the pending write now removes the draft rather than resurrecting it, and an untouched composer saves exactly as before.
…vila#14291) `loadTools` contracts every `requestedTools` entry as `() => Promise<Tool>` and the loader relies on it via `validTool().catch(...)`. The `ask_user_question` factory was registered synchronously, and `createAskUserQuestionTool()` returns a `DynamicStructuredTool` directly, so the call returned a tool with no `.catch`, throwing `TypeError: validTool(...).catch is not a function`. The throw happens inside the loop before `Promise.all`, so it aborted the entire tool load for the turn, not just this tool: agents with `ask_user_question` attached ran with no tools at all.
Removing the last file from an agent's Context panel fired no request and the file reappeared on reload. `FileContext` mounts `FileRow` only while `fileCount > 0`, and `useFileDeletion` lives inside `FileRow` behind a 1s debounce whose unmount cleanup called `debouncedDelete.cancel()`. Deleting the last file drops the count to 0, unmounting `FileRow` and cancelling the delete the user had already confirmed. Removing a non-last file kept the row mounted and worked, which made the failure look erratic. The same cancel also dropped deletes in any panel closed within the debounce window. Flush the pending batch on unmount instead of cancelling it.
…danny-avila#14277) * 🖼️ feat: Return Sandbox Images From `read_file` as Viewable Artifacts The code-execution sandbox `read_file` path refused every image extension because it reads files via `cat` over codeapi's JSON `/exec` transport, which lossily corrupts non-UTF-8 bytes. The skill-file read path already surfaced images as artifacts; this brings the sandbox path to parity so an agent can actually see a chart/screenshot it reads. - `readSandboxImage` (process.js): a Python base64 reader over `/exec` with an in-sandbox size guard so oversize images never cross the wire; base64 is ASCII-safe where `cat` corrupts. - `handleSandboxImageRead` (handlers.ts): byte-integrity check (guards against a truncated `/exec` stdout), MIME resolved purely from the magic-byte sniff (extension only routes; a mislabeled non-image falls back to the bash hint), and graceful degradation on every failure mode. - Shared `buildImageArtifactResult` used by both read paths; the result's `artifact.content` image_url reaches the UI (tool-end callbacks save it as an attachment) and the LLM (SDK folds it into the model-visible message for Anthropic/OpenAI/Google). * ✅ test: Sync read_file code-only description assertions with image wording * 🛡️ fix: Harden sandbox image reads (regular-file guard, completeness check) Addresses Codex review on PR danny-avila#14277: - readSandboxImage now os.stat's the target and rejects non-regular files (FIFOs, sockets, /dev/* symlinks) via stat.S_ISREG, and bounds the read at limit+1 bytes — a device/FIFO can no longer stream unbounded into memory until the request times out. - handleSandboxImageRead validates completeness (not just the magic header): PNG must end with the IEND trailer and WebP's RIFF size must match the byte length, so a truncated/interrupted image degrades to the bash hint instead of being sent as a corrupt image_url. JPEG/GIF stay header-level (they can carry trailing metadata; a strict end-marker would risk false rejections). * 🩹 fix: Chunk sandbox image reads to fit the runner stdout cap Inlining any real image failed with "is an image file (.png) and cannot be read as text". Root cause: readSandboxImage base64-encodes the file to STDOUT, but the runner caps stdout at SANDBOX_OUTPUT_MAX_SIZE (1024 bytes by default) and SIGKILLs the job on overflow (status OL), truncating the JSON mid-base64. The parse then threw and the handler degraded to the binary hint. The in-sandbox MAX_BINARY_BYTES=5MB guard never fired because the *transport*, not the file size, is the real ceiling: a 5MB image needs ~6.8MB of stdout. Reproduced against a live MicroVM — a 186KB matplotlib PNG died with 'stdout length exceeded' at exactly the 65536-byte cap. Read the file in windows instead: each /exec pulls raw bytes at an offset and base64s only that slice, so every response stays under the cap regardless of how the runner is configured; the chunks are reassembled and verified against the sandbox-reported total. Verified end-to-end on a real MicroVM: 25KB and 186KB PNGs both round-trip byte-exact (sha256 match). Also: - Detect the truncation explicitly (status OL) and name the fixable cause (chunk size / SANDBOX_OUTPUT_MAX_SIZE) instead of "unexpected output". - Parse the LAST stdout line so a shell banner can't break the read, and include a stdout snippet when it genuinely is unparseable. - LIBRECHAT_CODE_IMAGE_CHUNK_BYTES (default 32KB) tunes the window. - Tests drive the real reader against a mocked /exec transport rather than mocking readSandboxImage, which is why the existing suite stayed green through this bug. * 🎯 fix: Cap sandbox inline images at 1MB, separate from skill-file reads The sandbox and skill-file image paths shared MAX_BINARY_BYTES (5MB), but their transports differ: skill files stream from storage, while sandbox bytes come back base64 over /exec stdout under the runner's output cap, so the reader windows the file and cost scales in round-trips (~160 at 5MB vs ~32 at 1MB). Nothing is gained by allowing more — vision providers downsample to ~1.5-2k px regardless, so multi-MB originals buy no fidelity while grinding through round-trips. Give the sandbox path its own MAX_SANDBOX_INLINE_IMAGE_BYTES (1MB), used for both the read cap and the over-limit message (which previously quoted 5MB while the reader enforced something else). Skill-file reads keep 5MB. Verified against a live MicroVM: a 186KB PNG round-trips byte-exact, and a 1.4MB file returns tooLarge in a single round-trip with zero bytes transferred, degrading to the existing bash_tool hint.
* feat(api): add low-noise Redis observability * fix(api): preserve Redis proxy constructor
…4308) * 💬 refactor: Anchor In-Flight Steers Above the Composer Mid-run steers were rendered in-thread at the tail of the streaming assistant message, at a guessed injection point, then swapped to the persisted STEER part at its real index once the server applied them. In-flight steers now render as message bubbles anchored above the composer, so the thread only ever shows what the server committed: - InFlightSteers: sending/pending steers as left-aligned bubbles with image previews and a cancel affordance, anchored above the composer box - PendingSteerChips: unchanged, still owns the failed/queued control rows - SteerPart: drops the pending/onCancel props, now only ever the server-applied part - useSteerCancel: the optimistic cancel + restore-on-error, lifted out of the deleted PendingSteers slot The steer state machine is untouched: the 202 ACK reconciliation, reconnect reseeding, and queue conversion all key off status, not render location. * 🎨 fix: Match In-Flight Steer Presentation to the Applied Part Codex review on 6a5f36f7ef. All three findings were real, and all three were the same underlying mistake: the anchored bubble hand-rolled presentation instead of reusing the leaves the applied SteerPart uses, so a steer visibly changed on apply. - Images: the message `Image` sets an inline height from the file's dimensions and centers with object-contain, so clipping it into a 56px wrapper showed the blank top of a large element. Use ImagePreview, the composer's fixed-size thumbnail path (also gives click-to-enlarge). - Non-image files: FileContainer always renders a button, so without an onClick the chip was dead. Wire FilePreviewDialog, as SteerPart does. - Markdown: honor enableUserMsgMarkdown so text does not reflow the moment the server injects it. Splits files in a single pass rather than two filters. * 🎨 style: Outline the In-Flight Steer Bubble and Move the Bolt Inline The filled bubble read as a settled message. An outline reads as provisional, which is what an in-flight steer is, and separates it from the composer surface behind it. - Border + bubble keeps the composer's rounded-3xl radius so it reads as anchored to the input rather than floating over it. Border stays NEUTRAL: the failed-steer row already owns a colored (red) border, so a colored outline on the happy path would read as a warning. - The Zap moves inside the bubble, left of the text, where it prefixes the words as a status label instead of competing with cancel for the right edge. items-start pins it to the first line when text wraps. - Cancel drops plain `opacity-0` for `[@media(hover:hover)]:opacity-0`, matching SteerPart's info affordance: a hover-revealed control is unreachable on touch until a first tap (the danny-avila#14272 pattern).
…ila#14310) * 🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack Codex review on 9594ee7. Three valid P2s, all fallout from moving the steers out of the message region into the composer. - Run scope: the in-thread slot was gated on `effectiveIsSubmitting`, but the new one only checked `steering.enabled` (= steerable endpoint + primary composer), which is true with no run in flight. A chip that outlives its run — cancel's onError restoring one the final event already converted to a queued follow-up — stranded a bubble above the composer, possibly beside the queued row for the same text. Restores the run gate. - Height cap: a steer runs to 16k chars (DEFAULT_STEER_MAX_LENGTH) and a run takes up to 10 (STEER_QUEUE_MAX_DEPTH). Unbounded in the composer, that pushes the input off-screen; the old slot could grow freely because it scrolled with the thread. Caps the stack at 35vh. - Code execution: MarkdownLite defaults `codeExecution` on, but this bubble renders outside MessageContext, so Run Code would fire the tool mutation with no messageId and an empty conversationId. Passes codeExecution={false} — a provisional steer has nothing to run against. * 📜 fix: Keep the Newest In-Flight Steer in View Codex review on de9ede2. Valid, and a regression from the 35vh cap in the previous commit: steers append newest-last, so once the stack overflows it sits scrolled to the OLDEST entry. The steer just submitted — and its cancel control — lands below the fold and reads as dropped. The cap traded "composer pushed off-screen" for "newest steer hidden". Sticks the stack to the bottom, keyed on the newest steer id so it fires when one is appended rather than on every render. * 🧹 fix: Don't Restore a Steer That Already Settled Codex review on 09c9398. Valid, and it closes the hole the run gate only hid — I deferred this two rounds ago as pre-existing, which was wrong: the gate hides a stale entry while the run is idle, but useQueueDrain auto-sends the queued follow-up, isSubmitting flips back to true, and the previous run's entry renders as an in-flight bubble beside its own queued copy. Fixes it at the source instead: cancel's onError no longer restores a steer whose id is in appliedSteerIdsByConvoId — the settled set, stamped by both the apply path and the run-end conversion, and deliberately capped rather than cleared so it survives run end for exactly this race (same instrument as danny-avila#14276). The run gate stays: it's parity with the in-thread slot's effectiveIsSubmitting and still defends against any other leak.
…ny-avila#14312) danny-avila#14272 gated the hover-revealed "..." button on hover capability, but pinned agents still take two taps on iOS. That fix was aimed at the wrong mechanism for this list. Every favorite row is wrapped by DraggableFavoriteItem, and react-dnd's HTML5Backend stamps `draggable="true"` on that wrapper unconditionally (connectDragSource, HTML5BackendImpl.js:101 — `canDrag: false` does not suppress it, react-dnd#2909). iOS Safari hands a touch on a draggable element to the drag recognizer rather than synthesizing a click, so the row underneath only selects on the second tap. The draggable wrapper is what separates favorites from every other sidebar row. Conversation rows are more hover-dependent than favorites ever were (ungated `opacity-0 group-hover:opacity-100` plus an onMouseEnter that mounts ConvoOptions) and select on the first tap. Connect the drag source only under `(hover: hover)`. Nothing is lost on touch: HTML5Backend has no touch support, so drag-to-reorder never worked there. Passing null to the connector unsubscribes cleanly and resets the attribute, so a hybrid pointer flipping the query re-arms drag.
…-avila#14313) `RedisEventTransport.emitChunk` awaited `INCR` (sequence allocation) and then `PUBLISH` as two sequential round trips, per streamed delta. Fold both into one Lua `EVAL` that allocates the sequence and publishes server-side. Measured 50.6% reduction on the emit path (0.219ms -> 0.108ms per delta, 2000 deltas, loopback Redis). The saving multiplies by the token count of every response, and scales with RTT: on setups where Redis sits behind a network boundary (WSL2 loopback, cross-host, cross-AZ) at ~1-2ms/RTT this is ~0.5-1s on a 500-token response. The sequence is spliced into the payload server-side rather than round-tripped through cjson, which would coerce empty arrays to objects and alter float precision. The channel is passed as ARGV rather than KEYS: ioredis applies `keyPrefix` to EVAL keys but never to a pub/sub channel, so keying it would publish to a prefixed channel no subscriber listens on. PUBLISH is broadcast cluster-wide rather than slot-routed, so it needs no key for Cluster correctness. Also parallelize `getResumeState`'s three independent job-store reads (`getContentParts` / `getRunSteps` / `peekSteers`), collapsing 3 round trips into 1 on every resume. Safe despite `readCachedGraph`'s cache-drop side effect: each call catches its own unusable-graph throw and falls back to durable reconstruction, so ordering cannot change the result. `createMockPublisher` gains an `eval` that delegates to its own incr/publish mocks, keeping the existing error-propagation tests meaningful now that sequence allocation and publish are one operation.
* fix: harden request-scoped MCP lifecycle * chore: sort MCP integration test imports * fix: preserve MCP activity across connection replacement
…ncy (danny-avila#14262) Co-authored-by: anmol-kumar-us <anmolsrivastav.lw@gmail.com>
…4321) * ✋ feat: Add Reclaim-Gated Controls to In-Flight Steers Give a pending steer the same controls as a queued message — edit, convert to queue, and the during-run mode toggle — instead of only a bare cancel. Every re-homing action reclaims the steer from the server queue first and acts only on a confirmed `removed: true`. A steer leaves that queue only by injecting, so a lost race means the words are already in the run: queueing or editing them then would say the same thing twice. - Return a `SteerCancelOutcome` ('reclaimed' | 'applied' | 'failed') from useSteerCancel so callers can distinguish "the words are still mine" from "already injected" and "unknown fate" — the last two only toast. - Extract RowMenu, useDefaultToggleEntry, and the shared button classes into SteerMenu so both during-run surfaces use one implementation. - Offer controls only once `pending`: a `sending` steer has no server id to reclaim with, so its words cannot be held back. - Pin the control cluster visible while its menu is open — the portaled items hold focus outside the subtree, so `focus-within` alone would drop it. * 🩹 fix: Address Codex Findings on Reclaimed-Steer Controls Route a reclaimed steer through the shared conversion, and stop the async reclaim from stranding items or clobbering a composer that moved on. - Queue a reclaimed steer via useSteerConvert instead of enqueue, so it keeps its original id and createdAt. enqueue minted a fresh v4/Date.now() and appended, so a steer accepted BEFORE a later follow-up drained after it — breaking the invariant the leftover-steer path documents. - Submit the item directly when the run ended during the reclaim round-trip: the drain consumes its one-shot signal against an empty queue, so nothing was left to auto-send it. Read run state and conversation from refs, since the reclaim resolves after the bubble unmounts. - Refuse the composer restore when the origin conversation no longer matches or a newer draft is present, and queue the words instead of overwriting them. Neither text is the one to throw away. - Split useSteerReclaim (POST only) out of useSteerCancel, so the menu actions leave the chip alone until the outcome is known while the X stays optimistic. * 🛡️ fix: Harden Reclaimed-Steer Guards Against Stale State Both guards from the previous round read values that had moved on by the time the reclaim resolved. - Compare the origin conversation against a ref, not the closure. The `.then` holds `restoreReclaimedSteer` from the render it was clicked in, so its captured `conversationId` is the OLD chat — the guard compared that against itself and passed, while `methods` (one form, reused across conversations) wrote the steer into the chat now on screen. - Gate the direct send on the drain's own rule. `!isSubmitting` also covers a Stop or an error, so converting and then pressing Stop auto-sent the text past useQueueDrain's completed-or-armed-interrupt-only rule. Capture the run's outcome before the drain consumes the one-shot signal, and send only on a clean completion of THIS conversation. * ♻️ refactor: Re-Arm the Drain Instead of Direct-Sending Reclaimed Steers The direct send was the wrong mechanism: it re-implemented the drain badly, and each round of review found another rule it had skipped. Delete it and let useQueueDrain do the sending — it already owns every one of those rules. - Re-post the spent run-end signal under the conversation instead of calling sendNow. The drain then applies the completed-only rule, FIFO order (an older follow-up is no longer skipped), NEW_CONVO migration, and submits via `ask` — which, unlike the composer's sendNow, does not reset the form and so cannot wipe a draft typed while the reclaim was in flight. - No-op when a signal is still armed: that drain has not run yet and will see the item on its own, so arming a second carrier would send twice. - Watch the parked run-end too, not just the index one, so a run that ended while the user was in another chat is still seen. - Treat staged files, quotes, and skill picks as a draft when deciding whether a restore may overwrite the composer — editToComposer MERGES into them, so restoring over staged context would glue two submissions together. * 🎯 fix: Scope the Re-Arm Suppression to This Conversation The no-double-arm guard treated ANY armed index run-end as proof the drain would see this conversation's newly queued item. The index slot is shared: useQueueDrain parks a foreign signal under its own conversation and then inspects only the active one's queue, so a reclaimed steer sitting behind an unrelated run-end would never be looked at and would strand until sent by hand. Suppress only when the armed index signal belongs to THIS conversation — which is the case where the drain really will see the item. The parked check was already conversation-scoped by its key. * 🧭 fix: Trust the Refs Only While They Describe This Chat useSteering is reused across conversations, so after a navigation its live refs describe the NEW chat while the reclaim's callback still speaks for the old one. Restoring the conversation-identity guard I removed last round, which was wrong precisely because the refs are live but not conversation-scoped. - Skip the re-arm entirely once conversationIdRef no longer matches the steer's conversation. Reading isSubmittingRef there could suppress a needed re-arm, and lastRunEndRef could hold the NEW chat's run-end — parking that under the old conversation would make drainNext (which keys off end.conversationId) drain the wrong queue into the wrong chat. - Assert lastRunEnd.conversationId matches before re-arming, so the invariant is enforced where it is relied on rather than inferred from render order. Nothing is lost by stopping: the item is already queued under its own conversation, and that run's end parks under it and drains on return. * 🗝️ fix: Key the Captured Run-End by Conversation A single run-end slot could only answer for whichever chat was on screen when a reclaim landed, so the guard had to bail on navigation — stranding a steer whose run had already completed, contrary to "queue for after the response". Key the captured run-ends by conversation instead. The stored end always speaks for the chat the words belong to, so navigating away no longer suppresses the re-arm, and another chat's end can never be parked under this one (which would hand drainNext a foreign end.conversationId and drain the wrong queue). - Drop an entry when its conversation starts another run: a superseded end must not authorize a drain of the run now in flight. This replaces the isSubmitting guard, which described the wrong chat after navigation. - Remove conversationIdRef, now that no read depends on where the user is. * 🧹 fix: Close Three Reclaim Races Around Answer Mode and Run End - Refuse the composer restore while answer mode is active. `onSubmit` hands composer text to `answerMode.submitText` before any send/steer routing, so a restored steer would become the tool's answer on the next Enter. Read through a ref: the run can pause on ask_user_question mid-reclaim. - Skip the restore when a terminal conversion already queued the words. The chip stays interactive during the reclaim round-trip, so a run ending or erroring meanwhile converts it — restoring after would leave one copy queued and another in the draft. The queue action needed no guard; the conversion already dedupes by id. - Carry quotes/skill picks on the reclaimed steer itself. The conversion recovers them from the chip, which a competing X can delete mid-round-trip, silently dropping the picks. * 🎛️ feat: Fold Cancel Into an Always-Visible Steer Menu Make the in-flight steer bubble a single, discoverable affordance instead of two hover-hidden ones, matching how Codex/ChatGPT present the same control. - Fold Cancel into the ⋯ menu as an item (X icon), removing the standalone X button. It keeps the optimistic `useSteerCancel` path — no reclaim gate, since cancel drops the words rather than re-homing them. - Show the ⋯ at rest on every pointer instead of hover-gating it. A label-less menu hidden until hover is undiscoverable on desktop and unreachable on touch; always-visible also matches the queued rows' controls and drops the hover/focus/menu-open opacity juggling entirely. * 🪢 feat: Make Cancel and Queued Trash Non-Destructive Both removal actions now hand their text back to the composer instead of dropping it, so a message the user typed is never gone forever. - In-flight Cancel: before cancelling, restore the words to the composer via the gated `restoreReclaimedSteer` (skipped once applied — they are already in the response). The restore refuses on its own rather than clobber a draft, land in another chat, or fight answer mode; the cancel still runs reliably either way, so an unwanted steer stays killable. - Queued Trash: same safety net — thread the gated restore into the queued rows and return the words (with their carried quotes/skills) to the composer, then remove either way. Aligns the two surfaces on one behavior. - Export the shared `RestoreToComposer` type so both surfaces reuse it. Left the reliable-remove path intact (a steer sometimes must be killed before it reaches the model) and did NOT reach for a delete+Undo snackbar, which the shared Toast can't render without a cross-cutting action-button change. * 🔒 fix: Restore Cancelled Steer Text Only on a Reclaimed Outcome The cancel safety net restored the words to the composer synchronously, before the cancel POST resolved. On `applied` (cancel lost the race, steer still injects) or `failed` (POST errored, chip restored), the same text ended up both in the run/bubble and in the composer. Await `cancelSteer`'s outcome and restore ONLY on `reclaimed` — the one result that proves the steer never reached the run. `applied`/`failed` leave the words where the events place them, no composer copy. The gated restore still refuses rather than clobber a draft typed during the round-trip. * 🧵 fix: Never Drop Cancelled Text; Keep the Steer's Submit Time Two follow-ups on the cancel safety net. - Cancel no longer silently drops the words when the reclaim succeeds but the composer refuses the restore (draft typed, answer mode, navigated). The chip is already gone, so queue them like Edit does — never lost, just re-homed — with the same toast. - Preserve the true submission timestamp across submitSteer's chip states. The ACK and failure chips reset createdAt to a LATER Date.now(), so a draft queued during the 202 round-trip could sort ahead of a steer submitted before it and drain out of order. Capture the submit time once and reuse it for all three. * 🚪 fix: Refuse Reclaimed-Steer Restore Into an Unmounted Composer A reclaim/cancel round-trip can resolve after ChatForm unmounts (left the route, closed the pane). Its refs still hold the origin conversation, so `restoreReclaimedSteer` passed its checks, wrote into a dead form, and returned true — making the caller drop the steer instead of queueing it, losing the text. Track mount state and refuse the restore once unmounted, so the caller queues the words (recoil is global, so the queued chip survives the navigation).
…ctly (danny-avila#13930) * fix: stop agents from over-using the web_search tool The web_search tool context injected into the agent system prompt was framed action-first ("execute immediately without preface") and never told the model when NOT to search. Models treat this as a default-on signal and search on nearly every turn, across all providers (Anthropic API, OpenRouter, etc.) since this instruction is provider-agnostic. Reframe the instruction so the default is to answer from the model's own knowledge, gate searching behind a concrete trigger list, and explicitly reject the "a search might return relevant results" rationalization. Citation-format guidance is unchanged. * refactor: make web_search guidance concise and neutral Address maintainer feedback to keep the change minimal and generalized. Replace the verbose trigger list with a single neutral sentence: search when the user's request calls for it or when current/external information is required, and otherwise answer directly. Demote "execute immediately without preface" so it applies once a search is warranted rather than as the default, which was being read as "search on every turn."
… log (danny-avila#14346) When OPENID_REUSE_TOKENS is enabled and both the openidJwt strategy and the HS256 jwt fallback fail, the final 'Authentication failed after all strategies' warn log reported only the fallback's reason. For an RS256 provider (Keycloak, Auth0, Okta) that surfaces as 'invalid algorithm', which is the HS256 fallback rejecting the provider token, not the real reason openidJwt did not authenticate, and it was previously only visible at debug level. Include the captured primary (openidJwt) failure reason and error name in the final warn log so reused-token failures are diagnosable without enabling debug and are not misattributed to the fallback. Refs danny-avila#14311
…Vite `optimizeDeps` (danny-avila#14354) Enhance the Vite configuration by including specific node polyfills in the `optimizeDeps` section. This change ensures that the necessary polyfills for `buffer`, `process`, and `global` are optimized for better performance during development. This adjustment aims to improve compatibility and streamline the build process.
* fix: aggregate empty MCP tool logs * fix: retain server names in MCP tool logs
…anny-avila#14774) `AgentClient.recordTokenUsage` had no `transactions` parameter, so the setting never reached `createTransaction`, whose guard reads it from the object it is handed. `transactions?.enabled === false` saw `undefined` and the write went ahead. This path is reached only from `BaseClient`'s fallback branch, when the provider returns no usable stream usage, so the bulk path masked it wherever usage is reported. Where it is not, the setting had no effect at all.
…avila#14778) * 🛠️ refactor: Enhance console color handling and improve deleteNodeModules function * refactor: Use coloredConsole for consistent console output in invite-user.js * refactor: Convert year to string format in invite user payload
…y-avila#14779) Follow-up to danny-avila#14774. Its tests cover `AgentClient.recordTokenUsage` in isolation, so the `BaseClient` half of the fix was unpinned: deleting the `transactions` property from the call site restored the bug with the suite still green. These cases drive `sendMessage` with a real app config on `req` and assert the resolved value reaches `recordTokenUsage` — disabled, the default when no config is present, and the balance-enabled override that force-enables it. Each fails if either half of danny-avila#14774 is reverted. They also isolate `options.endpoint` for the block. `options` is shared across this file, and an endpoint left behind by an earlier case routes the balance-enabled arrangement into `checkBalance`.
* fix: attach request-scoped MCP servers * fix: satisfy MCP static checks * fix: format MCP runtime hint
…ny-avila#14768) * 🧭 fix: Finalize Parent Activity Phases at Run Completion * 🧭 fix: Preserve Activity Phase Boundaries * 🎨 fix: Format Activity Phase Boundary Check * 🧭 fix: Ignore Late Label Artifacts at Phase Completion * 🧭 fix: Preserve Logical Activity Phase Membership * 🩹 fix: Narrow Optional Activity Phase Marker * fix activity phase tail boundaries * fix activity phase test lint * fix straddling activity phase batches * preserve activity phase boundaries at scale * fix persisted activity phase final boundary * fix resumed activity phase edge cases * fix sparse activity phase grouping * fix sparse activity phase tail scan * fix resumed activity phase text fallback * fix sparse activity phase completion scans * avoid sparse activity phase runtime scans * stabilize sparse activity phase resumes * support activity phases on current ts target * preserve sparse phase reservations * finalize activity phase boundary handling * avoid sparse phase start scans * fix activity phase final text bounds * tighten activity phase summary boundaries * format activity phase boundary checks * leave final commentary outside activity phases * recognize lane-tagged final activity text * rebase retained activity boundaries on resume * bound activity phase collection work * correct resumed phase activity count * resolve late reasoning before phase completion * preserve lane-tagged final answers * assert durable activity phase bounds in e2e * preserve empty finalized activity phases * ignore empty reasoning at phase completion * format phase completion guard * fix(api): retain overflow reasoning anchors * perf(api): index overflow reasoning anchors * perf(api): skip empty reasoning index scans * fix(api): reconcile completion boundaries efficiently
…nny-avila#14777) * 📱 fix: Show Quote Popup for Block Selections and on Touch Devices The "Add to chat" popup never appeared for two whole classes of selection. Block-granularity gestures (triple-click, double-click then word-drag) park the selection's far boundary at the start of the next block. For a message's closing block that boundary sits outside `.message-render` — on the composer wrapper or the following message row — while selecting no text there, so the anchor/focus equality check suppressed the popup. Triple-clicking any earlier paragraph worked, which is what made this look like an edge case. The range is now clamped to the message before the check, and selections that really do carry visible text from another message are still refused. Touch platforms could not reach the feature at all. A long-press, and every drag of the native selection handles, emits no mouse event whatsoever — only `selectionchange` — while the popup was shown exclusively from mouseup, dblclick and keyup. Showing now also hangs off a settle-debounced `selectionchange`, gated so an in-progress mouse drag still cannot flicker it. Accepting was broken independently: the tap is also the gesture that dismisses the selection, unmounting the button before `click` could land, so touch commits on `pointerdown` instead. The desktop mousedown path is deliberately unchanged, since preventDefault on `pointerdown` can suppress the compatibility mousedown that click depends on. Two UX consequences of the same code: scrolling re-anchors the popup rather than dismissing it on the first event (the chat auto-scrolls constantly while streaming, and a mobile URL bar collapsing fires resize), and touch selections place the button below the text, clear of the OS Copy/Share callout, with a 44px tap target. Covered by six e2e tests — three desktop, three on an emulated Pixel 5 with a real touchscreen — each verified to fail against the pre-fix build. * 🩹 fix: Address Review Findings and Repair the Scroll Specs The two failing e2e shards were a defect in the specs, not the component. `scrollMessages` reached for `.scrollbar-gutter-stable` with a document-wide query, but the nav and side panels carry that class too, so it could grab a sidebar list that never scrolls — 0px moved, and only in CI, where the nav renders differently. The scroller is now reached from the message itself, the way `MessageNav` does it. The specs also centre the selection first and nudge by a quarter of the visible height, so the gesture cannot scroll the selection clean out of view and then blame the popup for going with it. Review findings, all in `QuoteButton`: Visibility was tested against the window, but the list scrolls inside a bounded container, so text can sit clipped under the header or the composer while its un-clipped rect is still inside the window — leaving the popup floating over unrelated UI. It is now clipped to the nearest scrollable ancestor. Touch committed on the press, so starting a scroll on the button, or touching it and thinking better of it, still added the quote. The excerpt is captured on the press and committed on the release, and only when that release lands on the button, restoring the cancellation every button is expected to have. Commit on press existed because the tap dismisses the selection before `click` fires; capturing the text up front keeps that safe, and an in-flight press is no longer allowed to unmount its own target. A visible popup also described the previous selection for up to the settle window, so a tap while dragging a native selection handle queued the stale excerpt. It is dropped as soon as a differing selection starts settling. Finally, `viaTouch` survived from the last press into keyboard-driven selections on hybrid devices, which could flip the popup into the touch layout; keydown clears it. The cancel path is covered by a new touch spec, verified to fail against a commit-on-press build. * 🧵 fix: Reconcile Cancelled Presses, Widen Clipping, Steady the Scroll Specs Second review round, with one finding taken on trust and flagged rather than claimed as proven. A cancelled touch press could leave the popup backed by a selection that no longer existed. A press deliberately keeps the button alive through a collapsing selection so the release has a target to be judged against, but a cancel then dropped the press without ever honouring the collapse it had masked, so a later tap could add a dead excerpt. Ending a press without committing now rechecks the live selection and dismisses if it went away. Visibility now intersects every clipping ancestor of the message rather than stopping at the nearest. This one is precautionary, not a proven fix: the review that prompted it describes scroll containers *inside* a message (a wide table, a code block) shadowing the outer chat scroller, but the walk starts from the message element, so those are descendants and were never in the chain. Behaviour is unchanged in the current layout — a spec covering a table-cell selection passes identically with and without it — and it is kept only because intersecting the whole chain stays correct if the list is ever nested inside a further-clipped panel. The comment says exactly this. The scroll specs were the real instability. They now move the selection between two positions that are both on screen instead of nudging by a pixel count: blind nudges kept pushing it under the composer, where the popup correctly hides, and the chat's own auto-scroll made the landing spot unpredictable. They also target the opening paragraph, since the closing one is the last content in the conversation and cannot be carried upward from a list already at maximum scroll. The reply fixture gained a table so a selection inside a nested scroll container is exercised, and a spec covers the cancelled press. 15/15 pass locally. * 🪟 fix: Judge Quote-Popup Visibility From the Selection, on Both Axes Third review round. All three findings held up, and each now has a spec that fails without its fix. Clipping is now measured from the selection rather than from the message, and on both axes. A wide table or a long code line scrolls inside its own container — and `overflow-x: auto` makes the computed `overflow-y` auto, so it clips vertically too — which means scrolling it sideways carries the selected text out of view while the message never moves. Walking up from the message could not see those containers at all, and a vertical-only test could not see that motion. This supersedes the previous round's precautionary widening, which was kept without evidence; the evidence is now a spec that scrolls a table past its own selection. Publishing a settled selection also checks visibility. Nothing is tracked during the 300ms settle interval, so a scroll inside that window never reached the re-anchoring path, and the reading was published off-screen and then clamped into view — stranding the popup over unrelated UI. The cancelled-press spec now reproduces the ordering it describes. Collapsing the selection and cancelling in one synchronous block let the asynchronous `selectionchange` arrive after the press had ended, which is the ordinary path and passes either way; it now waits for delivery in between, so the collapse lands while the press is still masking it. Two other specs needed the same scrutiny: `toBeHidden` is satisfied by an element that does not exist yet, so the settle spec sits out the interval before asserting, and it scrolls just past the container edge rather than to the end of the conversation, because a violent scroll re-renders the messages and drops the selection for unrelated reasons. The reply fixture's table is now wide enough to overflow sideways. 17/17 pass, and each new spec was re-run against a build with its own fix reverted to confirm it fails there.
… the MCPManager Suite (danny-avila#14782) * 🧭 fix: Rebase Activity Phase Bounds Onto Compacted Content `filterMalformedContentParts` compacts the aggregator's content array — `Array.prototype.filter` skips holes and drops malformed tool calls — but a parent phase marker's `activity_start_index`/`activity_end_index` still address the pre-filter positions. The array is routinely sparse: the aggregator writes parts at provider-source indexes, so a model turn that emits no text before its tool calls leaves an empty slot. Every part after a hole therefore shifts left on persistence while the bounds stay put, so the stored phase claims the wrong range — the final answer is swallowed into the parent card and the marker's own slot is counted as a child. The in-run analogue (`rebaseActivityPhaseBounds`) already rebases after completion-time reshaping; the final compaction had no such step. Rebase the bounds as part of the compaction, mapping each bound to the number of retained parts ahead of it. The mapping is monotonic, so `start <= end <= markerIndex` survives, and an identity mapping leaves untouched arrays — and their marker objects — exactly as they were. Markers are copied rather than mutated so the caller's array keeps its own coordinates, which the live stream and the resume snapshot still address. Fixes the `activity-phases` e2e failure on dev and the same defect on the two resume persistence paths. * 🔌 fix: Stop Replacing the Env Module in the MCPManager Suite `MCPManager.test.ts` mocked `~/utils/env` with a factory that replaced the whole module. danny-avila#14780 then made `~/mcp/utils` read `ALLOWED_BODY_FIELDS` from that module at module scope, so importing `~/mcp/oauth` -> `handler.ts` -> `~/mcp/utils` evaluated `undefined.map(...)` and the suite died at import time. All 111 of its tests have been silently skipped since; the shard has been red on dev, on this PR, and on release-v0.8.8-rc1. Spread the real module and keep only the mock that earns its place. `processMCPEnv` stays a seam: fifteen cases drive it with `mockReturnValue` / `mockImplementation` to hand the manager a specific processed config, and one asserts its call count, so making it real would couple these tests to env-substitution logic. `isPluginSourced` and `MCP_PLUGIN_SOURCE` were dropped — the factory restated the real implementations verbatim and no test referenced either, so they were duplication, not a seam. 111 tests now run and pass. * 🧪 test: Stop Replacing the Env Module in Three More Suites Same latent trap as the MCPManager suite: a `jest.mock('~/utils/env', ...)` factory that replaces the whole module. These three pass today only because their import graphs never reach `~/mcp/utils`, which reads `ALLOWED_BODY_FIELDS` from that module at module scope — the next module-scope constant added to `env.ts` would break all three the same silent way. Each mock is kept only where it earns its place: - `activityLabels/host.spec.ts` — dropped. `createSafeUser` was never referenced and the stub returned `undefined` where the real function returns `{}`, so the mock was strictly less faithful than the real, pure implementation. - `run-codeTools.test.ts` — dropped. Neither `resolveHeaders` nor `createSafeUser` was referenced by any case. - `run-summarization.test.ts` — `resolveHeaders` is now a spy wrapping the real implementation rather than an identity stub. One case asserts templated header values go through it, which only means something if the real substitution actually runs. `createSafeUser` dropped as unreferenced. 103 suites / 2708 tests green across `src/agents`, `src/utils`, and the MCPManager suite. * 📝 docs: Describe the Full Contract of filterMalformedContentParts Per Copilot's review: the public JSDoc still described the function as only dropping malformed tool calls, while the implementation also compacts empty slots and rebases parent activity-phase bounds. The detail lived on the private helper, so callers reading intellisense saw a stale contract. State what it actually produces, note that compaction is inherent rather than incidental (the aggregator writes at provider-source indexes, so the array is frequently sparse), and add an example of a hole moving a phase bound. The example was verified against the built runtime, not written from memory.
* fix(api): split phases at substantial text * tune(api): split phases after 200 text chars * fix(api): reanchor substantial text boundaries * test(api): type multi-phase payload captures * fix(api): preserve activity phase boundaries * fix(api): anchor retained phase partitions * fix(api): persist phase partition anchors * fix(api): harden activity phase boundaries * fix(api): preserve bounded phase partitions * fix: preserve final and delayed phase content * refactor(api): partition phase state at one boundary Boundary closure split fifteen separately-maintained fields by hand, and each fix partitioned one more while the next stayed unguarded. Fold the overflow bookkeeping into the tracked activity list so every counted activity carries a position, and route the split through a single partitionAt that returns both sides. Counts are now summed from the partition instead of reconstructed by subtraction, so a run past the anchor budget reports every activity it performed rather than the truncated window. Snapshots move to version 3; the reader still accepts versions 1 and 2 and rebuilds their unpositioned remainder as a bounded anchor, dropping it when its evidence is stale. Adds a boundary-conservation property covering every split point. * fix(client): drop empty phase content segments Late-child recovery can strip every index from a segment it already claimed, leaving a content segment with no parts. Each one still mounts a nested ContentParts that renders nothing, and it broke the exact-segment expectation in the late-child regression from 831a003. Route the four content pushes through one guard that skips index-less segments, matching the existing splice of fully recovered segments. * fix(api): keep the run's answer outside the collapsed phase The substantial-text boundary replaced completion's final-text boundary outright, so a short reply from a provider that emits no phase metadata was folded into the collapsed parent. That is the deterministic e2e failure at activity-phases.spec.ts:182 and codex's short-final-answer findings; 831a003 fixed only the path where the provider labels the step final_answer. Restore the completion boundary at the last materialized visible text whatever its length. Length now decides only whether intermediate text earns a boundary, and semantic commentary still stays inside. The "later work" check shares one predicate with partitionAt so the two cannot drift. Also splits a legacy v1/v2 remainder across the positions its saved tool anchors still materialize at, each carrying its own id so it can be located, and merges over-cap anchors by closest pair into the earlier position instead of folding the oldest forward. * fix(api): clear resolved anchors and keep folded agents Two findings from the latest review: A resumed activity whose tool was missing at construction kept its high fallback anchor after that tool materialized at a lower index, so the partition rejected it at any boundary below the stale value and pushed pre-boundary work into the following phase. Drop the anchor once every tracked call has materialized. Folding anchors past the cap spread only the surviving side, silently dropping the other's agent. close() now derives both marker attribution and the summarizer payload from the partitioned activities, so a merged anchor carries the union instead. Both regressions are mutation-checked against their own fix. * fix(api): anchor live batches awaiting materialization A batch tracked after its child-label slot is reserved but before its tool call reaches the shared array had no materialized position, and the partition read "nothing materialized" as "happened earlier". A boundary between the two then counted the batch in the earlier phase while ending before its eventual tool call, stranding the tool outside its parent. Record the tracked start as an unresolved anchor in that window so the existing retain branch keeps the batch on its own side. Using the plain fallback index instead is wrong: restored evidence-less activities carry a synthesized index, not a position. Regression is mutation-checked against its own fix. * perf(api): partition in one pass and reanchor filtered batches Dropping a batch's already-covered calls leaves a different activity behind, but the batch position was still the covered call's index. The survivor therefore inherited a position inside an emitted phase and was consumed by it instead of being held for its own. Re-derive the start from the retained ids, which also restores the unresolved-anchor signal when none of them have materialized. The boundary partition also classified every activity twice and rescanned retained ones to reanchor, walking the shared content array several times per activity per boundary. Build both sides in one pass with the materialized tool indices computed once and threaded into the predicate. Regression is mutation-checked; an earlier version of it was vacuous because the tool materialized before completion, converging both paths. * perf(api): carry tool indices through boundary resolution The previous pass cached the materialized indices only in the partition loop, so resolution still scanned for the batch start and again for the fully-materialized check, and an empty result triggered a third scan inside the boundary predicate. Walk the shared content array once per activity and carry the indices through resolution, classification, and reanchoring. findTrackedToolStart becomes its own first element and is dropped. * fix: trust rendered position over registration order Three findings from the latest review: Context partitioning only consulted the rendered index when the activity position tied the closing count, so a parallel lane registered before the closing tool hooks was assigned to the earlier phase despite rendering after the boundary. An activity position is registration order; a materialized index is proof, and now wins whenever it has one. Snapshot restore bound pending reasoning to the first part sharing its 80-character anchor, which could replay a still-pending lane on the earlier side of a boundary and delete it. An ambiguous anchor is treated as unresolved. Recovering the only filled child label out of a phase segment left its hasContent flag set, rendering an expandable card with an empty body. The context regression is mutation-checked against its own fix. * fix(api): decide context by proven position, both directions The previous change let a rendered index override registration order only when it proved the text was after the boundary, and trusted that index even when it was not provably this entry's. Both gaps were reachable. Context registered after work that already rendered ahead of it was retained despite rendering before the boundary, and a restored entry with no step id whose excerpt repeats after the boundary matched the later occurrence and moved to the wrong phase. Locating now reports whether the position is authoritative — anchored by a step index or a unique text match — and only then decides, in both directions. Otherwise the saved activity position stands. Each regression is mutation-checked against its own direction. * fix(api): carry unresolved positions through folded anchors Folding two anchors spread only the surviving side, dropping the later one's unresolved fallback. A boundary between them then saw just the earlier materialized tool index and closed the whole merged count, counting work whose tool call had not appeared and leaving that call outside its parent. Carry the later fallback into the merged anchor; resolution already clears it once every retained id materializes. Regression is mutation-checked against its own fix. * fix(client): keep phase headers recovery did not empty A completed phase can carry no children after compaction — its summary header is the whole segment. Recovery spliced any segment left with no retained indices, so a later marker deleted that header even though it recovered nothing from it. Only drop a segment recovery actually emptied, not one that arrived empty. Regression is mutation-checked against its own fix.
…-avila#14805) Bounded anchors were built in two places, each spreading one side and hand-picking the rest, so any field nobody named was dropped silently and nothing failed until a boundary landed badly. That already cost agentId and then unresolvedToolStartIndex in consecutive review rounds, and mergedAgentIds was never added to the demotion path at all. Both constructors now assign an AnchorFields literal, mapped over keyof Required<TrackedActivity>, so adding a field to TrackedActivity is a type error at both sites until its anchor semantics are decided. No behavior change: every field resolves to what the hand-picked versions already produced. Adds a folding property over count, failure count, agent attribution, and ordering.
…avila#14807) * refactor(api): make anchor construction exhaustive Bounded anchors were built in two places, each spreading one side and hand-picking the rest, so any field nobody named was dropped silently and nothing failed until a boundary landed badly. That already cost agentId and then unresolvedToolStartIndex in consecutive review rounds, and mergedAgentIds was never added to the demotion path at all. Both constructors now assign an AnchorFields literal, mapped over keyof Required<TrackedActivity>, so adding a field to TrackedActivity is a type error at both sites until its anchor semantics are decided. No behavior change: every field resolves to what the hand-picked versions already produced. Adds a folding property over count, failure count, agent attribution, and ordering. * refactor(api): resolve activity position once per boundary Positional fields on TrackedActivity are captured at different times against an array that keeps moving, so each was a cache that could go stale, collide, or be truncated — and closesBeforeBoundary read three of them directly. Roughly two thirds of the review findings on danny-avila#14785 were that pattern: a proxy outranking, outliving, or standing in for the rendered position. resolvePosition now folds tool indices, the prior partition floor, the unmaterialized fallback, and reasoning anchors into one value, and closesBeforeBoundary takes only that value. A caller cannot reach past it to a raw field, and the four branches the predicate used to carry collapse into one comparison: an activity closes early exactly when nothing locates it beyond the boundary. Resolution also decides when the saved fallback has gone stale, so the stripping that kept it out of snapshots is now a property of the resolved value rather than a separate step. No behavior change; the existing boundary and straddle regressions cover both directions.
* style: Unify message row layout and edit surfaces
Route chat, share, and search messages through a shared MessageRow so
user turns render as right-aligned bubbles and assistant turns keep a
visible identity column.
Replace per-part text editors with one edit surface that keeps tools,
errors, and artifacts visible. Preserve non-text fields when saving
content parts, copy the full serialized message, and hide hover actions
that do not apply during streaming or errors.
* style: Align edit footer and lighten editor field in dark mode
Drop the divider above the user edit footer so both edit surfaces share
the same footer treatment.
Move the editor fields to surface-tertiary-alt. Light mode is unchanged
at #fff, while dark mode lifts from #0d0d0d to #2f2f2f so the field sits
above the #212121 panel instead of sinking into near-black.
* style: Drop focus border and ring from message editors
The editor fields changed border color and added a ring on focus. Keep
the border static and rely on the app-level focus handling instead.
* fix: Keep a triggered message action visible when the row is not hovered
Hover actions fade out on non-last rows, and mobile.css only restored
display and visibility for an active button, never opacity. Opening the
fork popover therefore left it anchored to an invisible trigger once the
pointer left the row. Skip the fade entirely while a button is active.
Extract the recipe the three toolbars repeated so the rule has one home.
Rework the streaming guard to the contract the toolbar now implements:
edit and fork are omitted from a streaming response rather than rendered
disabled, and the settled turn above keeps its own actions. It asserted
the removed disabled-and-transparent behaviour and its opacity check only
held because the growing response shifted the row out from under the
pointer.
* style: Trim message edit chrome and stabilize the status row
The edit surface was a titled card sitting inside the conversation: a
bordered panel with an "Edit message" heading wrapping bordered fields,
which read as a settings dialog rather than an inline editor. Drop the
card background, border and heading, and take the footer buttons down to
the small size so the editor reads as a field in the message flow. The
captured row goes from 253px to 187px.
Move "Unsaved changes" into the footer and merge the rerun hint into the
same slot. Both previously added their own row, so typing pushed the rest
of the conversation down. The slot is clamped to two lines, which stays
under the 36px button row, so the footer height holds at 36px regardless
of which message is showing.
* test: Cover message edit layout stability
Add a mock e2e spec that measures the edit footer and section boxes and
asserts they hold steady as the status text appears, for both the
single-part user editor and a multi-part response.
The multi-part case needs an assistant message with two editable parts,
so add an E2E_THINK_REPLY marker to the fake model. Its think tags are
parsed downstream by the agents stream pipeline, which yields a reasoning
part followed by a text part.
* fix: Read the fork popover open state from its store
Fork mirrored the popover state into its own useState and reset it from an
onClose prop. Ariakit 0.4 has no onClose, and React's DOM types accept the
name on any element, so it type-checked, landed on a div and never fired.
Closing by Escape or an outside click therefore left the button reading as
active until the trigger was clicked again.
Read the state from the store instead so every close path clears it.
* fix: Keep the whole toolbar visible while an action is open
Only the triggered button escaped the hover fade, so opening the editor or
the fork popover left the row as a single floating button once the pointer
moved away. Mark the active button and have every action in the toolbar key
off it, so the group stays opaque for as long as a surface is open.
The marker is a dedicated class rather than the existing `active`, which
HoverButtons pins to the edit button of every assistant message and would
hold those toolbars open permanently.
The existing guard pressed Escape to close the editor while focus sat on the
body, so the editor never closed and its assertion only held because the
sibling faded regardless. Close the editor through its own control, and drop
focus before measuring the fade now that Escape returns it to the trigger.
* fix: Withhold copy while a response is still streaming
Text-to-speech, fork and feedback were all withheld from a message that is
still generating, but copy was rendered throughout, so the button offered to
put half a sentence on the clipboard. Gate it on the same condition.
That empties the toolbar for the duration, and SubRow collapses an empty row,
so a streaming response now carries no actions at all until it settles. Both
guards encoded the old contract: the unit test asserted copy was present and
counted a single button, and the browser guard used copy as its proof that the
toolbar had mounted. The settled turn above takes over that role.
* fix: Move retry navigation to the outer edge of a user turn
A user turn is right-aligned, but its sibling navigation rendered ahead of the
actions, so the retry counter sat inboard of the icons instead of under the
edge of the bubble it belongs to. Order it last on user turns.
* fix: Ride the stream instead of chasing it
Following a generating answer went through a helper throttled at 145ms, so the
thread caught up in visible jerks rather than flowing. It now writes the scroll
position directly on each frame, which is what an answer arriving a few pixels
at a time actually needs, and glides only for the one long trip a turn makes,
when sending has to travel from wherever the reader was down to the newest
word.
Whether to follow at all is now answered by where the reader is and which way
they were going, rather than by the abort flag. `useMessageProcess` raises that
flag on any wheel at all, downward ones included, through a throttle whose
trailing call lands after the gesture has ended, so nothing timed to the
gesture could outlive it. Scrolling down to the newest word could therefore
never resume the ride, while the scroll-to-bottom button, which touches no
wheel, always could.
Arrival is judged on the scroll it produces rather than the wheel tick that
started it, because wheel scrolling is animated and at tick time the thread is
still far short of where the tick is taking it. Arriving also counts from
further out than leaving does: while an answer streams the end recedes between
the last tick and the frame that measures it, so judging arrival as tightly as
departure leaves a reader unable to catch it at all.
* fix: Reveal retry navigation on hover while an answer generates
Copy, edit, fork and read-aloud are all withheld from a response that is still
generating, which left the retry counter as the only thing rendering under a
half-written answer. It now reveals on hover there, like the actions it sits
with, and stays put on a settled turn.
* fix: Keep a refused rerun from discarding the edit
While a response is streaming, the edit action stays available on every earlier
row, and those editors see a per-message submitting flag that is false, so
Update and rerun is enabled. The send itself is still refused: ask() returns
false for the duration of the active submission. Both editors ignored that and
closed anyway, so the draft went with them and no rerun ever started.
Both rerun paths now check the result and leave the editor untouched when the
send is refused, so the work survives until the thread is free.
* fix: Let an upward gesture beat the pending send glide
Sending arms a smooth glide down to the newest word, and the landing re-pins the
thread to the bottom. The landing was scheduled two ways, on scrollend and on a
700ms fallback, and neither was ever cancelled. A reader who changed their mind
and headed up mid-flight was pinned again regardless, then dragged back by the
next streaming resize. The fallback fires for the whole window, so this held even
after the glide had visibly settled.
The gesture now marks the glide interrupted, wherever it lets go of the bottom,
and the landing stands down when it sees that. A glide the reader leaves alone
still re-affirms the ride.
* fix: Fade retry navigation on every streaming response format
Every other action is withheld from the row that is still generating, so the
retry counter is the only thing left under a half-written answer. The plain text
row already faded it to hover-only there; the structured rows did not, and left
it sitting on its own.
Both structured paths now apply the same condition, and the class string the
three of them share moves next to the hover action styles it belongs with.
* i18n: Correct the copy the edit surface rewrite left behind
The multi-part hint told the reader to save first and then rerun, but a save
closes the editor and reopening seeds the drafts from what was just saved, so
there is nothing left to rerun and the button stays disabled. Rerunning carries a
single edited section by design, so the hint now states that limit rather than
pointing at a step that is not there.
Drop com_ui_save_submit as well: the per-part editor that used it is gone.
* test: Make the message visual baselines opt-in
The suite asserts sixteen screenshots and the repository tracks none, so
Playwright's default treats every one as a miss and the mock e2e job fails on
Linux. Baselines only compare cleanly against the machine that produced them, and
nothing here can generate ones that match the runner image.
The flows keep running and asserting their structure, which is where their value
was; only the pixel comparison is now gated behind E2E_VISUAL_SNAPSHOTS.
* style: Restore import order in the reworked message files
The repository sorter and CI disagreed with what these files were left holding
after the edit surface rework. No behavior change.
* test: Follow the reworded rerun hint in the edit layout spec
The multi-part hint was restated in the previous commit; this assertion still
expected the old wording and would have failed the mock e2e suite.
* fix: Leave the send glide alone while the answer streams in
Every delta of an answer reruns the scroll effect, and the plain follow writes
scrollTop outright, which cancels an animation on its first frame. So the glide a
send starts was killed by the first token to arrive and the reader was snapped
down instead of carried.
The follow now stands down while a glide is travelling, which is what the hook
already documented but only enforced on the resize path.
* fix: Write a saved edit onto the thread as it stands
An earlier turn stays editable while the newest answer streams, and the save
captured the thread before the request but wrote it back after. Every delta that
landed during the round trip was overwritten. Most of the time the next delta
re-merged and the damage showed as a one-frame truncation, but a save that
resolved after the stream's final write left the cache wrong for the rest of the
session.
The thread is now read once the request has resolved, which is what the content
part editor already did.
The editor actions in this file also wrap again rather than hold one unbreakable
row, for the reason given in the following commit.
* fix: Let the editor actions wrap on a narrow row
At 320px an assistant turn gives the editor about 252px once page padding, the
identity column and the row gap are taken out, and Cancel, Save and Update &
rerun need more than that in English alone. The group was pinned with shrink-0,
so it ran past the edge of the row instead of wrapping. A longer translated label
makes it worse, and the user turn had no margin left either.
Both editors wrap again, which is what the footer did before the status row was
folded into it.
* fix: Catch up to the new bottom when the glide lands
Following stands down for the length of the glide, so an answer that arrives
while it travels moves the bottom past the target the glide aimed at. A short
response that finished before the glide reported landing left the thread a few
lines short of its own end, with nothing left to correct it.
Landing now closes whatever gap opened, unless the reader took over on the way.
* test: Follow the renamed rerun button in the edit flow specs
The button became 'Update & rerun' when the edit surfaces were unified, but two
edit-flow specs still located 'Save & Submit' and would have waited for it until
they timed out. A type comment named the old button too.
* fix: Judge the first thread scroll against a real position
The direction check seeded its last-position ref at 0, so the first scroll
event on an opened thread, which arrives carrying a large positive
scrollTop, read as a jump downward. Near the end that cleared the abort
flag and re-pinned a reader to the stream they were scrolling away from.
Take the first event as a baseline and judge direction from the next.
* fix: Hold the content part editor to what it replaced
EditContentParts took over from EditTextPart and left two of its behaviors
behind.
An emptied box now blocks Save and rerun instead of persisting a blank
part. EditTextPart refused the same edit through its form's required rule
and the sibling EditMessage still does, so both editors hold one line. The
keyboard shortcuts reach the save paths directly, so they are guarded
there too, and the footer says why the buttons are down.
The editor also follows the chat direction again, taking dir and text
alignment from the same setting EditMessage reads.
* fix: Hold the footer height while a response streams
Every action is withheld from the row that is still generating, and a lone
sibling counter renders nothing, so the footer measured zero until the answer
landed and then sprang to the height of the buttons. The transcript stepped
upward under the reader at the moment a response completed.
The placeholder that used to reserve this space went when the footer became
unconditional, so hold the height on the row itself instead.
* fix: Remember where the thread was put before judging a gesture
Direction is judged against the last sample, and the thread is placed at its
end without the reader touching it. With no record of where it was put, their
first gesture was spent taking the baseline instead of being obeyed: a single
PageUp cleared no flag of its own, so the next streamed resize rode the reader
straight back to the end they were leaving.
Every programmatic move now records the position it left the thread at, so the
sentinel stands only until something has actually placed it.
* fix: Spend the start of a turn only once it can be honored
A reader who scrolls away during one answer leaves the abort flag raised, and
nothing lowers it until the next connection opens, which is after this effect
has already seen the send. Marking the turn as started on that first pass spent
it against a closed gate: by the time the flag cleared there was no start left
to honor, the reader was still detached, and the answer they had just asked for
streamed on offscreen.
Record the turn as started only on the pass that acts on it.
* fix: Show the part edits that survived a refused save
The editor saves every changed part through one button, but the endpoint
takes a single part per call and nothing rolls a write back. A part the
server refused therefore left the earlier ones stored while the editor
reported that the message could not be saved, so cancelling from there
walked away from edits that were already live.
Record the writes that landed and reconcile the transcript with them
whichever way the save ended. The refused parts are the only ones left
holding a draft, so a retry no longer rewrites what already arrived.
* fix: Stop a shared transcript from calling the sharer the reader
The share row reused the chat view's user label, which reads "You". It is
the screen-reader heading for the user turn, so anyone opening a share
link heard every prompt the sharer wrote credited to themselves.
Use the neutral "User" label on this surface. It keeps the localization
the row gained, unlike the untranslated string it replaced.
* fix: Let go of the stream when an interaction settles over several resizes
Expanding a tool result mid-answer renders the container first and fills it
once its contents arrive, so one gesture produces more than one resize. Only
the first was credited to the interaction. The second read the reader as still
riding the stream and put them back on the bottom they had just left.
The suppressed resize now settles the ride as well as the near-bottom measure,
using the position the interaction actually left the reader at, so an
interaction that kept them on the end still streams.
* fix: Edit inside a structured text part instead of flattening it
A text content part holds either a string or a { value, annotations } object.
The Assistants thread sync persists the structured form with its file
citations intact, and the editor reads the part through the same union, so
saving an edit wrote a bare string over the whole object and took every
citation with it.
The same object was handed to the tokenizer, which measures length, so a part
that had been edited this way also stored a NaN token count. Write the edit
into value, keep the rest of the part, and count the text itself.
* fix: Keep a saved part's citations in the transcript it is written back to
A text or think part holds either a bare string or a { value, annotations }
object, and the editor already read both through getPartText. Writing the
draft back into the local message cache put the string over the whole value,
so a response carrying file citations lost them the moment it was edited and
did not get them back until a refetch.
Reading and writing now go through the same accessor, so an edit lands in the
shape it was read from and the rest of the part survives.
* fix: Let the message editor follow the chosen font size
Editing a message dropped the draft to a fixed 14px regardless of the
Font Size setting. On dev the textarea carried the markdown class, so it
read --markdown-font-size like the rendered message does; restyling it
into a bordered box replaced that with text-sm, and the new per-part
editor was written the same way. Anyone on Extra Small, Large or Extra
Large saw the text jump the moment they entered edit mode.
Share the .message-content typography with the editors through a
message-editor-text class so a draft is sized like the message it
replaces and keeps tracking the setting.
…ny-avila#14808) * 🧹 chore: Remove Dead Legacy Agent Controller `_LegacyAgentController` has been unreachable since the resumable path became the only route: it is unreferenced, unexported, and untested. It had also drifted out of compilability against the live file — line 2009 called `attachConversationCreatedAt(req, { userId, conversationId, isNewConvo })` against the 3-argument signature declared at line 97, which would await `undefined` and then throw dereferencing `resolved.createdAt`. Keeping it was not free. It carried a third independent copy of the response message-id wiring (`getReqData`, `onStart`, four `updateMetadata` calls), so every change to how a generation identifies its response row had a dead third site to keep in step, and no test to say whether it had been kept in step. Removing the block leaves `createCloseHandler` and the `sendEvent`, `clientRegistry`, `requestDataMap` and `handleAbortError` imports with no remaining callers, so those go too. `AgentController` was a three-line passthrough to `ResumableAgentController`; the real controller is now exported directly, which also matches the `[ResumableAgentController]` prefix every log line in the file already uses. `server/routes/agents/chat.js` binds the export to its own local name and passes the same five arguments, so the route is unchanged. No behavior change: 379 lines removed, 2 added. * 🩹 fix: Remove Duplicated Anchor Block Breaking the `@librechat/api` Build `dev` does not build. `packages/api/src/agents/activityPhases/runtime.ts` carries two byte-identical 98-line copies of the same block (former lines 516-613 and 614-711), so rolldown fails to parse it: [PARSE_ERROR] Identifier `AnchorFields` has already been declared The duplicated block is the anchor-construction work from danny-avila#14805: `AnchorFields`, `laterDefinedIndex`, `foldedAgentIds`, `boundedAnchor` and `mergeAnchors`. danny-avila#14807 was squashed from a branch that predated danny-avila#14805 and re-included that commit, so both copies landed. Only the `type` produced an error — the four function declarations simply redeclare. This removes the first copy. The two blocks were verified byte-identical before the cut, and the resulting file has no duplicate top-level declarations, is missing nothing that danny-avila#14805 introduced, and retains everything new to danny-avila#14807 (`ResolvedPosition`, `resolvePosition`). Verified: `tsdown` builds, `tsc --noEmit` clean, `config/circular-deps.mjs` green across all five graphs (it was reporting `✗ @librechat/api` purely because the build it shells out to was failing), and the 68 tests in `activityPhases/runtime.spec.ts` pass. Carried here rather than in a separate PR because this PR's checks cannot go green until it lands: the failed `packages/api` build cascades into e2e, MCP list_changed, bombadil and the Docker image jobs.
…-avila#14810) `updateAgent` returned early when the resulting state matched the newest `versions` entry, so `findOneAndUpdate` never ran and the caller's update was discarded behind a 200 response. Suppressing a redundant version entry is correct; suppressing the write is not. The document is regularly not equal to its newest version entry: `$push`/`$pull`/`$addToSet` updates snapshot the pre-update state (as `addAgentResourceFile` does on every file attach), `skipVersioning` writes snapshot nothing, and `removeAgentResourceFiles` bypasses `updateAgent` altogether. Any update that moved the document back onto that entry's content was then dropped, leaving the drifted state in place. Keep the version entry suppressed, apply the write, and still report the unchanged `versions` count as `version` so callers keep their existing "no new version" signal. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-avila#14814) * 💬 style: Right-Align In-Flight Steer Bubbles to the Message UI The chat surface reads as message bubbles now — user turns on the right, assistant turns on the left — but the in-flight steer bubbles anchored above the composer were still left-aligned, so a steer sat on the opposite side from the words the user had just sent, then jumped across on `on_steer_applied` when the persisted `SteerPart` landed in-thread on the right. Align the overlay with the user turn it belongs to: - The bubble stack right-aligns and is constrained to the message column (`max-w-3xl`), so the in-flight bubble sits where its applied twin lands instead of ~52px further right (the composer runs wider than the message column at `xl`). - The bubble adopts the same theme-token geometry as `SteerPart` and every user turn (`rounded-theme-surface rounded-br-theme-control`, `px-theme-normal`), replacing the raw `rounded-3xl`/`pl-3 pr-4`. It keeps its outline: an in-flight steer is still provisional. - The controls flank the bubble — overflow menu outboard-left, send-now arrow outboard-right — so neither reads as belonging to the other. DOM order matches visual order, so focus order stays coherent. Also drops the thin `bg-border-medium` divider that bound the arrow to its message: with the arrow now outboard on the far side of the bubble it has nothing to separate, and `EscalateNowButton` no longer needs its fragment. * 💬 style: Center the Steer Controls on the Bubble's First Line The flanking controls read as neither top-aligned nor centered, because their resting position was an accident of `sticky top-2`: the topmost rail trips the sticky inset at rest and is shoved 8px below the row top, while every rail below it clears the inset and stays at the top. So the controls sat 3.8px above the bubble's centre — and stacked steers did not even agree with each other. Give each rail a `py-3` band that reproduces the bubble's own first line (its `py-2.5`, its 1px border, and half the gap between the 24px control and the taller text line box), and pad the overlay evenly so the topmost rail already clears the sticky inset instead of being displaced by it. A 24px control now centres on the first line: measured at 722.0 against the text's 721.8, versus 718.4 before. Beside a one-line steer that reads as centred; on a tall one it aligns to the opening line rather than drifting to the middle, and sticky still carries it while the stack scrolls.
…on (danny-avila#14815) `MCPServerInspector` overwrote the operator's `serverInstructions` declaration with the text fetched from the server. That made a YAML server's cached entry differ from its own raw config on an admin-configurable field, so `isUnmodifiedYamlServer` misclassified it as admin-modified and re-inspected it on the first user-scoped resolve. The second inspection produced a config with a newer `updatedAt`, which: - flipped `getServerConnectionStatus` to `disconnected` permanently, since the healthy app connection was then measured against the newer timestamp; and - made `isAppServerConfig` reject the effective config, gating off the app connection so `GET /api/mcp/tools` returned zero tools and cached nothing. Fetched instructions now land on a separate `resolvedInstructions` field, matching how every other inspector-derived value is stored, so the declaration survives inspection and the guard compares like with like. Bumps `REGISTRY_STORAGE_SCHEMA_VERSION` so Redis-backed deployments rewrite entries whose `serverInstructions` still holds fetched text. Fixes danny-avila#14798
…la#14816) A Streamable HTTP server allows one standalone `GET` SSE stream per session and releases its mapping from the response stream's cancel callback. That callback never runs when the connection dies at a proxy rather than at the client, so the server keeps holding a stream nobody is reading while the client knows its stream is gone. Every reconnect carrying that session id then gets a 409: SSE stream disconnected: TypeError: terminated Transport error (may require manual intervention): Streamable HTTP error: Failed to open SSE stream: Conflict Transport error (may require manual intervention): Maximum reconnection attempts (2) exceeded. Nothing there requires manual intervention. The connection recovers on its own in a few seconds, because the rebuild the first 409 escalates to sends the spec-mandated `DELETE`, which drops the server's session along with the stream it leaked. Two things made a self-healing event read as a fatal one. `extractSSEErrorMessage` classified status by scanning the message text for digits, but `StreamableHTTPError` and `SseError` carry the status on `code` and their messages do not always repeat it. "Failed to open SSE stream: Conflict" has no digits at all, so a 409 never reached the status branch and fell through to the terminal `isTransient: false` — the same verdict as a DNS typo. A 5xx arriving on `code` alone had the same blind spot. The status is now read from `code` when it is in HTTP range, with the message scan kept as a fallback, and 409 joins 5xx as transient: the stale session it reports is cleared by the rebuild, with nothing for an operator to do. The second is volume. Each SDK retry fires `onerror` twice — once with the raw throw out of `_startOrAuthSse`, once with the `Failed to reconnect SSE stream` wrapper. Only the wrapper matched the existing suppression, so every doomed retry logged at error level, and the retries are doomed by construction: nothing about the same session id can stop conflicting. The first conflict now escalates for rebuild and the rest are logged as the echo they are, along with the SDK's out-of-retries announcement when a rebuild is already underway. The non-conflict path for that announcement is untouched, so an exhausted budget still falls through to our reconnection everywhere else. `extractSSEErrorMessage` moves to `errors.ts` alongside `isOAuthAuthenticationError`. It had no test: `MCPConnection.test.ts` held a hand-copied clone marked "keep in sync with the actual implementation", so 66 assertions were exercising the copy. The clone is deleted and the suite now imports the real function, which it turns out had not drifted. `MCPConnectionSseConflict.test.ts` drives a real client transport against a real in-process `StreamableHTTPServerTransport` reproducing the sequence above: the stream opens, its socket is destroyed underneath the client, and every later `GET` on that session id conflicts while a rebuilt session gets a healthy stream.
…avila#14817) * 📎 fix: Alias Shell Script MIME Variants to `application/x-sh` Chrome on Linux reports `.sh` files as `application/x-shellscript` (freedesktop shared-mime-info) and libmagic reports `text/x-shellscript`. Neither string appears anywhere in the source, so uploads were rejected even though `application/x-sh` is in the default allowlist and `codeTypeMapping` maps `sh` to it — `inferMimeType` only consults the extension map when the client sends no type at all, so a non-empty browser value passed straight through to the allowlist check. Alias both variants to the canonical `application/x-sh`, matching the existing treatment of `text/x-markdown` and `application/x-zip-compressed`. Also attach `statusCode`/`body` to multer file-filter rejections. Without them the error misses the `isCustomError` branch in `ErrorController` and falls through to a bare `500 An unknown error occurred.`, so the rejection reason was logged server-side but never reached the client. The upload hook already surfaces `error.response.data.message`, so a rejected file now explains itself instead of showing a generic upload failure. * 🔁 refactor: Move Upload Error Contract Into `packages/api` Addresses codex P1 on danny-avila#14817. The producer of the `statusCode`/`body` pair now sits beside its consumer: `isCustomError` and `ErrorController` are already in `packages/api/src/middleware/error.ts`, and `CustomError` is already in `packages/api/src/types/error.ts` — only the construction of that pair was stranded in legacy JS. `createCustomError` is exported from the same module as the guard that recognizes it, and `multer.js` is back to a thin caller. Also pins the `.sh` back-compat claim with tests: configs from the documented workarounds (`application/x-sh` per danny-avila#4660/danny-avila#5689/danny-avila#6297, and the broad patterns from danny-avila#14804) still accept a `.sh` upload after the alias rewrites the type. A negative control confirms the endpoint config is genuinely in play rather than falling back to the default allowlist.
* ⚡ feat: Add Gemini 3.7 Flash Support Adds first-class support for Google's Gemini 3.7 Flash (`gemini-3.7-flash`) for both the Gemini API (AI Studio) and Google Cloud Gemini Enterprise Agent Platform, following the Gemini 3.6 Flash integration (danny-avila#14369). - Context window (1,048,576) in googleModels; API + cache pricing in tx.ts. - Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations. - Register the model in the Flash-family handler so it inherits the existing strip of deprecated sampling params (temperature/topP/topK), rejected penalty params, and thinkingBudget, and defaults to `medium` thinking. - Generalize that handler's enumerated table from a [id, level] tuple to a rule object, so a model can also declare thinking levels it rejects. Gemini 3.7 Flash errors on `minimal` (which the Google endpoint offers in its thinkingLevel slider), so an explicit `minimal` is substituted with the nearest supported level, `low`. Explicit low/medium/high pass through unchanged. - Apply Google's introductory pricing ($0.75 in / $3.75 out / $0.075 cached, per 1M) to Gemini 3.7 Flash and correct Gemini 3.6 Flash to the same rates. Both revert to $1.50 / $7.50 / $0.15 on 2027-01-01; noted at both call sites. Resolves danny-avila#14802 Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash Ref: https://ai.google.dev/gemini-api/docs/pricing * 📝 docs: Match the House Style for Promotional Rate Comments Align the Gemini 3.6/3.7 Flash introductory-pricing notes with the existing Sonnet 5 convention in the same file: one comment per group, naming the models and the exact values to restore, so the manual follow-up is unambiguous. No rate changes. * ⬆️ chore: Bump `@librechat/agents` to 3.4.7 for Gemini 3.7 Flash Prefill Unblocks this PR. `NO_PREFILL_GEMINI_MODELS` is model-enumerated in the agents SDK, so 3.4.6 does not know `gemini-3.7-flash` forbids a trailing `model`-role turn — editing an assistant reply and resubmitting would reach Google as a prefill and return HTTP 400 on a model this PR adds to the default list. 3.4.7 (danny-avila/agents#412, released via danny-avila#413) adds it. Verified the published tarball: `3.4.6...3.4.7` touches only `dist/{cjs,esm}/llm/google/utils/common.*` — the prefill array and its comment. `dist/types` is byte-identical, so there is no API surface change. Raises the declared range in both workspaces alongside the lock. `^3.4.6` already permitted 3.4.7, but the fix is required rather than merely compatible, so the floor should say so.
… Searching It (danny-avila#14820) `@modelcontextprotocol/sdk@1.30.0` is a small maintenance release on the 1.x line (upstream's active line is now the 2.0.0 scoped packages). The range was already `^1.29.0`, so only the lockfile pinned the old version; the manifests move too so the floor matches what we test against. Nothing in it is breaking. The four changed type declarations are additive — optional `maxBufferSize` on `StdioServerParameters`, an optional third constructor argument on `StdioServerTransport`, optional options on `ReadBuffer`, optional `keepAliveMs` on the server transport — and the only manifest change is `@hono/node-server` widening to `^1.19.9 || ^2.0.5`. No new dependencies. Two behavior changes are worth knowing about even though neither is an API break. `ReadBuffer` now caps a single stdio message at 10 MB (previously unbounded) and errors the transport instead of growing, which is reachable through `StdioClientTransport` if a stdio server returns a very large single result; it takes `maxBufferSize` if that ever needs raising. And Content-Type handling switched from substring search to parsed media types, client and server. Most of the release is Streamable HTTP server hardening we do not run — a 15s SSE keep-alive, `X-Accel-Buffering: no` on SSE responses, guards so a stale stream's cancel cannot tear down its successor, and `_closed` checks so a transport closing mid-request stops registering streams into swept maps. None of it changes how we behave as a client. In particular it does not address the stale-stream 409 in danny-avila#14816: that keep-alive runs in whichever server we connect to, not here. The same substring-vs-parse mistake the SDK corrected exists in our streamable HTTP response guard, which classified a response as SSE with `contentType.includes('text/event-stream')`. A `Content-Type` naming the SSE type in a parameter — `text/plain; boundary=text/event-stream` — is not an event stream, but matched. The guard then took `canEmitFallbackSSEError`, so an oversized body was answered with a synthetic SSE error frame the caller reads as a well-formed response body, rather than the throw a non-SSE response gets. The check now compares the parsed media type, via a `mediaTypeEssence` helper added to the header utils where `mergeHeaders` already lives. Verified against 1.30.0 rather than assuming: the package was staged into the worktree's own `node_modules` so it shadowed the shared install, and `packages/api` `src/mcp` ran green on it — same four pre-existing red suites as on 1.29.0 (`MCPReinitRecovery` plus three Redis `cache_integration` suites that need a live Redis), no new failures.
…danny-avila#14821) `activity-phases` asserted the parent `summary` was visible immediately after `sendMessage` resolved. A parent phase only exists once the turn completes, the phase closes, and its summary round-trips to the phase-label model — so that assertion raced the entire pipeline and only survived on Playwright's retries. It shows as `1 flaky` on the memory lane of a green dev run, and fails all three attempts on slower hardware. Gate the DOM on the durable projection instead. The test already fetched /api/messages twice; the first fetch now also waits for the persisted phase part before any DOM assertion runs, so the client is only asked about a phase the server has already written. Also drops the duplicate fetch. The two poll blocks queried the same endpoint for the same message and both asserted `finalTextIndex === activity_end_index`; the removed copy left `liveAssistant`, `livePhase` and `liveFinalTextIndex` shadowing their durable equivalents. No coverage removed — every assertion is preserved, reordered to follow the dependency chain: persisted shape, then DOM, then label-model requests, then the reload round-trip.
* 🚀 chore: Prepare v0.8.8-rc1 release * 📚 docs: Complete v0.8.8-rc1 operator references * 📚 docs: Mark stateful sessions experimental * 📚 docs: Clarify background code capability * 📚 docs: Refresh v0.8.8-rc1 operator guidance * 📚 docs: Highlight v0.8.8-rc1 features in README * 📦 chore: Bump publishable packages again * 📚 docs: Add streaming question progress * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release highlights * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release guidance * 📦 chore: Bump publishable packages again * 📚 docs: Highlight batched Agent questions * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note PowerPoint template support * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note latest provider and file support
Syncs upstream v0.8.7..v0.8.8-rc1 (357 commits) onto our 7 fork commits. Conflict resolutions of note: - useMCPToolOptions: upstream replaced the bespoke defer functions with a generic `useBooleanToolOption` factory. Our `serverDefault` feature is the generalization of upstream's two mirror helpers, so it is re-expressed by dispatching between them (`applyBooleanOption`) instead of reimplementing the hook. All 9 of our three-state tests pass unchanged. - MCPTool.tsx: deleted upstream by the Agent Builder redesign (danny-avila#13952). Our server-default defer threading is ported to its successor, McpSection.tsx, reading `deferLoading` from the live server rather than the stale snapshot. - definitions.ts: upstream rewrote MCP server-name resolution (direct-first `splitMCPToolKey` + alias fallback), so our `deferLoading` lookup moved to where the resolved RAW server name is known, still cached per server. - config.js: our availability filter and upstream's hidden-spec filter are both shape-preserving, so they compose. - openIdJwtStrategy.spec.js: dropped our hand-rolled `getOpenIdEmail` mock — upstream relocated that function into `@librechat/api` and mocks it with `requireActual`, which cannot drift from the real implementation. - tokens.ts: took upstream's corrected sonnet limits, kept our gateway alias. Five OIDC tests failed against our fork's token gate, which rejects bearer-only requests carrying no cookies. Two of them were already failing on apro-deploy before this merge; the other three are new upstream tests hitting the same pre-existing divergence. Their fixtures now supply an id token so they exercise their actual subject. No runtime behavior changed. Verified: all packages typecheck; 431 ToolService/MCP, 116 tools/tokens, 114 client hook, and 103 OIDC tests pass. Remaining failures are environmental (Redis-backed *.cache_integration suites) or reproduce identically on pure upstream (message.spec updateMessageText timeout). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges upstream
v0.8.8-rc1(357 commits fromv0.8.7) intoapro-deploy.Our branch sat exactly on upstream's
v0.8.7release commit plus 7 fork commits, so every conflict was genuinely "our patch vs their new code" rather than a replay of old resolutions. All 7 fork features are verified present after the merge.Conflicts of note
10 files conflicted. Most were additive unions; three needed real porting:
useMCPToolOptions.ts— upstream replaced the bespoke defer functions with a genericuseBooleanToolOptionfactory. OurserverDefaultfeature turned out to be the exact generalization of upstream's two mirror helpers (withBooleanOption= absence means false,withNativeBooleanOptOut= absence means true), so it is now a small dispatcher between them (applyBooleanOption) rather than a reimplemented fork of the hook. All 9 of our three-state tests pass unchanged, which is what verified the reuse was semantically exact and not merely plausible.MCPTool.tsx— deleted upstream by the Agent Builder redesign (🧰 feat: Redesign Agent Builder with Unified Tools Marketplace, Skills & Orchestration danny-avila/LibreChat#13952). Our server-default defer threading is ported to its successorMcpSection.tsx, readingdeferLoadingfromliveServerrather than the stale card snapshot, per upstream's own comment about that hazard.definitions.ts— upstream rewrote MCP server-name resolution (direct-firstsplitMCPToolKeywith alias fallback), so ourdeferLoadinglookup moved to where the resolved RAW server name is known, still cached per server to avoid repeat config reads.Smaller ones:
config.js— our availability filter and upstream's hidden-spec filter are both shape-preserving list filters, so they compose.openIdJwtStrategy.spec.js— dropped our hand-rolledgetOpenIdEmailmock; upstream relocated that function into@librechat/apiand mocks it withrequireActual, which cannot drift from the real implementation.tokens.ts— took upstream's corrected sonnet limits, kept our gateway alias.Two items needing a decision (not changed here)
OIDC token gate. Five OIDC tests failed against our fork's gate in
api/strategies/openIdJwtStrategy.js, which rejects requests with no id token and no refresh token — meaning bearer-only requests carrying no cookies are refused. Two of the five were already failing onapro-deploybefore this merge; the other three are new upstream tests hitting the same pre-existing divergence. This PR preserves the shipped fork behavior and updates the five fixtures to supply an id token so they exercise their actual subject (issuer validation, cache mode,idOnTheSourcecoercion). No runtime behavior changed, and 2 previously-red tests are now green. If refusing bearer-only API clients was not intended, that is a follow-up code change deliberately left out of a sync.sonnet: 64000is now inconsistent. Upstream correctedclaude-sonnet-4-6from 64000 to 128000 max output. Our bare gateway alias was kept at 64000 rather than smuggle a behavior change into a merge commit. If the gateway'ssonnetmaps to 4.6+, this under-reports and is a one-line fix.Verification
All five packages typecheck via explicit
tsc --noEmit. Worth noting the build alone proves nothing here:npm run buildusestsdown/vite, which do not typecheck (only data-provider runstsc).Passing: 431 ToolService/MCP, 116 tools/tokens, 114 client hook, 103 OIDC, 55 data-schemas message.
Two remaining failure classes are not merge regressions, each confirmed by isolation:
*.cache_integrationsuites need a live Redis cluster (All the root nodes are unavailable).message.specupdateMessageTexttimes out identically against pure upstreammessage.ts.Formatting was scoped to the files actually edited;
npm run formatis repo-wide and would have buried the merge in unrelated churn. Prettier and ESLint are clean on those files.🤖 Generated with Claude Code