chore(sync): sync to upstream LibreChat v0.8.8-rc1 - #365
Merged
Conversation
SYNC-01: the abort route now validates every target field before resolving one, so the empty abortKey mobile sent before the created event assigns an id is rejected 400 INVALID_ABORT_TARGET. Send conversationId="new" instead, the placeholder the user-scoped fallback is gated on. Older servers took that fallback unconditionally, so the new spelling is correct against both. A 409 RUN_STILL_ACTIVE is retried rather than reported as a failed stop. SYNC-02: a run reconciled away ends with a frame the route rewrites to event: error for protocol-v1 clients. The reply is already durable and the existing terminal-error reload fetches it, but the user was shown an error banner for it. Recognize the frame and end the stream without one. SYNC-03: the status route answers 503 SERVER_NOT_READY with Retry-After: 1 while its generation epoch settles. All three call sites read that as "the run is gone", so retry it in the repository rather than at each of them. SYNC-08: a send whose parent response is still being saved now 409s, which an immediate send after an abort reaches legitimately. Retried only when the 409 carries no code — every other 409 on that route is coded and must not be retried.
SYNC-04: a clarification can now arrive as up to four independently answerable questions. The resume route branches on the payload carrying a questions array — where it does, a bare answer string is rejected and an answers map covering every question id is required. Mobile modelled only the single-question shape, so a batched pause rendered question 1 and its submit hard-failed, leaving the run paused with Stop as the only way out. Render the batch as one form and submit the map keyed by the payload's own ids. Skip resolves the whole batch with the declined sentinel per id, since a local dismiss would leave the run paused until it expires. SYNC-15: model tool_call_id on the interrupt payload and drop only the paused call from the streaming tool cards. A sibling ask call in the same turn is genuinely still running and keeps its card.
SYNC-05: activity_label gained an activity_label_type discriminator, where absence still means the per-batch label and "phase" means a parent phase. A phase part is appended at the END of the content array while its own activity_start_index names where the phase began, so its position carries no scope — but grouping treats any filled label as the header of everything unclaimed before it, so a trailing phase label claimed the reply's answer text or rendered as a stray sentence beneath it. Skip phase labels in grouping. Nested collapsible phase groups are the parity fix and are not attempted: upstream re-anchored the bounds twice after the original PR (#14729, #14741), and skipping renders exactly what a server without the feature renders. SearchMatchEnumeration skips them too — that walk and the render walk are lockstep, so a part counted but not drawn shifts every later match. SYNC-19: model phase on text parts and the phase-bound fields on the label. Tests mutation-checked: breaking the discriminator fails four of them.
SYNC-07: the tool cache and the registry inspector now build tool keys as <toolName>_mcp_<normalizeServerName(server)>, but GET /api/mcp/servers still advertises the raw configured name. Mobile split on _mcp_ and wrote the raw name back, so a server named 'Google Workspace' produced a key no producer honours — 'Tool not found' at execution, with every per-tool option silently inert. Port the normalizer, normalize on write, and resolve a normalized name back to its raw form on read so display and matching are unaffected. Normalizing is a no-op for any name that already worked. SYNC-09: register the Anthropic document allowlist as a mirror. Mobile's routing is already correct and deliberately narrower than upstream's — a .docx resolves to UploadRoute.TEXT — so the gap was registry hygiene, not routing. Register the two mirrors this branch introduces at the same time. check-mirrors.py could not anchor a function declaration, which is what normalizeServerName is; teach extract_block to count from the body brace rather than balancing on the parameter list.
SYNC-06: PATCH /api/share/:shareId now carries the SHARED_LINKS CREATE permission — updating a link re-publishes conversation content, so revoking CREATE has to stop updates too. DELETE stays ungated. Mobile called it unconditionally, so a role with USE but not CREATE now gets 403 where it previously succeeded. Gate the affordance and give the 403 its own message: it is permanent for that role, and the row's delete still works. The route also stopped minting a new shareId, so the link is stable across a re-publish. That makes toggleShareVisibility doubly wrong as a name — it was never a visibility toggle and is no longer a refresh — so rename it to updateShareLink through every layer, swap the visibility-toggle icon for an update action, and add the confirmation upstream added: the URL is already in other people's hands and what changes is what sits behind it.
SYNC-11: isShared on conversations. Derived per list request and never persisted, so absence means unknown rather than not-shared. SYNC-16: adminPanelURL, langfuseFanoutEnabled and langfuseConnectionAccess on StartupConfig. adminPanelURL is admin-gated, so absence is the normal case and it must never be cached across accounts. SYNC-17: isEditable on agents, applied fail-closed — it only narrows the existing per-agent EDIT probe, never grants on absence, so an older server that sends nothing keeps the probe's verdict. Also documents that owner_contact no longer carries email (security advisory) and that the mailto affordance on the owner fallback is consequently dead. SYNC-18: flowId, oauthTimeout, failureReason, missingUserVars and authorizationState on the MCP reinitialize response, with the wire values of the two new enums named. ADD-07: an MCP server whose OAuth endpoints change now 400s with OAUTH_SECRET_REENTRY_REQUIRED until the secret is re-entered — retrying the same body never succeeds, so it has to prompt rather than report a failure. Both save paths handle it; the fix works on one screen otherwise.
ADD-01, verified against packages/data-provider/src/actions.ts at db431210733e. Two findings correct the raw claim: 1. An explicit default port validates fine. validateActionDomain's new port check compares getExplicitPort(clientDomain) against specUrl.port || protocol default, and mobile posts servers[0].url verbatim — so both sides derive the same port and can never disagree. The KDoc now records why that verbatim copy is load-bearing. 2. The specific 'Port mismatch:' / 'Domain mismatch:' text never reaches a client: the actions route logs domainValidation.message and returns a fixed generic sentence. The failure path already surfaces the server's message, so there is nothing to unwrap. The real defect is the Swagger 2.0 fallback. validateAndParseOpenAPISpec rejects any spec without a servers array before validateActionDomain runs, so a synthesized host+basePath domain is never compared against anything — the user fills in the whole action editor and is then told the spec has no 'servers'. Say so while the spec is still on screen.
SYNC-23: the client had no ErrorTypes mapping at all, so a typed error reached the user as its raw JSON payload — including the new resource_recovery_required, whose whole point is telling them to reattach files the run could not restore. Introduce StreamErrorType, map the codes with something actionable to say, and localize them at both platform error surfaces. Everything unrecognized keeps the server's own text, so a newer server's code degrades instead of surfacing as a bare identifier. MODEL_NOT_FOUND is not one of upstream's ErrorTypes: it arrives as a LangChain documentation URL inside provider prose and is matched by regex, so it is checked before the JSON parse. SYNC-30 is verified-unreachable rather than ported, and ModelSelectionDelegate now records why: an unresolvable stored agent is already INVALID here and the tier ladder re-arms, and no bare spec-name selection is ever persisted.
Pins the submodule to db431210733e (origin/dev, 2026-08-12), moves backendTargetVersion and UPSTREAM_VERSION onto it, and regenerates BackendCommitMap so date gates can classify servers in the new window. DISCOVERY.md records the range's wire contracts, including the four that are invisible from the client without them written down: the abort route's "new" placeholder gate, the reconcile frame's message being the only signal separating it from a real error, activity_label_type's absence meaning per-batch, and questions[] alone selecting the resume channel. It also records the verified non-changes so they are not re-discovered. VERSION_GATES.md records that this sync adds ZERO gates, and why each item does not meet the bar — an absence that reads as an omission otherwise.
Backticked declaration names may not contain a comma on Kotlin/Native, so :core:common:compileTestKotlinIosSimulatorArm64 failed while the JVM accepted the same name.
…hable The MCP write routes put their code under `error`, not `code`, and the wire value carries an `MCP_` prefix the TypeScript member name drops — so the client-secret re-entry branch could never be taken and every such refusal surfaced as the generic save failure. The `generation-reconcile-message` mirror watched an arrow assigned to a const, whose parameter list balances before its body does: the extracted "block" was the declaration line alone, so the region reported unchanged no matter what upstream did to it. `extract_block` now counts from a function-valued initializer's body, and `--self-test` pins that every watched declaration form extracts more than its signature. Also: re-resolve an agent's MCP server names when the server list is the fetch that lands second, decode `authorizationState` / `oauthTimeout` on the connection-status response, and record the synced commit's own date.
…rrors The preliminary-parent 409 retry could never fire. Its predicate asked for a 409 with no code, read through `ServerErrorCode.from`, which falls back to the `error` key for the MCP controller's convention — and that body carries its English sentence there. `from` handed the sentence back as a code, so the uncoded case was unreachable and the send surfaced as a failure the user had to act on. `generationCodeOf` reads `code` alone; every predicate on the generation routes now goes through one accessor built on it, so none of them can pick the fallback up again. Pinned at the repository, where the broken version is the one that retries zero times. `ChatUiState.error` carries typed `stream_error:<wire>` markers, and only the two snackbars resolved them. Both model-selector banners rendered the value verbatim, and the error is cleared only when the Long snackbar returns — so opening the selector in that window painted the bare identifier, on exactly the codes (`missing_model`, `models_not_loaded`, …) that send a user there. Register the two constants this branch hand-copied: the `ask_user_question` batch limits and the MODEL_NOT_FOUND URL pattern. Both are module-private upstream and served nowhere, so `check-mirrors.py` would have reported clean while either drifted — and a raised MAX_QUESTIONS is a permanently paused run, not a cosmetic gap. MAX_ANSWER_LENGTH was declared but unused; the answer boxes now budget against it so an answer the route would reject cannot be composed.
Four approved items only worked on the path nobody uses, and one Phase 0 claim
was wrong.
The batched `ask_user_question` fix stopped at the pause card. The composer —
the input the user actually reaches for, since the card sits at the tail of the
thread — still submitted a bare `answer` for a batch, which the resume route
rejects outright: it picks the body it accepts off the PAYLOAD, so a pause
carrying `questions` 400s and the run stays paused with Stop as the only way
out. A batch of one is now sent through the batched channel keyed by its id;
a real multi-question batch no longer claims the send at all, because one field
cannot cover ids it never showed and the card is the only input that can.
Its settled half rendered as raw JSON. A resolved batch stamps `{questions:[…]}`
as the call's args and `{"answers":{…}}` as its output, and neither is visible
to the single-question parse — so the durable record showed no question and the
answers map where the answer belongs, on every reload. It now renders one
question/answer row per id, each answer read back against its own options.
The MCP secret re-entry prompt could never appear: the refusal comes from the
update route alone, and both save paths POSTed a create even in edit mode —
which also asked the server to add a second server under a name it already
holds. Edit-mode saves now PATCH the stored server.
`isEditable` was consumed where the server never sends it. Upstream stamps it in
`getListAgents` only, so the detail screen's `canEdit && isEditable != false`
reduced to `canEdit` on every server. The list read now records the verdict
(account-keyed, dropped on any agent mutation) and the detail screen narrows
against that.
Also: restore the import order this branch broke in ChatScreenEffects, and
record that Phase 0's one DRIFT report (`memory-storage-error-types`, a
file-mode mirror) was unrelated churn — `registerMemoryTools` gained a
`toolNames` field while both guarded literals stayed byte-identical. The
pre-sync registry held 12 entries, not 11.
SYNC A2 audit: upstream 5e464bc9 (v0.8.8-rc1) turned multer file-filter
rejections from a bare 500 into 400/415 responses carrying a {message}
body ("Unsupported file type: <mime>", "No file provided"). Audited every
mobile upload surface for the generic-copy failure the item anticipated,
and found the pipeline already complete end-to-end:
- LibreChatHttpClient.extractErrorMessage lifts {message} into
ApiException(serverAuthored = true);
- toSafeError prefers that text after the looksLikeUserMessage screen;
- FilesViewModel, FileAttachmentDelegate (Android chat attach),
IosFileHandler (iOS chat attach) and the conversation-import path all
render Result.Error.message, falling back to generic copy only when the
server sent none.
No production change needed. The tests pin the seam the whole contract
rides on: a server-authored 415/400 reason reaches the message verbatim,
and the safety screen still swallows non-prose bodies.
SYNC A15: upstream da390fa9 fixed a server bug where an agent update matching the newest version entry returned a stale 200, which web then wrote into its query cache — the "Save reverted" report. Audited mobile's save flow for the same exposure: - AgentSaveDelegate.save() consumes only result.data.id (SaveSuccess event); the response body never reaches editor state. - AgentRepositoryImpl.updateAgent() invalidates the account-keyed list cache and bumps `revision` instead of caching the returned agent, so every consumer refetches; reopening the editor goes through getAgentForEditing (a fresh GET). - revertToVersion() does apply its response via applyAgentData, but that is the revert route, which the upstream bug does not touch. Mobile is clean — no behavioral change; a comment now pins the invalidate-not-cache invariant at the seam.
…sync The regenerated BackendCommitMap had lost every tag row older than 0.8.7-rc1: the submodule checkout only carried a partial tag set, so the generator saw 3 tags where the map (and BackendCommitMapTest's immutable anchors) expect the full history. Fetched the upstream v* tags and regenerated — 79 tag rows, dev window unchanged. Also: FilesViewModel's new ConfigRepository dependency registered in the Koin verify test, and the pre-HITL5 routing assertion updated to the new contract (a multi-question batch now claims the composer's send).
Per-target detekt tasks (detektAndroidDebug, detektIosSimulatorArm64Main) pick up KSP output such as Room *_Impl classes and fail on machine-written style. Exclude by file path: string patterns match relative to each source root, and the codegen roots live under build/generated themselves.
…enerate Two review fixes: - BackendCommitMap: the A12 regeneration ran against a shallow upstream clone, so rev-list silently returned 605 commits instead of the 1000-commit dev window and ~392 dev rows (2026-03-21..2026-05-31) were dropped — servers built from those commits resolved to an unknown backend. Unshallowed the submodule and regenerated: 1072 rows (79 tag + 993 dev). The generator now fails loudly when the submodule is shallow or rev-list comes up short of devCommitCount. - Regenerate (and edit-assistant, which replays the same parent user turn) now carries the original user message's persisted quotes into ChatRequest.quotes, mirroring web's overrideQuotes: the server rebuilds the user message from req.body.quotes on a regenerate, so omitting them silently dropped the quoted context while the quote chips stayed visible. Continue still sends none, matching web.
Two review fixes: - The server's own SSE created frame (which carries no generationCreatedAt) follows the start POST's synthetic Created on every fresh send, and onGenerationEpoch let it null the recorded epoch — so a pause on a live run resumed unfenced, defeating the 409 RUN_REPLACED check on its primary path. onGenerationEpoch now ignores null reports; clear()/expireNow() remain the resets. Covered at composition level by replaying the real event order through StreamingManagerDelegate with a real PendingActionDelegate (verified to fail without the guard), plus a direct delegate test. - Quotes docs (ChatRequest KDoc, VERSION_GATES, DISCOVERY) still said regenerate/continue/edit send no quotes; regenerate and edit-assistant now replay the parent user message's persisted quotes (overrideQuotes parity). Docs updated to the shipped wire behavior.
Derive the picker MIME types from a combine of the loaded file config and configRepository.detectedBackendVersion instead of a one-shot read at config-load time, so a detection that resolves late still surfaces the version-gated .potx entry (mirrors the chat side's reactive gates). Also record in DISCOVERY.md that iOS quote capture is deliberately deferred: chips render on iOS but the capture affordance is Android-only pending a CMP iOS text-context-menu investigation.
The composer's send during a multi-question ask_user_question pause recorded its answer in a map private to PendingActionDelegate, while the card rendered and submitted from its own remembered state. Nothing was routed wrong — the answer simply landed where nothing could see it: the first question's field stayed empty, the card's Send stayed disabled because that field was blank, and no resume ever went up. The invisible words then had to be handed back somewhere, so every swallowed send reappeared in the composer once the batch finally resolved from the card. The per-question drafts are now one hoisted map (MessagesState.askAnswerDrafts) that both inputs read and write, so a composer send fills the first question the card still shows as blank and the batch submits when the last one is in. The send is refused (and the composer keeps its text) when there is nothing left to answer, and the drafts are dropped rather than re-homed when the pause dies.
…lbar The quote affordance was published by wrapping LocalTextContextMenuToolbarProvider above the thread, and that local is null there: every SelectionContainer installs the platform toolbar provider inside itself (CommonContextMenuArea -> ProvideDefaultPlatformTextContextMenuProviders), so the wrapper read null, stood down, and left the stock Copy / Select all toolbar untouched on every message. Providing one there would not have helped either — the per-container install would sit below it. Foundation builds a menu's data by walking the toolbar handler's ANCESTORS (collectTextContextMenuData -> traverseAncestors), so the item is now contributed by a modifier above the message list instead, leaving the platform's own toolbar and its text-classification items alone. The appended item still needs the selected text, which only the built-in Copy item's onClick exposes; a filter captures that item (the builder cannot read what has already been collected) and drops the entry from any menu that has no Copy, so a text field's paste-only menu cannot quote a stale clipboard. The instrumented suite now drives the item through the real selection stack with the modifier placed as production places it.
A model-not-found failure on rc1 arrives as an ERROR content part: the assistant message persists with error:false and no text, and the raw provider JSON plus the LangChain troubleshooting URL exist only in that part. StreamErrorType.parse already matched it, but it was only ever called on the stream-end path, so the same failure produced actionable localized copy on the snackbar and raw provider JSON in the thread — and on a reopened conversation, where the run's end reason is gone, only the raw payload was left. Both sites now go through one entry point, StreamErrorType.markerOrText, so no second regex home can appear beside the first. Unknown codes still render the server's own text, which is the classifier's existing contract.
Android's DocumentsProvider reports .sh as text/x-sh, a spelling that appears nowhere upstream — not in mimeTypeAliases, not in fullMimeTypesList, not in the accept regexes — so rc1 answers the upload 415 "Unsupported file type: text/x-sh". Upstream's alias table targets freedesktop and libmagic because a browser is its only client, so there is nothing there to mirror. Add a separate PLATFORM_MIME_NORMALIZATION map, deliberately outside the mirrored MIME_TYPE_ALIASES and outside scripts/mirrors.json: a row added to the mirror would make every check-mirrors.py run report drift against a table upstream will never contain. It runs ahead of the mirrored aliases in the router and again in FilesApi, the one place the multipart part's Content-Type is written — the router deciding against one string while the wire carries another is what left this reachable. No version gate, unlike the shellscript aliases: the target application/x-sh is in upstream's fullMimeTypesList at v0.8.6, v0.8.7 and at the shellscript-alias commit's parent, so every supported server accepts it. The shellscript rows need their gate because their targets only became aliases at rc1.
…re-link copy Six defects found reviewing the v0.8.8-rc1 sync branch. Staged quote chips were consumed when the send spec was minted but never restored: an early abort or a pre-`created` stream error handed back only the text, so the retry went up without the excerpts. The restore now carries them. A failed batched ask_user_question submit re-homed the joined answers into the composer even though the card still shows every draft, duplicating text the user can see and arming a composer send that answerNextBatchQuestion can only refuse. Batches now restore nothing; their words live on the card. The pause expiry compared the server's expiresAt against the device clock, so a skewed clock dismissed live cards on sight. It now measures the wait from the server's own two timestamps, erring late — a stale resume 409s into the same copy. The Add to chat capture staged whatever the clipboard held when its poll timed out, quoting the user's previous clip. Capture is now identified by the clip's write timestamp and stages nothing when the copy never lands. Shared-link update promised the URL survives, which only holds on rc1+; earlier servers mint a new shareId. The confirmation copy is version-gated, fail-safe to the warning. The row is also patched rather than rebuilt from a response that carries no title, which relabelled every updated link "Untitled Conversation". The comparison secondary lane rendered an unanswered ask as a record card with an empty answer in a pane that hosts no card to answer it. Also registers the ErrorTypes and MCPErrorCodes mirrors in scripts/mirrors.json, which the repo requires in the same PR that hand-copies them.
Version gating answered in booleans, which forced two unlike servers into one `false`: a build whose tag proves it predates a feature, and one that could not be placed at all. The second population is not old — it is dev builds reporting the previous release (upstream bumps package.json at rc prep) and servers built past this app's commit-map pin, i.e. the servers most likely to HAVE the feature. The rc1 sync dropped the landedDate fallbacks on four gates, which handed that conflation real consequences. The worst is the queued-attachment TTL touch: a 0.8.8-cycle dev build reporting 0.8.7 no longer gets the hold, so the upload-window reaper can collect an attachment out from under a queued message and the send then references a file the server deleted. BackendVersion.featureSupport returns PRESENT / ABSENT / UNKNOWN, with supportsFeature now its `== PRESENT` shorthand, so every gate not touched here keeps its behaviour byte for byte. ABSENT is only ever a tagged build below the threshold or a dev build a landedDate settles; a dev build's reported version is a floor and never a ceiling, so nothing else is proof. The two gates whose features are discoverable moved to probe-and-latch: suppress on ABSENT, let UNKNOWN through to exactly one real call, latch its 404 for the rest of the server session. Restoring the dropped dates was the other candidate and rots by construction — day granularity cannot separate same-day commits, and the pin's coverage window re-hides the feature days after each sync. A 404 is exact and does not age. The latch needs a field of its own: isSupported also reads false before the first probe, so reusing it re-probes on every picker open. It is reset on account switch, since these repositories are app-wide singletons and the verdict describes one server — FileRepository gains clear() alongside the tool-favorites one NavHostViewModel already calls. The renewal heartbeat re-asks per tick rather than only at start, because this is the one gate whose answer legitimately flips, once, when the probe lands. Steering and the context-projection suppression stay two-state on purpose: one has nothing to probe and costs nothing closed, the other's ungated branch already issues a call a 404 makes free.
…erver The unit tests pin the decision table but cannot show that a call is really issued or really withheld. GateProbeDeviceTest drives both probe-and-latch gates over real HTTP, on a device, once per server identity. Two harness defects had to be fixed before it proved anything, and both are the reason the test is worth keeping. It first passed while its POST /api/files/usage never left the device: the test client lacked the production contentType default, so the request died inside ContentNegotiation and a counter watching onRequest recorded the intention as an event. It now counts responses. It then could not latch, because without the production HttpResponseValidator a 404 arrives as an ordinary response and the repository reads it as a successful touch. The rig runs the real image behind a small logging proxy: the log makes "no call was made" an observation rather than an inference, and a 404 mode on just the two gated routes stands in for a server that predates them. Server identity comes from the BUILD_COMMIT env var upstream already prefers over git rev-parse, so one server impersonates every population the gate must tell apart. Measured on a Pixel 10 Pro Fold AVD against v0.8.8-rc1: the rc1 tag resolves RC/0.8.8-rc1, the v0.8.7 tag resolves OFFICIAL/0.8.7, and an unmapped commit resolves to nothing. A proven-old identity issues no request at all; an unplaceable one issues exactly one and keeps the feature; a 404 latches it off for the rest of the session. The class self-skips without the rig and detects which of the two rigs is up, so neither half fails for want of a fixture.
A pause does not end the run, so isStreaming stays true across it and the per-frame follower keeps pinning the list's tail while the output it exists to follow has stopped. The pause card is what grows underneath — a long question, expanding options, an answer field taking a second line — so the follower chases it and walks the question off screen while it is being read. The one-shot scroll that brings a new card into view stays; it now flags itself as programmatic, since animating up to the tail otherwise reads to the scroll-away detector as the user dragging away from the bottom.
Comments describing bugs that only ever existed inside this branch read, on develop, as a regression trail that never happened; they are rewritten as present-tense invariants or dropped. Six explanations repeated across five to seven files are collapsed to one canonical home each, with the copies pointing at it. Wire-contract, data-loss and concurrency invariants are left alone.
…to-view The keyboard handler exists for the composer, which sits outside the list: the list learns about the IME from nothing but its own shrinking viewport, so it jumps to the tail of the last item. A field INSIDE the list needs no such help — foundation scrolls a newly focused node into view and knows where the focus is — but both running means the jump wins and scrolls the user off the very field they tapped, which a tall pause card makes obvious because its fields sit anywhere in it. The handler now stands down whenever the list holds focus.
Contributor
Android debug APKArtifact:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the client to the official LibreChat
v0.8.8-rc1tag (previouslyv0.8.7 +dev.91adcf3f). Most of the change is wire-protocol parity — reshaped streaming activity phases, multi-questionask_user_questionpauses, MCP OAuth/status fields, typed server errors — plus the rc1 upload MIME set. It also adds text quoting in chat and replaces the two-state backend version gate with a three-state one that can tell an unplaceable server from a proven-old one.Changes
Streaming and message shapes
{value, annotations}form./api/configand MCP connection status;agent_idsandphaseon content parts.Errors
ServerErrorCode/StreamErrorTypeclassify typed server errors into actionable messages through a single entry point. In-banderrorcontent parts go through the same path instead of rendering as raw JSON in the thread.HITL (
ask_user_question)generationCreatedAt, so the server can reject a resume onto a replaced generation.MCP servers
PATCH /api/mcp/servers/:serverName; only creation still uses the create route.MCP_OAUTH_SECRET_REENTRY_REQUIREDreports as an inline error on the client-secret field, which is the field the server is asking for, rather than as a generic failure.Files
.potxare offered on rc1 servers behind version gates. Android'stext/x-shis normalized before upload, since upstream's alias list targets freedesktop/libmagic values and omits it.Chat
Scrolling around a paused run
Version gating
FeatureSupport(PRESENT/ABSENT/UNKNOWN) replaces the boolean gate where a wrong answer costs data. A dev build reports the last released version, so a plain version compare reads as "absent" — for the queued-attachment TTL touch that meant withholding the touch and letting the reaper collect an attachment out from under a queued send. Where support is discoverable, the client now probes once onUNKNOWNand latches the 404.supportsFeatureis defined asfeatureSupport(...) == PRESENT, so gates that were not converted behave exactly as before.BackendCommitMapregenerated across the full dev window.Tooling and docs
check-mirrors.pyhandles the three declaration forms upstream writes and gains--self-test; the mirrors registry covers the new constants.UPSTREAM_VERSION,backendTargetVersion, the README compatibility badge,DISCOVERY.mdandVERSION_GATES.mdupdated.Testing
assembleDebug, the unit-test suite (2322 tests, 0 failures) anddetektMetadataCommonMainare green.ghcr.io/danny-avila/librechat:v0.8.8-rc1in Docker: single and batched HITL asks end to end, the capability-off config variant, streaming with activity phases, message editing, model list,.shupload, plus a regression pass over the existing chat and conversation flows.GateProbeDeviceTest(instrumented, self-skipping without the rig) exercises the three-state gate against a real server in both route-present and route-missing modes.MessageListPauseScrollInstrumentedTest(4 cases) covers the scroll behaviour above on a Pixel 9 Pro Fold, driving the frame clock by hand because the follower's per-frame loop never lets Compose go idle. Each assertion was checked against a build with the corresponding guard removed, so the two that pin this behaviour are known to fail without it.Notes
PendingAction.expiresAtis dormant upstream (the server never populates a TTL), so the expiry path cannot be exercised against a live server.