LibreChat/api/server/controllers
Danny Avila 88747f0ad8
🩺 fix: Render Stopped Run Steps From Explicit Status (#14871)
* 🩺 fix: Render Stopped Run Steps From Explicit Status

Tool calls decided "still running" vs "stopped" with a whole-message
heuristic:

  const cancelled = !isSubmitting && progress < 1 && !hasError;

That inference cannot tell which step actually stopped. An aborted step
keeps spinning while `isSubmitting` is still true, and when submitting
ends, every unfinished part flips to "Cancelled" at once regardless of
which one died.

`@librechat/agents` v3.4.6+ emits `on_run_step_closed`, a terminal
per-step signal carrying `status` and timestamps — including for steps
swept at end-of-run because the caller aborted. The pinned 3.5.1 already
ships it; nothing consumed it.

- `StepEvents.ON_RUN_STEP_CLOSED` plus `RunStepClosedEvent` /
  `RunStepStatus` types mirroring the SDK payload.
- `PartMetadata.runStepStatus` — a dedicated field, since `status` is
  already claimed by activity-label and question-form parts.
- Server handler forwards the event without the visibility gating the
  other step handlers apply: a step whose open reached the client must
  get its close, or the client is left inferring again.
- `useStepHandler` writes the terminal status onto the tool call part.
- Both decision points (`ToolCall`, the shared `useToolCallState`) prefer
  explicit status, keeping the heuristic as fallback for messages saved
  before this and endpoints that do not emit the event.

Threaded through the five cards sharing `useToolCallState`.

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

* 🩹 fix: Address Codex Review On Run Step Closure Rendering

- Persist the terminal status server-side. The handler emitted the
  closure without folding it into `contentParts`, so the status existed
  only on the live React message: a reload or resumable reconnect
  dropped it and fell back to the very heuristic this fixes. Now stamped
  onto the aggregated tool-call part (via `stepMap`, falling back to the
  event's own index) before forwarding.

- Honor terminal status independently of output parsing. Gating on
  `hasError` meant a `failed` step with unparseable output rendered as
  "cancelled", while a `failed`/`cancelled` step whose output did parse
  as an error was not terminal at all and shimmered indefinitely when no
  completion event arrived. A closed step now forces progress complete
  and reports `failed` as an error state on its own authority.

- Pass the status to the second `BashCall` branch, which rendered the
  same updated component without it.

- Reuse `Agents.RunStepClosedStatus` in `PartMetadata` instead of
  redeclaring the union, so a future SDK status cannot diverge between
  the event and the persisted part.

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

*  fix: Replay Closed Status On Redis Resume, Announce Failures

- Apply closure events during Redis reconstruction. The stamp added in
  the previous commit mutates only the originating process's in-memory
  `contentParts`; a resumable reconnect landing on another replica
  rebuilds from `RedisJobStore.getContentParts`, whose allowlist omits
  `on_run_step_closed`. The status was therefore absent from the sync
  snapshot and, being snapshot-covered, never redelivered as pending —
  so multi-replica resume fell back to the whole-message heuristic.
  Handled as a host-authored event alongside `on_steer_applied` and
  `on_activity_label`, since the SDK aggregator has no notion of it.

- Announce terminal failures in the live region. Forcing terminal
  progress for a closed step meant a `failed` tool reached the
  `aria-live` region through `getFinishedText()`, which only special-
  cased cancellation and otherwise announced "completed function" —
  telling screen-reader users the opposite of what the card showed. A
  regression introduced by the previous commit; error states now
  announce failure before any completion string.

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

* 🎯 fix: Resolve Closed Steps By ID, Never By Index

The steer and HITL offset wrappers clone and shift only `ON_RUN_STEP`
and `ON_AGENT_UPDATE`; every other event passes through untouched. A
stored `on_run_step_closed` therefore carries the SDK's unshifted index,
while the part it belongs to was rebuilt at the shifted one. Any run
containing a steer insertion or HITL resume would stamp the status onto
an earlier tool card, or none — leaving the real card on the fallback
heuristic while mislabeling a different one.

- Redis reconstruction builds a step ID -> index map from the replayed
  `on_run_step` payloads (which carry the shifted index) and resolves
  closures against it, mirroring what the live callback does via
  `stepMap`.

- The live handler drops its `?? data.index` fallback for the same
  reason. Skipping is the safe failure: a missing status degrades to the
  old heuristic, whereas a misplaced one actively mislabels the wrong
  card.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-15 10:14:16 -04:00
..
__tests__ 🛰️ fix: Attach Request-Scoped MCP Servers (#14780) 2026-08-12 23:43:02 -04:00
agents 🩺 fix: Render Stopped Run Steps From Explicit Status (#14871) 2026-08-15 10:14:16 -04:00
assistants 📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686) 2026-08-08 13:50:21 -04:00
auth 🔑 feat: Refresh-Capable Google Admin OAuth Sessions (#13832) 2026-08-07 00:10:34 -04:00
AuthController.js feat: Make OpenID Token Reuse Window Configurable (#13546) 2026-06-06 15:15:58 -04:00
AuthController.spec.js feat: Make OpenID Token Reuse Window Configurable (#13546) 2026-06-06 15:15:58 -04:00
Balance.js 🤫 chore: Quiet Repetitive Log Noise from Balance, CloudFront, and Capability Paths (#13461) 2026-06-01 20:40:16 -04:00
Balance.spec.js 🤫 chore: Quiet Repetitive Log Noise from Balance, CloudFront, and Capability Paths (#13461) 2026-06-01 20:40:16 -04:00
EndpointController.js
FavoritesController.js 📌 feat: Add Pin Support for Model Specs (#11219) 2026-04-09 18:37:25 -04:00
FavoritesController.spec.js 📌 feat: Add Pin Support for Model Specs (#11219) 2026-04-09 18:37:25 -04:00
mcp.js 🪶 chore: Aggregate Empty MCP Tool Logs (#14767) 2026-08-12 22:29:49 -04:00
ModelController.js perf: Reduce Agent Chat Startup Latency (#14423) 2026-07-25 07:58:20 -04:00
ModelController.spec.js perf: Reduce Agent Chat Startup Latency (#14423) 2026-07-25 07:58:20 -04:00
PermissionsController.js 🪪 fix: Filter ACL Principal Details (#13524) 2026-06-05 19:06:41 -04:00
PluginController.js 🙋 feat: ask_user_question - agent-initiated questions with durable pause/resume (#14139) 2026-07-08 15:31:05 -04:00
PluginController.spec.js 🙋 feat: ask_user_question - agent-initiated questions with durable pause/resume (#14139) 2026-07-08 15:31:05 -04:00
SkillStatesController.js 🗂️ feat: Add Deployment Skill Directory (#13523) 2026-06-05 10:24:28 -04:00
TokenConfigController.js 🗂️ fix: Scope Token Config Cache (#13770) 2026-06-15 15:25:19 -04:00
tools.js 🧯 fix: Harden Data Retention Semantics (#13049) 2026-05-19 21:58:42 -04:00
TwoFactorController.js 🔑 fix: Require OTP Verification for 2FA Re-Enrollment and Backup Code Regeneration (#12223) 2026-03-14 01:51:31 -04:00
UserController.js 📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686) 2026-08-08 13:50:21 -04:00
UserController.spec.js 🕒 feat: Track Terms Acceptance Timestamp (#10810) 2026-06-24 16:26:42 -04:00