Skip to content

feat(workspace): support async, durable workspace up execution - #798

Open
skevetter wants to merge 18 commits into
mainfrom
feat/async-workspace-up
Open

feat(workspace): support async, durable workspace up execution#798
skevetter wants to merge 18 commits into
mainfrom
feat/async-workspace-up

Conversation

@skevetter

@skevetter skevetter commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

workspace up currently runs synchronously end-to-end, streaming a single JSON blob to the caller only once the container is ready. That blocks the CLI/desktop for the whole provisioning window and gives up all in-progress state on a crash or disconnect.

Adds a submit/poll durable execution model alongside the existing synchronous flow:

  • pkg/task: a JSON-file task store (atomic writes, cross-process flock) recording status/PID/result for background-launched work.
  • workspace up --detach re-execs itself as a background process and returns a task ID immediately.
  • workspace task list/get/logs/cancel/rm: standard verb commands (kubectl/docker/gh-style) to manage submitted tasks.
  • pkg/devcontainer/status: a structured Phase/Event/Reporter model threaded through Runner.Up and the devcontainer build/run/setup pipeline, replacing ad hoc log lines with typed progress events.
  • A StatusUpdate RPC on the agent tunnel streams those events back to the CLI host live instead of waiting for the final result.
  • Desktop app updated to submit+poll via the new task model instead of parsing a single terminal JSON blob.

See docs/rfcs/async-workspace-up.md for the full design.

Includes an unrelated one-line test fix in pkg/compose/helper_test.go (skip when the local podman machine is unreachable, a machine-local CI-vs-local flake noticed during this work).

Summary by CodeRabbit

  • New Features
    • Added workspace up --detach with task-based progress you can query, follow logs, cancel, and remove.
    • Desktop now streams structured workspace status updates while workspace up runs in the background.
    • Enhanced tunnel/server status reporting to support real-time phase updates.
  • Bug Fixes
    • Provisioning task abandonment is now detected more reliably and marked as failed.
    • Improved cancellation ordering and handling for overlapping workspace operations.
    • Fixed process termination to properly handle errors in kill.
  • Tests
    • Added/expanded unit and e2e coverage for detached task flows, status streaming/envelopes, and lifecycle edge cases.

workspace up currently runs synchronously end-to-end, streaming a
single JSON blob to the caller only once the container is ready. That
blocks the CLI/desktop for the whole provisioning window and gives up
all in-progress state on a crash or disconnect.

Adds a submit/poll durable execution model alongside the existing
synchronous flow:

- pkg/task: a JSON-file task store (atomic writes, cross-process
  flock) recording status/PID/result for background-launched work.
- workspace up --detach re-execs itself as a background process and
  returns a task ID immediately.
- workspace task list/get/logs/cancel/rm: standard verb commands
  (kubectl/docker/gh-style) to manage submitted tasks.
- pkg/devcontainer/status: a structured Phase/Event/Reporter model
  threaded through Runner.Up and the devcontainer build/run/setup
  pipeline, replacing ad hoc log lines with typed progress events.
- A StatusUpdate RPC on the agent tunnel streams those events back to
  the CLI host live instead of waiting for the final result.
- Desktop app updated to submit+poll via the new task model instead of
  parsing a single terminal JSON blob.

See docs/rfcs/async-workspace-up.md for the full design.
@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for images-devsy-sh canceled.

Name Link
🔨 Latest commit 9301cd0
🔍 Latest deploy log https://app.netlify.com/projects/images-devsy-sh/deploys/6a6adcec78858b0008d0c66d

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@skevetter, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dabc896b-211c-4f86-80a8-ad7701b21991

📥 Commits

Reviewing files that changed from the base of the PR and between 32fe61f and 9301cd0.

📒 Files selected for processing (2)
  • pkg/compose/helper_test.go
  • pkg/task/export_test.go
📝 Walkthrough

Walkthrough

The PR adds detached workspace tasks, persistent worker-lock reconciliation, structured status propagation through container and tunnel paths, desktop IPC task streaming, task-control commands, and tests for lifecycle, concurrency, process termination, and envelope handling.

Changes

Workspace task lifecycle

Layer / File(s) Summary
Task persistence and reconciliation
pkg/task/*, pkg/config/pathmanager.go
Tasks persist lifecycle state, use worker locks for abandonment detection, reconcile abandoned workers as failed, and strengthen file durability.
Workspace command integration
cmd/workspace/up/*, cmd/workspace/task.go, cmd/workspace/workspace.go, pkg/flags/names/names.go
workspace up supports detached execution and task IDs, while task commands are registered for inspection and control.
Task validation
pkg/task/*_test.go, cmd/workspace/task_test.go, cmd/workspace/up/detach_test.go
Tests cover persistence, cancellation, worker locks, reconciliation, result envelopes, and detached argument handling.

Structured status propagation

Layer / File(s) Summary
Status contracts and envelopes
pkg/status/*, pkg/devcontainer/config/envelope.go, pkg/client/client.go, pkg/agent/tunnel/*
Status reporters, typed NDJSON envelopes, task envelopes, status parsing, and tunnel status messages are defined.
Container and tunnel reporting
pkg/devcontainer/*, pkg/agent/tunnelserver/*, cmd/internal/*, cmd/workspace/up/agent.go
Container lifecycle phases are reported through local reporters or tunnel RPCs, with no-op defaults for callers without a reporter.
Daemon output forwarding
pkg/client/clientimplementation/daemonclient/*
Daemon output is scanned for status envelopes, status events are forwarded to reporters, and partial lines are buffered across writes.

Desktop detached-up flow

Layer / File(s) Summary
Mock task backend
desktop/e2e/fixtures/mock-devsy.cjs, desktop/e2e/workspaces.e2e.ts
The mock CLI persists tasks, supports detached submission and task subcommands, materializes workspaces during logs, and emits structured completion results.
IPC task streaming
desktop/src/main/ipc.ts, desktop/src/shared/cli-error.ts
The desktop handler submits detached tasks, follows logs, parses status/result/error envelopes, serializes per-workspace operations, and tracks active task IDs.
Renderer status events
desktop/src/renderer/src/lib/ipc/events.ts, desktop/src/renderer/src/lib/types/index.ts
A workspace-status event and listener expose streamed workspace status payloads to the renderer.
IPC behavior tests
desktop/src/main/__tests__/ipc-up-tasks.test.ts
Tests cover replacement cancellation, concurrent submissions, cancellation failures, status forwarding, stderr filtering, and terminal cleanup.

Supporting concurrency changes

Layer / File(s) Summary
Concurrent feature resolution
pkg/devcontainer/feature/*
User feature resolution runs through an errgroup, while lockfile entry writes are mutex-protected.
Process termination handling
pkg/command/*
Process termination returns unexpected signal errors while treating already-exited processes as successful no-ops.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DesktopIPC
  participant DevsyCLI
  participant TaskStore
  participant TunnelServer
  participant Renderer
  DesktopIPC->>DevsyCLI: workspace up --detach
  DevsyCLI->>TaskStore: Create task
  DevsyCLI-->>DesktopIPC: task envelope
  DesktopIPC->>DevsyCLI: workspace task logs --follow
  DevsyCLI->>TunnelServer: Status(StatusUpdate)
  DevsyCLI-->>DesktopIPC: status/result/error envelopes
  DesktopIPC->>Renderer: workspace-status event
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: async, durable workspace up execution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 9301cd0
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a6adcea73c5fe0008226682

Fixes the unused status import left over from the intermediate up.go
split, plus cyclop, funcorder, errcheck, forbidigo, goconst, golines,
modernize, and revive findings across the new task/status/detach code.
fmt.Printf is disallowed by the forbidigo rule; fmt.Fprintf(os.Stdout, ...)
is the same output without needing a suppression comment.
workspace_up now submits `up --detach` and polls `workspace task logs
<id> --follow` instead of running `up` to completion. The e2e mock CLI
still implemented the old synchronous `up` only: `--detach` was
silently swallowed by parseArgs's generic flag skip, so mock-devsy
wrote plain progress text instead of a {kind:"task", id} envelope.
cli.run's JSON.parse then threw, the up handler took its catch path,
and the wizard's success check never ran -- so the "Open Workspace"
button never appeared, timing out the three affected e2e specs.

Adds a `task` verb to the mock (list/get/logs/cancel/rm) backed by a
`tasks` map in the persisted mock state, and makes `up --detach` write
the {kind:"task", id} envelope and defer the actual workspace/progress
simulation to `task logs --follow`, mirroring pkg/task and
pkg/devcontainer/config/envelope.go.
@skevetter
skevetter marked this pull request as ready for review July 29, 2026 13:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
desktop/src/main/ipc.ts (1)

751-841: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Old tunnel process isn't awaited before its map entry is overwritten — can leak an orphaned child.

existing.kill("SIGTERM") (Line 756) is fire-and-forget; execution continues immediately into the detached submit and eventually tunnelProcesses.set(wsId, child) (Line 841) overwrites the map key before the old process is confirmed dead. If it hasn't exited yet, its handle is lost — no future quiesceWorkspace call can find/kill it. quiesceWorkspace (Lines 187-199) already implements the correct await-exit pattern for exactly this; this block should share it instead of duplicating a weaker copy that also omits the cli/pty cancellation. Consider extracting the tunnel-kill logic (and the active-task-cancel logic duplicated at Lines 759-765 vs. 179-186) into one helper both call.

🔒 Proposed fix — await exit before proceeding
       const existing = tunnelProcesses.get(wsId)
       if (existing) {
-        existing.kill("SIGTERM")
         tunnelProcesses.delete(wsId)
+        const existingExit = new Promise<void>((resolve) => {
+          if (existing.exitCode !== null || existing.signalCode !== null) {
+            resolve()
+            return
+          }
+          existing.once("close", () => resolve())
+        })
+        existing.kill("SIGTERM")
+        await existingExit
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/main/ipc.ts` around lines 751 - 841, Update the workspace startup
cleanup around tunnelProcesses and activeUpTasks to reuse the existing
quiesceWorkspace cleanup logic, or extract a shared helper used by both paths.
Ensure the existing tunnel child is terminated and awaited before replacing its
map entry, and preserve cancellation of any active CLI task, including the
existing cli/pty cleanup behavior implemented by quiesceWorkspace.
pkg/devcontainer/run.go (1)

180-188: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drop the outer PhaseReady failure reporting for inner dispatch failures.

dispatchByConfigKind routes to paths that already report the actual failing phase (PhaseBuildingImage, PhaseStartingContainer, PhaseInjectingAgent, PhaseRunningLifecycleHook). This outer status.Fail(reporter, status.PhaseReady, err) re-emits those failures with Step: "ready", which mislabels build/startup/agent/lifecycle failures and duplicates the failure event.

🐛 Possible fix: only report the phase name once, closest to the source, and drop this generic outer Fail
 	result, err := r.dispatchByConfigKind(ctx, substitutedConfig, params)
 	if result != nil {
 		result.RecoveryContainer = r.recovering
 	}
 	if err != nil {
-		status.Fail(reporter, status.PhaseReady, err)
+		// Assumes the inner pipeline already reported the granular failing
+		// phase via status.Fail; avoid re-labeling it as "ready" here.
 		return result, err
 	}
 	status.Leave(reporter, status.PhaseReady, "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/devcontainer/run.go` around lines 180 - 188, Remove the outer status.Fail
call for errors returned by dispatchByConfigKind in the run flow, while
preserving the error return and result recovery assignment. Let the dispatched
paths report their specific phases—PhaseBuildingImage, PhaseStartingContainer,
PhaseInjectingAgent, or PhaseRunningLifecycleHook—without emitting a duplicate
PhaseReady failure.
pkg/client/clientimplementation/workspace_client.go (1)

1148-1163: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Forward the local-up status reporter into runTunnelServer.

pkg/client/clientimplementation/workspace_client.go:1158-1161 appends a final WithStatusReporter(status.NewLogReporter()), which overwrites any per-call status reporter because tunnelserver.New() applies options in order and each option sets s.statusReporter. This ignores the caller’s progress/reporter setup, including the UpCommandReporter wired in cmd/workspace/up/agent.go; status events from the agent over RPC are logged and then discarded. Thread cmd.statusReporter or UpOptions.Reporter into BuildAgentClientOptions.TunnelOptions instead of forcing a discard-only logger here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/client/clientimplementation/workspace_client.go` around lines 1148 -
1163, The runTunnelServer function currently overwrites caller-provided
reporters with status.NewLogReporter. Remove that forced reporter and ensure the
local-up status reporter, such as cmd.statusReporter or UpOptions.Reporter, is
propagated through BuildAgentClientOptions.TunnelOptions before reaching
RunUpServer.
🧹 Nitpick comments (1)
pkg/client/clientimplementation/workspace_client.go (1)

288-315: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Task-store file I/O runs while holding s.m for the whole Status() call.

taskStatusOverride()/latestUpTask() perform directory listing + per-file reads while s.m.Lock() is held for the entire method, extending lock contention beyond what's needed to safely read s.workspace.ID.

♻️ Suggested: snapshot the workspace ID, then do file I/O outside the lock
 func (s *workspaceClient) Status(
 	ctx context.Context,
 	opt client.StatusOptions,
 ) (client.Status, error) {
 	s.m.Lock()
-	defer s.m.Unlock()
-
 	var (
 		result client.Status
 		err    error
 	)
 	switch {
 	case s.isMachineProvider() && len(s.config.Exec.Status) > 0:
 		result, err = s.machineStatus(ctx, opt)
 	case opt.ContainerStatus:
 		result, err = s.getContainerStatus(ctx)
 	default:
 		result, err = s.workspaceFolderStatus()
 	}
+	workspaceID := s.workspace.ID
+	s.m.Unlock()
 	if err != nil || result != client.StatusNotFound {
 		return result, err
 	}
-
-	if override, ok := s.taskStatusOverride(); ok {
+	if override, ok := s.taskStatusOverride(workspaceID); ok {
 		return override, nil
 	}
 	return result, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/client/clientimplementation/workspace_client.go` around lines 288 - 315,
Update workspaceClient.Status to avoid holding s.m during task-store I/O: lock
only long enough to snapshot the workspace ID needed by
taskStatusOverride/latestUpTask, then unlock before performing status resolution
and override checks. Ensure taskStatusOverride uses the snapshot rather than
reading s.workspace.ID while the mutex is held, preserving existing status
precedence and return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/workspace/task.go`:
- Around line 154-194: Validate that the parsed interval is strictly positive
immediately after time.ParseDuration in the command handler, before passing it
through followTask or creating a ticker. Return a descriptive error for zero or
negative durations, while preserving the existing parse error and normal
positive-interval behavior.

In `@cmd/workspace/up/status.go`:
- Around line 30-39: Update plainStatusReporter.Report to handle non-started
phase events in addition to failures and starts, emitting a completion/ready log
for Leave events including the final PhaseReady event. Mirror the completion
behavior used by logReporter while preserving the existing failure and
phase-start messages.

In `@cmd/workspace/up/up.go`:
- Around line 226-237: Update the task initialization error paths in the
workspace-up flow so both openTask and SetWorkspaceID failures are passed
through reportErr(err, emitJSON, out) before returning. Preserve the existing
failTask behavior for SetWorkspaceID while ensuring JSON mode emits an error
envelope for either failure.

In `@pkg/client/clientimplementation/daemonclient/up.go`:
- Around line 450-492: Update statusSniffingWriter.Close to run the buffered
trailing bytes through config.ParseStatusLine before forwarding them to next,
matching the complete-line handling in Write. Report and suppress a trailing
status envelope without a newline; forward non-status content unchanged, then
clear the buffer and preserve the existing error behavior.

In `@pkg/command/process_supported.go`:
- Around line 45-47: In pkg/command/process_supported.go lines 45-47, update the
process escalation flow around syscall.Kill to retain and validate a stable
process identity before sending SIGKILL, avoiding blind signaling of a reused
numeric PID. In pkg/command/process_test.go lines 33-42, replace the stale
real-PID scenario with an injected or stubbed signal operation that returns
syscall.ESRCH, and verify the existing handling for that result.

In `@pkg/compose/helper_test.go`:
- Around line 194-196: Update the Podman reachability probe in the test setup to
use exec.CommandContext with a short timeout, canceling the context after the
probe completes. Preserve the existing skip behavior when the timed command
fails or times out.

In `@pkg/devcontainer/status/status.go`:
- Around line 32-35: Update jsonStatusReporter.Report to serialize access to Out
with synchronization before encoding and writing each status event. Ensure
concurrent tunnel Status RPC reports produce complete, non-interleaved NDJSON
lines while preserving the existing reporting behavior.

In `@pkg/task/store.go`:
- Around line 67-89: Update Store.Delete to acquire and hold the same per-id
flock used by update and write for the entire status-check and file-removal
sequence, releasing it on every return path. Reuse the existing locking helper
and preserve the force/non-terminal validation and deletion behavior while
ensuring no concurrent atomic rename can recreate the task after removal.

In `@pkg/task/task.go`:
- Around line 87-93: Guard Task.Succeed, Task.Fail, and taskReporter.Report with
the same State.Terminal() check used by Cancel before mutating state. Preserve
the existing terminal state and result/error when the task is already canceled
or otherwise terminal; only apply success, failure, or phase/step updates for
non-terminal tasks.

---

Outside diff comments:
In `@desktop/src/main/ipc.ts`:
- Around line 751-841: Update the workspace startup cleanup around
tunnelProcesses and activeUpTasks to reuse the existing quiesceWorkspace cleanup
logic, or extract a shared helper used by both paths. Ensure the existing tunnel
child is terminated and awaited before replacing its map entry, and preserve
cancellation of any active CLI task, including the existing cli/pty cleanup
behavior implemented by quiesceWorkspace.

In `@pkg/client/clientimplementation/workspace_client.go`:
- Around line 1148-1163: The runTunnelServer function currently overwrites
caller-provided reporters with status.NewLogReporter. Remove that forced
reporter and ensure the local-up status reporter, such as cmd.statusReporter or
UpOptions.Reporter, is propagated through BuildAgentClientOptions.TunnelOptions
before reaching RunUpServer.

In `@pkg/devcontainer/run.go`:
- Around line 180-188: Remove the outer status.Fail call for errors returned by
dispatchByConfigKind in the run flow, while preserving the error return and
result recovery assignment. Let the dispatched paths report their specific
phases—PhaseBuildingImage, PhaseStartingContainer, PhaseInjectingAgent, or
PhaseRunningLifecycleHook—without emitting a duplicate PhaseReady failure.

---

Nitpick comments:
In `@pkg/client/clientimplementation/workspace_client.go`:
- Around line 288-315: Update workspaceClient.Status to avoid holding s.m during
task-store I/O: lock only long enough to snapshot the workspace ID needed by
taskStatusOverride/latestUpTask, then unlock before performing status resolution
and override checks. Ensure taskStatusOverride uses the snapshot rather than
reading s.workspace.ID while the mutex is held, preserving existing status
precedence and return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a92c0000-ca8f-4986-be56-496cfd1301e6

📥 Commits

Reviewing files that changed from the base of the PR and between dcb0ace and 13723b1.

⛔ Files ignored due to path filters (2)
  • pkg/agent/tunnel/tunnel.pb.go is excluded by !**/*.pb.go
  • pkg/agent/tunnel/tunnel_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (44)
  • cmd/internal/agentworkspace/up.go
  • cmd/internal/container_tunnel.go
  • cmd/workspace/task.go
  • cmd/workspace/task_test.go
  • cmd/workspace/up/agent.go
  • cmd/workspace/up/detach.go
  • cmd/workspace/up/detach_test.go
  • cmd/workspace/up/status.go
  • cmd/workspace/up/up.go
  • cmd/workspace/up/up_flags.go
  • cmd/workspace/workspace.go
  • desktop/e2e/fixtures/mock-devsy.cjs
  • desktop/src/main/ipc.ts
  • desktop/src/renderer/src/lib/ipc/events.ts
  • desktop/src/renderer/src/lib/types/index.ts
  • desktop/src/shared/cli-error.ts
  • pkg/agent/tunnel/tunnel.proto
  • pkg/agent/tunnelserver/options.go
  • pkg/agent/tunnelserver/status_sender.go
  • pkg/agent/tunnelserver/tunnelserver.go
  • pkg/client/client.go
  • pkg/client/clientimplementation/daemonclient/stop.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • pkg/client/clientimplementation/daemonclient/up_test.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/client/clientimplementation/workspace_client_status_test.go
  • pkg/command/process_supported.go
  • pkg/command/process_test.go
  • pkg/compose/helper_test.go
  • pkg/config/pathmanager.go
  • pkg/devcontainer/build.go
  • pkg/devcontainer/config/envelope.go
  • pkg/devcontainer/feature/extend.go
  • pkg/devcontainer/feature/lockfile.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/setup.go
  • pkg/devcontainer/single.go
  • pkg/devcontainer/status/log.go
  • pkg/devcontainer/status/status.go
  • pkg/devcontainer/status/status_test.go
  • pkg/flags/names/names.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_test.go

Comment thread cmd/workspace/task.go
Comment thread cmd/workspace/up/status.go
Comment thread cmd/workspace/up/up.go
Comment thread pkg/client/clientimplementation/daemonclient/up.go
Comment on lines +45 to +47
err = syscall.Kill(parsedPid, syscall.SIGKILL)
if err != nil && !errors.Is(err, syscall.ESRCH) {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Numeric PID reuse makes both escalation and the ESRCH test unsafe. A PID may belong to a different process after the original target exits, so it cannot safely identify the process for a delayed SIGKILL or a real-process ESRCH test.

  • pkg/command/process_supported.go#L45-L47: retain and validate a stable process identity before escalation, rather than blindly signaling the numeric PID after the delay.
  • pkg/command/process_test.go#L33-L42: replace the stale real-PID scenario with an injected/stubbed signal operation that returns syscall.ESRCH.
📍 Affects 2 files
  • pkg/command/process_supported.go#L45-L47 (this comment)
  • pkg/command/process_test.go#L33-L42
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/command/process_supported.go` around lines 45 - 47, In
pkg/command/process_supported.go lines 45-47, update the process escalation flow
around syscall.Kill to retain and validate a stable process identity before
sending SIGKILL, avoiding blind signaling of a reused numeric PID. In
pkg/command/process_test.go lines 33-42, replace the stale real-PID scenario
with an injected or stubbed signal operation that returns syscall.ESRCH, and
verify the existing handling for that result.

Comment thread pkg/compose/helper_test.go Outdated
Comment thread pkg/status/status.go
Comment thread pkg/task/store.go
Comment thread pkg/task/task.go
@skevetter
skevetter marked this pull request as draft July 29, 2026 14:31
A canceled task whose worker finished afterwards had its cancellation
reason erased, because Succeed/Fail/Report mutated state without the
Terminal() guard Cancel already used. Guard all three so the recorded
outcome reflects why the task actually stopped.

Also from review of the async workspace-up changes:

- Delete holds the per-id flock across the status check and removal, so a
  concurrent update's atomic rename can't recreate the task.
- Reject a non-positive --interval instead of panicking in NewTicker.
- Serialize jsonStatusReporter writes; events arrive from both the local
  pipeline and the tunnel Status RPC goroutine, and interleaving would
  corrupt the NDJSON framing callers parse line-by-line.
- Report PhaseReady completion in plain output, and route task-init
  failures through reportErr so JSON mode emits an error envelope.
- Sniff trailing bytes left without a newline so a final status envelope
  is reported rather than leaking into the caller's stream as raw JSON.
- Order the tunnel's default log reporter ahead of caller-supplied
  options so it no longer clobbers them, and forward up's reporter into
  the platform path.
- Bound the podman probe with a context so an unreachable machine can't
  hang the test on the socket.
- Await the prior up child's exit before overwriting its map entry,
  which otherwise orphaned it beyond the reach of any later kill.
@skevetter
skevetter marked this pull request as ready for review July 29, 2026 19:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
desktop/src/main/ipc.ts (1)

177-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not discard task ownership when cancellation fails.

The mapping is removed before cancellation and errors are swallowed. If cancellation fails, workspace_stop/workspace_delete proceeds while the detached up task still runs, and no later action can retry cancelling it. Retain the entry until cancellation succeeds (or the task is confirmed terminal), and propagate unexpected cancellation failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/main/ipc.ts` around lines 177 - 183, The cancelActiveUp function
currently deletes the active task mapping and suppresses cancellation errors
before cancellation succeeds. Move activeUpTasks.delete(workspaceId) until after
a successful cancellation or confirmed terminal task state, and let unexpected
cli.run failures propagate so workspace_stop/workspace_delete cannot proceed
while the task remains active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src/main/ipc.ts`:
- Around line 761-764: Update the workspace_up flow around cancelActiveUp and
the detached submission/follower registration logic to serialize cancel, submit,
and registration per wsId, preventing concurrent requests from overwriting one
another. In result, error, and exit cleanup callbacks, remove tunnelProcesses
entries only when their stored task or process identity matches the completing
session; preserve newer workspace_up sessions and their cancellability.

---

Outside diff comments:
In `@desktop/src/main/ipc.ts`:
- Around line 177-183: The cancelActiveUp function currently deletes the active
task mapping and suppresses cancellation errors before cancellation succeeds.
Move activeUpTasks.delete(workspaceId) until after a successful cancellation or
confirmed terminal task state, and let unexpected cli.run failures propagate so
workspace_stop/workspace_delete cannot proceed while the task remains active.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 818eaff1-c8a6-478d-8d2a-cbb6c1e08c52

📥 Commits

Reviewing files that changed from the base of the PR and between 13723b1 and 351cf2e.

📒 Files selected for processing (11)
  • cmd/workspace/task.go
  • cmd/workspace/up/agent.go
  • cmd/workspace/up/status.go
  • cmd/workspace/up/up.go
  • desktop/src/main/ipc.ts
  • pkg/client/clientimplementation/daemonclient/up.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/compose/helper_test.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • cmd/workspace/up/status.go
  • pkg/compose/helper_test.go
  • cmd/workspace/up/agent.go
  • pkg/task/task.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/task/store.go
  • cmd/workspace/task.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • cmd/workspace/up/up.go

Comment thread desktop/src/main/ipc.ts Outdated
@skevetter
skevetter marked this pull request as draft July 29, 2026 19:36
Two ways a detached `up` could keep running with nothing left to stop it:

cancelActiveUp dropped the workspace's task mapping before cancelling and
swallowed the failure, so a failed cancel left the task running while
stop/delete proceeded to tear the workspace down under it. Hold the entry
until the cancel lands and let the failure propagate; the up handler
reports it and skips submitting a replacement, and stop/delete callers
already surface the rejection as a toast.

Concurrent workspace_up calls for one workspace also both passed the
cancel step before either registered its task, orphaning the first.
Serialize the cancel/submit/register sequence per workspace, and scope
the cleanup callbacks to the task they belong to so a finishing session
can't clear a newer one's entry.
The Phase/Event/Reporter model isn't devcontainer-specific; provider
install/init flows want the same structured progress events. Move it to
pkg/status before another pipeline imports it under a name that would
misdescribe it.

Import-path churn only; no behavior change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
pkg/client/clientimplementation/workspace_client.go (1)

336-380: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Reconcile abandoned tasks before reporting provisioning.

latestUpTask maps every persisted non-terminal task to StatusProvisioning. If the detached worker is killed after task creation but before it records a terminal result, later status checks can report provisioning indefinitely for a missing workspace. Add worker liveness/heartbeat expiry or startup reconciliation before applying this override.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/client/clientimplementation/workspace_client.go` around lines 336 - 380,
Update latestUpTask and taskStatusOverride so persisted non-terminal up tasks
are reconciled before returning StatusProvisioning. Detect abandoned tasks using
the task worker’s existing liveness/heartbeat expiry or startup reconciliation
mechanism, mark them terminal/failed as appropriate, and only report
provisioning for an actively running task; preserve the existing best-effort
ok=false behavior when task state cannot be read.
desktop/src/main/ipc.ts (1)

239-242: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

quiesceWorkspace bypasses the serialization chain, so stop/delete can still race an in-flight up submission.

workspace_up now runs cancel → submit → register inside serializePerWorkspace, but quiesceWorkspace (used by workspace_stop/workspace_delete) calls cancelActiveUp directly. If a stop lands while an up is between cli.run([... "--detach"]) and activeUpTasks.set(wsId, taskId), the cancel observes no mapping, then the up registers its task id — the detached task survives the stop/delete and keeps running against a workspace that is being torn down.

Enqueue the quiesce on the same per-workspace chain.

🔒️ Proposed fix
   async function quiesceWorkspace(workspaceId: string): Promise<void> {
-    await cancelActiveUp(workspaceId)
-    await Promise.all([cli.cancelFor(workspaceId), pty.cancelFor(workspaceId)])
+    await serializePerWorkspace(workspaceId, async () => {
+      await cancelActiveUp(workspaceId)
+      await Promise.all([
+        cli.cancelFor(workspaceId),
+        pty.cancelFor(workspaceId),
+      ])
+    })
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/main/ipc.ts` around lines 239 - 242, Update quiesceWorkspace to
execute cancelActiveUp and the cli/pty cancellation operations through
serializePerWorkspace for the given workspaceId, ensuring it joins the same
chain used by workspace_up. Preserve the existing cancellation ordering and
Promise.all behavior while preventing stop/delete from racing an in-flight
submission.
pkg/agent/tunnelserver/options.go (1)

80-83: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve the no-op reporter when the option receives nil.

Line 82 can overwrite the status.Nop() default with nil. A later status.Enter/Fail call then invokes Report on a nil reporter and panics. Ignore nil or normalize it to status.Nop().

Proposed fix
 func WithStatusReporter(reporter status.Reporter) Option {
 	return func(s *tunnelServer) *tunnelServer {
-		s.statusReporter = reporter
+		if reporter != nil {
+			s.statusReporter = reporter
+		}
 		return s
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/agent/tunnelserver/options.go` around lines 80 - 83, Update
WithStatusReporter so passing a nil reporter does not replace the tunnelServer’s
existing status.Nop() reporter; ignore nil or normalize it to status.Nop(),
while continuing to assign non-nil reporters.
🧹 Nitpick comments (1)
desktop/src/main/__tests__/ipc-up-tasks.test.ts (1)

93-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the streamed-envelope path too.

The three tests cover cancel/serialize/retain-on-failure well, but nothing exercises the parseCliEnvelope branch in workspace_up: status envelopes emitting workspace-status, and result/error envelopes releasing the task and completing the sink before the exit callback. Driving the runStreaming onLine callback with a few NDJSON lines would lock in that contract cheaply, and would also catch a getMainWindow() returning null regression (the current double always returns null, so the send path is never hit).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/main/__tests__/ipc-up-tasks.test.ts` around lines 93 - 154,
Extend the workspace_up test coverage to drive runStreaming’s onLine callback
with NDJSON envelopes parsed by parseCliEnvelope: verify status envelopes emit
workspace-status, while result and error envelopes release the task and complete
the sink before the exit callback. Update the getMainWindow test double to
return a window so the send path is exercised, while retaining coverage for the
null-window case if supported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src/main/ipc.ts`:
- Around line 801-817: Validate submitted.id immediately after cli.run in the
existing try block before assigning taskId or updating activeUpTasks; if it is
missing or invalid, throw a clear error so the existing catch routes the failure
through sink.done with the standard error metadata and returns cmdId.

---

Outside diff comments:
In `@desktop/src/main/ipc.ts`:
- Around line 239-242: Update quiesceWorkspace to execute cancelActiveUp and the
cli/pty cancellation operations through serializePerWorkspace for the given
workspaceId, ensuring it joins the same chain used by workspace_up. Preserve the
existing cancellation ordering and Promise.all behavior while preventing
stop/delete from racing an in-flight submission.

In `@pkg/agent/tunnelserver/options.go`:
- Around line 80-83: Update WithStatusReporter so passing a nil reporter does
not replace the tunnelServer’s existing status.Nop() reporter; ignore nil or
normalize it to status.Nop(), while continuing to assign non-nil reporters.

In `@pkg/client/clientimplementation/workspace_client.go`:
- Around line 336-380: Update latestUpTask and taskStatusOverride so persisted
non-terminal up tasks are reconciled before returning StatusProvisioning. Detect
abandoned tasks using the task worker’s existing liveness/heartbeat expiry or
startup reconciliation mechanism, mark them terminal/failed as appropriate, and
only report provisioning for an actively running task; preserve the existing
best-effort ok=false behavior when task state cannot be read.

---

Nitpick comments:
In `@desktop/src/main/__tests__/ipc-up-tasks.test.ts`:
- Around line 93-154: Extend the workspace_up test coverage to drive
runStreaming’s onLine callback with NDJSON envelopes parsed by parseCliEnvelope:
verify status envelopes emit workspace-status, while result and error envelopes
release the task and complete the sink before the exit callback. Update the
getMainWindow test double to return a window so the send path is exercised,
while retaining coverage for the null-window case if supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0634d59-0d8e-4640-86c6-543dacef7c11

📥 Commits

Reviewing files that changed from the base of the PR and between 351cf2e and 5e9f668.

📒 Files selected for processing (24)
  • cmd/internal/container_tunnel.go
  • cmd/workspace/task.go
  • cmd/workspace/up/status.go
  • cmd/workspace/up/up.go
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • desktop/src/main/ipc.ts
  • pkg/agent/tunnelserver/options.go
  • pkg/agent/tunnelserver/status_sender.go
  • pkg/agent/tunnelserver/tunnelserver.go
  • pkg/client/client.go
  • pkg/client/clientimplementation/daemonclient/stop.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • pkg/client/clientimplementation/daemonclient/up_test.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/devcontainer/build.go
  • pkg/devcontainer/config/envelope.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/setup.go
  • pkg/devcontainer/single.go
  • pkg/status/log.go
  • pkg/status/status.go
  • pkg/status/status_test.go
  • pkg/task/task.go
  • pkg/task/task_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/agent/tunnelserver/tunnelserver.go
  • pkg/task/task.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • cmd/internal/container_tunnel.go
  • pkg/devcontainer/setup.go
  • pkg/devcontainer/config/envelope.go
  • cmd/workspace/up/up.go

Comment thread desktop/src/main/ipc.ts
@skevetter
skevetter marked this pull request as draft July 29, 2026 23:25
Four findings from review of the async workspace-up changes:

A detached worker killed before recording a result left its task
non-terminal forever, so client.Status reported Provisioning indefinitely
for a workspace with no container. Reconcile against the worker's recorded
PID: a non-terminal task whose process is gone is marked failed. Tasks with
no PID yet are still starting, and Fail preserves an existing terminal
state so a cancellation reason isn't overwritten.

quiesceWorkspace cancelled directly instead of joining the per-workspace
chain workspace_up uses, so a stop landing between an up's submit and its
activeUpTasks.set saw no mapping to cancel — the task then registered and
outlived the teardown. Enqueue the quiesce on the same chain.

WithStatusReporter installed a nil reporter over the Nop default, which
would panic on the next status event. Ignore nil.

workspace_up trusted the submit response's id; a missing one was stored and
later used to cancel a task that doesn't exist, orphaning the real one.

Also covers the streamed-envelope path in the workspace_up tests, which had
no coverage: status envelopes emitting workspace-status, stderr lines not
being parsed as envelopes, and result/error envelopes releasing the task.
Signed-off-by: Samuel K <skevetter@pm.me>
@skevetter
skevetter marked this pull request as ready for review July 30, 2026 00:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
desktop/src/main/ipc.ts (1)

877-889: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the task registered when the log follower exits unexpectedly.

Line 878 releases taskId even when no result or error envelope arrived. A failed follower can therefore orphan a still-running detached task from later cancellation. Release only on a terminal envelope (or after independently confirming the task is terminal).

Proposed fix
           (code, cliError) => {
-            releaseTask()
             if (tunnelProcesses.get(wsId) === child) {
               tunnelProcesses.delete(wsId)
             }
             if (signalledDone) return

Add a regression test where the follower exits nonzero without an envelope, then verify the next workspace_up cancels the original task.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/main/ipc.ts` around lines 877 - 889, Update the follower exit
callback around releaseTask so an unexpected nonzero exit without a result or
error envelope does not unregister the still-running task; release the task only
after receiving a terminal envelope or independently confirming termination.
Preserve cleanup of tunnelProcesses and sink reporting, and add a regression
test verifying that a subsequent workspace_up cancels the original task.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/task/store.go`:
- Around line 113-122: Update Abandoned to avoid treating PID liveness alone as
worker identity: persist a worker-instance identity alongside State.PID and
verify that identity for the current process before considering the task alive.
If the identity is missing, mismatched, or the process is no longer running,
return true for a non-terminal task; preserve the existing safe behavior when
liveness or identity cannot be determined.

In `@pkg/task/task.go`:
- Around line 17-20: Remove the duplicate ErrAbandoned declaration from the
package-level var block in task.go, preserving the existing declaration in
store.go and retaining ErrCanceled unchanged.

---

Outside diff comments:
In `@desktop/src/main/ipc.ts`:
- Around line 877-889: Update the follower exit callback around releaseTask so
an unexpected nonzero exit without a result or error envelope does not
unregister the still-running task; release the task only after receiving a
terminal envelope or independently confirming termination. Preserve cleanup of
tunnelProcesses and sink reporting, and add a regression test verifying that a
subsequent workspace_up cancels the original task.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ed8f5388-9dac-41fd-a1ba-6367f893f6bc

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9f668 and 34ea02f.

⛔ Files ignored due to path filters (1)
  • pkg/agent/tunnel/tunnel.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (25)
  • cmd/workspace/task.go
  • cmd/workspace/up/detach.go
  • cmd/workspace/up/status.go
  • cmd/workspace/up/up.go
  • desktop/e2e/fixtures/mock-devsy.cjs
  • desktop/e2e/workspaces.e2e.ts
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • desktop/src/main/ipc.ts
  • desktop/src/renderer/src/lib/types/index.ts
  • pkg/agent/tunnel/tunnel.proto
  • pkg/agent/tunnelserver/options.go
  • pkg/client/client.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/client/clientimplementation/workspace_client_status_test.go
  • pkg/config/pathmanager.go
  • pkg/devcontainer/config/envelope.go
  • pkg/devcontainer/feature/extend.go
  • pkg/devcontainer/feature/lockfile.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/setup.go
  • pkg/status/log.go
  • pkg/status/status.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_test.go
💤 Files with no reviewable changes (4)
  • pkg/agent/tunnel/tunnel.proto
  • cmd/workspace/up/up.go
  • pkg/config/pathmanager.go
  • pkg/devcontainer/setup.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • pkg/status/log.go
  • desktop/src/renderer/src/lib/types/index.ts
  • pkg/devcontainer/config/envelope.go
  • cmd/workspace/up/detach.go
  • pkg/client/client.go
  • pkg/devcontainer/feature/lockfile.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/feature/extend.go
  • pkg/status/status.go

Comment thread pkg/task/store.go Outdated
Comment thread pkg/task/task.go
Comment on lines +17 to +20
var (
ErrCanceled = errors.New("canceled")
ErrAbandoned = errors.New("worker exited without recording a result")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate ErrAbandoned declaration.

pkg/task/store.go:19 already declares the package-level ErrAbandoned identifier. Defining it again here causes a Go compile error; retain a single declaration in the task package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/task/task.go` around lines 17 - 20, Remove the duplicate ErrAbandoned
declaration from the package-level var block in task.go, preserving the existing
declaration in store.go and retaining ErrCanceled unchanged.

@skevetter
skevetter marked this pull request as draft July 30, 2026 01:01
PID-based liveness had two problems: a recycled PID makes a dead worker
look alive, so an abandoned task could still be reported as running; and
command.IsRunning panics on Windows, so Abandoned would crash there.

Have the worker hold an exclusive flock on <id>.json.worker.lock for its
lifetime instead. The kernel releases it however the process ends, so
liveness becomes "is the lock acquirable" — no PID to be reused, and it
works on every platform. Separate from the read-modify-write lock in
withLock, which is held only per update. HoldWorkerLock also rejects a
second worker for the same task.

Also from review: the log follower exiting no longer releases the task.
The follower dying says nothing about the detached worker, and releasing
there left a live task with no id to cancel it by.

Restores the podman reachability probe in pkg/compose, dropped by an
earlier comment cleanup; without it the test asserts against whichever
compose backend the helper falls back to instead of skipping.
Abandoned released the worker lock before Reconcile wrote ErrAbandoned. A
replacement worker could claim it in that window and then be marked failed
while actually running — withLock guards state writes, not this lock, so it
could not prevent that.

Reconcile now claims the lock itself and holds it through the write, and
re-reads state under it so a result the worker recorded in the meantime
stands instead of being overwritten.

The regression test needs a seam: the window is between two internal calls,
so a test can only act inside it via a hook. Without one the test passes
against the buggy version, which is worse than no test.

Also release worker locks via t.Cleanup so an fd stays closed when an
assertion fails, which can otherwise block TempDir removal.
write() renamed a temp file without syncing it or the directory. rename(2)
stops a reader seeing a torn file, but after a crash the entry could point
at unflushed data — and a task that loses its terminal update reads as
permanently in-flight, which is exactly the state reconciliation exists to
avoid.

Sync the temp file before the rename and the directory after, matching
pkg/secrets/atomicwrite.go. pkg/provider's helper deliberately skips the
directory sync because its callers re-resolve on the next run; task state
has no such fallback.

Costs ~8ms per update, so ~160ms across a workspace up's ~20 status
events — immaterial against a multi-minute provision, and the writes were
already serialized under the per-task lock.
@skevetter
skevetter marked this pull request as ready for review July 30, 2026 04:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/task/export_test_helpers.go (1)

1-17: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Rename file so it's excluded from production builds.

export_test_helpers.go does not end in _test.go, so Go compiles it into every regular build — ReleaseWorkerLockForTest() and SetAfterClaimForTest() (which force-drop a held worker lock / inject a callback into Reconcile's locked critical section) ship as exported production API, not gated behind go test. These directly bypass the worker-lock exclusivity this PR relies on to prevent duplicate detached workers.

🔒 Proposed fix — use the real `_test.go` suffix
-pkg/task/export_test_helpers.go
+pkg/task/export_test.go
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/task/export_test_helpers.go` around lines 1 - 17, Rename
export_test_helpers.go to a filename ending in _test.go so
ReleaseWorkerLockForTest and SetAfterClaimForTest are compiled only during tests
and excluded from production builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/compose/helper_test.go`:
- Line 191: Update the reachability probe in the affected test to use the
existing testPodmanCmd symbol instead of the hardcoded "podman" executable,
while preserving the current compose version check and skip/failure behavior.

---

Outside diff comments:
In `@pkg/task/export_test_helpers.go`:
- Around line 1-17: Rename export_test_helpers.go to a filename ending in
_test.go so ReleaseWorkerLockForTest and SetAfterClaimForTest are compiled only
during tests and excluded from production builds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 890d35d8-39f8-47e4-adba-cb41be6f9c97

📥 Commits

Reviewing files that changed from the base of the PR and between 34ea02f and 32fe61f.

📒 Files selected for processing (12)
  • cmd/workspace/up/detach.go
  • cmd/workspace/up/up.go
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • desktop/src/main/ipc.ts
  • pkg/client/clientimplementation/workspace_client_status_test.go
  • pkg/compose/helper_test.go
  • pkg/config/pathmanager.go
  • pkg/devcontainer/config/envelope.go
  • pkg/task/export_test_helpers.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_test.go
💤 Files with no reviewable changes (1)
  • pkg/devcontainer/config/envelope.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/workspace/up/up.go
  • pkg/config/pathmanager.go

Comment thread pkg/compose/helper_test.go Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant