From 534c489a841cb870dfdf13b553122901fe42c770 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 27 Sep 2026 01:23:38 +0000 Subject: [PATCH 1/3] docs: concurrent backends contract (#4446) --- docs/config/server-access.mdx | 29 +++++++++++++++++++ .../builtInSkillContent.generated.ts | 29 +++++++++++++++++++ .../services/subagentAttemptSettlements.ts | 3 +- src/node/services/taskService.ts | 8 ++++- src/node/services/workspaceService.ts | 2 +- src/node/utils/concurrency/processLiveness.ts | 20 +++++++++++++ 6 files changed, 88 insertions(+), 3 deletions(-) diff --git a/docs/config/server-access.mdx b/docs/config/server-access.mdx index 05aaba727f8..181a916434f 100644 --- a/docs/config/server-access.mdx +++ b/docs/config/server-access.mdx @@ -94,6 +94,35 @@ After activation, the server exits gracefully and the supervisor relaunches it. configure unlimited restarts (`max_restart_attempts=0`) before relying on self-update. +## 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. + + + Do not drive the same workspace or task from both apps at the same time. Pick one app per + workspace. + + + + When one of the two processes starts, it re-drives the sub-agent tasks that were `running`, even + if the other process is still running them. Start the second app when no sub-agent tasks are + running, or expect those tasks to restart. + + +Running two different Xum versions on the same data directory is not supported. + ## Related - [CLI reference](/reference/cli) diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 736b3b2fe6f..4212308cb02 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -5317,6 +5317,35 @@ export const BUILTIN_SKILL_FILES: Record> = { " configure unlimited restarts (`max_restart_attempts=0`) before relying on self-update.", "", "", + "## 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.", + "", + "", + " Do not drive the same workspace or task from both apps at the same time. Pick one app per", + " workspace.", + "", + "", + "", + " When one of the two processes starts, it re-drives the sub-agent tasks that were `running`, even", + " if the other process is still running them. Start the second app when no sub-agent tasks are", + " running, or expect those tasks to restart.", + "", + "", + "Running two different Xum versions on the same data directory is not supported.", + "", "## Related", "", "- [CLI reference](/reference/cli)", diff --git a/src/node/services/subagentAttemptSettlements.ts b/src/node/services/subagentAttemptSettlements.ts index 7cc6196fbb2..a2ee96439ad 100644 --- a/src/node/services/subagentAttemptSettlements.ts +++ b/src/node/services/subagentAttemptSettlements.ts @@ -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 diff --git a/src/node/services/taskService.ts b/src/node/services/taskService.ts index c62a23d01cd..08d70311daa 100644 --- a/src/node/services/taskService.ts +++ b/src/node/services/taskService.ts @@ -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; @@ -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 } diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 1bb936fd05d..6327194200b 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -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, diff --git a/src/node/utils/concurrency/processLiveness.ts b/src/node/utils/concurrency/processLiveness.ts index 77e17d4fe87..46253465bde 100644 --- a/src/node/utils/concurrency/processLiveness.ts +++ b/src/node/utils/concurrency/processLiveness.ts @@ -7,6 +7,26 @@ * 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: a superseded attempt's stale effects never land on its successor, and + * for a retired attempt a workflow accepts either the late report or the replacement, never + * both. Not guaranteed: a backend's startup recovery re-drives `running` sub-agent tasks that a + * live backend still runs (rotating their attempt); mixed Xum versions on one root (#4480). */ import { spawnSync } from "node:child_process"; From 1aff690ff286ca299bfdcfc8d0b237e516b7d649 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 27 Sep 2026 01:30:28 +0000 Subject: [PATCH 2/3] docs: narrow the fenced guarantee; cover all recovered task states and stale UIs Addresses Codex review on #4802. --- docs/config/server-access.mdx | 8 +++++--- src/node/utils/concurrency/processLiveness.ts | 13 +++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/config/server-access.mdx b/docs/config/server-access.mdx index 181a916434f..18f8f6dfeb7 100644 --- a/docs/config/server-access.mdx +++ b/docs/config/server-access.mdx @@ -109,6 +109,7 @@ 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. Do not drive the same workspace or task from both apps at the same time. Pick one app per @@ -116,9 +117,10 @@ What does not work across the two: - When one of the two processes starts, it re-drives the sub-agent tasks that were `running`, even - if the other process is still running them. Start the second app when no sub-agent tasks are - running, or expect those tasks to restart. + 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. Running two different Xum versions on the same data directory is not supported. diff --git a/src/node/utils/concurrency/processLiveness.ts b/src/node/utils/concurrency/processLiveness.ts index 46253465bde..fd55ca26d33 100644 --- a/src/node/utils/concurrency/processLiveness.ts +++ b/src/node/utils/concurrency/processLiveness.ts @@ -23,10 +23,15 @@ * 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: a superseded attempt's stale effects never land on its successor, and - * for a retired attempt a workflow accepts either the late report or the replacement, never - * both. Not guaranteed: a backend's startup recovery re-drives `running` sub-agent tasks that a - * live backend still runs (rotating their attempt); mixed Xum versions on one root (#4480). + * 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"; From 90b54794f41de55497901e59d4450fb2f1dc3a9e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 27 Sep 2026 01:35:51 +0000 Subject: [PATCH 3/3] docs: regenerate embedded docs snapshot Addresses Codex review on #4802. --- .../services/agentSkills/builtInSkillContent.generated.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 4212308cb02..422db337cda 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -5332,6 +5332,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "- 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.", "", "", " Do not drive the same workspace or task from both apps at the same time. Pick one app per", @@ -5339,9 +5340,10 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "", "", - " When one of the two processes starts, it re-drives the sub-agent tasks that were `running`, even", - " if the other process is still running them. Start the second app when no sub-agent tasks are", - " running, or expect those tasks to restart.", + " 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.", "", "", "Running two different Xum versions on the same data directory is not supported.",