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
>
}