# How OpenWork Workspace and Session Management Handles Multiple Worktrees

> Discover how OpenWork workspace and session management supports multiple worktrees using a dual-path system for efficient branch specific editing and parallel testing.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-09

---

**OpenWork treats workspaces as logical file collections backed by one or more Git worktrees, using a dual-path system where an optional `worktree` field overrides the canonical `directory` path to enable branch-specific editing and parallel testing scenarios.**

The `different-ai/openwork` repository implements a sophisticated workspace model that decouples logical workspace identity from physical filesystem locations. This architecture allows developers to run multiple Git worktrees simultaneously while maintaining a consistent workspace ID across different branches and checkouts. Understanding how OpenWork workspace and session management handles multiple worktrees requires examining the type definitions, ID generation algorithms, and runtime resolution strategies implemented across the codebase.

## Workspace Schema Design with Directory and Worktree Fields

At the core of OpenWork's multi-worktree support lies the `WorkspaceWire` type defined in [`packages/types/src/workspace.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/workspace.ts) (lines 15-27). This interface establishes a dual-path architecture where each workspace record contains both a **`directory`** field representing the canonical path and an optional **`worktree`** field pointing to an alternate checkout location.

Both fields are typed as nullable strings and are always emitted by the server and the desktop Electron IPC bridge. This design allows the client to choose the most appropriate path at runtime based on the execution context. The `directory` field typically references the primary Git checkout, while `worktree` enables the workspace to pivot to branch-specific directories without altering the workspace's logical identity.

## Deterministic Workspace ID Generation

Workspace IDs in OpenWork are deterministic hashes derived from path information, ensuring that the same logical workspace resolves to the same identifier regardless of which worktree is currently active. The implementation in [`apps/server/src/workspaces.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/workspaces.ts) (lines 5-30) provides distinct functions for local and remote scenarios.

### Local Workspace IDs

For local workspaces, the system uses `workspaceIdForPath` to generate IDs based on the resolved filesystem path. This ensures that switching between worktrees on the same machine produces consistent identifiers when the underlying repository matches.

### Remote Workspace IDs

Remote workspaces utilize `workspaceIdForRemote` (also referenced as `workspaceIdForOpenwork`), which constructs IDs by hashing the base URL combined with either the `directory` or `worktree` value. This approach allows remote servers to distinguish between different checkout locations while maintaining stable references for API calls.

```typescript
// Generate a workspace ID that accounts for a worktree
import { workspaceIdForRemote } from "@/apps/server/src/workspaces";

const id = workspaceIdForRemote(
  "https://example.com/openwork",
  "/tmp/my-repo/worktrees/feature-xyz"
); // → ws_… (deterministic hash)

```

## Runtime Path Resolution Strategy

When the server needs to start an OpenCode engine or resolve file paths, it employs a clear precedence rule: **prefer `worktree`, fall back to `directory`**. This logic appears explicitly in [`apps/server/src/opencode-plugins/openwork-extensions-preview.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/opencode-plugins/openwork-extensions-preview.ts) (lines 245-253), where the runtime evaluates `context.worktree?.trim() || context.directory?.trim()`.

The server locates eligible workspaces using `findManagedEngineWorkspace` in [`apps/server/src/workspaces.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/workspaces.ts) (lines 85-87), which searches for the first local workspace containing a non-empty resolved path. If both fields are populated, the worktree takes precedence, enabling dynamic switching between branches without restarting the workspace session.

```typescript
// Resolve the effective path inside an extension
import { optionalStringProperty } from "@/utils";

function resolvePath(context: any) {
  // Prefer worktree, fall back to directory
  const worktree = optionalStringProperty(context, "worktree");
  const directory = optionalStringProperty(context, "directory");
  return worktree?.trim() || directory?.trim() || "";
}

```

## Session Management Across Worktrees

A **session** represents an isolated execution context that lives inside a workspace and inherits its filesystem bindings. The `OpenWorkContext` schema in [`packages/types/src/openwork-context.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/openwork-context.ts) defines `sessionId` and `directory` fields, with the latter capable of storing worktree-specific paths.

The Headless Threads client ([`packages/headless-threads/src/client.ts`](https://github.com/different-ai/openwork/blob/main/packages/headless-threads/src/client.ts)) constructs REST URLs following the pattern `/sessions/:threadId`, where `threadId` corresponds to the session ID. While sessions typically inherit the `worktree` path from their parent workspace, they may explicitly override this value by passing a `worktree` parameter in the request payload, creating isolated execution contexts even within shared workspaces.

```typescript
// Creating a new session bound to a specific worktree
import { createThread } from "@openwork/headless-threads/client";

await createThread({
  workspaceId: id,
  // The worktree path is sent in the session payload
  payload: { worktree: "/tmp/my-repo/worktrees/feature-xyz" },
});

```

## Supporting Multiple Concurrent Worktrees

Because `directory` and `worktree` operate as independent fields, OpenWork enables sophisticated multi-worktree workflows:

1. The server stores a canonical `directory` representing the primary checkout
2. The desktop client opens additional Git worktrees and passes their absolute paths via the `worktree` field
3. All subsequent API calls—including file reads, extension executions, and AI-generated content—resolve the effective path by preferring `worktree` then falling back to `directory`

This architecture supports critical development scenarios:

- **Branch-specific editing**: Each branch lives in its own worktree while sharing the same workspace ID and session history
- **Parallel testing**: CI runners spin up isolated worktrees without interfering with the primary developer checkout or requiring duplicate workspace registrations

## Summary

- OpenWork workspaces use a dual-path schema with `directory` (canonical) and `worktree` (optional override) fields defined in [`packages/types/src/workspace.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/workspace.ts)
- Workspace IDs are deterministic hashes generated by `workspaceIdForPath` and `workspaceIdForRemote` in [`apps/server/src/workspaces.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/workspaces.ts), ensuring consistent identity across worktrees
- Runtime resolution explicitly prefers the `worktree` value over `directory` as implemented in [`openwork-extensions-preview.ts`](https://github.com/different-ai/openwork/blob/main/openwork-extensions-preview.ts)
- Sessions inherit parent workspace paths but can override them via payload parameters to the Headless Threads client
- The system supports simultaneous multiple worktrees per logical workspace, enabling branch-specific editing and parallel CI testing

## Frequently Asked Questions

### How does OpenWork distinguish between a primary repository and a Git worktree?

The `WorkspaceWire` type stores both a canonical `directory` path and an optional `worktree` path. When both exist, the runtime resolution logic in [`apps/server/src/opencode-plugins/openwork-extensions-preview.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/opencode-plugins/openwork-extensions-preview.ts) explicitly prefers the `worktree` value using the expression `context.worktree?.trim() || context.directory?.trim()`, allowing the system to treat the worktree as an override while preserving the primary directory reference for fallback purposes.

### Can multiple worktrees share the same workspace ID in OpenWork?

Yes. Workspace IDs are deterministic hashes derived from path information using functions in [`apps/server/src/workspaces.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/workspaces.ts). Since the ID generation logic accepts either `directory` or `worktree` paths as input parameters, multiple physical checkouts can map to the same logical workspace identity, enabling seamless branch-specific editing scenarios without fragmenting session history.

### How does session management handle worktree-specific file operations?

Sessions inherit the `worktree` path from their parent workspace according to the `OpenWorkContext` schema in [`packages/types/src/openwork-context.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/openwork-context.ts), but individual sessions can override this by passing an explicit `worktree` value in the request payload to the Headless Threads client. This ensures isolated file contexts even when multiple sessions operate within the same logical workspace.

### What happens if both directory and worktree fields are null or empty?

The resolution logic implemented in [`openwork-extensions-preview.ts`](https://github.com/different-ai/openwork/blob/main/openwork-extensions-preview.ts) uses the expression `context.worktree?.trim() || context.directory?.trim()`, which falls back to the `directory` field if `worktree` is falsy, and ultimately returns an empty string if both are undefined. Production deployments typically require at least one valid path for the OpenCode engine to initialize successfully.