Add waitForState endpoint for blocking state transitions#167
Merged
Conversation
Adds GET /instances/{id}/wait that blocks until an instance reaches
a target state, the timeout expires, or a terminal/error state is
detected. This avoids client-side polling when waiting for state
changes like waiting for an instance to become Running.
- Polls internally every 1s using existing GetInstance state derivation
- Default timeout 60s, max 5m, configurable via query parameter
- Returns immediately on Stopped (terminal) or Unknown (error) states
- Handles client disconnects via context cancellation
- Returns current state + timed_out flag in response
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the ✅ hypeman-go studio · code
|
Tests cover: already-in-target-state, terminal state early return, invalid/negative timeout validation, missing resolved instance (500), instance deleted during wait (404), context cancellation, timeout capping, and default timeout behavior. All tests use fake middleware contexts to avoid Docker Hub / KVM dependencies while still exercising the handler logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GET /instances/{id}/wait endpoint was missing from the scope
mapping, causing TestAllRoutesHaveScopes to fail in CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracts the poll loop, terminal/error state detection, and timeout handling from the HTTP handler into a reusable WaitForState function. The handler now only parses request params and maps the result to HTTP responses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Standby as terminal state for early return (alongside Stopped) - Add initial terminal/error state check before entering poll loop - Include StateError in context cancellation return path - Extract isTerminalForWait helper for consistent terminal detection - Add StandbyTerminalEarlyReturn test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the timeout timer fires, after fetching the latest state, check whether it actually matches the target before reporting TimedOut: true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…back Add a subscription mechanism (subscribe.go) that lets WaitForState receive state changes via channels instead of relying solely on polling. Manager methods (create, start, stop, standby, restore, fork, delete) now broadcast state changes to subscribers via notifyStateChange. WaitForState uses a 3-way select: subscription channel, 5s polling fallback (with warning log if it detects a missed event), and context cancellation/timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove incorrect notifyStateChange(id, forked) in ForkInstance that passed the fork result as the source instance state. Add trySend helper with deferred recover to prevent panic if CloseAll closes a channel between Notify's copy and send. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Derived state transitions (e.g. Initializing→Running via boot markers) are not broadcast via subscription since they happen inside GetInstance. The polling fallback detecting these is expected, not a missed event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Subscribe() call before initial state checks in WaitForState to close the TOCTOU race between checking state and subscribing. Remove notifyStateChange calls from CreateInstance, ForkInstance, and ForkSnapshot — newly created instance IDs have no subscribers, so these notifications were no-ops. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow the existing convention where instance method names match the URL suffix (standby, restore, fork, start, stop, logs, stat, stats). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Paused requires explicit user action (resume/shutdown/standby) to progress, so WaitForState should return early instead of polling until timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TestWaitForState_Integration: a real integration test that creates an nginx instance and uses WaitForState (subscription + polling fallback) to wait for it to reach Running, proving the mechanism works end-to-end. Also add Paused to isTerminalForWait since it requires explicit user action to progress, with a unit test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shutdown requires explicit user action (StopInstance or restart) to progress, so WaitForState should return early instead of polling until timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Map the GET /instances/{id}/wait endpoint as instances.wait_for_state
and add WaitForStateResponse as a model in the instances resource.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
sjmiller609
reviewed
Mar 25, 2026
sjmiller609
approved these changes
Mar 25, 2026
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.

Summary
Adds
GET /instances/{id}/wait?state=Running&timeout=30sendpoint that blocks until an instance reaches the target state, avoiding client-side polling.Behavior:
GetInstancestate derivation logic, polling internally every 1stimeoutquery parameterStoppedwhen not the target) or error states (Unknown)Response shape:
{ "state": "Running", "state_error": null, "timed_out": false }Changes:
openapi.yaml— newWaitForStateResponseschema +/instances/{id}/waitendpointlib/oapi/oapi.go— regenerated from speccmd/api/api/instances.go— handler implementationTest plan
timed_out: truewith current state)Note
Medium Risk
Adds a new blocking API endpoint and introduces state-change subscriptions into the
instances.Managerinterface, which affects all manager implementations and state transition paths. Risk is mainly around missed notifications, channel lifecycle on delete, and potential regressions in lifecycle methods now emitting events.Overview
Adds a new
GET /instances/{id}/waitendpoint that blocks until an instance reaches a requested state (with validated target states, configurable timeout capped at 5m, andtimed_out/state_errorin the response).Implements
instances.WaitForState, backed by a new per-instance subscription mechanism (Manager.Subscribe,subscribe.go) with a 5s polling fallback, and wires lifecycle operations (StartInstance,StopInstance,StandbyInstance,Restore*) to broadcast state changes and close subscribers on delete.Updates OpenAPI/client codegen (
openapi.yaml,lib/oapi/oapi.go), scopes mapping, Stainless config, and adds unit/integration tests covering immediate return, invalid params, deletion during wait, context cancellation, and subscription behavior.Written by Cursor Bugbot for commit b0d3836. This will update automatically on new commits. Configure here.