Repository navigation
fix(llc): surface the persisted skip reason in the heartbeat monitor (#12681) - #14761
Merged
Merged
Conversation
…12681) A heartbeat that never dispatched records SKIPPED and stores the reason in `error`, and `GET /api/llc/agents/{id}/runs` has always returned that field. The monitor rendered the status and dropped the reason, so every skip looked the same: a missing adapter binary, an active cooldown and an agent with no work were indistinguishable without reading the database. The field was one line from visible. `AgentRun` now carries `error`, the run row renders it, and the label distinguishes a skip from a failure so a SKIPPED run is not read as a crash. SKIPPED also had no status colour at all — the one status whose whole point is "this did not run" rendered like every other row. Labels are added to all 11 locales. The tests mount the component rather than asserting on the response shape: the field being present in the payload proves nothing while the template never reads it, which is how this stayed invisible.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…12681) `رد شد` is the phrasing this codebase already uses for *denied* — see `common.permissionDenied.title` ("دسترسی رد شد" = access denied) — so a Persian operator would read a skipped heartbeat as a rejected one. That is the exact confusion between "did not run" and "was refused" this change exists to remove, so the label defeated its own purpose in one of the eleven locales. Also unmounts the component between tests. It starts a 15s auto-refresh interval on mount that only `onUnmounted` clears, and this is the first test file the component has had, so nothing was clearing it.
5 tasks
This was referenced Aug 22, 2026
mrveiss
added a commit
that referenced
this pull request
Aug 22, 2026
…12681) (#14790) The backend image installs claude and gh from signed apt repos and proves both resolve at build time. Nothing did the equivalent on an ansible-provisioned host: searching the ansible tree for claude returned zero files. resolve_cli_binary therefore returned None for every claude_code / copilot_local agent hired on a systemd deployment — the hire succeeded, dispatch could not, and the failure surfaced as a skipped run rather than a provisioning error. Same signed repos as the image, fetched with get_url rather than piped into a shell, plus a resolve check on the host so a broken PATH fails provisioning instead of coming up hosting agents that cannot run. The tasks sit before the #12139 restart-guarded window: they reach two third-party repos, and inside that block a transient outage would abort the whole deploy after the service was stopped. gh is included because copilot_local_adapter resolves it the same way; installing only claude would leave that adapter equally unrunnable. The guard asserts the provisioning rather than the resolver, and every assertion was mutation-tested — removing gh from the package list, deleting the verify task and dropping claude from its loop each turn exactly one assertion red. Refs #12681 — item 1 of three. Item 3 shipped as #14761; item 2 is #14797.
mrveiss
added a commit
that referenced
this pull request
Aug 22, 2026
…4797) Hiring already rejected an unregistered adapter_type, and the comment on that check names the failure it prevents: an agent whose every heartbeat is skipped, looking degraded forever. A registered type whose CLI is missing produced exactly that one level down. Nothing new was needed. is_cli_available() and cli_not_found_message() already existed on the subprocess base, and GET /adapters already reported availability from them; the hire path simply never asked. Review caught the gate reproducing the defect it was written to close: codex_subscription is registered, has no probe, and its invoke raises NotImplementedError, so treating a missing probe as in-process let it be hired while the UI greyed it out. Both surfaces now decide from the same hasattr. Of the five registered adapters, four inherit the subprocess base and codex_subscription is the only one without the probe, so a missing probe means unimplemented, never in-process. A crashing probe fails closed, matching GET /adapters. Refs #12681 — item 2 of three; items 1 and 3 landed as #14790 and #14761. The template import path creates agents without these checks: #14800.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #12681. Deliberately not
Closes— this is one of three items the issuecovers; see "What this does not fix".
Thinking Path
#12681 reports that setup-wizard provisioning fails invisibly. Tracing where the
information is actually lost changed the fix.
A heartbeat that never dispatched raises
HeartbeatDispatchSkipped, and thescheduler already handles it deliberately — it records
SKIPPEDand storesexc.reason:_run_to_dictalready returns that field, so the reason survives all the way tothe client. It was never displayed: the component's
AgentRuninterface did notdeclare
error, and the run row rendered only status, date and duration.So this is not a missing-plumbing problem. The reason is persisted, served, and
then dropped one line short of the screen. Every skip therefore looked identical
— a missing adapter binary, an active cooldown, and an agent with no work were
indistinguishable without querying the database directly, which is precisely the
"fails invisibly" symptom the issue describes.
I also found
SKIPPEDhas no status colour:.status-skippedwas never defined,while
queued,running,failed,timed_outandunknownall are. The onestatus whose entire meaning is "this did not run" rendered as unstyled text.
What Changed
AgentRundeclareserror?: string | null.so a
SKIPPEDrun is not misread as a crash..status-skippedstyling added, matching the existing status colour scheme.skipReasonLabel/errorLabeladded to all 11 locales.Verification
The tests mount
HeartbeatMonitorand drive it through the real path(
fetchAgents→ click row →openHistory→ render), rather than asserting onthe response shape. That distinction is the point: the field was already present
in the payload, so any test written against the API would have passed for the
entire time the defect was live.
.run-reasonpresent, contains the reason textand the skip label
error: null→ no.run-reasonelementThe third is the control. Without it the first two would still pass if the row
were rendered unconditionally, which would put an empty label on every run.
Locale edits were made structurally and verified to be exactly two added lines
per file (
11 files changed, 22 insertions(+)), with each file confirmed toround-trip byte-for-byte beforehand so no reformatting is smuggled in.
Not verified locally: the frontend suite is not run here, so the component tests
are verified by CI, not by me.
What this does not fix
#12681 covers three independent items. This is item 3 only.
Item 1 — nothing installs the
claudeCLI onto the service PATH — and item 2 —llc/api/agent_hires.pyvalidates that an adapter type is registered but neverthat it is available, so a hire can succeed for an adapter that cannot run —
are both untouched here.
Those two are sequenced: item 2 must not land before item 1. Today an
operator can hire an agent and hand-install the CLI afterwards. Rejecting the
hire first, while nothing installs the binary, would make provisioning strictly
worse rather than better.
This item is independent of that ordering, which is why it goes first.
Model Used
Claude Opus 5