src/vs/platform/agentHost/node/agentPluginManager.ts

292 LOC · 282 covered · 10 uncovered · 52 ranges · 22 concepts · 14 introducers · 12 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.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/files/common/fileService.ts · 1491 LOCcommon/fileService.tssrc/vs/platform/files/common/inMemoryFilesystemProvider.ts · 358 LOCcommon/inMemoryFilesyste…agentPluginManager.test|title=AgentPluginManager syncCustomizations evicts a previously locked older nonce on startup once released|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations retains a locked older nonce so both revisions coexist|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations new nonce materializes a fresh subdirectory and evicts the stale one|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.ts ×1 · 2 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×2 · 3 introduced LOCagentPluginManager.ts ×2agentPluginManager.ts ×4 · 19 introduced LOCagentPluginManager.ts ×4agentPluginManager.ts ×1 · 3 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×3 · 11 introduced LOCagentPluginManager.ts ×3agentPluginManager.test|title=AgentPluginManager syncCustomizations mixes loaded and error results|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.ts ×1 · 1 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×1 · 3 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×3 · 4 introduced LOCagentPluginManager.ts ×3agentPluginManager.test|title=AgentPluginManager syncCustomizations returns loaded status and pluginDir for each synced plugin|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations skips copy when nonce matches|occurrence=1, agentPluginManager.test|title=AgentPluginManager syncCustomizations serializes concurrent syncs of the same URI|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.ts ×2 · 10 introduced LOCagentPluginManager.ts ×2agentPluginManager.test|title=AgentPluginManager syncCustomizations fires progress callback with changed customization status|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.ts ×1 · 5 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×1 · 2 introduced LOCagentPluginManager.ts ×1fileService.ts ×15 · 173 introduced LOCfileService.ts ×15agentPluginManager.test|title=AgentPluginManager syncCustomizations returns error status without pluginDir when source missing|occurrence=1 · 0 introduced LOCagentPluginManager.test|…agentPluginManager.ts ×1 · 6 introduced LOCagentPluginManager.ts ×1agentPluginManager.ts ×19 · 198 introduced LOCagentPluginManager.ts ×1…agentPluginManager.test|title=AgentPluginManager LRU eviction evicts least recently used plugins when limit exceeded|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager LRU eviction evicts least recently used plugins when limit exceeded|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager LRU eviction retains a locked LRU candidate and skips ahead to evict an unlocked one|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager LRU eviction retains a locked LRU candidate and skips ahead to evict an unlocked one|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager cache persistence restores nonce cache from disk on new manager instance|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager cache persistence restores nonce cache from disk on new manager instance|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations evicts a previously locked older nonce on startup once released|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations evicts a previously locked older nonce on startup once released|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations fires progress callback with changed customization status|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations fires progress callback with changed customization status|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations mixes loaded and error results|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations mixes loaded and error results|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations new nonce materializes a fresh subdirectory and evicts the stale one|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations new nonce materializes a fresh subdirectory and evicts the stale one|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations retains a locked older nonce so both revisions coexist|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations retains a locked older nonce so both revisions coexist|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations returns error status without pluginDir when source missing|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations returns error status without pluginDir when source missing|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations returns loaded status and pluginDir for each synced plugin|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations returns loaded status and pluginDir for each synced plugin|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations serializes concurrent syncs of the same URI|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations serializes concurrent syncs of the same URI|occurrence=1agentPluginManager.test|…agentPluginManager.test|title=AgentPluginManager syncCustomizations skips copy when nonce matches|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/agentPluginManager.test|title=AgentPluginManager syncCustomizations skips copy when nonce matches|occurrence=1agentPluginManager.test|…Focused file · src/vs/platform/agentHost/node/agentPluginManager.ts · 292 LOCnode/agentPluginManager.…

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 > /*--------------------------------------------------------------------------------------------- agentPluginManager.ts ×19
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 { VSBuffer } from '../../../base/common/buffer.js';
7 > import { SequencerByKey } from '../../../base/common/async.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { IFileService } from '../../files/common/files.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import { IAgentPluginManager, type ISyncedCustomization } from '../common/agentPluginManager.js';
12 > import { CustomizationLoadStatus, type ClientPluginCustomization, type PluginCustomization } from '../common/state/sessionState.js';
13 > import { toAgentClientUri } from '../common/agentClientUri.js';
14 >
15 > const DEFAULT_MAX_PLUGINS = 20;
16 >
17 > /** On-disk cache entry format. */
18 > interface ICacheEntry {
19 > readonly uri: string;
20 > readonly nonce: string;
21 > }
22 >
23 > /**
24 > * Implementation of {@link IAgentPluginManager}.
25 > *
26 > * Syncs plugin directories to local storage under
27 > * `{userDataPath}/agentPlugins/{key}/{nonce}/`. Materializing each nonce in
28 > * its own subdirectory means a new revision is copied into a fresh directory
29 > * rather than overwriting (and deleting) the previous one. This both avoids
30 > * `EBUSY` failures when the in-use copy is still locked and allows multiple
31 > * revisions of the same plugin to coexist — e.g. a long-running session may
32 > * still reference an older nonce that we cannot delete yet. Uses a
33 > * {@link SequencerByKey} per plugin URI so that concurrent syncs of the same
34 > * plugin are serialized and cannot clobber each other.
35 > *
36 > * Older nonces of a plugin are evicted opportunistically: when the manager
37 > * starts up and again after each fresh sync of the same plugin. If a stale
38 > * nonce directory cannot be removed (e.g. it is still locked), it is retained
39 > * in the LRU and retried on a later cleanup pass.
40 > *
41 > * The LRU (which records each plugin's URI and nonce) is persisted to a JSON
42 > * file in the base path so it survives process restarts.
43 > */
44 > export class AgentPluginManager implements IAgentPluginManager {
45 > declare readonly _serviceBrand: undefined;
46 >
47 > private readonly _basePath: URI;
48 > private readonly _cachePath: URI;
49 > private readonly _maxPlugins: number;
50 >
51 > /** Serializes concurrent sync operations per plugin URI. */
52 > private readonly _sequencer = new SequencerByKey<string>();
53 >
54 > /**
55 > * LRU of synced plugins, most recently used at the end. Each entry records
56 > * the plugin's original customization URI and the nonce materialized on
57 > * disk under `{key}/{nonce}`.
58 > */
59 > private readonly _lru: ICacheEntry[] = [];
60 >
61 > /** Whether the on-disk cache has been loaded. */
62 > private _cacheLoaded = false;
63 >
64 > constructor(
65 > userDataPath: URI,
66 > @IFileService private readonly _fileService: IFileService,
67 > @ILogService private readonly _logService: ILogService,
68 > maxPlugins: number = DEFAULT_MAX_PLUGINS,
69 > ) {
70 > this._basePath = URI.joinPath(userDataPath, 'agentPlugins');
71 > this._cachePath = URI.joinPath(this._basePath, 'cache.json');
72 > this._maxPlugins = maxPlugins;
73 > }
74 >
75 > get basePath(): URI {
76 return this._basePath;
77 }
79 > async syncCustomizations(
80 > clientId: string,
81 > customizations: ClientPluginCustomization[],
82 > progress?: (status: PluginCustomization) => void,
83 > ): Promise<ISyncedCustomization[]> {
84 > await this._ensureCacheLoaded();
85 >
86 > // Sync each customization in parallel, serialized per URI
87 > const results = await Promise.all(customizations.map(ref =>
88 > this._sequencer.queue(ref.uri, async (): Promise<ISyncedCustomization> => {
89 > try {
90 > const pluginDir = await this._syncPlugin(clientId, ref);
91 > const customization: PluginCustomization = { ...ref, load: { kind: CustomizationLoadStatus.Loaded } }; fileService.ts ×15
92 > progress?.(customization);
93 > return { customization, pluginDir }; agentPluginManager.ts ×19
94 > } catch (err) {
95 > const message = err instanceof Error ? err.message : String(err); agentPluginManager.ts ×1
96 > this._logService.error(`[AgentPluginManager] Failed to sync plugin ${ref.uri}: ${message}`);
97 > const customization: PluginCustomization = { ...ref, load: { kind: CustomizationLoadStatus.Error, message } };
98 > progress?.(customization);
99 > return { customization };
100 > }
102 > ));
103 >
104 > return results;
105 > }
106 >
107 > // ---- plugin storage logic -----------------------------------------------
108 >
109 > /**
110 > * Syncs a single plugin to local storage. Each nonce is materialized in its
111 > * own `{key}/{nonce}` subdirectory; when the same nonce is already present
112 > * the copy is skipped. After a fresh copy, older nonces of the same plugin
113 > * are evicted on a best-effort basis (retained in the LRU if still locked).
114 > * Returns the local directory URI.
115 > */
116 > private async _syncPlugin(clientId: string, ref: ClientPluginCustomization): Promise<URI> {
117 > const pluginUri = toAgentClientUri(URI.parse(ref.uri), clientId);
118 > const destDir = this._dirFor(ref.uri, ref.nonce);
119 >
120 > // Nonce cache hit — the plugin is already materialized under the nonce
121 > // subdirectory, so skip the copy.
122 > if (ref.nonce && this._findEntry(ref.uri, ref.nonce) && await this._fileService.exists(destDir)) {
123 > this._touchLru(ref.uri, ref.nonce); agentPluginManager.ts ×2
124 > this._logService.trace(`[AgentPluginManager] Nonce match for ${ref.uri}, skipping copy`);
125 > return destDir;
126 > }
128 > this._logService.info(`[AgentPluginManager] Syncing plugin: ${ref.uri}${destDir.toString()}`);
129 >
130 > await this._fileService.copy(pluginUri, destDir, true);
132 > this._removeEntry(ref.uri, ref.nonce);
133 > this._lru.push({ uri: ref.uri, nonce: ref.nonce ?? '' }); agentPluginManager.ts ×19
134 >
135 > // Try to clean up superseded nonces of this plugin; undeletable ones stay
136 > // in the LRU for a later attempt.
137 > await this._cleanupStaleNoncesFor(ref.uri);
138 > await this._evictIfNeeded(); fileService.ts ×15
139 > await this._persistCache();
140 >
141 > return destDir;
143 >
144 > private _keyForUri(uri: string): string {
145 > return this._sanitize(uri);
146 > }
147 >
148 > private _keyForNonce(nonce: string | undefined): string {
149 > return (nonce && this._sanitize(nonce)) || 'default';
150 > }
151 >
152 > private _sanitize(value: string): string {
153 > return value.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 128);
154 > }
155 >
156 > /** Directory in which a specific `(uri, nonce)` revision is materialized. */
157 > private _dirFor(uri: string, nonce: string | undefined): URI {
158 > return URI.joinPath(this._basePath, this._keyForUri(uri), this._keyForNonce(nonce));
159 > }
160 >
161 > /** Parent directory holding all materialized nonces of a plugin. */
162 > private _pluginRootFor(uri: string): URI {
163 > return URI.joinPath(this._basePath, this._keyForUri(uri)); agentPluginManager.ts ×3
164 > }
166 > private _findEntry(uri: string, nonce: string | undefined): ICacheEntry | undefined {
167 > const n = nonce ?? ''; fileService.ts ×15
168 > return this._lru.find(entry => entry.uri === uri && entry.nonce === n);
169 > }
171 > private _removeEntry(uri: string, nonce: string | undefined): void {
172 > const entry = this._findEntry(uri, nonce); fileService.ts ×15
173 > if (entry) {
174 this._removeEntryRef(entry);
175 }
178 > private _removeEntryRef(entry: ICacheEntry): void {
179 > const idx = this._lru.indexOf(entry); agentPluginManager.ts ×1
180 > if (idx !== -1) {
181 > this._lru.splice(idx, 1);
182 > }
183 > }
185 > private _touchLru(uri: string, nonce: string | undefined): void {
186 > const entry = this._findEntry(uri, nonce); agentPluginManager.ts ×2
187 > if (entry) {
188 > this._removeEntryRef(entry);
189 > this._lru.push(entry);
190 > }
191 > }
193 > /** Best-effort recursive delete; returns `true` only when the dir is gone. */
194 > private async _tryDeleteDir(dir: URI): Promise<boolean> {
196 > await this._fileService.del(dir, { recursive: true });
197 > return true; agentPluginManager.ts ×1
198 > } catch (err) { agentPluginManager.ts ×3
199 > this._logService.warn(`[AgentPluginManager] Failed to remove plugin dir ${dir.toString()}`, err); agentPluginManager.ts ×1
200 > return false;
201 > }
204 > /** Attempts to evict older nonces of every tracked plugin. */
205 > private async _cleanupStaleNonces(): Promise<void> {
206 > for (const uri of new Set(this._lru.map(entry => entry.uri))) { agentPluginManager.ts ×4
207 > await this._cleanupStaleNoncesFor(uri);
208 > }
209 > }
211 > /**
212 > * Attempts to evict every nonce of {@link uri} except the most recently used
213 > * one. Entries whose directory cannot be removed are left in the LRU so they
214 > * can be retried later, once whatever was holding them has released them.
215 > */
216 > private async _cleanupStaleNoncesFor(uri: string): Promise<void> {
217 > const entries = this._lru.filter(entry => entry.uri === uri); fileService.ts ×15
218 > // `entries` preserves LRU order; the last is the current revision.
219 > const stale = entries.slice(0, -1);
220 > for (const entry of stale) {
221 > this._logService.info(`[AgentPluginManager] Evicting stale nonce for plugin: ${uri}`); agentPluginManager.ts ×2
222 > if (await this._tryDeleteDir(this._dirFor(entry.uri, entry.nonce))) {
223 > this._removeEntryRef(entry); agentPluginManager.ts ×1
224 > }
228 > private async _evictIfNeeded(): Promise<void> {
229 > // Pop from the head until we're at-or-below the cap. Entries whose fileService.ts ×15
230 > // directory can't be deleted (still locked by a running session)
231 > // are kept in the LRU so they can be retried on a later eviction
232 > // pass; the cap may be exceeded temporarily in that case.
233 > let i = 0;
234 > while (this._lru.length > this._maxPlugins && i < this._lru.length) {
235 > const candidate = this._lru[i]; agentPluginManager.ts ×3
236 > this._logService.info(`[AgentPluginManager] Evicting plugin: ${candidate.uri}`);
237 > if (await this._tryDeleteDir(this._dirFor(candidate.uri, candidate.nonce))) {
238 > this._lru.splice(i, 1);
239 > if (!this._lru.some(entry => entry.uri === candidate.uri)) {
240 > await this._tryDeleteDir(this._pluginRootFor(candidate.uri));
241 > }
242 > } else {
243 > // Locked — keep it in the LRU and try the next candidate. agentPluginManager.ts ×1
244 > i++;
245 > }
249 > // ---- cache persistence --------------------------------------------------
250 >
251 > private async _ensureCacheLoaded(): Promise<void> {
252 > if (this._cacheLoaded) {
254 > }
255 > this._cacheLoaded = true; agentPluginManager.ts ×19
256 >
257 > try {
258 > if (!await this._fileService.exists(this._cachePath)) {
259 > return;
260 > }
261 > const content = await this._fileService.readFile(this._cachePath); agentPluginManager.ts ×4
262 > const entries: ICacheEntry[] = JSON.parse(content.value.toString());
263 > if (!Array.isArray(entries)) {
264 return;
265 }
267 > // Entries are stored in LRU order (oldest first)
268 > for (const entry of entries) {
269 > if (typeof entry.uri === 'string' && typeof entry.nonce === 'string') {
270 > this._lru.push({ uri: entry.uri, nonce: entry.nonce });
271 > }
272 > }
273 > this._logService.trace(`[AgentPluginManager] Loaded ${entries.length} cache entries from disk`);
274 > } catch (err) {
275 this._logService.warn('[AgentPluginManager] Failed to load cache from disk', err);
276 }
278 > await this._cleanupStaleNonces();
279 > await this._persistCache();
281 >
282 > private async _persistCache(): Promise<void> {
283 > try { fileService.ts ×15
284 > // Write entries in LRU order (oldest first)
285 > const entries: ICacheEntry[] = this._lru.map(entry => ({ uri: entry.uri, nonce: entry.nonce }));
286 > await this._fileService.createFolder(this._basePath);
287 > await this._fileService.writeFile(this._cachePath, VSBuffer.fromString(JSON.stringify(entries)));
288 > } catch (err) {
289 this._logService.warn('[AgentPluginManager] Failed to persist cache to disk', err);
290 }