fix(workspace): make the routing section follow the pinned workspace

Merged
SA
2mo ago
+287-1

Fixes #1337.

Scope note: this fixes the naming half — the contradiction the report opens with. It does not make warehouse calls execute against the pinned workspace; see What this does NOT change below. If the routing half should stay open, say so and I'll split it into a follow-up issue and downgrade this to a reference.

Problem

The IDE extension's pin outranks the project's stored binding inside resolveBindingOutcome, so identity, skills and memory follow the panel's selection. precedence.currentBinding() did not — it read the on-disk cache directly, and the pin is deliberately never persisted (state.ts strips pinned before every write).

That left one turn naming two workspaces: the identity section said the pinned one, the routing section said whatever the project was linked to, with nothing telling the model which governs execution.

What this changes

precedence.currentBinding() consults the pin first, through a single exported arm of the resolver the other consumers already use — one precedence rule, not two implementations. The pin's security properties (containment against its root, credential-scoped validation cache, TTLs, membership check) are reused as-is.

A pin that cannot be honoured — malformed, outside its root, unresolvable credentials, or naming a workspace the account cannot see — fails closed rather than falling through to the project's link.

What this does NOT change, and why

It does not make warehouse calls execute against the pinned workspace. In serve mode engine-overlay.atTurnStart bails at if (!isEnabled() || isServe()) and records disabled; SERVING.disabled is false, so attributableEngine rejects it and derive settles unattributed. Routing is off in serve mode by design — the comment on isServe() says the extension runs its own engine and bridge under the same key, and overriding it there would remove its extension-type tools.

So the reachable effect of the pin here is which workspace the section names, which is the contradiction the issue opens with. Making routing itself follow the pin means revisiting that isServe() bail, which is a larger decision than this issue.

Removed after review

The first cut also changed engine-probes.resolveBinding. That was unreachable: its only callers are engine-overlay:220 and :477, both behind the same isServe() bail, and the pin exists only when ALTIMATE_CODE_SERVE=1. manage.ts imports its resolveBinding from ./state, which is already pin-aware. Reverted in d5227fe — which also removed a real TOCTOU that CodeRabbit and cubic both caught in that arm (it re-read credentials via currentScope() after resolvePinnedBinding had validated against its own snapshot, so a credential change between the awaits could pair one account's validated id with another's scope). Deleting code with no caller beat guarding it.

Thanks to Kilo and cubic for catching the reachability problem — the original tests called resolveBinding directly and so stepped straight past the production gate.

Verification

test/altimate/workspace/routing-pin.test.ts — 8 tests, driven through refresh() under a real Instance, so they exercise the path that actually runs:

$ bun test test/altimate/workspace/routing-pin.test.ts
  8 pass, 0 fail

The existing precedence.test.ts drives everything through the precedenceInternals.binding seam, which this change checks before the pin — so those tests structurally cannot reach this path.

Mutation-checked — disabling the pin arm fails exactly the two tests that encode the reported behaviour, and nothing else:

(fail) names the pinned workspace, not the project's own link
(fail) fails closed when the pin cannot be honoured, rather than naming the project's link
  6 pass, 2 fail

No regression — full workspace suite, 19 files: 691 pass, 1 fail. That failure is skill sync > flushPendingSyncs waits for a sync a short-lived process would abandon, a 5s-timeout test that also fails on pristine main under a full-directory run and passes 3/3 in isolation there. Pre-existing flake under load.

bun run typecheck: 13/13. script/check-tracker-leaks.ts: exit 0.

Follow-ups

  • The stop-gap identity copy at identity.ts:123 still says "warehouse tool routing still follows the project's own link, which may name a different workspace." With this merged that sentence needs rewording — routing no longer names a different workspace, though it still does not execute against the pinned one. Left out so this PR reads as the behaviour change alone.
  • Whether serve-mode sessions should route through the workspace engine at all is the open question behind the rest of #1337.
  • Unchanged: #1335, and the pin's known gaps (no backfill on pin, no purge-before-fetch in skill-sync).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Workspace routing honors valid pinned IDE-extension bindings before project-level workspace links.
    • Workspace bindings can use a cached result when the server is unavailable.
  • Bug Fixes

    • Invalid, inaccessible, or out-of-scope pins no longer route incorrectly or appear as an unbound project.
    • When a pin cannot be honored, routing reports the binding as unreadable. An enabled local-integration escape hatch skips pin resolution.