Myeongseon Choi 4572cd5320 fix(lsp): rewrite local/remote file URIs in stdio broker for Pyright
Remote pyright saw editor file:// cache paths; recurse JSON strings and swap prefixes toward the helper and back.

Add SESSIONS_BRIDGE_DIAG_LOG events for lsp-stdio attach and per-frame broker I/O.

Extend lsp-stdio CLI and .sublime-project command with --lsp-local-uri-prefix/--lsp-remote-uri-prefix.

Made-with: Cursor
2026-04-22 00:34:58 +09:00
2026-04-22 00:19:19 +09:00
2026-04-22 00:19:19 +09:00
2026-04-22 00:19:19 +09:00

Sessions

Sessions is a Sublime Text package and Rust helper toolkit for remote workspaces over SSH.

Current focus:

  • Completed milestones: Phase 06.2 (all closed), Phase 7 - Stability Hardening (closed), Phase 8 - Rust Transport Expansion (closed). See planning/GITEA_ISSUES.md.
  • Open milestones: Phase 9 - Quality Gates & Scale (#10, #29), Remote LSP integration track (#34).
  • Execution order (2026-04, Rust-first): P0.5 stabilization → crate consolidation → artifact publish + manifest/checksum → #24 Rust runtime ownership → #32 large-file → #29 diff-centric product. Normative detail: planning/GITEA_ISSUES.md (execution priority and schedule), migration waves: planning/PYTHON_RUST_BOUNDARY.md.
  • P0.5 stabilization (2026-04):
    • Done: persistent bridge, download-only helper, reconnect, mirror ignore patterns, save conflict UI, wire contract test coverage (bridge stdout fixtures, binary smoke test, ABI smoke test), stability hardening (prune symlink/permission edges, multi-window dedup, refresh race prevention)
    • In progress: remote file auto-reload (periodic stat → revert), LSP-ready on-demand fetch (external path mapper + on_window_command interceptor)
  • Remote LSP implementation (next): local_bridge lsp-stdio endpoint + persistent broker attach IPC, session_helper lsp_stdio child supervision, URI rewrite/save barrier/materialization in local_bridge, and host-scoped install with workspace-scoped env/config (#34, #35, #36, #37).
  • SSH config driven workspace selection
  • session-bound helper over SSH stdio
  • local cache with local-host-independent workspace identity
  • formatter and linter execution in the remote environment (baseline + #30 pipeline on save)
  • long-term evolution toward a multi-session agent window (after the MVP above)

Repository layout

  • sublime/: Sublime package code and Python tests
  • rust/: Rust workspace for bridge/helper/shared crates
  • docs/: human-facing project documentation (when present)
  • planning/: roadmap, Gitea issue bootstrap, transport model, Python/Rust boundary, deep-research notes

Planning index (browse on Gitea)

Document Role
planning/GITEA_ISSUES.md Milestones, parent issue text, execution priority
planning/PYTHON_RUST_BOUNDARY.md Python vs Rust split, Rust-first migration waves 05
planning/VSCODE_REMOTE_TRANSPORT_MODEL.md Envelope + logical channels (VS Codealigned)
planning/REMOTE_DEV_MVP_LSP.md Phase 6.2 LSP / tool transport choices
planning/DEEP-RESEARCH-REPORT.md External audit + priority reconciliation (end)

Installing In Sublime Text

sublime/ is the actual Sublime package root for this repository.

  • Development install: link or copy sublime/ to Packages/Sessions
  • Current example: Packages/Sessions -> /path/to/sessions/sublime
  • Unsupported layout: linking the repository root directly

The repository root remains a development workspace for Python tooling, Rust crates, docs, and planning files. Sublime does not discover the nested sublime/ directory automatically when the whole repository is linked as one package.

Building A Release Archive

Build a distributable archive from the sublime/ package root with:

uv run python scripts/build_sublime_package.py

The script writes dist/Sessions.sublime-package and excludes development-only artifacts such as tests/ and __pycache__/.

To build the current platform's Rust bridge/helper in release mode and bundle them automatically, run:

uv run python scripts/build_sublime_package.py --bundle-built-rust-binaries

To ship pre-built Rust binaries (or any other release-only files), pass an explicit layout rather than relying on discovery inside sublime/:

  • Repeat --bundle-file SOURCE=PATH_IN_PACKAGE so each PATH_IN_PACKAGE is the zip entry path (POSIX separators, no ..).
  • Or point --bundle-manifest at a JSON file: a list of objects with source and path_in_package strings. Relative source paths resolve from the manifest directory, which keeps CI artifacts and the install tree layout in one place.

Example manifest:

[
  {
    "source": "artifacts/darwin-aarch64/local_bridge",
    "path_in_package": "sessions/bin/local-bridge/darwin-aarch64/local_bridge"
  },
  {
    "source": "artifacts/linux-x86_64/session_helper",
    "path_in_package": "sessions/bin/remote-helper/linux-x86_64/session_helper"
  }
]

Rust Binary Status

Current product policy (local vs remote):

  • Remote host: Linux only. The remote session_helper is still resolved the same way: the remote machine downloads a pre-built binary from the Gitea generic registry (curl / wget), keyed by Linux platform tag and rust/ revision. No remote cargo build.
  • Local machine (editor side): Linux, macOS, and Windows are supported for running Sublime + this package. For day-to-day development, treat local_bridge as built on that machine (cargo build -p local_bridge, see Development). The plugin discovers rust/target/debug/local_bridge (or .exe on Windows) when no shipped binary is present under sessions/bin/....

Planned packaging (later): pre-built local_bridge per local OS/arch inside release archives; until then, local build is the supported path for macOS and Windows editors.

Current behavior:

  • Persistent bridge session: local_bridge --persistent runs as a long-lived process per host, communicating with session_helper over a single SSH stdio session. Python sends NDJSON request envelopes and receives async responses via a background reader thread. Each request gets a unique monotonic envelope_id to prevent response mis-routing under concurrency.
  • Download-only helper resolution: session_helper is downloaded directly by the remote machine from the Gitea generic registry (no cargo build fallback, no local download). The binary is identified by git revision + platform tag and cached at $HOME/.cache/sessions/helpers/<revision>/session_helper. If the download fails, the connection fails explicitly.
  • Required handshake fields: Handshake.remote_home and Handshake.arch are required (no Option, no fallback). The bridge merges helper ensure + launch into a single SSH command to avoid double authentication.
  • Async multiplexer: local_bridge acts as an async multiplexer with a background mirror thread performing BFS via session_helper. Python sends commands and manages async I/O; file opens are never blocked by mirror progress.
  • Reconnect: SessionsReconnectCurrentWorkspaceCommand runs in a background thread with ssh_prompt_callback, explicitly resets the bridge, and fails fast on handshake timeout (kills the bridge process immediately instead of returning a broken session).
  • After you open a remote file into the workspace cache, a normal editor save (Cmd+S / Ctrl+S) writes the local cache file and then pushes those bytes to the remote path automatically; Sessions: Save Remote File remains available as an explicit palette action.

Planned end-user install behavior (when releases ship all local binaries):

  • the local local_bridge binary will ship inside the released Sessions package for each supported local platform/architecture
  • the remote session_helper binary is downloaded on demand from the Gitea generic registry by the remote host itself (curl/wget)
  • the uploaded helper path is versioned as $HOME/.cache/sessions/helpers/<revision>/session_helper
  • the bridge rejects helper handshakes whose reported helper version does not match the bridge version, so mixed bridge/helper bundles fail fast
  • end users should not need a repository checkout or Cargo installed

Windows: running Sessions on the editor host

Typical flow when your PC is Windows and the SSH server is Linux:

  1. Sublime Text — Install a current build (ST4). This package is loaded from Packages/Sessions pointing at this repos sublime/ tree (symlink or copy; see Installing In Sublime Text above).
  2. SSH client — Use Windows optional OpenSSH Client or Git for Windows so ssh is on PATH. Put hosts in %USERPROFILE%\.ssh\config (same idea as ~/.ssh/config on Unix).
  3. Rust toolchain — Install rustup for Windows. From the repository root (the directory that contains rust/ and sublime/), run: cargo build --manifest-path rust/Cargo.toml -p local_bridge That produces rust\target\debug\local_bridge.exe, which the package picks up when no shipped binary exists under sessions/bin/local-bridge/windows-*/.
  4. Gitea / registry (if required) — If helper downloads need auth or a non-default host, set the sessions_gitea_* keys in Preferences → Settings for Sessions (see comments in sublime/Sessions.sublime-settings). The Linux remote still performs the download; these settings only affect URL and headers resolved on the editor side.
  5. Use the package — In Sublime, run the Sessions connect flow, pick your Host from SSH config, then open a remote folder. The first connection builds or reuses the persistent local_bridge session; the remote runs session_helper after fetching the Linux artifact.

If bridge commands fail with “local_bridge binary not found”, confirm the Cargo debug output path exists and that you opened a workspace whose sublime/ parent is the repo root (so rust/target/debug resolves correctly).

Troubleshooting: duplicate “Sessions:” commands in the palette

The package ships a single Sessions.sublime-commands file; if every Sessions entry appears twice, Sublime is almost certainly loading two package trees that both contribute commands (merged resources).

Typical causes:

  1. Unpacked + packaged copy — e.g. Packages/Sessions/ (dev link) and Installed Packages/Sessions.sublime-package from Package Control. Remove one: disable the package in Package Control, or delete the shipped archive, or set ignored_packages so only your dev link is active.
  2. Wrong link target — install only sublime/ as Packages/Sessions, not the repository root (see Installing In Sublime Text). A mis-linked tree can lead to confusing layouts; fix the symlink/copy so there is exactly one package root.
  3. Case-insensitive volume duplicates — on Windows or default macOS disks, avoid having two folder names that only differ in case both pointing at the same code.

cargo build --workspace does not register Sublime commands by itself; rebuilding Rust only changes binaries. If duplicates appeared right after a package change, reload the editor (Developer: Reload from Disk / restart) once duplicates are resolved so stale command tables are cleared.

The shipped Sessions.sublime-commands intentionally lists sessions_run_remote_python_tool twice with different args (lint vs format); that is not a duplicate installation.

Troubleshooting: Remote LSP install vs save-time ruff/pyright

Install Remote LSP Server (and the status panel) use the persistent bridges exec/once path to run install/remove scripts and probe_argv on the remote host. That checks that tools such as ruff --version exist in the remote environment.

Save-time diagnostics for mirrored .py files are controlled separately by sessions_remote_python_auto_diagnostics_on_save and sessions_remote_python_tool_pipeline in Sessions.sublime-settings. That pipeline still needs an active bridge session. If you see Rust bridge closed the persistent session (or similar), reconnect before expecting on-save lint to run.

Enable sessions_debug_trace_enabled for <Sublime cache>/Sessions/logs/debug-trace.log (correlates Python bridge.* lines with Rust bridge.rust_* when SESSIONS_BRIDGE_DIAG_LOG is set). After this package version, remote LSP install/remove/probe also emit [Sessions LSP] lines to the console (stderr).

Remote workspace window title

Generated .sublime-project files include a name field such as app [SSH: prod] (remote root basename plus SSH host alias) so the Sublime window title is easier to recognize when several projects are open.

Output panels and remote diagnostics

Sessions output panels (LSP status, tool output) scroll to the end after open so the latest lines stay visible. Remote Python diagnostics from Sessions use add_regions on the mirrored buffer; if underlines from another package (for example Sublime LSP) look stale until a window reload, that is usually editor refresh ordering rather than the remote ruff run being skipped.

Per-project LSP settings are owned by the LSP package you use, not by Sessions: configure them in .sublime-project / project-specific user settings as documented for that package (for example client enablement, server paths, and syntax scopes). Sessions writes/updates sessions_workspace_key and workspace display name, while preserving existing project settings (including settings.LSP) on rematerialize/reconnect. sessions_remote_code_servers in Sessions.sublime-settings is for Sessions transport wiring, not Sublime LSP UI.

Troubleshooting: git status noise (GitSavvy)

The Sessions sidebar mirror adds your workspace cache folder (under Sublimes cache path) to the project. That directory is a plain file tree, not a Git repository. GitSavvy (and similar packages) may still run git status for the current working directory and log errors such as:

fatal: not a git repository (or any of the parent directories): .git

with a stack trace under GitSavvy.sublime-package/.../status_bar.py or git_mixins/status.py. That comes from GitSavvy, not from Sessions own code.

Mitigations:

  1. Turn off GitSavvys status-bar polling (simplest): in Preferences → Package Settings → GitSavvy → Settings, set "git_status_in_status_bar": false. Branch/dirty text disappears globally, but GitSavvys palette commands still work when you are inside a real repo.
  2. Project-only override: if GitSavvy exposes project settings for your build, add the same key under the .sublime-project settings block for Sessions-only windows.
  3. Unrelated console line top level value must be an array: usually means some JSON (often folders in a .sublime-project) is not shaped the way Sublime expects. If it appears right after editing the project file, validate that "folders" is a JSON array of objects.

Sessions does not ship GitSavvy; avoiding non-repo git status spam is a third-party / user-settings concern.

Development

The expected workflow is:

  1. add or refine a skeleton
  2. add or update focused tests
  3. implement the smallest coherent behavior
  4. run the relevant checks
  5. commit only after pre-commit passes
Description
Multiple session agent windows in SublimeText
Readme 25 MiB
Languages
Python 72.1%
Rust 27.9%