Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/config/server-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@ After activation, the server exits gracefully and the supervisor relaunches it.
configure unlimited restarts (`max_restart_attempts=0`) before relying on self-update.
</Warning>

## Running the desktop app and `xum server` together

If `xum server` is already running when you open the desktop app on the same machine and Xum data directory, the desktop app does not start a second API server. It still runs its own full backend on the same data: both processes read and write the same projects, workspaces, chat history, and tasks.

What stays consistent between the two:

- Config changes, such as creating a workspace or changing a setting, are written under a shared lock.
- Chat history appends are written under a shared lock.
Comment thread
ThomasK33 marked this conversation as resolved.
- A workspace turn that one process runs is never marked interrupted by the other while the first process is alive.
- A consent change you make in one app wins over a default that the other app was about to apply.

What does not work across the two:

- Streams, Stop, and message queues belong to the process that started them. Stopping a turn in one app does not stop a turn that the other app runs.
- Busy state and delegated-turn checks are per process. The other app can admit a message into a workspace that looks busy only in the first app.
- An open chat or sidebar does not update when the other app writes. Reload the app or reopen the workspace to see the other app's messages and changes.

<Warning>
Do not drive the same workspace or task from both apps at the same time. Pick one app per
workspace.
</Warning>

<Note>
When one of the two processes starts, it recovers the sub-agent tasks that were starting, running,
or awaiting a report, even if the other process is still running them. That can start a second
copy of a task while the first copy keeps running. Start the second app only when no sub-agent
tasks are active.
</Note>

Running two different Xum versions on the same data directory is not supported.

## Related

- [CLI reference](/reference/cli)
31 changes: 31 additions & 0 deletions src/node/services/agentSkills/builtInSkillContent.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5317,6 +5317,37 @@ export const BUILTIN_SKILL_FILES: Record<string, Record<string, string>> = {
" configure unlimited restarts (`max_restart_attempts=0`) before relying on self-update.",
"</Warning>",
"",
"## Running the desktop app and `xum server` together",
"",
"If `xum server` is already running when you open the desktop app on the same machine and Xum data directory, the desktop app does not start a second API server. It still runs its own full backend on the same data: both processes read and write the same projects, workspaces, chat history, and tasks.",
"",
"What stays consistent between the two:",
"",
"- Config changes, such as creating a workspace or changing a setting, are written under a shared lock.",
"- Chat history appends are written under a shared lock.",
"- A workspace turn that one process runs is never marked interrupted by the other while the first process is alive.",
"- A consent change you make in one app wins over a default that the other app was about to apply.",
"",
"What does not work across the two:",
"",
"- Streams, Stop, and message queues belong to the process that started them. Stopping a turn in one app does not stop a turn that the other app runs.",
"- Busy state and delegated-turn checks are per process. The other app can admit a message into a workspace that looks busy only in the first app.",
"- An open chat or sidebar does not update when the other app writes. Reload the app or reopen the workspace to see the other app's messages and changes.",
"",
"<Warning>",
" Do not drive the same workspace or task from both apps at the same time. Pick one app per",
" workspace.",
"</Warning>",
"",
"<Note>",
" When one of the two processes starts, it recovers the sub-agent tasks that were starting, running,",
" or awaiting a report, even if the other process is still running them. That can start a second",
" copy of a task while the first copy keeps running. Start the second app only when no sub-agent",
" tasks are active.",
"</Note>",
"",
"Running two different Xum versions on the same data directory is not supported.",
"",
"## Related",
"",
"- [CLI reference](/reference/cli)",
Expand Down
3 changes: 2 additions & 1 deletion src/node/services/subagentAttemptSettlements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import writeFileAtomic from "@/node/utils/writeFileAtomic";
* (#4545; driving one task from two backends is unsupported, and a Stop in one backend does not
* stop the other's turn). Consumers must read the report artifact first: a receipt followed by a
* report is "reported". Once a workflow claim retires the attempt, a late report is refused, so a
* step accepts either that report or the replacement, never both.
* step accepts either that report or the replacement, never both (see CONCURRENT BACKENDS in
* processLiveness.ts).
*
* Producers are TaskService's settlement paths (persistOwnedAttemptSettlement and the stop-record
* release). The only reader so far is the lineage proof at reawaken/reactivation; the classifier
Expand Down
8 changes: 7 additions & 1 deletion src/node/services/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9312,6 +9312,8 @@ export class TaskService implements AgentTaskIntegration {
const targetIsAgentTask =
coerceNonEmptyString(targetEntry.workspace.parentWorkspaceId) != null;
const unrelatedRoot = relation === "target_unrelated" && !targetIsAgentTask;
// Process-local courtesy: another backend's delegated turn is invisible here (#4446; see
// CONCURRENT BACKENDS in processLiveness.ts).
const delegatedRootUnavailable = (): boolean =>
unrelatedRoot &&
this.getWorkspaceTurnManager().getLiveWorkspaceTurnRegistration(targetId) != null;
Expand Down Expand Up @@ -14257,7 +14259,11 @@ export class TaskService implements AgentTaskIntegration {
);
}

/** An on-demand root-workspace address book, separate from either ownership graph. */
/**
* An on-demand root-workspace address book, separate from either ownership graph. Activity and
* delegated-turn state come from this backend's memory; another backend's live work is not
* reflected (see CONCURRENT BACKENDS in processLiveness.ts).
*/
listInstanceWorkspaces(
callerWorkspaceId: string,
options: { query?: string | null; limit?: number | null; offset?: number | null }
Expand Down
2 changes: 1 addition & 1 deletion src/node/services/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3596,7 +3596,7 @@ export class WorkspaceService
* durable staging/apply state does, via refine-apply.lock). Multi-instance
* mode is a development escape hatch — concurrent turn traffic against one
* workspace from two backends is unsupported beyond those durable-state
* locks.
* locks. See CONCURRENT BACKENDS in processLiveness.ts.
*/
private acquireContextMutationAdmissionGuard(
workspaceId: string,
Expand Down
25 changes: 25 additions & 0 deletions src/node/utils/concurrency/processLiveness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@
* or replaced container, rebooted host) is retired and cannot resume before
* a new one accesses the root. Concurrent cross-domain sharing is
* unsupported, not prevented.
*
* CONCURRENT BACKENDS (#4311, #4414, #4446, #4545): two full backends can share one root, the
* desktop app beside a `xum server` that started first (the desktop then skips only its API
* server) or any pair under XUM_ALLOW_MULTIPLE_INSTANCES=1. One rule governs them:
* A backend turns in-memory knowledge into a durable claim only about state it owns. It
* re-checks the claim's precondition against durable state under the lock that serializes the
* claim's write. It never judges a durable record whose liveness lives in another backend's
* memory; when it would need that knowledge, it leaves the record alone, answers
* "indeterminate", or refuses.
* Serialized across backends: config.json edits (editConfig CAS), history appends (the history
* write lock), and the crossProcessLock/fileLock kits (including the workspace-turn live-owner
* locks and refine-apply.lock). Per process: streams, Stop, owned attempts and settlement
* ledgers, send/rename/remove/busy guards, and delegated-turn reservations, so peer admission and
* instance discovery honor another backend's delegated turn only as a courtesy (the airtight
* version needs a cross-process admission registry, #4476).
* Driving one workspace or task from two backends at once is unsupported. Under that misuse two
* guarantees still hold for the attempt-fenced orchestration writes (report publication, settlement
* receipts, the plan-handoff boundary, task-state transitions): a superseded attempt's writes never
* land on its successor, and for a retired attempt a workflow accepts either the late report or the
* replacement, never both. Not fenced: the effects of an execution that is already running (its
* stream, tool calls, file changes and history rows continue until that backend stops it). Not
* guaranteed: a backend's startup recovery re-queues or re-drives `starting`/`running`/
* `awaiting_report` sub-agent tasks that a live backend still runs (a duplicate execution);
* UIs do not observe the other backend's writes until they resubscribe; mixed Xum versions on one
* root (#4480).
*/

import { spawnSync } from "node:child_process";
Expand Down
Loading