Commit graph

388 commits

Author SHA1 Message Date
Marco Beretta
318da18d28
Fix canonical artifact ownership and frontend review regressions 2026-09-06 18:46:50 +02:00
Marco Beretta
5f9315e944
Resolve follow-up review findings 2026-09-06 18:46:50 +02:00
Marco Beretta
ea6e72fc7b
fix: address remaining Codex findings after dev rebase
Stamp agent ownership on streaming and final search snapshots. Compose the shared floating thought button. Preserve rebased group behavior, correct stale test selectors, and keep expansion callbacks outside state updaters.
2026-09-06 18:46:50 +02:00
Marco Beretta
f0f5bdd3a0
fix: persist failed summaries and stop double-rendering memory mutations
Stamp the failed marker on the aggregated summary part server-side, mirroring
how ON_RUN_STEP_CLOSED already stamps runStepStatus. The SDK aggregator ignores
a complete event that carries no summary, so an errored round's partial deltas
were saved unmarked and a reload re-rendered the truncated text under
"Conversation summarized".

Skip memory attachments that carry a toolCallId in the message-level
MemoryArtifacts decoration. Those calls already render inline as a MemoryCall
card with the same key, value and outcome, so one mutation appeared twice.
Attachments with no originating call keep the decoration as their only surface.

Keep a reasoning-bearing group expanded when Open Thinking Dropdowns by Default
is on. The folded-in THINK part only renders inside the group body, which is
not even mounted while collapsed, so the preference had no visible effect until
the action group was opened by hand.

Give same-named place results unique keys. Several branches of one chain share
a name and often carry no identifier, so rows whose address, rating and map
target all differ collided on one React key.
2026-09-06 18:46:10 +02:00
Danny Avila
aeac7a2065
🪪 feat: Add Per-Agent Git Identity (#15628)
* feat: Add per-agent Git identity

* fix: annotate git identity schema

* fix: complete git identity controls

* fix: clear git identity on version restore

* fix: Require a complete agent Git identity

* fix: Complete Git Identity Across Bash And Form Validation

* fix: Discard Incomplete Hidden Git Identity Edits

* fix: validate Git identity across panel lifecycle

* fix: preserve saved Git identity when discarding drafts
2026-09-05 23:56:23 -04:00
Ravi Kumar L
000349681e
🩻 feat(insights): Scope Insights to Authorized Agents (#15549)
Some checks failed
Docker Dev Images Build / publish (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
* feat(insights): add agent-scoped access

* fix(insights): exclude unattributed assistant messages

* fix(sharing): key role insight toggles by principal

* fix(sharing): preserve insight grant snapshot

* fix(permissions): preserve insight bits atomically

* fix(permissions): guard insight audit rollback

* fix(insights): keep admin grant controls accessible

* fix(permissions): reconcile duplicate principal updates

* fix(insights): show automatic admin access

* fix(permissions): harden insights access updates

* fix(insights): simplify authorization and preserve client state
2026-09-04 20:16:58 -04:00
Danny Avila
770238f22b
🔣 refactor: Replace Mongo Filters With Typed Criteria (#15580)
* ♻️ refactor: Take Typed Criteria, Not Mongo Filters, for Actions and Assistants

`createActionMethods` and `createAssistantMethods` accepted `FilterQuery<T>`,
putting Mongoose's query language in the package's public surface: every caller
wrote raw Mongo, and no engine other than Mongo could satisfy the signatures.

Both now take domain criteria — `ActionQuery` and `AssistantQuery` — naming
concepts rather than stored fields, with an array meaning "any of these". A
per-domain field map translates them, so `FilterQuery` survives only inside the
two translators.

`buildFilter` throws on a criterion the field map does not cover. Callers in
`api/` are JavaScript and get no compile-time checking, and the server sets
`strictQuery`, under which a silently dropped criterion widens the filter
instead of narrowing it — an unscoped `findOne` returns an arbitrary document
rather than none. Failing closed is the only safe default.

`AssistantQuery.avatarFilepath` covers the avatar-authorization lookup, which
queries a nested path no plain field name reaches.

No behaviour change: every migrated call site produces the same filter as before.

* 🧪 test: Update Avatar Authorization Assertion to Typed Criteria

`validateImages.spec.js` asserted the old `{ 'avatar.filepath': { $in: [...] } }`
call shape for `getAssistant`. The earlier sweep matched on `action_id`,
`agent_id` and `assistant_id`, none of which appear in a dotted avatar path, so
this one assertion was missed.

The `getAgent` assertions in the same file keep the filter shape — that method
still takes a `FilterQuery`.

* 🔒 fix: Reject Unknown Criteria Before Omitting Undefined Ones

`buildFilter` skipped a criterion whose value was `undefined` before checking it
against the field map, so a JavaScript caller misspelling a key that happened to
carry `undefined` — `deleteActions({ agent_id: maybeId })` — produced `{}` and
reached `deleteMany` unscoped. That is the exact failure the guard exists to
prevent, defeated by the order of the two checks.

Validate every key first, then apply the undefined-omission rule. The lookup is
an own-property check so inherited names cannot resolve through the field map's
prototype: `{ toString: 'x' }` previously found `Function.prototype.toString`,
passed the truthiness test, and wrote a garbage filter key instead of throwing.

A recognized criterion left `undefined` is still omitted, so optional parameters
keep working.

* 🔒 fix: Reject Query Fragments as Criteria and Keep the Translator Internal

Three review findings on the criteria translator, all of the same shape: the
guard was narrower than the surface it protects.

Criterion values are now validated as scalars or lists of scalars. `matchAny`
copied any non-array through unchanged, so a JavaScript caller could pass
`{ agentId: { $ne: null } }` and have the operator land verbatim in the filter
— the fail-open case the field map exists to close. `null` is rejected too; it
would have matched documents missing the field entirely.

`matchAny`, `buildFilter` and `FieldMap` are no longer re-exported from the
package barrel. They emit Mongo, and shipping them as public API undercut the
point of the change; they were reachable as `require('@librechat/data-schemas')
.buildFilter`. `OneOrMany` moves to `types/query` because the domain query types
reference it and it names no engine — which also drops the `types` -> `utils`
import.

`loadActionSets` names `ActionQuery` through `import()` so the JSDoc type
actually resolves.
2026-09-04 11:54:21 -04:00
Dustin Healy
5840e2f2ae
🪓 feat: Add Tenant-Scoped Agent Deletion (#15573)
* feat: add Agent Management delete endpoint

* fix: scope agent deletion cleanup by tenant
2026-09-04 09:51:12 -04:00
Dustin Healy
ab0d74b492
✏️ feat: Add Agent Management Update Endpoint (#15566)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* feat: add Agent Management update endpoint

* fix: reject unsupported Agent update values

* test: simplify Agent update mocks
2026-09-04 00:16:57 -04:00
Dustin Healy
2ffa9ec420
➕ feat: Add Agent Management Creation Endpoint (#15564)
* feat: add Agent Management creation endpoint

* 🧫 fix: Reject Null Agent Management Models (#15581)

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-03 23:45:04 -04:00
Danny Avila
8a46ca070c
🧊 feat: Persist Context Fading Tier Across Agent Runs (#15505)
* 🧊 feat: Persist Context Fading Tier Across Agent Runs

Carry the pruner's latched context-fading tier from `@librechat/agents`
through `contextMeta` the way `calibrationRatio` already travels, so a
historical tool result keeps the same truncated bytes from one run to the
next and Anthropic's prefix-based prompt cache survives across turns
(danny-avila/agents#497).

- Add `IAgentFadingTier` to the message and conversation `contextMeta`
  types, schemas and the data-provider zod schema
- Add `isAgentFadingTier`, `resolvePersistableFadingTier` and
  `resolveRunContextMeta` in `packages/api`; a tier is persisted once it
  carries information (masking active or a budget below the window), and
  a latched tier is kept even at a neutral calibration ratio
- Seed `createRun({ fadingTier })` from the parent response's contextMeta
  regardless of encoding, since caps are character-based; the field rides
  the existing forward-compatible `runConfig` variable and older SDK
  versions ignore it, as does `Run.getFadingTier` being absent
- Accept a valid `fading` field in event actor context and reject a
  malformed one, matching the calibration checks
- Capture run context meta through one module-level helper for both
  completion paths instead of two inline copies

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

* 🧊 fix: Seed Fading Tier Across HITL Resume, Validate at the DB Layer

Address review findings on fading tier persistence:

- Leave the "is this tier worth persisting" decision to the SDK, whose
  `Run.getFadingTier()` now returns only informative tiers; comparing
  against the client's window misclassified untouched conversations
  whenever a reserve ratio was configured
- Carry `contextMeta` through the HITL pause projection (staged approval,
  job metadata, resume state) and seed the rebuilt client from it, so a
  paused turn resumes with the same tier and calibration instead of
  re-deriving a shallower one and rewriting the prefix
- Share run seeding between `chatCompletion` and `resumeCompletion`, and
  make capture tolerate partial runs and bare resume contexts
- Validate `contextMeta.fading` in `commitAgentEventActorState` with a
  shared `isAgentFadingTier` guard in data-schemas, so a malformed tier is
  rejected at the DB layer instead of silently cold-starting the actor
- Reuse `IAgentEventActorContextMeta` for `IMessage.contextMeta` instead of
  extending an inline duplicate

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

* 🧊 test: Cover Resume Seeding From Paused Context Meta

Assert the resume controller seeds the rebuilt client from the context meta
captured at the pause before rebuilding the run, and move the capture
helper's JSDoc back above its function.

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

* 🧹 style: Sort Imports in Fading Tier Plumbing

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

* 🧊 fix: Persist Fading Tier Through Redis Jobs and Branches

Address Codex and Copilot review findings on fading tier persistence:

- Deserialize `contextMeta` in `RedisJobStore`, whose explicit read mapper
  rebuilt every other pause field but this one, so Redis-backed
  deployments resumed with no tier to seed after every HITL pause
- Copy server-private `contextMeta` onto the assistant message created by
  `POST /api/messages/branch`, so the next turn seeds its pruner the same
  way it would from the parallel source response
- Constrain the `fading` subdocuments in both Mongoose schemas (version
  enum, positive budget, required fields) to match the zod schema
- Throw a distinct "fading tier is invalid" error from both context meta
  validators instead of reporting a calibration failure

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

* 🧊 fix: Carry Context Meta Through Stop and Re-Pause

Addresses the second Codex pass on the fading-tier persistence:

- A re-pause whose resumed segment has nothing persistable now clears the
  job's `contextMeta` in the same transition instead of leaving the first
  pause's calibration and tier for the next resume to seed from.
- The client publishes the run's live calibration and fading tier onto the
  job after each pre-invoke context snapshot (deduplicated on value), and
  the Stop path copies `jobData.contextMeta` onto the stopped response, so
  a follow-up from a stopped turn keeps the prompt prefix stable.
- Mid-run captures (HITL pause, Stop) read the graph's live state; `Run`
  refreshes its own getters only after the stream settles, so the pause
  used to persist the seeds it started from.

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

* 🧊 fix: Fence Context Meta Publication Ahead of Stop

A Stop handled by another replica reads the job before the owner is
signaled, so a fire-and-forget publish could lose the race, and a run
stopped before its first context snapshot never published the tier it
inherited. The client now publishes the inherited seed before the run
streams or resumes, and the context-usage handler awaits each
snapshot's publish so the write lands before the model call it
describes begins. Failures still only log; the pre-run publish is
optional-called so bare resume contexts keep working.

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

* 🧊 fix: Read Back Context Meta After the Abort Claim

The run owner publishes its context meta ahead of each model call and
awaits the write, so a publish landing between the abort's initial job
read and its terminal claim describes the call whose partial output the
abort snapshot carries. abortJob now reads the same-epoch job back after
the claim and content re-read and takes its context meta, so the stopped
response persists the tier that produced its bytes. The refresh is
best-effort and logs on failure like the content refresh beside it.

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

* 🧊 fix: Share In-Flight Context Meta Writes and Clear Neutral Live State

Two snapshot callbacks in a parallel-agent run can capture the same tier
before the first write settles; the publisher now caches the in-flight
promise per serialized value so the second caller awaits the same
durable write instead of treating an uncommitted value as published.
A live snapshot with nothing left to persist after an earlier publish now
writes a neutral record (ratio 1, no tier), since a running job's fields
cannot be deleted through the metadata writer, so a Stop no longer
persists an earlier non-neutral state onto the response. The pre-run
seed publish is unchanged and is told apart from live snapshots by a
flag from the context-usage sink.

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

* 🧊 chore: Drop Unused Rest Parameter in Context Meta Test

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

* 🧊 feat: Persist Per-Agent Fading Tiers as Compact Context Meta

Reconciles the host with @librechat/agents at 9aacad1a, where graph
history stays canonical and each Run derives a provider-only projection
from the latched tiers. LibreChat therefore persists only compact state:
the calibration ratio, the default agent's tier, and now the per-agent
tiers from Run.getFadingTiers(), stored as validated entries so agent IDs
never become MongoDB field names and restored onto a null-prototype
record for RunConfig.fadingTiers. Truncated messages, canonical tool
content, projection provenance and per-message truncation state are
never persisted; the SDK's latched flag is stripped on capture.

The per-agent map travels everywhere the single tier already did:
response contextMeta, event-actor checkpoint state, HITL pause and
resume job metadata in both stores, the Stop path, and branch creation.
A shared Mongoose definition keeps the message and conversation schemas
identical, the zod schema validates entries, and commitAgentEventActorState
rejects malformed maps. Comments no longer describe graph messages as
truncated; only the provider projection is.

Tests cover: tool results and inputs reach the persisted content parts
at full size; contextMeta carries exactly the compact tier and
calibration fields; a persisted snapshot round-trips into the seeded
RunConfig unchanged and prototype-safe; per-agent tiers survive pause,
Redis job round-trips, and event-actor context validation.

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

* 🧊 fix: Restore Event Actor Types and Publish Seeds Before Abortable Setup

The previous commit's doc rewording in the conversation types dropped the
event-actor interfaces that precede the fading tier, which broke the
declaration bundle in CI; they are restored unchanged.

The inherited context meta is now published onto the job as soon as the
parent's state is loaded and awaited at the top of chatCompletion and
resumeCompletion, ahead of run creation and the other abortable setup
stages, so a Stop during setup still persists the parent's tiers onto the
stopped response. The partial response saved when every subscriber
disconnects now carries the job's context meta like the Stop and pause
paths do. The branch route keeps the source's context meta in the saved
message but strips it from the HTTP response, matching client reads.

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

* 🧊 fix: Serialize Distinct Context Meta Publications

Overlapping snapshots from parallel agents could carry different tiers;
each started its own write and both job stores keep whichever write
finished last, so an older snapshot could overwrite a newer one. Each
distinct publication is now issued after the previous one settles, in
call order, so the newest snapshot is what a Stop reads back. Equal
values still share one write.

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

* 🧊 fix: Project Root Message Reads and Keep Inherited Meta on Setup Failure

The root GET /api/messages paths returned raw documents: the
single-message query read called getMessages without a projection and the
cursor page performed an unprojected find. Both now apply
CLIENT_MESSAGE_SELECT, with getMessagesByCursor accepting a select option,
so the persisted fading tiers never reach the client through the
paginated endpoint; the search-hydration read projects the same way.

A turn that loads a parent's context meta but fails before its run exists
used to replace the inherited meta with the capture of a missing run, so
the persisted error response lost the last valid tiers. Both finalizers
now keep the inherited meta when no run was created while a created run's
neutral state may still clear it.

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

* 🧊 refactor: Move Context Meta Publication Into packages/api

The publication coordinator (ordering, deduplication, in-flight sharing,
failure state) now lives in packages/api as createContextMetaPublisher,
with selectRunContextMetaToPublish deciding what a publication carries;
the legacy client keeps only the wiring to the job metadata writer. A
transiently failing write is retried with a short backoff before the
publication is reported and forgotten, so a snapshot's model call no
longer proceeds on the first rejection while the job holds the previous
record. The SDK swallows handler errors, so retrying is the only way to
strengthen the fence without failing the turn.

A terminal save now supplies contextMeta: null when the finished run has
nothing to carry, and saveMessage unsets a previously stored value in
that case, so a disconnect snapshot's record cannot outlive the run that
completed neutrally.

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

* 🧊 fix: Widen the saveMessage Method Type for the Null Unset

The exported method interface still typed contextMeta as the message
field, so the spec exercising the null unset failed typechecking; it now
mirrors the implementation signature.

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

* 🧊 fix: Publish Every Agent's Snapshot, Sanitize Search Hits, Unbound Agent IDs

The context-usage handler now awaits the run's context meta publish for
every ON_CONTEXT_USAGE event, hidden sequential agents included, while
the visible-snapshot bookkeeping and the client emission keep their gate;
a Stop before the next visible snapshot finds the tier a hidden agent's
model call latched. Search results are built from the hydrated hit minus
its server-private contextMeta, since Meilisearch hydration projects every
schema field. The per-agent tier entry guard no longer caps the agent ID
length: ephemeral agent IDs encode endpoint, model and sender without a
bound, and dropping such an entry would let that agent alone re-derive
its tier on the next turn. Also formats the publication spec so the
static checks pass.

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

* 🧊 fix: Unset Context Meta Atomically, Keep Publication State After a Failed Write

- `saveMessage` folds the `contextMeta` unset into the same update that
  persists the terminal response, on both the plain and the provenance-merge
  paths, so a failure between two writes can no longer leave a completed row
  carrying a disconnect snapshot's state.
- `createContextMetaPublisher` tracks whether any record has committed
  separately from the retryable latest publication, so `hasPublished` stays
  true after a later publication exhausts its retries and a following neutral
  snapshot still overwrites the earlier record on the job.

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

* 🧊 fix: Carry Context Meta Through the Resumable Stop Route and Neutral Resumed Completions

- The resumable Stop route copies the refreshed `jobData.contextMeta` onto the
  terminal assistant message it persists, as the abort middleware already
  does, so a stopped resumable response keeps the tiers that produced its
  partial output.
- A resumed run that completes with neutral context state now saves
  `contextMeta: null`, which unsets the paused segment's record instead of
  letting an omitted field keep it on the completed row.

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

* 🧊 fix: Trust Only Server-Authored Context Meta, Unset It on a Neutral Stop, Read It From the Job on Disconnect

- `POST /api/messages/:conversationId` and conversation imports drop any
  client-supplied `contextMeta`, and the run seeds its calibration and fading
  tiers only from a server-authored parent response, so a forged tier can never
  shape a provider projection.
- Both Stop paths save `contextMeta: jobData.contextMeta ?? null`, so a job that
  re-paused with neutral state unsets what an earlier pause stored on the row
  instead of leaving it behind.
- The disconnect partial save reads the same-epoch job record for the run's
  published context meta, since the client-facing resume snapshot never carries
  server-private state.

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

* 🧪 test: Mock the Job Store in the Resume Metadata Spec

The disconnect partial save now reads the same-epoch job record through
`GenerationJobManager.getJobStore()`, so the resume metadata spec's manager
mock provides it as the other disconnect spec already does.

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

* 🧊 fix: Keep Context Meta Off the POST Message Response

`POST /api/messages/:conversationId` returned the saved row verbatim, so a
client write against an existing server-authored message echoed the row's
stored context meta. The route now projects the row through the same client
projection the branch route uses, which drops the server-private field.

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

* 📦 chore: Bump @librechat/agents to 3.7.16 for Persisted Fading Tiers

3.7.16 is the first release that carries danny-avila/agents#497, so
`Run.getFadingTier()` / `getFadingTiers()` now report the latched tier
this branch persists and `RunConfig.fadingTier` / `fadingTiers` seed the
next run from it. The dependency set is unchanged from 3.7.15; only the
package entry moves.

Adds an SDK-backed round trip to the fading spec: a first pruner latches
an informative tier, `resolveRunContextMeta` reduces it to the persisted
shape, and a second pruner seeded from that shape reproduces the first
run's projected tool-exchange bytes under instruction and calibration
drift, while an unseeded pruner under the same drift relaxes its tier and
rewrites them.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-02 20:29:43 -04:00
Danny Avila
29f738dea8
⏭️ feat: Honor an Interrupt Before the Model Has Answered (#15491)
* ⏭️ feat: Honor an Interrupt Before the Model Has Answered

An interrupt armed while the model is still thinking sat as "Interrupting"
until the entire turn finished, then landed as a terminal continuation. The
cause is upstream: the SDK reads `shouldPreempt` once per streamed chunk, and
its seal gate requires non-empty text — so a silent provider is never polled at
all, and a reasoning-only turn is never sealable.

`@librechat/agents` gains a discard-and-restart path for exactly that window
(`HOOK_PREEMPT_RESTART_CAPABLE`). This wires the host half:

- `GenerationJobManager.subscribePreempt` registers run-scoped wake listeners,
  notified after an ACCEPTED arm — local or cross-replica, since both land in
  `armPreemptIds`. Fenced on `createdAt` like every other preempt entry point,
  and a throwing listener never fails the arm a queued steer depends on.
- `createSteerPreemptPoll` hands that channel to the SDK as
  `StreamPreemption.subscribe`, gated on a new `isSteerPreemptRestartSupported`
  probe so an SDK that cannot act on a wake is never handed one.

The wake is a hint only: `isPreemptRequested` stays the single authority, so
the level-triggered contract the seal path depends on is unchanged.

Inert until `@librechat/agents` publishes the restart contract — the probe
returns false against the pinned 3.7.11 and the run keeps today's behavior.

* 🩹 fix: Replay an Already-Armed Preempt on Subscribe

Preempt requests are level-triggered, and the window between a job becoming
steerable and the SDK installing its model-attempt listener is real. An
interrupt landing there was recorded with no callbacks to notify, and on a
silent or reasoning-only turn no later chunk poll may ever run — leaving the
request to wait out the whole turn, the exact stall this channel removes.

`subscribePreempt` now replays an existing arm to the newly registered listener
only, after registration: waking the whole set would re-notify runs that
already looked, and waking before registration would leave a concurrent arm
with nowhere to land. The `createdAt` fence is unchanged and still gates the
replay.

The SDK also reads the level flag once at each attempt's start, so this is
belt-and-braces rather than the sole cover — but it makes the host correct on
its own terms instead of resting on when the SDK happens to look.

* 🔧 chore: Update @librechat/agents dependency to version 3.7.14 in package-lock.json and related package.json files

* 🔧 chore: Update dependencies in package-lock.json and package.json

- Bump versions for @humanfs/core (0.19.1 to 0.19.2), @humanfs/node (0.16.6 to 0.16.8), @xmldom/xmldom (0.8.13 to 0.8.15), and qs (6.15.2 to 6.16.0).
- Add @humanfs/types (0.15.0) as a new dependency for @humanfs/node.
- Update dependencies for side-channel and side-channel-list to their latest versions.
2026-09-02 13:35:52 -04:00
Dustin Healy
0b96fb9f70
🪪 feat: Propagate Tenant to Agent Model Headers (#15521)
* fix: propagate tenant to agent model headers

* fix: propagate tenant across agent model calls
2026-09-02 13:25:38 -04:00
Danny Avila
1c7a172321
🕹️ feat: Configure BYOM Tool Permissions (#15504)
* feat: add configurable BYOM permissions

* test: cover BYOM permission settings live

* fix: harden BYOM permission settings

* perf: reuse principals for environment discovery

* chore: normalize BYOM settings style

* fix: Align BYOM Discovery and Admission

* fix: Harden BYOM Permission Boundaries

* fix: Align BYOM Skill Approval Routing

* fix: Preserve Lazy BYOM Approval Capabilities

* perf: Parallelize Cached Environment Reads
2026-09-02 10:38:00 -04:00
Danny Avila
b1c56239f9
🧤 feat: Enforce Safe BYOM Tool Approvals (#15503)
* feat: add safe default approvals for BYOM code

* fix: preserve safe BYOM approval baseline
2026-09-02 08:53:16 -04:00
Marco Beretta
2cf1007f3c
🧺 feat: Preserve Partial Work at Tool Call Limits (#15486)
* feat: Handle reaching the agent tool-call limit gracefully

When the graph exhausts its per-turn step budget, persist the turn as
unfinished with finish_reason tool_call_limit instead of a red error.
Warn the model as the budget runs low, and offer Keep going or Answer now.

* docs: Add screenshots of the tool-call-limit notice

Light and dark captures of the unfinished-turn card for the PR description.

* style: Tighten the tool-call-limit notice card

Pin dismiss to the top-right with uniform rounding, use a slightly
smaller hit target, and round the card to xl. Refresh the PR screenshots.

* fix: Address Copilot review findings on the step-budget path

Skip empty and subagent-scoped PostToolBatch events when counting
rounds. Drop the double-cast in the notice test helper and coerce the
tool-call-limit finish reason in the Continue-gate spec.

* fix: Address Codex review findings on budget and dismiss

Pass the resolved recursion limit into the OpenAI-compatible
processStream path so the step-budget hook matches the graph.
Reset the notice dismiss state when the displayed message id changes.

* fix: Unblock CI typecheck and import sorting

Declare recursion_limit on the OpenAI service agent type, sort that
file's local imports, and route the notice test context through unknown
so tsc accepts the partial ChatContext fixture.

* fix: Sort remaining PR imports for static checks

* chore: Remove tool-call-limit notice screenshots

* fix: Keep recovery actions from consuming composer drafts

Pass empty file, skill, and quote overrides on Keep going and Answer
now so a staged compose is not attached or drained.
2026-09-01 21:38:02 -04:00
Danny Avila
164f146bdf
🛞 feat: Manage Personal Code Environment Lifecycles (#15369)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* feat: manage personal code environments

* fix: harden personal code environment lifecycle

* fix: satisfy backend lifecycle types

* fix: harden principal environment lifecycle and quotas

* fix: fence environment lifecycle races

* fix: reconcile code environment stack

* style: format code execution headers

* fix: reconcile code environment lifecycle

* fix: close self-service pairing gaps

* fix: close code environment reconciliation gaps

* fix: close code environment UI review findings

* fix: satisfy code environment type checks

* fix: route code priming to selected worker

* fix: harden code environment lifecycle fences

* fix: type lease renewal interval

* fix: harden environment lifecycle recovery

* fix: close lifecycle recovery races

* fix: type compensated agent timestamps

* fix: fence lifecycle cleanup by tenant and revision

* fix: scope environment revision fencing
2026-09-01 21:10:05 -04:00
Danny Avila
a3c2277c38
🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer (#15478)
* 🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer

The chat MCP picker offers a server only when the operator has not hidden it
(`chatMenu: false`) and the user reaches it directly rather than through an
agent (`consumeOnly`). Nothing enforced either condition on the request, so a
stale client, a replayed body, or a hand-written one could name any server and
get its tools — `consumeOnly` in particular is a permission the registry derives
per user, so ignoring it let a user reach in plain chat a server they had only
been granted through an agent.

Narrow the picker's selection in the two ephemeral loaders, against the same
predicate the client's `selectableServers` uses. The resolver is injected, so
`packages/api` stays free of the registry and the loaders keep their existing
`deps` shape.

Provenance is why this lives in the loaders rather than at the tool-resolution
chokepoint: only here is a server name known to have come from the picker.
Servers a model spec pins are the operator's own choice and are added after the
narrowing, so pinning a chat-hidden server to a spec keeps working.

`chatMenu` only ever comes from the config tier, so the overlay settles it with
no lookup at all. Lookups for the rest run concurrently and are cached per
server and user. A name the registry cannot resolve — request-tier, declared on
the body and never registered — is kept, as is one whose lookup fails: this
narrows an already-authenticated selection and is not the authorization
boundary.

* 📝 docs: Say What chatMenu Now Bounds

The schema still described `chatMenu` as dropdown visibility alone, which is
misleading now that it also bars a server from the chat selection a request
carries. Spell out both halves, and what it deliberately does not restrict:
agents, and servers a model spec pins.

* 🛡️ fix: Close Four Gaps in the Chat MCP Narrowing

Dedupe before resolving. The selection is request-supplied and the loaders used
to build a `Set` immediately; mapping the raw array first let one body repeat a
name enough times to launch a resolver — registry, cache, and ACL reads, with no
single-flight — per occurrence.

Resolve names through the tool-loading aliases. A configured `private server` is
addressable as `private_server` downstream, so an exact-key lookup let the
normalized spelling walk past a `chatMenu: false` check and still load tools.
Names are returned as sent; only the decision is canonicalized.

Let the request overlay settle `chatMenu`. The config tier is where the flag
lives and is what the picker itself was built from, so an overlay that re-enables
a server now outranks the durable config the registry returns, which was silently
dropping a valid selection. `consumeOnly` still comes from the registry.

Narrow the request body, not just the loader's copy. `applyContextToAgent` reads
`req.body.ephemeralAgent.mcp` directly and prefers it over the agent's tools, so
a hidden server's `serverInstructions` reached the model context even after its
tools were dropped.

* 🧭 fix: Ask the Registry Which MCP Servers the Picker May Offer

Three rounds of review found the same mistake in three disguises: this narrowing
re-derived the registry's tier precedence instead of asking for it. A config
entry was treated as authoritative over a user-DB or process-backed server of the
same name; the alias map was built from config names only, so a DB server named
`private_server` was misrouted to a config `private server`; and the overlay
short-circuit ran before any of that could be consulted.

Resolve the accessible set through `resolveAllMcpConfigs` instead — the same call
behind `GET /api/mcp/servers`, so what the request may use and what the picker
was offered come from one place and cannot drift. Precedence, cross-tier name
collisions, and the config overlay are the registry's job again, and the
selectability test is a single flag check over what it returns.

The result is smaller: no overlay branch, no per-server lookups, one catalog read
regardless of how long the selection is, and aliases built over the whole
accessible set so an exact name always outranks another server's normalized form.

Publish the servers actually in play back onto the request body, spec pins
included. `applyContextToAgent` prefers that list over the agent's tools, so
narrowing to the picker selection alone had started dropping a spec-pinned
server's `serverInstructions` while suppressing a hidden one's.
2026-09-01 18:47:36 -04:00
Danny Avila
51edb2a660
🛫 feat: Enable Self-Service Code Worker Pairing (#15367)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* style: format code execution headers

* fix: close self-service pairing gaps

* fix: satisfy code environment type checks
2026-09-01 18:46:33 -04:00
Danny Avila
aa7539909b
🪶 feat: Enable Standalone Skill Authoring (#15480)
* feat: enable standalone skill authoring

* test: align skill e2e flow with enable toggle

* fix: preserve legacy skill state on version restore
2026-09-01 18:39:05 -04:00
Danny Avila
dccd7ae496
🧶 fix: Preserve Multi-Agent Tool Call History (#15468)
* 🧭 fix: Preserve Multi-Agent Tool History

* 🧰 fix: Preserve Lazy Subagent Tool History

* 🧪 test: Enable Lazy Code History Fixture

* 🧭 fix: Derive Effective Lazy Tool History

* 🧪 fix: Tolerate Empty MCP Name Audits

* 🧪 test: Mock Lazy MCP Name Audit

* 🧭 fix: Expand Lazy Historical Tool Names

* 🧱 fix: Derive Durable Wildcard History

* 🧬 fix: Traverse Nested Wildcard History

* 🔭 fix: Resolve Wildcard Server Boundaries

* 🔐 fix: Persist MCP Call Identity

* 🪪 fix: Stamp Resolved MCP Execution Identity

* 🕸️ fix: Resolve Graph Member MCP Identity

* 🧭 fix: Fall Back From Empty Graph Member ID

* 🧭 fix: Preserve Aggregated MCP Identity

* 🧭 fix: Harden MCP Identity Resolution

* 🧭 test: Assert Concurrent Graph Limit Invariant

* 🧭 fix: Centralize Lazy History Policy

* 📦 chore: Bump `@librechat/agents` to v3.7.13

* 🧪 test: Align Content Filter Fixture With SDK History
2026-09-01 18:37:45 -04:00
Danny Avila
982a9a5141
🧴 fix: Strip LangChain URLs From Provider Errors (#15421)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🧹 fix: Strip LangChain Attribution From User-Facing Provider Errors

LangChain classifies provider failures by mutating `error.message` with a
docs URL, and that message is forwarded verbatim into the error content part
we persist, so users saw `docs.langchain.com/...` credited for a provider's
own 429. The client only intercepted `MODEL_NOT_FOUND`, leaving every other
code to render raw.

Resolve the classification server-side instead: codes with localized copy
become the typed payload the client already knows how to localize, and every
other message keeps the provider's wording with the docs URL removed before
it reaches the database. The untouched error still reaches the logs through
`getSafeErrorMetadata`.

The client keeps a code-driven lookup for messages persisted before this, and
gains `com_error_model_rate_limit` alongside the existing not-found copy. The
OpenAI-compatible and Responses controllers now share one provider-error
helper rather than three copies of it, so the strip applies on those surfaces
too.

* 🛡️ fix: Harden Error Sanitization For Non-String Messages And Legacy Text

Codex review follow-up.

`stripLangChainTroubleshootingUrl` assumed a string, so an error-like object
with a truthy non-string `message` threw `.replace is not a function` inside
the error handler that was meant to report it — the template interpolation it
replaced had coerced such values safely. It now coerces at the boundary, as
does the code lookup.

Legacy messages whose LangChain code has no localized copy still rendered the
docs URL, which is exactly the historical case the client-side path exists to
clean up. The rendered fallback is now stripped too.

Both helpers move to `librechat-data-provider`, which is shared by the client
and the backend, so one implementation serves both instead of a second regex
drifting in the component.
2026-08-31 12:51:34 -04:00
Danny Avila
b7ae67307e
🕰️ fix: Anchor Temporal Variables to Turn Start (#15419) 2026-08-31 10:38:10 -04:00
Danny Avila
413906abc3
🖍️ fix: Honor Agent Image Detail Settings (#15407)
* 🖼️ fix: Apply an Agent's Configured Image Detail

The Agent Builder exposes an Image Detail slider and saves it to
`model_parameters.imageDetail`, but nothing ever read it back: `encodeAndFormat`
resolves the level from `req.body.imageDetail`, which the agents route never
populates, so every image was encoded as `detail: "auto"`. Providers that reject
`auto` — MiniMax's OpenAI-compatible API among them — fail every image upload
through an agent, and the setting that would fix it has no effect.

The root cause is a classification error rather than a missing argument.
`extractLibreChatParams` splits LibreChat's own settings out of an agent's model
parameters, and its key set is derived from the `librechat` group in
`parameterSettings`. `imageDetail` sat in `baseDefinitions` instead, so it was
neither extracted for LibreChat's use nor stripped from the options handed to the
provider.

It is now grouped with `resendFiles` and the other LibreChat-level parameters,
which gives it the same path those already take: extracted during agent
initialization, carried on the initialized agent, passed to the client, and read
by `encodeAndFormat`. `AgentClient.getSaveOptions` already expected
`options.imageDetail` — nothing had ever populated it.

The request body still wins where it is set, so the classic endpoints keep
resolving detail from conversation settings exactly as before.

Fixes #15359

* 🔤 fix: Type Image Detail as Its Enum, Not a Bare String

`TConversation.imageDetail` is the `ImageDetail` enum, so the extractor's return
type and the initialized agent's field are now declared as that enum rather than
widened to `string`, and the tests pass enum members instead of raw literals.

Caught by CI's type check, which I had run before adding the tests and not after —
jest compiles the specs through babel, so the suite stayed green over a type error.
2026-08-31 08:34:05 -04:00
Danny Avila
c7f0a8f247
📬 feat: Move Agent Queued Turns Server-Side (#15370)
* feat: move agent queued turns server-side

* test: update queued turn composition coverage

* fix: harden queued-turn lifecycle ownership

* fix: centralize queued-turn lifecycle ownership

* fix: close queued-turn lifecycle gaps

* fix: unify queued turn lifecycle transitions

* fix: fence queued-turn deletion admission

* fix: preserve queued-turn lifecycle ownership

* fix: close queued-turn reconciliation races

* fix: reconcile quarantined queued-turn admissions

* fix: annotate admission evidence default

* fix: deepen queued turn lifecycle admission

* fix: fence queued turn provider admission
2026-08-30 21:53:58 -04:00
Danny Avila
7533d138fa
🧬 perf: Evolve Compaction Guidance on Warm Turns (#15371)
* perf: evolve compaction guidance on warm turns

* style: sort compaction adapter imports
2026-08-30 17:11:20 -04:00
Danny Avila
30124f21b2
🎻 refactor: Orchestrate Agent Runs Through a Request-Free Host (#15366)
* refactor: decouple agent initialization from HTTP

* refactor: centralize remote agent execution lifecycle

* fix: preserve pre-settlement error rendering

* fix: preserve request-backed tool loading

* style: sort agent execution imports

* fix: adapt public agent tool loaders
2026-08-30 15:38:57 -04:00
Danny Avila
c1cb591d49
🗿 feat: Add Attached Stateful Code Environments (#15352)
* feat: add attached stateful code environments

* fix: include code environment in lazy agent type

* fix: harden stateful environment routing

* fix: complete code environment route isolation

* fix: declare code environment map type

* fix: preserve configured code execution routes

* fix: harden stateful environment updates

* test: assert route-scoped sandbox readiness

* fix: harden stateful environment lifecycle

* fix: isolate migrated code sessions

* test: assert route-qualified code sessions
2026-08-30 15:38:45 -04:00
Danny Avila
cd3768ed1f
🍵 feat: Continue Late Steers in Warm Agent Runs (#15357)
* feat: continue late steers in warm agent runs

* chore: bump agents sdk to v3.7.9

* fix: guard terminal steer admission
2026-08-30 11:54:17 -04:00
Danny Avila
2ae6c8aea9
🛎️ feat: Wake Agents on Background Tool Completion (#15350)
* feat: wake agents for background tool completion

* fix: isolate background completion contracts

* fix: anchor background completion identity

* fix: close background completion delivery gaps

* fix: preserve manual background polling

* fix: preserve legacy tool group identity

* fix: close background wakeup identity gaps

* test: type background wakeup enqueue mock

* fix: preserve background completion identity

* test: type activity phase fixture

* test: mock phase media query

* fix: arbitrate background result ownership

* fix: retain tool step routing helper

* test: expand phase groups before identity checks

* fix: preserve background completion ownership

* fix: bound durable background results

* fix: type wakeup input budget export

* chore: sort background handler imports

* fix: persist timed-out background completions

* fix: reconcile background completion ownership

* fix: type background completion capabilities

* fix: harden background completion terminalization

* test: type missing completion evidence

* fix: require evidence for completion retirement

* chore: satisfy background completion static checks

* test: cover automatic background completion wakeups

* test: assert background wakeup agent identity

* fix: wake capability-fenced trigger deliveries

* test: keep capability shield fixture public

* test: assert capability worker wakeup

* fix: close background completion ownership gaps

* test: satisfy completion lease static checks

* fix: preserve artifact completion wakeups

* fix: close background delivery recovery gaps

* fix: simplify background receipt guidance

* fix: recover dead background completion batches

* fix: fence background completion recovery

* fix: recheck background recovery ownership

* fix: fence unpublished background continuations

* test: await background message hydration
2026-08-30 08:56:41 -04:00
Danny Avila
e3ccaba5af
🛄 feat: Restore Compaction Guidance Across Continuations (#15356)
* 🧭 feat: preserve compaction guidance across continuations

* 🔧 fix: narrow persisted compaction fields
2026-08-30 08:13:24 -04:00
Danny Avila
70f735336d
🛎️ fix: Enroll Remote Agent Runs in the Generation Lifecycle (#15349)
* fix: enroll remote agent runs in generation lifecycle

* fix: close remote lifecycle ownership gaps

* fix: close remote conversation drain races

* fix: fence remote runs during conversation deletion

* fix: reconcile remote deletion and settlement races

* fix: complete owner deletion recovery

* fix: preserve remote cleanup receipts

* fix: consume deletion receipts before cleanup

* fix: expose idempotent deletion option

* chore: sort remote lifecycle imports
2026-08-30 07:14:13 -04:00
Danny Avila
10981b8637
🍱 feat: Guide Compaction With a Bounded Semantic Index (#15340)
* 🧭 feat: guide compaction with semantic context

* 🛡️ fix: fail closed on semantic intent collisions
2026-08-29 17:53:04 -04:00
Danny Avila
773127bff2
🎠 refactor: Route Every Event Actor Turn Through One Lifecycle (#15325)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* refactor: unify Event Actor turn lifecycle

* fix: retain Event Actor fence ownership

* fix: preserve mixed-version actor suspension safety
2026-08-28 17:30:23 -04:00
Danny Avila
3fa33b740b
🛫 refactor: Promote Generation Protocol V2 Automatically (#15324)
* refactor: promote generation protocol v2 automatically

* fix: remove unused protocol import
2026-08-28 17:17:09 -04:00
Danny Avila
77c2a51cf3
🪧 fix: Advertise Detached Event Actor Support from the Generation Store (#15322)
Some checks failed
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Has been cancelled
Frontend Unit Tests / Build packages (push) Has been cancelled
Frontend Unit Tests / TypeScript type checks (client) (push) Has been cancelled
Frontend Unit Tests / Tests: @librechat/client (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Has been cancelled
Frontend Unit Tests / Vite build verification (push) Has been cancelled
* fix: activate detached event actions automatically

* fix: shield detached terminal generations

* perf: share capability availability index

* style: flatten capability status selection

* fix: preserve mixed-version lifecycle shells

* fix: complete detached action store adapters

* fix: close mixed-version capability races

* fix: honor legacy capability success
2026-08-28 16:33:23 -04:00
Danny Avila
487984193a
🪂 feat: Make Event Actor Detached Actions Durable (#15307)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* feat: make Event Actor detached actions durable

* fix: align detached actor checks with merged base

* fix: close detached actor lifecycle gaps

* fix: break detached action outcome cycle

* fix: deepen detached action ownership

* fix: fence detached launch handoffs

* fix: preserve detached rollout fencing

* test: type detached recovery failure

* fix: close detached ownership handoffs

* fix: stage detached action activation

* fix: persist detached terminal retries

* fix: fence detached durability ownership

* fix: fence trigger lane publication

* fix: fence detached transition generations

* fix: retire failed detached predecessors

* fix: close detached rollout consumers

* test: type detached retry turns

* fix: close detached ownership gaps

* fix: fence detached action resumes

* fix: fence prior-head event actor resumes

* fix: preserve legacy event actor resumes

* fix: align detached resume boundaries
2026-08-28 14:21:04 -04:00
Danny Avila
c06b09c945
🤖 feat: make Event Actor HITL durable (#15305)
* 🤖 feat: make event actor HITL durable

* 🤖 fix: break event actor outcome cycle

* 🤖 fix: close durable actor terminal races

* fix: harden durable event actor recovery proofs

* fix: close event actor resume publication races
2026-08-28 08:05:11 -04:00
Danny Avila
6fba51a35a
🫆 feat: Fingerprint Agent Context for Zero-Read Warm Continuation (#15299)
* feat: add context-fingerprinted warm continuation

* fix: preserve manual skills across warm turns

* perf: keep ordinary agent initialization neutral

* fix: fingerprint complete agent topology

* fix: close warm context replay gaps

* fix: preserve lazy subagent memory scope

* perf: fingerprint only model-bound memory

* fix: project lazy memory capability

* style: sort warm continuation imports

* fix: make warm continuation reconstructable

* fix: preserve lazy skill isolation

* fix: retain scoped skill semantics
2026-08-27 23:01:14 -04:00
Danny Avila
a5fd00cd7a
🎼 feat: Unify Agent Turn Execution Behind One Immutable Plan (#15296)
* feat: unify agent turn execution planning

* fix: preserve automatic subagent activity handles

* fix: gate subagent wakeups for ephemeral parents

* fix: scope subagent wakeup guidance by agent
2026-08-27 22:26:46 -04:00
Danny Avila
f4efef4b3a
♟️ fix: Settle First Bound Actor Events (#15300) 2026-08-27 19:06:41 -04:00
Danny Avila
8ba76507ae
🧷 fix: Verify Scheduled HITL State Before Pausing (#15284)
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
* fix: require durable storage for scheduled HITL

* fix: verify scheduled HITL checkpoints before pause

* fix: narrow scheduled HITL capability checks

* fix: honor ask tool filters in schedule preflight

* fix: enforce durability across scheduled HITL resumes

* fix: scope scheduled HITL admission to active tools

* refactor: move scheduled HITL admission to TypeScript

* fix: close scheduled HITL admission gaps

* fix: bind scheduled pauses to current checkpoint

* fix: cap pending actions at binding deadline

* fix: reject expired pending actions

* fix: guard approval deadlines atomically
2026-08-27 11:16:24 -04:00
Danny Avila
41ba808e6f
🧅 feat: Reveal Subagent Thread History and Event Detail on Demand (#15283)
* ✨ feat: Refine Subagent Thread Activity UX

* test: align subagent completion lifecycle

* chore: satisfy subagent panel static checks

* fix: harden subagent activity pagination

* fix: preserve paged subagent history

* fix: surface unrecoverable activity boundaries

* fix: rebase paged activity after live advances

* fix: bound child activity history state

* fix: preserve rebased child history order

* fix: retain unavailable child history boundary

* fix: fence child history cursor generations

* fix: preserve subagent history continuity

* fix: retain live turns during history rebase

* fix: close subagent history edge cases

* fix: harden subagent history phase transitions
2026-08-27 08:56:06 -04:00
Danny Avila
44edcbe014
🧾 feat: Store Durable Event Actor Receipts (#15265)
* 🧾 feat: Store durable event actor receipts

* 📊 fix: Scope actor delivery metrics

* 🧱 fix: Close durable receipt recovery gaps

* fix: serialize actor action admission

* fix: retire terminal batch members

* fix: close actor terminal recovery races

* fix: reclaim lanes after terminal receipts

* fix: close actor receipt recovery gaps

* fix: preserve ambiguous legacy handling

* fix: recover legacy actor outcomes

* fix: admit leased actor actions

* fix: serialize actor admission recovery

* fix: close actor recovery races

* fix: token-fence actor admissions

* fix: preserve mixed-version actor fences

* fix: harden actor rollout and metrics

* fix: gate durable actor receipt rollout

* fix: enforce base actor receipt rollout

* fix: align experimental actor rollout
2026-08-27 06:00:43 -04:00
Danny Avila
68fc46a055
🪢 feat: Resume Bound Event Actors from Checkpoint Forks (#15227)
* feat: resume event actors from checkpoint forks

* fix: fence event actor checkpoint uncertainty

* fix: satisfy event actor type contracts

* fix: make actor reconciliation recoverable

* fix: preserve event actor lifecycle transitions

* fix: fence event actor lifecycle outcomes

* fix: enforce event actor lifecycle ownership

* fix: retain event actor settlement proof

* 🔒 fix: Retain Event Actor Receipts Through Repair and Bound Their Journal

Repair and compensation deleted the reconciliation row they resolved, which
was the only durable proof that the invocation had already applied an external
action. A delayed duplicate owner could then reacquire the same invocation id
and repeat that action. Both resolutions now retire their receipt to `settled`
and record how it settled, so the same-id tombstone survives; `history_repaired`
and `action_compensated` still force a cold rebuild. Compensation undoes the
effect without re-authorizing the delivery, so a legitimate retry must arrive
under a new invocation id. A retried repair converges on its own receipt.

Bound the journal so a long-lived actor cannot grow its conversation document
without limit: a new fence is admitted only when no active lifecycle row
exists, so a capped push can evict nothing but the oldest settled receipts.

Stop shipping the unbounded source payload on every bound-child continuation.
It rode the delivery body regardless of the feature flag while the sibling
`fire` body deliberately sends event identity alone, so a large webhook payload
could push a previously working delivery past the chat route's body limit. The
actor binds an invocation from identity and never builds the prompt from it.

Blank the positional token map on warm continuations. It is derived from the
full DB history, while a warm run executes on checkpoint-restored state, so its
indices address different messages and the pruner never recounts them —
misattributing cached counts to the wrong messages in both directions.

Keep the replaced-claim exit on its cleanup path when preserving reconciliation
fails: the committing CAS already left a blocking row, so the failed status
upgrade costs provenance, not safety.

* 🧪 test: Pin the Warm Continuation's Map/Summary Asymmetry

Give the warm-continuation client test a populated token map and a real
cross-run summary so its assertions bite: the positional map must arrive
blank (checkpoint-restored state no longer matches DB-derived indices, and
the pruner never recounts a populated entry) while `initialSummary` must
pass through unchanged — it rides the system tail and summarizes
pre-boundary turns that were excluded from the very history the committed
checkpoint was built from, so blanking it would silently drop context no
warm run can recover.

* ⚖️ fix: Honor Compensation in Settlement and Age-Bound the Receipt Journal

A compensated receipt still tombstones its invocation id, but its external
effect was explicitly undone — the terminal handler nonetheless replayed
every settled lifecycle's stored action as authoritative and settled the
public outcome as applied, telling an action-aware source the operation
stands and suppressing the new-invocation retry compensation requires. The
handler now settles a compensated invocation as failed with an explicit
compensation error, overriding even fresh applied run evidence from a
replayed generation; verified and repaired receipts continue to replay
applied.

Receipt eviction is now primarily age-based: a stale same-id owner is
bounded by time, not by how many newer invocations settle, so the previous
count-only slice let a high-rate actor evict a tombstone while its delayed
duplicate owner could still wake and repeat the action. Admission prunes
only settled receipts older than a retention window that dwarfs every
generation, job, and delivery-retry lifetime, and the count cap is demoted
to a raised document-size backstop.

* 🔀 fix: Serialize Compensation Against Settlement and Never Evict Live Receipts

The terminal handler read its lifecycle snapshot, verified history, and then
settled the public outcome — so a compensation resolving the same receipt
during that window lost: the handler settled applied from its stale snapshot
and no retry could ever change the replay-identity-locked outcome. The
receipt's status CAS is now the serialization point: verification resolves
the receipt BEFORE settling, whichever transition wins determines the public
outcome, and a crash between resolve and settle converges through the
retained receipt's replay. The verified-replay probe requires the receipt's
own resolution, so a compensated receipt can never satisfy a verification
retry. This inverts the settle-before-receipt ordering deliberately: that
ordering guarded proof that resolution used to delete, and the receipt now
retains its full action proof through resolution.

The document-size cap is no longer an eviction quota. A receipt inside its
retention window is never discarded: when the journal holds a full cap of
unexpired receipts, new invocations are refused fail-closed until receipts
age out, making duplicate protection and document integrity simultaneous
invariants instead of a rate-dependent trade.

* 🎓 fix: Keep Skill-Bearing Event Actors on the Legacy Path

Skill primes are spliced into the message list directly ahead of the newest
message, and a warm continuation forwards only that newest message — so a
checkpoint-restored actor would keep serving the prime bodies baked in at
its last cold start and never observe an edited or newly attached skill.
Until the actor head carries a context fingerprint that forces a cold
rebuild when the agent's skill context changes, agents with always-apply or
manual skill primes stay on the legacy path, which re-primes fresh bodies
every turn: correct on every event, just never warm.

* 📜 fix: Gate Fork Mode on the Skills Capability, Not Just Request-Time Primes

History-derived re-priming was a third path into the same staleness class:
an actor that previously invoked a skill carries no request-time prime
arrays, yet primeInvokedSkills re-resolves that skill's current body from
history each turn and the warm slice drops the reconstruction — leaving the
checkpoint's old body active after edits. The fork gate now keys on the
priming hook itself (present exactly when the skills capability is enabled)
alongside the request-time arrays, so every skill-body path routes to the
legacy rebuild until #15235's context fingerprint restores warm
continuation for skill-bearing actors.

* 🧾 fix: Capture Applied-Action Proof at Tool Execution, Not After sendMessage

The executor read applied-action evidence from the run-step collection the
instant sendMessage resolved, but that collection is populated
asynchronously — an applied invocation could classify as actionless
(runSteps still empty while the tool result already streamed), discarding
its fork and stranding the actor cold while the terminal handler later
settled the same delivery as applied from the persisted evidence.

Authoritative proof is now recorded in graph context the moment the
expected tool executes: the request-owned recorder observes the tool-end
chain (which ToolNode dispatches synchronously with both input and output)
and applies the same fences as run-step evidence — exact tool name with the
MCP-suffixed form, the declared argument subset against the execution
input, an error-free result, and the background non-execution receipt
exclusion. readAppliedAction consults the receipt first; run-step
inspection remains the fallback for paths that bypass the tool-end chain.

Regression coverage reproduces the observed ordering: the real executor
commits the head from the receipt while run steps are empty, warm-continues
the next event, and never re-executes the action; recorder fences and the
receipt-first controller wiring are covered separately.

* 🎯 fix: Supply Execution Arguments to the Tool End Callback

The live Vertex + MCP canary exposed a contract mismatch the synthetic
fixtures hid: the ON_TOOL_EXECUTE execution path invoked its tool end
callback with output only, while the action recorder must verify the
declared argument subset against the execution input. The receipt never
qualified, every turn fell back to cold history rebuilds, and the
tournament advanced with zero actor heads and zero retained checkpoints
while looking successful.

The execution handler owns both halves at the same moment, so the fix is
at the source rather than a correlation store: ToolEndCallbackData gains
the executed call's input and every callback site passes tc.args. A
handler-level regression drives the real createToolExecuteHandler and
asserts the callback receives both fields; recorder regressions pin the
production shapes — an output-only tool end must starve an
argument-fenced receipt rather than trust an unfenced match, and still
qualifies a name-only expected action.

* 🕵️ fix: Mark Background Deliveries So They Cannot Impersonate Applied Actions

The background-claim callback reports the ORIGINAL tool's name for artifact
attribution on the poll turn that harvests a completed task. A name-only
expected action could therefore be impersonated by work some earlier turn
dispatched: the recorder would attribute that delivery to the current
invocation and commit a head whose state never contained the invocation's
own action. The run-step evidence path never had this hole — it sees the
poll tool's name — so the recorder must match its provenance discipline.

Delivery callbacks now carry an explicit backgroundDelivery marker set at
the one site that rewrites the name, and the recorder ignores marked
deliveries outright. Regressions pin both halves of the contract: the
delivery callback must carry the marker with the poll call's arguments,
and a marked delivery can never qualify even a name-only expected action.

* 🧿 fix: Version Invalidations, Keep Evidence Ahead of Output Policy, Gate Detachable Actions

Three closeout-round findings, each converted into an invariant.

Every legacy-path invalidation now advances a durable epoch — including for
headless and already cold-marked actors, where the marker alone leaves no
CAS-visible trace — and the actor-head CAS requires the epoch observed at
preparation. A concurrently prepared fork whose history predates an
intervening legacy turn can no longer commit past it; the commit reports an
ordinary conflict and journals.

Execution identity is now emitted before post-execution output policy: when
a side-effecting tool succeeds but its returned content is withheld by the
output filter, the callback delivers an outputFiltered receipt with blank
content — the recorder accepts it as proof (rejecting model-detached calls
it cannot distinguish through the blank shape), the artifact path never
sees it, and an applied action is no longer reclassified actionless and
re-executed on retry.

Background-capable expected actions stay off the fork path: dispatch
returns a launch handle every evidence fence correctly rejects, and the
completion is provenance-marked as another turn's work, so a fork would
settle actionless before the external effect lands with nothing to stop a
retry from dispatching it again. The gate mirrors the MCP-suffix name
matching of the evidence path.

* 🚧 fix: Seal the Whole Legacy Turn Behind a Second Epoch Advance

The epoch fenced only the legacy turn's start: a fork preparing after the
begin invalidation but before the turn's message persistence observed the
new epoch and cold marker, rebuilt from history that did not yet contain
the turn, and committed cleanly because nothing advanced the epoch again —
making the incomplete rebuild authoritative and clearing the marker.

Every legacy event turn now seals its invalidation at terminal persistence
with a second epoch advance, on the success, replaced-claim, and error
exits alike. Sealing deliberately carries no quiescence requirement — it
must succeed while a fork fence is active, because that is exactly the
mid-turn race it defeats — and a fork that already committed against the
begin epoch is healed the same way: the seal re-marks the head cold, so
the next event rebuilds with complete history. Seal failure never diverts
the turn's own exit; the begin bump still fences everything prepared
before the turn.

* 🔗 fix: Replace the Best-Effort Epoch Bump With a Durable Legacy-Turn Fence

The second epoch advance could not make a legacy turn atomic, and three
findings shared that root cause: two conditional updates left a headless
gap a fork could create the head inside; the error exit sealed before
saveErrorTurn made the error history durable; and any crash or failure
between persistence and sealing left an incomplete fork authoritative,
because the seal was best-effort and its failure was swallowed.

A legacy turn now carries one durable fence. A token is written before
execution by a single update-pipeline write — no two-write gap, and the
cold marker is applied only where a head exists via $cond/$$REMOVE. While
the token is present no fork may prepare (the adapter refuses) or commit
(the CAS requires its absence), because the turn's messages are not yet
durable. One atomic write clears the exact token and advances the epoch
once history is persisted — after saveErrorTurn on the error exit — and
success, replacement, and error exits all route through it.

Failure is now fail-closed rather than silent: a failed seal leaves the
token set, which keeps blocking forks and is logged as such, and a fence
abandoned by a crashed turn is reclaimed only once stale, advancing the
epoch and marking any head cold so the next event rebuilds from whatever
history actually survived.

* fix: serialize legacy event actor turns

* fix: close legacy actor fence ownership gaps

* fix: preserve legacy actor persistence fences
2026-08-26 08:39:00 -04:00
Danny Avila
1489623fa3
🎬 test: Record-Once/Replay-Forever Model Fixtures for Mock E2E (#15210)
* 🎬 test: Record-Once/Replay-Forever Model Fixtures for Mock E2E

The mock e2e lane's only credential-free model is a hand-authored script:
`fake-model.js` decides responses from ~60 `E2E_*` prompt markers. That
covers scripted shapes well, but no scenario replays a *real* recorded
provider conversation through the assembled chain, so real streaming
shapes — provider chunk cadence, reasoning deltas, usage metadata — are
only ever approximated.

This adds a record-once/replay-forever tier alongside the marker routing.

Record (`E2E_MODEL_FIXTURES=record`, needs a provider key): the run hook
appends a LangChain callback handler to every agent context's
`clientOptions.callbacks` instead of overriding the model, so the REAL
provider streams while each invocation's `AIMessageChunk`s serialize to
`e2e/fixtures/model-replay/<name>.jsonl` — text deltas, tool_call_chunks,
reasoning kwargs, and genuine usage metadata. Only the latest human text
is recorded for binding; system prompts and tool schemas never enter the
fixture.

Replay (default, keyless): `fake-model.js` consults `tryBindReplay` ahead
of marker routing, binding a conversation whose prompt matches the next
unconsumed invocation. The replaying model is not hand-assigned — it is
registered as SDK provider `librechat-e2e-replay` via `registerProvider`
and constructed through the SDK's own `initializeModel`, so registry
lookup, constructor clientOptions, and real `bindTools` all run the way a
live provider's would. Recorded chunks therefore stream through the same
createRun → graph → SSE → persistence chain.

Consumption is enforced rather than assumed: every invocation re-checks
its prompt against the recording, an invocation past the end of the
script throws, and a per-fixture ledger lets the spec assert at teardown
that every recorded invocation and chunk was drained. Streaming
incrementality is asserted from that ledger, not by sampling transient
DOM, which is a race by construction.

The credential-free profile is unchanged when not recording: the record
provider and its selector entry are template markers that stay comments,
and no existing spec's routing is touched (a fixture only binds on an
exact prompt match; everything else falls through).

Verified: record vs the real DeepSeek API 1 passed (15.2s); keyless
replay 1 passed twice (11.4s, 12.7s) with the ledger fully drained (2/2
invocations, 10/10 chunks, no overruns or mismatches); app-load,
completion, and chat 10 passed unchanged.

* fix(e2e): rebind a replay fixture from the top for a new conversation

The replay cursor is process-global while the web server outlives a
Playwright retry, so a fully consumed fixture left the retry unable to
bind its first prompt: it fell through to marker routing and failed
deterministically, burning every configured CI retry. A partially
consumed attempt failed the same way.

Binding now restarts the fixture when the incoming prompt matches its
first recorded invocation, resetting the ledger with the cursor so the
new attempt is judged on its own consumption instead of accumulating the
previous one's counts. Continuing an in-progress binding still outranks
restarting, so a fixture whose opening prompt repeats later in the script
advances rather than rewinding.

The over-consumption guard is untouched — it fires inside the stream when
the cursor passes the end, not at bind time.

* fix(e2e): close three replay-lane gaps found in review

Restart the recorder on a retry. Its state is process-global like the
replay cursor, so a failed attempt that had already recorded invocations
left the counter advanced: the retry appended 2/3 after 0/1, or kept the
previous attempt's `error` line, and the fixture was unusable for replay.
Recording now truncates and restarts when the opening prompt reappears,
mirroring the replay side's rule and its caveat.

Retain a consumed binding for the conversation that drove it. An extra
user turn past the final recorded invocation found no next invocation and
fell through to ordinary fake-model routing, so it was answered with a
mock reply: the over-consumption guard never ran and the already-drained
ledger still passed. Such a conversation is now recognized by its human
turns opening with the fixture's recorded prompts, and stays bound so the
stream raises the overrun. Continuing an in-progress binding still
outranks restarting, which outranks retaining a consumed one, so a
retry's fresh conversation rewinds rather than being read as an extra
turn.

Validate the fixture the recording actually wrote. Record mode honors
`E2E_MODEL_FIXTURE_NAME`, but the spec always inspected the committed
`deepseek-two-turn`; another name wrote elsewhere while the assertions
read the pre-existing file, and because the prompts are fixed the stale
answers could match and green a run that verified nothing it produced.

* fix(e2e): make replay binding correct for tool and subagent fixtures

Round two's retry and consumed-binding fixes both assumed one model
invocation per user turn. A turn that calls a tool breaks that: the model
is invoked again after the tool result under the same latest human
message.

Identify a retry by the conversation boundary, not the prompt. The
recorder ran per invocation and truncated whenever the opening prompt
reappeared, so a tool round trip looked like a retry and discarded the
recorded tool-call invocation. Restart detection now sits in
`installRecorder`, which runs once per `createRun`: a turn whose history
holds no prior human message begins a conversation.

Compare consumed bindings against user turns, not invocations. Several
recorded invocations can share one prompt, so a one-to-one comparison
could not recognize the originating conversation — invocations `[A, A, B]`
against history `[A, B, C]` failed on both length and elements, and the
extra turn fell through to the fake model with the drained ledger still
passing. Fixtures now carry their collapsed turn sequence.

Override the subagent model too. `graph.overrideModel` is not inherited
by child executors, so a fixture recording a subagent call — record mode
captures child invocations already — would leave the child on its
configured provider: an underrun, and a real provider request in a lane
that must stay keyless.

Reject ambiguous prompt matches. Binding order followed filesystem
enumeration, so a second fixture sharing a prompt could silently redirect
a scenario to the wrong chunks and ledger; the spec's choice never
reaches the server-side loop, so ambiguity fails instead of picking a
winner. Fixture identity is the file name for the same reason — a
recorded `meta.name` is descriptive, and trusting it let a copied fixture
collapse onto another's registry key and ledger.

Prove the recording is fresh. The spec removes the selected fixture
before driving, so a run whose hook never installed the recorder fails
instead of greening against a stale artifact whose answers still match
these deterministic prompts.

* fix(e2e): rewind a replay fixture at the conversation boundary

Consecutive invocations can share a prompt — a tool call produces exactly
that — so an attempt stopping mid-turn left the cursor on an invocation
whose text still equalled the opening prompt. Matching the cursor first
meant a retry's fresh conversation resumed after the tool call instead of
rewinding, consuming the post-tool invocation and silently replaying a
different script than was recorded.

A conversation boundary now outranks an in-progress cursor: a fresh
conversation whose prompt opens the fixture rewinds even when the cursor
would have matched. Continuing still outranks restarting within a
conversation, so a turn that calls a tool advances to its post-tool
invocation rather than rewinding on its own repeated prompt.

* fix(e2e): refuse cross-conversation binding and prove content streaming

A fresh conversation could steal a partly consumed fixture's later turn.
Only a conversation opening with the fixture's first prompt was treated
as a boundary, so after `[A, B]` had consumed `A`, an unrelated new
conversation whose first message was `B` matched the cursor, received the
recorded second-turn response, and advanced the shared cursor without
ever having driven `A`. A conversation start may now only rewind a partly
consumed fixture, never continue it; continuation within a conversation
is unaffected.

The incrementality assertion counted empty frames. Providers emit empty
initialization and usage-metadata chunks around the content deltas, so a
total chunk count above one was satisfied by a single delta: the previous
fixture's closing turn had four chunks and one content-bearing delta
carrying the whole answer, and both modes stayed green without proving
incremental assistant-content streaming at all. Fixtures now track
content-bearing chunks separately, the closing prompt asks for prose
rather than a number, and both modes require several content deltas on
that turn. Re-recorded: the closing turn now carries 28 content deltas.

* fix(e2e): scope record mode to the fixture spec

`E2E_MODEL_FIXTURES=record` replaces the fake-model hook globally, so an
unfiltered entry point such as `npm run e2e:mock` sent every spec under
specs/mock to the paid real-provider endpoint, while each fresh
conversation truncated and rewrote the one selected fixture — leaving an
artifact from whichever scenario happened to run last.

Record mode now matches only the fixture spec: an unfiltered recording
run lists one test instead of 203. Replay mode is untouched and still
collects the full suite.

* 🪪 fix: Bind Replay Fixtures by Conversation, Not Prompt Text

Prompt text was standing in for conversation identity, and three review
rounds found the same class of defect underneath it: a tool call repeats
a prompt across invocations, a retry repeats it across attempts, and a
resumed run has neither prompt nor history because `createRun` is rebuilt
with no messages while state comes from the checkpoint. Each fix in that
space created the next gap.

Thread the identity instead. `createRun` accepts a `conversationId` and
passes it to the run hook, which the agents controller supplies at both
call sites — the same value it already uses as the checkpointer's
`thread_id`. The field is optional and the hook is env-gated, so nothing
changes when the harness is not in use.

Binding then collapses to ownership. A fixture is owned by the
conversation that claimed it, and its cursor is authoritative wherever it
stands: an extra turn reaches the over-consumption guard rather than
falling through to the scripted fake model, and a resumed run keeps
replaying with no prompt to match. A different conversation may claim the
fixture only by opening it, which rewinds — what a Playwright retry looks
like. Everything else is refused, so an unrelated conversation can no
longer continue someone else's partly consumed script by repeating a
later prompt. The prompt is still re-checked on every real turn; only a
resume, which structurally carries no human message, is exempt. The
previous text-and-history rules remain as a fallback when identity is
absent.

The recorder keys the same way: a new attempt is a new conversation, so a
resume no longer truncates the fixture mid-turn and discards its
tool-call invocation.

Record summarization too. The summary provider runs on its own model with
its own callback list, so a scenario crossing the context-pruning
threshold recorded the agent's invocations but not the summariser's,
leaving a fixture that could not reproduce the pruned context.

* 🧾 fix: Harden Record Mode and Make the Rendered-Text Assertion Honest

CI caught what local runs had not: the committed fixture was never
replayed locally, because the record run overwrote it after the replay
check rather than before. Re-recording and replaying in that order is
what surfaced the rest of this.

The DOM assertion compared raw recorded text against rendered markdown.
The previous answer opened with `52.`, which Markdown renders as an
ordered-list marker, so those characters never appear in the DOM and the
match failed on all three CI attempts while replay itself was correct.
The closing prompt now asks for prose beginning with a word, a leading
enumerator is stripped before matching, and only a prose prefix is
compared.

Derived configs discarded the record-mode restriction. `config.redis.ts`
and `config.mermaid.ts` spread this config and then replace `testMatch`,
so `e2e:mock:redis` in record mode would still send its specs to the paid
provider. A restriction expressed as an overridable value cannot hold, so
record mode now refuses any config but the mock one.

Superseded recording callbacks could write across a reset. A failed
attempt with a provider call still in flight keeps its handler on the old
graph; after the retry reset, that call would allocate an invocation from
the new counter or append an `error` entry with a cleared mapping.
Handlers now carry the recording generation they were installed for and
ignore everything from an older one, and attachment dedupes against the
current generation so a graph carried across a restart is not left with
an inert handler.

* 🚧 fix: Make Summarization an Explicit Boundary, Not a Half-Feature

Recording summarization invocations without replaying them is worse than
ignoring them. Replay routes the agent model and subagents only, so a
recorded summarization entry takes a slot in the fixture sequence that
replay never consumes, and the next primary call reads the summariser's
chunks — a prompt mismatch or, worse, silently wrong content.

The attachment was also aimed at the wrong shape: the SDK reads
`summarizationConfig.parameters`, not `.parameters` nested under
`.config`, so the previous attempt would have attached to nothing in a
real run. Its test passed only because the test built the shape the code
expected rather than the shape the SDK provides.

Rather than ship a half-routed feature, recording now fails the moment
summarization runs, naming the reason. Both shapes are guarded so the
guard cannot miss the way the recorder did. Summarization fixtures need
replay routing for that model before they can be supported.

The derived-config guard added alongside it was itself broken: workers do
not carry `--config`, and the argument lookup fell through to
`process.argv[0]`, so every recording run aborted claiming the node
binary was an unexpected config. The flag is now located explicitly and
absence is treated as "not the process that parsed the CLI".

* 🔒 fix: Close the -c Config Alias and Pin the Recorder's Fixture Name

Playwright documents `-c` as an alias for `--config`, so record mode
launched as `playwright test -c e2e/playwright.config.redis.ts` slipped
past a guard that recognised only the long spelling. Both spellings and
both `=` and space forms are now parsed.

Accepting arbitrary fixture names also worked against the ambiguity
check. This spec drives one fixed prompt pair, so recording under another
name left two fixtures sharing those prompts; replay then refused to bind
either and the keyless lane stopped working — a successful documented
recording run could disable the suite it exists to serve. The spec now
records only the fixture it owns and says so when asked for another.

* 🔧 test: Record a Real Tool-Call Turn and Replay It Through the Tool Node

The fixture format carried `tool_call_chunks` and the binding advanced
through a turn's invocations, but nothing had recorded a real
tool-calling conversation end to end — the path was covered only by
hand-written synthetic fixtures, and it is the first one a new scenario
would exercise.

This records one: the provider calls the `remember_fact` MCP tool, the
tool runs, and the model is invoked a second time with its result. That
is the shape a single prompt cannot express — one user turn spanning
several model invocations, all sharing one prompt — so it is what proves
the turn-vs-invocation distinction the binding rules were built around.

Replay drives the real tool node rather than replaying its output, so the
tool executes again and the assertion checks its live result.

Two fixtures now coexist, which the record path had to grow for: the
config keeps an allowlist so an unknown name is still refused, record
mode collects every replay spec, and each spec records only the fixture
it owns and stands down for the others.

MCP tools reach the model under a server-qualified name
(`remember_fact_mcp_e2e-memory`); that qualification has changed before,
so the assertions match the base name as a prefix rather than pinning the
suffix.

Verified: record 1 passed (15.6s, real API) then replay 1 passed (13.6s)
against that fixture, ledger drained 2/2 invocations and 35/35 chunks;
both replay specs together 2 passed; app-load, completion, chat and
mcp-ephemeral 12 passed.
2026-08-25 18:29:55 -04:00
Danny Avila
290b8664d9
🧾 feat: Track Authoritative Agent Event Outcomes (#15213)
* feat: track authoritative agent event outcomes

* fix: isolate agent event outcome types

* fix: declare agent event handler result

* fix: simplify agent event status selection

* fix: preserve authoritative event outcomes

* fix: preserve terminal event evidence

* test: use completed run-step envelope

* test: scope deferred HITL question locator

* fix: settle every agent event terminal path

* style: sort terminal host action imports

* fix: fence agent event terminal evidence

* fix: recover agent event terminal settlement

* fix: scope terminal retry hints by generation

* fix: settle terminal host actions exactly
2026-08-25 18:20:57 -04:00
James Todaro
018775de07
🧾 fix: Honor Disabled Transactions on the Assistants Usage Path (#15100)
* 🧾 fix: Honor Disabled Transactions on the Assistants Usage Path

Thread the resolved transactions config through `recordUsage` from each of
its callers, so `transactions.enabled: false` is honored on the assistants
token spend path.

* 🧾 fix: Thread the transactions config through the vision-request caller

Address review: `ToolService.processVisionRequest` also records usage without
the resolved config, and `recordUsage`'s documented return type did not match
the function.

* 🧾 fix: Set the resolved transactions config after the usage spread

- provider usage could carry a `transactions` key that overwrote the trusted value
- matches the ordering the other `recordUsage` callers already use
2026-08-25 08:30:12 -04:00
Danny Avila
862ebf3235
🪢 fix: Persist Failed Agent Turns Before Error Publication (#14118) 2026-08-25 06:50:46 -04:00
Danny Avila
ac2aef00f6
🫗 fix: Drain Quoted Excerpts Into Mid-Run Steering (#15175)
* 🧭 fix: Carry Quoted Excerpts Through Mid-Run Steering

"Add to chat" quote chips were dropped by every during-run steer path: the
steer POST had no quotes concept, so a composer-origin steer left the chip
staged (gluing onto the NEXT send) and a queued item steered into the live
run lost its quotes silently.

Quotes now ride the steer protocol end to end:
- POST + admission: `quotes` on the steer body, normalized like the chat
  route's (getReferencedQuotes caps), part of the idempotency fingerprint
  only when present so pre-existing receipts still replay.
- Injection: merged into the model-bound turn as Markdown blockquotes at
  both boundaries (text-only and media paths), mirroring prependQuotes.
- Persistence + replay: the STEER content part stores `quotes` separately
  from the typed text; stampSteerPartMedia re-merges them per turn (even
  with resendFiles off) via the SDK's transient media stamp, with the quote
  block folded into the token budget.
- UI: composer steers/interrupt-steers drain the chips (skill picks stay
  staged — they configure a NEW turn's run); SteerPart and the in-flight
  bubble render the same MessageQuotes reference blocks as user bubbles;
  queued/failed rows show a quote count; reconnect reseeds fall back to the
  server item's quotes when no local chip survives.
- buildMessages keeps its zero-await path to the parallel context kickoff
  via a synchronous stamp-target probe.

* 🧭 fix: Keep Quotes in the Client-Safe Steer Projection

toPendingSteer is the projection behind resume-state pendingSteers, abort
responses, and terminal leftover claims — dropping quotes there would lose
them on exactly the recovery paths the reconnect reseed's server fallback
relies on.

* 🧪 test: In-Flight Steer Bubble Renders Carried Quotes

* 🔁 fix: Re-Stage Quotes When a Pre-Quotes Replica Accepts the Steer

Codex flagged the rolling-deploy window: an old replica 202s a quoted steer
while dropping the excerpts, so the client cleared the chips for context the
model never received.

The 202 (fresh and receipt replay) now echoes quotesAccepted from the
DURABLE item; a missing echo on a quote-bearing composer-origin steer
re-stages the excerpts as composer chips — the pre-steer behavior, so they
ride the next send instead of vanishing — and strips them from the surviving
chip so a later terminal conversion cannot duplicate them. Queued-origin
steers keep quotes on the item, whose restore paths already return it
intact. The residual cross-version lost-ACK retry stays fail-closed as a
409 idempotency conflict (failed chip with retry controls).

* 🔁 fix: Close the Remaining Cross-Version Quote-Loss Windows

Codex round 2:
- Send now of a quoted queued item against a pre-quotes replica now
  re-stages the excerpts too (the row is consumed and the words inject
  bare, so the composer is their only remaining home); the strip clears the
  chip's captured origin copy so reclaims and terminal conversions cannot
  duplicate them.
- A quoted retry whose lost first ACK was accepted by a pre-quotes replica
  now REPLAYS that legacy receipt instead of 409ing: the stored fingerprint
  matching the quote-less hash of the same words proves the cross-version
  case, and the replayed 202's missing echo drives the re-stage. Different
  quotes against a quote-bearing receipt still conflict.
- TSteerAppliedEvent.part gains the quotes field (typed SSE consumers).

* 🧪 test: Drop the Stale Narrow SteerDrainOutput Alias

The spec's local intersection re-declared injectedMessages with
content: string, predating the SDK pin that declares the field natively
(content: string | MessageContentComplex[]). Under CI's clean install the
hook's BaseHookOutput is no longer assignable to that narrower alias; the
plain PostToolBatchHookOutput is the correct type for every drain/boundary
assertion. Verified against the published 3.6.16 dist and the local one.

* 🔁 fix: Honor the Generation Owner's Quote Capability End to End

Codex round 4:
- steerQuotesCapable rides job metadata (createJob + HITL resume rewrite),
  mirroring preemptCapable's owner-recorded pattern: an upgraded admission
  replica no longer stores quotes — or claims them accepted — for a
  generation whose older owning drain would silently drop them at
  injection. The missing echo drives the client re-stage, and a later
  capable handover cannot double-deliver restored context.
- Applied events reconcile dropped quotes: when a quote-less applied part
  settles a quote-bearing chip (the lost-202 ordering the ACK-echo path
  cannot see), resolveSteerChip and both reconnect settle paths re-stage
  the chip's excerpts before removing their only copy. mergeRestagedQuotes
  dedupe keeps every trigger idempotent for the same excerpts.

* 🔁 fix: Re-Read Quote Capability at the Last Moment and Cap Restaged Chips

Codex round 5:
- A HITL resume rewrites steerQuotesCapable without changing the
  generation's createdAt, so the enqueue fence cannot see a
  capable-to-legacy handover landing during admission's awaits. Re-read
  the owner's flag immediately before item construction (paid for only by
  quote-bearing requests); the residual between re-read and enqueue commit
  matches preemptCapable's documented race.
- mergeRestagedQuotes now respects the 10-quote contract with the staged
  chips winning: a restored tail that cannot ride the next send is dropped
  explicitly instead of rendering as a chip the submission would silently
  discard. MAX_QUOTE_COUNT moves to utils/steer as the single client
  source; QuoteButton imports it.

* 🔁 fix: Steer Quote Coverage for Preflights, Memory, and Single-Scan Stamping

Codex round 6:
- Stored-message policy inspection now extracts steer-part quotes as quote
  fragments (path /content/N/quotes/M), so conversation import and shared
  link preflights inspect the newly persisted field exactly like top-level
  message.quotes.
- The memory copy gets its own quote-merge stamp (text only, resendFiles
  false): formatAgentMessages ignores part.quotes, so without it a steer
  whose substance lives in its excerpt reached the chat model but never
  memory extraction.
- collectSteerStampTargets replaces the boolean probe: buildMessages
  collects once and hands the targets to stampSteerPartMedia, keeping the
  zero-await fast path without scanning the history twice.

* 🔁 fix: Redis Quote Plumbing, Conversion-Race Guard, and Quote-Bound Recovery Proof

Codex round 7:
- RedisJobStore.deserializeJob now restores steerQuotesCapable (the explicit
  mapper otherwise dropped it on every read, leaving quote steering inert in
  Redis deployments), with the round-trip spec extended.
- Both Lua parked-steer projections (terminal close + generation
  replacement) forward item.quotes, matching toPendingSteer — a lost final
  no longer strips excerpts from durable recovery in Redis mode.
- The no-echo restage reads the SURVIVING chip (reclaimRejectedChipQuotes):
  a terminal conversion that beat the delayed 202 already moved the quotes
  onto the queued follow-up, and re-staging them again double-delivered.
  Regression-tested with the conversion-before-ACK ordering.
- RecoveredSteerPayload binds normalized, order-significant quotes (builder,
  validator, TS matcher, and the Lua decode+matcher): a stale client
  presenting the same recoverySteerId with altered or missing quotes cannot
  consume the parked source. Quote-less sources keep matching quote-less
  recoveries.

* 🔁 fix: Execution-Bound Quote Capability with an Atomic Enqueue Predicate

Codex round 8:
- steerQuotesCapable becomes a transient assertion translated (at createJob
  and in ApprovalLifecycle.resolve) into steerQuotesExecutionId, valid only
  while it equals the LIVE providerExecutionId. A legacy replica winning a
  HITL resume rewrites the execution id without knowing the marker, so its
  stale assertion self-invalidates — a bare boolean could not be cleared by
  code that predates it.
- The fenced enqueue evaluates that equality atomically (all three Redis
  scripts decode-and-strip like the existing preemptCapable normalization;
  both InMemory sites mirror it) and returns the persisted item, so the
  quotesAccepted echo reflects exactly what was stored even when a handover
  lands between admission's read and the commit. The last-moment re-read is
  gone — the transaction is the authority.
- Tests: capable-resume re-binding, legacy-resume omit-not-clear
  invalidation, the admission-vs-handover race (capability read true, then
  execution rewritten before enqueue), and the Redis round-trip of the
  marker.

* 🔁 fix: Full Redis Parking Coverage and Loss-Moment Quote Restaging

Codex round 9:
- The two remaining Redis parking projections (terminal status CAS and
  stale-running cleanup) forward item.quotes — every field-picked steer
  projection now carries them (audited: 2 Lua 'projected' + 2 Lua
  'clientItem' + toPendingSteer).
- The ordinary no-echo ACK no longer re-stages: the steer has not injected
  yet, so the quotes stay carried on the pending chip. A quote-less applied
  event re-stages them at the actual loss; a terminal leftover conversion
  carries them onto the recovered row, whose normal send delivers quotes on
  any server — re-staging at the ACK let that leftover auto-send bare text
  while the excerpts glued onto an unrelated draft. Only the settled
  receipt replay (already injected, no future event) reclaims immediately.

* 🔁 fix: Legacy-Replayable Receipts with Separate Quote Identity

Codex round 10: an upgraded-first receipt stored a quote-inclusive
fingerprint no pre-quotes replica could recompute, so a lost-ACK retry
routed through one 409'd already-accepted words with duplicate-send
controls.

The durable fingerprint reverts to the quote-independent 3-field hash —
the one shape EVERY deployed version computes, replayable across a rolling
deploy in both directions — and quote identity moves beside it as
requestedQuotesFingerprint (of the REQUESTED quotes, pre any capability
strip, so an incapable-owner acceptance still replays its own retries).
Absent records (legacy-written or quote-less) accept any same-words retry,
preserving the round-5 rule; present records must match exactly, keeping
different-quotes clientSteerId reuse a 409 on quote-aware readers. Under
the keep-on-chip client contract a legacy replay's missing echo is
harmless — the excerpts stay carried on the pending chip.

* 🧪 chore: Re-Trigger CI After Dropped Workflow Events
2026-08-24 22:29:13 -04:00