fix: [AI-9171] create a quick workspace from an already-linked project

Closed
SA
1mo ago
+634-44

Problem

In a project that is already linked, the altimate link picker's "+ Create a quick workspace … here" row always failed — and the error told the user to re-run the command they were already inside.

◇  Currently linked to "jaffle_shop-altimate". Pick a workspace (or create a new one):
│  + Create a quick workspace "jaffle_shop-altimate" here
│
■  Failed to create workspace.
■  This project is already linked to "jaffle_shop-altimate". Re-run `altimate-code link`
   to switch to a different workspace.

The row's own hint promises the opposite: "Creates a new workspace and repoints this project to it (no browser step)."

Cause

createThenBindOrRebind already had a correct rebind branch. It was simply unreachable.

Step one called createAndBind → POST /datamate-project-bindings/, whose server handler pre-checks both identifiers and returns 409 before creating anything — deliberately, so a binding conflict cannot strand a half-created workspace. On an already-linked project that refuses the entire call, so nothing was created and the rebind below it was dead code.

The handler's own comment shows the assumption that broke — "the new workspace exists but the binding still points at the OLD workspace". It does not exist: create and bind are atomic server-side, so the binding conflict takes the creation down with it.

Worth noting the neighbouring "+ Set up in browser" row is correctly hidden when already linked, with a comment explaining this exact 409. One of the two guards was applied; the other was missed.

Fix

Split the two cases, because the server offers two different things:

  • Unlinked — unchanged. createAndBind still creates and binds in one transaction, which is what makes a stranded workspace impossible there.
  • Already linked — create the workspace unbound via POST /datamates/, then repoint through the rebind path that already existed.

The trap this had to avoid

createWorkspaceUnbound sends memory_enabled and knowledge_engine_enabled explicitly. POST /datamates/ is the SaaS/extension creation path and defaults both to false, while the create-and-bind path (_create_datamate_flush_only) sets both true.

Without those two lines the same menu row would hand back a differently-configured workspace depending only on whether the project happened to be linked — memory and the knowledge engine silently off, with nothing surfacing the difference. The comment names the coupling so the two move together if backend defaults change.

Error message

After this change a 409 can only mean another workspace claimed the project mid-selection, so it says that, instead of directing the user back into the command they are already running.

Verification

Against a real backend

Local backend on :5001, tenant hackdev, project bound to workspace 63.

Before — what the old code did:

POST /datamate-project-bindings/  {"name":"jaffle_shop-altimate", <same identifiers>}
  HTTP 409
  detail: This project is already linked to a different workspace

Nothing created, rebind unreachable.

After — what the new code does:

POST /datamates/  {..., "memory_enabled":true, "knowledge_engine_enabled":true}
  -> {"id": 66}

PUT /datamate-project-bindings/by-remote
    {"target_datamate_id":66, "expected_current_datamate_id":63}
  -> {"binding":{"id":44,"datamate_id":66, ...}}

GET /datamate-project-bindings/by-remote
  -> bound to workspace 66 - 'jaffle_shop-altimate'

And the new workspace carries the workspace defaults, not the SaaS ones:

memory_enabled           = True
knowledge_engine_enabled = True

All seeded rows removed afterwards; hackdev restored to its prior counts.

Tests

New test/altimate/workspace/create-workspace-unbound.test.ts — 7 tests stubbing globalThis.fetch, following the skill-sync.test.ts harness (real credentials file, assertions on the request that actually goes out). They pin the two things that would regress silently: that this does not reach the binding router, and that both feature flags are sent.

Mutation-tested rather than assumed — each of these fails the suite:

MutationResult
drop memory_enabled1 fail
drop knowledge_engine_enabled1 fail
post to the bindings router instead1 fail
remove the id guard2 fail

Existing suites: test/cli/cmd/link.test.ts + all of test/altimate/workspace/ → 503 pass, 0 fail. Typecheck clean on both changed files. oxlint contributes no new errors (the 4 reported are pre-existing, in unrelated tsconfig.json files).

Note for review

The alternative fix is backend-side: a replace_existing flag on POST /datamate-project-bindings/ would make this atomic and remove the duplicated defaults entirely. That is the cleaner long-term shape, but it is a two-repo change; this keeps the fix in the CLI and reuses the rebind path that already exists.

Ticket: AI-9171


Summary by cubic

Fixes the “+ Create a quick workspace … here” row in both the CLI and TUI on already-linked projects, where it always failed and pointed the user back into the command they were already running. The row now creates the workspace unbound, then repoints the existing binding to it.

  • Unlinked projects keep the atomic create-and-bind behavior; only the already-linked path takes the new two-step route.
  • The unbound create sends memory_enabled and knowledge_engine_enabled explicitly, matching the configureation the create-and-bind path produces.
  • The account is pinned across the create-and-rebind pair so an account switch mid-flow cannot rebind on the wrong tenant.
  • A strict id guard rejects malformed responses instead of coercing values (a boolean would have rebound the project to workspace 1).
  • The rebind's server binding row is what gets cached, replacing locally-derived identifiers the server never stored.
  • 409 messages are split by which create ran: a conflict on the unbound path is reported as another workspace claiming the project.

Summary by CodeRabbit

  • New Features

    • Added support for creating a standalone private workspace during altimate link.
    • Existing workspace links can now be safely repointed without creating an unintended project binding.
    • Workspace creation applies memory and knowledge engine defaults automatically.
    • Management links are displayed and opened when available.
  • Bug Fixes

    • Improved handling of workspace creation failures and invalid responses.
    • Prevented rebinding when the active account changes during workspace creation.
    • Clarified conflict messaging when no workspace is created.