Give the residual per-Space files an owner, and prove the result - #134
Merged
cxxxxxn (cxxxxxn) merged 15 commits intoSep 4, 2026
Conversation
Yuge Zhang (ultmaster)
force-pushed
the
feat/space-dispositions
branch
from
August 29, 2026 06:01
58d3b14 to
19c8995
Compare
Two families were files only because they always had been: memory-worker bookkeeping in `.memory/state.json`, and the debug prompt log under `.history/chat/`. Neither is a Space record and neither is a blob — they are some other module's own state, which happened to live next to storage's. The obvious repair is a port member per feature (`memory`, `promptLogs`), which obliges every future backend to model data it has no stake in — the thing §12.5.7 rejected when it declined a `SpaceChats.list()` port. The next-obvious one is a namespaced key/value member, and it is wrong for a reason worth recording: it fixes one access shape — whole-value rewrite, no queries, no indexes — for every owner forever, and an owner with real query needs then encodes its own index inside an opaque value. So `SpaceHandle.extension(namespace)` hands over a connection point and nothing else: a reserved directory on Disk today, a table prefix or schema when those adapters exist. The owner brings its own store and its own queries. Storage keeps lifecycle, because only it can — a namespace is created on demand and destroyed with the Space, which keeps `beginDelete()` whole without any owner registering a cleanup hook. The substrate returns null for a Space that does not exist, and that is load-bearing rather than defensive. The memory trigger carried its own resurrection guard: its op-counter hook fires *after* a delete has removed the Space, and a bare write would recreate the directory as a stub holding nothing but bookkeeping. Every owner writing through an ad-hoc path needed that guard. Refusing a substrate for a Space that is gone states it once, in the one place that can, and the guard is deleted rather than moved. The namespace grammar is narrower than it looks like it needs to be, and the reasons are recorded where it is defined: an owner prefix is required because storage cannot arbitrate a collision it never sees the data behind, and `_` is reserved because a backend keyed on identifiers has to fold the dots into something legal and the obvious fold is `_`. The contract asserts isolation and lifecycle only — there is no data behaviour to assert about data the port cannot read — so the harness supplies the read and write, and the suite says what must be true of whatever an owner stored. Destruction is checked by recreating the Space under the same id, because on Disk it falls out of placement and on another backend it will not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`BlobScopeRef` had one kind, mapped onto `.artifacts/`, so anything else a Space holds as bytes stayed a bare `readFileSync` against a path the caller assembled: the RFS access guide at the Space root, the agent's memory body, the upload scratch. Naming one blob `skill.md` would have put it in a hidden directory rather than where a user authors it, and the alternative — hierarchical blob names — §7.1 excluded for every backend. So there is now one kind per **user-visible area**: artifacts, guide, memory, upload. A kind is a union member and one placement rule per adapter; a path separator inside a name would have been a contract change. It also lets retention diverge later — scratch is not an artifact — without moving bytes a second time. The guide scope is the interesting one. Its area *is* the Space root, which also holds `space.json` and every node directory, so a scope that claimed the folder would list storage's own records and delete the Space on `deleteAll()`. It is therefore bounded by its member names instead — a fixed set is a tighter namespace than a directory, not a looser one — and the adapter refuses a name the scope does not own before it reaches the filesystem. The contract pins both halves, because "answers for the folder" is the shape an adapter would naturally reach for. Deletion now sweeps every per-Space scope, derived from one list so the saga cannot fall behind the union. On Disk the extra sweeps are redundant with removing the directory; on a backend where dropping the record does not remove the area the bytes sit in, an unswept kind is an orphan. Two consumers changed shape rather than just changing calls. The memory writers took an absolute path and a parent directory, which stopped being expressible once one of their three tiers was a blob and the other two were still Workspace-scoped files — so they take a document instead, and every rule about content above it stopped caring which. And the memory body's sandbox resolver is gone: the port's own precondition already refuses a write to a Space whose record does not exist, which is what that resolver's directory check was approximating. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…not be Six features are about a filesystem rather than about storage: revealing a Space in Finder, adopting a document dropped in from outside, a bundle that is a directory zipped up, the built-in file tools, and the Windows handle dance that lets a watched directory be renamed. A backend keeping Spaces in tables has no honest answer for any of them. Until now that was only knowable by reading the code, or by clicking the button. An outcome of "unavailable" is an acceptable product limitation rather than debt — but only if an operator can learn it when they select a profile, so the matrix sits beside profile validation and is reported at startup. Reported, not raised. A profile that offers fewer features is a stated limitation; a profile naming a backend with no adapter is a misconfiguration and still fails fast. Conflating the two would either refuse a legitimate deployment or let a real misconfiguration through as a warning. The matrix is an exception list, not an inventory. Listing the portable features too would mean editing it whenever anything was built, and it would go stale silently — so an id nobody wrote down is available by construction, and adding a portable feature is not a matrix edit. Each listed feature still refuses at its own call site, because a matrix nobody consults at runtime is documentation. Those refusals now share the matrix's wording, so the sentence an operator read when they chose the profile is the sentence they see in the failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every product test so far either named the Disk tree or swapped in a stub. A stub proves the application talks to an interface; it cannot prove a backend serves the product, which is the half that decides whether a second adapter works. `storage/testing.ts` opens a real profile against a temporary Workspace through the production lifecycle — prepared Workspace, opened connections, `ensureWorld()` — and `product-boundary.test.ts` runs the criterion against every entry in `PRODUCT_STORAGE_PROFILES`. Phase 5 adds one entry and the same behaviours are covered for SQLite without a line of the suite changing, which is also the check that the suite never learned a backend's layout. A guard reads the suite's own source and rejects a directory, a filename, or a `readFileSync` appearing in it, because the failure mode here is a helpful-looking assertion someone adds later. `closeStorage()` arrives with it, registered on graceful Server shutdown and used by the harness between profiles. On Disk it is close to a no-op — which is why it has to exist before a connection-holding backend does: a pool nobody closes leaks on every restart, and the lifecycle is where that is visible rather than the adapter. The records the suite reads back are built through the write engine rather than hand-rolled, because a fixture that skips the engine asserts nothing about what the product actually stores. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EKZXxEWwZvAZo8YMaLv4m
`StructuredStore` vended a handle per Space while `BlobStore` made the caller assemble a `BlobScopeRef` first — the same subject reached two different ways, and the asymmetry showed up in the facade, where four scope refs were built by hand beside the structured handle. `BlobStore.space(canvasId)` now returns `SpaceBlobs`: one member per user-visible area, named rather than tagged. The areas are what the union's `kind`s already were, so nothing about placement, name bounding, or the delete sweep changes — only how a caller says which one it means. `Space` composes the two handles flat, so every durable part of a Space sits at one level and which axis stores it stays this module's business. The artifact area loses its unqualified spelling: `space(id).blobs` became `space(id).artifacts`, beside `.guide`, `.memory`, and `.uploads`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ufq2A7GReTSpRdtApkpdNZ
§12.8 was the plan for the last change set; it now records what landed — the substrate and its first two owners, the per-area blob scopes and why the guide area cannot be a directory scope, the capability matrix, and the product harness — along with the two things building surfaced: that the resurrection guard belonged in the port rather than in each owner, and that upload scratch earns its area without yet earning its writers. The architecture doc's Disk diagram still showed `state.json` under `.memory/` and no `.ext/` tier, which is now where per-namespace extension state lives, and described the residual direct-filesystem capabilities as an undesigned gap rather than a declared limitation. Both are the claim the matrix changes: off Disk those features are unavailable and said so at startup, not a reason the profile cannot be selected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EKZXxEWwZvAZo8YMaLv4m
Mechanical: `pnpm run lint --fix` import ordering for the two new imports, and Prettier on the two files the cherry-picks left unformatted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EKZXxEWwZvAZo8YMaLv4m
Yuge Zhang (ultmaster)
force-pushed
the
feat/space-dispositions
branch
from
September 3, 2026 03:43
19c8995 to
604b0c6
Compare
…or it The Disk substrate joins its path against the process-global Workspace, so awaiting the port-shaped `read()` left a window: activation could switch between validating the Space in A and resolving `canvasRoot()` against B, and the owner's first write would land in the newly active Workspace. Bind the extension to the Workspace that built the handle, and keep the existence check and the path join in one synchronous turn. This is the guard blob scopes already carry, stated once for the substrate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
The prompt log left `.history/chat/` for the `huabu.prompt.log` extension namespace, which moved it out of reach of the `includeHistory=false` opt-out. A bundle exported to share therefore carried the system and user prompts of every thread in the Space. Conversational history now lives in two tiers, so the ignore list names both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
…havior Two owners changed behavior when their files became a substrate namespace and a blob, in ways neither move intended. `dumpAssembledPrompt` resolved its Space inside the queued append, so a Workspace switch mid-turn bound the write to the newly active Workspace and wrote one Workspace's prompt into another's Space. Resolve the substrate when the turn is queued, and convert rejection to data there so a slow append ahead of it cannot strand an unhandled rejection while it waits its turn. `readCanvasMemory` lost the `catch` that made an unreadable document absence rather than a failed agent turn. Memory is optional context; restore it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Concurrent Space-memory edits can lose updates, and several Disk-only paths bypass the declared refusal contract.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR completes residual per-Space storage ownership by introducing blob areas, extension substrates, capability declarations, and backend-neutral product verification.
Changes:
- Adds namespaced extension storage and four Space blob areas.
- Declares Disk-only capabilities and storage shutdown lifecycle.
- Migrates consumers and adds real-backend contract coverage.
File summaries
| File | Description |
|---|---|
docs/proposals/multi-backend-storage.md |
Marks phase 12.8 implemented. |
docs/architecture/canvas-storage.md |
Documents the new storage architecture. |
apps/server/src/server.ts |
Logs unavailable capabilities at startup. |
apps/server/src/modules/workspace/paths.ts |
Removes migrated per-Space path helpers. |
apps/server/src/modules/storage/testing.ts |
Adds the real-profile test harness. |
apps/server/src/modules/storage/storage.ts |
Composes blob areas and storage shutdown. |
apps/server/src/modules/storage/profile.ts |
Documents capability/profile distinction. |
apps/server/src/modules/storage/product-boundary.test.ts |
Exercises portable storage behavior. |
apps/server/src/modules/storage/ports/structured.ts |
Adds the extension substrate contract. |
apps/server/src/modules/storage/ports/namespace.ts |
Validates extension namespaces. |
apps/server/src/modules/storage/ports/namespace.test.ts |
Tests namespace validation. |
apps/server/src/modules/storage/ports/contracts/space-extension.contract.ts |
Defines reusable extension tests. |
apps/server/src/modules/storage/ports/contracts/blob-store.contract.ts |
Covers blob areas and bounded scopes. |
apps/server/src/modules/storage/ports/blob.ts |
Introduces Space blob areas. |
apps/server/src/modules/storage/module-boundaries.test.ts |
Updates storage boundary enforcement. |
apps/server/src/modules/storage/index.ts |
Exports new storage APIs. |
apps/server/src/modules/storage/compatibility/delete-canvas.test.ts |
Updates deletion lifecycle tests. |
apps/server/src/modules/storage/capabilities.ts |
Defines the capability matrix. |
apps/server/src/modules/storage/capabilities.test.ts |
Tests capability declarations. |
apps/server/src/modules/storage/backends/disk/structured-store.ts |
Exposes Disk extension substrates. |
apps/server/src/modules/storage/backends/disk/structured-store.test.ts |
Tests extension workspace binding. |
apps/server/src/modules/storage/backends/disk/storage-recovery.test.ts |
Migrates recovery tests to artifacts. |
apps/server/src/modules/storage/backends/disk/space-repository.test.ts |
Runs the extension contract on Disk. |
apps/server/src/modules/storage/backends/disk/space-extension.ts |
Implements Disk extension directories. |
apps/server/src/modules/storage/backends/disk/layout.ts |
Adds memory and upload placements. |
apps/server/src/modules/storage/backends/disk/blob-store.ts |
Implements four Disk blob areas. |
apps/server/src/modules/storage/backends/disk/blob-store.test.ts |
Updates Disk blob tests. |
apps/server/src/modules/storage/backends/disk/blob-store.rename-retry.test.ts |
Updates retry coverage. |
apps/server/src/modules/remote_fs/skill.ts |
Reads Space guides through blobs. |
apps/server/src/modules/remote_fs/rfs.route.ts |
Awaits asynchronous guide resolution. |
apps/server/src/modules/remote_fs/interactive-view.rfs.test.ts |
Updates artifact test setup. |
apps/server/src/modules/preprocessing/pipeline.ts |
Renames blob dependency to artifacts. |
apps/server/src/modules/preprocessing/pipeline.test.ts |
Updates preprocessing fixtures. |
apps/server/src/modules/preprocessing/dispatcher.ts |
Supplies the artifact area. |
apps/server/src/modules/interactive-view/interactive-view.service.ts |
Checks renderer artifacts through storage. |
apps/server/src/modules/interactive-view/interactive-view.route.ts |
Serves interactive-view artifacts. |
apps/server/src/modules/canvas/snapshot-nodes.ts |
Migrates snapshot artifact access. |
apps/server/src/modules/canvas/import-node-src.ts |
Stores imported sources as artifacts. |
apps/server/src/modules/canvas/import-node-src.test.ts |
Updates import assertions. |
apps/server/src/modules/canvas/external.route.ts |
Documents external-note capability ownership. |
apps/server/src/modules/canvas/canvas.route.ts |
Updates artifact and export behavior. |
apps/server/src/modules/canvas/canvas.route.test.ts |
Tests extension-aware bundle export. |
apps/server/src/modules/canvas/canvas-executor.ts |
Reads image dimensions from artifacts. |
apps/server/src/modules/canvas/canvas-executor.test.ts |
Updates executor artifact setup. |
apps/server/src/modules/canvas/canvas-content-cas.test.ts |
Adapts BlobStore test doubles. |
apps/server/src/modules/artifact/utils.ts |
Resolves images from artifacts. |
apps/server/src/modules/artifact/artifact.route.ts |
Migrates artifact endpoints. |
apps/server/src/modules/artifact/artifact.route.test.ts |
Updates artifact route coverage. |
apps/server/src/modules/agent/tools/handlers/image-generation.ts |
Stores generated images as artifacts. |
apps/server/src/modules/agent/tools/handlers/fs-write.ts |
Writes Space memory through blobs. |
apps/server/src/modules/agent/tools/handlers/fs-write.test.ts |
Tests blob-backed memory writes. |
apps/server/src/modules/agent/tools/handlers/fs-sandbox.ts |
Uses shared capability refusals. |
apps/server/src/modules/agent/tools/handlers/fs-read.ts |
Awaits blob-backed memory reads. |
apps/server/src/modules/agent/memory/writers.ts |
Abstracts file and blob documents. |
apps/server/src/modules/agent/memory/trigger.ts |
Moves bookkeeping to extensions. |
apps/server/src/modules/agent/memory/trigger.test.ts |
Tests extension-backed bookkeeping. |
apps/server/src/modules/agent/memory/sandbox.ts |
Removes obsolete memory paths. |
apps/server/src/modules/agent/memory/read.ts |
Reads Space memory from blobs. |
apps/server/src/modules/agent/memory/analyzer.ts |
Awaits portable memory reads. |
apps/server/src/modules/agent/memory/analyzer.test.ts |
Updates analyzer storage mocks. |
apps/server/src/modules/agent/conversation/prompt/debug-prompt.ts |
Moves prompt logs to extensions. |
apps/server/src/modules/agent/conversation/prompt/attachments.ts |
Reads attachments from artifacts. |
apps/server/src/app.ts |
Closes storage during shutdown. |
Review details
Suppressed comments (1)
apps/server/src/modules/storage/capabilities.ts:148
- This does not share wording with the startup declaration: this function emits “is not available … (capability …),” while
describeUnavailableCapabilitiesemits “: … — unavailable on the structured backend.” That defeats the stated guarantee that operators see the same sentence at startup and at refusal. Build startup lines from this canonical formatter (or have both call one formatter).
- Files reviewed: 63/63 changed files
- Comments generated: 5
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Both writers are a read-modify-write, and the move to a `MemoryDocument` made both halves awaited. `runForTier` still locked only the workspace tier, which was safe when a canvas edit was `readFileSync` + `atomicWriteText` in one turn and is not safe now: two `replace_string` calls for one Space read the same body, and the later `put` discards the earlier edit while both report success. Serialising by tier cannot fix that — the Space body is one document per Space, and the workspace body is one document shared by every Space. So the lock is keyed by the document. `target` cannot be that key, because it is the caller's vocabulary and every Space calls its body `memory/space.md`; documents now carry an identity of their own alongside the name they display. The tier knob keeps its other two jobs, cap enforcement and skill-cache invalidation, and loses the branch it no longer needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
…ared §12.8.3 says a feature's refusal shares its wording with the startup declaration, so the sentence an operator read when they chose the profile is the sentence they see in the failure. Bundle import and the built-in file tools did that; bundle export and reveal-in-file-manager answered "Canvas directory not found" / "Nodes folder not found", and external-note claim answered "External note not found". A profile that cannot serve the feature was therefore indistinguishable from missing data. Split the two: an absent `diskTree` is the declared limitation and answers with the matrix sentence, while a Space whose directory is genuinely gone keeps its own 404. Nothing changes on Disk, where `diskTree` is never null — which is also why the new coverage serves the real Disk stores under a profile that claims to keep Spaces in tables, rather than waiting for a second adapter to exercise a refusal that is already written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
The criterion said "every area, not only artifacts" and then seeded two of the four. An adapter that left `guide` or `uploads` behind — or an edit that dropped one from `spaceBlobAreas` — would still have passed the product-level proof. Seed all four and assert each is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
`workspace/paths.ts` described the memory body as one of the per-Space families it anchors on the Disk tree, and promised the move as future work. The move happened in this change set — the body is a blob under the Space's own scope, and this module no longer names its placement. ACP sessions are the sole family left; the layout block and the memory-scope note said otherwise too. The proposal counted "two" matrix-declared `diskTree` consumers and then listed three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
Two conflicts, one textual and one semantic. `docs/architecture/canvas-storage.md`: main added the `resolveDirectChildPath()` rename boundary above the paragraph this branch rewrote. Both kept — the new paragraph, then this branch's version of the deletion paragraph, whose last sentence stops calling the residual direct-filesystem capabilities blockers for a non-Disk profile now that the matrix declares them. Cross-Space Move (microsoft#148) was written against the previous blob port and did not conflict textually: it reached `getBlobStore().scope({kind: 'canvas', canvasId})`, which this branch replaced with one area per user-visible family behind `space(canvasId)`. Its artifact clone now reads and writes through `space(id).artifacts`, which also puts the destination write behind the cross-store rule the facade owns — a Space deleted mid-move can no longer take orphan bytes with it. Verified on the merge: server 1187 passed, web 1149 passed, shared 379 passed, typecheck across every package, lint (0 errors), format, i18n parity, and license headers. The `Agenetes.rehome` type errors seen first were a stale subtree build, not a merge defect — `pnpm run build:agenetes` clears them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011J3JGQNx3Aecxc96HeuTPc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The per-Space state that is still a file because it always was one gets an owner, the resulting profile declares what it cannot do, and the whole thing is proved against a real backend rather than a stub.
Last of three change sets, specified in
docs/proposals/multi-backend-storage.md§6.4.2–§6.4.4 and §12.8. No SQLite schema, driver, migration, or profile-selectability branch.Its own commits (
57f5d461…58d3b144):space(canvasId).extension(namespace)hands an owner an isolated place — a reserved directory on Disk — and nothing else. Storage validates the namespace, creates it on demand, and destroys it with the Space, which is the one operation an owner cannot perform itself. Its contract is isolation and lifecycle only; there is no data behaviour to assert because the port never sees the data. Memory-worker state and the debug chat prompt log are its first two owners, retiring two ad-hoc file formats.existsSync(spaceDir)check before writing into a Space that might be gone.extension()returningnullfor an absent Space states it once, and the per-owner guards were deleted rather than moved.BlobStore.space(id)returnsSpaceBlobs—artifacts,guide,memory,uploads— symmetric withStructuredStore.space(id), so the facade stops assembling scope descriptors by hand and the Disk paths a user sees are unchanged. Theguidearea is bounded by its member names rather than a directory: its area is the Space root, where a directory scope would letlist()claimspace.jsonanddeleteAll()remove the Space. Upload scratch got its area but keeps itsfs-sandboxwriters — RFS upload is a streaming handler plus path classification, not the barereadFileSync/writeFilethe exception covers.validateStorageProfile()consults. Startup logs what the selected profile does not offer and each refusal reuses that wording. An unavailable feature is a stated limitation and startup continues; a profile naming an unimplemented backend stays a misconfiguration that fails fast.storage/testing.tsopens a real profile against a temporary Workspace through the production lifecycle — prepared Workspace, opened connections,ensureWorld().product-boundary.test.tsruns the exit criterion against every entry inPRODUCT_STORAGE_PROFILES, and a guard reads that suite's own source and rejects a directory, a filename, or areadFileSyncappearing in it. Phase 5 adds one list entry and the same behaviours are covered for SQLite.closeStorage()closes both connections on graceful shutdown. On Disk it is close to a no-op, which is why it exists before a connection-holding backend does.The exit criterion now holds on both halves. Neutrality: no production module outside
storage/imports a Disk layout or legacyCanvasStoresymbol. One handle: every storage capability for one Space is reached throughspace(canvasId), and the five consumers still holdingdiskTreeare the three the matrix declares Disk-only, RFS's sidecar-to-record mapping (deferred until a second backend has a file plane), and the ACP session path that leaves with the AgenetesNamespacechange.Stacking. Builds on #133 → #132 → #130; none of those branches exist on
microsoft/Huabu, so the diff below carries their commits too. This PR's own work starts at57f5d461.pnpm run checkpasses at this branch's tip.🤖 Generated with Claude Code
https://claude.ai/code/session_011EKZXxEWwZvAZo8YMaLv4m