Skip to content

fix: safe workspace delete and agh session remove command#239

Merged
pedronauck merged 5 commits into
compozy:mainfrom
igorcesarcode:fix/cli-session-workspace-improvements
Jun 2, 2026
Merged

fix: safe workspace delete and agh session remove command#239
pedronauck merged 5 commits into
compozy:mainfrom
igorcesarcode:fix/cli-session-workspace-improvements

Conversation

@igorcesarcode
Copy link
Copy Markdown
Contributor

@igorcesarcode igorcesarcode commented May 29, 2026

Summary

  • DeleteWorkspace is now transactional and active-session-safe: wraps the delete in a BEGIN IMMEDIATE transaction, checks for active sessions via a new listActiveSessionIDsByWorkspace helper, cascades deletion of stopped/orphaned sessions, and returns ErrWorkspaceHasActiveSessions (HTTP 409) when live sessions block the delete.
  • agh session remove <id>: new CLI subcommand that deletes a session and its persisted history through the daemon UDS client.
  • DeleteSession on DaemonClient: added to the interface and implemented on unixSocketClient.

Changes

File Change
internal/store/globaldb/global_db_workspace.go Transactional DeleteWorkspace with active-session guard and stopped-session cascade
internal/workspace/workspace.go Export ErrWorkspaceHasActiveSessions sentinel
internal/api/core/session_workspace.go Map ErrWorkspaceHasActiveSessions → HTTP 409
internal/cli/client.go Add DeleteSession to DaemonClient interface + implementation
internal/cli/session.go Add session remove subcommand
*_test.go Coverage for each new path

Test plan

  • agh session remove <stopped-session-id> — removes session and history, prints session info
  • agh workspace remove <id> with active sessions — returns 409 conflict, workspace untouched
  • agh workspace remove <id> with only stopped sessions — removes workspace and stopped sessions
  • make test passes
  • make lint passes (zero golangci-lint issues)

Summary by CodeRabbit

  • New Features

    • Added session remove command to permanently delete a session and display the removed session record.
    • Added open command to launch the daemon UI in the system browser.
  • Improvements

    • Removing a workspace now cascades and removes stopped sessions.
    • Workspace deletion is blocked when active sessions exist and returns a conflict (HTTP 409).
  • Tests

    • Added/updated unit and integration tests covering session removal and workspace-delete behavior.

Workspace delete now guards against active sessions: wraps the
operation in a transaction, checks for active sessions using a new
listActiveSessionIDsByWorkspace helper, cleans up stopped sessions
before removing the workspace row, and returns ErrWorkspaceHasActiveSessions
(HTTP 409) when live sessions block the delete.

Adds agh session remove <id> CLI command that deletes a session and
its persisted history via the daemon UDS client.

- globaldb: transactional DeleteWorkspace with active-session guard
- workspace: export ErrWorkspaceHasActiveSessions sentinel error
- api/core: map ErrWorkspaceHasActiveSessions to HTTP 409
- cli/client: add DeleteSession to DaemonClient interface and implement
  on unixSocketClient
- cli/session: add session remove subcommand
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

@igorcesarcode is attempting to deploy a commit to the Compozy Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd9e89b0-f7fe-46d8-a815-9975bfc4fc9f

📥 Commits

Reviewing files that changed from the base of the PR and between cd37d18 and cbb0818.

📒 Files selected for processing (3)
  • internal/api/core/workspaces.go
  • internal/api/httpapi/handlers_error_test.go
  • internal/api/httpapi/handlers_test.go

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

Blocks workspace deletion when active sessions exist (new ErrWorkspaceHasActiveSessions), cascades deletion of stopped sessions, maps the sentinel to HTTP 409, adds CLI DeleteSession and agh session remove, and provides an open CLI command to launch the daemon URL.

Changes

Session deletion and workspace cascade cleanup

Layer / File(s) Summary
Error contract for active session blocking
internal/workspace/workspace.go, internal/workspace/workspace_test.go
New exported sentinel ErrWorkspaceHasActiveSessions and tests for errors.Is matching and distinctness from existing session errors.
Database workspace deletion with session cascading
internal/store/globaldb/global_db_workspace.go, internal/store/globaldb/global_db_test.go
DeleteWorkspace runs in a transaction, checks for active sessions (errors with ErrWorkspaceHasActiveSessions when present), deletes stopped sessions, deletes workspace row, and adds helper to list active session IDs; DB tests updated for cascade and active-session rejection.
API core: delete workspace orchestration
internal/api/core/workspaces.go, internal/api/core/memory_workspace_test.go
DeleteWorkspace precomputes stopped session IDs, errors if active sessions exist, unregisters workspace, deletes stopped sessions via helper, and test stubs updated to use stopped session state where appropriate.
HTTP API error status mapping & handler tests
internal/api/core/session_workspace.go, internal/api/core/session_workspace_internal_test.go, internal/api/httpapi/handlers_error_test.go, internal/api/httpapi/handlers_test.go
Map ErrWorkspaceHasActiveSessions to HTTP 409 Conflict; add tests asserting 409 for active sessions, verify stopped-session deletion ordering, and assert empty 204 response bodies.
CLI DeleteSession transport method
internal/cli/client.go, internal/cli/helpers_test.go
Add DeleteSession(ctx, id) to DaemonClient, implement unixSocketClient.DeleteSession to issue HTTP DELETE, and extend stub client with deleteSessionFn.
CLI session remove command
internal/cli/session.go, internal/cli/session_test.go
New agh session remove <id> subcommand: validates id, loads session, calls client.DeleteSession, and prints removed session bundle; test verifies delete call received id and JSON output.
CLI open command
internal/cli/open.go, internal/cli/root.go
New open subcommand queries daemon status for host:port and launches the system browser using OS-specific commands.
CLI integration tests and helpers
internal/cli/cli_integration_test.go
Integration test exercising session removal and workspace removal with filesystem assertions; helpers added for session creation and path existence checks.
Memory workspace test update
internal/api/core/memory_workspace_test.go
Stub session state updated to session.StateStopped for handler tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • compozy/agh#7: Both PRs touch the workspace HTTP error-status mapping layer—statusForWorkspaceError and its tests—so they relate to mapping workspace active-session errors to HTTP 409.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.13% 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 two main changes: making workspace deletion safe by checking for active sessions, and adding the new session remove CLI command.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@internal/cli/session_test.go`:
- Around line 616-654: Wrap the body of TestSessionRemoveDeletesSession in a
t.Run subtest using the "Should ..." naming convention (e.g., t.Run("Should
delete session", func(t *testing.T) { ... })), preserving the existing setup and
assertions (including variables like deletedID, the stubClient with
getSessionFn/deleteSessionFn, the call to executeRootCommand, and the JSON
unmarshal/asserts against SessionRecord); ensure you run t.Parallel() either
inside the subtest or keep it at the top-level per project convention and update
scope of variables accordingly so tests still compile and behave the same.

In `@internal/store/globaldb/global_db_workspace.go`:
- Around line 156-158: The deferred call defer func() { _ = rows.Close() }
ignores the Close error; update the defer to either handle or document why it's
safe to ignore by replacing the blank-assignment with a short justification
comment explaining that rows.Close() is on a read-only query result where Close
errors are not actionable (or that the caller cannot recover) and referencing
rows.Close() in the comment to satisfy the guideline about not ignoring errors
without justification; keep the defer but add the explanatory comment
immediately above it mentioning rows.Close() and the rationale.
- Around line 102-104: The deferred tx.Rollback() currently ignores its returned
error; add a brief justification comment immediately above or inside the
deferred func explaining why the error is intentionally ignored (e.g., rollback
is best-effort and will be a no-op after Commit, and any real error path will be
handled by the surrounding code), and keep the current _ = tx.Rollback()
pattern. Reference the deferred func and tx.Rollback() in global_db_workspace.go
so reviewers see the rationale inline.
- Line 98: DeleteWorkspace currently calls g.db.BeginTx(ctx, nil) which does not
acquire SQLite write lock immediately; update DeleteWorkspace to use the
immediate-transaction helpers instead — either wrap the delete logic in
GlobalDB.withImmediateTransaction or use store.ExecuteWrite so the helper opens
a BEGIN IMMEDIATE transaction, or explicitly call g.db.ExecContext(ctx, "BEGIN
IMMEDIATE") before performing the deletes and then use the returned tx for
Exec/Prepare/Commit; ensure you replace the direct BeginTx usage and reference
the same tx variable throughout the delete operations (function:
DeleteWorkspace; helpers: GlobalDB.withImmediateTransaction, store.ExecuteWrite,
or ExecContext("BEGIN IMMEDIATE")).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cca23c7a-a374-4580-945f-542da5a8de88

📥 Commits

Reviewing files that changed from the base of the PR and between cd4b155 and d82bf20.

📒 Files selected for processing (10)
  • internal/api/core/session_workspace.go
  • internal/api/core/session_workspace_internal_test.go
  • internal/cli/client.go
  • internal/cli/helpers_test.go
  • internal/cli/session.go
  • internal/cli/session_test.go
  • internal/store/globaldb/global_db_test.go
  • internal/store/globaldb/global_db_workspace.go
  • internal/workspace/workspace.go
  • internal/workspace/workspace_test.go

Comment thread internal/cli/session_test.go
Comment thread internal/store/globaldb/global_db_workspace.go Outdated
Comment thread internal/store/globaldb/global_db_workspace.go Outdated
Comment thread internal/store/globaldb/global_db_workspace.go Outdated
Adds `agh open` CLI command that fetches the daemon's HTTP address via
DaemonStatus and opens the web UI in the default system browser.
Also fixes errcheck lint violation in DeleteWorkspace deferred rollback.
…tion

- Replace BeginTx with store.ExecuteWrite (BEGIN IMMEDIATE with SQLITE_BUSY
  retry) in DeleteWorkspace for correct write lock acquisition
- Add justification comment for rows.Close() blank assignment in
  listActiveSessionIDsByWorkspace
- Wrap TestSessionRemoveDeletesSession body in t.Run("Should ...") subtest
  per project naming convention
@igorcesarcode
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@internal/cli/open.go`:
- Around line 33-34: The URL is built with fmt.Sprintf("http://%s:%d",
status.HTTPHost, status.HTTPPort) which breaks for IPv6; build the host:port
using net.JoinHostPort(status.HTTPHost, strconv.Itoa(status.HTTPPort)) (or
construct a url.URL and set Host via net.JoinHostPort) and then prepend the
scheme before calling openBrowser(ctx, url); update the code around the URL
construction in internal/cli/open.go where status.HTTPHost/status.HTTPPort are
used and pass the corrected url string to openBrowser.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: af3d6aac-71d0-43a4-b0df-4d14aefbf11e

📥 Commits

Reviewing files that changed from the base of the PR and between d82bf20 and 9f86427.

📒 Files selected for processing (4)
  • internal/cli/open.go
  • internal/cli/root.go
  • internal/cli/session_test.go
  • internal/store/globaldb/global_db_workspace.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/session_test.go
  • internal/store/globaldb/global_db_workspace.go

Comment thread internal/cli/open.go
Comment on lines +33 to +34
url := fmt.Sprintf("http://%s:%d", status.HTTPHost, status.HTTPPort)
return openBrowser(ctx, url)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and show the relevant portion of internal/cli/open.go
sed -n '1,120p' internal/cli/open.go | nl -ba | sed -n '1,120p'

# Show surrounding lines around the cited location (lines 25-60)
sed -n '20,70p' internal/cli/open.go | nl -ba

# Find where status.HTTPHost/HTTPPort are defined and what types they are
rg -n "HTTPHost|HTTPPort" internal | head -n 50

# Print the definitions for HTTPHost/HTTPPort if found
rg -n "HTTPHost" -S internal --glob='*.go' | head -n 20

# Search for URL formatting patterns elsewhere for consistency
rg -n "http://%s:%d|JoinHostPort\\(|url\\.URL|http://\\s*\\+" internal/cli internal | head -n 50

Repository: compozy/agh

Length of output: 98


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/cli/open.go (top) =="
cat -n internal/cli/open.go | head -n 160

echo "== internal/cli/open.go (around lines ~20-70) =="
sed -n '20,70p' internal/cli/open.go | cat -n

echo "== occurrences of HTTPHost / HTTPPort =="
rg -n "HTTPHost|HTTPPort" internal | head -n 80

echo "== definitions (HTTPHost token) =="
rg -n "HTTPHost" -S internal --glob='*.go' | head -n 80

echo "== URL formatting patterns elsewhere =="
rg -n "http://%s:%d|JoinHostPort\\(|url\\.URL|http://\\s*\\+" internal/cli internal | head -n 120

Repository: compozy/agh

Length of output: 14010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/daemon/settings.go around status HTTPHost/HTTPPort =="
sed -n '120,190p' internal/daemon/settings.go | cat -n

echo "== internal/api/core/status.go around HTTPHost/HTTPPort =="
sed -n '150,240p' internal/api/core/status.go | cat -n

echo "== internal/settings/models.go HTTPHost/HTTPPort fields =="
rg -n "type .*HTTPHost|HTTPHost[[:space:]]+string|HTTPPort[[:space:]]+int" internal/settings/models.go
sed -n '420,470p' internal/settings/models.go | cat -n

echo "== open.go current imports =="
sed -n '1,25p' internal/cli/open.go | cat -n

Repository: compozy/agh

Length of output: 7934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where HTTP.Host is set/validated (both config defaults and parsing)
rg -n "config\\.HTTP\\.Host|HTTP\\.Host|http\\.host|Host:\\s*.*HTTP" internal --glob='*.go' | head -n 120

# Specifically look for any bracket/IPv6 normalization around HTTP.Host
rg -n "JoinHostPort\\(|\\[.*\\]|IPv6|To16\\(|IsIP\\(|HasPrefix\\(\"\\[\"\\)" internal --glob='*.go' | head -n 120

# Show likely config HTTP host field usages
rg -n "HTTP\\.(Host|Port)" internal --glob='*.go' | head -n 120

# Also check contract fields: how daemon status HTTPHost is serialized
rg -n "http_host" -S internal --glob='*.go' | head -n 120

Repository: compozy/agh

Length of output: 29399


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/config/config.go around http.host validation =="
# Show around the reported line number 1216 for "http.host is required"
sed -n '1180,1260p' internal/config/config.go | cat -n

echo "== internal/api/httpapi/server.go around host normalization/binding =="
sed -n '500,620p' internal/api/httpapi/server.go | cat -n

echo "== internal/api/httpapi/handlers.go around boundHost assignment =="
sed -n '60,130p' internal/api/httpapi/handlers.go | cat -n

echo "== internal/api/httpapi/helpers_test.go mustURL for URL patterns =="
sed -n '680,760p' internal/api/httpapi/helpers_test.go | cat -n

Repository: compozy/agh

Length of output: 13295


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "serverLocalhostKey" internal/api/httpapi/server.go internal/api/httpapi/handlers.go internal/api/httpapi/*.go
rg -n "HTTP\\.Host\\s*=|http\\.host" internal/config/config.go internal/config/*.go internal/settings/*.go | head -n 120
rg -n "JoinHostPort\\(|\\bParseIP\\(|\\bIsIP\\(|\\[::|\\[.*\\]" internal/api/httpapi internal/config internal/settings internal/daemon internal/cli --glob='*.go' | head -n 200

Repository: compozy/agh

Length of output: 19569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '460,560p' internal/daemon/settings.go | cat -n

Repository: compozy/agh

Length of output: 4228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "\\[::|::1|IPv6" internal --glob='*.go' | head -n 200
rg -n "Trim\\(.*\"\\[\\]\"|Trim\\(.*\"\\[\\[\\]|strings\\.Trim\\(.*\"\\[\\]\"|Trim\\(.*\\[\\]|\"\\[\\]\"" internal --glob='*.go' | head -n 200
rg -n "Trim\\(\\s*strings\\.TrimSpace\\(.*\\),\\s*\"\\[\\]\"|Trim\\(\\s*strings\\.TrimSpace\\(.*\\),\\s*\"\\[\\]\"" internal --glob='*.go' | head -n 200

rg -n "net\\.JoinHostPort\\(|address := net\\.JoinHostPort" internal/api/httpapi/server.go internal/daemon/settings.go internal/cli/open.go | head -n 200

# Look for any status->URL formatting helper besides open.go
rg -n "http://\\%s:%d|http://\\s*\\+|url\\.URL\\{.*Host" internal/cli --glob='*.go' | head -n 200

Repository: compozy/agh

Length of output: 2652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/api/httpapi/server.go around address := net.JoinHostPort =="
sed -n '620,710p' internal/api/httpapi/server.go | cat -n

echo "== internal/api/httpapi/middleware.go around host normalization (strings.Trim(..., \"[]\")) =="
sed -n '180,280p' internal/api/httpapi/middleware.go | cat -n

echo "== internal/testutil/e2e/runtime_harness_test.go IPv6 loopback HTTP cases =="
sed -n '80,140p' internal/testutil/e2e/runtime_harness_test.go | cat -n

echo "== config HTTPConfig definition (type HTTPConfig) =="
rg -n "type HTTPConfig" internal/config --glob='*.go'

Repository: compozy/agh

Length of output: 9019


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "type HTTPConfig struct|HTTPConfig\\)|HTTPConfig\\b" internal/config/config.go
sed -n '70,140p' internal/config/config.go | cat -n
rg -n "Trim\\(.*Host.*\"\\[\\]\"|Trim\\(.*\"\\[\\]\".*Host|strings\\.Trim\\(.*host.*\"\\[\\]\"|strings\\.Trim\\(.*Host.*\"\\[\\]\"" internal/config --glob='*.go' | head -n 200
rg -n "http\\.host" internal/config --glob='*.go' | head -n 200

Repository: compozy/agh

Length of output: 3135


Build the open URL with net.JoinHostPort to handle IPv6 correctly.

internal/cli/open.go currently formats the address as http://%s:%d from status.HTTPHost/status.HTTPPort; for IPv6 like ::1 this produces an invalid URL. Construct the host:port portion with net.JoinHostPort (or url.URL) before calling openBrowser.

Suggested patch
 import (
 	"context"
 	"errors"
 	"fmt"
+	"net"
 	"os/exec"
 	"runtime"
+	"strconv"

 	"github.com/spf13/cobra"
 )
@@
-			url := fmt.Sprintf("http://%s:%d", status.HTTPHost, status.HTTPPort)
+			url := "http://" + net.JoinHostPort(status.HTTPHost, strconv.Itoa(status.HTTPPort))
 			return openBrowser(ctx, url)
🤖 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 `@internal/cli/open.go` around lines 33 - 34, The URL is built with
fmt.Sprintf("http://%s:%d", status.HTTPHost, status.HTTPPort) which breaks for
IPv6; build the host:port using net.JoinHostPort(status.HTTPHost,
strconv.Itoa(status.HTTPPort)) (or construct a url.URL and set Host via
net.JoinHostPort) and then prepend the scheme before calling openBrowser(ctx,
url); update the code around the URL construction in internal/cli/open.go where
status.HTTPHost/status.HTTPPort are used and pass the corrected url string to
openBrowser.

Copy link
Copy Markdown
Member

@pedronauck pedronauck left a comment

Choose a reason for hiding this comment

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

Automated review — self + Codex (gpt-5.5, xhigh)

  • Self verdict: FIX_BEFORE_SHIP (0 blockers, 1 risk, 2 nits)
  • Codex verdict: unavailable — the external Codex run could not be executed/validated in this environment (intermittent sandbox stdout/file-read drops made the side-effecting compozy exec subprocess untrustworthy). This is an environment failure, not a model error and not a "clean" result.

This PR already carries a human CHANGES_REQUESTED, so it is not an auto-merge candidate regardless.

Looks good (verified, no action)

  • The DELETE session surface is complete. BaseHandlers.DeleteSession (internal/api/core/handlers.go:466) exists and is registered on both httpapi/routes.go:119 and udsapi/routes.go:117 (workspaceSessions.DELETE("/:session_id")). The new CLI DeleteSession client + agh session remove command wire to a complete, pre-existing HTTP+UDS handler — surface is closed, not a blocker.
  • DeleteWorkspace is transactional via store.ExecuteWrite (BEGIN IMMEDIATE + bounded SQLITE_BUSY retry, internal/store/write.go:57,157), with an active-session guard returning ErrWorkspaceHasActiveSessions → HTTP 409 (internal/api/core/session_workspace.go:138-142). %w/errors.Is consistent; the single _ = rows.Close() carries a written justification; no SQLite column/index/constraint change, so no numbered migration is required.

Risks

  • Workspace-delete cascade may orphan per-session event stores. The cascade DELETE FROM sessions WHERE workspace_id = ? (internal/store/globaldb/global_db_workspace.go:112) removes only agh.db.sessions rows, but per-session events live in a separate events.db (internal/store/sessiondb). Unless the workspace-delete path also invokes the per-session cleanup that DeleteSession uses, deleting a workspace can orphan stopped sessions' event DBs/files. Fix: ensure the cascade reuses the full session-removal cleanup (events.db file + dir), or document the scope.

Nits

  • Misleading CLI help. newSessionRemoveCommand (internal/cli/session.go:229-254) lists two byte-identical examples, one captioned "Remove an active session (stops it first)", but RunE only does GetSessionDeleteSession and never stops the session. Fix: drop the duplicate/misleading example, actually stop active sessions first, or state that active sessions are rejected.
  • No CLI↔daemon contract/integration test for the new delete path (only a stub-client unit test in session_test.go). Fix: add an end-to-end test exercising agh session remove against the real handler.

The external Codex round will be re-run in a healthy environment to add its independent verdict.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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)
internal/api/core/workspaces.go (1)

228-285: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid deleting session history before the workspace delete commits.

DeleteWorkspace now removes stopped sessions before Unregister. If Unregister fails afterwards—especially on the race where a session becomes active after ListAll—the request returns an error but the stopped session artifacts are already gone. That leaves the workspace intact in one layer and partially deleted in another.

Please move the irreversible session cleanup to a post-commit step, or have the workspace layer return the stopped session IDs to clean up only after a successful unregister.

🤖 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 `@internal/api/core/workspaces.go` around lines 228 - 285, The current flow
deletes stopped sessions in deleteStoppedWorkspaceSessions before calling
Workspaces.Unregister, which can leave the system in a partial state if
Unregister fails; change the flow so deletion of stopped session artifacts
happens only after a successful workspace unregister (or move the deletion into
a post-commit step). Specifically, in the handler that calls
deleteStoppedWorkspaceSessions and Workspaces.Unregister (the DeleteWorkspace
flow), stop invoking Sessions.Delete before Unregister; instead, collect stopped
session IDs via Sessions.ListAll (reuse deleteStoppedWorkspaceSessions or
extract a new function that returns []string), call Workspaces.Unregister(ctx,
workspace.ID), and only upon successful Unregister iterate over the returned
stopped IDs and call Sessions.Delete, handling session.ErrSessionNotFound as
before; alternatively have the workspace layer return the stopped session IDs to
be cleaned up after Unregister.
🧹 Nitpick comments (2)
internal/api/httpapi/handlers_test.go (1)

1353-1361: ⚡ Quick win

Also assert the 204 response body is empty.

The ordering checks are solid, but this still leaves the HTTP contract half-covered. For a 204 No Content handler, please assert that the body is empty too.

As per coding guidelines, "Assert both HTTP status code AND response body in HTTP tests — status-code-only assertions are insufficient."

🤖 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 `@internal/api/httpapi/handlers_test.go` around lines 1353 - 1361, The test
currently checks only the status for the DELETE handler using performRequest and
recorder; add an assertion that the response body is empty for the 204 No
Content case by asserting recorder.Body.String() (or recorder.Body.Len()) is
empty after verifying recorder.Code == http.StatusNoContent in the same test
(the block that uses performRequest(t, engine, http.MethodDelete,
"/api/workspaces/ws_alpha", nil) and variable recorder).
internal/api/httpapi/handlers_error_test.go (1)

231-242: ⚡ Quick win

Assert the 409 response body, not just the status.

This new handler test verifies the branch, but it still leaves the error payload contract untested. Please decode the body and assert the expected conflict message as well.

As per coding guidelines, "Assert both HTTP status code AND response body in HTTP tests — status-code-only assertions are insufficient."

🤖 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 `@internal/api/httpapi/handlers_error_test.go` around lines 231 - 242, Update
the test to assert the 409 response body payload in addition to the status:
after performRequest(t, engine, http.MethodDelete, "/api/workspaces/ws_alpha",
nil) and the existing status check, decode resp.Body (e.g., into the error
response struct used by your handlers or a map[string]interface{}) and assert
the expected conflict message text (the same message your delete workspace
handler returns on active sessions). Keep the existing unregisterCalled check;
use the same response shape your handlers emit so the test validates both status
and body contract.
🤖 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 `@internal/api/core/workspaces.go`:
- Around line 228-285: The current flow deletes stopped sessions in
deleteStoppedWorkspaceSessions before calling Workspaces.Unregister, which can
leave the system in a partial state if Unregister fails; change the flow so
deletion of stopped session artifacts happens only after a successful workspace
unregister (or move the deletion into a post-commit step). Specifically, in the
handler that calls deleteStoppedWorkspaceSessions and Workspaces.Unregister (the
DeleteWorkspace flow), stop invoking Sessions.Delete before Unregister; instead,
collect stopped session IDs via Sessions.ListAll (reuse
deleteStoppedWorkspaceSessions or extract a new function that returns []string),
call Workspaces.Unregister(ctx, workspace.ID), and only upon successful
Unregister iterate over the returned stopped IDs and call Sessions.Delete,
handling session.ErrSessionNotFound as before; alternatively have the workspace
layer return the stopped session IDs to be cleaned up after Unregister.

---

Nitpick comments:
In `@internal/api/httpapi/handlers_error_test.go`:
- Around line 231-242: Update the test to assert the 409 response body payload
in addition to the status: after performRequest(t, engine, http.MethodDelete,
"/api/workspaces/ws_alpha", nil) and the existing status check, decode resp.Body
(e.g., into the error response struct used by your handlers or a
map[string]interface{}) and assert the expected conflict message text (the same
message your delete workspace handler returns on active sessions). Keep the
existing unregisterCalled check; use the same response shape your handlers emit
so the test validates both status and body contract.

In `@internal/api/httpapi/handlers_test.go`:
- Around line 1353-1361: The test currently checks only the status for the
DELETE handler using performRequest and recorder; add an assertion that the
response body is empty for the 204 No Content case by asserting
recorder.Body.String() (or recorder.Body.Len()) is empty after verifying
recorder.Code == http.StatusNoContent in the same test (the block that uses
performRequest(t, engine, http.MethodDelete, "/api/workspaces/ws_alpha", nil)
and variable recorder).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 00f8ea24-ea90-4e62-a370-017749629652

📥 Commits

Reviewing files that changed from the base of the PR and between 9f86427 and cd37d18.

⛔ Files ignored due to path filters (18)
  • packages/site/content/runtime/cli-reference/agh.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/onboarding/complete.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/onboarding/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/onboarding/meta.json is excluded by !**/*.json
  • packages/site/content/runtime/cli-reference/onboarding/reset.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/onboarding/status.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/open.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/scheduler/drain.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/session/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/session/meta.json is excluded by !**/*.json
  • packages/site/content/runtime/cli-reference/session/remove.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/task/fail.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/task/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/task/meta.json is excluded by !**/*.json
  • packages/site/content/runtime/cli-reference/task/recover.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/workspace/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/cli-reference/workspace/remove.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/resolver.mdx is excluded by !**/*.mdx
📒 Files selected for processing (7)
  • internal/api/core/memory_workspace_test.go
  • internal/api/core/workspaces.go
  • internal/api/httpapi/handlers_error_test.go
  • internal/api/httpapi/handlers_test.go
  • internal/cli/cli_integration_test.go
  • internal/cli/session.go
  • internal/cli/workspace.go
💤 Files with no reviewable changes (1)
  • internal/cli/session.go
✅ Files skipped from review due to trivial changes (2)
  • internal/api/core/memory_workspace_test.go
  • internal/cli/workspace.go

@pedronauck pedronauck merged commit 8cc8ff3 into compozy:main Jun 2, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants