Skip to content

dsh-layerfs 0.1.0 on OrbStack (kernel 7.0.14): 8/10 scripted scenarios stop at workspace.unmount (stage Join); Docker Desktop 4.22.0 cannot create Sandboxes (API 1.43 < 1.54) #466

Open

Description

@mengmengmengqiang

Summary

The dsh-layerfs 0.1.0 scripted scenario suite (dsh web, macOS worker + sealed Linux bundle, sibling Sandboxes) fails on 8 of 10 scenarios on macOS/OrbStack, every failure at workspace.unmount:

workspace.unmount → Failed
connection drain stopped at Join: loops { configured: 2, entered: 2, exited: 0, joined: 0 },
work { active: 0, retained: 0, requests: 65, completed: 63, terminal: true }

The mount is already gone (/proc/mounts has no fuse entry, no process holds /workspaces/*), but both FUSE receive loops stay blocked, the hard-coded 5 s DRAIN_WAIT expires, and sandbox.close then reports MountsActive. Unmounts without recent I/O sometimes succeed, so results vary run to run (3/10, 1/10, 2/10 across three OrbStack runs on the same install).

This is integration-level evidence for the defect family already tracked in:

The new data point is the dsh-layerfs scenario matrix (real dsh agent loop, 10 scripted scenarios, both cli and sdk forms, 2 at a time) on this kernel, plus a second, unrelated environment blocker: Docker Desktop 4.22.0 cannot run LayerFS Sandboxes at all (engine API 1.43).

Filed first in the fork: tokenaissance#1

Environment

Host macOS 26.5.1 (Darwin 25.5.0, arm64)
Docker Engine OrbStack, Docker 29.4.0, API 1.54, kernel 7.0.14-orbstack-00380-ga7e0a2dc9535
Node / pnpm / python 22.23.2 / 11.28.5 / 3.9.18
dsh 0.2.1-rc.2, commit 687f282e8625c6b8d11ff58211a90524be2e4317
dsh-layerfs 0.1.0, commit baac1b92b97f0bd492002c285f6a13401b4f4c0b
LayerFS commit 9f97a7ec95b662e9cb288704afa165feff132cfa (v1.1.0), sealed build build-linux-a6848f09a5db4d528b9a68f9bd24a6c1, manifest sha256 f901a26d92c2b29b7ae6a14824136385a24ff869f1149d45b8ff9b775e9e6eb5
Runtime image sha256:d93570c6600be9eb0214efb2038a1f47f186f56e4715934188c61eace9a957c9 (plugin tools/runtime/Dockerfile, cargo 1.85.1 verified)
Profile default ~/.dsh/profiles/web, layerfs-host with image, bundle, containerPrefix: layerfs-dsh-

Reproduction

  1. Install per the README (LayerFS checkout at the pinned commit, sealed bundle via tools/worktree.py build --platform linux --profile release, runtime image, pnpm install + pnpm run build with DSH_SOURCE, dsh plugin --profile web add, layerfs-host settings).
  2. Start dsh web --no-open --port 3080, open the LayerFS page → Tests.
  3. Press Run all once (up to 2 at a time; 10 scenarios, 5 cli + 5 sdk).

GET /layerfs/status reports "missing":[]; All projects shows Docker 29.4.0 green and "Store calls · worker ready" green. Checkpoints (no Sandbox) always passes.

Results (three OrbStack runs, same install)

Scenario Run A (first) Run B (17 Sandboxes accumulated) Run C (clean state, 0 projects)
Tour FAIL 4/14 FAIL 4/14 FAIL 6/14
Parallel Branches PASS FAIL 5/14 FAIL 3/14
Conflicts FAIL 6/14 FAIL 6/14 FAIL 6/14
Commit & merge PASS FAIL 4/14 PASS
One Sandbox, two Workspaces FAIL 4/7 FAIL 4/7 FAIL 4/7
5 agents FAIL 44/197 FAIL 27/197 FAIL 29/197
Shop FAIL 4/18 FAIL 2/18 FAIL 1/18
Depot FAIL 1/98 FAIL 1/98 FAIL 1/98
Crew FAIL 3/967 FAIL 3/967 FAIL 3/967
Checkpoints PASS PASS PASS

Every failure is workspace.unmount:Failed followed by sandbox.close: MountsActive. Run C (clean, no leftover Sandboxes/volumes) still fails 8/10, so accumulated load is not the cause; it is timing-sensitive.

Exact daemon failure

OperationFailure { request: Unmount(UnmountControl { token: …, discard: false }),
  cause: Retained(TeardownCustody { stage: Join, detached: true,
    work: Some(MountWork { loops_configured: 2, loops_entered: 2, loops_exited: 0, loops_joined: 0,
      active: 0, retained: 0, completed: 63, served: 63, inline: 0, refused: 2, terminal: 0, forget_units: 0 }),
    detail: "connection drain stopped at Join: loops SessionSnapshot { phase: Serving, revision: 6,
      configured: 2, created: 2, entered: 2, exited: 0, joined: 0 },
      work Some(MountWork { active: 0, retained: 0, requests: 65, completed: 63, terminal: true })" }) }

workspace.unmount took 5 060 ms; the deadline is DRAIN_WAIT = 5 s (core/crates/layerfs-daemon/src/application/filesystem.rs:54, drain path core/crates/layerfs-daemon/src/control/detach.rs:85). After the failure, inside the Sandbox container:

  • grep fuse /proc/mounts → none
  • no process with cwd/fd under /workspaces/*
  • /dev/fuse present; Sandbox runs with CAP_SYS_ADMIN, --device /dev/fuse, apparmor=unconfined

So plain umount2 succeeded (detached: true), but the receive loops never observed the connection close and joined nothing.

Separate environment blocker: Docker Desktop 4.22.0

Docker Desktop 4.22.0 (engine 24.0.5, API 1.43, kernel 5.15.49-linuxkit-pr) cannot create a Sandbox at all: the addon hard-codes /v1.54, and the engine answers

HTTP 400 {"message":"client version 1.54 is too new. Maximum supported API version is 1.43"}

Reproduced directly (POST /v1.54/volumes/create → 400, /v1.43/... → 201). Not a LayerFS bug, but the README's macOS requirement should state the minimum Docker API version (or Docker Desktop release) explicitly; "Docker Desktop" alone is not sufficient for an old install.

Proposed fix

No LayerFS code was changed to produce this report; the scenario projects and traces were retained for inspection.

Activity

  1. yifanxuaaa commented on Oct 27, 2026

    @yifanxuaaa
    Contributor

    Thanks for the detailed matrix. The three runs and the exact TeardownCustody output are very useful.

    A lot has changed since v1.1.0 (9f97a7ec95). Could you update to the latest main (currently ea11b00408) and rebuild the sealed bundle, then run Run all once more on OrbStack?

    If it still fails at Join on the latest main, could you open a PR with the fix? We don't have OrbStack (or a 7.0.14 kernel) to reproduce this, so you're in the best position to write and check it. Making detach wake or abort the receive loops (your second option, per #460) is the direction we'd prefer over only making the deadline configurable, since a longer wait just delays the same wedge. Please link #426, #460 and this issue. Include a before/after from your scenario matrix, plus a test that fails without the fix where that's practical. AGENTS.md has the repo rules for tests and commit messages.

  2. mengmengmengqiang commented on Oct 28, 2026

    @mengmengmengqiang
    ContributorAuthor

    Updated to the latest main and rebuilt the sealed bundle — the Join stall is still here on a newer OrbStack kernel, but #481 now recovers it.

    Answering the request to retest on latest main. Environment below; probe uses the public SDK only.

    Host macOS 26.5.1 (Darwin 25.5.0, arm64)
    Docker Engine OrbStack 29.8.2, API 1.54, kernel 7.2.9-orbstack-00391-g2b298d05fbd4 (newer than 7.0.14 in the report)
    Node 22.23.2
    LayerFS 9f97a7ec95 (v1.1.0) → 582bc786f9 (main; includes #481, #488, #476, #463 layout)
    Bundle rebuilt from that checkout with tools/worktree.py build --platform linux --profile release --jobs 6, sealed as build-linux-aa6c5132eb224caaa85dbbfb26a4b010
    Runtime image unchanged, sha256:d93570c6… (node/rust/chromium only; the daemon comes from the bundle)
    dsh-layerfs baac1b9 → b9bf3bc (Follow LayerFS's #463 layout; pins fabefdf87a)

    Probe

    Same shape as the earlier #426 comment: for each iteration sandbox.create() → workspace.mount(sandbox, branch) immediately → sleep 1500 → workspace.unmount(ws); on refusal workspace.unmount(ws, true, true); then sandbox.close().

    Results

    Build Mount after create Plain unmount Then Outcome
    v1.1.0 9f97a7ec 62 ms Failed (Join), 5 064 ms sandbox.close → MountsActive wedge, only docker rm -f clears it (1/1)
    main 582bc786 ×6 13–79 ms Failed (Join), 5 044–5 504 ms — 6/6 --force → ForceUnmounted close → Closed 6/6, 0/6 wedged

    Same refusal signature as before the upgrade:

    Failed: OperationFailure { request: Unmount(… discard: false, force: false …),
      cause: Retained(TeardownCustody { stage: Join, detached: true,
        … loops_exited: 0, joined: 0 … }) }
    

    Control on the same upgraded build: delay 2 500 ms between sandbox.create() and the mount → plain Unmounted 3/3 (21–27 ms). On v1.1.0 the delayed loop was 7/7, and six Sandboxes in parallel with the 2.5 s delay were 6/6. So the stall tracks the early-mount window, not load; on this kernel it is deterministic there.

    What this says

    Since the early-mount stall reproduces 6/6 here, I can also test a candidate fix for #460 (waking / aborting the receive loops) against these runs, and run Run all once the scripted model is enabled.

  3. mengmengmengqiang commented on Oct 28, 2026

    @mengmengmengqiang
    ContributorAuthor

    Follow-up: I wrote the fix in the direction you suggested — the daemon now wakes the stalled connection itself instead of adding to the deadline.

    PR: #496 (fix(daemon): end a stalled normal unmount at Join with its own abort write).

    On a Join stop the normal unmount makes one abort_detached write on the already-detached connection and runs the same forced drain, then revokes and closes as usual. Only a refused or short write, or a second stop, keeps custody.

    Same probe as my previous comment, same machine (kernel 7.2.9-orbstack-00391), this branch's daemon:

    plain unmount sandbox.close
    582bc786 Failed (Join), 5.0–5.5 s — 6/6 only after --force (6/6)
    PR #496 Unmounted 6/6 (5.05–5.08 s: the drain deadline, then the abort) Closed 6/6

    The new stalled_unmount test fails on the parent (which keeps Retained { stage: Join }) and passes with the change; the docs say what --force is still for. Happy to change the design (opt-in flag, or a different abort timing) if you'd prefer that over doing it inside a normal unmount.

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

    orbstackSeen on OrbStack (macOS Docker engine)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions