Skip to content

[pull] main from danny-avila:main - #241

Merged
pull[bot] merged 8 commits into
innFactory:mainfrom
LibreChat-AI:main
Sep 1, 2026
Merged

pull[bot] merged 8 commits into
innFactory:mainfrom
LibreChat-AI:main

Conversation

@pull

@pull pull Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

danny-avila and others added 8 commits August 31, 2026 22:29
…15444)

The first native-ARM run published `lc-dev-api` correctly but failed merging
`lc-dev`:

  Merging 4 platform digests: 18f83e4a... b4700f27... ca747c51... edf23a05...
  ERROR: ghcr.io/danny-avila/lc-dev@sha256:ca747c51...: not found

`lc-dev` is a prefix of `lc-dev-api`, so the merge job's
`pattern: digests-lc-dev-*` matched all four uploaded artifacts — both images'
digests — and then tried to resolve an API digest against the app repository.
`lc-dev-api` was unaffected because no other image name prefixes it.

Two changes:

- Artifact names carry an explicit `-arch-` separator, so one image's pattern
  can no longer reach another's digests.
- The merge asserts an exact digest count rather than merely rejecting zero.
  The old guard only caught an empty set, which is why four sailed through. Too
  few digests would publish a manifest missing an architecture; too many means
  the glob crossed images. Either is now a hard failure before anything is
  tagged.

No bad manifest was published: `imagetools create` failed while resolving its
sources, so `lc-dev` kept pointing at its previous build. The run also confirms
the approach itself works — `lc-dev-api` merged to a proper index carrying
linux/amd64 and linux/arm64, and the native arm64 legs built in 3-4 minutes
against 13+ minutes under QEMU.
* fix(agents): check skill version in codeEnvRef cache-hit path

When a SKILL.md body is edited, `updateSkill` bumps `Skill.version`
but does not clear the `codeEnvRef` on bundled skill files. The
cache-hit path in `primeSkillFiles` reused those stale refs without
comparing versions, causing Code API to return 403 ("Unauthorized file
reference") because the session key no longer matched.

Add a version check to the cache-hit guard: a ref whose version does
not match the current skill version is treated as a cache miss, forcing
a fresh upload under the new session key.

Closes #15275

* fix(agents): apply version check to aggregate cache path in primeInvokedSkills

The previous commit added a version check to the per-skill cache-hit
path in primeSkillFiles. Codex review pointed out that
primeInvokedSkills has its own aggregate cache-hit branch that returns
cached refs before delegating to primeSkillFiles. That branch checked
ref presence and session activity but not ref.version against
skill.version, so a stale ref from an older skill revision would still
bypass the guard.

Add the same version comparison to the aggregate filter and include the
skill object in the allResolved entries so the version is accessible.

Addresses Codex review on #15413

* fix(agents): type skill cache version checks

---------

Co-authored-by: dymux <putramkti@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🛰️ ci: Publish Every Image Through One Native-ARM Workflow

#15441 removed QEMU from the dev-branch image build and #15444 fixed the
prefix-glob bug it shipped with. Both changes lived in one of five workflows
that build images the same way; the other four still emulate arm64 with no
layer cache and take 20-40 minutes.

Rather than paste the pattern four more times, the build/merge logic moves to a
reusable `docker-publish.yml` that every image workflow calls. #15444 is the
argument for this: one wrong artifact-name expression would otherwise have
needed correcting in five files, and the same prefix hazard exists in all of
them (`librechat`/`librechat-api`, `librechat-dev`/`librechat-dev-api`,
`lc-dev-staging`/`lc-dev-staging-api`). The shared implementation is the
dev-branch one that already ran green, generalised only where callers differ.

Callers pass what actually varies:

- `images`   — the target/file/image_name triples
- `tag_suffixes` — newline-separated tags applied in both registries
- `checkout_ref` / `build_branch` — main-image-workflow builds `main`, not the
  dispatch ref

`main-image-workflow` and `tag-images` resolve their tags from git history, so
each grew a small `resolve-tag(s)` job whose output feeds the publish. That also
removes a duplicated `git fetch --tags` that previously ran once per matrix leg.

Two details worth noting:

- The digest-count assertion now derives its expected value from the `arches`
  input rather than a hard-coded 2, so the architecture list has a single
  source of truth shared by the build matrix and the merge guard.
- Nothing is interpolated into a `run:` block any more; `build_branch` and the
  build digest arrive through `env`, so a ref name cannot reach a shell.

Net 135 lines lighter, and QEMU no longer appears anywhere in .github/workflows.
Workflow names are unchanged, so retry-docker-builds.yml still matches all five.

* 🔗 fix: Pin Release Builds to One Commit and Survive Full Re-runs

Review findings on the reusable publish workflow.

**P1 — every build leg could resolve `main` separately.** `main-image-workflow`
passed the symbolic ref `main` as `checkout_ref`, so the four build jobs each
resolved it at whatever moment they were scheduled. If `main` advanced in
between, the amd64 and arm64 halves of a release — or the API and application
images — could be built from different commits and published under the same
release and `latest` tags. `resolve-tag` now exports the commit it resolved the
tag from, and the publish pins to that SHA. The single-job workflow this
replaced checked out once, so this was a regression introduced by the split.

`docker-publish` likewise now falls back to `github.sha` rather than passing an
empty `ref` to `actions/checkout`, so callers that omit `checkout_ref` also get
an immutable commit instead of relying on checkout's empty-ref handling.

**P2 — digest artifacts on a full re-run.** `upload-artifact@v4` will not
upload over an existing artifact name, and a full re-run keeps the run id, so a
retry would fail before the merge could run. The suggested fix was to qualify
the artifact name with `run_attempt`, but `retry-docker-builds` picks its
endpoint by conclusion: `rerun` only when a job was cancelled, and
`rerun-failed-jobs` otherwise. Under the latter the merge re-runs alone and must
still find digests uploaded by the *previous* attempt's build legs, which a
run_attempt-qualified name would hide — precisely the shape of the #15444
failure, where the builds passed and only the merge failed. `overwrite: true`
resolves the conflict while keeping names stable across attempts.

**Unreachable error branch.** `LATEST_TAG=$(git tag ... | grep ... | head -1)`
under `set -euo pipefail` exits on grep's non-match, so the "No stable v<semver>
tag found" message could never print. Added `|| true`, matching what
`tag-images` already does. Pre-existing, carried over verbatim in the split.
… Shape (#15448)

* 🩹 fix: Stop the Subagent Composer Unmounting, and Give It Main Chat's Shape

Measured with react-scan and a DOM presence probe against the live panel:
every external delivery unmounted and remounted the composer inside 10ms,
taking focus and any half-typed steer with it.

- The composer stays mounted across a delivery. A delivery re-keys the
  thread query to its new task and the task view blanks for the render or
  two that takes — deliberately, so task-scoped fields are never
  misattributed — but presence followed it down. The last mode the thread
  showed now carries the surface across that gap, holding submission until
  the new task's own view lands. `dispatched` also counts as live, so a run
  that has only just started no longer withdraws the field either.
- One control, not three labels. Submitting IS the steer; with nothing to
  send the send button becomes main chat's Stop, which cancels the task.
  Queue and interrupt keep the chords main chat gives them
  (⌘/Ctrl+Enter, ⌥/Alt+Enter) instead of spelling themselves out beside
  the field, and every control says what it is through its tooltip and
  accessible name rather than a word label.
- The two composers line up. The field takes `ChatForm`'s own metrics and
  the footer clears the viewport floor by the height of main chat's
  disclaimer, so both textareas now measure top 805 / height 52 and both
  surfaces end at 910 in a 950px viewport.
- The header is main chat's header: a 52px bar that floats over the thread
  and fades into it, so the conversation scrolls under it instead of
  starting below a solid bordered bar.

Residual re-renders are the per-second timestamp and elapsed tickers plus
the 2s poll both queries run while a child is live — shared with main chat,
about 2ms of render time across a 14s window, and left alone.

* 🎛️ fix: Bind the Chord to Its Own Submission, and Keep Queue Reachable

Codex and Copilot, round one.

- A refused chord no longer decides a later click. `Composer` hands the
  submitting event to its host, and the panel keeps the resolved control
  bound to THAT event — a ⌘/Ctrl or ⌥ chord the composer then refuses
  (empty field, settling, a command in flight) leaves nothing behind for
  the next pointer submission to pick up.
- The control comes from the reader's own key policy, not from raw
  modifiers. With Enter-to-send off, ⌘/Ctrl+Enter IS the submit chord, so
  reading modifiers directly made steering unreachable; the verdict from
  `resolveComposerKeyDown` decides instead.
- Queue and interrupt keep a pointer. Chords alone stranded touch readers,
  and anyone whose shortcuts are disabled, rebound, or yielded. They are
  back as icon-only controls that say what they are through their tooltip
  and accessible name rather than a word label.
- Stop cannot render unlabelled. Handler and label travel as one value
  (`ComposerStopProps`), so the type refuses a Stop without a name.

* 🔗 refactor: Hang the Panel's Alternate Submissions Off One Shared Send Control

Queue and interrupt had no pointer, then briefly had controls of their own
beside the field — which repeats what submitting already does. Main chat
never did that: it hangs its during-run actions off the send button.

- `SendActions` moves to `@librechat/client`: the hovercard, the rows, the
  chord chips and their classes, lifted out of `DuringRunSendButton`
  unchanged. Each host passes its own anchor whole, so main chat keeps its
  ref (Enter's synthetic click routes through it), its submit type, and its
  identifying attributes. Its 13 existing tests still cover the rows.
- The panel offers steer, queue and interrupt from that same control, and
  advertises a chord only while it still reaches its action — each hint is
  read from the decision table that will execute it, so a rebound, yielded
  or disabled chord advertises nothing.
- The send control names what IT does (`com_ui_steer_send`) as distinct
  from the `Steer` row it offers, exactly as main chat pairs them.

Two hosts now share one implementation, which is what the seam wanted.

* 🧹 chore: Sort Panel Imports and Drop an Unrelated Reorder

The import sort ran before the last edits to this file. Also reverts a
Tailwind class reorder `eslint --fix` made to `MultiSearch`, which this
change has no business touching.

* 🛡️ fix: Only Offer Controls a Dispatched Task Can Actually Take

Codex, round two.

- A dispatched task is controllable only once the server can address it.
  `control.ts` answers 404 when the task has no durable input row and no
  live lease, which this panel reads as an inaccessible task and closes its
  controls for good — so treating every dispatched task as controllable
  could brick them. The field still stays (withdrawing it as a run starts
  is the swap this panel exists to avoid); submission waits for evidence.
  A task whose controls are definitively closed holds nothing.
- Semantic status roles replace `text-amber-500`, `text-cyan-500` and
  `text-red-500` in the action rows — in main chat's list too, since both
  now render through the same module.
- One popup over the send control. With actions to offer, the tooltip is
  withheld: both attached to the same button, opened on the same focus,
  above the same point, and the tooltip's higher z-index covered the list.
- Where the pointer cannot hover, the alternate submissions are controls of
  their own: a tap on the hovercard anchor submits, so touch readers could
  not reach queue or interrupt at all.

* 🚪 fix: Send Every Control Through the Same Addressability Gate

Codex, round three — both findings were doors around the gate added last
round.

- Stop and the inline actions now answer to `controlAvailable`, not just to
  `composerMode`. A retained surface over a dispatched task the server
  cannot address yet still offered Stop, and the touch-side queue and
  interrupt buttons were live too; each is a command, and each 404 closes
  this task's controls for good.
- A trapped panel keeps its actions inside itself. At mobile width with a
  hover-capable pointer the list still hung off the send control, portaled
  outside the `aside` the focus trap knows — so Tab never reached it. The
  actions come inline whenever the panel is a modal, hover or not.
… Interrupt Copy (#15450)

* ✔️ style: Anchor the Applied Steer Receipt at the Row's Trailing Edge

* 💬 style: Say Tool Calls Are Not Interrupted Either
* 🗂️ fix: Resolve Code Interpreter Destination Collisions

Codeapi mounts each `/exec` input at a destination derived from its `name`
and rejects the whole request when two entries land on one. LibreChat never
enforced that: `primeFiles` sent `name: file.filename` verbatim, and the
graph seed deduped by `(storage_session_id, id)` — identity, not destination.

Two conversation files sharing a filename therefore killed code execution for
the rest of the conversation. The rejected request never reaches the sandbox,
so no result comes back to collapse the pair, and every later turn re-primes
both and fails identically. Only code-generated outputs are covered by the
`(filename, conversationId, context, tenantId)` partial unique index; uploads
carry `context: message_attachment`, so one conversation can hold several
records sharing a filename — two uploads of `image.png`, or an upload whose
name a later execution wrote back in place.

`packages/api/src/files/code/destinations.ts` now owns the name → destination
mapping for every contributor to an `/exec` file list, mirroring codeapi's
conflict rule including directory-prefix conflicts:

- `primeFiles` claims per pushed file, disambiguating with a `-2`, `-3`
  counter and echoing the resolved path into the tool context, so the model
  is told where each file actually mounts. Claim order comes from immutable
  `createdAt`, not `getFiles`'s `updatedAt` sort, which usage accounting and
  re-upload both bump — order-dependent destinations would silently repoint
  a path an earlier turn had already handed the model. Newest wins the bare
  name, matching `ToolNode.updateCodeSession` and giving an in-place rewrite
  precedence over the original it superseded.
- `seedCodeFilesIntoSessions` drops a later file whose destination is already
  claimed. Renaming there would invent a path nothing told the model about,
  and a conflict at that layer means one logical file arrived twice under
  different storage pointers.

Fixes #15443

* 🧹 chore: Tolerate Holes in the Destination Priority Sort

`primeFiles` sorts the raw `getFiles` result before its own per-entry guard
runs, so the comparator has to survive a null row rather than throw ahead of
the loop that was already defending against one.

Also tightens the tool-context assertion in the upload-vs-output case, which
was passing on a trailing space rather than on the path.

* 🔁 fix: Address Codex Review on Destination Assignment

Four findings from the review of `b0f61d0`.

**Identity-derived suffixes instead of a collision counter.** A counter is
assigned from whatever is in the set, so a third file arriving under the
literal name a displaced file had been given pushed that file along, and code
the model wrote in an earlier turn would silently start reading the newcomer.
The fallback now hashes the file's own `file_id`, so a displaced file keeps
its destination for the life of the conversation and only the winner of the
bare name can change. A trailing counter still guards a hash collision so the
search terminates on any input.

**Rank by last content write, not record creation.** `processCodeOutput`
claims one row per `(filename, conversationId)` and rewrites it in place, so
a repeatedly written output keeps its original `createdAt` while recording the
latest writer in `metadata.sourceDispatchedAt`. Ranking on `createdAt` alone
handed the bare path to an upload the output had since been rewritten over.
Recency is now `max(sourceDispatchedAt, createdAt)` — still never `updatedAt`,
which usage accounting bumps without any content changing.

**Advertise a displaced generated output.** Generated files are left out of
the tool context because the model already knows what it wrote — which stops
being true once a newer same-named file takes the bare path and the output
mounts somewhere the model has never seen. Displaced outputs are now listed
with the name they were written as, and displaced uploads name their original
too.

**Rank conversation files above an agent's private ones.** Each agent claims
destinations over its own candidate set — the conversation's files plus its
own — so a private file taking a bare name from a shared file in one agent but
not in another left two agents advertising different paths for the same file
into one shared mount namespace. Shared files now rank first, so the majority
that every agent sees resolves identically. The private-versus-private case
still drops at the merge and is documented there; closing it needs one
assignment across contributors, made before any agent renders its context.

* 📌 test: Pin Alias Ownership on an In-Place Rewrite

Codex round 2 flagged that the identity alias still falls back to a counter
when the alias itself is literally claimed, so a displaced file can move after
all. The mechanism is real and my JSDoc overstated the guarantee, which this
corrects.

The behaviour stays as it is. The realistic way a file arrives under another
file's alias is the model rewriting the displaced file in place: it was told
`<stem>-<hash>.ext`, wrote back to it, and `processCodeOutput` registered an
output under that literal name. Keeping the alias with the newest holder is
what makes the model's familiar path resolve to its own edit; reserving it
against the newcomer would hand that path back to the bytes it replaced. The
test pins that ordering so a later change cannot invert it silently.
* fix: scope refresh user lookups to system context

* fix: preserve refresh system context

* fix: bind refresh recovery to user tenant
* fix: downgrade stale generation fence telemetry

* fix: preserve fenced persistence failures

* fix: avoid duplicate fence warnings

* fix: propagate terminal publication fences

* style: format fencing audit changes

* fix: keep successor transitions generation scoped

* fix: bound predecessor handoff retirement

* fix: reconcile discovered successors completely

* fix: preserve successor handoff acknowledgement
@pull pull Bot locked and limited conversation to collaborators Sep 1, 2026
@pull pull Bot added the ⤵️ pull label Sep 1, 2026
@pull
pull Bot merged commit cdfe54c into innFactory:main Sep 1, 2026
11 of 12 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants