Skip to content

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

Merged
skevetter merged 7 commits into
mainfrom
feat/async-workspace-up
Aug 1, 2026
Merged

feat(workspace): support async, durable workspace up execution#798
skevetter merged 7 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 detached workspace provisioning with task tracking and task management commands for listing, viewing, monitoring, canceling, and removing tasks.
    • Added live provisioning status updates across CLI, desktop, and remote workflows.
    • Added workspace status events for desktop UI integration.
    • Workspace status now reflects active provisioning and failed tasks.
  • Bug Fixes

    • Improved handling of abandoned tasks and process termination.
    • Reduced race conditions when starting, canceling, or monitoring workspace tasks.
    • Improved persistence reliability for task state.

@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for images-devsy-sh canceled.

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

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17427c8b-65ee-4d90-b5fa-3be6ec2a3432

📥 Commits

Reviewing files that changed from the base of the PR and between 5a67e40 and 2158976.

📒 Files selected for processing (12)
  • cmd/workspace/task.go
  • cmd/workspace/task_test.go
  • cmd/workspace/up/detach.go
  • desktop/e2e/fixtures/mock-devsy.cjs
  • pkg/command/process_test.go
  • pkg/devcontainer/config/envelope.go
  • pkg/devcontainer/config/envelope_test.go
  • pkg/status/log.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_pid_supported_test.go
  • pkg/task/task_test.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • pkg/status/log.go
  • cmd/workspace/up/detach.go
  • cmd/workspace/task.go
  • cmd/workspace/task_test.go
  • pkg/task/task_pid_supported_test.go
  • pkg/task/task.go
  • pkg/command/process_test.go
  • pkg/task/store.go
  • pkg/task/task_test.go
  • pkg/devcontainer/config/envelope.go
  • desktop/e2e/fixtures/mock-devsy.cjs

📝 Walkthrough

Walkthrough

Workspace provisioning now supports persistent detached tasks, structured lifecycle status, tunnel and client propagation, and desktop IPC task streaming. Task commands support listing, retrieval, logs, cancellation, and removal. Supporting changes add process handling and concurrent feature resolution.

Changes

Workspace task execution and status reporting

Layer / File(s) Summary
Task persistence and worker reconciliation
pkg/task/*, pkg/config/pathmanager.go
Tasks persist lifecycle state, use worker locks, reconcile abandoned workers, and write state durably.
Detached workspace execution and task commands
cmd/workspace/task.go, cmd/workspace/up/*, cmd/workspace/workspace.go, pkg/flags/names/names.go
Workspace up supports detached execution, task reopening, polling, status reporting, cancellation, removal, and JSON or text output.
Workspace status overrides
pkg/client/clientimplementation/workspace_client.go, pkg/client/clientimplementation/workspace_client_status_test.go
Workspace status uses the newest relevant active or failed up task for provisioning and failure overrides.

Structured status propagation

Layer / File(s) Summary
Status contracts and tunnel transport
pkg/status/*, pkg/devcontainer/config/envelope.go, pkg/client/client.go, pkg/agent/tunnel/*
Status events, NDJSON envelopes, client reporter options, tunnel status RPCs, and asynchronous tunnel reporting are defined.
Devcontainer and client reporting
pkg/devcontainer/*, pkg/client/clientimplementation/daemonclient/*, cmd/internal/*, cmd/workspace/up/*
Lifecycle reporters are propagated through runner, setup, daemon, proxy, machine, SSH, and container startup paths. Daemon output separates status envelopes from ordinary output.

Desktop detached flow

Layer / File(s) Summary
Detached CLI fixture and IPC orchestration
desktop/e2e/fixtures/mock-devsy.cjs, desktop/src/main/ipc.ts, desktop/src/shared/cli-error.ts, desktop/src/renderer/src/lib/ipc/events.ts, desktop/src/renderer/src/lib/types/index.ts
Detached submission returns task IDs, task logs produce terminal envelopes, IPC serializes workspace operations, and renderer status events are emitted.
Desktop validation
desktop/src/main/__tests__/*, desktop/e2e/workspaces.e2e.ts
Tests cover cancellation, serialization, status forwarding, terminal cleanup, follower failures, and updated status text.

Supporting execution changes

Layer / File(s) Summary
Concurrent feature resolution and process termination
pkg/devcontainer/feature/*, pkg/command/*, pkg/compose/helper_test.go
Feature resolution runs concurrently with synchronized lockfile updates. Process tests cover termination and exited processes. Compose tests skip when Podman Compose is unreachable.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.35% 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 and concisely describes the main change: asynchronous and durable execution for workspace up.
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.

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 2158976
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a6d3c57b268e80008ef09b2

@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 thread pkg/command/process_supported.go
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
@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
@skevetter
skevetter marked this pull request as ready for review July 29, 2026 23:17

@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
@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
@skevetter
skevetter marked this pull request as draft July 30, 2026 01:01
@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
@skevetter
skevetter marked this pull request as draft July 30, 2026 14:15
@skevetter
skevetter marked this pull request as ready for review July 30, 2026 19:39

@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.

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)

821-832: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle log-follower startup failures after detached submission.

cli.runStreaming(...) runs outside the existing try. If it rejects, the background task has already been created, but the handler rejects without completing sink, leaving the log open and the task tracked without a follower process. Catch this failure, complete the sink with an error, and retain the task ID for later cancellation/reconciliation.

🛠️ Proposed fix
         activeUpTasks.set(wsId, taskId)

-        const child = await cli.runStreaming(
-          ["workspace", "task", "logs", taskId, "--follow"],
-          ...
-        )
+        let child
+        try {
+          child = await cli.runStreaming(
+            ["workspace", "task", "logs", taskId, "--follow"],
+            ...
+          )
+        } catch (error) {
+          const err = error as Error & { cliError?: CLIError }
+          void sink.done(formatLogLine(err.message, "ERROR"), {
+            level: "error",
+            cliError: err.cliError ?? {
+              code: "up_follow_failed",
+              message: err.message,
+            },
+          })
+          return cmdId
+        }
🤖 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 821 - 832, Handle rejection from
cli.runStreaming in the task-log handler after detached submission: catch
startup failures, complete sink with the error, and preserve the submitted
taskId for later cancellation or reconciliation. Keep the releaseTask ownership
guard intact and ensure activeUpTasks is not cleared when no follower process
starts.
🤖 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.

Outside diff comments:
In `@desktop/src/main/ipc.ts`:
- Around line 821-832: Handle rejection from cli.runStreaming in the task-log
handler after detached submission: catch startup failures, complete sink with
the error, and preserve the submitted taskId for later cancellation or
reconciliation. Keep the releaseTask ownership guard intact and ensure
activeUpTasks is not cleared when no follower process starts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 51533f7c-f14a-4e51-8efd-52eb1d4f4329

📥 Commits

Reviewing files that changed from the base of the PR and between 32fe61f and 3ee2366.

📒 Files selected for processing (7)
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • desktop/src/main/ipc.ts
  • pkg/agent/tunnelserver/options.go
  • pkg/compose/helper_test.go
  • pkg/task/export_test.go
  • pkg/task/store.go
  • pkg/task/task.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/agent/tunnelserver/options.go
  • pkg/compose/helper_test.go

@skevetter
skevetter marked this pull request as draft July 30, 2026 19:46
@skevetter
skevetter force-pushed the feat/async-workspace-up branch from 3ee2366 to 4206641 Compare July 30, 2026 19:58
@skevetter
skevetter marked this pull request as ready for review July 31, 2026 14:46

@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.

🧹 Nitpick comments (2)
cmd/workspace/task.go (1)

51-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider using the same envelope contract as task get/task logs.

taskListCmd.run JSON-encodes states ([]*task.State) directly with json.NewEncoder(os.Stdout).Encode(states). Every other JSON output path in this file (reportTaskStateJSON, used by task get, task logs, indirectly task cancel) goes through config2.WriteStatusJSON/WriteResultJSON/WriteErrorJSON. Exposing task.State's internal field names directly as the task list --output json contract diverges from the rest of the command group and makes the list output's shape change silently whenever task.State's fields change.

Wrap each state with the same envelope helpers (or a list-specific envelope) used elsewhere in this file.

🤖 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 `@cmd/workspace/task.go` around lines 51 - 73, Update taskListCmd.run’s
emitJSON branch to use the established config2 status/result envelope helpers,
matching reportTaskStateJSON and the task get/logs/cancel JSON contract instead
of encoding []*task.State directly. Preserve the existing state retrieval and
human-readable output paths, and ensure each listed state is represented through
the shared envelope or an equivalent list-specific envelope.
pkg/status/status.go (1)

43-48: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Preserve step context for named status.Fail failures.

Current status.Enter calls pass an empty step, but failures now report Step: string(phase). If multi-step phases such as PhaseRunningLifecycleHook or PhaseWaitingFor report meaningful sub-steps later, status.Fail should accept a step argument and reporters should keep failing that diagnostic path, including consumers like cmd/workspace/up/status.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/status/status.go` around lines 43 - 48, Update status.Fail to accept a
step argument and use it for Event.Step instead of deriving the step from phase.
Preserve the existing PhaseFailed and error reporting behavior, then update all
status.Fail callers, including cmd/workspace/up/status.go, to pass the relevant
step context while keeping existing Enter calls compatible.
🤖 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.

Nitpick comments:
In `@cmd/workspace/task.go`:
- Around line 51-73: Update taskListCmd.run’s emitJSON branch to use the
established config2 status/result envelope helpers, matching reportTaskStateJSON
and the task get/logs/cancel JSON contract instead of encoding []*task.State
directly. Preserve the existing state retrieval and human-readable output paths,
and ensure each listed state is represented through the shared envelope or an
equivalent list-specific envelope.

In `@pkg/status/status.go`:
- Around line 43-48: Update status.Fail to accept a step argument and use it for
Event.Step instead of deriving the step from phase. Preserve the existing
PhaseFailed and error reporting behavior, then update all status.Fail callers,
including cmd/workspace/up/status.go, to pass the relevant step context while
keeping existing Enter calls compatible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bcc0bde-3b0d-4714-a5be-8970657972eb

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee2366 and 4206641.

⛔ 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 (47)
  • 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/e2e/workspaces.e2e.ts
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • 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/flags/names/names.go
  • pkg/status/log.go
  • pkg/status/status.go
  • pkg/status/status_test.go
  • pkg/task/export_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 (38)
  • cmd/internal/container_tunnel.go
  • desktop/e2e/workspaces.e2e.ts
  • cmd/internal/agentworkspace/up.go
  • pkg/devcontainer/feature/lockfile.go
  • cmd/workspace/up/agent.go
  • pkg/devcontainer/single.go
  • desktop/src/renderer/src/lib/ipc/events.ts
  • pkg/config/pathmanager.go
  • cmd/workspace/up/up_flags.go
  • pkg/devcontainer/setup.go
  • pkg/agent/tunnelserver/status_sender.go
  • pkg/compose/helper_test.go
  • pkg/devcontainer/build.go
  • pkg/agent/tunnel/tunnel.proto
  • pkg/client/clientimplementation/daemonclient/stop.go
  • cmd/workspace/task_test.go
  • cmd/workspace/up/detach_test.go
  • pkg/command/process_supported.go
  • pkg/flags/names/names.go
  • pkg/command/process_test.go
  • desktop/src/shared/cli-error.ts
  • cmd/workspace/up/up.go
  • pkg/client/clientimplementation/daemonclient/up_test.go
  • pkg/client/client.go
  • pkg/agent/tunnelserver/tunnelserver.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/feature/extend.go
  • pkg/client/clientimplementation/workspace_client.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • pkg/devcontainer/config/envelope.go
  • pkg/agent/tunnelserver/options.go
  • desktop/e2e/fixtures/mock-devsy.cjs
  • cmd/workspace/workspace.go
  • pkg/task/export_test.go
  • pkg/status/log.go
  • pkg/status/status_test.go
  • pkg/task/task.go
  • pkg/task/store.go

@skevetter
skevetter marked this pull request as draft July 31, 2026 20:50
@skevetter
skevetter marked this pull request as ready for review July 31, 2026 20:50
Adds a submit/poll durable execution model alongside the existing
synchronous flow:

- pkg/task: a JSON-file task store (atomic writes, cross-process
  flock, worker-held liveness lock) 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 to
  manage submitted tasks.
- pkg/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.

Squashed from the individual commits on this branch, rebased onto main.
@skevetter
skevetter force-pushed the feat/async-workspace-up branch from 4206641 to 5a67e40 Compare July 31, 2026 22:47

@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

🤖 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/task_pid_supported_test.go`:
- Around line 38-74: The test TestCancelDoesNotSignalAProcessThatReusedThePID
exceeds the cyclop complexity limit because of its inline process-wait
assertion. Extract the final select that waits on exited into a small t.Helper
function, then call that helper from the test while preserving the existing
failure and timeout 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: cc86f524-b894-4259-ba4c-e3e45b8eaba7

📥 Commits

Reviewing files that changed from the base of the PR and between 4206641 and 5a67e40.

⛔ 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 (48)
  • 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/e2e/workspaces.e2e.ts
  • desktop/src/main/__tests__/ipc-up-tasks.test.ts
  • 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/flags/names/names.go
  • pkg/status/log.go
  • pkg/status/status.go
  • pkg/status/status_test.go
  • pkg/task/export_test.go
  • pkg/task/store.go
  • pkg/task/task.go
  • pkg/task/task_pid_supported_test.go
  • pkg/task/task_test.go
🚧 Files skipped from review as they are similar to previous changes (44)
  • desktop/e2e/workspaces.e2e.ts
  • cmd/internal/agentworkspace/up.go
  • cmd/internal/container_tunnel.go
  • cmd/workspace/up/up_flags.go
  • cmd/workspace/up/agent.go
  • pkg/devcontainer/single.go
  • pkg/devcontainer/feature/lockfile.go
  • pkg/task/export_test.go
  • pkg/status/log.go
  • pkg/devcontainer/build.go
  • pkg/flags/names/names.go
  • pkg/command/process_supported.go
  • cmd/workspace/up/detach.go
  • pkg/config/pathmanager.go
  • cmd/workspace/up/status.go
  • cmd/workspace/up/detach_test.go
  • desktop/e2e/fixtures/mock-devsy.cjs
  • pkg/task/task.go
  • pkg/compose/helper_test.go
  • pkg/agent/tunnelserver/status_sender.go
  • pkg/client/clientimplementation/daemonclient/stop.go
  • pkg/devcontainer/setup.go
  • cmd/workspace/task.go
  • desktop/src/main/tests/ipc-up-tasks.test.ts
  • pkg/agent/tunnelserver/options.go
  • pkg/command/process_test.go
  • cmd/workspace/up/up.go
  • pkg/agent/tunnel/tunnel.proto
  • desktop/src/shared/cli-error.ts
  • pkg/client/client.go
  • pkg/status/status_test.go
  • desktop/src/renderer/src/lib/types/index.ts
  • pkg/client/clientimplementation/daemonclient/up_test.go
  • cmd/workspace/workspace.go
  • pkg/client/clientimplementation/workspace_client.go
  • desktop/src/renderer/src/lib/ipc/events.ts
  • pkg/agent/tunnelserver/tunnelserver.go
  • pkg/client/clientimplementation/workspace_client_status_test.go
  • pkg/status/status.go
  • pkg/task/store.go
  • pkg/task/task_test.go
  • pkg/client/clientimplementation/daemonclient/up.go
  • pkg/devcontainer/run.go
  • pkg/devcontainer/config/envelope.go

Comment thread pkg/task/task_pid_supported_test.go
@skevetter
skevetter marked this pull request as draft July 31, 2026 22:51
Fixes still open after the squash/rebase (most other review findings
were already addressed by the branch's prior commits):

- Task.Cancel: skip signaling by PID when the task's worker lock is
  free, so a dead worker's recycled PID can't be handed to an
  unrelated process.
- taskReporter.Report: a PhaseFailed event now sets Status/Phase/Step,
  not just Error, so the task is actually left terminal.
- workspace task rm: emit a JSON result payload; failed-task messages
  never render blank.
- workspace up detach: record HoldWorkerLock/SetPID failures on the
  task instead of leaving it stuck non-terminal.
- process_test.go: restrict to the platforms process_supported.go
  supports.
- pkg/status/log.go: include Event.Step in non-failure log lines.
- mock-devsy.cjs: match the real CLI's task existence/force/terminal
  semantics for cancel, rm, and logs, and recognize --detach=true/-d.
@skevetter
skevetter force-pushed the feat/async-workspace-up branch from 5a67e40 to 337acf2 Compare July 31, 2026 23:08
…for task list

- Revert the Fprintln(Sprintf(...)) experiment flagged by staticcheck
  (S1038) back to plain Fprintf.
- workspace task list now renders plain output via pkg/table, and JSON
  output via json.MarshalIndent, matching every other 'list' command
  (cmd/ide/list.go, cmd/pro/list.go, etc.) instead of a hand-rolled
  tab-separated line and a compact encoder. Safe here because desktop's
  cli.run() JSON.parse()s the whole captured stdout rather than reading
  NDJSON line-by-line; that per-line contract only applies to the
  status/result/error envelope writers used during streaming, which
  this command doesn't touch.
A line matching {"kind":"status",...} but missing phase or started
isn't one of ours, yet was accepted as a valid event, which could
zero out a task's Phase or spuriously flip it Pending -> Running.
Reachable only if unrelated output sharing the same stream (build/pull
logs, etc.) happens to produce such a line, but the check is cheap.

Started becomes *bool on the wire so an omitted field is
distinguishable from an explicit false; WriteStatusJSON always sets it,
so legitimate output is unaffected.
@skevetter
skevetter marked this pull request as ready for review August 1, 2026 01:10
@skevetter
skevetter merged commit 76910ca into main Aug 1, 2026
67 of 68 checks passed
@skevetter
skevetter deleted the feat/async-workspace-up branch August 1, 2026 01:38
skevetter added a commit that referenced this pull request Aug 1, 2026
Adding a provider showed a red "not initialized" badge for several
seconds before flipping to "initialized". provider add --use=false
persists the provider to config.yaml before init runs, and
parseProviderEntries collapsed "not yet known" and "genuinely not
initialized" into the same false, so the card rendered the destructive
badge for both.

Two lifecycle axes were collapsed into one boolean. They're now
separate:

- Persisted truth stays Initialized bool on disk: "did this binary run
  Exec.Init". No schema change, no migration.
- Transient job state moves to a main-process ProviderJobs registry,
  fed by the CLI's structured status events and broadcast on the
  existing providers-changed channel. Main-process ownership means it
  survives the wizard closing, navigation, and window reload.

This also distinguishes states the flag cannot: installing vs
initializing vs updating, and failed vs never-initialized.

Also included:

- provider add/init/set-source emit structured progress (pkg/status
  Phase/Event/Reporter, as workspace up does), replacing the desktop's
  "Exit code: 0" string-sniffing. pkg/devcontainer/status ->
  pkg/status, since it's no longer devcontainer-specific.
- Correctness fix: set-source left Initialized stale, so a provider
  updated to a new binary still reported initialized: true though the
  new binary's init never ran. Update and version-pin now chain
  provider init.
- runStreaming hang: it registered close but not error, so a spawn
  failure never invoked onExit, hanging callers and leaking the
  concurrency slot. Affected every streaming command, not just
  providers.

Squashed from the individual commits on this branch, rebased onto main
after #798 merged. Conflicts resolved by keeping both branches'
independent fixes (e.g. task PID-reuse guard, envelope validation)
alongside this branch's new Pipeline/ProviderJobs work.
skevetter added a commit that referenced this pull request Aug 1, 2026
)

* fix(provider): represent provider install/init lifecycle in the UI

Adding a provider showed a red "not initialized" badge for several
seconds before flipping to "initialized". provider add --use=false
persists the provider to config.yaml before init runs, and
parseProviderEntries collapsed "not yet known" and "genuinely not
initialized" into the same false, so the card rendered the destructive
badge for both.

Two lifecycle axes were collapsed into one boolean. They're now
separate:

- Persisted truth stays Initialized bool on disk: "did this binary run
  Exec.Init". No schema change, no migration.
- Transient job state moves to a main-process ProviderJobs registry,
  fed by the CLI's structured status events and broadcast on the
  existing providers-changed channel. Main-process ownership means it
  survives the wizard closing, navigation, and window reload.

This also distinguishes states the flag cannot: installing vs
initializing vs updating, and failed vs never-initialized.

Also included:

- provider add/init/set-source emit structured progress (pkg/status
  Phase/Event/Reporter, as workspace up does), replacing the desktop's
  "Exit code: 0" string-sniffing. pkg/devcontainer/status ->
  pkg/status, since it's no longer devcontainer-specific.
- Correctness fix: set-source left Initialized stale, so a provider
  updated to a new binary still reported initialized: true though the
  new binary's init never ran. Update and version-pin now chain
  provider init.
- runStreaming hang: it registered close but not error, so a spawn
  failure never invoked onExit, hanging callers and leaking the
  concurrency slot. Affected every streaming command, not just
  providers.

Squashed from the individual commits on this branch, rebased onto main
after #798 merged. Conflicts resolved by keeping both branches'
independent fixes (e.g. task PID-reuse guard, envelope validation)
alongside this branch's new Pipeline/ProviderJobs work.

* fix(desktop): close remaining gaps from CodeRabbit review

Confirmed real by verification, not just the review's say-so:

- watcher.ts: refreshProviders() called pollProviders() directly,
  bypassing the polling/pollQueued coordination schedulePoll uses. A
  manual refresh (e.g. after an install finishes) could run
  concurrently with a scheduled poll, and whichever CLI call landed
  last could overwrite the other's result — reintroducing the same
  "stale state wins" bug class this PR exists to fix. Both paths now
  go through a serializing queue scoped to provider polls. Added a
  unit test that fails against the old code (proved by reverting
  locally) and passes with the fix.
- ipc.ts: runProviderWithStatus discarded cli.runStreaming's promise
  with `void`, so a rejection from a failure before onExit is wired up
  would hang the caller forever instead of surfacing. Low probability
  given runStreaming's own error handling, but a zero-risk safety net.
- providers.e2e.ts: wrapped both provider lifecycle tests' assertions
  in try/finally so a failed assertion still runs the provider_delete
  cleanup, instead of leaking dirty mock CLI state into later specs.

Skipped two other findings from the same review after tracing actual
consumers:
- cmd/provider/add.go's status.Enter uses the pre-resolution provider
  name (empty when --name is omitted). No current consumer (CLI plain
  text output, desktop's job tracking) reads that field for this
  event, so there's no observable effect to fix.
- provider-jobs.ts's generation tracking already prevents a delayed
  release from clobbering a newer job — verified via an existing test
  (`does not clear a newer job started while refresh was in flight`).
  The suggested renderer-side token would be redundant.

* style(desktop): trim a comment duplicating the field doc above it

* test(desktop): make the second watcher test assert ordering, not just count

The prior version asserted callCount === 2 after two concurrent
refreshProviders() calls, which passes whether or not they're
serialized — two concurrent queries and two queued ones both finish
two queries. Recording start/end order is what actually distinguishes
them; verified by reverting the fix locally and confirming this
version fails while the count-based one didn't.

* fix(desktop): stop finish() from misattributing errors and resurrecting cleared jobs

Both confirmed with reproductions before fixing (see the two new
tests, which fail against the prior code and pass against these
fixes):

- withProviderJob's success-path finish(name) call lived inside the
  try block, so a rejection from it (e.g. the refresh it awaits
  failing) was caught by the catch clause and re-reported via
  finish(name, <refresh error>) as if the operation itself had
  failed, discarding that fn() actually succeeded. Moved the
  success-path finish() outside the try/catch.
- ProviderJobs.finish()'s error branch wrote a new "failed" job entry
  unconditionally, even when the job had already been cleared (e.g.
  by a concurrent provider_delete). A delayed/stale error arriving
  after that point resurrected UI activity for a provider the user
  already considers gone — the same invariant report() already
  documents and enforces. Now returns early when the job is gone.

* fix(workspace): stream the detached worker's real log output over --follow

Since the async up model landed, the desktop no longer runs `workspace
up` directly and streams its output live — it submits `--detach` and
polls `workspace task logs --follow`. The worker's real stdout/stderr
get redirected to a *.streams file (pkg/command.StartBackground) that
nothing ever read back, so the UI only ever saw synthesized
phase-transition lines. Enabling debug mode had no effect because the
worker's actual log lines, at any level, never reached it.

followTask now tails that streams file each poll and forwards new
lines to stderr, skipping structured NDJSON envelopes (status/result/
error) since those are already reported from polled task state and
would otherwise show up twice. Verified with the CLI directly first
(confirmed --debug does emit debug-level lines, and confirmed nothing
read the streams file before this change), then with unit tests for
the tailer and an end-to-end test that runs followTask against a real
task while concurrently writing to its streams file.

* fix(desktop): show a Deleting badge on the Workspaces list while removal is in flight

Mirrors the ProviderJobs pattern with a WorkspaceJobs registry so the UI
reflects an in-progress delete instead of showing stale status until the
next poll picks up the CLI's exit.

* fix(desktop): close CodeRabbit-confirmed gaps in the workspace delete job

- WorkspaceJobs.finish() now requires the generation start() returned,
  so a stale exit callback can't misattribute its result to a retry
  that already superseded it.
- A failed delete no longer keeps the row disabled forever: it drops
  out of "deleting" and shows a dismissible "Delete failed" badge,
  matching the provider status convention.
- mock-devsy.cjs's delayed delete re-reads persisted state instead of
  writing back a stale in-memory snapshot, matching the existing
  provider-init fixture's pattern.
- workspaces.e2e.ts's intentionally unawaited delete call now swallows
  its rejection instead of risking an unhandled rejection.

* fix(workspace): satisfy gosec G304 on the tailer test's append-open

CI's golangci-lint run flagged the variable path passed to
os.OpenFile in the append-and-repoll test; it's t.TempDir()-derived,
same as the file's other os.WriteFile calls.

* style: trim verbose comments to their essential why

Several multi-line comments restated context already clear from
the surrounding code; condensed to one line each.

* fix(desktop): use the correct workspace_up param name in the delete e2e test

Passed "id" instead of "workspaceId", so the main process fell back
to the raw source URL as the log-path segment. Windows rejects the
colon in "https://", failing mkdir; Unix tolerates it, so this only
surfaced on the Windows CI runner.

* fix(desktop): stop the workspace-delete IPC test racing a real timer

waitForExitCallback() slept a fixed 10ms hoping the mock's
setTimeout(fn, 0) had already fired. Fake timers make the wait
deterministic instead.

* style: clean comments
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