Commit graph

5147 commits

Author SHA1 Message Date
Danny Avila
d85bea41cc
🧪 ci: Mock the Parent Subagent Index Handler in Convos Route Tests (#15146) 2026-08-23 22:54:19 -04:00
Marco Beretta
dd85c6d6d0
🪹 feat: Shared Empty State for Side Panels (#15123)
* feat: shared empty state for side panels

Bookmarks and Memories each hand-rolled the same empty state: the same bordered
card, the same circular icon surface, the same title and caption sizes, written
out twice. Schedules had none at all, so an account with no schedules got a bare
list with nothing to explain what the panel is for.

One EmptyState primitive in packages/client, taking an icon, an optional title
and description, and an optional action. Bookmarks and Memories move onto it with
no visual change and no copy change. Schedules gets a real empty state, and an
error state with a Retry action, so a panel that failed to load offers a way out
instead of looking empty.

A description with no title takes the title's size rather than the caption's:
where it is the only line, it IS the message.

* fix: drop the create hint for roles without schedule create access

The panel already hides its create button behind hasCreateAccess, but
the empty state still told a USE-only viewer to create a schedule it
offers no way to create. The invitation now renders only when the
capability does.

* fix: suppress the create hint when the quota already blocks creation

A maxPerUser of 0 disables the create button on an empty list, so the
empty state must not say to create one either; the hint now follows the
same effective gate as the button.
2026-08-23 18:56:34 -04:00
Danny Avila
fc2b8584c4
📇 feat: Surface Event Child Activity Through a Bounded Parent Index (#15142)
* feat: surface event-driven child activity

* fix: keep child task aggregation documentdb-compatible

* fix: address event activity review findings

* test: provide markdown message context defaults

* fix: report bounded child history truncation

* fix: preserve current child activity state

* fix: preserve durable event child activity

* fix: handle missing task timestamps

* fix: keep active event snapshots live

* fix: preserve event activity across valid anchors

* fix: close event child activity gaps

* fix: preserve event activity across resume
2026-08-23 18:50:00 -04:00
Marco Beretta
8a118c7cb3
⏱️ feat: Shared Time Picker for Schedule Times (#15122)
A schedule's time was three dropdowns side by side: hour, minute, meridiem. That
is three controls for one value, it cannot be read at a glance, and the minute
list was a fixed set of four with the stored value bolted on, so a schedule
already running at :07 could be kept but never chosen.

They become one TimePicker: hour, minute and, where the clock format calls for
one, meridiem, as scrollable columns behind a single trigger showing the selected
time. An hourly cadence gets MinutePicker, the same control with its other
columns dropped, so it reads as the same widget rather than a different one. Both
live in packages/client with their wording passed in as props, so the primitive
carries no translation keys of its own.

Not `<input type="time">`: the browser owns its rendering, and it cannot be
brought in line with the rest of the form.

`hour12` is a required prop rather than a locale-derived guess. The app has
already resolved its Clock format setting, and re-deriving the answer inside the
picker would let it disagree with the summary printed beside it.

The trigger names its selected value as well as its field: `aria-labelledby`
replaces a button's child text, so pointing it at the label alone announced
"Time" and left a screen reader user unable to tell what was selected without
opening the columns and reading them. The columns are a roving-tabindex
radiogroup, arrow keys wrap, and the selected row is scrolled to the middle of
its column on open.

The popover is deliberately not portaled. A Radix dialog sets `pointer-events:
none` on the body while open, so a popover portaled out of it renders correctly
but receives no clicks or wheel events, and its focus trap puts the content out
of tab order too.

Hour and minute are set in one change. Behind separate fields a half-applied edit
could submit a time the user never picked, and the form now carries the hour as
the 0-23 value the cadence stores rather than a 12-hour value plus a meridiem it
has to recombine.
2026-08-23 22:47:33 +00:00
Marco Beretta
7834ebab33
🕰️ feat: Clock Format and Week Start Preferences (#15121)
* feat: clock format and week start preferences

Times were written in whatever convention the browser locale implied, and the
week always started on Sunday. Neither is right for a large part of the user
base: most of Europe reads a 24-hour clock and starts the week on Monday, and a
user running an English interface in a region that does either is currently
given the American convention with no way to change it.

Two General settings, Clock Format (System / 12-hour / 24-hour) and Week Starts
On (System / Sunday / Monday). Their System branch reads the runtime locale
rather than `i18n.language`, which is normalized down to a translation bundle:
`en-GB` and `en-AU` both become `en`, which is exactly the regional part these
two settings depend on, and reading it would report a 12-hour clock and a Sunday
week to a British user.

Week start is typed on the same 0-6 Sunday-first scale the schedule cadence uses
rather than being narrowed to Sunday/Monday, because the System branch reports
whatever the locale says and several (ar-EG, fa-IR) start the week on Saturday.
Engines without `Intl.Locale.prototype.getWeekInfo` fall back to a short list of
Sunday-first regions with Monday, the ISO 8601 default, otherwise: this is a
display default the toggle can always override, so an imperfect fallback degrades
rather than breaking.

Both settings are stored per browser. They describe how this device reads a
clock, which is a property of where someone is sitting rather than of their
account, and a user who moves between a European desktop and a US phone wants
each to read its own way.

Applied to message timestamps, the schedule dialog and card, key expiry and
refill dates, prompt and agent version dates, memory dates, and project chat
lists. The weekday order also drives the schedule dialog's day pills and the way
a weekly cadence reads back, so a wrap-around selection of Sat+Sun+Mon reads
"Monday, Saturday, Sunday" in a Monday-first week instead of "Sunday, Monday,
Saturday".

Dropdown now names its selected value as well as its field label. `aria-labelledby`
REPLACES the trigger's own text, so pointing it only at the caller's label left
the selected value unannounced, which these two settings are the first consumers
to hit.

* fix: teach the week-start fallback the Saturday-first regions

The no-week-data heuristic could only answer Sunday or Monday, folding
ar-EG to Sunday and fa-IR to Monday when CLDR says both start on
Saturday, and the selector offers no explicit Saturday override to
recover with. It now carries CLDR's Saturday-first territories, and the
UAE moves off the Sunday list to the Monday default, where CLDR put it
when its weekend moved to Sat-Sun. The fallback tests delete the
engine's week data for their duration, so they exercise the heuristic
on every engine instead of skipping wherever getWeekInfo exists.

* fix: infer likely regions for bare language tags and stop rebuilding clock formatters

A runtime that reports a language-only locale (bare ar or fa) carried no
region for the week-start heuristic, so those users fell to the Monday
default even though maximize() knows their likely region starts the week
on Saturday. The heuristic now maximizes before defaulting.

The runtime locale and each locale's meridiem answer are also cached at
module scope: every message timestamp mounts useClockFormat, so the
uncached path built a fresh Intl.DateTimeFormat per rendered message,
hundreds in a long conversation, even when the preference ignores the
locale entirely.

* fix: keep the Maldives on Friday in the week-start fallback

CLDR's lone Friday-first territory was in neither fallback set, so
dv-MV (and bare dv, which maximizes to MV) fell to Monday on engines
without week data, with no Friday override in the selector to recover
with. The three per-day sets consolidate into one region-to-day map.

* fix: complete the Sunday-first fallback from CLDR week data

The hand-picked ten Sunday-first regions left the System preference on
Monday for en-IN, id-ID, bn-BD, ur-PK, th-TH and the rest of the long
tail on engines without week data. The list is now every territory whose
und-XX week does not start Monday per CLDR, deprecated codes included,
with a note on how to regenerate it when CLDR moves a territory.

* fix: mock message context across markdown test suites and prevent global plugin cache leak
2026-08-23 18:41:14 -04:00
Danny Avila
b421f900dd
🗳️ ci: Log the Exact Spec List in Codegraph E2E Votes (#15143) 2026-08-23 18:26:34 -04:00
Danny Avila
d864597731
⚡ perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array (#15141)
* ⚡ perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array

Every saveConvo read every message id in the conversation (sorted) and
wrote the array back onto the document — twice per chat turn, O(n) in
conversation length, from a write path. The turn's savers know exactly
which message they just wrote, so they now pass it as
metadata.appendMessageIds and saveConvo $addToSet-s it, skipping the
read and the full-array rewrite. Every save without the option — titles,
archive, fork, import, threads — still rebuilds from the database, which
remains the heal point for the drift that message deletion has always
left behind (deletes never ran saveConvo).

The array's consumers read presence or length, or use it as an
optimistic cache placeholder, so incremental maintenance is
behaviorally identical; on traced turns the array stays exactly equal
to the messages collection.

Per-turn queries: 15 -> 13 (two Message.find gone), and the growing
array payload no longer crosses the wire twice per turn.

* 🎯 fix: Brand the Lineage-Only Resolved Conversation Instead of Guessing by Shape

The resolved-conversation files fast path treated an absent files
property as unresolved so the lineage-only partial from a bound
agent-event continuation could not silently hide a conversation's
uploads. But MongoDB never stores an empty files array, so nearly every
real conversation also lacks the property and the fast path never fired
— a follow-up turn on an upload-free conversation still paid the
getConvoFiles round trip.

The synthesized partial is the one object that cannot speak for the
database, so it now carries an explicit symbol brand
(PARTIAL_RESOLVED_CONVERSATION, non-serializing and invisible to key
iteration), and a stored document without files means what it means:
no files. Traced follow-up turns drop from 14 queries to 13.

* 🧪 test: Expect the Appended Message Id in the Route's saveConvo Metadata

messages-get.spec.js pins the exact metadata POST /api/messages passes to
saveConvo; the route now forwards the saved message's _id as
appendMessageIds, which is the behavior the append path depends on.
2026-08-23 16:52:44 -04:00
Danny Avila
9da51cb507
chore: bump agents sdk to v3.6.15 (#15140) 2026-08-23 15:31:47 -04:00
Marco Beretta
b55222b6f4
📆 feat: Weekly Schedules on Several Days (#15120)
* feat: weekly schedules on several days

A weekly cadence has always stored `daysOfWeek` as a list, and the API has always
accepted several, but the dialog offered a single day-of-week dropdown. That
picker could only hold `daysOfWeek[0]`, so a multi-day schedule created through
the API read back as running on one day, and the form needed a preservation rule
to avoid collapsing the rest of the set on an unrelated edit.

The dropdown becomes a row of toggles, one per day, so the control can represent
what the cadence already stores. The preservation rule goes with it: there is
nothing left to preserve once an untouched picker shows the real set.

Each pill is a toggle button rather than a checkbox because it renders as one,
and carries the long weekday name as its accessible label since "Mon" reads fine
at a glance but poorly aloud. The submitted set is sorted, so two schedules
picked in a different order are the same cadence.

Weekly with nothing selected is expressible in the form but not on the wire, so
it blocks submit with a message rather than silently saving as Monday.

* fix: keep the weekday pills one line tall and honest about empty sets

Seven 3rem pills wrapped to a second line inside their md cell, spending
height the dialog's no-scroll budget does not have; they now share the
row equally with the locale's narrow weekday labels, composed on the
shared Button so the pills carry its focus ring rather than a bare
feature-styled element. Both labels are built once per locale instead of
fourteen Intl constructions per keystroke, and the cadence summary no
longer describes the Monday fallback while the form says to pick a day.

* fix: name each weekday pill outright on hover

The narrow labels repeat within a week and read by position; until the
week-order preference lands, the position is fixed Sunday-first, so the
title gives a sighted user the full day name without relying on it.
2026-08-23 15:21:05 -04:00
Danny Avila
2ac7986947
🫂 fix: Route Subagent Activity Through the Chat Renderer (#15137)
* fix: align subagent activity with chat UI

* fix: preserve subagent activity boundaries

* fix: preserve subagent panel state semantics

* fix: preserve subagent activity metadata

* fix: preserve live subagent event metadata

* fix: scope subagent phases by message step

* fix: retire closed subagent message phases
2026-08-23 15:20:28 -04:00
Marco Beretta
591f05d2e1
🧹 chore: Sort Client Imports Left Behind by the Import Order Gate (#15139) 2026-08-23 15:18:05 -04:00
Danny Avila
f3c6e24f84
⚡ perf: Cut Serial Round Trips and a 100ms Admission Stall from Chat Turns (#15138)
* ⚡ perf: Stop Awaiting the Conversation Access Marker Write

Without Redis the CONVO_ACCESS violations namespace is backed by keyv-file,
whose debounced write resolves after ~100ms. validateConvoAccess awaited
that write before calling next(), so the first message to any existing
conversation waited ~100ms before the request was even admitted — once
per conversation per ten-minute window, on every default deployment.

The marker only short-circuits the next check, so the write no longer
gates the request. The same read now stashes the full document on
req.resolvedConversation (null when absent) for downstream consumers.

First-turn ack on an existing conversation: 109ms -> 5ms.

* ⚡ perf: Read the Conversation Once per Chat Turn

A chat turn read the same conversation document four times: the access
check (two fields), the subagent thread guard (full document), agent
initialization (the files field), and the first save. The access check
now reads the full document and leaves it on req.resolvedConversation,
the guard accepts that pre-resolved document instead of re-reading, and
initializeAgent takes the conversation's file refs from it rather than
issuing a separate findOne.

Two serial round trips removed from every turn; the same document still
serves the first save as before.

* ⚡ perf: Remove Duplicate JWT Authentication on Agents Routes

routes/agents/index.js applies requireJwtAuth and then mounts the v1
router at '/', which applied requireJwtAuth again. Every request through
the agents router — chat turns included — ran the passport strategy
twice: two signature checks and two user document reads. The v1 router
is mounted nowhere else; its separately exported avatar router carries
its own auth in files/index.js.

* ⚡ perf: Skip the History Read for Root-Parent Turns and Walk the Tree in O(n)

loadHistory fetched every message in the conversation and then walked the
parent chain from the request's head. For a new conversation — or a new
branch from the root of an existing one — the head is the root sentinel,
which no message carries as its id, so the walk was empty by construction
and the fetch was wasted. It now returns early.

getMessagesForConversation found each ancestor with Array.find inside the
walk, O(n^2) on a linear conversation (~5ms at 1000 messages). A Map by
messageId makes it O(n); first-match semantics are preserved.
2026-08-23 15:17:35 -04:00
Marco Beretta
1724489024
🧭 feat: Choose a Schedule's Time Zone (#15119)
* feat: choose a schedule's timezone

A schedule's timezone was whatever zone the browser reported when it was created,
and nothing in the dialog could change it afterwards. That is wrong for anyone
who travels, for a shared account, and for a team schedule that should follow an
office rather than whoever happened to open the dialog.

The zone becomes a picker over every IANA zone the runtime knows, with the user's
own zone and UTC pinned first. `Intl.supportedValuesOf` is unavailable on older
engines, so that pinned pair doubles as the fallback list: a user who cannot
browse zones can still keep the one their schedule already uses. Each option
carries its current offset, since a name alone does not tell two similar zones
apart.

A zone change on its own is a timing edit, so it is submitted like one. The
server recomputes the next run whenever the timezone changes and measures the
interval floor against the effective pair, which is what makes `0 0,12 * * *` a
12-hour gap in UTC and an 11-hour one in America/New_York on the day it springs
forward. The dialog now mirrors that: the cron field validates against the
selected zone and the floor is measured in it, so a cadence cannot be accepted
here and refused by the API.

* fix: keep zone-only edits out of the cadence and the zone list findable

A timezone-only edit rode the cadence dirty flag, so the PATCH carried a
cadence rebuilt from the form and could overwrite stored fields the
pickers cannot represent, an API-created hourly's nonzero hour for one.
The floor still validates a zone change as the timing edit it is, but
only touched cadence controls put a cadence on the wire, and the spec
now pins that down instead of only checking the zone.

The picker list also gains the modern IANA names supportedValuesOf
omits (it reports CLDR's legacy canonical forms, Asia/Calcutta for
Asia/Kolkata), each probed against the engine before inclusion, and the
per-zone offset labels are cached per locale so reopening the dialog
stops rebuilding ~400 Intl.DateTimeFormat instances.

* fix: carry the full tzdb rename set into the zone picker

Five names covered the famous renames but Node still accepts and omits
fourteen more modern canonical identifiers (the Argentina provinces,
Indiana and Kentucky city moves, Kathmandu, Asmara, Faroe, Chuuk,
Pohnpei, Kanton, Atikokan). The set is now the tzdb rename list, still
probed per engine and deduped; deprecated links like US/Eastern and the
sign-inverted Etc/GMT forms stay out deliberately, since they duplicate
zones already listed under their canonical names.
2026-08-23 15:02:41 -04:00
Danny Avila
f7b65cb7ae
🎭 ci: Gate Playwright Lanes and Docker Smokes on Codegraph Selection (Stage 2) (#15136)
* 🎭 ci: Gate Playwright Lanes and Docker Smokes on Codegraph Selection (Stage 2)

* ci: surface the fail-open reason in the stage-2 select summaries

* ci: log the stage-2 select decision for harvesting

* ci: fail open on fetch failure or truncated file list; type-strict skip decisions (Codex)

* ci: check curl's exit status before honoring a selection (Codex r2)
2026-08-23 14:58:45 -04:00
Marco Beretta
44d97f859d
⏰ feat: Custom Cron Cadence for Scheduled Chats (#15084)
* feat: custom cron cadence for scheduled chats

Scheduled chats could only be built from four fixed presets, each pinned to a
single hour and minute, so anything outside that shape (twice a day, every 15
minutes, the 1st of the month) was not expressible. This adds a Custom cadence
that takes a raw five-field cron expression.

The cadence schema becomes a discriminated union on `frequency`. A cron row
carries `expression` instead of the hour and minute it cannot represent, since
there is no single hour for `0 9,17 * * 1-5`, and the Mongo schema requires each
field only for the shape that has it: a blanket `required` would reject every
cron write, and dropping it entirely would let a structured cadence silently
fire at 00:00 with a missing hour.

Five fields only. croner also reads a six-field form carrying seconds and a
seven-field form that pins a year, and both are refused. Seconds would promise a
precision the engine does not keep, since it polls on a thirty-second tick and
offsets each schedule by up to two minutes of jitter. A pinned year makes a
cadence that runs out, and every place that computes a next run reads "no next
occurrence" as a cadence it cannot read.

Compilation, validation, next-run previews and interval measurement live in
packages/data-provider so the dialog and the engine share one parser and cannot
drift. The dialog previews the next occurrences, enforces the admin interval
floor and disables its own submit from the same functions the server validates
with, so it cannot offer a Create the API answers 400 to.

The interval floor now covers cron, and measures it twice, taking the smaller.
The nominal gap is probed in UTC and discounted by the same worst-case DST
allowance the structured branches carry, which keeps `0 9 * * *` reporting
exactly what the Daily preset reports. Real elapsed time is then measured in the
schedule's own zone across each of that zone's transitions, because
spring-forward compresses a gap that straddles one: `0 0,12 * * *` in
America/New_York is 11 hours that day, not 12, and a floor between the two would
otherwise be bypassed. The floor ships with the schedules list so the dialog can
mirror it rather than surfacing it as a 400 after submit.

Radio gains a wrap variant, since five frequency segments no longer fit one row
in a phone-width dialog and a translated label can push even a desktop one over.
Its indicator follows the selection across rows; the single-row default is
unchanged.

* fix: mark the cron input invalid when the interval floor rejects it

A floor-violating expression disabled Create and rendered the cadence
message, but the input itself still said aria-invalid=false and its
aria-describedby never reached that message, leaving a screen reader
user with a disabled Create and no stated reason.
2026-08-23 14:35:18 -04:00
Marco Beretta
0f376884bd
📱 feat: Offer a Dismissable Strip of Chat Beside the Mobile Drawer (#14990)
* feat: leave a dismissable strip of chat beside the mobile drawer

The drawer took the whole viewport, so opening it read as a screen change
rather than a layer over the conversation, and the only ways back were the
header button and a swipe.

It now stops at 80% and the chat stays visible behind a scrim, which is
itself the dismiss target: tapping it closes the drawer and returns to the
conversation, which never navigated away. The scrim renders as a sibling
of the pane rather than inside it, because the pane is inert while the
drawer is open and would swallow the click. Drawer width and pane travel
derive from one constant so they cannot drift.

Closing had to change with it. A programmatic close repositioned the pane
instantly, which was invisible only because a full-width opaque drawer
covered the jump; with a strip on screen that jump lands in plain view, so
both surfaces animate together, the motion the drag path already produced.
The spec that pinned the old reveal is rewritten to pin this.

The easing also changed: the previous curve spent its last third of the
duration on a few percent of the distance, which read as the panel
sticking just before it landed, most obvious on close.

Three things the scrim has to respect, each found in review:

- It routes through useSidebarToggle rather than writing the atom, so the
  slide still starts imperatively and a large conversation cannot stall it.
- It drops its fade under prefers-reduced-motion, matching the snap
  kickDrawerAnimation already performs.
- It stays the pointer target until the close animation settles, derived
  from the committed state so every close path is covered, and cleared on a
  timer so a scrim unmounting at the breakpoint cannot strand it. Focus
  returns to the drawer's opener once the closed state commits, since the
  scrim goes aria-hidden and untabbable.

* fix: close the mobile drawer predictably from every path

Move the close handling out of Root into useDrawerDismiss, which fixes three
things the split scrim-owned version got wrong:

A breakpoint crossing derives the drawer closed with nothing to animate, so
narrowing a window or rotating a tablet armed the pointer guard and left a
transparent full-screen scrim swallowing taps for 300ms.

The scrim stays the pointer target through that guard, where the state has
already committed. A tap there closed again, a no-op that never reached the
focus handoff, stranding the restore flag to fire on a later close.

Focus was only restored when the scrim itself closed the drawer, and only to
the header opener. Closing from the drawer button or Escape left focus in a
subtree that goes inert, and routes that render no opener left it on the
scrim once it went aria-hidden. Every close path now restores, to the opener
or the pane, and only when the close is what dropped focus.

* style: sort imports in the new drawer hook

* fix: reclaim focus from the scrim when Escape closes the drawer

The drawer's Escape handler closes it without going through the scrim, so a
keyboard user who tabbed there kept focus on a button that becomes
aria-hidden and untabbable. Inert drops focus to the body by itself;
aria-hidden does not, so it has to count as lost too.

* feat: make the mobile chat strip a setting, off by default

The drawer covering the full width and closing by swipe stays the default.
Turning the setting on stops it short of the edge, leaving a strip of the
conversation visible that also closes the drawer when tapped.

Both surfaces read one custom property for how far the drawer opens, so the
value can change at runtime without threading a number through the swipe
gesture, and their travel still cannot drift apart. The fallback is the
default, so anything rendered outside the property's scope agrees too.

The scrim moves into its own component, which is what makes its tab order,
aria-hidden and pointer-events states testable.

* fix: keep the reveal close on the default full-width drawer

Making the strip opt-in put the paired close animation on the default path,
where the drawer covers the pane: selecting a conversation then visibly
shifted the chat leftward while the new one committed into the moving layer,
which is the regression the reveal existed to avoid.

The reveal is now chosen from geometry rather than the setting, since it is
safe exactly when the drawer hides the pane, however the width was arrived
at. The drawer also transitions its width, so toggling the setting while it
is open moves both surfaces on one curve instead of jumping the width in a
frame while the pane eases across the transition.

* fix: honour reduced motion when the strip setting changes the width

Changing the setting updates the width custom property directly rather than
going through the snap path, so the drawer eased its width and the pane its
transform for the full transition even for a user who asked for no motion.

The preference now reaches the declarative styles on both surfaces, and the
snap no longer hands an animating transition back afterwards, which is what
left the element ready to ease the next change.

* fix: cover the gesture snap, the close frame and the breakpoint focus

The gesture settle restored the transitions directly rather than through the
reduced-motion handoff, so a swipe left both surfaces ready to animate the
next width change.

The close guard was armed from a passive effect, which runs after paint,
leaving one frame where the pane had dropped inert and the scrim had not yet
taken the pointer back. It is armed in the committing frame now.

Crossing into mobile with focus inside the expanded desktop sidebar drops it
when that subtree unmounts. The guard is still right to stay disarmed there,
since nothing animates, but the focus handoff has to run, so the two no
longer share an early return.

* fix: keep the pointer guard tied to a pane that actually moves

Disabling the strip unmounts the scrim at once while the drawer needs the
whole transition to widen, so a close begun in that window still slid the
pane with nothing holding the pointer. The scrim now stays mounted while a
close is in flight.

Arming that guard is tied to the same geometry the close path already
branches on. A close under a drawer that covers the pane is a reveal, with
the pane already in place, so holding the pointer there would only make the
default configuration feel unresponsive for the length of the transition.

* fix: guard the swipe close and hand focus back off the mobile breakpoint

The guard read the drawer's width to decide whether the pane was moving, but
a swipe animates the pane at any width, so the default configuration went
unguarded through the one close path that does move it. It now asks the pane
itself: the reveal leaves transition none behind, every animated path leaves
the shared transition on it before the state commits.

Leaving mobile unmounts the drawer and the scrim, so focus sitting on either
went to the document. The same handoff runs for that direction, and it now
confirms the opener actually took focus rather than assuming: the opener
stays mounted across breakpoints but is hidden on desktop.

The scrim is imported through the mobile directory's barrel.

* fix: address PR review bot findings

Codex:
- Start the scrim fade with the drawer slide, not the deferred Recoil commit
- Keep the scrim focus ring inside the overflow-hidden shell

* fix: address PR review bot findings

Codex:
- Capture pointer events on the scrim as soon as an open slide starts
- Expire the close guard at the animation deadline, not a fresh 300ms
- Keep pointer capture through a reveal close while the drawer still slides

* fix: hand focus back once the close guard releases

Codex:
- Defer focus restoration until the pane is no longer inert

The guard reapplies inert to the pane in the same commit the close lands,
and both the opener and the pane itself sit inside it, so the handoff was
ejected to the body with no dependency left to re-run it. The release now
flushes before focus moves.

* fix: drop the scrim pointer override when the close slide starts

Codex:
- Clear the opening pointer override on close

The opening kick writes an inline pointer-events override that only the
buffered release cleared, so a dismiss inside that window left the invisible
scrim swallowing taps past the guard. The close now hands capture back to the
classes, which already hold it for the guard's duration.

* fix: carry the focus handoff and the slide's own clocks through a close

Codex:
- Preserve focus when the motion preference changes mid-close
- Keep the scrim armed when an opening is canceled
- Stabilize the drawer width before closing mid-toggle

The handoff is now keyed off the guard releasing rather than the timer, so a
guard cancelled by a dependency change still hands focus back once the pane
sheds inert. A close that cancels an uncommitted open never reaches the
isClosing classes, so it keeps the scrim's pointer override instead of
returning capture that nothing else holds. And the close pins the drawer's
measured width, so a width transition still in flight cannot drive its edge
from a second clock and open a gap against the pane.

* fix: guard a close the committed state never reports

Codex:
- Guard canceled opens when the strip is disabled
- Stabilize the drawer width before an in-flight swipe

A second toggle inside the deferred flip, or an open drag that falls short,
moves the pane without expanded ever changing, so the guard had no transition
to arm from and the default configuration left the pane live as it uncovered.
The slide now reports itself and arms the guard directly, which also makes the
scrim's pointer override unconditional again: every close hands capture back
to the classes.

Claiming a gesture drops the transition, which lands a width still easing
toward the strip target on that target in the same frame, so the touchstart
snapshot went stale and held a gap open between the surfaces for the rest of
the drag. The claim remeasures.

* fix: cover the opening travel and compose the scrim's button

Codex:
- Guard the pane during default drawer opens
- Compose the shared button primitive for the scrim

Recoil's flip is deferred past the opening frames and the closing transition
outlives it at the other end, so the committed state brackets the travel too
late and drops it too early. The guard is now named for what it measures and
arms for any slide the committed state does not report, so the default
configuration covers the pane while the drawer travels over it. Only a close
records the focus handoff; an open hands focus to the drawer's header.

The scrim now composes the shared Button, keeping only the inset ring the
overflow-hidden shell requires.
2026-08-23 14:34:39 -04:00
Danny Avila
b40fcb4c5a
🔥 chore: Bump @librechat/agents to v3.6.14 (#15135) 2026-08-23 10:36:37 -04:00
Marco Beretta
69bf8ffdf1
📏 fix: Size Agent Builder Tool and Skill Rows (#15134)
The info and remove buttons on each Tools/Skills row passed `size-6 p-0`
through className without a `size` prop. tailwind-merge 1.14.0 has no
`size-*` group, so `size-6` never conflicted with the default size
variant's `h-10 px-4 py-2` and the buttons rendered at 40px. Because they
sit at opacity-0 until hover, the rows read as 52px of mostly empty
padding.

Pass size="icon-xs" so the recipe replaces the default outright. Rows go
from 52px to 40px.
2026-08-23 10:36:01 -04:00
Danny Avila
1d9c2fc591
🪴 feat: Fork Completed Subagents Into Continuable Chats (#15133)
* feat: continue completed subagents as chats

* chore: sort continuation imports
2026-08-23 10:09:14 -04:00
Danny Avila
77cb72e50c
🧮 perf: Enable Agent Context Count Reuse (#15130)
* perf: enable agent context count reuse

* fix: declare token counter return type

* fix: initialize cached token counters

* style: sort token counter imports

* fix: Keep cached token counts exact
2026-08-23 03:05:02 -04:00
Danny Avila
e9a5b61f8c
⛩️ ci: Gate Backend Jest on Codegraph Selection (Stage 1) (#15132) 2026-08-23 03:04:27 -04:00
Danny Avila
8f7864ba2a
⏱️ test: Stabilize Meilisearch Retry Cleanup Assertion (#15129) 2026-08-23 02:37:57 -04:00
Danny Avila
8969ee4b18
🎚️ feat: Configure Agent Event Runtime in YAML (#15128) 2026-08-23 02:37:33 -04:00
Danny Avila
dd146ff74d
🧾 fix: Report Complete Agents API Usage (#15127)
* fix: report complete agents api usage

* fix: preserve invoked usage context

* test: cover absent usage context

* fix: type responses usage finalization

* fix: preserve reasoning usage aliases

* fix: declare reasoning usage alias
2026-08-23 02:37:09 -04:00
Danny Avila
719b04f389
🍃 ci: Cache MongoDB Memory-Server Binaries in Backend Test Jobs (#15131) 2026-08-23 02:36:15 -04:00
Danny Avila
2018c70040
🧫 test: Lock Subagent File Context Propagation (#15126) 2026-08-23 02:06:57 -04:00
Danny Avila
c411eb4cc6
📦 chore: bump @librechat/agents to v3.6.12 (#15125) 2026-08-23 01:59:59 -04:00
Danny Avila
c2aa688d73
🖥️ feat: Stream PTC Inner Tool Calls as a CLI-Style Trace (#15115)
* 🖥️ feat: Stream PTC Inner Tool Calls as a CLI-Style Trace

Programmatic tool calling runs a whole program inside the sandbox, and the
tool calls that program makes open no run step of their own. The card showed
one running spinner for the entire execution, with no sign of what the code
was doing.

Emit a new `on_ptc_tool_call` step event for each inner invocation — once on
dispatch, once on settle — and render them under the code as a terminal-style
trace: status glyph, tool identity, argument preview, duration, with a failure
message printed under the call that produced it.

The seam is the tool map the sandbox bridge resolves inner calls against.
`instrumentPtcToolMap` proxies `invoke` on each entry and leaves every other
property (name, schema, mcp) passing straight through, so nothing about
execution changes and emission failures can never fail a tool call.

Client state is a per-tool-call Recoil atom keyed like the sandbox-starting
and subagent atoms — live for the session, cleared on conversation switch so
a finished program's trace stays readable.

* 🩹 fix: Address Codex Review on the PTC Tool Trace

Five findings, all confirmed against the source before fixing.

Scope the trace atoms to a message occurrence. The hook already documents
that providers repeat a tool_call_id across turns and even within one
message, and `call_id` restarts at :0 for every outer call — so two programs
sharing `call_0` merged into one card. Key by (response message id, tool call
id) via `ptcTraceKey`, mirroring `subagentProgressKey`; the event's `runId`
already carries the message id and the card reads its own from MessageContext.

Prune unsettled rows on resume. Inner calls are not content parts, so the
resume snapshot cannot rebuild them, and `trackReplayEvent` only persists
OAuth events — a call that settled during a disconnect left a spinner that
never resolved. Settled rows are real history and stay.

Make the argument preview budget-aware. Iterate keys rather than entries so
the budget check can actually skip work, and clip against a bounded window so
a multi-megabyte value is never collapsed in full to build a 40-character
preview.

Catch the resumable emission promise. The synchronous try/catch around the
emitter cannot observe a rejected `emitChunk`, so a failing transport raised
an unhandled rejection per event instead of dropping telemetry.

Announce completion to assistive technology. The check glyph is decorative and
a fast call renders no duration, so a settled row previously announced no
outcome; each row now carries an sr-only status and the visible cell that
duplicated it is hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* 🧹 fix: Repair CI Failures on the PTC Tool Trace

Two failures on the previous head, both mine.

`Tests: api (shard 2/3)` — 46 failures in `initialize.spec.js`, all
`TypeError: createPtcProgressEmitter is not a function`. The suite mocks the
callbacks module with an object literal, and wiring the new emitter into
`initialize.js` without adding it there left the factory undefined at call
time. Added it alongside `createAttachmentEmitter`, plus an assertion that it
receives the same generation fence as every other resumable emitter — a stale
epoch would leak one run's inner calls into the next.

`Static checks` — import-order drift in `PtcToolTrace.tsx` and `handlers.ts`,
repaired with `scripts/sort-imports.mts`. ESLint and Prettier both passed, so
only the dedicated check caught it.

`openai.js` and `responses.js` never take the emitter, so their specs were
unaffected; verified the initialize mock now covers every name the module
destructures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* 🔐 fix: Address Second Codex Review on the PTC Tool Trace

Three of five findings actioned; two answered on the thread.

Respect tool-argument PII filtering (P1). Inner calls never reach
`filteredToolArgumentsResult` — the sandbox bridge invokes them directly — so
the trace was the one path putting their values on the wire in a deployment
that had configured `filters.toolArguments.pii`. When any of the name /
arguments / output fields are filtered, the emitter now omits both the
argument preview and the failure message, which routinely quotes the argument
that caused it. Name, status and duration still report.

Drop the light/dark-specific background (P1). `dark:bg-transparent` stepped
outside the semantic roles and would lose the intended separation under a
custom theme. The pane now sets no background at all and inherits the card's
surface, which resolves to the same color the override produced in both
default themes and stays correct when a theme reassigns its roles.

Bound the live trace (P2). A program looping over a large collection made
every event copy an ever-growing array and rendered a row per call. The trace
now keeps a rolling tail of 100 rows and counts what it evicted, surfaced as
"+N earlier calls" so the cap is never silent. A settle whose row is gone —
evicted, or pruned across a resume gap — no longer reappears out of order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* ✅ test: Keep PTC Trace Tests Aligned With Caller-Capability Filtering

Left out of the merge commit by a staging slip; without them
`handlers.spec.ts` fails on the merged tree.

`#15105` restricts the PTC tool map to tools whose `allowed_callers` admit
code execution, so the existing trace test's registry entry — which declared
none, defaulting to `direct` — was filtered out before the instrumentation
could see it. Declare the fixture `code_execution`.

Add a guard for the resolution itself: a `direct`-only tool must never appear
in the instrumented map. Tracing wraps the eligible map, and this fails if a
later change reorders that and lets the trace widen what the sandbox reaches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* 🛡️ fix: Close Name Disclosure and Follow the PTC Trace Tail

Two findings from the third Codex pass on `17a9ec9`.

Redact filtered inner-tool names (P1). The previous gate suppressed argument
and failure previews but the event still carried `name` verbatim, so a
deployment whose `filters.toolArguments.pii.fields` includes `name` could see
a blocked identifier disclosed through the trace — the one path inner calls
take, since they never reach `filteredToolArgumentsResult`. Inner tool names
are now inspected once per PTC call with the same `extractToolArgumentContent`
+ `inspectContent` pair the executor uses; any that trip the policy are left
unwrapped, so they still execute and emit nothing. An un-inspectable name
fails closed.

Follow the trace tail (P2). The row list is a 200px scroller that never moved,
so once a program exceeded the viewport the card sat on the oldest calls while
live activity accumulated below the fold. Reuse `useFollowScroll` — the hook
the code and command panes already use — which pins to the tail while calls
are running and yields the moment the reader scrolls up. The host card threads
its disclosure state so a collapsed pane is never scrolled invisibly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* 📌 fix: Pin the PTC Trace Through Its Final Settle

The fourth Codex pass on `4bf68e1`, one P2 finding.

`useFollowScroll` returned early whenever `active` was false, so the one
change it most needed to follow was the one it skipped. A failing inner call
settles by appending its error line in the same commit that clears the last
running row: the content grows and the stream ends together, and the pin that
would have revealed that line never fired. On an expanded, bottom-pinned pane
the failure — the row a reader most wants — stayed below the fold.

The falling edge of `active` now pins too, but only when the content changed
with it. Ending a stream on its own still leaves the pane where the reader
left it, which is what the existing contract promises and what the sibling
code and command panes rely on; a reader who has scrolled up is untouched
either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

* 🔌 fix: Keep PTC Calls That Outlive a Reconnect

Fifth Codex pass on `085a83f`; one of its two findings.

Pruning rows across a resume gap deleted every `running` row, but a stream gap
is not proof the call ended. A call still executing across the reconnect
settles normally on the restored live stream — and `applyPtcToolCall` drops a
settle whose row is gone, by design, so an evicted row cannot reappear out of
order. The call therefore vanished from the trace despite having run, which is
worse than the spinner the pruning existed to prevent.

Rows are now marked `interrupted` instead of removed. A call whose settle was
genuinely lost in the gap reports that honestly rather than spinning forever,
and one that survives the gap settles onto the row it opened, reporting its
real outcome and duration. `interrupted` is a client-side conclusion, so it
widens the row status locally and leaves the wire contract alone.

Two cases added: the gap marks rather than drops, and a post-reconnect settle
lands on its marked row; plus a render case for the new outcome.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBRQUsPXSvDBnjrSDoXWHP

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-23 01:18:14 -04:00
Danny Avila
caa938fec6
🎬 test: Cover Detached Subagent Activity Lifecycle (#15117)
* test: cover detached subagent activity lifecycle

* test: strengthen detached activity lifecycle gates

* test: tighten detached activity assertions
2026-08-23 01:16:25 -04:00
Danny Avila
1de88e7e91
📨 feat: Continue Bound Child Agents from Events (#15112)
* feat: add authenticated agent event ingress

* style: sort agent ingress imports

* fix: harden agent event ingress

* fix: bind event provenance to API keys

* fix: inspect event input with legacy PII filters

* fix: scope event status reads to source keys

* fix: bind event status reads to remote sources

* feat: add bound event-driven child turns

* fix: harden event-bound child continuations

* fix: satisfy event binding type contracts

* fix: close event actor lifecycle races

* fix: harden event actor dispatch continuity

* fix: fence event actor resume lifecycle

* fix: bind event actor state to lifecycle

* fix: preserve cascade write outcomes

* test: type cascade failure injection

* style: sort cascade test imports

* fix: harden event child lifecycle boundaries

* fix: make event cleanup retryable

* fix: annotate event retention clock

* fix: reconcile partial cascade metadata

* fix: recheck event binding expiry on resume

* fix: fence event actors by retention deadline

* fix: close event actor lifecycle races

* fix: harden event child lease acquisition

* fix: lazy-load event child lease adapter
2026-08-23 01:15:57 -04:00
Danny Avila
8f9fae0a6e
🛂 fix: Preserve Legacy Assistant Attribution (#15118) 2026-08-22 16:21:45 -04:00
Joseph Licata
fc7d56dffe
🪙 chore: Update GPT-5.6 Sol Token Cost Rates (#15114) 2026-08-22 11:02:29 -04:00
Danny Avila
89494d45fd
🚦 fix: Restrict Programmatic Tool Execution Maps (#15105)
* fix: restrict programmatic tool execution maps

* chore: bump `@librechat/agents` to v3.6.10

* fix: honor live programmatic caller projections

* style: sort caller capability imports

* test: expect caller projection loader argument

* chore: bump agents sdk to v3.6.11

* refactor: use SDK caller projection type

* style: sort agent handler imports
2026-08-22 11:02:09 -04:00
Danny Avila
d3e70159ca
📡 feat: Stream Detached Subagent Activity (#15111)
* feat: stream detached subagent activity

* fix: annotate activity stream limits

* fix: isolate subagent activity imports

* fix: harden detached subagent activity lifecycle

* test: cover synchronous activity transport failure

* test: include required subagent activity identity

* fix: identify and reconnect subagent activity events

* fix: bound subagent activity lifecycles

* fix: close subagent activity handoff races

* fix: bind and synchronize activity subscriptions

* fix: detect fresh activity attachment

* fix: complete activity synchronization handoff

* fix: bind activity sync and failure circuits

* fix: expose subscription-bound synchronization

* fix: fence activity reconnect publications

* test: make detached timeout settlement deterministic

* fix: fence Redis activity attachments

* fix: close failed activity streams

* perf: reuse fenced activity frontier

* style: sort subagent thread imports

* fix: preserve queued subagent activity

* test: type activity publication counter

* fix: disconnect subagent activity subscriber

* fix: close background activity lifecycle gaps

* fix: preserve streamed activity spacing

* fix: preserve bounded live subagent activity

* fix: merge durable subagent activity safely

* fix: model detached activity coverage

* fix: type detached activity inputs

* fix: order overlapping subagent activity

* chore: sort activity test imports

* fix: buffer subagent activity handoff gaps

* fix: flush activity after parent close

* fix: advance closed activity suffixes

* fix: preserve detached activity ordering

* fix: close detached activity delivery races

* fix: bound shared Redis subscriber readiness

* fix: expire shared Redis subscription readiness

* fix: clean up late Redis subscriptions

* fix: preserve late Redis subscription fallback
2026-08-22 09:45:27 -04:00
Danny Avila
3ebef4c84e
📨 feat: Add Authenticated Agent Event Ingress (#15110)
* feat: add authenticated agent event ingress

* style: sort agent ingress imports

* fix: harden agent event ingress

* fix: bind event provenance to API keys

* fix: inspect event input with legacy PII filters

* fix: scope event status reads to source keys

* fix: bind event status reads to remote sources
2026-08-22 01:09:04 -04:00
Danny Avila
f384e71f77
🧯 fix: Prevent Quote Popup Update Loop (#15113) 2026-08-22 01:08:44 -04:00
Danny Avila
67b7b441b2
🛂 feat: Filter Model-Bound Content by Source (#14425)
* feat: introduce optional content protection seam

* feat: enforce source-aware content filters

* feat: complete source-aware content enforcement

* test: activate skill file-text fail-close fixtures

* fix: harden source-aware content filters

* fix: harden model-bound content filtering

* fix: preserve legacy filters and generated files

* fix: inspect shared scalar metadata

* test: align mocks with current dev dependencies

* feat: add persisted content filter safeguards

* feat: complete source-aware content filter enforcement

* fix: move resume content preflight into TypeScript

* fix: close content inspection edge cases

* fix: harden content protection boundaries

* fix: complete content protection safeguards

* test: align persisted memory filter coverage

* fix: reconcile content protection with current dev

* fix: reconcile content protection with latest dev

* fix: close content protection review gaps

* fix: enforce source-aware provider boundaries

* fix: preserve legacy PII preflight semantics

* test: stabilize stored branch preflight fixture

* fix: defer agent writes until protected model admission

* perf: harden source-aware model-bound filtering

* fix: canonicalize provider lineage before validation

* fix: satisfy model-bound callback type checks

* perf: Bound content protection filtering work

* fix: Bound submission array traversal

* fix: Stabilize bounded content snapshots

* fix: Scope model-bound traversal overflows

* fix: Preserve scoped content inspection

* fix: Accumulate aggregate traversal scopes

* fix: centralize content policy boundaries

* test: align deferred tool policy context

* test: align controller policy mocks

* style: normalize content protection imports

* fix: close content policy review gaps

* fix: narrow active skill policy config

* fix: address content protection review boundaries

* fix: retain exact provenance overflow sentinel

* fix: preserve literal and scoped provenance updates

* fix: narrow persisted edit provenance

* fix: isolate exact overflow attribution

* fix: centralize stored prompt protection

* fix: fail closed on incomplete transcript evidence

* fix: align canonical transcript routing

* refactor: centralize content policy preflights

* fix: isolate upload policy error typing

* style: sort policy preflight imports

* refactor: centralize content policy boundaries
2026-08-21 22:43:32 -04:00
Danny Avila
10f95c0ce9
🐇 refactor: Decouple Meilisearch From Document Saves (#15109)
* 🐇 fix: Decouple Meilisearch from document saves

* 🧭 fix: Preserve detached indexing invariants

* 🧬 fix: Version detached Meilisearch writes
2026-08-21 22:26:44 -04:00
Danny Avila
08c9cc3d3d
🖼️ fix: Restore Shared Subagent Activity as a Read-Only View (#15108) 2026-08-21 20:58:14 -04:00
Marco Beretta
199de92c51
⚡ perf: Warm Feature Catalogs in the Background After First Paint (#15047)
* perf: warm feature catalogs in the background after first paint

Prompt groups and MCP server/tool queries no longer fire on the app
startup path. A catalog warmup store releases them after first paint
on browser idle, staggered with jitter so a fleet of clients does not
burst the API all at once. Panels opened before warmup activates
their catalog immediately and fall back to their existing loading
states. The prompts list endpoints now also run their independent
access lookups in parallel instead of in three serial rounds.

* perf: gate MCP icon observers and re-arm warmup across sessions

MCP icon/name observers mounted from rendered messages now wait for the
warmup gate like every other server-catalog consumer, so conversations
with MCP tool calls no longer pull the server list onto the first-render
path. The warmup schedule resets on logout so a second login in the same
tab warms on its own stagger instead of releasing every catalog at once.
Panel mount activations now require a visible sidebar, since a persisted
active panel stays mounted while hidden.

* perf: void stale warmup callbacks and gate the agent panel tools query

Reset now bumps a generation captured by every idle callback and its
stagger timer, so callbacks pending across a logout can no longer release
catalogs into the next session. The agent form's MCP tools query keeps
its own readiness gate so a hidden persisted panel cannot pull the tools
request ahead of its stagger once the server list resolves.

* perf: reset warmup on Root unmount and honor the insights route collapse

Root can unmount in the same render that flips authentication on logout,
so the warmup effect now resets from its cleanup as well as the
unauthenticated branch. Panel activations mirror UnifiedSidebar's
panelExpanded condition, treating the insights route as collapsed instead
of reading the raw sidebar atom.

* test: re-expand the approval tool card the saved message remounts

The helper opened the card once and then waited on its body. Saving the
response swaps the placeholder message id for the persisted one, which
rekeys every part in the turn: the card remounts collapsed, its body
unmounts, and the output assertion waits out its timeout against a
disclosure nothing is going to reopen. The redis transport lane pays a
round trip per stream event, so its finalization lands late enough to
catch the helper mid-assertion.

Wait for the closing model turn before expanding anything, then re-open
the group and the card on each attempt until the scoped output is on
screen.

* Update AgentPanelContext.tsx import order

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-21 20:33:29 -04:00
Danny Avila
749eed0d60
🪟 feat: Unify Subagent Activity Panel (#15106)
* feat: unify subagent activity panel

* fix: fence durable activity to selected task

* fix: preserve exact panel activity semantics

* fix: scope panel identity to parent turn

* fix: keep detached readiness status neutral

* fix: harden subagent activity invariants

* test: support backend TypeScript target

* fix: preserve subagent invocation identity

* fix: bound subagent activity correlation

* fix: drain exact-parent subagent updates
2026-08-21 19:51:11 -04:00
Danny Avila
21b7f78d56
🙋 feat: Collapse Settled Question Records by Default (#15107)
* 🙋 feat: Collapse Settled Question Records by Default

The durable `ask_user_question` record rendered as a permanently open
card. Answers are frequently long, multi-paragraph text, so a settled
Q&A buried the reply that followed it.

It now reads as one collapsed tool-call line — the same `ProgressText`
primitive `ToolCall`/`SkillCall` use — naming the question (or the
batch count, reusing the keys `ToolCallGroup` already had) and opening
on demand under the existing `autoExpandTools` preference. Only the
settled record collapses; the live pause and the interim progress card
are untouched.

The expanded panel was also hard to read. Authored text rendered
without `pre-wrap`, so a numbered or paragraphed answer collapsed into
one wall; the answer ran on from its inline label; and batch items sat
flush against their divider. Line breaks are now content, the answer
sits under its own label behind a rule, and dividers have air on both
sides.

`ProgressText`'s subtitle now truncates and absorbs the flex shrink, so
arbitrary authored text ellipsizes instead of pushing the line past the
message column — this also fixes long MCP server names on tool cards.

* 🩹 fix: Address Codex Round 1 on the Collapsed Question Record

- Settle the summary tense. A live, unanswered pause returns before the
  header, so every state reaching it is settled — an abandoned pause read
  "Asking" forever, and the collapse hid the "no answer" line that used to
  qualify it. Past tense unconditionally, matching `ToolCallGroup`.

- Move the rejection announcement out of the disclosure. `useExpandCollapse`
  marks the closed panel `inert`, so the failure explanation's `role="status"`
  could never reach the accessibility tree; it is now an sr-only status
  outside the panel, carrying both the label and the explanation.

- Count records, not repeated text, in the Bombadil observation. With
  Auto-expand tool details on, one settled record shows the question in both
  its summary line and its panel, so the old selector double-counted it and
  broke the `<= 1` singularity invariant.
2026-08-21 19:50:40 -04:00
Danny Avila
8ae94afa91
🪡 fix: Thread Parent Message ID Through MCP Request-Scoped Bodies (#15095)
* fix: Unify MCP request-scoped headers

* fix: address request-scoped MCP review findings

* test: preserve request scope on status errors

* fix: treat authorized on-demand MCP servers as ready

* refactor: separate MCP readiness from connection state

* fix: preserve on-demand MCP readiness labels

* test: satisfy OpenAI conversation ownership guard

* fix: keep MCP action predicates boolean

* fix: close deferred MCP request context gaps

* fix: preserve on-demand MCP configuration actions

* fix: fail closed on unavailable MCP parent context

* test: complete MCP connecting-state mocks

* fix: preserve missing MCP parent on continuations

* fix: align native MCP request identities

* fix: preserve edited MCP parent identity

* test: use scoped Agent initializer fixture

* test: expose MCP request body helper

* fix: preserve MCP turn identity across resume

* style: sort stream metadata imports

* fix: carry normalized MCP identity to execution
2026-08-21 16:33:01 -04:00
Dustin Healy
f02ce63d57
✂️ fix: Strip Redundant Server-Name Prefixes from MCP Tool Keys (#14732)
* ✂️ fix: Strip Redundant Server-Name Prefixes from MCP Tool Keys

MCP servers that prefix every tool with their own name produce model-facing keys that embed the server twice once the _mcp_<server> suffix is appended, pushing long tool names past provider 64-character function-name limits. Tool keys now drop a leading <normalizedServerName>_ prefix (case-insensitive, skipped when a sibling tool already owns the stripped name). The original upstream name is recorded as serverToolName on the cached definition and is always what tool calls send to the server, and runtime lookups also try the stripped spelling of persisted pre-strip keys so existing agents keep resolving.

* 🩹 fix: Keep Stripped MCP Tool Keys Provider-Safe and Collision-Free

Assistant writers submit catalog entries verbatim, so the internal serverToolName mapping is now removed from provider-facing definitions before they reach create/update payloads. Prefix stripping is collision-guarded over the resulting name set rather than raw siblings only, which also covers case-variant prefixed pairs under the case-insensitive match.

Assistant payload healing now rewrites a pre-strip persisted key to the stripped catalog key when that key actually exists in the loaded definitions, and legacy agent references keep their persisted spelling as the runtime instance name so per-tool options stay applied while the upstream call still uses the matched entry's raw name.

* 🧷 fix: Harden Stripped MCP Tool Keys Against Heal, Collision, and Cache Edges

The pre-strip heal now resolves the key boundary against both raw and normalized server spellings, mapping back to the raw name for the shadow and membership guards, so keys persisted after server-name normalization heal too. Collision detection iterates to a fixpoint so a fallback to a raw name cannot silently collide with another sibling's stripped result, and a stripped remainder equal to a synthetic marker (wildcard or server pin) is never produced.

MCP catalog cache slices are versioned so replicas that predate serverToolName never read stripped entries during a rolling deploy; stale slices expire on their own.

* 🔎 fix: Resolve Pre-Strip Keys in Event-Driven Definitions and Reinspect Persisted Catalogs

The event-driven definitions loader now tries the stripped spelling of a persisted key when the exact lookup misses, keeping the persisted name so it matches the runtime instance, which stops legacy agents from failing initialization with expected tools unavailable. The registry storage schema version is bumped so followers rebuild persisted toolFunctions instead of republishing pre-strip definitions into the versioned catalog namespace.

The assistants heal also fails closed when a normalized-suffix reference lands on a contested server-name slot, since rewriting persisted data must not bind an ambiguous reference to the tie-break winner.

* 🛰️ fix: Reserve the Synthetic OAuth Name and Heal User-Owned Server Keys

A stripped remainder equal to oauth would make the client stream handlers treat a real tool call as a synthetic authentication prompt, so it joins the reserved remainders alongside the wildcard and pin markers.

The assistants heal now audits the FULL accessible server set on every run instead of operator config names only, since assistants reference user-owned servers whose catalogs the definitions loader already resolves; an unavailable audit still skips healing entirely.

* 🧬 fix: Verify Upstream Identity for Legacy Keys and Reserve Sibling Raw Names

Stripped results now reserve every sibling's raw name even when that sibling itself strips, so a stripped key can never shadow another tool's pre-rollout persisted references within the same snapshot. Every legacy fallback (runtime lookup, event-driven definitions, assistants heal) accepts a stripped-spelling match only when the entry's recorded serverToolName proves the same upstream tool, so a stale key for a removed tool degrades to unavailable instead of calling a different sibling.

To keep that identity visible to the heal, assistant tool definitions retain serverToolName and the controllers sanitize entries through toProviderToolDefinition at the provider submission boundary instead. The agent editor migrates pre-strip persisted ids the same identity-verified way, with the upstream name exposed on the MCP tools payload.

* 🧭 fix: Heal Wildcard Tool Options and Reserve the OAuth Namespace

Wildcard-expanded catalogs rename stripped tools without any agent.tools entry to preserve the spelling, so buildToolClassification now aliases persisted pre-strip tool_options keys onto the current instance names in place, identity-gated on the definition's recorded upstream name and never overriding an explicit entry. Both loading modes flow through it: instances carry mcpServerToolName from createToolInstance and event-driven definitions thread serverToolName from the catalog.

stripServerNamePrefix also reserves the entire oauth namespace rather than the exact name, since the client stream handlers classify every oauth-prefixed key as a synthetic authentication call.

* 🛡️ fix: Derive the Full Reserved Namespace and Heal Approval Policies

The reservation guard now covers every namespace consumers classify by prefix: the wildcard and server-pin markers alongside oauth, plus the server-scoped mcp_ pluginKey namespace that pre-strip keys could never enter. Stripping also never produces a key whose isActionTool classification differs from the raw key's, since a server whose normalized name contains _action_ would otherwise see a real MCP tool routed down the OpenAPI action path past MCP authorization.

Admin toolApproval globs written against upstream tool naming keep applying: pattern lists are healed at run wiring with the current names of tools whose pre-strip spelling matches, list-level so deny, ask, and allow precedence is unchanged and a non-matching deny can no longer fail open. The MCP tools wire type also declares serverToolName end to end.

* 🪪 fix: Alias Both Key Spellings for Approval Policies and Hook Matchers

Identity aliases are now collected once at tool classification, in both directions: a stripped instance aliases its pre-strip spelling and a legacy-named instance aliases its current catalog spelling, with the current name recorded on legacy matches by the runtime lookup and the event-driven definitions loader alike. The aliases ride the agent config through both loading modes, so approval pattern healing applies to deny rules written against either spelling, closing the bypass where a rule targeting the current name missed an unedited agent's legacy instance.

Programmatic approval hook matchers get the same treatment: each hook is additionally registered under an anchored exact-name pattern for tools whose other spelling its regex matches, keeping the admin's matcher semantics intact while argument, user, and tenant specific deny or ask decisions keep executing for renamed tools.

* 🔁 fix: Alias Tool Options in Both Spelling Directions

Options aliasing now consumes the same bidirectional alias pairs as policy healing and hook matchers, so options the editor migrated to the current catalog spelling still reach a legacy-named instance retained by an unedited agent.tools entry. The previous serverToolName-only derivation skipped exactly that case since the legacy key equals the instance name there.

* 🤝 fix: Reserve the Agent Handoff Namespace Before Stripping

The client renders any lc_transfer_to_ prefixed call as an agent handoff and the background and intent passes exclude such names, so a stripped remainder inside that namespace would misclassify a real upstream tool. It joins the mcp_ pluginKey namespace as a bare-prefix reservation, which pre-strip keys could never enter.

* ⚡ fix: Reuse the Loader's Server Snapshot and Index the Editor Catalog

getAssistantToolDefinitions now returns the accessible-server snapshot from the same merged registry read that resolved the catalogs, and the heal consumes it instead of repeating the app-config and registry round trips on the assistant write path; without a snapshot the heal still fetches and fails closed as before.

The agent editor's id migration uses a memoized tool_id map, so the per-key form heal does constant-time lookups instead of scanning the catalog per option.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-21 14:30:29 -04:00
Yorgos K
9f8d71a3c5
🪢 fix: Preserve Response Identity and Branch During Resumable SSE Sync (#14788)
* fix(client): preserve resumable response identity

Fixes #14787

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(client): align resumable sync regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(client): clarify resumable response ownership

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(client): preserve resumed regeneration ordering

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(client): cover missing resumed response row

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(client): preserve resume identity on page reload

* fix(client): replace reassigned resume placeholder

* fix(client): preserve content during response id handoff

* fix(client): limit resume placeholder handoff

* fix(client): preserve resume display metadata

* fix(client): reconcile resume metadata in one pass

* fix(client): reconcile preliminary resume user

* fix(client): restore regenerated branch on early abort

* test(client): cover external regeneration resume

* fix(client): preserve regeneration history on errors

* fix(client): replace reused regeneration error ids

* fix(client): preserve exact-id regeneration rollback

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-21 14:25:03 -04:00
Oliver Faust
b399ad8370
📄 fix: Serve Stored Text for "Upload as Text" File Downloads (#14723)
* 📄 fix: Serve Stored Text for Text-Source File Downloads

"Upload as Text" attachments store extracted content in the DB with
source 'text'; OCR uploads persist the OCR strategy name (e.g.
'mistral_ocr') as a filepath placeholder since no backing file exists.
The download route resolved these records to the local strategy and
passed the placeholder to fs.createReadStream, which failed with
ENOENT — and the response was never ended after the stream error, so
the request hung until the client timed out.

Serve the stored text directly as a .txt download for text-source
files (re-fetched by _id, as getFiles excludes 'text' by default),
and end the response on stream errors: 500 without the download
headers before headers are sent, otherwise abort the truncated
response so clients detect the failure.

* fix: Preserve text-source preview semantics

* fix: Complete text-source download coverage

* fix: Tie text downloads to blob lifecycle

* fix: Isolate preview downloads and share text snapshots

* fix: Keep shared previews in share scope

* style: Sort text download imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-21 14:24:43 -04:00
Danny Avila
d602452c05
🪪 fix: Support MCP Server Titles With Hyphens (#15094)
* fix: Support MCP Display Titles With Hyphens

* fix: Preserve Legacy Regex Target Compatibility
2026-08-21 12:44:31 -04:00
Danny Avila
5a598a138f
🔭 fix: Attach to Runs This Pane Did Not Start (#15074)
* 🔭 fix: Attach to Runs This Pane Did Not Start

A run started somewhere else — another tab, another device, a scheduled
trigger — announces itself to this client only through the user-scoped active
job list. Nothing consumed it for attachment: `useActiveJobs` feeds the
sidebar's generating indicators and a `hasActiveJob` hint inside the messages
query, and that is all.

That left the status query as the only path to an attachment, and it closes
for the rest of a conversation's mount the moment it has answered inactive
once, because `processedConvoRef` is set on that answer. So a pane already
sitting on a conversation when a run begins elsewhere never attaches, never
refetches (the messages query disables refetch on focus, mount and reconnect),
and shows history it cannot see has moved on — until a reload or a navigation
remounts the query. Worse than the stale render: a send from that pane derives
its parent from the stale tail and forks a sibling branch. The two send-time
staleness guards in `useChatFunctions` do not fire, because nothing invalidated
this pane's cache, so it looks fresh.

Re-arm the status query when the viewed conversation appears in the active
list. The announcement is consumed once per run rather than held open — a job
stays listed for its whole lifetime, and re-opening on every poll would turn a
five-second heartbeat into a five-second status read — and released when the
run leaves the list so the next one re-arms in turn.

* 🩺 fix: Make the External-Run Re-Arm Survive Warm Caches and Back-to-Back Runs

Five gaps between the announcement and the attachment it was supposed to
produce, none of which the happy-path test could see.

The announcement could never arrive. `useActiveJobs` disables its interval
while nothing is listed and `refetchOnWindowFocus: true` refetches only stale
queries, so a run another client started inside the five-second `staleTime`
window was invisible on return to the tab — the exact sequence this is for.
Focus refetches unconditionally now.

Re-arming could consume a stale answer. Toggling `enabled` only fetches when
the cached data is stale, and `useStreamStatus` holds `staleTime: 1000`, so an
inactive status answered moments earlier was replayed as "nothing running" and
recorded as handled. The re-arm invalidates the status query rather than
trusting the toggle.

Attaching could graft onto a hole. An external client may have completed whole
turns this pane never saw before starting the one now running; the resume
submission and `finalHandler` both build on the local snapshot. And when the
announced run turned out to be already terminal, nothing refreshed history at
all — the messages query disables refetch on focus, mount and reconnect, so
those turns simply stayed missing and a send from here still forked. The
re-arm invalidates history too, which also re-gates `messagesLoaded` so the
check waits for it.

Consecutive runs could be missed. A latch released by observing the list empty
never releases when a second run starts before the next poll, since the list
reads the same throughout. Rate-limit to the list's own heartbeat instead,
keyed on `dataUpdatedAt` — structural sharing keeps the payload reference
stable across identical refetches, so only the fetch stamp moves.

Wiring, found by these tests rather than by review: clearing a ref neither
schedules a render nor re-runs an effect, so the arm is a state value the
check depends on.
2026-08-21 12:14:12 -04:00
Marco Beretta
33e42e6d5d
🎛️ feat: Configurable SearXNG Search Options (#14987)
* feat: configurable SearXNG search options

SearXNG queries were hardcoded to google,bing,duckduckgo with no way to
change the engine list, the result language, or the request timeout. Most
self-hosted instances get served CAPTCHAs by DuckDuckGo, so a third of
every query silently returns nothing and operators have no lever to pull.

Add a searxngSearchOptions block to the webSearch config that accepts
engines (as a comma-separated string or a list), language, timeRange, and
timeout, and thread it through to the search tool. Engines are normalized
to the comma-separated form SearXNG expects, with blank entries dropped so
a stray comma cannot produce an empty engines parameter.

Refs #14117

* fix: normalize SearXNG engines on the runtime config path

The engines transform lived only on the zod schema, but loadCustomConfig
returns the raw YAML object rather than result.data, so nothing downstream
ever saw the transformed value. A YAML list reached the SDK as an array and
threw "options?.engines?.trim is not a function" when the search tool was
built, taking web search down entirely for the exact block the example yaml
documents. An untrimmed string reached SearXNG with spaces still in it.

Extract the normalization into normalizeSearxngEngines and apply it in
loadWebSearchConfig as well as the schema, so both the parsed and the raw
path produce the same comma-separated value. Widen the loader's parameter to
TWebSearchConfigInput, which models engines as the list or string an operator
actually writes, and cover the raw path with tests that call the loader rather
than the schema.

* chore: drop unused RerankerTypes import in web config loader
2026-08-21 11:36:23 -04:00
Marco Beretta
f5f462a1c6
🫥 feat: Add Temporary Chat Empty State and Active Indicator (#15086)
* feat: add temporary chat empty state and active indicator

Temporary Chat gave users a toggle but no page-level confirmation that
they had entered the mode or what it changes. The only cue was the
toggle's pressed state, which is easy to miss, and the toggle itself
retires once the conversation starts, leaving an active temporary chat
with no indication at all.

The landing now swaps its identity block for a temporary-chat empty
state: a dashed message icon, a "Temporary Chat" heading, and a line
explaining that the chat stays out of history and is deleted
automatically. It clears on its own once the first message is sent,
since the landing unmounts at that point.

useTemporaryChat gains isActive for the window where temporary mode is
locked in for a conversation in progress. TemporaryChatIndicator renders
exactly then, so the toggle and the read-only pill never overlap. It is
shown at every breakpoint, collapsing to the icon alone below md while
keeping its accessible name.

The copy matches actual behavior: buildRetentionVisibilityFilter keeps
isTemporary conversations out of the list query, and temporary chats are
stamped with expiredAt from temporaryChatRetention.

* fix: keep temporary conversations out of the sidebar and compose the status pill

The empty state told users a temporary chat would not appear in their
history, but the client seeded it into the conversation list caches
anyway, so the chat sat in the sidebar for the rest of the session until
a refetch or reload dropped it. The history query already excludes
temporary conversations server-side, so the copy described the intended
behavior while the UI contradicted it.

Temporary mode lives on the submission rather than on the draft
conversation, so the optimistic record never carried the flag and every
consumer of that cache entry read a new temporary chat as an ordinary
one. It is now stamped onto the optimistic conversation, only when true
so the legacy expiredAt inference is untouched, and the sync handler
gains the same isTemporary guard the title handler already had.
upsertConvoInAllQueries refuses temporary conversations outright, which
holds the invariant at one point rather than at each caller.

The header indicator now composes the shared Chip primitive instead of
hand-building a pill. Its theme size and shape tokens resolve to the
same 2.25rem height, 0.75rem radius and 0.375rem gap the local classes
hardcoded, so the appearance is unchanged while the indicator follows
future theme work. It also carries role="status" so the mode change
reaches assistive technology, which matters below md where the label is
visually hidden and only the icon remains.
2026-08-21 11:35:12 -04:00