agentHost: multi-root turn changesets and session diff summary - #329533
agentHost: multi-root turn changesets and session diff summary#329533Don Jayamanne (DonJayamanne) wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds multi-root Agent Host diff aggregation, repository-aware blob resolution, and all-folder session summaries.
Changes:
- Aggregates turn diffs across Git and non-Git workspace roots.
- Adds multi-folder summary and operation/UI filtering behavior.
- Expands unit coverage for aggregation, filtering, and blob URIs.
Show a summary per file
| File | Description |
|---|---|
src/vs/sessions/contrib/providers/agentHost/test/browser/agentHostSessionChangesets.test.ts |
Tests primary-root filtering. |
src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts |
Passes workspace observables to changesets. |
src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts |
Filters displayed turn changes. |
src/vs/platform/agentHost/test/node/sessionDiffAggregator.test.ts |
Tests root-filtered aggregation. |
src/vs/platform/agentHost/test/node/gitDiffContent.test.ts |
Updates blob URI expectations. |
src/vs/platform/agentHost/test/node/agentSideEffects.test.ts |
Updates changeset fake. |
src/vs/platform/agentHost/test/node/agentHostTurnTelemetry.test.ts |
Updates telemetry fake. |
src/vs/platform/agentHost/test/node/agentHostToolCallTelemetry.test.ts |
Updates telemetry fake. |
src/vs/platform/agentHost/test/node/agentHostMultiRootDiff.test.ts |
Tests multi-root orchestration. |
src/vs/platform/agentHost/test/node/agentHostCommitOperationHandler.test.ts |
Updates changeset test service. |
src/vs/platform/agentHost/test/node/agentHostChangesetOperationService.test.ts |
Tests operation suppression. |
src/vs/platform/agentHost/test/node/agentHostChangesetCoordinator.test.ts |
Tracks summary refreshes. |
src/vs/platform/agentHost/test/common/sessionTestHelpers.ts |
Updates no-op service helper. |
src/vs/platform/agentHost/test/common/agentHostWorkingDirectories.test.ts |
Tests root selection utilities. |
src/vs/platform/agentHost/node/sessionDiffAggregator.ts |
Adds root-based edit filtering. |
src/vs/platform/agentHost/node/gitDiffContent.ts |
Exposes blob absolute paths. |
src/vs/platform/agentHost/node/agentService.ts |
Resolves owning blob repository. |
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts |
Implements multi-root aggregation. |
src/vs/platform/agentHost/node/agentHostChangesetService.ts |
Computes turn diffs and summaries. |
src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts |
Suppresses aggregate operations. |
src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts |
Starts summary refreshes. |
src/vs/platform/agentHost/common/agentService.ts |
Documents summary semantics. |
src/vs/platform/agentHost/common/agentHostWorkingDirectories.ts |
Adds repository-root selection. |
src/vs/platform/agentHost/common/agentHostChangesetService.ts |
Extends the changeset service API. |
Review details
Suppressed comments (1)
src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts:101
- This does not mirror the changeset service's gate: that service uses
getEffectiveWorkingDirectories, which inherits a parent's roots for subagents, while this reads only the child session's own state. A subagent without its own directories under a multi-root parent therefore gets an aggregated turn changeset but still advertises primary-repository operations. UseIAgentConfigurationService.getEffectiveWorkingDirectorieshere too.
private _isMultiFolderCopilot(sessionKey: string): boolean {
if (AgentSession.provider(sessionKey) !== 'copilotcli') {
return false;
}
const dirs = this._stateManager.getSessionState(sessionKey)?.workingDirectories;
return !!dirs && dirs.length > 1;
- Files reviewed: 24/24 changed files
- Comments generated: 8
- Review effort level: Balanced
ec27bbb to
f0b5f08
Compare
For multi-root Copilot sessions (more than one working folder), aggregate file changes across all folders. Single-folder and non-Copilot sessions keep today's behaviour byte-for-byte. No AHP protocol changes. - Problem 1: the per-turn changeset returns files from every folder — a new reusable orchestrator diffs each unique git repo (parent->current turn checkpoint) and falls back to the path-scoped DB edit-tracker for non-git or failed-git folders. Dedups by repository root, runs per-repo diffs in parallel, caps at 20 targets, and never hard-fails (per-folder failures are logged). - Problem 2: SessionSummary.changes is computed independently across all folders using per-repo branch semantics, on its own sequencer key. The branch changeset's summary write is guarded to single-folder so it can't clobber the all-folder aggregate. - Turn and compare-turns changeset operations are suppressed centrally in the operation service for multi-root sessions (covers both publish and updateOperations). - git-blob content resolution is now repository-aware: the owning repo root is derived server-side from the blob's absolute path (Option A), so non-primary folder diffs open correctly without trusting a client-held cwd. - Agents Window 'Last Turn Changes' is filtered to the primary repository root so the single-root Changes panel stays consistent. Adds unit coverage for the orchestrator, the includeUnder DB filter, git-blob absolute-path parsing, repo-root selection, turn/compare operation suppression, and the Q6b client filter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f0b5f08 to
82b8e9d
Compare
There was a problem hiding this comment.
Review details
Suppressed comments (7)
src/vs/platform/agentHost/node/agentHostChangesetOperationService.ts:78
- Compare-turns changesets are still computed only from
workingDirectories[0]inAgentHostChangesetService.computeCompareTurnsChangeset(lines 479-521), so they do not aggregate repositories. This condition removes valid primary-repository operations based on a false assumption. Either aggregate compare-turns across roots too, or restrict suppression toChangesetKind.Turn.
if ((parsed.kind === ChangesetKind.Turn || parsed.kind === ChangesetKind.Compare) && this._isMultiFolderCopilot(sessionKey)) {
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:131
- Pushing into
gitResultsfrom parallel callbacks makes result order depend on which repository finishes first. That contradicts the documented effective-directory ordering and also makes the winner for duplicate IDs nondeterministic. Return each target's result fromPromise.alland flatten ingitTargetsorder instead.
gitResults.push(...diff);
src/vs/platform/agentHost/node/sessionDiffAggregator.ts:147
optionsis silently ignored wheneverincrementalis supplied: only the full-mode branch forwards it, while incremental turn edits, full history, and carriedpreviousDiffsremain unfiltered. Either apply the filter throughout incremental mode or make the API reject/disallow this argument combination.
options?: IDiffFilterOptions,
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:162
- This deduplicates URI identities by raw string, so casing variants of the same file remain separate on case-insensitive platforms. Use
extUriBiasedIgnorePathCase.getComparisonKey(URI.parse(resource)), consistent withsrc/vs/base/common/resources.ts:340-344and the repository URI-key convention.
const id = diff.after?.uri ?? diff.before?.uri;
if (!id) {
continue;
}
byId.set(id, diff);
src/vs/platform/agentHost/common/agentHostWorkingDirectories.ts:33
- Reconstructing a file URI from only
absolutePathloses UNC authority. A root such asfile://server/share/repocarriesserverinauthority, whilebuildGitBlobUristores only the file URI's.path; this createsfile:///share/repo/..., which can never match the trusted root and makes secondary UNC diffs fail with NotFound. Carry the complete file URI (or its authority) through the git-blob URI.
export function selectRepositoryRootForBlobPath(absolutePath: string, repositoryRoots: readonly URI[]): URI | undefined {
const fileUri = URI.from({ scheme: Schemas.file, path: absolutePath });
return findDeepestContainingWorkingDirectory(fileUri, repositoryRoots);
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:85
- The safety cap is applied only after this
Promise.allhas launched a repository-root probe for every directory. Since each distinct probe can rungit rev-parse, a pathological workspace still creates unbounded concurrent processes before the 20-target cap takes effect. Resolve with bounded concurrency and stop once enough unique targets have been collected.
const resolved = await Promise.all(workingDirectories.map(async dir => {
try {
return { dir, repoRoot: await ctx.getRepositoryRoot(dir) };
src/vs/platform/agentHost/node/agentHostChangesetService.ts:816
- This JSDoc enumerates implementation branches and internal scheduling details across 21 lines. Sessions guidance limits JSDoc to 1–2 short sentences; condense it to the method's purpose and leave the implementation details in the named helpers.
/**
* Computes the branch changeset AND the all-folder session-list summary for a
* multi-root Copilot session in a single pass (Problem 2), folded together so
* the primary repository is diffed exactly once and every folder is diffed in
* parallel.
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Balanced
Resolve each secondary repository's default branch before computing its branch-semantic diff, falling back to the edit tracker when no default branch is available. Deduplicate file resources with platform-aware path casing while preserving exact non-file identities. Make the new comments self-contained and concise, correct the compare-operation rationale, and add focused regression coverage for default-branch resolution and deduplication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (4)
src/vs/platform/agentHost/common/agentHostWorkingDirectories.ts:33
- Reconstructing the file URI from
pathalone always uses an empty authority. For a UNC repository,URI.file('\\\\server\\share\\repo')has authorityserver, while this reconstructed URI does not, soisEqualOrParentnever finds the owning root and multi-root git-blob reads return NotFound. Preserve the original file URI authority (or the complete file URI) in the git-blob payload and reconstruct it here.
src/vs/platform/agentHost/node/agentHostChangesetService.ts:832 - Secondary repositories are diffed without a
baseBranch.computeSessionFileDiffsexplicitly falls back toHEADin that case, so the all-folder summary omits committed-on-branch changes from every non-primary repository. Resolve each secondary repository's default branch and pass it into the diff computation.
}
/**
src/vs/platform/agentHost/node/sessionDiffAggregator.ts:147
optionsis only forwarded in full mode; whenincrementalis also supplied,includeUnderis silently ignored and both newly loaded edits and carriedpreviousDiffscan contain files outside the requested roots. Either support filtering throughout the incremental path or reject/document this parameter combination so this exported API cannot return an unfiltered result.
incremental?: IIncrementalDiffOptions,
options?: IDiffFilterOptions,
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:83
- The safety cap is applied only after this mapping has started a repository-root probe for every working directory. A pathological workspace can therefore still launch an unbounded number of concurrent Git processes before being truncated to 20 targets, defeating the cap's resource-protection purpose. Bound/concurrency-limit root resolution or stop once enough unique targets have been identified.
try {
- Files reviewed: 17/17 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Treat a primary cwd nested inside its repository as part of the full repository while preserving separate worktree isolation. Add production-shaped regression tests for both layouts and document the workspace metadata distinction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace a decorative Unicode arrow in a new comment with ASCII prose so the repository hygiene check passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject stale Turn and Compare operations after dynamic root changes, and preserve cached session summaries when every multi-root diff target fails. Make aggregation outcomes explicit and reject unsupported incremental filtering.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (6)
src/vs/platform/agentHost/node/agentHostChangesetService.ts:811
- This fallback reads only the parent session database. Peer chats record file edits in separate databases (see
_openPeerChatSourcesin this file), so any non-Git or failed-Git root omits peer-chat edits from the new all-folder summary. Include all peer-chat sources in the filtered union and dispose their database refs after the aggregate finishes.
computeFallbackDiff: roots => computeUnionedDiffs([{ sessionUri: session, db }], this._diffComputeService, { includeUnder: roots }),
src/vs/platform/agentHost/node/agentHostChangesetCoordinator.ts:138
- A real root-set mutation never invalidates the changeset data here: 2→3 returns immediately, while 1→2 only refreshes operation metadata below. Because this is the only working-directory mutation hook, subscribed Branch/Session/Turn changesets and
summary.changesremain computed from the old roots until an unrelated edit or Git refresh. Recompute subscribed changesets for every non-idempotent root change, and keep the boundary check only for operation suppression.
const wasMultiRoot = (previousWorkingDirectories?.length ?? 0) > 1;
const isMultiRoot = (currentWorkingDirectories?.length ?? 0) > 1;
if (wasMultiRoot === isMultiRoot) {
return;
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:101
- Concurrent callbacks append to
gitResultsin completion order, and the finalMappreserves that order. Repository timing can therefore reorder the same changeset between refreshes; the client equality check is order-sensitive, so this also causes unnecessary updates and visible file-list jumps. Return each result fromPromise.alland flatten them ingitTargetsorder.
successfulTargetCount++;
gitResults.push(...diff);
} catch (err) {
ctx.logService.error(`[MultiRootDiff] Git diff failed for ${repoRoot.toString()} in ${ctx.session}: ${errText(err)}; falling back to edit-tracker.`);
fallbackRoots.push(repoRoot);
src/vs/platform/agentHost/common/agentHostWorkingDirectories.ts:26
absolutePathcame fromURI.path, which does not carry a file URI's authority. Reconstructing an authority-less URI here cannot match UNC roots such asfile://server/share/repo, so multi-rootgit-blobcontent resolution returns NotFound for those repositories. Carry the complete absolute file URI (including authority) in the blob metadata instead of only its path.
export function selectRepositoryRootForBlobPath(absolutePath: string, repositoryRoots: readonly URI[]): URI | undefined {
const fileUri = URI.from({ scheme: Schemas.file, path: absolutePath });
return findDeepestContainingWorkingDirectory(fileUri, repositoryRoots);
src/vs/sessions/contrib/providers/agentHost/browser/agentHostSessionChangesets.ts:37
- For a rename,
change.uriis the after path, so moving a file from the primary checkout to another root is dropped even though the primary checkout lost that file. This also conflicts with the backend filter, which deliberately retains renames when either the current or original path is under a selected root. Include the before-side path when deciding primary-root membership.
return changes.filter(change => {
const resource = isIChatSessionFileChange2(change) ? change.uri : change.modifiedUri;
return extUriBiasedIgnorePathCase.isEqualOrParent(resource, primaryRoot);
src/vs/platform/agentHost/node/agentHostMultiRootDiff.ts:55
- The target cap is applied only after every working directory has concurrently run
getRepositoryRoot. Since working-directory actions have no count limit and this lookup invokes Git, a large root set can still launch an unbounded number of subprocesses despiteMAX_MULTI_ROOT_DIFF_TARGETS. Bound the resolution concurrency and stop once the retained target budget is satisfied.
const resolved = await Promise.all(workingDirectories.map(async dir => {
try {
return { dir, repoRoot: await ctx.getRepositoryRoot(dir) };
- Files reviewed: 22/22 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Migrate six comparison findings into this branch while preserving this implementation's advantages and avoiding the other implementation's defects: - #2 Extract a pure module (agentHostMultiRootDiff.ts) holding the diff-dedup and source-evaluation logic, decoupled from the service. - #7 Dedup turn diffs first-wins, keyed on the destination URI with a platform-aware comparison for file: URIs and exact identity otherwise. - #5 Evaluate multi-root diff sources by availability; preserve the cached branch summary only on total failure (zero available sources) and write a genuine {0,0,0} when a source is empty (no over-preserve). - #10 Add an opt-in onRootError callback to resolveSessionRepositories so a single failed root in a turn falls back to the non-git DB diff; summary and git-blob callers keep rethrow semantics. - #16 Clear suppressed turn/compare operations before the git-state gate so a multi-root transition clears them even when git state is absent, while non-suppressed kinds still defer. - #14 Add active tests only; the pre-existing upstream suite.skip is untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
No description provided.