Skip to content

[Bug]: Docker conversation runtime creates linked worktree across incompatible host/container filesystem namespaces #5307

Description

@hectormhr

Is there an existing issue for the same bug?

  • I have searched existing issues and this is not a duplicate.

Bug Description

When Agent Server is configured with OH_CONVERSATION_RUNTIME=docker, creating a conversation with worktree=true produces a linked Git worktree that is valid inside the conversation container but immediately appears as prunable from the host.

In the reproduced case, the host repository is bind-mounted into the conversation container as /workspace. The inner Agent Server then creates the requested linked worktree under /tmp/conversation-worktrees/<conversation-id>/workspace.

The worktree’s .git file points to:

gitdir: /workspace/.git/worktrees/workspace

while the host repository’s corresponding .git/worktrees/workspace/gitdir points back to:

/tmp/conversation-worktrees/<conversation-id>/workspace/.git

That /tmp/... path exists only inside the container filesystem namespace, so the host cannot resolve it.

As a result, while the conversation container is still running and the worktree is healthy inside it, the host repository reports the same worktree as:

prunable gitdir file points to non-existent location

This was reproduced with OpenHands Agent Server 1.49.4 and Agent Canvas 1.22.0 on Docker Desktop for macOS.

Expected Behavior

A Docker-backed conversation created with worktree=true should use an isolated Git worktree whose metadata remains valid for the lifetime of the conversation.

The linked worktree should be valid both from inside the conversation runtime and from the host repository that owns the Git metadata.

In particular, the host repository should not report the active conversation worktree as:

prunable gitdir file points to non-existent location

while the conversation container is still running.

Actual Behavior

The conversation container creates the requested worktree successfully and uses the expected branch:

## openhands/<conversation-id>...origin/main

However, the linked-worktree metadata spans two filesystem namespaces.

Inside the container, the worktree lives at:

/tmp/conversation-worktrees/<conversation-id>/workspace

and its .git file points to:

/workspace/.git/worktrees/workspace

The corresponding Git metadata in the host repository points back to:

/tmp/conversation-worktrees/<conversation-id>/workspace/.git

Because that /tmp/... path exists only inside the container, the host repository immediately considers the worktree invalid:

prunable gitdir file points to non-existent location

This occurs while the conversation container is still running, so the condition is not caused by container teardown or cleanup.

Steps to Reproduce

  1. Start Agent Canvas with per-conversation Docker runtime enabled, for example:
OH_CONVERSATION_RUNTIME=docker \
OH_CONVERSATION_IMAGE=ghcr.io/openhands/agent-server:1.49.4-python \
agent-canvas
  1. Select an existing Git repository as the workspace.

  2. Create a new conversation using New worktree.

  3. Send any minimal message so the conversation is actually created.

  4. Identify the conversation container and verify that the original repository is mounted at /workspace.

  5. Inside the container, inspect the generated worktree:

find /tmp/conversation-worktrees -maxdepth 3 -print

A worktree is created under:

/tmp/conversation-worktrees/<conversation-id>/workspace
  1. Inspect the worktree’s .git file:
cat /tmp/conversation-worktrees/<conversation-id>/workspace/.git

Observed:

gitdir: /workspace/.git/worktrees/workspace
  1. On the host, inspect the corresponding reverse pointer:
cat <host-repo>/.git/worktrees/workspace/gitdir

Observed:

/tmp/conversation-worktrees/<conversation-id>/workspace/.git
  1. While the conversation container is still running, check the host repository’s worktree state:
git -C <host-repo> worktree list --porcelain

Observed:

worktree /tmp/conversation-worktrees/<conversation-id>/workspace
HEAD <sha>
branch refs/heads/openhands/<conversation-id>
prunable gitdir file points to non-existent location
  1. At the same time, verify the worktree from inside the container:
git -C /tmp/conversation-worktrees/<conversation-id>/workspace \
  status --short --branch

Observed:

## openhands/<conversation-id>...origin/main

This shows the same linked worktree is healthy inside the container but already invalid/prunable from the host.

Acceptance Criteria

  • Creating a Docker-backed conversation with worktree=true produces a Git worktree that remains valid for the lifetime of the conversation.

  • While the conversation container is running, git worktree list --porcelain on the host repository does not report the conversation worktree as prunable.

  • The worktree remains on the expected openhands/<conversation-id> branch.

  • The worktree is valid both from inside the conversation runtime and from the host repository that owns the Git metadata.

  • Automated coverage includes the combined case of:

    • conversation_runtime=docker
    • an existing Git repository workspace
    • worktree=true
  • The regression test verifies the worktree state from both the inner container/runtime and the host repository.

Installation Method

Agent Canvas 1.22.0 using its OpenHands Agent Server integration, with Docker conversation image ghcr.io/openhands/agent-server:1.49.4-python

If you selected "Other", please specify

No response

SDK Version

1.49.4

Version Confirmation

  • I have confirmed this bug exists on the LATEST version of OpenHands SDK

Python Version

No response

Model Name (if applicable)

No response

Operating System

MacOS

Logs and Error Messages

Host-side Git state while the conversation container is still running:

worktree /Users/hhernand/Development/repos/agent-orchestrator-sandbox
HEAD dc3dee71e13e2f5a8180fce30bf1362b6c6d7b10
branch refs/heads/main

worktree /tmp/conversation-worktrees/5725b8f5-28f5-4cee-9d68-a95832c18258/workspace
HEAD dc3dee71e13e2f5a8180fce30bf1362b6c6d7b10
branch refs/heads/openhands/5725b8f5-28f5-4cee-9d68-a95832c18258
prunable gitdir file points to non-existent location

At the same time, inside the active conversation container:

## openhands/5725b8f5-28f5-4cee-9d68-a95832c18258...origin/main

The worktree .git file inside the container contains:

gitdir: /workspace/.git/worktrees/workspace

while the host repository metadata contains:

/tmp/conversation-worktrees/5725b8f5-28f5-4cee-9d68-a95832c18258/workspace/.git

The reverse pointer therefore targets a path that exists only inside the container filesystem namespace.

Minimal Code Sample

No response

Screenshots and Additional Context

This appears to be an integration regression between two features that were introduced separately.

The conversation worktree feature was added in PR #3180 (feat(agent-server): add conversation worktree option) and merged on 9 May 2026. Its documented validation covers creation of /tmp/conversation-worktrees/<conversation_id> worktrees and openhands/<conversation_id> branches, but the end-to-end test keeps both the repository and the worktree in the same filesystem namespace.

The per-conversation Docker runtime was added later in PR #3403 (feat(agent-server): add docker runtime mode for per-conversation containers), merged on 16 September 2026 and released in v1.49.0. That PR includes substantial Docker-runtime validation, including 205 focused tests and a live Docker-backed conversation.

However, I could not find evidence in the documented validation or relevant Docker-runtime tests that the combined case was exercised:

conversation_runtime=docker
+ existing Git repository
+ worktree=true

The failure reproduced here occurs specifically in that combination:

  • the Git common directory lives in the host repository bind-mounted at /workspace;
  • the linked worktree itself is created under the container-private /tmp/conversation-worktrees/...;
  • Git records absolute paths between those two locations;
  • the reverse pointer stored in the host repository therefore references a path that only exists inside the container.

A regression test that creates a Docker-backed conversation with worktree=true and validates the resulting worktree from both the inner runtime and the host repository should reproduce the issue directly.

For completeness: this was reproduced on 1.49.4. The current release is 1.49.5, and the relevant worktree creation logic is unchanged between 1.49.4 and 1.49.5, while the Docker conversation start path still rewrites the inner workspace to /workspace. I have not yet performed the live reproduction on 1.49.5, so I have not marked the “confirmed on latest version” checkbox.


OpenHands AI triage

The following comments and acceptance criteria were added by the OpenHands AI agent.

Triage

Valid bug, confirmed against main. The Docker conversation runtime rewrites the inner workspace to /workspace and bind-mounts the host repository there (docker_runtime/routers.py sets body["workspace"] = {"working_dir": "/workspace"}; registry.py mounts workspace_dir -> /workspace). The inner agent-server then creates the linked worktree under its configured conversation_worktree_root, defaulting to /tmp/conversation-worktrees (config.py, conversation_service.py). Git records absolute paths: the worktree .git file points at /workspace/.git/worktrees/<name>, while the host repo's .git/worktrees/<name>/gitdir records the container-only /tmp/... path. The host therefore reports the still-running worktree as prunable.

Scope: make the conversation-worktree path contract resolvable from both filesystem namespaces for Docker-backed worktree creation. The worktree the inner agent-server creates must be reachable from the host repository that owns the Git metadata, so the reverse pointer the host stores resolves to a real path. Non-goals:

  • Do not change worktree behavior for conversation_runtime=local, the openhands/<conversation-id> branch naming, the start-point resolution policy, or worktree cleanup semantics.
  • Do not add client- or host-specific special-case branches in SDK core; keep any namespace/mount assumption contained to the Docker runtime, config, or workspace layer per CONTRIBUTING.md.
  • Do not attempt to keep the path valid after the owning mount/container is gone.

Acceptance Criteria

  • Creating a conversation with conversation_runtime=docker, an existing Git repository as the workspace, and worktree=true creates a linked worktree on the expected openhands/<conversation-id> branch while the container is running.
  • While the conversation container is still running, git -C <host-repo> worktree list --porcelain lists that worktree without prunable gitdir file points to non-existent location, and the host repository does not report the active worktree as prunable.
  • The absolute path stored in the host repository's .git/worktrees/<name>/gitdir resolves to an existing path on the host for the lifetime of the conversation, and the worktree .git file resolves inside the container.
  • The same worktree is reported valid from inside the conversation runtime (git -C <worktree> status --short --branch inside the container) and from the host repository, at the same time.
  • Non-Docker behavior is unchanged: conversation_runtime=local with worktree=true still uses the existing conversation_worktree_root default, and existing worktree tests still pass.
  • A non-Git workspace with worktree=true under conversation_runtime=docker still starts successfully without creating a worktree (existing fallback preserved).
  • After the conversation ends, the worktree and its openhands/<conversation-id> branch are cleaned up, or the host repository no longer reports a stale worktrees/<name> registration, so repeated conversations in the same repository do not accumulate prunable entries.
  • Automated coverage exercises the combined case conversation_runtime=docker + an existing Git repository workspace + worktree=true, and asserts the host-side git worktree list --porcelain view as well as the inner-runtime view. A test that only inspects the inner runtime, or that never checks the host repository, does not satisfy this.
  • Any affected production artifact (packaged agent-server or Docker image) is exercised, not only a mocked unit test, consistent with repository guidance for mount/packaging changes.

Activity

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

    bugSomething isn't workingpriority:highFor bugs, affecting nearly all users and degrading performance or UX.ready-for-devIssue meets development readiness criteria

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions