Skip to content

fix: persist remote workspace records on create - #173

Merged
delete-cloud merged 1 commit into
mainfrom
fix/durable-workspace-record-on-create
May 15, 2026
Merged

delete-cloud merged 1 commit into
mainfrom
fix/durable-workspace-record-on-create

Conversation

@delete-cloud

@delete-cloud delete-cloud commented May 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add asyncpg as a declared dependency for PostgreSQL storage backends
  • persist durable remote workspace records when creating provisioned cloud workspace sessions
  • include provider instance/root/host metadata in remote session origin for durable retention
  • add regression coverage for provisioned vs explicit cloud bindings

Validation

  • uv run pytest tests/ui/test_session_manager_runtime.py -k "workspace_record or close_session or shutdown_session_runtime" -q
  • uv run pytest tests/ui/test_http_server.py tests/ui/test_session_persistence.py tests/coding_agent/environment -k "workspace_record or retention or provider_instance or pin or cleanup_failed or result_refs or workspaces" -q
  • uv run basedpyright --level error src/coding_agent/ui src/coding_agent/environment tests/ui/test_session_manager_runtime.py tests/ui/test_http_server.py
  • uv run ruff format --check src/coding_agent/ui/session_manager.py src/coding_agent/ui/http_server.py tests/ui/test_session_manager_runtime.py tests/ui/test_http_server.py
  • git diff --check
  • dogfood check: POST /sessions with docker workspace_source, then admin GET /workspaces shows the active durable record

Summary by CodeRabbit

  • New Features

    • Improved cloud workspace session metadata capture and persistence for enhanced session management.
  • Dependencies

    • Added asyncpg>=0.30.0 for database operations.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 15, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Session creation now persists cloud workspace metadata by enriching origin fields in the HTTP server, defining a protocol contract for persistence storage, implementing persistence logic in SessionManager before owner lease acquisition, and validating the flow through updated test infrastructure.

Changes

Cloud Workspace Record Persistence

Layer / File(s) Summary
Workspace metadata store protocol
src/coding_agent/ui/session_manager.py
WorkspaceMetadataStoreProtocol defines a required async save() method for persisting WorkspaceRecord instances.
HTTP server cloud workspace origin enrichment
src/coding_agent/ui/http_server.py
_session_origin_from_request enriches origin with cloud workspace metadata (workspace_provider, provider_instance_id, workspace_root_ref, workspace_host_label) when binding is CloudWorkspaceBinding, with fallback to derive label from provider instance ID.
SessionManager workspace record persistence
src/coding_agent/ui/session_manager.py
_persist_workspace_record_for_session validates and persists WorkspaceRecord from session origin with active status and delete-on-close retention. create_session now persists cloud workspace records before acquiring owner lease and enhances rollback to release partially acquired leases.
Test doubles and persistence coverage
tests/ui/test_http_server.py, tests/ui/test_session_manager_runtime.py
Test doubles implement async save() method. New tests verify cloud workspace records are persisted for provisioned origins and skipped for explicit bindings. Existing retention tests updated with cloud origin metadata and direct record assignment.
Asyncpg dependency
pyproject.toml
Added asyncpg>=0.30.0 to main project dependencies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • delete-cloud/Coding-Agent-Loop#161: Implements the durable remote workspace retention semantics from ADR-0025 by persisting provider/cloud workspace metadata via a new WorkspaceMetadataStoreProtocol.save() path during SessionManager.create_session().
  • delete-cloud/Coding-Agent-Loop#112: Wires a cloud binding resolver from config in the HTTP/server-to-SessionManager startup path, while this PR extends that path to enrich and persist cloud workspace metadata based on the resolved origin.
  • delete-cloud/Coding-Agent-Loop#125: Also modifies _session_origin_from_request to enrich session.origin with metadata fields (cloud workspace data in main PR; auth-derived owner/scope in that PR), feeding the shared session origin path.

Poem

🐰 Hops through the clouds with records in hand,
Each workspace persists across the land,
From origin's fields to storage so deep,
Sessions now remember what they must keep.
AsyncPG whispers, the data takes flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: persist remote workspace records on create' directly and specifically describes the main change: implementing persistence of workspace records during session creation for remote/cloud workspaces.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/durable-workspace-record-on-create

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
coding-agent/src/coding_agent/ui/session_manager.py (1)

945-955: ⚡ Quick win

Strengthen validation to check for non-empty strings.

The validation checks that fields are strings but doesn't verify they're non-empty. While the HTTP server populates origin with non-empty values, defensive validation would prevent potential issues if origin is constructed elsewhere or if the HTTP server logic changes.

🛡️ Proposed validation improvement
     if (
         not isinstance(provider, str)
+        or not provider.strip()
         or not isinstance(provider_instance_id, str)
+        or not provider_instance_id.strip()
         or not isinstance(workspace_root_ref, str)
+        or not workspace_root_ref.strip()
         or not isinstance(workspace_host_label, str)
+        or not workspace_host_label.strip()
         or not isinstance(owner_label, str)
+        or not owner_label.strip()
         or not isinstance(source_kind, str)
+        or not source_kind.strip()
     ):
         raise RuntimeError(
             "cloud workspace session is missing durable workspace metadata"
         )

Additionally, consider making the error message more specific by indicating which field(s) are missing or invalid.

🤖 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 `@coding-agent/src/coding_agent/ui/session_manager.py` around lines 945 - 955,
The current validation in the session metadata block only checks types; update
the check in the session_manager validation (the if that inspects provider,
provider_instance_id, workspace_root_ref, workspace_host_label, owner_label,
source_kind) to also assert each value is a non-empty string (e.g.,
isinstance(..., str) and bool(... .strip()) ), and when raising the RuntimeError
include a clear message naming which field(s) are missing or empty so callers
can see exactly which metadata failed validation.
coding-agent/tests/ui/test_session_manager_runtime.py (1)

1943-1943: 💤 Low value

Consider adding defensive assertion before record assignment.

The pattern store.records[0] = _workspace_record(...) assumes create_session() persisted exactly one record. While this provides implicit coverage of the persistence logic, an explicit assertion would make the assumption clear and provide better failure messages.

🛡️ Optional: Add defensive check

For example, in test_close_session_retains_workspace_when_policy_is_pinned:

     session_id = await manager.create_session(
         execution_binding=binding,
         origin={
             "channel": "http",
             "binding_kind": "cloud",
             "workspace_source_kind": "git",
             "workspace_provider": "docker",
             "provider_instance_id": "docker-host-a",
             "workspace_root_ref": "/workspaces",
             "workspace_host_label": "docker-host-a",
             "owner_label": "owner:test",
         },
     )
+    assert len(store.records) == 1, "Expected create_session to persist one record"
     store.records[0] = _workspace_record(
         session_id=session_id,
         workspace_id=binding.workspace_id,
         retention_policy="pinned",
     )

Apply similar assertions in the other three tests that use this pattern.

Also applies to: 1981-1981, 2022-2022, 2065-2065

🤖 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 `@coding-agent/tests/ui/test_session_manager_runtime.py` at line 1943, Before
doing the assignment store.records[0] = _workspace_record(...), add a defensive
assertion that the session creation actually persisted the expected record (e.g.
assert len(store.records) == 1 or assert len(store.records) >= 1, with a helpful
message) so the test fails clearly if persistence didn't occur; apply this
change in test_close_session_retains_workspace_when_policy_is_pinned and the
other three tests that use the same pattern (the lines using store.records[0],
create_session(), and _workspace_record).
🤖 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 `@coding-agent/src/coding_agent/ui/session_manager.py`:
- Around line 1487-1499: When owner lease acquisition
(_acquire_owner_for_session) fails after the workspace record was persisted
(_persist_workspace_record_for_session), add rollback logic to mark the
persisted workspace record as stale so GC can collect it: after popping the
session from _session_cache and before/after releasing the owner lease
(_release_owner_lease_for_session), update the persisted workspace record for
that session (either by re-calling _persist_workspace_record_for_session with
status="stale"/retention_policy="delete_on_close" or by calling a new helper
e.g. _mark_workspace_record_stale(session_id)) so that
_workspace_record_is_gc_eligible will return True for the orphaned record;
ensure this update is done inside the same exception handling block and surround
it with its own try/except to log failures without masking the original
exception.

---

Nitpick comments:
In `@coding-agent/src/coding_agent/ui/session_manager.py`:
- Around line 945-955: The current validation in the session metadata block only
checks types; update the check in the session_manager validation (the if that
inspects provider, provider_instance_id, workspace_root_ref,
workspace_host_label, owner_label, source_kind) to also assert each value is a
non-empty string (e.g., isinstance(..., str) and bool(... .strip()) ), and when
raising the RuntimeError include a clear message naming which field(s) are
missing or empty so callers can see exactly which metadata failed validation.

In `@coding-agent/tests/ui/test_session_manager_runtime.py`:
- Line 1943: Before doing the assignment store.records[0] =
_workspace_record(...), add a defensive assertion that the session creation
actually persisted the expected record (e.g. assert len(store.records) == 1 or
assert len(store.records) >= 1, with a helpful message) so the test fails
clearly if persistence didn't occur; apply this change in
test_close_session_retains_workspace_when_policy_is_pinned and the other three
tests that use the same pattern (the lines using store.records[0],
create_session(), and _workspace_record).
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93fb0198-3e71-4c84-b452-77671cd406a5

📥 Commits

Reviewing files that changed from the base of the PR and between 678bdd2 and 72f8604.

⛔ Files ignored due to path filters (1)
  • coding-agent/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • coding-agent/pyproject.toml
  • coding-agent/src/coding_agent/ui/http_server.py
  • coding-agent/src/coding_agent/ui/session_manager.py
  • coding-agent/tests/ui/test_http_server.py
  • coding-agent/tests/ui/test_session_manager_runtime.py

Comment on lines +1487 to +1499
await self._persist_workspace_record_for_session(session)
await self._acquire_owner_for_session(session_id)
except BaseException:
self._session_cache.pop(session_id, None)
try:
await self._release_owner_lease_for_session(session_id)
except asyncio.CancelledError:
pass
except BaseException:
logger.exception(
"Failed to release partially created owner lease during rollback: %s",
session_id,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: Orphaned workspace records are not cleaned up when owner lease acquisition fails.

When workspace record persistence succeeds (line 1487) but owner lease acquisition fails (line 1488), the rollback removes the session but doesn't clean up the workspace record. The orphaned record will have status="active" and retention_policy="delete_on_close", but because the session doesn't exist, the workspace_id won't be in active_workspace_ids.

Checking the GC eligibility logic (_workspace_record_is_gc_eligible, lines 654-668), an orphaned record with status="active" returns False at line 668 because status != "stale". This means orphaned workspace records will accumulate and never be cleaned up by GC.

Potential solutions

Option 1: Update orphaned record status in rollback

             except BaseException:
                 self._session_cache.pop(session_id, None)
                 try:
                     await self._release_owner_lease_for_session(session_id)
                 except asyncio.CancelledError:
                     pass
                 except BaseException:
                     logger.exception(
                         "Failed to release partially created owner lease during rollback: %s",
                         session_id,
                     )
+                # Mark workspace record as stale so GC can clean it up
+                if self._workspace_metadata_store is not None and isinstance(binding, CloudWorkspaceBinding):
+                    try:
+                        record = await self._workspace_metadata_store.load_for_session_workspace(
+                            session_id=session_id,
+                            workspace_id=binding.workspace_id,
+                        )
+                        if record is not None:
+                            await self._workspace_metadata_store.update_status(
+                                record.workspace_record_id,
+                                status="cleanup_failed",
+                                cleanup_error="Session creation failed after workspace provisioning",
+                            )
+                    except Exception:
+                        logger.exception(
+                            "Failed to mark orphaned workspace record as cleanup_failed: %s",
+                            session_id,
+                        )
                 try:
                     await self._run_store_io(self._store.delete, session_id)

Option 2: Persist workspace record after owner lease acquisition
Move line 1487 to after line 1488, though this contradicts the stated design intent in the layer description.

Option 3: Enhance GC to detect orphaned records
Modify _workspace_record_is_gc_eligible to check if the session exists and clean up records with non-existent sessions.

🤖 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 `@coding-agent/src/coding_agent/ui/session_manager.py` around lines 1487 -
1499, When owner lease acquisition (_acquire_owner_for_session) fails after the
workspace record was persisted (_persist_workspace_record_for_session), add
rollback logic to mark the persisted workspace record as stale so GC can collect
it: after popping the session from _session_cache and before/after releasing the
owner lease (_release_owner_lease_for_session), update the persisted workspace
record for that session (either by re-calling
_persist_workspace_record_for_session with
status="stale"/retention_policy="delete_on_close" or by calling a new helper
e.g. _mark_workspace_record_stale(session_id)) so that
_workspace_record_is_gc_eligible will return True for the orphaned record;
ensure this update is done inside the same exception handling block and surround
it with its own try/except to log failures without masking the original
exception.

@delete-cloud
delete-cloud merged commit 159b2eb into main May 15, 2026
1 check passed
@delete-cloud
delete-cloud deleted the fix/durable-workspace-record-on-create branch May 15, 2026 09:15
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.

1 participant