Skip to content

fix(cli): propagate cloud workspace routing and incremental sync#712

Merged
phernandez merged 5 commits intomainfrom
codex/fix-cloud-workspace-routing-and-incremental-sync
Apr 3, 2026
Merged

fix(cli): propagate cloud workspace routing and incremental sync#712
phernandez merged 5 commits intomainfrom
codex/fix-cloud-workspace-routing-and-incremental-sync

Conversation

@phernandez
Copy link
Copy Markdown
Member

Summary

This PR fixes two related cloud CLI issues:

Root Cause

Two separate regressions were still present in the current branch state:

  1. The cloud sync and bisync commands always passed force_full=True when triggering post-sync database indexing.
  2. Several cloud CLI paths bypassed workspace-aware routing, so multi-workspace users could upload and create projects in the wrong workspace.

What Changed

  • added shared config-based workspace resolution in async_client.py
  • taught get_client() to auto-apply per-project workspace_id and default_workspace for cloud routing
  • updated get_cloud_control_plane_client() to accept and send X-Workspace-ID
  • wired workspace headers through cloud project list/create/existence helpers
  • updated bm cloud upload to reuse the resolved workspace for API checks and WebDAV uploads
  • updated cloud sync/bisync follow-up database sync calls to use incremental indexing
  • added focused regression tests for workspace propagation and incremental sync behavior

Impact

  • multi-workspace cloud uploads now target the configured workspace instead of silently falling back to personal
  • bm cloud sync and bm cloud bisync avoid unnecessary full reindex work in normal operation
  • cloud CLI routing is more consistent across control-plane and proxy requests

Validation

  • uv run pytest -q tests/mcp/test_async_client_modes.py tests/cli/cloud/test_cloud_api_client_and_utils.py tests/cli/cloud/test_upload_command_routing.py tests/cli/cloud/test_project_sync_command.py
  • uv run ruff check src/basic_memory/mcp/async_client.py src/basic_memory/cli/commands/cloud/cloud_utils.py src/basic_memory/cli/commands/cloud/upload_command.py src/basic_memory/cli/commands/cloud/project_sync.py tests/cli/cloud/test_project_sync_command.py tests/cli/cloud/test_cloud_api_client_and_utils.py tests/cli/cloud/test_upload_command_routing.py tests/mcp/test_async_client_modes.py
  • uv run pyright src/basic_memory/mcp/async_client.py src/basic_memory/cli/commands/cloud/cloud_utils.py src/basic_memory/cli/commands/cloud/upload_command.py src/basic_memory/cli/commands/cloud/project_sync.py

Notes

The targeted pytest run still emits an existing Alembic runtime warning in these CLI tests, but the suite passed.

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Claude finished @phernandez's task in 1m 34s —— View job


PR Review: Fix cloud workspace routing and incremental sync

  • Read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & security
  • Post final review summary

Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations and async patterns
  • SQLAlchemy 2.0 best practices (N/A — no DB layer changes)
  • FastAPI and Typer conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

Testing & Documentation

  • Unit tests for new functions/methods
  • Test coverage for edge cases (workspace priority: project > default > none)
  • Minor: create_cloud_project docstring omits the new workspace parameter (cloud_utils.py:63-66)
  • 100% test coverage appears maintained

Basic Memory Architecture

  • Local-first architecture preserved — routing logic correctly defaults to ASGI
  • Preserves factory injection priority (factory > explicit > per-project > default)
  • No database schema changes; no migration needed

Security & Performance

  • No hardcoded secrets
  • Workspace header only set when workspace is non-None (safe)

Compatibility

  • No emoji/unicode in console or log output
  • No direct Path string comparisons that would break on Windows

Issues Found

1. Stale/misleading comment in upload_command.py (line 131-132)

# Force full scan after bisync to ensure database is up-to-date with synced files
if sync and not dry_run:
    ...
    await sync_project(project, force_full=True)

The comment says "after bisync" but this is the upload command, not bisync. The force_full=True here might be intentional for a fresh upload (many new files), but the comment is copy-paste residue from bisync and should be updated or removed. If force_full=True is intentional post-upload, the comment should explain why upload is different from sync/bisync.

Fix this →

2. Silent exception suppression in project_exists() (cloud_utils.py:143)

except Exception:
    return False

All exceptions — including auth failures, network errors, and unexpected bugs — are silently swallowed. CLAUDE.md guidance is "fail fast" and "do not swallow exceptions." If the intent is graceful degradation, at minimum a logger.warning should be added. As written, a misconfigured API key or wrong hostname looks identical to "project not found."

Fix this →

3. assert in production code in project_sync.py (line 198)

assert sync_entry is not None

The comment above explains the invariant correctly (_get_sync_project validates local_sync_path which comes from sync_entry), but assert can be silently disabled at runtime with Python's -O flag. Prefer a runtime guard or explicit raise if this invariant must be enforced.

Fix this →


Positive Highlights

  • resolve_configured_workspace() is a clean, well-tested extraction that correctly implements the priority chain (explicit > project > default). The decision-point comments throughout async_client.py are excellent examples of the literate programming style called for in CLAUDE.md.
  • The parametrized test test_cloud_sync_commands_use_incremental_db_sync covers both sync and bisync in a single compact test.
  • The workspace propagation test (test_cloud_utils_use_configured_workspace_headers) verifies the three-call priority sequence exactly, which is the right level of specificity.
  • Fixing force_full=False in project_sync.py is a minimal, correct change. No unnecessary refactoring.

Overall this is a solid, well-scoped fix. The three issues above are minor but the silent exception suppression (#2) is worth addressing before merge since it could make auth and routing issues hard to diagnose in production.

@phernandez phernandez changed the title [codex] Fix cloud workspace routing and incremental sync fix: Fix cloud workspace routing and incremental sync Apr 3, 2026
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez phernandez marked this pull request as ready for review April 3, 2026 15:20
@phernandez phernandez changed the title fix: Fix cloud workspace routing and incremental sync fix(cli): propagate cloud workspace routing and incremental sync Apr 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Claude finished @phernandez's task in 2m 10s —— View job


PR Review: Fix cloud workspace routing and incremental sync

  • Read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & security
  • Post final review summary

Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations and async patterns
  • SQLAlchemy 2.0 best practices (N/A — no DB layer changes)
  • FastAPI and Typer conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

Testing & Documentation

  • Unit tests for new functions/methods
  • Test coverage for edge cases (workspace priority: explicit > project > default)
  • Minor: create_cloud_project docstring omits the new workspace parameter (cloud_utils.py:63-66)
  • 100% test coverage appears maintained

Basic Memory Architecture

  • Local-first architecture preserved — routing logic correctly defaults to ASGI
  • Preserves factory injection priority (factory > explicit > per-project > default)
  • No database schema changes; no migration needed
  • resolve_configured_workspace() is reused consistently across get_client(), get_cloud_control_plane_client(), and upload_command.py — no duplicated resolution logic

Security & Performance

  • No hardcoded secrets
  • Workspace header only set when workspace is non-None (safe)
  • force_full=False in sync/bisync avoids unnecessary full reindex

Compatibility

  • No emoji/unicode in console or log output
  • No direct Path string comparisons that would break on Windows

Issues Found

1. Stale comment in upload_command.py (line 130)

# Force full scan after bisync to ensure database is up-to-date with synced files
if sync and not dry_run:
    ...
    await sync_project(project, force_full=True)

The comment says "after bisync" but this is the upload command. The force_full=True here is likely intentional (many new files after an upload), but the comment is copy-paste residue and misleading. Should be updated to explain why upload uses force_full=True while sync/bisync use force_full=False.

Fix this →

2. Silent exception suppression in project_exists() (cloud_utils.py:142)

except Exception:
    return False

All exceptions — including auth failures, network errors, and unexpected bugs — are silently swallowed. CLAUDE.md guidance is "fail fast" and "do not swallow exceptions." As written, a misconfigured API key or wrong hostname is indistinguishable from "project not found," which makes auth/routing issues very hard to diagnose.

At minimum, a logger.debug or logger.warning before returning False would help. Alternatively, only catch expected failure modes (e.g., CloudUtilsError) and let unexpected errors propagate.

Fix this →

3. assert in production code in project_sync.py (line 198)

# Update config — sync_entry is guaranteed non-None because
# _get_sync_project validated local_sync_path (which comes from sync_entry)
sync_entry = config.projects.get(name)
assert sync_entry is not None

The comment correctly explains the invariant, but assert statements can be silently disabled at runtime with Python's -O flag (common in production deployments). A proper runtime guard is safer and consistent with the project's fail-fast guideline:

if sync_entry is None:
    raise RuntimeError(f"Sync entry for project '{name}' unexpectedly missing after validation")

Fix this →


Positive Highlights

  • resolve_configured_workspace() is a clean, well-named extraction with correct priority chain (explicit > project > default). The decision-point comments in async_client.py are excellent examples of the literate programming style required in CLAUDE.md.
  • _workspace_headers() in cloud_utils.py is a smart DRY helper that keeps workspace resolution consistent across fetch/create/exists operations.
  • The parametrized test_cloud_sync_commands_use_incremental_db_sync covers both sync and bisync in one compact test — precisely the right level of coverage.
  • test_cloud_upload_uses_project_workspace_for_api_and_webdav verifies the three-call priority sequence (project_exists, control_plane_client, upload_path) in a single focused test.
  • The force_full=False change in project_sync.py is minimal and correct — no unnecessary refactoring.
  • Passing project_name=name to get_client() in _get_cloud_project() and _verify_project_exists() ensures these discovery calls also respect per-project routing.

Overall this is a solid, well-scoped fix. The silent exception suppression (#2) is the most important item to address before merge since it will make auth and routing failures invisible to users. Issues #1 and #3 are lower severity but worth fixing for correctness and debuggability.

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Claude finished @phernandez's task in 1m 59s —— View job


PR Review: Fix cloud workspace routing and incremental sync

  • Read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & security
  • Post final review summary

Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations and async patterns
  • SQLAlchemy 2.0 best practices (N/A — no DB layer changes)
  • FastAPI and Typer conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

Testing & Documentation

  • Unit tests for new functions/methods
  • Test coverage for edge cases (workspace priority chain: explicit > project > default)
  • create_cloud_project docstring omits new workspace parameter (cloud_utils.py:62-75)
  • 100% test coverage appears maintained

Basic Memory Architecture

  • Local-first architecture preserved
  • Factory injection priority preserved (factory > explicit > per-project > default)
  • No database schema changes; no migration needed
  • resolve_configured_workspace() is a clean, well-named extraction reused consistently across get_client(), get_cloud_control_plane_client(), and upload_command.py

Security & Performance

  • No hardcoded secrets
  • Workspace header only set when workspace is non-None
  • force_full=False in sync/bisync avoids unnecessary full reindex

Compatibility

  • No emoji/unicode in console or log output
  • No direct Path string comparisons that would break on Windows

Issues Found

1. Stale comment in upload_command.py (line 130) — still unfixed from prior reviews

# Force full scan after bisync to ensure database is up-to-date with synced files
if sync and not dry_run:
    ...
    await sync_project(project, force_full=True)

This was flagged in both previous review rounds. The comment says "after bisync" but this is the upload command. force_full=True here is likely intentional (many new files after bulk upload), but the comment is copy-paste residue and misleading. Should explain why upload uses force_full=True while sync/bisync use force_full=False.

Fix this →

2. Silent exception suppression in project_exists() (cloud_utils.py:142) — still unfixed from prior reviews

except Exception:
    return False

Flagged in both previous review rounds. CLAUDE.md says "fail fast" and "do not swallow exceptions." All exceptions — including auth failures, misconfigured API keys, wrong hostnames — are silently swallowed and look identical to "project not found." At minimum, add logger.debug(f"project_exists check failed: {e}") before returning False.

Fix this →

3. assert in production code (project_sync.py:198) — still unfixed from prior reviews

# Update config — sync_entry is guaranteed non-None because
# _get_sync_project validated local_sync_path (which comes from sync_entry)
sync_entry = config.projects.get(name)
assert sync_entry is not None

Flagged in both previous review rounds. assert can be silently disabled with Python's -O flag. Replace with a runtime guard:

if sync_entry is None:
    raise RuntimeError(f"Sync entry for project '{name}' unexpectedly missing after validation")

Fix this →

4. Minor: Optional[str] vs str | None inconsistency in async_client.py

resolve_configured_workspace() and get_client() use Optional[str] (from typing) while cloud_utils.py uses the Python 3.10+ str | None union syntax throughout. The project targets Python 3.12+ so str | None is preferred for consistency.

Fix this →


Positive Highlights

  • resolve_configured_workspace() is a clean, well-named extraction with correct priority chain. The decision-point comments throughout async_client.py are excellent examples of the literate programming style required in CLAUDE.md.
  • _workspace_headers() is a smart DRY helper — centralized workspace resolution keeps fetch_cloud_projects, create_cloud_project, and project_exists consistent.
  • The parametrized test_cloud_sync_commands_use_incremental_db_sync covers both sync and bisync — precisely the right level of coverage.
  • test_cloud_upload_uses_project_workspace_for_api_and_webdav verifies all three call sites (project_exists, control_plane_client, upload_path) in one focused test.
  • Passing project_name=name to get_client() in _get_cloud_project(), _verify_project_exists(), and the _trigger_db_sync closures ensures consistent per-project routing throughout.
  • The force_full=False change in project_sync.py is minimal and correct.

Note: Issues 1–3 were flagged in both previous review rounds but remain unaddressed in this push. Issue 2 (silent exception suppression) is the most important to fix before merge as it makes auth and routing failures invisible to users in production.

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Claude finished @phernandez's task in 1m 12s —— View job


PR Review: Fix cloud workspace routing and incremental sync

  • Read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & security
  • Post final review summary

Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations and async patterns
  • SQLAlchemy 2.0 best practices (N/A — no DB layer changes)
  • FastAPI and Typer conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

Testing & Documentation

  • Unit tests for new functions/methods
  • Test coverage for edge cases (workspace priority chain: explicit > project > default)
  • create_cloud_project docstring omits new workspace parameter (cloud_utils.py:62-75)
  • 100% test coverage appears maintained

Basic Memory Architecture

  • Local-first architecture preserved
  • Factory injection priority preserved (factory > explicit > per-project > default)
  • No database schema changes; no migration needed
  • resolve_configured_workspace() reused consistently across get_client(), get_cloud_control_plane_client(), and upload_command.py

Security & Performance

  • No hardcoded secrets
  • Workspace header only set when workspace is non-None
  • force_full=False in sync/bisync avoids unnecessary full reindex

Compatibility

  • No emoji/unicode in console or log output
  • No direct Path string comparisons that would break on Windows

Issues Found

All four items below were flagged in one or more prior review rounds and remain unaddressed in this push.

1. Stale comment in upload_command.py (line 130) — flagged 3x

# Force full scan after bisync to ensure database is up-to-date with synced files
if sync and not dry_run:
    ...
    await sync_project(project, force_full=True)

This is the upload command, not bisync. The force_full=True is likely correct post-upload (many new files), but the comment is copy-paste residue. It should explain why upload uses force_full=True while sync/bisync use force_full=False.

Fix this →

2. Silent exception suppression in project_exists() (cloud_utils.py:142) — flagged 3x, highest priority

except Exception:
    return False

All exceptions — auth failures, misconfigured API key, wrong hostname, unexpected bugs — are silently swallowed and look identical to "project not found." CLAUDE.md says "fail fast" and "do not swallow exceptions." This will make routing and credential issues invisible in production. At minimum:

except Exception as e:
    logger.debug(f"project_exists check failed: {e}")
    return False

Fix this →

3. assert in production code (project_sync.py:198) — flagged 3x

assert sync_entry is not None

assert is silently disabled when Python runs with -O (common in production). Replace with a proper runtime guard:

if sync_entry is None:
    raise RuntimeError(f"Sync entry for project '{name}' unexpectedly missing after validation")

Fix this →

4. Optional[str] vs str | None inconsistency in async_client.py — flagged once

resolve_configured_workspace(), get_client(), get_cloud_control_plane_client(), get_cloud_proxy_client(), and _cloud_client() all use Optional[str] (imported from typing) while the rest of the codebase (cloud_utils.py etc.) uses the Python 3.10+ str | None union syntax. The project targets Python 3.12+ so str | None is preferred throughout.

Fix this →


Positive Highlights

  • resolve_configured_workspace() is a clean extraction with correct priority chain and excellent decision-point comments.
  • _workspace_headers() is a solid DRY helper keeping workspace resolution consistent across fetch_cloud_projects, create_cloud_project, and project_exists.
  • Parametrized test_cloud_sync_commands_use_incremental_db_sync covers both sync and bisync in one compact test.
  • force_full=False in project_sync.py is a minimal and correct fix.
  • Passing project_name=name to get_client() in _get_cloud_project() and _verify_project_exists() ensures consistent per-project routing throughout.

Summary: Issues 1–3 persist from prior rounds. Issue 2 (silent exception suppression) is the most important to resolve before merge — it will hide credential and routing failures from users in production.

Signed-off-by: phernandez <paul@basicmachines.co>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Claude finished @phernandez's task in 1m 28s —— View job


PR Review: Fix cloud workspace routing and incremental sync

  • Read changed files
  • Check previously flagged issues
  • Review code quality & standards
  • Post final review summary

Code Quality & Standards

  • Follows Basic Memory's coding conventions in CLAUDE.md
  • Python 3.12+ type annotations and async patterns
  • SQLAlchemy 2.0 best practices (N/A — no DB layer changes)
  • FastAPI and Typer conventions followed
  • 100-character line length limit maintained
  • No commented-out code blocks

Testing & Documentation

  • Unit tests for new functions/methods
  • Test coverage for edge cases (workspace priority chain: explicit > project > default)
  • create_cloud_project docstring now documents the workspace parameter (cloud_utils.py:62-76)
  • 100% test coverage appears maintained
  • test_cloud_bisync_fails_fast_when_sync_entry_disappears covers the new RuntimeError guard

Basic Memory Architecture

  • Local-first architecture preserved
  • Factory injection priority preserved (factory > explicit > per-project > default)
  • No database schema changes; no migration needed
  • resolve_configured_workspace() reused consistently

Security & Performance

  • No hardcoded secrets
  • Workspace header only set when workspace is non-None
  • force_full=False in sync/bisync avoids unnecessary full reindex

Compatibility

  • No emoji/unicode in console or log output
  • No direct Path string comparisons that would break on Windows

Previously Flagged Issues — Status Update

1. Stale comment in upload_command.pyFIXED

The comment now uses the literate programming style required by CLAUDE.md:

# Sync project if requested (skip on dry run).
# Trigger: upload adds new files the watcher has not observed locally.
# Why: force_full ensures those freshly uploaded files are indexed immediately.
# Outcome: upload keeps its eager reindex while sync/bisync stay incremental.
if sync and not dry_run:

2. Silent exception suppression in project_exists()FIXED

The bare except Exception: return False is gone. The function now lets CloudUtilsError propagate from fetch_cloud_projects, and the caller in upload_command.py catches CloudUtilsError explicitly — a better approach than just logging.

3. assert in production code (project_sync.py) — FIXED

Replaced with a proper runtime guard:

if sync_entry is None:
    raise RuntimeError(
        f"Sync entry for project '{name}' unexpectedly missing after validation"
    )

Remaining Issue

Optional[str] vs str | None inconsistency in async_client.py — still unfixed

resolve_configured_workspace(), get_client(), get_cloud_control_plane_client(), get_cloud_proxy_client(), _cloud_client(), and _client_factory all use Optional[str] / Optional[Callable[...]] imported from typing (async_client.py:3, 72-74, 94, 113, 132, 149, 164-166). The rest of the codebase uses str | None throughout. The project targets Python 3.12+ so str | None is preferred.

Fix this →


Positive Highlights

  • All three high-priority issues from prior review rounds (Pre release #1fix: fix permalink uniqueness violations on create/update/sync #3) are resolved.
  • project_exists() now correctly propagates CloudUtilsError rather than silently swallowing it — this is a better fix than the suggested logger.debug approach.
  • The literate programming decision-point comment in upload_command.py is exactly the style required by CLAUDE.md.
  • test_cloud_bisync_fails_fast_when_sync_entry_disappears tests the RuntimeError path directly, ensuring the guard is exercised.
  • resolve_configured_workspace() is a clean, consistent extraction used across all three call sites without duplication.

Summary: This PR is in good shape. The three substantive issues from previous rounds are all addressed. The only open item is the minor Optional[str] vs str | None style inconsistency in async_client.py — low risk, but worth cleaning up for consistency before merge.

@phernandez phernandez merged commit e6b98a1 into main Apr 3, 2026
9 checks passed
@phernandez phernandez deleted the codex/fix-cloud-workspace-routing-and-incremental-sync branch April 3, 2026 19:25
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.

Cloud sync/bisync commands hardcode force_full=True instead of using incremental sync Cloud upload and API routing don't support workspace selection

1 participant