How OpenWork Manages Session and Workspace State in the Desktop Application

OpenWork uses a four-layer state architecture that combines Zod-validated read models for session data, a SQLite-backed workspace-scoped KV store, atomic session-group state management with serialized updates, and deterministic workspace identification to maintain reliable state across the desktop application.

Managing complex state in desktop applications requires careful separation between transient UI data and persistent configuration. In the different-ai/openwork repository, the desktop application implements a robust system for handling both interactive conversation data from the OpenCode SDK and local workspace-specific settings. Understanding how session and workspace state is managed reveals a design that prioritizes type safety, data consistency, and reliable workspace identification through four complementary architectural layers.

The Four-Layer State Architecture

OpenWork organizes state management into distinct layers, each handling specific concerns from SDK response validation to local SQLite persistence.

Session Read Model Layer

The first layer normalizes raw JSON returned by the OpenCode SDK into typed, safe objects that the UI can rely on. Located in dev/apps/server/src/session-read-model.ts, this layer defines Zod schemas for time stamps, summaries, status updates, todos, and messages.

The SDK returns several response shapes including SessionGetResponse, SessionMessagesResponse2, SessionStatusResponse, and SessionTodoResponse. The buildSessionList and related helpers use parseOrThrow to validate these payloads against strict schemas. Invalid payloads raise an ApiError with code 502, allowing the desktop app to surface clear error messages rather than propagating malformed data.

Workspace-KV Store Layer

For per-workspace mutable state such as session-group assignments and custom configuration, OpenWork implements a generic key-value store backed by SQLite. The implementation in dev/apps/server/src/workspace-kv-store.ts provides createWorkspaceKvStore<T>, which builds a typed store for any JSON-serializable type.

This layer creates a table with workspace_id TEXT PRIMARY KEY on demand and uses UPSERT statements for atomic writes. All reads and writes are asynchronous and scoped to a specific workspaceId, which is derived from the workspace configuration. This ensures that settings for different projects remain isolated while sharing the same database schema.

Session-Group State Layer

Building atop the KV store, the session-group state layer provides higher-level organization of sessions through grouping, ordering, and assignment management. The SessionGroupState type defined in dev/apps/server/src/session-groups.ts consists of a list of groups plus a map of sessionId → groupId.

Normalisation functions enforce length limits and unique IDs before persistence. The module exports readSessionGroupState, writeSessionGroupState, and updateSessionGroupState functions that interact with the session_group_states table in the workspace-KV store. To prevent race conditions, an in-memory queue (updateQueueByWorkspace) guarantees serialized updates per workspace, ensuring that concurrent modifications to group assignments do not corrupt state.

Workspace Identification and Selection

The final layer handles the transformation of workspace configurations into stable identifiers used by the KV store and the OpenCode engine. In dev/apps/server/src/workspaces.ts, the workspaceIdForKey function hashes a canonical string—whether a local path, remote base URL, or OpenWork host—into a deterministic ID.

The findManagedEngineWorkspace function scans the workspace list and returns the first local workspace with a non-empty path, determining where the OpenCode engine actually boots. This ensures that engine state and KV store operations align with the correct project environment.

Working with Session and Workspace State in Code

The following examples demonstrate how to interact with these state layers in practice.

Reading and Validating Session Data

To fetch sessions from the OpenCode SDK and convert them to type-safe read models:

import { opencode } from "@opencode-ai/sdk/v2/client";
import { buildSessionList } from "./session-read-model.js";

async function loadSessions() {
  const response = await opencode.session.list({ limit: 200 });
  // `response` is typed as SessionListResponse by the SDK.
  const sessions = buildSessionList(response);
  return sessions; // SessionInfoReadModel[]
}

Persisting Workspace-Scoped Settings

To create a custom store for UI state or other workspace-specific data:

import { createWorkspaceKvStore } from "./workspace-kv-store.js";

type UIState = { showTodos: boolean };
const uiStateStore = createWorkspaceKvStore<UIState>({
  tableName: "ui_state",
  valueColumn: "state_json",
  parse: JSON.parse,
  serialize: JSON.stringify,
});

async function saveUiState(config, workspaceId, state: UIState) {
  await uiStateStore.set(config, workspaceId, state);
}

Atomically Updating Session Groups

To modify session-group assignments with automatic serialization:

import { updateSessionGroupState } from "./session-groups.js";

async function assignSessionToGroup(config, workspaceId, sessionId, groupId) {
  await updateSessionGroupState(config, workspaceId, (current) => {
    const newAssignments = { ...current.assignments, [sessionId]: groupId };
    return { ...current, assignments: newAssignments };
  });
}

Selecting the Engine Workspace

To determine which workspace the OpenCode engine should start in:

import { findManagedEngineWorkspace } from "./workspaces.js";

const workspaces = await loadWorkspaceConfig(); // [{ id, path, workspaceType, …}]
const engineWs = findManagedEngineWorkspace(workspaces);
if (engineWs) console.log("Engine will start in:", engineWs.path);

Summary

  • Session read models in session-read-model.ts enforce type safety through Zod validation of SDK responses including SessionGetResponse and SessionMessagesResponse2.
  • Workspace-KV store in workspace-kv-store.ts persists mutable state per workspace using SQLite with UPSERT operations and automatic table creation.
  • Session-group state in session-groups.ts builds higher-level UI concepts atop the KV store with an in-memory queue (updateQueueByWorkspace) ensuring serialized updates.
  • Workspace identification in workspaces.ts provides deterministic ID generation via workspaceIdForKey and engine workspace selection via findManagedEngineWorkspace.

Frequently Asked Questions

How does OpenWork ensure type safety when handling SDK responses?

OpenWork uses Zod schemas defined in session-read-model.ts to parse all incoming SDK responses. Helper functions like buildSessionList call parseOrThrow on the raw JSON, which validates the data against TypeScript interfaces and throws an ApiError (502) for invalid payloads, preventing malformed data from reaching the UI.

What database does OpenWork use for local state persistence?

The application uses SQLite for local state storage. The createWorkspaceKvStore function in workspace-kv-store.ts creates tables on demand with workspace_id TEXT PRIMARY KEY columns and performs atomic writes using SQLite's UPSERT syntax, ensuring that per-workspace data remains consistent even during concurrent access.

How are session groups kept consistent when multiple updates occur?

The session-groups.ts module implements an in-memory queue called updateQueueByWorkspace that processes updates serially per workspace. When calling updateSessionGroupState, the function queues the update callback, ensuring that read-modify-write cycles do not interfere with each other and preventing race conditions in group assignments.

How does OpenWork determine which workspace to start the engine in?

The findManagedEngineWorkspace function in workspaces.ts scans the configured workspace list and returns the first local workspace with a defined path. This workspace serves as the anchor for the OpenCode engine, while workspaceIdForKey generates stable identifiers for KV store operations based on the workspace's canonical path or remote URL.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →