src/vs/platform/agentHost/common/agentHostChangesetService.ts

293 LOC · 293 covered · 0 uncovered · 1 ranges · 1109 concepts · 1 introducers · 516 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostChangesetService.ts ×1
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { createDecorator } from '../../instantiation/common/instantiation.js';
7 > import type { ChangesSummary } from './state/protocol/state.js';
8 > import type { ISessionFileDiff, URI as ProtocolURI } from './state/sessionState.js';
9 >
10 > /** Metadata key under which the branch changeset's diff list is persisted. */
11 > export const META_CHANGESET_BRANCH = 'agentHost.changeset.branch';
12 >
13 > /** Metadata key under which the session-wide changeset's diff list is persisted. */
14 > export const META_CHANGESET_SESSION = 'agentHost.changeset.session';
15 >
16 > /**
17 > * Legacy metadata key used by older builds to persist the session-wide
18 > * changeset's diff list. Read-only fallback for {@link META_CHANGESET_SESSION}.
19 > */
20 > export const META_LEGACY_DIFFS = 'diffs';
21 >
22 > /**
23 > * Metadata key under which the session's changes is persisted.
24 > */
25 > export const META_CHANGES_SUMMARY = 'agentHost.changes';
26 >
27 > /**
28 > * The set of session-DB metadata keys the changeset service needs in a
29 > * batched read to synthesise the `summary.changes` aggregate for the
30 > * session-list overlay. {@link IAgentHostChangesetService.getListMetadataKeys}
31 > * returns this (or `undefined` when live state already answers the question);
32 > * `AgentService` merges the returned keys into its own metadata key set so the
33 > * DB is hit exactly once per session.
34 > */
35 > export const CHANGESET_DB_METADATA_KEYS: Record<string, true> = {
36 > [META_CHANGESET_BRANCH]: true,
37 > [META_CHANGESET_SESSION]: true,
38 > [META_CHANGES_SUMMARY]: true,
39 > [META_LEGACY_DIFFS]: true,
40 > };
41 >
42 > /** The two static changeset kinds we publish by default. */
43 > export type StaticChangesetKind = 'branch' | 'session';
44 >
45 > /**
46 > * Raw metadata values for the persisted changeset blobs, batch-read
47 > * by the caller (`AgentService.listSessions` / `AgentService.restoreSession`).
48 > * The caller owns the database read so multiple metadata keys can be
49 > * fetched in a single round-trip; the service owns parsing, applying,
50 > * and `seedIfEmpty` gating.
51 > */
52 > export interface IPersistedChangesetMetadata {
53 > readonly branchRaw?: string;
54 > readonly sessionRaw?: string;
55 > readonly legacyRaw?: string;
56 > }
57 >
58 > /**
59 > * The parsed diffs returned from {@link IAgentHostChangesetService.restorePersistedStaticChangesets},
60 > * suitable for synthesising a `summary.changes` aggregate for the
61 > * session-list overlay (see {@link IAgentHostChangesetService.computeListEntryChanges}).
62 > */
63 > export interface IRestoredChangesetDiffs {
64 > readonly branch?: readonly ISessionFileDiff[];
65 > readonly session?: readonly ISessionFileDiff[];
66 > }
67 >
68 > export const IAgentHostChangesetService = createDecorator<IAgentHostChangesetService>('agentHostChangesetService');
69 >
70 > /**
71 > * Owns the lifecycle of static and per-turn changesets for the agent host:
72 > * registers the `<session>/changeset/{uncommitted,session,turn/<id>}` URIs
73 > * on the state manager, runs git-driven and edit-tracker-driven diff
74 > * computations, debounces mid-turn recomputes, publishes file lists
75 > * (`changeset/fileSet` / `changeset/fileRemoved`) and aggregate counts
76 > * (`session/summaryChanged`), and persists results to the session DB so
77 > * restarts can rehydrate without recomputing.
78 > *
79 > * Created locally by `AgentService` (not via `registerSingleton`) and
80 > * added to the local `ServiceCollection` so `AgentSideEffects` can
81 > * resolve it via `@IAgentHostChangesetService`. `AgentHostStateManager`
82 > * is passed as a plain ctor argument (it has no decorator today); the
83 > * git / log / session-data services are DI-injected.
84 > */
85 > export interface IAgentHostChangesetService {
86 > readonly _serviceBrand: undefined;
87 >
88 > /**
89 > * Registers the two static changeset URIs (`uncommitted`, `session`)
90 > * on the state manager so client subscriptions resolve to a
91 > * `status: computing` snapshot before the first compute pass
92 > * completes. The catalogue itself (`state.changesets`) is seeded
93 > * upstream by `_buildInitialSummary` / `restoreSession` — this only
94 > * deals with the state-manager-side per-changeset entries.
95 > *
96 > * Idempotent; safe to call on every create and restore path.
97 > */
98 > registerStaticChangesets(session: ProtocolURI): void;
99 >
100 > /**
101 > * Re-seed a static changeset (`uncommitted` or `session`) from a
102 > * previously persisted file list (e.g. read out of the session DB on
103 > * restore / listSessions). Idempotently registers the changeset URI
104 > * on the state manager, fans the persisted files out as
105 > * `changeset/fileSet` actions, and transitions the status to `Ready`.
106 > */
107 > restoreStaticChangeset(session: ProtocolURI, kind: StaticChangesetKind, diffs: readonly ISessionFileDiff[]): void;
108 >
109 > /**
110 > * Parses the persisted changeset metadata blobs (`uncommitted`,
111 > * `session`, and the legacy `diffs` fallback for `session`) without
112 > * mutating live state. Intended for list overlays that only need
113 > * aggregate catalogue counts and should not pin full changeset state in
114 > * memory.
115 > */
116 > parsePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs;
117 >
118 > /**
119 > * Applies parsed persisted changeset diffs to live state via
120 > * {@link restoreStaticChangeset}. This is the side-effectful half of
121 > * persisted restore and should only be used on real restore/subscribe
122 > * paths that need a subscribable changeset snapshot.
123 > *
124 > * Honours `seedIfEmpty`: when a live changeset state already has files
125 > * for the same kind, persisted diffs are NOT applied (they would
126 > * otherwise overwrite the live state).
127 > */
128 > applyPersistedStaticChangesets(sessionUri: ProtocolURI, diffs: IRestoredChangesetDiffs): void;
129 >
130 > /**
131 > * Compatibility wrapper that parses persisted changeset metadata and then
132 > * applies it to live state. New list-overlay callers should prefer
133 > * {@link parsePersistedStaticChangesets}; restore/subscribe callers can
134 > * use this method when they intentionally want both parse and seed.
135 > *
136 > * The `AgentService` orchestration boundary batches the metadata read
137 > * (custom title + read / archive flags + config values + these three
138 > * blobs) in a single database round-trip, then hands the raw values
139 > * here; the service does not open the database itself for this method.
140 > */
141 > restorePersistedStaticChangesets(sessionUri: ProtocolURI, metadata: IPersistedChangesetMetadata): IRestoredChangesetDiffs;
142 >
143 > /**
144 > * Fire-and-forget persistence of the `summary.changes` aggregate to the
145 > * session DB under {@link META_CHANGES_SUMMARY}. Used both by the
146 > * happy-path turn-complete write and by the {@link ChangesetSessionCoordinator}
147 > * one-shot migration that reads the old `META_CHANGESET_SESSION` /
148 > * `META_LEGACY_DIFFS` blobs and projects them into the new key on
149 > * sessions written by older builds. Errors are logged, not thrown.
150 > */
151 > persistChangesSummary(sessionUri: ProtocolURI, summary: ChangesSummary): void;
152 >
153 > /**
154 > * Returns the session-DB metadata keys to merge into a batched read for
155 > * `sessionUri` (so the session-list overlay can synthesise the `changes`
156 > * aggregate), OR `undefined` when live state already answers the
157 > * aggregate-counts question (loaded session or a ready live
158 > * `changeKind: 'session'` changeset state) so the caller can skip loading
159 > * the potentially-large persisted diff blobs.
160 > */
161 > getListMetadataKeys(sessionUri: ProtocolURI): Record<string, true> | undefined;
162 >
163 > /**
164 > * Computes the `summary.changes` aggregate (additions / deletions / files
165 > * for the session-wide changeset) for a single session-list entry, using
166 > * the already-batched DB `metadata` read. Returns `undefined` when no
167 > * aggregate should be advertised (loaded session whose `summary.changes`
168 > * the caller already projected, or no live/persisted source).
169 > *
170 > * Precedence: live session (caller owns projection) > persisted
171 > * `META_CHANGES_SUMMARY` blob > ready live `changeKind: 'session'`
172 > * changeset state > parsed persisted session-wide diff blob. The latter
173 > * two paths also migrate the result forward to {@link META_CHANGES_SUMMARY}.
174 > */
175 > computeListEntryChanges(sessionUri: ProtocolURI, metadata: Record<string, string | undefined>): ChangesSummary | undefined;
176 >
177 > /**
178 > * Returns true when the static changeset identified by `changesetUri` is
179 > * currently being recomputed. Used by cache eviction to avoid dropping a
180 > * slot while its producer is mid-flight.
181 > */
182 > isStaticChangesetComputeActive(changesetUri: ProtocolURI): boolean;
183 >
184 > /**
185 > * Refreshes the list of changesets for the given session.
186 > */
187 > refreshChangesetCatalog(session: ProtocolURI): void;
188 >
189 > /**
190 > * Lazy refresh of the branch changeset, kicked off when a client
191 > * first subscribes to `<session>/changeset/branch`. Self-defers when the
192 > * session's working directory is not yet known; the deferred refresh is
193 > * drained by {@link onWorkingDirectoryAvailable}.
194 > */
195 > refreshBranchChangeset(session: ProtocolURI): void;
196 >
197 > /**
198 > * Lazy refresh of the session changeset, kicked off when a
199 > * client first subscribes to `<session>/changeset/session` or the
200 > * session URI itself (e.g. Agents Window observing the session). The
201 > * recompute keeps the catalogue chip fresh across session opens even
202 > * when no turn has run since process start. Self-defers when the
203 > * session's working directory is not yet known.
204 > */
205 > refreshSessionChangeset(session: ProtocolURI): void;
206 >
207 > /**
208 > * Drains static changeset refreshes (`branch` / `session` /
209 > * `uncommitted`) that were deferred because the session's working
210 > * directory was not yet known. Called when a session is materialized or
211 > * restored. Recomputes every changeset currently subscribed for the
212 > * session via {@link recomputeSubscribedChangesets}; subscriptions that
213 > * dropped while the working directory was unknown are naturally skipped.
214 > * Idempotent.
215 > */
216 > onWorkingDirectoryAvailable(session: ProtocolURI): void;
217 >
218 > /**
219 > * Recomputes every changeset currently subscribed for `session`, read
220 > * from the shared changeset subscription service. Each subscribed changeset
221 > * is dispatched to its kind-specific recompute (branch / session / uncommitted
222 > * / turn); the individual recomputes self-defer when the working directory is
223 > * not yet known. Used as the session-level refresh entry point (drain on
224 > * materialization, git-state change).
225 > */
226 > recomputeSubscribedChangesets(session: ProtocolURI): void;
227 >
228 > /**
229 > * Forgets any deferred static changeset refreshes queued for a session
230 > * that is being disposed.
231 > */
232 > onSessionDisposed(session: ProtocolURI): void;
233 >
234 > /**
235 > * Computes and publishes the per-turn changeset for `turnId` on `session`.
236 > * Per-turn changesets are not persisted.
237 > */
238 > computeTurnChangeset(session: ProtocolURI, turnId: string): Promise<ProtocolURI>;
239 >
240 > /**
241 > * Computes and publishes the compare-turns changeset between
242 > * `originalTurnId` (the "from" endpoint) and `modifiedTurnId` (the
243 > * "to" endpoint) on `session`. Diff direction is
244 > * `originalTurnId.current → modifiedTurnId.current` — endpoint-to-
245 > * endpoint, so it captures what differs between the two turn states.
246 > *
247 > * Implemented via git: both refs come from the per-turn checkpoint
248 > * captured at the end of each turn. When either checkpoint is missing
249 > * (non-git session, baseline never captured, capture failure), the
250 > * changeset transitions to `status: Error` instead of rejecting; no
251 > * SDK edit-tracker fallback exists.
252 > *
253 > * Compare-turns changesets are not persisted and are computed once
254 > * on subscribe (no live recompute).
255 > */
256 > computeCompareTurnsChangeset(session: ProtocolURI, originalTurnId: string, modifiedTurnId: string): Promise<ProtocolURI>;
257 >
258 > /**
259 > * Computes and publishes the uncommitted changeset for `session`
260 > * directly via git (`git status` against HEAD). The uncommitted slot
261 > * has no SDK edit-tracker fallback — the aggregator answers a different
262 > * question than `git status` and would silently rebrand SDK-tracked
263 > * edits as uncommitted git changes. When the session has no working
264 > * directory, the working directory isn't a git work tree, or the git
265 > * command fails, the changeset transitions to `status: Error`.
266 > *
267 > * Uncommitted changesets are not persisted; callers schedule recomputes
268 > * (e.g. on turn complete, post-commit, working-tree watcher event)
269 > * directly via this method.
270 > */
271 > computeUncommittedChangeset(session: ProtocolURI): Promise<ProtocolURI>;
272 >
273 > /**
274 > * Hook called by `AgentSideEffects` after a tool call that produced
275 > * file edits completes. Schedules a debounced session-changeset recompute.
276 > */
277 > onToolCallEditsApplied(session: ProtocolURI, turnId: string): void;
278 >
279 > /**
280 > * Hook called by `AgentSideEffects` when a turn completes. Cancels any
281 > * pending mid-turn debounce, then schedules a final session + uncommitted
282 > * recompute. Ordering matters — see implementation.
283 > */
284 > onTurnComplete(session: ProtocolURI, turnId: string | undefined): void;
285 >
286 > /**
287 > * Hook called by `AgentSideEffects` when a session is truncated (turns
288 > * removed). Recomputes the session changeset from scratch (no
289 > * `changedTurnId`, no incremental reuse).
290 > */
291 > onSessionTruncated(session: ProtocolURI): void;
292 >
293 > }