Repository navigation
fix(restart): say a restart Harper accepted is not back yet, never that it failed - #1899
Draft
dawsontoth wants to merge 6 commits into
Draft
dawsontoth wants to merge 6 commits into
dawsontoth wants to merge 6 commits into
Conversation
…at it failed restartInstance sent the restart and then watched for the target with user_info, and both steps rejected the same promise, so a target slow to answer again read as a failed restart everywhere: "Failed to restart instance.", "Only N of M instances restarted", and success: false to the chat agent. Each of those invites a second restart of a target that already went down once. Harper's answer to the restart request is its only verdict: `restart` answers before it tears down (bin/restart.ts schedules the teardown 50ms out), and `restart_service` once its job has started (executeJob). So a probe failure after a 2xx now rejects with RestartUnconfirmedError (cause: the last probe error), shown as "Not back yet ... check its status before you restart it again". - The watch keeps going past the usual recovery time (a second probe schedule, no second settle) and calls onSlowToReturn once in between, so the Restart button and both loops say "still waiting" instead of giving up. Every restart mark's TTL comes from the same constants, and the probes skip the gateway replay, so no mark lapses mid-watch. - The cluster restart and rolling config update stop at a member that never comes back instead of restarting the next one, which could leave no member serving, and report the members they left alone. - createApp keeps success: true (the app exists) and restartHTTPService returns copy without the "Error:" prefix telling the agent not to restart again. Both now replace their progress toast on any failure, which used to keep spinning. Closes #1895 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…members too Review of the previous commit: - A Cancel clicked while the loop waited on a member that never came back replaced the "Not back yet, check before you restart it again" toast with "Cancelled", hiding the one warning that matters. The uncertain outcome now wins; Cancelled still shows when nothing is uncertain. - The uncertain-outcome toast counted the members that refused only implicitly. It now says "N instances failed to restart", from one helper both loops share (describeUncertainRollingOutcome). - onSlowToReturn is queued rather than called inline, so a callback that throws cannot cut the watch short or become its cause. Not adopted: counting a 401/403 probe as the target being back. Through central manager's proxy a 403 is CM refusing the caller before it contacts the instance (notAllowed() in HDBInstance.js), so it says nothing about the target; recorded in DESIGN.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hat it never answered Review round 2: - A probe can fail with an answer (a 401/403, a Harper 500), so "hasn't answered since" was not always true. Every surface now says Studio couldn't confirm the target came back, which is what a failed watch proves. The watch itself still counts only a successful user_info as back: through central manager's proxy a 403 is CM refusing the caller (notAllowed() in HDBInstance.js) before it contacts the instance. - A throwing onSlowToReturn is caught and logged instead of escaping as an uncaught microtask exception; the watch carries on either way. - RollingOutcome is no longer exported; it has no consumer outside its module. Not adopted: dismissing the button's loading toast for an unconfirmed restart. It already is: the catch dismisses it before deciding whether to add its own toast. A test now pins that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ever answered Review round 3: DESIGN.md and restartInstance's doc comment still said the target "never answers", which contradicts the rule that only a successful user_info counts as back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces the RestartUnconfirmedError to handle scenarios where a restart request is successfully accepted by Harper, but Studio cannot confirm that the instance has recovered. This prevents these cases from being incorrectly flagged as failures, which could lead to redundant restarts. The changes update the restart logic, rolling configuration updates, and chat tools to handle this error gracefully with clearer user notifications, and include comprehensive unit and integration tests. There are no review comments to assess, and I have no additional feedback to provide.
dawsontoth
added this pull request to stack #1900
October 11, 2026 21:24
…es' into claude/1895-restart-accepted-not-back Brings in #1897's adjudicated-review fixes: a restart with no verdict keeps its mark until the target answers again, the license warning counts refused licenses, and an unanswered redeploy or import refreshes. Conflicts were import lists and adjacent inserts, kept from both sides: - useRestartInstanceClick.ts: the restartInstance import names both RESTART_MARK_TTL_MS and waitUntilAnsweringAfterRestart; the finally block holds the mark on an unanswered restart and keeps this branch's comment. - useRestartClusterClick.tsx: the catch counts instancesFailed and then waits on an unanswered member, after this branch's RestartUnconfirmedError break. - useRestartInstanceClick.test.tsx: both sides' imports. Also adds RESTART_MARK_TTL_MS to the new useRestartClusterClick.test.tsx mock, which this branch's hook now reads on every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…start-accepted-not-back
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.
⊙ Problem
A restart Harper accepted is reported as failed when the target is slow to answer again:
restartInstancesends the restart, then watches for the target withuser_info, and both steps rejected the same promise. So the Restart button toasts "Failed to restart instance.", the cluster restart and rolling config update count the member as not restarted, and the chat tools returnsuccess: false. Each one invites a second restart of a target that already went down once (#1895).💡 Solution
Harper's answer to the restart request is its only verdict on the restart.
restartreturnsRESTART_RESPONSEon the main thread and schedules the teardown 50ms later "so we give the HTTP thread a chance to return a response" (harperbin/restart.ts:116-188; a non-object result is wrapped as{ message }atserver/serverHelpers/serverUtilities.ts:163-165).restart_serviceruns throughexecuteJoband answers{ message: 'Restarting http', job_id }once its job thread has been launched (serverUtilities.ts:590-617,server/jobs/jobRunner.ts:92-94); a bad service is a 400 fromjobs.ts:130-133. So a 2xx means accepted, and the probe that follows only tells "back" from "not back yet". (Read-only harper checkout atv5.3.1-158-g16e5c663c.)restartInstancenow rejects with the restart request's own error when that request failed (refused, or unanswered, which fix(errors): say a write's outcome is unknown when it got no verdict, not that it failed #1897's "outcome unknown" handles), and wraps only a failure after the POST resolved in the newRestartUnconfirmedError(cause: the last probe failure; its message keeps the probe's text soshouldKeepEventfilters it as before).onSlowToReturnonce between them so callers say "Still restarting" instead of giving up. The probes skip the gateway replay, the restart POST gets an explicit 60s timeout, andRESTART_MARK_TTL_MSis derived from the same constants, so no restart mark lapses mid-watch.describeErrormaps the new error to Not back yet: "Harper accepted the restart, but Studio couldn't confirm it came back. It may still be starting, so check its status before you restart it again."shouldToastOwnFailurekeeps the button from adding "Failed to restart" beside it.success: true(their action was carried out) with copy telling the agent not to restart, or create the app, again.Also fixed: both chat tools now replace their loading toast on any failure; before, "Restarting HTTP service..." / "Creating from template..." kept spinning, since a loading toast never dismisses itself.
⚖️ Alternatives
The planning review (codex) returned
better-alternative-exists; its variant was adopted: keep the producer-owned typed error, but stop the rolling loops on it, keep watching inside one deadline, and give the chat tools a non-contradictory result.{ data, answeredAgain: false }): fails open, since any caller not updated, and the mutation's global handler, would report an unconfirmed restart as "restarted!".bin/restart.ts:135-137, is unbounded) without changing what the edge says.🔧 Changes
src/integrations/api/instance/status/restartUnconfirmed.ts(new): the error class, the Not back yet copy, anddescribeUncertainRollingOutcome, which both loops use for their final toast when an outcome is unknown: confirmed count, members that failed, unanswered writes (fix(errors): say a write's outcome is unknown when it got no verdict, not that it failed #1897's sentence), and the member not back plus the ones left alone.src/integrations/api/instance/status/restartInstance.ts: the typed rejection, the two-schedule watch (a callback that throws is logged and the watch carries on), probes without gateway replay, the explicit request timeout, and the derived mark TTL.src/react-query/queryClient.ts:describeErrorandshouldToastOwnFailurerecognise the new error.src/hooks/useRestartInstanceClick.ts: marks with the shared TTL and turns its toast into "Still restarting" halfway through the watch.src/hooks/useRestartClusterClick.tsx: shared TTL for both marks, a "Waiting On Instance i of n" toast, the stop, a failed count, and an uncertain outcome that outranks Cancel, which would otherwise hide the warning not to restart again.src/hooks/useRollingConfigUpdate.tsx: the same waiting toast, stop and Cancel precedence; it marks nothing, so there is no TTL to change.describeToolError.tsaddstoastToolFailure;restartHTTPService/execute.tsandcreateApp/execute.tsreturnsuccess: truewith no-retry copy for an unconfirmed restart, and replace their progress toast on every failure, including the app-creation one.src/features/instance/operations/index.tsx: the Operations console's restart mark usesRESTART_MARK_TTL_MSinstead of its own 5 minutes, since itswaitUntilAnsweringAfterRestartwatch is now longer; its test's module mock gains the constant.DESIGN.md: a new section recording the verdict rule, the shared deadline, the successful-probe-only rule, the loop stop, and the chat-tool contract.✅ Verification
End-to-end route: integration-style tests through the real
restartInstance, a real instance client (getInstanceClientwith a fake adapter), and the real React Query mutation error handler; the loops are exercised withrestartInstancemocked to reach each outcome. Not run against a live instance.a35e2bed0showed the bug's signature, two toastsError / timeout of 3000ms exceededandError / Failed to restart instance.for a restart Harper answered 200; with the fix it shows oneNot back yettoast and dismisses the loading toast. A second test holds the probe open past the first schedule and checks the mark is held, the button pending, "Still restarting" shown, then success once it answers.restartInstance.test.tsdrives a real instance client: an accepted restart that never comes back rejects withRestartUnconfirmedErrorafter exactly 24 probes on the 10s + 23×15s schedule, a slow target is still watched, refused / gateway / no-answer POSTs keep their own error, and RUM filtering is unchanged for wrapped timeouts and network errors.useRestartClusterClick.unconfirmed.test.tsxanduseRollingConfigUpdate.unconfirmed.test.tsx: both loops stop at a member that never comes back, still carry on past a refusal, and keep the warning when Cancel is clicked; the cluster file also checks that a slow member that answers lets the loop go on, and that refused members are named.restartTools.test.tspins both chat-tool results and toasts;queryClient.test.tspins the shared copy.useRestartClusterClick.test.tsxaddsRESTART_MARK_TTL_MSto itsrestartInstancemock, because the hook now imports it.describeError,shouldToastOwnFailure, both loop stops and their Cancel precedence, the failed count, both chat-tool branches, the tool toast, the button's TTL and toast dismissal).npx vitest run411 files / 3871 passed (also run by the pre-commit hook on every commit),npx tsc -bexit 0,npx oxlint --format stylish .exit 0,npx dprint checkexit 0.06dd4020: merged fix(errors): say a write's outcome is unknown when it got no verdict, not that it failed #1897, which carries fix(api): replay a gateway error only for reads, so a write is never sent twice #1891's gemini-code-assist follow-up (isReadRequestnow reads an operation only from a POST body). No conflicts. Focusedvitest runoversrc/integrations/api,src/lib/restart,src/react-query,src/hooksandsrc/features/instance/operationspassed 50 files / 697 tests, andnpx tsc -bexited 0.Stack: based on #1897 (
claude/1890-outcome-unknown-writes) → #1891 → #1882 →stage. Overlap with the other restart chain, #1861 → #1867 (claude/1853-restart-cluster-load-failure): it reworks the same two loops inuseRestartClusterClick.tsxanduseRollingConfigUpdate.tsx, so whichever lands second resolves those conflicts, keeping #1867's load-failure and Cancel handling alongside this PR's stop rule and outcome accounting. The new loop tests live in*.unconfirmed.test.tsxso they don't add/add-conflict with #1867's newuseRestartClusterClick.test.tsx/useRollingConfigUpdate.test.tsx; #1867's "failed to restart" test throws a plainError, which still reads as failed here.Review coverage gaps: the Cursor legs failed on every round (
git fetchrefused by the 1Password SSH agent), and the Harper domain adjudication leg failed on auth (expired OAuth session), so no round was adjudicated; outside findings were triaged by hand on evidence. Two gemini findings were rejected as factually wrong: the button does dismiss its loading toast (pinned by the first test above), anduseRollingConfigUpdatemarks nothing.Closes #1895
🤖 Generated with Claude Code (Claude Opus 5.5); posted via @dawsontoth.
Related PRs: #1897 overlaps (this PR's base), #1882 overlaps (base of the stack; shares the restart TTL), #1867 overlaps (reworks both restart loops), #1861 overlaps (reworks useRollingConfigUpdate)
Complexity: medium
Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-grok(no-receipt),cursor-composer(no-receipt),domain(auth); declined=cursor-kimi,cursor-muse; rounds=3; full=1 @ 06dd402
Review-Attention: study ~9m (raised: degraded review, open major) @ 06dd402