waitForAgentReady waits for the workspace agent to become reachable and for its startup scripts to finish. It returns status fields suitable for merging into a tool response.
( ctx context.Context, db database.Store, agent database.WorkspaceAgent, agentConnFn AgentConnFunc, )
| 617 | // reachable and for its startup scripts to finish. It returns |
| 618 | // status fields suitable for merging into a tool response. |
| 619 | func waitForAgentReady( |
| 620 | ctx context.Context, |
| 621 | db database.Store, |
| 622 | agent database.WorkspaceAgent, |
| 623 | agentConnFn AgentConnFunc, |
| 624 | ) map[string]any { |
| 625 | result := map[string]any{} |
| 626 | agentID := agent.ID |
| 627 | |
| 628 | // Phase 1: retry connecting to the agent. |
| 629 | if agentConnFn != nil { |
| 630 | agentCtx, agentCancel := context.WithTimeout(ctx, agentConnectTimeout) |
| 631 | defer agentCancel() |
| 632 | |
| 633 | ticker := time.NewTicker(agentRetryInterval) |
| 634 | defer ticker.Stop() |
| 635 | |
| 636 | var lastErr error |
| 637 | for { |
| 638 | attemptCtx, attemptCancel := context.WithTimeout(agentCtx, agentAttemptTimeout) |
| 639 | conn, release, err := agentConnFn(attemptCtx, agentID) |
| 640 | attemptCancel() |
| 641 | if err == nil { |
| 642 | release() |
| 643 | _ = conn |
| 644 | break |
| 645 | } |
| 646 | lastErr = err |
| 647 | |
| 648 | select { |
| 649 | case <-agentCtx.Done(): |
| 650 | result["agent_status"] = "not_ready" |
| 651 | // External agents may need user action on a different |
| 652 | // host. Surface that guidance instead of the raw dial |
| 653 | // error after the retry window has elapsed. The retry |
| 654 | // loop itself is unchanged, so a Connecting external |
| 655 | // agent still gets the full window to come online. |
| 656 | if msg := externalAgentReadyError(ctx, db, agent); msg != "" { |
| 657 | result["agent_error"] = msg |
| 658 | } else { |
| 659 | result["agent_error"] = lastErr.Error() |
| 660 | } |
| 661 | return result |
| 662 | case <-ticker.C: |
| 663 | } |
| 664 | } |
| 665 | } |
| 666 | |
| 667 | // Phase 2: poll lifecycle until startup scripts finish. |
| 668 | scriptCtx, scriptCancel := context.WithTimeout(ctx, startupScriptTimeout) |
| 669 | defer scriptCancel() |
| 670 | |
| 671 | ticker := time.NewTicker(startupScriptPollInterval) |
| 672 | defer ticker.Stop() |
| 673 | |
| 674 | var lastState database.WorkspaceAgentLifecycleState |
| 675 | for { |
| 676 | row, err := db.GetWorkspaceAgentLifecycleStateByID(scriptCtx, agentID) |