GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Agent lifecycle state ignores `start_blocks_login`: one non-blocking script timing out marks the whole agent `start_timeout` · Issue #31428 · coder/coder

Agent lifecycle state ignores start_blocks_login: one non-blocking script timing out marks the whole agent start_timeout #31428

New issue
Copy link
New issue
Copy link
Open
Open
Agent lifecycle state ignores start_blocks_login: one non-blocking script timing out marks the whole agent start_timeout#31428
Copy link

Description

@blinkagent
blinkagent
bot
opened on Oct 28, 2026
Issue body actions

Summary

The agent collapses the outcome of every run_on_start script into a single agent-wide lifecycle state, without considering each script's start_blocks_login. A single non-essential script that exceeds its own timeout therefore puts the whole agent into start_timeout, even when that script explicitly declares start_blocks_login = false.

Because start_timeout is terminal for that build, the agent never reaches ready, and clients that gate on the lifecycle state leave the workspace permanently presented as still starting — even though the build is running, the agent is connected, and SSH works fine.

Filing this as a design question rather than asserting a particular fix, since changing the rollup has compatibility implications.

Current behaviour

Runner.Execute in agent/agentscripts/agentscripts.go selects start scripts purely on RunOnStart:

runScript := (option == ExecuteStartScripts && script.RunOnStart) ||
	(option == ExecuteStopScripts && script.RunOnStop) ||
	(option == ExecuteCronScripts && script.Cron != "") ||
	option == ExecuteAllScripts

It then runs them in an errgroup and returns eg.Wait(), i.e. the first non-nil error from any script. StartBlocksLogin is not consulted. The ordered path via lifecycleExecutor.execute likewise returns a single aggregate error.

agent/agent.go maps that one error onto the agent's lifecycle:

if err != nil {
	a.logger.Warn(ctx, "startup script(s) failed", slog.Error(err))
	if errors.Is(err, agentscripts.ErrTimeout) {
		a.setLifecycle(codersdk.WorkspaceAgentLifecycleStartTimeout)
	} else {
		a.setLifecycle(codersdk.WorkspaceAgentLifecycleStartError)
	}
} else {
	a.setLifecycle(codersdk.WorkspaceAgentLifecycleReady)
}

So start_blocks_login only ends up affecting:

  • the derived legacy startup_script_behavior field, in coderd/database/db2sdk/db2sdk.go
  • whether coder ssh --wait=auto waits, in cli/ssh.go and cli/vscodessh.go

It has no bearing on the lifecycle state itself.

Why this matters

The lifecycle state is the readiness signal clients actually consume. A non-blocking script is, by definition, one the template author has said should not hold up login — but its failure still degrades the only readiness signal those clients have.

Observed in the field: a workspace built from a template with an inline blocking startup_script plus a separate module-provided coder_script with start_blocks_login = false and timeout = 120. The module script hung on a network call and timed out. Result:

{
  "status": "running",
  "agents": [
    {
      "status": "connected",
      "lifecycle_state": "start_timeout"
    }
  ]
}

The agent accepted SSH normally (coder ssh --wait=no worked), but the JetBrains Toolbox plugin presented the environment as Starting indefinitely and would not open a session. Tracked on the client side in coder/coder-jetbrains-toolbox#380; the plugin has its own bug there, but the ambiguous lifecycle state is the upstream cause and would mislead any client that trusts it.

The failure is also hard to attribute. The agent log reports the timeout against the script's fallback log path:

/tmp/coder-script-<log-source-id>.log script failed  log_source_id=<uuid>  log_path=/tmp/coder-script-<uuid>.log  script_data_dir=/tmp/coder-script-data/<uuid>  execution_time=1m59.998714s  exit_code=255  error="script timed out"

There is nothing in the aggregate startup script(s) failed line, or in the lifecycle state, indicating whether the script that failed was one that blocks login.

Steps to reproduce

  1. Template with a coder_agent whose inline startup_script completes normally
  2. Add a coder_script with run_on_start = true, start_blocks_login = false, a short timeout, and a body that blocks past it
  3. Start the workspace
  4. Observe latest_build.status == "running", agent status == "connected", and agent lifecycle_state == "start_timeout"

Expected: a script declared not to block login does not prevent the agent from reaching ready. Its failure surfaces per-script, which it already does — WorkspaceAgentScript carries status and exit_code, and the build timings endpoint reports timed_out for that script specifically.

Actual: the agent-wide lifecycle state becomes start_timeout and never recovers for that build.

Discussion points

  1. Should setLifecycle consider only the outcome of scripts with start_blocks_login = true, so non-blocking failures are reported per-script without degrading agent readiness?
  2. If that changes the meaning of start_timeout / start_error too much for existing consumers, is there room for a separate signal that distinguishes "a blocking script failed" from "some start script failed"? Per-script status and exit_code already exist on the agent's scripts, so clients have a richer source available — but the single lifecycle field is what they use today.
  3. Should Execute return per-script results rather than a single eg.Wait() error, so the caller can weigh them?
  4. Independent of the above: a start_timeout or start_error agent is still fully SSH-capable. Is the current client guidance that these states are connectable? If so, it may be worth documenting, since at least one first-party client treats them as not-ready.

Created on behalf of @MoeRT09

Activity

  1. linear-code commented on Oct 28, 2026

    @linear-code
    linear-codeBot
    on Oct 28, 2026 – with Linear Code
    More actions

    ENG-3492

  2. dyson-025 commented on Oct 28, 2026

    @dyson-025
    dyson-025
    on Oct 28, 2026
    More actions

    Hi, I'd like to investigate this issue and contribute a fix.

    My initial understanding is that the aggregate error from startup scripts currently determines the agent lifecycle without considering start_blocks_login. I'd like to trace the existing execution and lifecycle behavior, review the relevant tests, and explore a backward-compatible approach that prevents non-blocking script failures from incorrectly marking the agent as not ready.

    Would the maintainers prefer adjusting the lifecycle rollup or introducing a separate readiness signal? I'd be happy to work on the implementation and regression tests once the intended behavior is clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.