Repository navigation
feat(api,admin): agent events endpoint and failure notifications (CLEAN-139) - #134
Merged
maksymhryzodub-prog merged 7 commits intoOct 12, 2026
Merged
Conversation
…AN-139) The team must find out when an agent goes down. Ranch already detected most failures and told nobody; an outside sender would be a second witness and had nowhere to post. api - POST /agent-events, authorised by an API key with the new events:write scope and nothing else. An event is stored and may notify; it never writes an agent's status. - AgentGateway.updateStatus announces a real transition exactly once, at any replica count (conditional update), through a new AgentStatusChanges port. Transitions to failed / unreachable become Ranch-witness events. - One open incident per agent, enforced by a unique column. It closes when Ranch has seen the agent running for ten quiet minutes; silently when a person stops or deletes the agent. Outside events for a stopped or restarting agent are recorded and notify nobody. - Slack through an incoming webhook, behind an outbox with retries. The address has its own table and routes and is never returned. - Read routes for the console, six operator tools, 90 days of retention. - One additive migration, four tables. admin - Events page, an Events section on the agent page, Settings → Notifications with a test button and the sender's guide, events:write in the key dialog. docs - docs/operations/agent-events.md (the sender's guide), README section, specs/020-agent-event-notifications. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…keep internal errors off the wire (CLEAN-139) Two findings from the security review of the first commit. The limit of 60 events a minute per key was a database count followed by an insert. A burst of parallel requests could all read "none so far" and all pass. A per-key sliding window in the process now takes the slot synchronously, before anything is awaited; the database count stays for what one process cannot see — the other replicas. 200 parallel posts against a running API: 60 accepted, 140 answered 429. POST /agent-events answered an unexpected internal error with the error's own text (the global interceptor's behaviour), which can name tables and columns to a caller that holds only a sender key. It now answers a plain "try again" and logs the detail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Asked for after the first real delivery to Slack. The message that an incident opened now starts with a channel mention, so it reaches people who are not looking; the message that the agent is back mentions nobody. The mention is Ranch's own and sits outside everything a sender supplied — a sender's `<!channel>` is still escaped to plain characters (FR-037). Also records in tasks.md that delivery to Slack was verified against a real channel: test message, opening and closing messages from an outside event and from Ranch's own watch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AN-139) CI's api#lint failed on seven `unbound-method` errors in agentEvent.controller.spec.ts: route handlers were read off the controller's prototype as `Controller.prototype.post` to inspect their guard metadata. They are now taken through the property descriptor by a small `handlerOf` helper — a value read for its metadata, never a method lifted off its object. Also runs Prettier over the files this branch added; CI's `eslint --fix` was reformatting fourteen of them on the fly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… (CLEAN-139) With real data the page read badly: an "open incidents" table stacked on a flat table of every report, so five identical reports of one crash loop and a burst about an unknown agent buried what the page is opened for. It is now two views behind tabs, held in ?view=: - Incidents (default, the tab shows the open count): one row per incident — a state badge (Failed / Unreachable, Recovering once Ranch has seen the agent come up, how it ended once closed), the agent, how many reports, the first cause, since when and for how long, who reported, whether the opening and closing messages were delivered. Open / Closed / All, search by agent. A row opens into its reports. - Event log: every report as it arrived, with a "No incident" filter for the ones attached to nothing. api: GET /agent-events takes incidentId, for a row's timeline. SDK regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ast the limits (CLEAN-139) Asked: how heavy is the console's constant re-requesting, and does paging work once the limits are passed. Measured on a local Postgres. The poll was not the cost; one of its reads was. GET /agent-incidents counted reports and de-duplicated senders through AgentEvent on every call — 5 ms at 149 events, 160 ms at 60 000 in one incident, repeated every 5 s by every open console. api - AgentIncident keeps eventCount and witnesses on the row, updated as each report is attached (the sender's name by a conditional update, so two reports from a new sender add it once). Second migration, with a backfill. The list reads no events: 4 ms at 60 000. - Index on AgentIncident.openedAt for the all-agents list. admin - The 5 s refresh asks only for what the open tab shows: two requests instead of three; a tab is caught up when it is opened. - Open incidents are read whole (the cursor is followed) — "what is down" never stops at a page size. - An incident's reports are paged: "Load older reports", "showing the latest 50 of 60,011". - A refresh that brings a full page sharing nothing with the list restarts the list from that page instead of leaving an unseen hole. - Lists say how much is loaded; search works on loaded rows and says so. Verified against 60 149 events and 261 incidents: every page of each list walked through the API — no duplicate, no gap, in order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in CLEAN-138, CLEAN-144 and CLEAN-149. The admin SDK is regenerated from the merged API rather than auto-merged. .specify/feature.json keeps this branch's feature. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
maksymhryzodub-prog
marked this pull request as ready for review
October 12, 2026 13:45
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.
Ticket: CLEAN-139 · spec, plan and tasks in
specs/020-agent-event-notifications/Draft: the screens still need a walk-through in a browser — see "Not verified".
What it does
The team must find out when an agent goes down. Ranch already detected most failures and told nobody; an outside sender would be a second witness and had nowhere to post.
POST /agent-events— cluster tooling reports an agent failure. Authorised by an API key with one new scope,events:write, that can do nothing else. Body:agentId,status(failed|recovered), optionaldatetime,reason,source,eventId. The sender's guide isdocs/operations/agent-events.md.AgentGateway.updateStatusnow announces a real transition exactly once — whatever the replica count — through a newAgentStatusChangesport; transitions tofailed/unreachableare recorded with Ranch as the witness.<!channel>is escaped. The address lives in its own table and is never returned by any route or tool (GET /settingshands secret values back to the console today, so it is not a setting).events:writein the key dialog.list_agent_events,list_agent_incidents,get_/set_/remove_notification_destination,send_test_notification.One additive migration, four tables. No new dependency.
Verified
cd api && bun run buildcd api && NODE_OPTIONS=--experimental-vm-modules npx jest src/slices/agent/event src/slices/agent/agent src/slices/user/apiKey src/slices/mcp— 34 suites, 428 testscd admin && bun test slices— 171 tests ·cd admin && npx nuxt typecheck·bun run locale:checkprisma migrate diffreports no difference from the schema): every answer of the endpoint (201 / 200 duplicate / 400 / 401 / 403 / 429 withRetry-After), the key refused on every other route tried,suppressed_stoppedandsuppressed_starting, the same failure posted to both processes at once and ten at once (one incident, one opening message), cursor paging, roles on the console routes, non-Slack and in-cluster addresses refused. Ranch's own watch marking pod-less agents failed produced exactly one event per transition with both processes sweeping; a stopped agent's incident closed silently; an incident aged past the quiet period closed with exactly one closing message.<…>and&arrived as plain characters..vuecompiles in the admin dev server.Not verified
fetchonly.Twin consoles
adminslices with a twin that were touched:agent(one section insections.ts, one branch inCanvas.vue),user(theevents:writescope),common(formatSpan, one sidebar icon). Checked inapp/:app/slices/agenthas no section list (its workspace isProvider,Rail,RailItem),app/slices/userhas no API-key screen, andappshows no time span and has its own sidebar. Events are an operator's concern — a customer is not told that their agent went down — soappneeded nothing.app/is untouched.Worth a reviewer's eye
api/src/slices/agent/agent/data/agent.gateway.ts—updateStatusis the most carefully guarded write in the API. It now does a conditionalupdateManyfirst and falls back to the oldupdatewhen the status did not change; what is written is the same. The existing agent-slice specs pass untouched.Open points that are not this PR's to close
k8s/platform/apisays 2 with HPA 2–5,k8s/deploy/30-api.yamlsays 1, and the bridle hub's presence map is per process. This feature is correct at any count; but with more than one replica agent statuses can flap today and each flap would become an incident. Check before a destination is set (see "Before switching notifications on" in the guide).ADMIN_URLon the API deployment — without it messages carry no link.🤖 Generated with Claude Code