Skip to content

feat(workspace): make workspace changes take effect on restart - #129

Closed
Yuge Zhang (ultmaster) wants to merge 4 commits into
microsoft:mainfrom
ultmaster:feat/workspace-restart-bound
Closed

feat(workspace): make workspace changes take effect on restart#129
Yuge Zhang (ultmaster) wants to merge 4 commits into
microsoft:mainfrom
ultmaster:feat/workspace-restart-bound

Conversation

@ultmaster

Copy link
Copy Markdown
Contributor

Closes #126.

What changes

A server process now serves one workspace for its lifetime. Selecting a different Home folder persists the choice and takes effect on restart, rather than moving a live process from workspace A to workspace B.

  • Startup owns the choice. Electron reads <userData>/workspace.json before forking the server and hands the path over as HUABU_WORKSPACE_STARTUP. That is deliberately not HUABU_WORKSPACE: the operator's variable locks the workspace, hides the path, removes the picker, and fails the boot when the folder cannot be opened — right for a deployment, wrong for a user whose drive is unplugged. A shell-chosen workspace that will not open leaves the process unconfigured with the reason in WorkspaceInfo.startupError, so the client shows the picker instead of the app being unusable.
  • One activation per process. PUT /api/workspace for a different path returns 409 WORKSPACE_RESTART_REQUIRED without preparing anything; re-sending the active path stays a no-op, because the client re-sends its remembered path on every boot and a second tab must not be told to restart. Standalone deployments keep using HUABU_WORKSPACE.
  • The renderer restarts where it can. Electron saves the path and relaunches (workspace:restart); a browser tab, which cannot restart the server it is talking to, saves the choice and says a restart is needed.
  • The lifecycle machinery that only existed for online switching is gone. Workspace operation leases, the "belongs to an inactive workspace" guards on every Disk handle / repository / blob scope / log-task-node writer, the workspace-qualified CanvasStore LRU, and the workspace component of the node-tombstone and admission-gate keys. Committing a workspace now drops the instance cache and node fences together (resetStorage()), which is the one part that was load-bearing — it keeps a test moving through several temporary workspaces from reading a stale one.

Net: -651 lines across server, desktop, web, and shared.

Why now

This is the storage-lifecycle simplification issue #126 asks for, and it lands before phase 4.6 on purpose: without it, the multi-backend work would have to build staged connection publication, detach, stale-mount recovery, and workspace-scoped invalidation for connection-holding backends (SQLite, Postgres, Azure Blob) — machinery whose only job is a switch that is not a product requirement. Phase 4.6 (#125) rebases on top of this and drops that scope.

Verification

pnpm run check passes end to end: lint, format, typecheck, 118 server test files (1004 tests), 131 web test files (990 tests), i18n parity, agent-team skills, license headers.

Tests written for the new behavior:

  • startup adoption — both env forms, precedence, operator failure fails the boot, shell-chosen failure recovers to the picker with a reason, relative paths rejected;
  • activation — refuses a different workspace and touches nothing (not even preparing the folder it was asked for), accepts the active one again, leaves the process unconfigured when preparation times out;
  • storage — the cache-boundary and handle tests now assert what remains true: committing a workspace drops cached instances, a freshly resolved handle reads the active workspace, and the blob adapter still resolves each operation's paths once before its first await (which was never about workspaces — it is about a Space directory renamed mid-write).

Not done here

The proposal's open question about connections on a free-mode switch is answered rather than deleted; the Phase 2/4 retrospectives keep their historical wording. docs/architecture/canvas-storage.md and desktop-startup.md are updated to describe the one-workspace process.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q6jDM21KBHPynupZHVg2o3

Yuge Zhang (ultmaster) and others added 3 commits August 21, 2026 10:46
Every layer had to answer "which workspace am I looking at?" before it
could do its job: handles captured the active path and rejected the next
call if it had moved, the cache keyed on it, and `createSpace`/`deleteSpace`
took an operation lease so a switch could not strand them mid-flight. All
of it existed to move a live process from workspace A to B.

Issue microsoft#126 says that is not a product requirement, and the price is about
to rise — a connection-holding backend would have to be staged, published,
and detached to keep the trick working, and a backend with no workspace at
all (Postgres plus Azure Blob) would still have to answer the question.

So the workspace is now fixed for a process. Activation happens at most
once: asking for a different one is refused with `WORKSPACE_RESTART_REQUIRED`
without preparing a thing, and asking for the active one again is a no-op,
because the client re-sends its remembered path on every boot. Choosing a
different folder saves the choice and restarts — the desktop shell does it,
since it owns both `workspace.json` and the server child; a browser tab says
so instead.

The shell hands its choice to the server as `HUABU_WORKSPACE_STARTUP`,
deliberately not `HUABU_WORKSPACE`: the operator's variable locks the
workspace, hides the path, and fails the boot when the folder cannot be
opened, which is right for a deployment and wrong for a user whose drive is
unplugged. A shell-chosen workspace that will not open leaves the process
unconfigured with the reason in `WorkspaceInfo.startupError`, so the client
shows the picker rather than the app being unusable — and does not spend
another 70-second preparation on the path the server just failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6jDM21KBHPynupZHVg2o3
Every Disk handle, repository, blob scope, and log/task/node writer captured
the workspace active when it was built and refused to run once the active one
moved. The registries did the same by other means: the `CanvasStore` LRU
self-detected a switch and cleared itself, and its key — along with the node
tombstone map's and the Task mutation mutex's — carried a workspace path
beside the Space id.

All of it answered a question a process can no longer ask. The workspace is
fixed for a process lifetime, so a Space id already denotes one Space, and a
handle can only have been built against the workspace being served.

The guards are gone, the keys are Space ids, and the one thing that was
genuinely load-bearing is now explicit: committing a workspace drops the
instance cache and the node fences together, which is what keeps a test moving
through several temporary workspaces from reading a stale one.

The blob adapter keeps resolving each operation's directory once before its
first await — that was never about workspaces. It stops a Space directory
renamed mid-write from landing the temp file in one place and the destination
in another, so its test now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6jDM21KBHPynupZHVg2o3
The architecture docs still described a Server that could move between
workspaces while running: activation kept "the previously active workspace"
serving, deletion held an active-Workspace lease, handles and cache keys were
Workspace-qualified, and the desktop cold start paid for a `PUT` the renderer
had to make because the server always booted with nothing.

None of that is true after issue microsoft#126. What replaces it is shorter: the
workspace is named at startup — by the operator, by the shell, or by the one
runtime activation an unconfigured process accepts — a second choice comes
back as `WORKSPACE_RESTART_REQUIRED`, and nothing in storage records which
workspace it was built for.

The proposal's open question about connections on a free-mode switch is
answered rather than restated, since a backend that holds one now opens at
startup and closes at shutdown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6jDM21KBHPynupZHVg2o3
@ultmaster
Yuge Zhang (ultmaster) force-pushed the feat/workspace-restart-bound branch from dde402b to 0159c07 Compare August 21, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make workspace changes take effect on restart

1 participant