Skip to content

bug: regular sandbox → open_sandbox_direct → clone_repo leaves the thread with an unusable sandbox #173

Open

Description

@MingweiSamuel

Summary

In one thread I used a regular (jj workspace) sandbox, then open_sandbox_direct on a parent directory, then clone_repo again on the original repo. That left the thread unable to run any command. The second clone_repo reports success, but every execute_command fails with a jj error before running, and re-initializing is refused. The only way forward is to spawn a child thread.

Possibly related: #132 (sandbox position reset on suspend/reload).

Setup (generic names)

  • REPO = /home/user/ws/src/repo-a: a git repo with jj (.jj present), several bookmarks.
  • WS = /home/user/ws: a parent directory that contains REPO (and sibling repos under WS/src/). WS itself is not a git or jj repo.

Steps to reproduce

  1. clone_repo(REPO). This works normally and creates a jj workspace sandbox at REPO/.infinity/.sandboxes/.tmpXXXX with a sandbox-<thread-id> bookmark. A long session of edits and commands runs fine.
  2. open_sandbox_direct(WS), to read and build sibling repos under WS/src/. It returns Repository initialized (Direct mode …), even though WS is not a git repo.
  3. Keep working. Observed after step 2:
    • The first few execute_command calls still started in the old sandbox path (pwd = REPO/.infinity/.sandboxes/.tmpXXXX). Later calls started in WS. The cwd switch is not immediate.
    • The old sandbox became read-only. Writes there fail with EROFS, because it is nested inside WS, which direct mode mounts read-only by default.
    • cd <absolute path under WS> is rejected with Do not cd to the absolute path … Commands are already executed in a sandboxed copy of that repository. This includes cd into the old sandbox path and into sibling repos.
    • Edits outside direct mode's scope work through additional_permissions (write-orig, write:/some/path). But each permission scope appears to get a different $TMPDIR, so a temp file written under one permission set is gone in the next call. I lost a build log this way.
  4. At some point, I'm not sure when, the old sandbox directory REPO/.infinity/.sandboxes/.tmpXXXX and its sandbox-<thread-id> bookmark were deleted. No data was lost, because the work had been committed to another bookmark, but nothing reported the cleanup.
  5. clone_repo(REPO) again, to get a writable regular sandbox back. It returns Repository initialized (using Jujutsu workspaces).
  6. Every execute_command, even pwd, now fails before running with:
    Error: jujutsu command failed: jj failed: Error: There is no jj repo in "."
    
  7. clone_repo(REPO) again fails with:
    Error: A repository has already been initialized for this thread. Re-initializing with a different repository is not supported.
    
    (It is the same repository as step 1 and step 5.)

Expected

  • Switching modes should either be supported cleanly or be refused up front. Any of these would work:
    • open_sandbox_direct should not silently degrade the existing regular sandbox, by making it read-only or deleting it.
    • Or it should refuse or warn when the thread already has a regular sandbox.
    • Or it should document that the regular sandbox is dropped.
  • A second clone_repo should yield a working sandbox, or fail loudly, not report success and leave a broken one.
  • If a thread's sandbox is broken, there should be a way to re-initialize it, at least for the same repo.
  • Ideally the cwd switch after open_sandbox_direct should be immediate and consistent.
  • $TMPDIR should be stable across calls that differ only in additional_permissions. Or the docs should say it isn't.

Actual

The thread is permanently unable to run commands. Workaround: spawn a child thread, which gets a fresh sandbox.

Notes

  • The tool docs say open_sandbox_direct is for when clone_repo fails, so mixing the two may be off the happy path. But the use case is real: a direct sandbox on a parent directory is how I reached sibling repos from a thread whose regular sandbox covers only one repo.
  • A nice alternative for that use case would be a way to add a second regular sandbox for another repo in the same thread, or a supported read-only view of sibling paths.

Activity

  1. kaluli123123 commented on Nov 3, 2026

    @kaluli123123

    Implemented in draft PR #183 (commit 4b2066306e681f92e08eea0fbd7792886fcbc862).

    • Root cause: Initialization changed RepoState but reused a thread-keyed cached directory created for a different repository or sandbox mode.
    • Fix boundary: Reject Direct reinitialization before side effects; preserve supported Direct-to-managed upgrades while clearing the old Direct cache after successful repository detection.
    • Focused checks: clone_repo_switch: 6 passed, including cached Direct upgrades, managed/Direct reinitialization refusal, non-VCS upgrades and failed clone preservation.
    • Runtime proof: Actual cargo-run RAP smoke populated Direct cache, upgraded, executed writes only in the sandbox and confirmed original marker absent; non-VCS Direct upgrade moved cwd and kept originals unchanged.
    • Full all-target test run: 359 passed, 7 failed, 0 ignored. The exact same seven TUI snapshots fail on clean base 1abc6fcff1f186124f924b56aa92c3a3684c90c5 (355 passed, 7 failed). No baselines or tolerances were changed.
    • Doctests: 45 passed, 1 ignored. Rust formatting, workspace Clippy with warnings denied, and all-target compilation passed. Independent read-only review found no blocking issues.

    I am maintaining this proposed fix. The PR remains Draft, not ready or merged: full checks are not green, remote CI currently lists no checks, and maintainer review remains outstanding. The repository script skipped docs/UI Prettier because node_modules are absent; optional browser e2e and THIRD-PARTY verification were not reached after the test gate failed. No live model account was used. No merge, Issue closure, or deployment has been performed.

    This prevents new mode/cache corruption; it does not claim automatic repair of sessions already corrupted by older versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions