condensate

Build and connect

Workspaces and execution

Set the repositories, accounts, models and limits a task may use.

On this page

A workspace gives a task its project context and its limits. It names the repositories the task works in, the accounts whose subscriptions it may spend, and the models it may run. The runtime checks these rules when a task is spawned, before anything starts, so a task that breaks a rule is refused with a reason. This page covers the registry file, policy fields, how a spawn picks its workspace, where tasks run, and heads.

Project context and execution are separate
WorkspaceRepositories, accounts, policy
ThreadTask identity and ordered history
SessionActive model conversation
Execution happens on a runnerThe local process runner is the starting point for this guide.
A workspace supplies the context for a task. A thread records its history; an engine session performs its work.

Configure the registry

The runtime reads ~/.condensate/workspaces.json. Set CONDENSATE_WORKSPACES_FILE to read a different file. The file is a JSON document with one workspaces array, and the runtime validates it against a schema. A missing required field or a value of the wrong type is an error. A key the schema does not know is dropped without a warning, so a misspelled optional field such as modles silently means no limit. Check the result with the read below.

You do not need to restart the runtime after an edit. It remembers the file's modification time and size, and rereads the file when either changes.

If the file does not exist, the registry is empty. No task can name a workspace, so every spawn that names one is refused. If the file exists but cannot be read or decoded, spawns fail with a 500 that names the file, instead of treating the registry as empty. That keeps a broken file from looking like a missing one. Fix the file promptly: an existing thread whose session is rebuilt while the file is broken runs as if it had no workspace.

This example uses placeholders. Replace my-repo and my-account with a repository directory name and an account name from your account registry:

{
  "workspaces": [{
    "name": "my-project",
    "repos": [{ "name": "my-repo", "writable": true }],
    "accounts": ["my-account"],
    "policy": { "models": ["claude-opus-5-5"] }
  }]
}

Check what the running runtime sees with a read:

curl -fsS http://127.0.0.1:4741/workspaces

The port comes from CONDENSATE_AGENTS_PORT and defaults to 4741. The response includes each repository's GitHub slug when the runtime can find it. Do not infer the registry from a similarly named file in another checkout. Ask the running runtime.

Each workspace has these fields:

FieldMeaning
nameThe key that threads carry. Keep it unique: the runtime does not check, and the first workspace with a name wins. Use lowercase words joined by hyphens.
reposThe repositories the workspace works in. Each entry has a name and a writable flag.
accountsThe account names the workspace may spend, in order of preference.
gitIdentityWhose name commits, pushes and pull requests carry. account is the default. app uses the GitHub App.
githubUserThe GitHub login whose token the workspace's proxied tasks use for GitHub calls.
policyRequired. Limits on models, failover, the Fable pool and container size. Use {} for no limits. See Policy.

Repositories

The registry names repositories. It does not store their paths. A repository applies to a task when its name is one of the directory names in the task's working path, compared without regard to case. A task in /work/my-repo/src matches my-repo.

Set writable: false on a repository that a workspace should read but never change. The runtime refuses pull request work and GitHub writes to that repository with a 409 repo-read-only response. The flag does not change file permissions in the checkout.

Git identity and GitHub access

The GitHub tokens behind these settings are swapped in by the credential proxy. The proxy is on by default. With CONDENSATE_PROXY=off, an app workspace still writes commits as condensate-dev[bot], but pushes and GitHub calls use the account's own credential, and githubUser has no effect.

  • With gitIdentity: "account", commits and pushes use the account's own GitHub access.
  • With gitIdentity: "app", commits are made as the condensate-dev[bot] identity. The runtime mints an installation token for the GitHub calls.
  • With githubUser, the proxied task's GitHub token comes from gh auth token --user for that login, not from whichever gh account is active.

When a workspace sets both gitIdentity: "app" and githubUser, the app wins.

Policy

The policy object sets limits. A field that is absent means the runtime applies no limit.

FieldEffectWhen absent
modelsThe models a task may name. A spawn on any other model is refused with a 400 model-not-allowed, and the response lists the allowed models. A model with and without the [1m] bracket counts as the same model.Any model the engine serves
failover{ "engine", "model" }. When every account a task may spend on its engine is out of quota, the turn moves to this engine and model. engine is claude, codex or pi. The workspace needs at least one account for the target engine.Off. The turn fails, and the runtime raises an alert that the accounts are spent.
pools.fable{ "maxUtilization", "fallback" }. When a Fable spawn would run on an account whose five-hour window is at or above maxUtilization percent, the task runs fallback instead. The fallback must be a permitted model.No guard
container{ "cpus", "memoryMb" } limits for each container task. CPUs range from 1 to 16. Memory ranges from 256 to 32768 MiB.2 CPUs and 4096 MiB
maxSeatsStored in the file. No code path enforces it yet.Not applicable

Replace a workspace's whole policy with PUT /workspaces/:name/policy. The body is the policy object, and it replaces the old one. The runtime checks the body against the same schema as the file, and it also refuses any field the schema does not know. It refuses a fallback model that routing does not permit, with a 400 policy-refused. It then writes the file atomically, so a reader never sees a half-written registry. An unregistered name gets a 404 unknown-workspace.

How a spawn picks its workspace

A spawn can name its workspace. If it does not, the runtime picks one in this order:

  1. The workspace named in the spawn.
  2. The workspace whose repository matches the task's working path.
  3. The parent thread's workspace, for a child task.

A named workspace that is not registered is refused with a 400, and the message lists the registered names. The runtime never falls through to a different workspace. A task with no workspace may spend only accounts that no workspace lists, so a workspace's subscriptions cannot leak into an unassigned task.

Accounts and model refusals

The runtime refuses these before a task starts:

  • An account outside the workspace. The spawn names an account that the workspace does not list. The response lists the workspace's accounts (400, account-not-in-roster).
  • A model outside models. The response includes the allowed list (400, model-not-allowed).
  • A model no account can serve. None of the workspace's accounts runs the model's engine (400, model-not-served). A Claude task needs a Claude account. A Codex task needs a Codex login. Condensate's own agent (engine id pi) spends a Codex login for an OpenAI model, and an OpenCode or Cerebras account for those providers' models.

Within a workspace, the runtime tries accounts in order and moves on when one is out of quota. When every account is spent, the turn fails unless failover is set. The accounts guide explains how credentials and routing fit together.

Calls to the OpenAI-compatible completions endpoint must name a workspace in the x-condensate-workspace header. A call without it is refused with a 400 workspace_required. The call is charged to that workspace and runs on its accounts. See the HTTP reference.

Where a task runs

A workspace is not a machine. Each spawn chooses a runner with its runner field: process, container, k8s, or any. any, or leaving the field out, lets the runtime choose. When Kubernetes is configured and a node labelled condensate.dev/seats=true is ready, the runtime places reviewer and fixer tasks there, if the Kubernetes runner accepts them. Everything else runs in a local process.

RunnerTurned on byRunsLimits
processAlways available. It is the default.A local process on the hostSupports plugins, MCP servers, host-side tools and every engine: Claude Code, Codex and Condensate's own agent
containerCONDENSATE_CONTAINER_IMAGE, CONDENSATE_CONTAINER_PROXY_URL and CONDENSATE_CONTAINER_NETWORKA Claude task in its own apple/container VMClaude only. No plugins, forks, middleware keys or MCP servers. The task's checkout is mounted at /work, and the VM has no internet access.
k8sCONDENSATE_KUBECONFIG, plus CONDENSATE_SEAT_IMAGE and CONDENSATE_PROXY_SECRETS_DIRA Kubernetes JobClaude only. No plugins, MCP servers or forks. The task runs in the pod's /work.

Both the container and Kubernetes runners need the credential proxy, which is on by default, so do not set CONDENSATE_PROXY=off. The task's Claude account must reach them as a proxy placeholder. The container network must be a host-only apple/container network with the proxy on its gateway. Setting CONDENSATE_KUBECONFIG without CONDENSATE_SEAT_IMAGE and CONDENSATE_PROXY_SECRETS_DIR stops the runtime from starting, and a Kubernetes task must use /work as its working directory.

Codex tasks and tasks on Condensate's own agent run only on the process runner, because the container and Kubernetes runners refuse any engine other than Claude.

Mac-only MCP servers (iMessage, Calendar, Spotify and Mac tools) and host-side stdio MCP servers need the process runner. The other runners refuse them and say why.

Presence of a runner's configuration is not the same as a working runner. Check the runner that a task will use before you move work to it. A fresh checkout does not have a Kubernetes cluster, and it does not have a container image unless you configure one.

Heads

A workspace has at most one head. A head is a coordinating thread that reads, decides, briefs other tasks, watches their progress and reports back. It hands the implementation to child tasks.

  • A spawn that asks for a second head in the same workspace is refused with a 409 that names the existing head.
  • GET /workspaces/:name/head returns the head thread, or a 404 when the workspace has none.
  • Demote a head with POST /threads/:id/role and the body { "role": "chat" }. The log and the session stay. A head that has any events is refused with a 409 head-holds-transcript unless the body sets "explicit": true. A demotion that would leave the workspace with no head is refused with a 409 workspace-would-be-headless unless it sets "allowHeadless": true. A thread that is not a head gets a 409 not-a-head.
  • A child task is its own durable thread with its own log. Follow it through the thread event APIs and /threads/:id/subscriptions.

Memory and workspace events

Each workspace has its own memory bank. The bank is named after the workspace, under a configured prefix. Threads with no workspace use the default bank. Recall, retain and reflect through /workspaces/:name/memory/....

A workspace also exposes its activity as one set of routes:

RouteReturns
GET /workspaces/:name/eventsThe workspace log as server-sent events, in one cursor order across all of its threads
GET /workspaces/:name/tailRecent events
GET /workspaces/:name/changesChanges to the workspace's threads
GET /workspaces/:name/approvals and /questionsTool calls and questions waiting on a person
POST /workspaces/:name/prs/:number/agentsA build task on a pull request's branch, in a worktree the runtime owns

The HTTP reference lists every route.

Change a workspace safely

  • Spawn checks read the registry when a task starts, so a change to accounts or models applies to tasks spawned after it. A running session keeps the account it started with. When the runtime builds a new session for an existing thread, for example after a restart, it reads the registry again and picks from the current accounts.
  • Threads keep their own log when you change the registry.
  • Before you change where the runtime stores its data, back up threads.db with SQLite's online backup, and back up the blobs/ folder beside it and the engine state too. A copy of threads.db alone is not a complete backup while the runtime runs. The troubleshooting guide has the commands.

For how threads, sessions and runners relate, see the runtime.