spec: multiroot sessions (draft for feedback)#337
Conversation
- Add `MultipleWorkspaceFoldersCapability` and `AgentCapabilities.multipleWorkspaceFolders` to `types/channels-root/state.ts`, mirroring the `multipleChats` pattern. Presence signals the agent supports multiple equal-peer working directories. - Add `CreateSessionParams.workingDirectories` (plural, replaces the singular `workingDirectory`) to `types/channels-session/commands.ts`. The old singular field is retained as a deprecated backwards-compatible shorthand. Forked sessions ignore `workingDirectories` and inherit from the source session. - Add `AddWorkspaceFolderParams`, `RemoveWorkspaceFolderParams`, `WorkspaceFolderResult`, `AddWorkspaceFolderResult`, and `RemoveWorkspaceFolderResult` to `types/channels-session/commands.ts`. Both commands return the full directory set after the mutation. `removeWorkspaceFolder` is modelled as idempotent reconfigure-to-reduced-set (no server-side atomic remove primitive). - Register `addWorkspaceFolder` and `removeWorkspaceFolder` in `CommandMap` in `types/common/messages.ts` and `_ExpectedCommands` in `types/version/message-checks.ts`. - Version-gating: new commands and capability land in AHP 0.6.0 (a 0.x minor bump per the versioning doc, matching a capability boundary). Gating is purely via the `multipleWorkspaceFolders` capability + version handshake. The `ACTION_INTRODUCED_IN` / `NOTIFICATION_INTRODUCED_IN` maps are not touched because neither new symbols are state actions nor notifications. - Update `CHANGELOG.md` (spec and all five clients) with `## [Unreleased]` entries under `### Added` / `### Deprecated`. - Update `docs/guide/state-model.md` with a new "Multiroot Sessions" section plus updated `SessionState` / `SessionSummary` snippets. - Regenerate all client mirrors (Swift, Rust, Kotlin, TypeScript, Go) and update the four generator scripts (`generate-swift.ts`, `generate-rust.ts`, `generate-kotlin.ts`, `generate-go.ts`) with the new type registrations and helper methods. All 312 tests pass; 100% branch coverage on reducers.ts maintained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends the multiroot session design with per-chat directory subsets: - Add `SessionMetadata.workingDirectories` (on `SessionState` and `SessionSummary`), the mirror of the `CreateSessionParams.workingDirectories` set as maintained by `addWorkspaceFolder`/`removeWorkspaceFolder`. Deprecate the singular `workingDirectory` on `SessionMetadata`; hosts SHOULD keep populating it (first entry) for backwards compatibility. - Add `ChatState.workingDirectories` and `ChatSummary.workingDirectories`: the subset of the session's `workingDirectories` that this chat's agent has tool access to. When absent the chat inherits the full session set. Every entry must be present in the session's set. Deprecate the singular `workingDirectory` on both. - Add `CreateChatParams.workingDirectories`: initial subset at create time. Ignored for forked chats (they inherit the source chat's set). Gated on `multipleWorkspaceFolders` capability. - Add `addChatWorkspaceFolder` and `removeChatWorkspaceFolder` commands (symmetrical to the session-level pair) for mutating the chat's subset on a running chat. The add command enforces the subset invariant (server rejects any directory not in the session's set). The remove command is idempotent. Both return `AddChatWorkspaceFolderResult` / `RemoveChatWorkspaceFolderResult` carrying the chat's full subset after the mutation. - Register both commands in `CommandMap`, `_ExpectedCommands`, all four generator scripts, and all CHANGELOGs. - Update `docs/guide/state-model.md` with a "Per-chat working-directory subsets" subsection and updated `ChatState` code snippet. All 316 tests pass; 100% branch coverage maintained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an optional Changeset.workingDirectory (with a 'directory' changeKind hint) so a multiroot session groups its file changes by directory — advertising one changeset per working directory rather than nesting changes as arrays-of-arrays. Also add the shareable multiroot-sessions feature proposal under docs/proposals/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The @Version JSDoc tag is a per-message revision counter (integer, starting at 1 for a brand-new message), not the protocol semver. The new multiroot commands were mistakenly tagged @Version 0.6.0; align them with the convention used by every other first-revision message. Documentation-only: nothing parses @Version, so no generated output changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expand the proposal's protocol-surface section with a per-symbol change table and concrete TypeScript signatures (capability, session/chat working-directory fields + deprecations, the four add/remove commands, Changeset.workingDirectory, CommandMap entries) plus a versioning/gating note, so reviewers can grasp the exact protocol delta at a glance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| * Omit for changesets that are not directory-scoped (e.g. a single-directory | ||
| * session, or a session-wide roll-up spanning every directory). | ||
| */ | ||
| workingDirectory?: URI; |
There was a problem hiding this comment.
Would it work to say that Changesets always just cover all directories? Then you can have one changeset which covers all changes made, and it should be easy to see which dir a particular change is part of, based on which dir the file is in.
Also I'm not sure how the "Other files" work today, we can already track changes outside of a working dir
There was a problem hiding this comment.
Yea, that also avoids future questions about nested git repos etc., I would drop this unless we have an experience we struggle to drive without it
There was a problem hiding this comment.
I'd actually push to keep this and go further — make server-side grouping a normative MUST (while the field stays optional). My reasoning is that grouping changes by directory is a host responsibility, not a client one.
Per AHP's doctrine, AHP is a "client-facing presentation model" giving clients "display-ready session state ... without requiring the client to understand a particular filesystem model," with the "host [owning] the authoritative state." Deriving a file's directory by prefix-matching its URI against the session's working directories is a filesystem model — and if the server doesn't do it, every client (VS Code, CLIs, web, mobile) re-implements the same grouping independently and inconsistently. That's exactly the semantic work AHP is meant to centralize in the host.
And the client can't even do it correctly: only the host knows the real VCS boundaries. A file under repo-a/ may actually live in a nested repo-a/vendor/repo-c/ git repo; longest-prefix matching gets that wrong, and the client can't see symlinks, submodules, or worktrees. The host is the only party that knows which root a change truly belongs to — so grouping belongs where that knowledge is.
So rather than dropping it, I've changed the spec to say: a host with multiple working directories MUST emit one changeset per directory (each with workingDirectory set). The field stays optional only so the cases you both raised still work — a single-directory session, and out-of-tree / aggregate changesets that intentionally span or sit outside the working dirs (Rob's "Other files"). So nothing we can track today is lost, a minimal client can still ignore the field, and it's gated by multipleWorkspaceFolders.
Bonus: it also fits the existing catalogue (the host already advertises session/turn/compare-turns/branch/uncommitted views — directory is one more server-owned slice), and gives per-repo operations like create-pr/commit a natural scope, since those are inherently per-repo with sibling repositories.
On nested repos specifically — I'd argue an explicit workingDirectory on the changeset resolves the ambiguity rather than creating it: the host states which root owns the changeset instead of leaving clients to guess.
There was a problem hiding this comment.
I don't understand the point about VCS boundaries- we care about matching the file to a directory, which is a list we already have, not a git repo.
Also, I'm not actually sure that the client would typically care to group by working directory anyway, does it matter? I think I'd render a tree for the session and call it a day.
But if you want to group them, understanding URIs at this level doesn't seem like it's making wild assumptions about the remote.
There was a problem hiding this comment.
You've convinced me — dropped Changeset.workingDirectory (and the 'directory' changeKind). You're right that matching a file to one of the session's directories is a simple client-side operation against a list we already have, not a VCS-boundary problem, so the "clients can't do it" argument doesn't hold.
There's also a modelling flaw I'd missed: a changeset can span directories (a per-turn diff touches several), so a single-directory scalar can't represent the common case anyway. So the new shape is: changesets stay cross-cutting; a client that wants a per-directory view groups the files itself against workingDirectories; and a host MAY additionally advertise dedicated per-directory changesets as extra catalogue entries (the changesets list is already unbounded — no schema change needed). If we later want machine-readable per-directory entries, a {workingDirectory} uriTemplate variable is the natural follow-up.
There was a problem hiding this comment.
@sandy081, I would like to point out what while some changeset may span multiple working directories (ex: last turn changes), some changesets are very much scoped to a git repository (ex: branch changes). Branch changes which currently is the default changeset for a session, compares branchA with main with the branch information even present in the changeset description which is surfaced in the UI. In light of that I would like to suggest to reconsider the optional workingDirectory on the changeset.
- Directory mutations are now state actions, not commands: replace the four add/remove(Chat)WorkspaceFolder commands with client-dispatchable session/workingDirectorySet, session/workingDirectoryRemoved, chat/workingDirectorySet, chat/workingDirectoryRemoved actions (keyed by the directory URI, idempotent), with reducers, ACTION_INTRODUCED_IN entries (0.6.0), and full-branch conformance fixtures. - Hard-remove the deprecated singular `workingDirectory` from CreateSessionParams, SessionMetadata, ChatState, and ChatSummary (0.6.0 is a breaking release). Update hand-written client reducers/mergers in every language accordingly. - Add `immutablePrimary?` to MultipleWorkspaceFoldersCapability so backends that pin a fixed process root can advertise it while keeping equal peers the default. - Keep Changeset.workingDirectory and strengthen it to a normative MUST: a multiroot host groups changes by directory (server-side), per AHP's display-ready-state doctrine. - Move the CHANGELOG entries into a docs/.changes fragment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nd 2) - Rename the capability `multipleWorkspaceFolders` -> `multipleWorkingDirectories` (and `MultipleWorkspaceFoldersCapability` -> `MultipleWorkingDirectoriesCapability`) for consistency with the `workingDirectory` vocabulary used everywhere else (per DonJayamanne). - Drop `Changeset.workingDirectory` and the `'directory'` changeKind. A changeset can span working directories (e.g. a per-turn diff), so a single-directory scalar is the wrong model. Per-directory presentation is optional: clients group a changeset's files against the session's workingDirectories, and a host MAY advertise extra per-directory catalogue entries (the changesets list is already unbounded). Resolves the reviewer thread rather than holding the MUST. - Update state-model / changesets guides and the proposal accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…root-sessions-support-spec # Conflicts: # types/channels-chat/actions.ts
CI (cargo/go/gradle/swift test) went red after the merge because the four new working-directory state actions were only handled by the TypeScript reducer — the hand-written Rust/Go/Kotlin/Swift reducers no-op'd them, so the shared conformance fixtures (append/remove cases) failed. Add the session/chat workingDirectorySet + workingDirectoryRemoved cases to each native reducer (idempotent set / no-op remove, mirroring activeClientSet). Also fix two Rust integration-test struct literals that still set the removed singular `working_directory` field (cargo test compiles tests; the earlier local `cargo build` did not, so it slipped through). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…root-sessions-support-spec
spec/v0.6.0 released on 2026-07-20 without multiroot and still contains the singular `workingDirectory` fields this feature removes. Removing released state fields breaks compatibility within the 0.6.x MINOR, so per the versioning policy this feature must land in a new MINOR. - Bump PROTOCOL_VERSION 0.6.1 -> 0.7.0; SUPPORTED_PROTOCOL_VERSIONS = [0.7.0, 0.6.0, 0.5.2, 0.5.1]. - Retag the four working-directory actions ACTION_INTRODUCED_IN 0.6.0 -> 0.7.0 (0.6.0 shipped without them, so a 0.6.0 peer does not understand them). - Rotate the CHANGELOG dev heading 0.6.1 -> 0.7.0 and update the proposal's versioning section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit 40eb488.
|
0.7.0 is fine here, this is a breaking change |
This reverts commit 0d715aa.
… chat) Per finalized design: - Rename the capability flag immutablePrimary -> requiresPrimary. It now means "the agent needs one directory designated as primary" (a distinguished root), rather than "index 0 is a fixed process root". Primacy is explicit, never inferred from array position. - Add primaryWorkingDirectory (URI) to CreateSessionParams + SessionMetadata (-> SessionState/SessionSummary) and to CreateChatParams + ChatState/ ChatSummary. A chat inherits the session's primary when it omits its own. - Update hand-written client reducers/mergers (Go/Rust/Kotlin/Swift) to carry primaryWorkingDirectory through session/chatUpdated, add the field to Go's hand-maintained PartialChatSummary, and fix Rust test struct literals. - Docs (state-model, proposal) and changelog fragment updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update:
|
| return state; | ||
| } | ||
| const updated = list.slice(); | ||
| updated.splice(idx, 1); |
There was a problem hiding this comment.
AI Review: Could this removal be rejected unless related state is updated transactionally? As written it can leave primaryWorkingDirectory pointing outside the session set and existing chat subsets referencing a directory the session no longer owns. There is also no action for reassigning the primary before removing it.
| */ | ||
| export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ | ||
| '0.6.1', | ||
| '0.7.0', |
There was a problem hiding this comment.
AI Review: Can clients built from this source still truthfully advertise 0.6.0 fallback? CreateSessionParams has removed the singular workingDirectory, so after negotiating 0.6 this client no longer has a way to construct that version's session-creation wire shape.
| // ── Working Directories ─────────────────────────────────────────────── | ||
|
|
||
| case ActionType.ChatWorkingDirectorySet: { | ||
| const list = state.workingDirectories ?? []; |
There was a problem hiding this comment.
AI Review: When workingDirectories is absent, the state contract says this chat inherits the full session set. Here absence is treated as an empty set, so adding one directory narrows the inherited set to a singleton, while the removal case below cannot remove an inherited directory. How can these actions preserve the documented inheritance semantics without access to the session state?
| if (changes.modifiedAt !== undefined) merged.modifiedAt = changes.modifiedAt; | ||
| if (changes.project !== undefined) merged.project = changes.project; | ||
| if (changes.workingDirectory !== undefined) merged.workingDirectory = changes.workingDirectory; | ||
| if (changes.workingDirectories !== undefined) merged.workingDirectories = changes.workingDirectories; |
There was a problem hiding this comment.
AI Review: Should this also merge changes.primaryWorkingDirectory? Without it, the cached session summary can retain a stale primary. The Rust and Swift host runtime summary mergers have the same omission.
| WorkingDirectory *URI `json:"workingDirectory,omitempty"` | ||
| // The subset of the session's working directories this chat uses. | ||
| WorkingDirectories []URI `json:"workingDirectories,omitempty"` | ||
| // The chat's primary working directory. |
There was a problem hiding this comment.
AI Review: omitempty collapses a present-but-empty slice into an absent field, but the chat contract distinguishes explicit empty (no working-directory access) from absent (inherit the session set). Can the Go representation preserve that distinction, or should the protocol drop the explicit-empty semantic?
|
|
||
| case ActionType.SessionWorkingDirectorySet: { | ||
| const list = state.workingDirectories ?? []; | ||
| if (list.includes(action.directory)) { |
There was a problem hiding this comment.
AI Review: What defines URI identity for this set? Every reducer currently uses raw string equality, so equivalent URI spellings can be duplicated or fail removal across clients. The protocol should define wire-level equality or provide conformance fixtures for the intended behavior.
…xed field Finalized design for "primary working directory": - The SESSION has no primary. Dropped primaryWorkingDirectory from SessionMetadata (SessionState/SessionSummary). The session is purely an equal-peer directory set. - Primary is a PER-CHAT notion: ChatState.primaryWorkingDirectory (mirrored on ChatSummary) is user-visible read-only state, fixed at chat creation. There is no action to change it and it does not participate in session/chatUpdated — "in state" (so late subscribers can read it) but immutable (no action). - Set via CreateChatParams.primaryWorkingDirectory; CreateSessionParams. primaryWorkingDirectory now seeds the session's default chat. - Reworded the requiresPrimary capability accordingly (per-chat, fixed at creation) and the session/workingDirectoryRemoved decline note. - Removed the primaryWorkingDirectory handling from the client chatUpdated partial-summary merges (immutable → not a partial-update field) and dropped it from the Session rust struct literals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What this is
A first-class multiroot sessions proposal for AHP: a session can grant its agent tool access to multiple working directories (equal peers, no privileged "primary"); a chat operates on a subset of those; and file changes group by directory.
The plan/spec document is
docs/proposals/multiroot-sessions.md— start there. It covers the problem, mental model, what-it-is/isn't, a concrete protocol-surface summary (change table + TypeScript signatures in §8), design decisions, and open questions.Protocol changes (all additive & optional, target
0.6.0)AgentCapabilities.multipleWorkspaceFoldersgates the whole feature.CreateSessionParams.workingDirectories+SessionMetadata.workingDirectories(→SessionState/SessionSummary); singularworkingDirectorydeprecated.addWorkspaceFolder/removeWorkspaceFolder(session) andaddChatWorkspaceFolder/removeChatWorkspaceFolder(chat subset); removal is idempotent, modelled as reconfigure-to-reduced-set.ChatState/ChatSummary/CreateChatParams.workingDirectories(subset ⊆ session); singular deprecated.Changeset.workingDirectory+'directory'changeKind; one changeset per directory instead of arrays-of-arrays.Old clients that never set the fields / send the commands behave exactly as today. See §8 of the proposal for the full change table and signatures.
Open questions for reviewers
workingDirectorydeprecated (as here) vs. hard-remove it (pre-1.0 allows it)?resolveSessionConfig/sessionConfigCompletionsmultiroot-aware? (currently keep their singularworkingDirectorycontext.)ChangesSummary.byDirectory)?Validation
npm testgreen — typecheck + lint + verify-release-metadata + verify-changelog + 316 reducer/message tests, 100% branch coverage. Schemas + all five clients regenerated.Note
Branch is ~10 commits behind
origin/main; will rebase before any real merge. Draft-only for feedback.