Skip to content

fix: never report a launching process as Disabled - #548

Open
shivakumargn wants to merge 3 commits into
F1bonacc1:mainfrom
shivakumargn:main
Open

shivakumargn wants to merge 3 commits into
F1bonacc1:mainfrom
shivakumargn:main

Conversation

@shivakumargn

@shivakumargn shivakumargn commented Sep 20, 2026 •

Copy link
Copy Markdown

Summary

Fix manual start and restart of processes configured with disabled: true.

A disabled process that was started or restarted by hand read the stored
config in runProcess, so its new state came out as Disabled while it was
waiting for dependencies. The TUI showed it as not started, and
IsReadyReason() reported it as ready ("process is disabled").

newIncarnationState now turns a would-be Disabled status into Pending.
Anything that reaches runProcess is being launched, so it is never
Disabled. This covers manual start and every restart path (TUI Ctrl+R,
process restart, the MCP restart tool, watch triggers) in one place.

The stored project config stays disabled: true, so initial startup still
skips the process. StartProcess no longer modifies the config, so file-watch
registration behaves as before.

Changes since the first revision

Per review feedback:

  • Moved the fix from StartProcess into newIncarnationState, so restarts
    are covered too.
  • Removed the config mutation in StartProcess, which also removes the
    watchAdd side effect.
  • Added a restart case to the regression test.

Tests

TestStartDisabledProcessIsPendingUntilDependenciesAreHealthy covers:

  • Disabled -> Pending -> Running on manual start
  • restart while Pending on an unready dependency: state is Pending and
    IsReadyReason() reports not ready
  • the stored config remains disabled: true

The restart assertion fails without the fix (want Pending, got Disabled).

make lint and make testrace pass on Linux (Go 1.27).

@sonarqubecloud

Copy link
Copy Markdown

Comment thread src/app/project_runner.go Outdated
// to skip this process. A manual start must use an enabled runtime copy,
// however, or its new state remains Disabled while it waits for
// dependencies and the TUI falsely reports it as not started.
processConfig.Disabled = false

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect worth calling out: watchAdd(&processConfig) further down gets this same copy. With Disabled cleared, isWatchEligible now passes, so a disabled (or up <proc> deselected) process gets a file watch when started by hand. Before this change that call was always a no-op for them.

The comment down there says that's the intent, so I'm not against it, but it is a behavior change and it's not in the description or the test. A few things follow from it:

  • once a one-shot started this way exits, the armed watch keeps the project from completing
  • the first save restarts it through launchUnlessStopped, which runs into the Disabled issue above
  • it only kicks in if some other process had a watch at startup, because startWatcher doesn't create the watcher otherwise

If you go with the newIncarnationState change, this goes back to the old behavior and we can handle the watch question separately. If you'd rather keep it, please mention it in the description and add a test for it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. StartProcess no longer touches the config, so watchAdd gets the original disabled config and behaves as before. The watch question can be handled separately.

Comment thread src/app/project_runner.go
// however, or its new state remains Disabled while it waits for
// dependencies and the TUI falsely reports it as not started.
processConfig.Disabled = false
p.runProcess(&processConfig)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the start path, but runProcess is also called from launchUnlessStopped, and that one still reads the stored config with Disabled: true. So a restart brings the old state right back.

Repro: disabled manual depends on dep (process_healthy). Start manual and wait for Running, make dep not ready, then RestartProcess("manual"). While it waits, manual shows Disabled again and IsReadyReason() returns true, "process is disabled", so is-ready and the TUI treat it as ready even though it hasn't launched. You get the same result from Ctrl+R in the TUI, process restart, the MCP restart tool, or a watch trigger.

I'd move the fix one level down. newIncarnationState is only called from runProcess, and anything that gets there is being launched, so it should never come out as Disabled:

state := types.NewProcessState(config)
// Whatever reaches runProcess is being launched, so it is never Disabled,
// even if the stored config still says so (manual start, restart).
if state.Status == types.ProcessStateDisabled {
	state.Status = types.ProcessStatePending
}

That covers start and restart in one place, and StartProcess doesn't have to touch the config at all. Could you also add a restart case to the test? Start it, make the dependency not ready, restart, and expect Pending.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as suggested. The fix is now in newIncarnationState, so start and restart both get Pending. I added a restart case to the test. One difference from the suggested repro: I restart while manual is still Pending on an unready dependency, rather than after it reached Running. Once a dependency has been ready, that is latched, so if it later fails, a restart ends up Skipped instead of waiting. Restarting during Pending goes through the same launchUnlessStopped path with the stored Disabled: true config, and the assertion fails without the fix.

A disabled process that is started or restarted manually read the stored
config in runProcess, so its new state came out as Disabled while it
waited for dependencies. The TUI showed it as not started and
IsReadyReason reported it as ready.

Normalize the status in newIncarnationState instead of mutating the
config in StartProcess. Anything reaching runProcess is being launched,
so it is Pending. This covers start and restart (TUI, CLI, MCP, watch)
and leaves watch registration as it was.
@sonarqubecloud

Copy link
Copy Markdown

@shivakumargn shivakumargn changed the title fix: start disabled processes manually fix: never report a launching process as Disabled Oct 28, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants