1
>
/*---------------------------------------------------------------------------------------------
agentPluginManager.ts
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 } };
92
progress?.(customization);
94
>
} catch (err) {
95
const message = err instanceof Error ? err.message : String(err);
96
this._logService.error(`[AgentPluginManager] Failed to sync plugin ${ref.uri}: ${message}`);