How OpenWork Workspace and Session Management Handles Multiple Worktrees
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 (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 (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.
// 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 (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 (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.
// 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 defines sessionId and directory fields, with the latter capable of storing worktree-specific paths.
The Headless Threads client (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.
// 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:
- The server stores a canonical
directoryrepresenting the primary checkout - The desktop client opens additional Git worktrees and passes their absolute paths via the
worktreefield - All subsequent API calls—including file reads, extension executions, and AI-generated content—resolve the effective path by preferring
worktreethen falling back todirectory
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) andworktree(optional override) fields defined inpackages/types/src/workspace.ts - Workspace IDs are deterministic hashes generated by
workspaceIdForPathandworkspaceIdForRemoteinapps/server/src/workspaces.ts, ensuring consistent identity across worktrees - Runtime resolution explicitly prefers the
worktreevalue overdirectoryas implemented inopenwork-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 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. 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, 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →