Commit graph

5210 commits

Author SHA1 Message Date
Danny Avila
d4c64d485f
ci: gate the e2e activity-phase DOM assertions on the persisted phase (#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.
2026-08-14 01:42:39 -04:00
Danny Avila
2f0cd2eb75
🔌 chore: Bump the MCP SDK to 1.30.0 and Parse Content-Type Instead of Searching It (#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
#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.
2026-08-14 01:12:56 -04:00
Danny Avila
24d111fde9
feat: Add Gemini 3.7 Flash Support (#14818)
*  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 (#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 #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 #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.
2026-08-14 01:12:42 -04:00
Danny Avila
5e464bc930
📎 fix: Alias Shell Script MIME Variants to application/x-sh (#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 #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 #4660/#5689/#6297, and the
broad patterns from #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.
2026-08-14 01:12:23 -04:00
snapydziuba
6c46fd1252
📄 feat: accept PowerPoint template MIME type (#14761) 2026-08-14 00:22:01 -04:00
Danny Avila
6cbfd82772
🔌 fix: Recover Quietly From Stale MCP SSE Stream Conflicts (#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.
2026-08-14 00:01:13 -04:00
Danny Avila
0654efb7ed
🔌 fix: Preserve MCP serverInstructions Declaration Through Inspection (#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 #14798
2026-08-13 23:37:58 -04:00
Danny Avila
7694428ca9
💬 style: Right-Align In-Flight Steer Bubbles to the Message UI (#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.
2026-08-13 23:37:18 -04:00
Mihidum
da390fa919
🩹 fix: apply agent updates that match the newest version entry (#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>
2026-08-13 23:18:34 -04:00
Danny Avila
abc669ab58
🩹 fix: Restore the @librechat/api Build and Remove Legacy Code (#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 #14805:
`AnchorFields`, `laterDefinedIndex`, `foldedAgentIds`, `boundedAnchor` and
`mergeAnchors`. #14807 was squashed from a branch that predated #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 #14805 introduced, and retains
everything new to #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.
2026-08-13 19:57:32 -04:00
Lizzy
5bd745783c
🖌️ style: Use correct text colour for answer textarea in dark mode (#14791) 2026-08-13 19:37:13 -04:00
Marco Beretta
d920328bfa
💬 style: Unify Message Row Layout and Edit Surfaces (#14770)
* 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.
2026-08-13 19:30:39 -04:00
Danny Avila
78eb0c98ce
🧭 refactor: Resolve Activity Phase Position Once Per Boundary (#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 #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.
2026-08-13 19:29:42 -04:00
Danny Avila
619ed2f1fb
🧱 refactor: Make Activity Phase Anchor Construction Exhaustive (#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.
2026-08-13 19:29:18 -04:00
Danny Avila
05ed7ad8c0
🔖 fix: Split Activity Phases at Substantial Text (#14785)
* 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 831a00353.

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; 831a00353 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.
2026-08-13 16:16:09 -04:00
Danny Avila
bcbe26ab4c
🪑 fix: Rebase Activity Phase Bounds Onto Compacted Content and Unskip the MCPManager Suite (#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. #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.
2026-08-13 07:52:12 -04:00
Danny Avila
6755544cee
🌍 ci: Harden Locize Translation Sync (#14784) 2026-08-13 07:29:29 -04:00
Danny Avila
155f71f81a
📱 fix: Show Quote Popup for Block Selections and on Touch Devices (#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.
2026-08-13 00:36:45 -04:00
Danny Avila
df6e15a0de
🔖 feat: Bound Parent Activity Phases With an Exclusive End Index (#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
2026-08-12 23:43:35 -04:00
Danny Avila
1a3e2aebcb
🛰️ fix: Attach Request-Scoped MCP Servers (#14780)
* fix: attach request-scoped MCP servers

* fix: satisfy MCP static checks

* fix: format MCP runtime hint
2026-08-12 23:43:02 -04:00
Danny Avila
c44d11ebf4
🧾 test: Pin the Transactions Config Wiring on the Fallback Path (#14779)
Follow-up to #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 #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`.
2026-08-12 23:42:23 -04:00
Danny Avila
298a3d9ee9
📦 chore: Update @librechat/agents to v3.4.6 (#14781) 2026-08-12 23:42:13 -04:00
Danny Avila
3a3a8dcad0
🖍️ refactor: Typed Console Colors and Hardened Script Helpers (#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
2026-08-12 22:48:36 -04:00
Danny Avila
8f1f961212
🧱 refactor: Require Broad Config Management for Base Field Mutations (#14775) 2026-08-12 22:32:38 -04:00
James Todaro
e696b07619
🧾 fix: Honor Disabled Transactions on the Token-Count Fallback Path (#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.
2026-08-12 22:31:13 -04:00
Danny Avila
dccef82254
🪶 chore: Aggregate Empty MCP Tool Logs (#14767)
* fix: aggregate empty MCP tool logs

* fix: retain server names in MCP tool logs
2026-08-12 22:29:49 -04:00
Danny Avila
861cfe8a3c
🧩 fix: Normalize Malformed MCP Required Schemas (#14771) 2026-08-12 22:29:25 -04:00
Danny Avila
8f1f43f33e
🫧 refactor: Fading Pre-Response Dot, Legacy Cursor CSS Removed (#14772)
* 🫧 refactor: Fading Pre-Response Dot, Legacy Cursor CSS Removed

The pre-response dot keeps its classic 12px shape but now enters on the
streamed-text fade curve (lc-fade-in) and breathes on opacity, replacing
the legacy pulseSize scale throb that read as jittery. One dot for all
users — the interim result-thinking-fade variant and its per-component
class wiring are removed, and the dot goes static under
prefers-reduced-motion.

Clears out the legacy cursor CSS while here: the scaleX/scale3d mixed
keyframes, -webkit- prefixed animation/transform/backface duplicates,
translateZ and will-change layer hacks, subpixel font-smoothing, the
duplicate #0d0d0d fallback, the unused .blink utility, and all three
duplicate @keyframes blink definitions (the auth logo uses Tailwind's
logo-blink; nothing references these).

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

* 📝 docs: Explain the Pre-Response Dot's Custom-CSS Exception

Per AGENTS.md, custom CSS needs a stated reason a shared primitive cannot
express the requirement: the dot is a pseudo-element gated on an ancestor's
`.submitting` state plus its own `:empty`, kept in CSS so it costs no React
renders per streamed token.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 22:29:05 -04:00
Ravi Kumar L
9980b6221f
🪢 feat: add Langfuse session links (#14776)
* feat: add Langfuse session links

* fix: tighten Langfuse session link resolution

* fix: clear stale Langfuse session links

* test: verify tenant Langfuse session links

* fix: align Langfuse link with client conventions
2026-08-12 22:25:23 -04:00
Marco Beretta
5ff282f900
🎙️ fix: Align Speech Engine Configuration With Runtime (#14736)
* fix: align speech engine configuration with runtime

* fix: guard speech recording shortcuts

* fix: reconcile speech engine availability

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-12 13:22:28 -04:00
Marco Beretta
92a8058f02
🛟 fix: Isolate Invalid Skills During GitHub Sync (#14735)
* fix: treat unrecognized SKILL.md frontmatter keys as warnings

An unknown key in one SKILL.md failed that skill outright, and because the
GitHub sync runner marks a source failed on any validation error, a single
stray key took down every other skill in the repository. Syncing
github.com/cloudflare/skills failed entirely because 2 of its 13 skills
carry a `references:` key.

UNKNOWN_KEY is now a warning, so the skill is stored (unknown keys and all)
and the issue is surfaced rather than fatal. `references` joins the allowed
set with a shallow JSON-safety check instead of a strict kind match: real
files use a string, a list of strings, a list of objects, and a map, and
pinning one shape would reintroduce the same failure.

Malformed frontmatter stays fatal: INVALID_TYPE, INVALID_SHAPE and the
non-plain-object check are unchanged.

* fix: skip individual skills instead of failing a whole sync source

Any error inside the discovery or commit loop reached the outer catch and
marked the entire source failed, so one unusable SKILL.md, one oversized
blob, or one duplicate name cost every other skill in the repository.

Each skill now runs inside its own boundary and a failure is recorded
against that skill. Errors that mean nothing else in the run can succeed
(lock loss, GitHub auth failures, rate limiting) still abort the source
rather than being charged to whichever skill hit them first. Skills are
marked seen before the attempt, so the reconcile pass cannot mirror-delete
the previously synced copy of a skill a later run can repair, and duplicate
names now drop the whole colliding group instead of letting tree order pick
an arbitrary winner.

Status gains `partial` (published some, skipped others) plus a capped
sample of the skipped skills with the reason for each. A run that publishes
nothing and skips something is still `failed`, carrying the first skip's
error. The skipped entries name repository paths, so they follow the same
visibility rule as owner/repo/paths; the bare count does not.

Sync warnings are logged too: a background run has no user-facing surface,
so the log is the only place a maintainer sees why an upstream SKILL.md
looks off.

* test: cover skill sync warnings reaching the log

An unrecognized frontmatter key no longer fails the skill, so a background
sync has nowhere to report it except the log. Every mock in this spec
returned an empty warning list, which left that path unexercised.

* fix: describe nested frontmatter values in the shared skill type

`SkillFrontmatterValue` allowed only scalars and string arrays, while the
server has always stored `hooks` and `metadata` as JSON-safe objects, and now
`references` too. A skill carrying any of them could not be represented by
`TSkill`, `TCreateSkill` or `TUpdateSkillPayload` without a cast.

The type stays free of `any` and `unknown`: values remain JSON-safe by
construction, and the server keeps bounding depth, string length and array
size when it validates them.

* fix: protect moved mirrors and rolled-back counts when a skill is skipped

Continuing past a failed skill exposed two problems that aborting the whole
source used to hide.

A moved skill's mirror keeps its old upstream id until the update lands, and
only the new path was marked as seen, so the reconcile pass read the mirror as
stale and deleted the very copy the skip path exists to preserve. The old id is
now marked as seen too.

Deletion counters were incremented when a stale name-conflicting mirror was
removed, but never undone when the following commit failed and the mirror was
restored. The run no longer stops there, so the status persisted a deletion
that did not happen and the reconcile pass counted the restored row again.
Counters are now rolled back when the restore succeeds.

* fix: bound unknown frontmatter values and keep moved mirrors through duplicates

Tolerating an unrecognized key meant its value skipped the shared JSON-safety
check, so a deeply nested or oversized payload was accepted and persisted under
a key nobody validates. The key stays non-blocking; the value is now held to
the same depth, array and string bounds as every structured key.

A skill that moves into a name another discovered skill also claims is dropped
with the rest of its duplicate group before the sync path can reuse its mirror,
which left the still-published copy unmarked and reconciled away. Both paths now
mark the moved mirror through one helper.

* fix: end the source when a skipped skill fails to roll back

A skill that fails and rolls back cleanly is just a skipped skill. One whose
restore or delete also fails leaves a mirror with half-rewritten files or a
half-created row, and the run now continues past it, so the source could report
partial success while that mirror stayed inconsistent and its pre-marked
upstream id kept reconciliation away from it.

Failed rollbacks now raise a source-fatal error carrying the original failure,
which stops the source the way a lost lock or a refused GitHub token does.

* test: cover a skipped skill discovered at the repository root

A repository-level SKILL.md is discovered with an empty path, so this pins
that a skip recorded against it still persists with the rest of the partial
status rather than taking the whole status row down with it.

* docs: describe unknown skill frontmatter warnings

* fix: preserve mirrors after partial skill sync

* fix: preserve skill validation details during sync

* fix: fail sync when mirror identity cannot be restored

* fix: harden skill sync failure boundaries

* fix: preserve skipped skills on fatal sync

* fix: surface skill sync diagnostics and rollback failures

* fix: preserve skill frontmatter extension keys

* fix: reject skill frontmatter keys that collide when normalized

Frontmatter keys are matched case-insensitively against the canonical
key list, so "Name" and "name" both resolve to "name". Every call site
normalized independently, and the last key in iteration order silently
won, meaning the effective value depended on YAML ordering rather than
on anything the author could see.

Centralize the normalization in normalizeSkillFrontmatterKeys and have
it fail when two recognized keys resolve to the same canonical key,
rather than picking one. parse.ts, deployment.ts and the agent handler
now surface that as a parse error; createSkill and updateSkill surface
it as a blocking DUPLICATE_KEY validation issue. Unrecognized keys are
still passed through untouched so extension frontmatter survives.

deriveStructuredFrontmatterFields and both write paths now run on the
normalized map, so a "Disable-Model-Invocation" key derives the same
column a lowercase one does.

* fix: harden github skill sync against dropped requests and failed cleanup

Three failure paths in the GitHub sync could leave a source looking
healthier than it was.

githubJson only handled HTTP-level errors. A fetch that rejected before
producing a response (DNS failure, socket reset, abort) escaped as a
raw TypeError, so the sync reported a generic crash instead of a typed
sync error. Wrap it as GITHUB_REQUEST_FAILED and add that code to the
fatal set, since a source whose requests never complete cannot be
partially synced.

When a synced file failed to persist, the orphaned upload was cleaned
up on a best-effort basis and the cleanup error was only logged. If the
cleanup itself failed, the source still ended with the original error
and left a real orphan behind. Promote that to a rollback failure so
the source reports SYNC_ROLLBACK_FAILED with the triggering error.

Skill warnings were logged inside commitRemoteSkill, before the file
sync and viewer setup that can still roll the skill back. A skill that
never survived publication therefore emitted warnings as though it had.
Return the warnings from the commit and log them once the skill is
fully published.

* fix: report skipped github skills before credential errors

serializeErrorMessage checked isCredentialError first, and that check
matches on the error text. A skipped skill whose path happens to
contain a credential-ish word, for example skills/credential-helper,
was therefore redacted to "GitHub skill sync credentials are not
available" for admins without credential-metadata access, hiding a
parse failure behind a wrong diagnosis.

Check the promoted skipped-skill case first, since it is identified by
error code rather than by text and is the more specific match. The
credential redaction still applies to everything else.
2026-08-12 13:22:06 -04:00
Danny Avila
ae24461146
🚣‍♀️ feat: Smooth Streaming Text Fade-In (#14757)
* 🚣‍♀️ feat: Smooth Streaming Text Fade-In

Adds a native FlowToken-style fade-in for streamed message text, with no
changes to the streaming data path: a per-block rehype plugin wraps newly
arrived words in one-shot CSS fade spans, using document-order character
offsets so already-visible text never re-animates, even when markdown
re-parsing restructures the tree. Words still inside their animation
window replay identical props so in-flight fades are never cut short.

- New Content/animate.tsx: word splitting (Intl.Segmenter for CJK),
  offset-based new-text classification, rehype plugin factory, and an
  AnimatedText component for plain-text reasoning content
- MarkdownBlocks: per-block plugin instance appended to the cached
  rehype plugins only while animating; block memoization untouched
  (bench asserts identical code-block render counts)
- Animation gated on isSubmitting && isLatestMessage and dropped at
  stream end, so settled messages render without wrapper spans
- Skips code, pre, math/KaTeX, artifacts, citations, and MCP UI subtrees
- Smooth streaming toggle (default on) under Settings → Chat → Messages;
  animation disabled under prefers-reduced-motion
- Extends the streaming bench with a fade variant (+9% render time in
  jsdom, 62/62 code-block renders) and adds unit tests for the plugin,
  classification, and AnimatedText

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

* 🔧 chore: Fix Import Order in Markdown and Settings Registry

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

*  test: Cover Kana-Only CJK Word Segmentation

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

* 🛡️ fix: Harden Streaming Fade for Resume, Concurrency, and Reduced Motion

Addresses Codex review on the smooth streaming fade:

- Hydrated/resumed content becomes the animation baseline: a renderer whose
  first run already exceeds FADE_HYDRATION_THRESHOLD (reconnected stream,
  conversation switch) no longer re-fades the entire accumulated response
- Classification is now transactional under React 18 concurrency: runs are
  staged during render and published via commit() from a layout effect, so
  abandoned renders (interruption, StrictMode double-render) leave no trace
- prefers-reduced-motion now disables the rehype transform and AnimatedText
  in the render gate, not just the CSS animation, so reduced-motion users
  skip the span-wrapping work entirely

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

* 🌏 fix: Refine Fade Hydration Signal, Spaceless Scripts, Collapsed Reasoning

Second Codex review round on the smooth streaming fade:

- Hydration is now an explicit signal instead of a per-block length guess:
  Markdown captures whether substantial content already exists at the render
  where its animate gate flips on, and passes it to each block's plugin. New
  blocks mounting later in the stream always animate regardless of size, and
  resumed content becomes the baseline regardless of block sizes
- Word segmentation now covers all spaceless scripts (Thai, Lao, Tibetan,
  Myanmar, Khmer) in addition to CJK/Hangul, so continuously streamed text in
  those scripts keeps fading instead of freezing after the first window
- Reasoning text no longer runs the word transform while the thinking panel
  is collapsed; expanding mid-stream starts from a hydrated baseline

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

* 🖱️ feat: Hide Streaming Cursors While Smooth Fade Is Active

The word fade itself signals streaming, so the trailing block cursor
(result-streaming) and the pulsing thinking dot (result-thinking) are
now suppressed whenever the smooth streaming fade is enabled. Both
return when the setting is off or the device prefers reduced motion.
Extracts the shared gate into a useSmoothStreaming hook consumed by
Markdown, Reasoning, TextPart, DisplayMessage, EmptyText, and the
legacy loading fallback.

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

*  test: Pin Cursor Assertions to Fade-Off State

The pulsing thinking cursor now only renders while the smooth streaming
fade is off, so the tests asserting it set the toggle off first (at file
level where earlier renders would cache the atom's first read).

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

* 💫 feat: Restore Start-of-Run Dot With Fade-Matched Treatment

The trailing streaming cursor stays hidden while the fade is active (the
fade itself signals text arriving), but the pre-first-token dot returns:
nothing else tells the user the run started before any text exists.

Restyled to match the word fade rather than the classic size throb — it
fades in on the same 250ms ease-out curve, then breathes on opacity, so
"run starting" and "text arriving" read as one system. Applied only when
the fade is active; with the setting off or reduced motion preferred the
dot keeps its original pulseSize behavior, so the cursor assertions in
the existing suites hold unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 13:21:52 -04:00
Danny Avila
ee8c0abe2d
🪝 feat: Execute Agent Plugin Command Hooks (#14755)
* 🪝 feat: Execute Agent Plugin Command Hooks

Implement the missing PluginHookExecutor boundary so deployment plugins'
ai.librechat/hooks/hooks.json documents execute instead of loading inert:

- Command executor runs handlers as child processes outside the API
  process: Claude-shaped JSON payload on stdin, exit 0 + JSON stdout as
  sanitized hook output, exit 2 blocks with stderr as the reason, minimal
  allowlisted environment plus PLUGIN_ROOT/PLUGIN_DATA, abort-signal kill
- Plugin loading carries the parsed hooks document on the contribution and
  threads hookCapabilities from startup, gated on the operator opt-in
  DEPLOYMENT_PLUGIN_HOOKS (off by default: parsed-but-inert with warning)
- Runs register every ready plugin hook onto the per-run HookRegistry after
  internal policy hooks, with once-per-conversation SessionStart dedup

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

* 🪝 fix: Harden Plugin Hook Execution Boundary

Address CI and Codex/Copilot review findings on #14755:

- Break the agents -> plugins import cycle: the run seam now reads a
  PluginHookSource wired at startup (mirrors the tool-approval registry)
- Tighten plugin ask decisions to deny unless the run has HITL wiring,
  so an un-resumable interrupt can never strand OpenAI-compatible callers
- Scope cross-run dedup keys by authenticated user and handler identity:
  caller-supplied conversation ids cannot collide across principals, and
  sibling SessionStart handlers all fire; once handlers persist across runs
- Replace a literal NUL byte in source with an escape (file diffed binary)
- Kill the whole detached process group on abort, not just the shell
- Map exit 2 on events without a decision channel to preventContinuation
- Reserve PLUGIN_ROOT/PLUGIN_DATA against allowlist overrides, quote
  PowerShell args, cap captured output by bytes with one-pass decoding,
  and serialize payloads inside the executor's error boundary
- Fix import ordering flagged by the static checks

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

* 🪝 fix: Close Plugin Hook Policy and Namespace Gaps

Address the second Codex review round on #14755:

- Drop updatedInput from plugin command outputs: hooks in one dispatch all
  receive the original arguments, so a plugin rewrite would reach the tool
  without the approval policy re-evaluating it (host-only now)
- Translate Claude tool aliases (Bash/Write/Edit/Read) to LibreChat runtime
  names in matchers, with reverse payload mapping, so Claude-authored guards
  fire instead of planning ready and never matching
- Key once-only state by declaration position as well as handler contents,
  so sibling declarations with identical handlers stay independent
- Thread sessionStartSource through createRun and mark the HITL resume
  rebuild as 'resume', so SessionStart matchers see the real lifecycle

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

* 🪝 fix: Translate Regex-Form Claude Tool Aliases

Address the third Codex review round on #14755: alias translation now
substitutes word-bounded tokens, covering regex matchers like ^Bash$ and
^(Write|Edit)$ that the exact-token pass left registered against Claude
names and silently never firing. A regex whose alias sits inside a
character class or escape is rejected as unmapped so it fails loudly at
plan time instead of never running.

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

* 🪝 fix: Scope Alias Translation and Reuse Load-Time Plans

Address the fourth Codex review round on #14755:

- Add the WebSearch -> web_search alias so Claude-authored web-search
  guards fire against the LibreChat built-in
- Apply alias translation only to tool-name events; a StopFailure matcher
  like ^Bash failed$ stays untouched and keeps matching the error text
- Reuse each plugin's load-time hook plan at run registration instead of
  re-planning up to 512 handlers on every chat turn

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

* 🪝 fix: Translate Aliased Tool Inputs and Harden Hook Domains

- Present aliased tool inputs under Claude field names (file_path,
  old_string, new_string, including nested edits), so Write/Edit/Read
  guards see the fields they check instead of silently allowing
- Derive the alias table from canonical tool-name definitions
  (BashExecutionToolDefinition, CREATE_FILE_TOOL_NAME, Tools.web_search)
  instead of a parallel hand-authored table
- Reject matchers naming Claude built-ins with no runtime equivalent
  (Task, Glob, Grep, WebFetch, ...) as unmapped at plan time instead of
  registering guards that never fire
- Replace per-event Sets and Stop special-cases with an exhaustive
  EVENT_TRAITS record over HookEvent, so new engine events demand
  explicit semantics at compile time
- Move cross-run once-state behind a PluginHookOnceStore seam with a
  least-recently-marked memory default: active conversations refresh
  their keys each turn, so capacity eviction can no longer re-fire a
  conversation that is still in use; the seam admits a shared-cache
  store for multi-replica deployments
- Gate portable-only command handlers at plan time on Windows via a new
  supportsHandler capability (commandWindows or shell powershell
  required) instead of spawning bash that cannot exist
- Kill Windows hook process trees with taskkill /t on abort
- Require declaration indices on execution requests, stamped from the
  plan instead of defaulted at execution time

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

* 🪝 fix: Keep Group SIGKILL Escalation Armed After Wrapper Exit

An aborted hook whose descendant ignores SIGTERM could leak that
descendant: the wrapper shell's exit fired close, which cancelled the
scheduled group SIGKILL. The escalation timer is now never cancelled —
it is unref'd and killTree already tolerates a vanished process group,
so a redundant late sweep is harmless while a surviving descendant is
reliably killed at the grace deadline. killGraceMs is configurable on
CommandExecutorOptions, with a regression test driving a trap-protected
descendant past the wrapper's exit.

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

* 🪝 fix: Scope Once Retention by Conversation and Reject Clear Source

- Restructure the once store around conversation scopes: registration
  touches the scope every run, so rarely-matching once handlers keep
  their keys while the conversation is active; eviction removes whole
  idle conversations (capacity counts conversations, not keys)
- Reject SessionStart matchers naming the clear lifecycle source at
  plan time — no LibreChat run-construction path emits clear, so the
  handler would plan ready and never fire; wildcard warning text now
  reflects the sources that actually occur
- Make the SIGKILL-escalation regression test real: the surviving
  descendant redirects its stdio away from the captured pipes so the
  wrapper's close fires while it is still alive, exercising the
  window a close-time cancellation would leak

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

* 🪝 fix: Bound Alias Tokens by Tool-Name Characters and Host Shells

- Translate Claude aliases (and reject unsupported built-ins) only when
  delimited by characters that cannot appear in a runtime tool name:
  action tool names preserve hyphens, so an alias embedded in a longer
  name like deploy-Bash-v2_action_example_com stays the literal tool
  name instead of being rewritten into a matcher that never fires
- Reject PowerShell-only command handlers on POSIX hosts at plan time
  (and skip them at runtime): bash cannot run PowerShell syntax, so the
  guard would fail open; a handler with both variants still runs its
  portable command
- Handle rejected asynchronous once-store calls: a failed touch logs
  instead of raising an unhandled rejection during run construction,
  and a failed markOnce lookup fails open per the store's documented
  over-fire direction

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

* 🪝 fix: Probe Group Liveness Before Cancelled or Delivered SIGKILL

The never-cancelled escalation timer could signal a recycled
process-group id when an aborted hook's whole tree exits early in the
grace window. Escalation now probes the group with signal 0: close
cancels the timer only when the group is verifiably empty, and the
deadline re-probes before delivering the group SIGKILL, so surviving
descendants are still reaped while a fully-dead group never receives a
blind late signal. The residual probe-to-signal race is documented as
irreducible without pidfd support.

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

* 🪝 fix: Gate Windows Escalation on Root-Process Liveness

Windows taskkill /t walks the tree from the root process, so once Node
observes the root's exit an escalation pass can reap nothing and a late
forced taskkill could only hit a recycled PID. The liveness gate is now
platform-aware in one helper: POSIX probes the process group with
signal 0, Windows checks the root's observed exit state, and both the
close-time cancellation and the deadline delivery consult it — no
platform retains a blind late signal. Orphaned SIGTERM-ignoring
descendants on Windows are documented as the platform limitation they
are without Job Objects.

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

* 🪝 fix: Scope Payload Namespace to Declarations and Reap Stray Workers

- Reverse name/input translation now applies only to declarations whose
  matcher actually required Claude-alias translation: the plan records
  requiresToolNameTranslation per entry, so a native-authored matcher
  like ^create_file$ receives native tool names and fields instead of
  Claude-shaped payloads its guard never expected
- Coordinate the two dedup layers via a shouldExecute gate on the
  executor: a declaration suppressed by spent once-state declines
  before claiming the per-input dedup slot, so an identical handler
  under an overlapping matcher can still claim it and fire its own
  independent once-key instead of being permanently shadowed
- Reap process groups that outlive a successful hook: a backgrounded
  worker left running after normal wrapper exit gets the same
  term-then-escalate sequence an abort uses, since unsupported async
  handlers mean no lifecycle owns such processes

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

* 🧰 chore: Vendor Pocock Codebase-Design and Architecture Skills

Adds mattpocock/skills engineering/codebase-design and
engineering/improve-codebase-architecture (MIT, license included) under
.claude/skills so future sessions share the deep-module vocabulary
(module, interface, depth, seam, adapter, leverage, locality) and the
architecture-review process. Force-added past the /.claude/ gitignore
deliberately; relocate if project skills should live elsewhere.

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

* 🪝 refactor: Extract Process-Tree Reaping Into a Reaper Module

Tree lifecycle — five of the last seven review findings — lived as
event-handler wiring inside runCommand with its invariants in comments.
It now sits behind a two-method seam: createReaper(child, graceMs)
exposes reap() and onClose(), hiding the term-grace-escalate state
machine, the per-platform liveness gates, the recycled-id guards, and
the clean-exit sweep. The executor shrinks to capture-and-parse, and
the reaper is unit-tested directly with real process trees through its
own interface instead of only via whole-executor integration runs.

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

* 🪝 fix: Scope Translation Per Alternative and Sweep at Root Exit

- Track which runtime tool names alias translation produced, so a
  mixed-namespace matcher like Bash|create_file presents Claude-shaped
  payloads only for bash_tool invocations while the natively-authored
  create_file alternative keeps native names and fields; a capability
  omitting the produced-names list keeps declaration-wide translation
- Sweep the process tree at root exit as well as close: a backgrounded
  descendant holding the captured pipes delays close until it dies, so
  the exit-time sweep terminates it promptly instead of stalling the
  hook until its timeout aborts
- Pass the primary agent's resolved model and identity into the plugin
  hook context, so SessionStart payloads carry model and agent_type
  instead of always omitting them

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

* 🪝 fix: Default Wildcard Declarations to the Document Namespace

- Matcherless (or wildcard) tool-payload declarations now inherit the
  hook document's Claude namespace: with no alternatives to carry
  namespace evidence, the plan marks them for declaration-wide reverse
  translation, so a wildcard guard inspecting standard Claude names and
  fields sees Write/file_path instead of silently failing open on
  native payloads; PostToolBatch entries translate the same way
- Recognize aliases delimited by regex metacharacters: dots leave the
  tool-name boundary class (runtime names never contain them — action
  ids underscore domain dots), so ^Bash.*$ translates to ^bash_tool.*$
  instead of registering a guard that never fires
- Expand Claude's ${CLAUDE_PLUGIN_ROOT} spelling in hook commands and
  export it in the child environment alongside PLUGIN_ROOT
- Scope SessionStart once-keys by lifecycle source, so a startup firing
  no longer suppresses the conversation's resume rebuild

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

* 🪝 fix: Normalize Claude Structured Hook Output

Stock Claude hooks return decisions under hookSpecificOutput
(permissionDecision/permissionDecisionReason), surface context there,
and use continue:false plus the legacy approve/block decisions — none
of which the sanitizer's native field names recognized, so a guard that
works in Claude silently allowed in LibreChat. Parsed JSON now passes
through a dialect normalizer first: hookSpecificOutput fields map to
decision/reason/additionalContext, continue:false becomes
preventContinuation, approve becomes allow, and block becomes deny on
events that block by denying. Native fields win when both dialects
appear, and the ask-to-deny gate applies to the Claude dialect too.

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

* 🪝 fix: Validate Native Decisions and Slim Once Keys

- Strip malformed native output fields before the dialect merge, so a
  placeholder like {"decision":null} can no longer suppress a valid
  Claude permissionDecision into a silent allow; only recognized
  decision tokens take precedence
- Preserve the caller's working directory in hook payloads: cwd now
  reports the run's session context instead of the plugin installation
  path, which commands already receive as PLUGIN_ROOT and which the
  executor still uses as each process's working directory
- Store a compact sha256 digest instead of the full serialized handler
  in once keys: declarations may carry 32 KB commands and 256 args, and
  the previous key embedded them in every retained conversation scope

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

* 🪝 fix: Validate Decisions Per Event Channel and Control Post-Tool Blocks

- Accept native decision tokens only from the target event's own
  vocabulary: "continue" is valid on Stop but malformed on a tool
  event, where it previously survived validation, blocked the Claude
  dialect merge, and was then dropped by sanitization into a silent
  allow
- Translate a structured "block" on events with no deny channel
  (PostToolUse, PostToolUseFailure, and the other prevent-trait events)
  into preventContinuation with the block reason as stopReason, instead
  of discarding it and returning a reason that controls nothing
- Document why LibreChat runs supply no payload cwd: tool paths address
  a remote code-execution sandbox rather than the API host where hook
  commands run, so no host directory describes the run

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 13:21:15 -04:00
Bas
db43121073
⌨️ fix: allow file upload shortcut when editing (#14764)
Co-authored-by: Bas Schleijpen <bas.schleijpen@surf.nl>
2026-08-12 16:18:57 +02:00
Danny Avila
b2128a7d18
📡 fix: Preserve Redis Abort Terminal Delivery (#14749)
* fix(stream): preserve terminal delivery after Redis fences

* fix(stream): cover Redis abort acknowledgment window

* chore: sort stream timing imports

* fix(stream): follow durable replacement handoffs

* fix(stream): bound replacement handoff retirement

* test(stream): cover handoff deadline drain

* test(stream): cover fenced steer retirement grace

* fix(stream): scope fenced retirement lifecycle

* fix(stream): retire fenced subscribers safely

* test(stream): settle subscription fixtures
2026-08-12 07:33:07 -04:00
Danny Avila
88e08c91e8
🧷 fix: Preserve Elicitation Answers Across Aborts (#14745)
* fix: preserve elicitation answers across aborts

* chore: sort stream type imports

* fix: guard malformed resolved answers

* fix: close abort answer race gaps

* fix: retain exact answers across pauses

* fix: retain answers across resumed pauses

* test: satisfy HITL fixture types

* fix: retain legacy answers through approvals

* fix: preserve answers in reconnect snapshots

* test: type legacy answer fixture

* fix: bind legacy answers to paused content

* test: guard optional resume content

* fix: resume questions without streamed content

* fix: Preserve legacy answers through abort filtering

* test: Narrow reconstructed abort fixture

* test: Type abort transform fixture explicitly

* fix: Isolate answers with missing ask content

* test: Type missing-content ask fixture
2026-08-12 07:32:48 -04:00
Danny Avila
236ee6c1ab
🧭 fix: Re-Anchor Parent Activity Phase Bounds (#14741)
* test: cover parent activity phase finalization

* test(e2e): stabilize parent phase coverage

* fix(agents): reanchor parent activity phase bounds

* fix(agents): preserve delayed tools in activity phases

* test(agents): keep phase slice bounds typed

* fix(agents): preserve sparse activity phase bounds

* test(e2e): read structured phase replies
2026-08-11 10:16:57 -04:00
Danny Avila
ba29a6c5d6
🔐 fix: Preserve Plugin MCP Provenance Through Registry Storage (#14744)
* 🔐 fix: Preserve Plugin MCP Provenance Through Registry Storage

Agent Plugins MCP servers are tagged `source: 'plugin'` by the plugin
loader so `processMCPEnv` returns them verbatim, keeping any `${VAR}` a
plugin declared literal. The registry derived `source` from the storage
tier alone, so every startup server routed through
`addServer(..., 'CACHE')` was retagged `'yaml'` — before inspection and
again before persistence.

That dropped the marker for deployment plugin servers merged into the
startup MCP config, so `processMCPEnv` treated plugin-authored strings as
operator-authored templates and expanded them from `process.env`. A
malicious plugin declaring `Authorization: Bearer ${OPENAI_API_KEY}`
received the host's key at its own endpoint, at both boot-time inspection
and every runtime connection.

`resolveServerSource` now carries an existing plugin marker through
instead of re-deriving it, and is applied at all four tag sites
(`addServer`, `addServerStub`, `inspectServerUpdate`, and config-tier
lazy init, which hardcoded `'config'` and would have re-opened the same
hole). The marker is only honored for operator-loaded tiers: a DB entry
is user-authored and stays `'user'`, so user input cannot claim plugin
provenance to escape the sandboxed placeholder rules.

* 🔒 fix: Address Codex review — config-override provenance & upgrade re-tag

Two follow-ups from the Codex review of the provenance fix, plus a test
cleanup.

P2 — a Config-tier override that shadows a same-name plugin base inherited
the base's `source: 'plugin'` through the merge in `getServerConfig` /
`getAllServerConfigs`, so `processMCPEnv` stopped resolving the operator's
own `${VAR}` placeholders and silently broke their server. New
`overlaySource` helper keeps an operator override on its own trusted
source when the base is plugin-sourced; all other bases still inherit as
before. Fails safe (never a leak), but the regression is real.

P1 — the init fingerprint hashes only the raw MCP config, which already
carried `source: 'plugin'` before the provenance fix, so the hash is
unchanged by it. On a Redis-backed rolling restart with no config change,
followers short-circuit on the stale `INITIALIZED_CONFIG_HASH` and the
old `source: 'yaml'` plugin entries survive with no expiry — the fix
never takes effect. Fold a `REGISTRY_STORAGE_SCHEMA_VERSION` into the
fingerprint so an upgrade forces exactly one cluster-wide re-init.

Also drop unnecessary `as` casts in the provenance tests (declare the
fixture as `ParsedServerConfig`, assert with `toMatchObject`) and add a
regression test for the P2 override case.
2026-08-11 08:55:07 -04:00
Danny Avila
e108955c20
🧷 ci: Enforce Durable Agent Finalization for E2E tests (#14740)
* test: enforce agent generation finalization

* test(e2e): correlate canonical persisted turns
2026-08-11 08:27:50 -04:00
Danny Avila
01e9d119bf
🛻 ci: Move the ESLint Config Sweep Into Its Own Job (#14742)
The full-sweep regression gate lints api+client+packages twice — once under
the PR's config and once under the base ref's — inside the same job as ~20
later steps (data-provider/data-schemas/api builds, config migration tests,
unused-i18n scan, and four depcheck passes), all sharing one 30-minute budget.

Two type-aware sweeps of the whole tree cost more than everything else in that
job combined. When they run long the job hits its timeout mid-sweep, so every
step behind the gate never executes and Static checks reports no result at all
— strictly worse than not running the gate. continue-on-error: true hides this,
because the step never fails; it simply never finishes.

Move the gate to its own job with its own budget so it cannot starve the other
checks, and bound each sweep so an over-budget run degrades to a notice rather
than a failure — an unfinished sweep is no evidence of a regression, and the
gate is advisory about config scope. Behaviour on a sweep that completes is
unchanged: coverage loss and new (file, rule, severity) diagnostics still fail.
2026-08-11 08:26:42 -04:00
Danny Avila
ea6f9e3f4f
🌐 chore: Restore English Localization Source Values (#14743) 2026-08-11 01:22:14 -04:00
Danny Avila
60fe67d27e
🌍 i18n: Update translation.json with latest translations (#14739)
* 🌍 i18n: Update translation.json with latest translations

* fix: restore stable English translation labels
2026-08-11 01:11:56 -04:00
Danny Avila
7347cfc195
🍡 feat: Batched User Questions With A Single Bounded Answer Form (#14737)
* feat: support batched user questions

* test: align batched question fixtures

* fix: harden batched question lifecycle

* test: submit batched HITL answers in e2e

* fix: address batched question review findings

* fix: preserve invoke return typing
2026-08-11 01:06:16 -04:00
Danny Avila
d89b11d34d
🎛️ feat: Adopt Composer Density Tokens (#14730) 2026-08-10 22:57:33 -04:00
Danny Avila
c93609cb82
📸 fix: Guard Screenshot Export Against Main-Thread Freezes (#14733)
* 📸 fix: Guard Screenshot Export Against Main-Thread Freezes

* 🧪 test: Cover Conversation Export Flows End-to-End

* 🧪 test: Stabilize Export Spec CSV and Toast Assertions
2026-08-10 22:47:50 -04:00
Marco Beretta
f7d9f36922
🎨 feat: Refine Client Colors and Sharing Dialogs (#14734)
* Refine client colors and settings interactions

* Align dark dialog theme tokens

* Preserve custom hover themes and badge contrast

* feat: redesign sharing dialogs

* fix: preserve theme compatibility and role menus

* fix: address review findings and static checks
2026-08-10 22:43:57 -04:00
Danny Avila
dfd4d9dd81
🧩 ci: Close Workflow Path-Filter Gaps (#14728)
* 🧩 ci: Close Workflow Path-Filter Gaps

Six trigger-filter gaps found by reading all workflows against the live
dependency graph (AI-1755, codegraph FINDINGS §6l):

- backend-review/frontend-review: root package.json/package-lock.json now
  trigger unit tests — a lockfile-only dependency bump previously ran zero
  backend or frontend unit tests while every test job installs from it
- agents-integration-tests: widen to the three package src trees it builds
  and imports (was only src/agents/**)
- cache-integration-tests: same shape — verified live that
  packages/api/src/flow/manager.ts (imported by mcp/oauth) matched neither
  integration filter
- docker-smoke: plain Dockerfile had no PR-time validation despite shipping
  via dev-images/tag-images; new node-image-smoke job builds it, gated by
  paths-filter to Dockerfile/.dockerignore changes
- dev-images/dev-branch-images: add config/**, skill/**, .dockerignore —
  the single-stage image COPYs the full build context
- static-checks: eslint.config.mjs now re-triggers the lint job (gap carried
  over from eslint-ci.yml in the #14716 consolidation)
- delete generate_embeddings.yml: fired on docs/**, which no longer exists,
  and its docs-root-path pointed at the same missing directory

* 🧩 ci: Address Codex Review Findings

- Build caches: all 26 build-* keys across 8 workflows now lead with
  root package.json + package-lock.json so manifest-only bumps cannot
  restore stale dists (data-provider embeds the root version); unifies
  the split key families (playwright already hashed the lockfile)
- static-checks: config changes now gate on the ESLint config loading
  and applying to representative files, plus a report-only full-tree
  sweep (70 pre-existing errors at dev HEAD block a hard gate for now)
- docker-smoke: the workflow file itself triggers the plain-Dockerfile
  build so job edits are validated
- dev-images/dev-branch-images: re-include skill/**/*.md after !**.md
  so shipped deployment-skill Markdown rebuilds images

* 🧩 ci: Gate Config Lint Sweep on Regression vs Base Config

Second-round codex finding: the report-only sweep swallowed config-wide
breakage in scoped blocks the representative files don't exercise. The
sweep now lints the same tree under the PR's config and the base ref's
config and fails only when the PR's config produces more diagnostics for
some (file, rule) pair — pre-existing debt never fails the gate, and
fixes are never penalized. Base-config unavailability degrades to the
load gate with a notice. Outcome surfaced in the failure summary.

* 🧩 ci: Harden Config Lint Gate per External Review

- Coverage direction: fail when the PR config stops linting files the
  base config covered (set difference on linted files) — a mis-scoped
  ignores previously only removed diagnostics and passed both gates
- Severity-aware fingerprints: (file, rule, severity) so warn->error
  escalations gate on a clean tree for that rule; downgrades still free
- Hard-fail when the base commit is missing so a future shallow-checkout
  change cannot silently disable the gate; annotate fetch-depth: 0
- Tab-separated fingerprint keys (space-in-path proof), --config on
  both sweeps, EXIT trap for the base config copy, comment on why it
  must live at the repo root (flat-config pattern base paths)
- Narrow skill md re-include with !skill/README.md: top-level README is
  documentation-only; runtime skill Markdown still rebuilds images
2026-08-10 17:26:50 -04:00
Danny Avila
9bb599435f
📎 fix: Re-enable Send After File Upload (#14727)
* fix: enable composer send after file upload

* style: sort composer imports
2026-08-10 15:05:22 -04:00
Danny Avila
09cbd54f48
🪆 fix: Rebase Activity Phase Bounds over Sparse Content (#14729)
The aggregator writes content parts at provider-source indexes, which can
skip slots and leave holes in contentParts. Array.prototype.map preserves
those holes and the Map constructor iterates them as undefined, so
rebaseActivityPhaseBounds threw "Iterator value undefined is not an entry
object" at the end of every run with sparse content — deterministic with
parent phase summaries enabled, on both the completion and resume paths.

Build the identity map with an index loop that skips nullish slots. Holes
must stay out of the map: one undefined key would falsely match every hole
in previousParts as a retained part and corrupt the rebased bound.
2026-08-10 15:05:10 -04:00
Danny Avila
a3cec67e08
🪆 feat: Add Parent Activity Phase Summaries (#14721)
* feat: add activity phase summaries

* fix: preserve activity phase lifecycle semantics

* fix: satisfy activity phase type checks

* fix: simplify activity phase status mapping

* style: format activity phase changes

* fix: rebase activity phase bounds after shaping

* fix: link activity phase trace ancestry

* fix: reconcile activity phase bounds

* style: format activity phase reconciliation test

* style: align activity phase assertion

* fix: retain reasoning across commentary

* fix: preserve activity phase boundary state

* fix: detect renderable phase children

* test: type parallel phase assertion

* chore: bump agents SDK for activity phases

* fix: retain unphased lane reasoning

* fix: preserve tool group expansion across phases

* style: format phase expansion regression

* fix: preserve phase interaction state efficiently

* perf: skip sparse phase segment holes

* perf: partition phase segments with offsets

* fix: preserve phase boundaries and cursor state

* test: align activity phase regressions with CI

* test: keep phase context mock hoist-safe
2026-08-10 13:41:37 -04:00