fix(runtime-host): escalate reconnect delay while a Host never stabilizes - #3462
Conversation
jackwener
left a comment
There was a problem hiding this comment.
Automated review of exact head 6e323981ddd625ca6eab0ee3aa70ad3be83ad677 against main@f1f4e71a9540a4da23159052c924fee72202e989.
Two actionable blockers:
-
Existing valid
maxMsconfigurations now throw.requireDelayallowsmaxMsthrough 120000, and before this change a caller could set (for example){ minMs: 100, maxMs: 90000 }. Lines 116–123 defaultunstableMaxMsto 60000 and then require it to be at leastmaxMs, so that previously valid call now rejects during construction even though the caller never opted into the new field. WhenunstableMaxMsis omitted, derive it asMath.max(DEFAULT_UNSTABLE_MAX_MS, this.#maxMs)(or otherwise preserve the old valid range), and add the missing compatibility test. -
This does not fix the issue it closes. #3458 requires that prolonged flapping not exhaust the main-process heap and calls for backoff escalation / circuit-breaking instead of unbounded regeneration. This patch only changes the unbounded loop from roughly once per 5 seconds to once per 60 seconds; if the observed cross-generation retention remains, it delays the same OOM rather than bounding it. The PR itself says the retention root still needs investigation. Either re-scope this as a mitigation (
Refs #3458, keep #3458 open) or add a real bounded/circuit-breaking ownership rule or fix the retained root before usingFixes #3458.
Required conclusions:
- Optimal for the actual problem: no for the OOM claim; it is a useful churn mitigation, not the root fix.
- Production code to delete: none identified.
- Tests to delete/replace: none; add the omitted-
unstableMaxMs/ large-maxMscompatibility case. - Deeper refactor: not required if the PR is honestly narrowed to mitigation; the retention/circuit-break owner still needs a separate completed fix.
- Ready to merge: no on this head; both points above need resolution, required
testhas not run, and the PR also omits the CONTRIBUTING-required AI-use declaration (state explicitly “none” if none was used). - Residual risks: the flapping root and retained heap owner remain unknown; the new cadence still retries forever.
This changes exported reconnect behavior and user-visible recovery timing. Independent human review is required; this automated review is not approval.
6e32398 to
082085c
Compare
…izes A Host that dies immediately after spawn previously forced a regeneration attempt roughly every maxMs (5s by default) forever. Hours of this churn re-ran the full desktop registration stack thousands of times and ended in a main-process OOM (apache#3458). Once the failure streak persists - no connection stayed installed for stableConnectionMs - the jittered delay now keeps doubling past maxMs up to an absolute ceiling (unstableMaxMs, default 60s, must be >= maxMs), and the ladder restarts as soon as a connection stabilizes again. Callers that need the old flat ceiling can set unstableMaxMs equal to maxMs. When omitted, the ceiling derives from max(60s, maxMs) so previously valid configurations with a large maxMs keep working. This is a mitigation for the churn amplifying the OOM; the retained root still needs a heap snapshot under reproduction before apache#3458 closes. Generated-by: Maka
082085c to
5f24d20
Compare
|
Both blockers addressed at 1. maxMs compatibility. When New compatibility test: 2. Scope. Agreed — this bounds the churn that amplifies #3458; it does not bound or fix the retained root. Re-scoped: the PR now says Mitigates #3458, adds a "What this deliberately does not do" section stating plainly that if cross-generation retention persists, this delays the same OOM (hours-scale → days-scale at worst) rather than bounding it, and moves the heap-snapshot/circuit-breaking work to Follow-ups so #3458 stays open for the real fix. The commit message carries the same framing. AI declaration: the tip commit was missing its Local verification on the rebased head: full |
| ): number { | ||
| if (attempt <= 0 || minMs === 0) return 0; | ||
| const exponential = Math.min(maxMs, minMs * 2 ** Math.min(attempt - 1, 30)); | ||
| const ceiling = Math.max(maxMs, unstableMaxMs); |
There was a problem hiding this comment.
[P1] maxMs no longer bounds anything — the two ceilings are folded into one.
const ceiling = Math.max(maxMs, unstableMaxMs);The constructor already validates unstableMaxMs >= maxMs (and derives it as Math.max(60_000, maxMs) when omitted). So Math.max(maxMs, unstableMaxMs) is identically unstableMaxMs — maxMs is dead in the delay computation and survives only as a validation input. There is no streak-gated second phase here; there is one ladder that climbs straight to unstableMaxMs.
This is the exact-head CI failure. apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts:585-587 configures minMs: 100, maxMs: 150 and asserts the ladder:
actual: [ 100, 200 ]
expected: [ 100, 150 ]
The second attempt jumps past the configured maxMs instead of clamping to it.
Why it contradicts the PR description. The PR says escalation happens "while the failure streak persists" and "only engages after the exponential ladder would have saturated anyway." The saturation point is maxMs — so for any caller whose maxMs is close to minMs, the ladder never has an unchanged window at all. The desktop test is exactly that caller.
Reach, stated honestly: I grepped for production callers of reconnectBackoff — runtime-host-desktop-manager.ts:566 is the only one, and it forwards an optional config that no production site sets. So today the observable blast radius is the defaults (100 / 5_000 / 60_000), where the first six rungs are unchanged and the intended 12x churn reduction lands as designed. The regression is in the contract, not yet in shipped behavior — but the contract is public API on RuntimeHostReconnectBackoff, and the test that broke is the one documenting it.
Two ways out, and they are genuinely different:
- Keep
maxMsas the stable-phase ceiling and gate escalation on the streak — clamp tomaxMsuntil the streak passes some threshold, then let it climb tounstableMaxMs. This is what the PR description already claims the code does, and it keeps the existing desktop test green untouched. - Accept that
maxMsis now just the floor of the ceiling — then say so: rename or re-document it, drop the no-opMath.max, and update the desktop test deliberately rather than letting it fail.
I would not pick option 2 silently. The desktop test failing is the codebase telling you the parameter meant something to somebody.
There was a problem hiding this comment.
Confirmed locally before fixing — reproduced actual: [ 100, 200 ] / expected: [ 100, 150 ] on the exact head, and verified the two claims behind it: the \[100, 150\] desktop assertion is pre-existing on main (untouched by this PR), and the folded ceiling is identically unstableMaxMs given the constructor invariant.
Fixed at 6d8a2eb26: the regular ladder clamps at maxMs again; escalation toward unstableMaxMs engages only once the natural doubling leaves the ceiling band (exponential >= 2 * maxMs) — i.e. after the ladder has genuinely saturated. The one assertion that had encoded the fold ([3, 6], added by this PR) now pins the plateau ([3, 5], next delay > 5). Desktop manager suite passes unmodified; runtime-host reconnect suite 13/13; biome + typecheck clean.
|
Independent review of Short version: What I want to be fair about: I grepped the tree for production callers that set On the PR as a whole: the framing is the best part of it. Calling this a mitigation for #3458 and writing out — in the PR body — that the retained root still needs a heap snapshot, and that without it this converts an hours-scale OOM into a days-scale one, is the kind of honesty that makes the change reviewable. The follow-up list is concrete rather than decorative. No objection to the direction at all. The inline comment lays out two ways to resolve it; they lead to different APIs, so it is worth picking deliberately rather than just making the test match the code. Reviewed at 2026-08-23 12:15 UTC. No P0, no P2, no P3 beyond the above. |
…rates Review follow-up on the never-stabilized escalation: Math.max(maxMs, unstableMaxMs) folded the two ceilings into one, so maxMs stopped bounding the delay and tight-ratio callers (desktop manager) saw attempt two jump past the configured ceiling instead of clamping to it. The regular exponential ladder now clamps at maxMs again; escalation toward unstableMaxMs engages only once the natural doubling leaves the maxMs band (exponential >= 2 * maxMs), i.e. after the ladder has actually saturated. The desktop manager contract [100, 150] is restored untouched; the new runtime-host coverage pins the plateau at maxMs before escalation. Generated-by: maka
Astro-Han
left a comment
There was a problem hiding this comment.
Approving 6d8a2eb26618c35dafabdca55a278ccb9cf04ef7. Required test is completed / success bound to that exact SHA. No P0–P2.
Re-review at the current head; the CHANGES_REQUESTED on this PR is bound to the older 6e323981, so every earlier finding was re-derived rather than assumed closed.
The [P1] is closed, and the fix is better than the one that was asked for. The complaint was that Math.max(maxMs, unstableMaxMs) made maxMs inert. The ceiling is now selected rather than merged:
const ceiling = exponential >= 2 * maxMs ? unstableMaxMs : maxMs;So maxMs still governs the ordinary ladder, and the extended ceiling only takes over once natural doubling has already carried the delay past 2 * maxMs — which is the point at which "this Host is not coming back soon" has actually been demonstrated rather than assumed. A configured maxMs keeps its meaning instead of being silently widened.
Also closed: the compatibility case when unstableMaxMs is omitted with a large maxMs; the AI-use trailer; and the description's claim, which now says Mitigates rather than Fixes #3458 and names the retained-root and circuit-breaker work as follow-ups. That last one is worth calling out — a PR that narrows its own claim to what it actually does is doing the reader a favour.
The increment since the earlier reviewed head is 2 files, +10/−4. It does not touch retry authority, state transitions, failure classification or cancellation.
Two [P3]s, neither blocking:
- The exported JSDoc still says omitting
unstableMaxMsdefaults to 60s, but the code usesMath.max(60_000, maxMs)(reconnect-lifecycle.ts:136). With amaxMsabove 60s the documented default is not what a caller gets. - The "stabilized connection restarts the ladder" test does not actually pin the reset — delete the production
failureCount = 0and it still passes. Worth strengthening while the reasoning is fresh.
Recorded rather than escalated: the 12-hour Windows OOM, the retained heap root, and a genuinely bounded/circuit-breaker design are not verified here. The PR does not claim them, which is why this is a note and not a finding.
Disclosure, because it changes what this approval is worth: this is an AI review. Under CONTRIBUTING.md §Review it does not count as the required independent human review. Note also that the existing CHANGES_REQUESTED remains on the PR and gates merge independently of this approval.
Resolved on exact head 6d8a2eb after Kabi incremental re-review: omitted unstableMaxMs now derives max(60s, maxMs) with a 90s compatibility regression test; the PR now says Mitigates #3458, keeps #3458 open, and explicitly scopes retained-root/circuit-breaker work as follow-up; AI declaration/trailers are present. Exact-head test completed/success. Dismissing only this old-head review; current P3 documentation/test-strength notes remain nonblocking.
Mitigates #3458
Problem
When a Runtime Host dies immediately after spawn (transport
read_eofon every attempt), the reconnect lifecycle retried roughly everymaxMs(5s by default) forever. Each retry re-ran the full desktop registration stack — new candidate process, IPC handlers, subscriptions, capability publisher bind. ~12 hours of this churn produced hundreds of candidate generations and ended in an Electron main-process OOM (heap pinned at ~3.6 GB), with the log dominated by repeatedMCP capability alignment failederrors.Change (mitigation, not the OOM root fix)
This change bounds the churn that amplifies the OOM; it does not fix the retained root behind it.
RuntimeHostReconnectBackoffgains an optionalunstableMaxMsceiling (default 60s, validated>= maxMs; when omitted it derives frommax(60s, maxMs)so a previously valid largemaxMskeeps working). While the failure streak persists (no connection stays installed forstableConnectionMs, which already resets the streak), the jittered delay now keeps doubling pastmaxMsup to that ceiling instead of saturating atmaxMs. As soon as a connection stabilizes, the ladder restarts fromminMs.unstableMaxMsequal tomaxMs; callers withminMs: 0are unaffected.unstableMaxMsbut withmaxMs > 60sare preserved — the ceiling derives frommaxMsinstead of rejecting them.What this deliberately does not do
The retention root behind the OOM still needs a heap snapshot under reproduction. If cross-generation retention persists, this patch delays the same OOM rather than bounding it — the escalation reduces regeneration frequency ~12x, which converts an hours-scale OOM into a days-scale one at worst. A real bounded/circuit-breaking ownership rule or the retained-root fix should land as a follow-up before #3458 closes.
Testing
reconnecting-connection.test.ts: delay escalates pastmaxMsand saturates atunstableMaxMs; a stabilized connection restarts the ladder;unstableMaxMs < maxMsis rejected; an omittedunstableMaxMsstays compatible with a largemaxMs(the compatibility case from review).@maka/runtime-hostsuite green on the rebased head.Follow-ups (not in this PR)
read_eoforigin) is observable.AI use
Tool(s) and scope: Maka (AI coding agent) authored the implementation and tests; the diff was human-reviewed before push.
Generated-by: Makatrailers are present on the branch commits (the tip commit was missing its trailer on the previously reviewed head — now added).中文说明
本 PR 是对 #3458 的缓解而非根因修复:持续启动即死的 Host 触发的重建风暴从约每 5 秒一次降级到最多每 60 秒一次(新增
unstableMaxMs上限,默认 60s 且省略时从max(60s, maxMs)派生,保证旧的大maxMs配置兼容)。OOM 的保留根因仍需堆快照定位;若保留仍在,此补丁只是把小时级 OOM 推迟到天级,#3458 的关闭需要后续的边界/熔断修复。已按 review 意见补充 maxMs 兼容回归测试,tip commit 补上Generated-by: Makatrailer。