Repository navigation
fix(api,admin,k8s): hold the unreachable sweep while more than one copy of the API is alive (CLEAN-140) - #144
Merged
Conversation
maksymhryzodub-prog
force-pushed
the
fix/CLEAN-140-api-replica-guard
branch
from
October 21, 2026 10:19
306aa56 to
3c067ce
Compare
…d thread messages (CLEAN-165) renderOpened and renderClosed already escaped it; renderRoot and every renderReply branch put the raw name into `text`, the field Slack uses for notifications, so a name like "<!channel> Bot" could page from there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…py of the API is alive (CLEAN-140) Two API replicas against one in-memory bridle hub flipped every prod-mazda agent unreachable/running every two minutes for four days, one Slack message per flip. Each API process now registers itself in Postgres with a heartbeat; while more than one is live the drift sweep writes no unreachable, keeps no window and warns once per five minutes. Settings -> Notifications and get_notification_destination show the replica count with a warning. The platform manifests run ranch-api as one replica, no HPA. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
maksymhryzodub-prog
force-pushed
the
fix/CLEAN-140-api-replica-guard
branch
from
October 21, 2026 10:21
3c067ce to
515b415
Compare
maksymhryzodub-prog
changed the base branch from
feat/CLEAN-167-environment-label
to
main
October 21, 2026 10:21
maksymhryzodub-prog
added a commit
that referenced
this pull request
Oct 26, 2026
…174) (#146) Every workflow on main that installs dependencies has been red since the 2026-10-22 push (b54c7e7, #144): error: lockfile had changes, but lockfile is frozen No commit caused it. bun.lock last changed in 40eaf56 (CLEAN-112) and no manifest has touched a dependency line since. What moved is the bun version: every workflow pulls bun through oven-sh/setup-bun with bun-version: latest, and all three Dockerfiles build FROM oven/bun:1 - both floating, and both rolled from 1.4.3 to 1.4.4 between the green run on 2026-10-21 and the red one the next day. On the exact files each install sees, 1.4.3 passes --frozen-lockfile and 1.4.4 fails, so the lock had been drifting for a while and only the older bun tolerated it. Three workflows were affected. ci.yaml runs on every pull request, so any PR opened now is red before its own code is looked at. build-images.yaml fails twice - the locale gate on the runner and build-api in Docker - and i18n, build-app, build-admin and deploy sit behind them, so main has shipped no image since 2026-10-21. publish-cli.yaml installs at the repo root too, so the next version bump would have failed to publish. The drift is hoisted build-toolchain binaries and nothing else: the top-level @esbuild/* platform packages collapse 0.27.7 to 0.25.12 with the matching vite, rollup, lightningcss and nitropack/rollup entries. 78 insertions, 220 deletions, no application dependency, and the repo declares no overrides, resolutions or patchedDependencies that a regenerated lock could silently drop. Regenerated with bun 1.4.4, the version CI and the images now run. The result passes --frozen-lockfile under both 1.4.4 and 1.4.3, in the five-file shape the Dockerfiles copy and in the full checkout ci.yaml installs, so nobody has to upgrade a local bun and all three workflows go green without touching them. The builder stages are pinned to oven/bun:1.4.4 so the next bun release cannot repeat this inside an image. Pinning api/Dockerfile's builder also pins the bun that installs the sibling runtime repo, the third frozen install in that image. The api runtime stage stays on oven/bun:1-slim: it runs no install, and pinning it would freeze its security patches. packageManager said bun@1.2.12, which had not been true for a long time; it now matches the images. setup-bun still says bun-version: latest in all four workflow steps, so CI remains exposed to the next release. Pinning those lives in a follow-up commit - pushing it needs a token with the workflow scope, which the current one lacks. Checked under 1.4.4 and healthy, so left alone: cli/bun.lock, and the CleanSlice/runtime lock that build-api clones into the context. The same lock blob sat on the Mazda fork and is fixed there in connectsolutions/mazda-ai-ranch#46 (CLEAN-173); the regenerated lock here is byte-identical to that one, as are the three Dockerfiles and package.json, so the next catch-up merge has nothing to resolve. Gates - the whole ci.yaml job, run locally: frozen install green on 1.4.4 and 1.4.3 in both shapes; prisma-import + generate; api build + generate:swagger; turbo typecheck app + admin; i18n:check 7 pairs in sync; locale:check 962 files no findings; lint 0 errors; api jest 156 suites, 2165 pass, 0 fail; admin 206 pass; app 147 pass; scripts 94 pass. Plus both nuxt production builds, the step that failed inside the image: admin 23.7 MB, app 23.5 MB.
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.
Jira: CLEAN-140. Two commits: the guard, and a small escaping fix in the Slack renderers the push security review caught (CLEAN-165 code).
What happened
On prod-mazda the Slack threads of incidents #4–#7 hold 3 300 "Failed again / Running again" pairs per agent over four days, two minutes apart. The seconds give it away: every unreachable write sits on one 30-second grid (:29.6 / :59.6) and every running write on another (:23.1 / :53.1) — two API processes, each sweeping against its own in-memory bridle hub. The copy without the agent's socket saw "pod running, hub absent" and after the 60 s grace wrote
unreachable; the other copy wroterunningback. The runtime never disconnected.k8s/platform/apiruns 2 replicas with an HPA 2–5.The API is single-process by design: the hub, chat routing (
sendToAgentanswers "Agent is not connected" on the copy without the socket) and MCP sessions all live in memory. This PR does not make several copies work together; it makes a second copy harmless to the status and loud.What it does
ApiInstancerow per process (id, hostname, heartbeat every 10 s; live for 30 s; pruned after an hour; removed on shutdown).ApiInstanceServicein thesettingslice, exported for anyone who must trust the hub.detectDriftreadsliveCount()first. With more than one copy alive it writes nounreachable, keeps no observation window (it would count minutes the agent spent happily on the other copy and fire the moment this copy is alone), and warns once per five minutes:N copies of the API are alive … Run ranch-api as one replica (CLEAN-140). A vanished pod is still a failure — Kubernetes' word, not the hub's. A rolling update's one-minute overlap is covered by the same rule.apiReplicason the destination view: Settings → Notifications shows a red box with the count and what it means;get_notification_destinationcarries the number.k8s/platform/api/deployment.yaml→replicas: 1with the reason;hpa.yamlremoved. The Mazda fork has the same two files; the patch is ready for that repo.docs/operations/agent-events.md: the "Before switching on" check becomes "One copy of the API".renderRootand everyrenderReplybranch put the raw agent name into the Slacktextfallback — the field notifications are built from — whilerenderOpened/renderClosedescaped it. Now all of them do; a name like<!channel> Botcannot page from there. Spec added.Verified
api:bun run build; jest forsetting+agent/agentstatus +agent/event(252), and the full suite; Prettier +eslint --quieton changed files. New specs: heartbeat on boot and every 10 s, the 30 s live window, hourly prune, failed heartbeat does not stop the timer, removal on shutdown; the sweep with 2 copies alive (no unreachable across three sweeps, one warning, the window restarts from scratch once alone, a vanished pod still fails); the view'sapiReplicas.admin:npx nuxt typecheck,bun test slices(195),bun run locale:check. SDK regenerated (apiReplicasonly).apiReplicas: 2onGET /agent-events/destination, both logged the warning exactly once over 90 s of sweeps, heartbeats refreshed every 10 s. One process killed hard (taskkill /F, a crashed pod): its row froze, and 40 s later the survivor reportedapiReplicas: 1. The startup sweep still marked pod-less agents failed. Not seen live: theunreachablewrite itself needs a Running+Ready pod, which the local run has none of — that branch is the unit-tested one.Twin consoles
adminchanged (the Notifications form's one box).apphas noagent/eventsub-slice: nothing to mirror.For DevOps
The code alone ends the incident spam: with this merged, a second replica no longer produces
unreachable, incidents or Slack messages. Going to one replica (replicas: 1, no HPA, as the manifests now say) is the recommendation, because with two copies the pre-existing problems stay: a chat may land on the copy without the agent’s socket and answer "Agent is not connected", MCP sessions are per process, and unreachable detection is off while the red box shows. That trade-off is the cluster owner’s call; the console will say which state it is in.🤖 Generated with Claude Code