Repository navigation
fix(workspace): isolate Peer requests and preserve SSH session storage - #3341
Merged
Merged
Conversation
Two saved SSH connections to one host and root share a workspace record and session mirror. Rejecting such records at activation broke imported catalogs and the same-host multi-account case, so activation no longer validates storage owners; session identity verification keeps histories apart. Reopening an existing remote record with a different connection now rebinds it only when the ids are equivalent (legacy ssh-user@host:port), the previous owner is no longer saved, or the user confirmed through the new optional rebindConnection field on open_remote_workspace. Otherwise Desktop and the CLI peer host return the stable remote_workspace_connection_conflict code as the whole error message. Older hosts ignore the field. The legacy migration adapter and core now share one canonical SSH connection id helper. Equivalence canonicalizes one side at a time so current ids for bare IPv6 hosts are never merged.
When opening a remote folder hits remote_workspace_connection_conflict, the Web UI asks whether to move the workspace to the selected connection and retries with rebindConnection only after confirmation. Cancelling restores the previous remote workspace and keeps the file browser open, and the record is opened before the host-side workspace is switched so cancellation leaves no host state behind. Startup restore never rebinds silently: it defers with a localized notification and an error status. The conflict is not stored as the workspace manager's error state.
require_session_storage_path no longer commits a binding while reading. Before admission it resolves from the session's workspace configuration and validates against any pending claim, so a reader during a contested claim fails instead of following an uncommitted location. Turn lookup, compression transcripts, model exchange traces, reference materialization, evidence persistence and evicted-session restores read only committed bindings. The filesystem alias test now places its user root below the alias, because product home is derived from the user root's parent.
Legacy session metadata without a workspaceId was matched only against opened workspaces. A worktree session whose worktree is not opened then had no owner. Resolution now also considers recent workspace records, and a regression test covers a pre-ID worktree session listed under its project without borrowing the project's execution identity.
Add an ESLint rule that rejects await inside api.invoke(...) arguments, because the device surface is captured only after they resolve. Migrate the remaining hand-written getActiveSurfaceScope plus assertCurrent sites in AgentAPI session restore and listing, WorkspaceAPI file operations, CronAPI, SnapshotAPI, EditorDocument and TerminalService to invokePrepared, which also checks the activation after the response. invokePrepared now skips activation checks for controller-local commands from the generated registry, matching ApiClient.
Same-path local workspaces hash to one workspace ID on every device, so module-level caches keyed only by workspace ID leaked across Peer devices. - Git trust prompts dedupe per activation epoch, re-check the activation before granting, and never grant an answer from a departed device. The Git scene treats that cancellation quietly. - Review platform snapshot and detail caches key by the rendered surface and clear on activation; review launch dedupe is surface scoped. - Tool info descriptions are cached per surface. - In-flight maps delete entries only when they still own them.
Replace the removed remote_workspace_storage_conflict activation rule with the connection rebind policy, document side-effect free storage binding reads, the invoke lint guard, controller-local exemption and surface-scoped caches, and list the coordination owner filter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix workspace and session ownership when switching Peer devices or opening local and SSH projects with the same path. Asynchronous request preparation could send the previous device's workspace ID to the newly selected host; backend history restore and some persistence paths then resolved the execution path again instead of retaining the session's admitted storage owner.
awaitinsideapi.invoke(...)arguments, the remaining hand-written scope sites useinvokePrepared, andinvokePreparedhonors controller-local commands likeApiClient.PathManager. Reading a binding never commits one, and readers never follow an uncommitted index entry.SSH connections sharing one host and root
The supported SSH storage layout remains host plus remote root, so two saved connections to the same host and root share a workspace record and session mirror. Activation does not reject such records; session identity verification keeps their histories apart, and imported records for each connection stay listed and activatable.
Reopening an existing remote record with a different connection rebinds it only for an allowed reason:
ssh-user@host:portversus currentssh-user@host; one side is canonicalized at a time so bare IPv6 hosts are never merged);rebindConnectionfield onopen_remote_workspace(Desktop and CLI peer host).Otherwise the command fails with
remote_workspace_connection_conflict: ...as the whole error string. The interactive Web UI asks the user and retries withrebindConnection; cancelling restores the previous remote workspace. The entire lookup, confirmation, retry, and activation carries the initiating device activation, including A → B → A cancellation. Startup restore checks record ownership before updating the host remote pointer, both for an existing connection and a newly reconnected transport, and defers conflicts with a localized notification (en-US, zh-CN, zh-TW). A device switch abandons stale restore work without publishing into the next surface. Supporting simultaneous distinct endpoints for one host and root still requires a versioned storage-identity migration.Upgrade compatibility
rebindConnectionis additive with#[serde(default)]; older hosts accept and ignore it and keep their previous rebind behavior.Type and Areas
Type: bug fix / regression fix
Areas: Rust core workspace/session/coordination, Desktop and CLI
open_remote_workspace, Web UI API, Peer, SSH remote and review boundaries, architecture documentation.Verification
Validated after the final SSH ownership and activation fix (
c178f0da2):pnpm run check:web, focused ESLint for both changed files, andgit diff --checkpassed.Broader verification recorded on the preceding head (
65de9742a):NODE_OPTIONS=--no-experimental-webstorageso jsdom provideslocalStorage. New tests cover connection-conflict confirm/cancel/background restore, conflict error parsing, pre-ID worktree sessions, controller-localinvokePrepared, cross-device Git trust answers, review cache reuse across devices (observed failing before the fix), and per-device tool info.tsc --noEmitpassed;eslint .reports 0 errors (10 pre-existing warnings in untouched files);pnpm run i18n:audit,pnpm run check:core-boundaries,pnpm run check:repo-hygiene, andpnpm run canvas:sdk:checkpassed.check:web:appearancetests passed; its generated-artifact steps need built design-system packages and are left to CI.openbitfun-core --features product-full --lib: 2558 passed, including the four previously failing CI tests.--no-default-features --features agent-runtime,git,remote-workspace: workspace 45, session 192, coordination 212 passed.openbitfun-clipeer_host::tests (102),openbitfun-legacy-migration-adapters(55), andopenbitfun-services-core --features workspace-persistenceworkspace_identitypassed.cargo checkpassed foropenbitfun-desktop --libandopenbitfun-cli --all-targets.pnpm run fmt:rs,git diff --check, andnode scripts/check-git-object-sizes.mjspassed. Remaining compiler warnings are in files this PR does not touch.Reviewer Notes
AI-assisted. Testing level: lightly tested in the contributor-guide terminology; focused automated coverage is extensive, but live remote integration remains unverified.
Remote scenarios exercised: Peer Device Mode with mocked surface activations; remote workspace (SSH) with catalog/storage fixtures and the CLI peer host command path. Remote Control (mobile web, IM bots) does not open remote workspaces, so the conflict cannot reach it. Detached Dispatch: the dispatch-hold paths now return an error while a storage claim is contested instead of following an uncommitted location, covered by unit tests only. No live two-device Peer, SSH transport, mobile Remote Control, or Detached Dispatch end-to-end run was performed.
The only wire change is the optional
rebindConnectionrequest field. No command, Cargo feature, dependency, or runtime ownership migration is introduced.Checklist