Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ export const platformRootSchema = createSchema({
type: 'string',
title: localize('agentHost.config.showExternalSessions.title', "Show External Agent Sessions"),
description: localize('agentHost.config.showExternalSessions.description', "Controls whether sessions created outside the Agent Host are included in the session catalog."),
enum: [ChatExternalSessionsMode.None, ChatExternalSessionsMode.Recent, ChatExternalSessionsMode.Last24Hours, ChatExternalSessionsMode.Last7Days, ChatExternalSessionsMode.All],
enum: [ChatExternalSessionsMode.None, ChatExternalSessionsMode.Recent, ChatExternalSessionsMode.Last24Hours, ChatExternalSessionsMode.Last7Days, ChatExternalSessionsMode.Last30Days],
default: ChatExternalSessionsMode.None,
}),
[AgentHostCopilotMultiRootEnabledConfigKey]: schemaProperty<boolean>({
Expand Down
81 changes: 74 additions & 7 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ import { AgentHostCheckpointService } from './agentHostCheckpointService.js';
*/
const SESSION_GC_GRACE_MS = 30_000;
const DAY_MS = 24 * 60 * 60 * 1000;
const EXTERNAL_SESSION_MAX_AGE_MS = 30 * DAY_MS;
const EXTERNAL_SESSION_PRUNE_DELAY_MS = 60_000;
const RECENT_EXTERNAL_SESSION_LIMIT = 2;
/** A catalog pass slower than this is logged at info, since it delays every session-list refresh. */
const SLOW_LIST_SESSIONS_THRESHOLD_MS = 1_000;
Expand Down Expand Up @@ -841,6 +843,7 @@ export class AgentService extends Disposable implements IAgentService {
session => this._agentMergeController.getTurnContext(session),
);
this._serverToolHost = new AgentServerToolHost(this._stateManager, buildServerToolGroups(this._createSessionServerToolAccessor(), agentMergeTools, this._createArtifactServerToolAccessor()));
this._scheduleExternalSessionPrune();
}

/**
Expand All @@ -861,6 +864,54 @@ export class AgentService extends Disposable implements IAgentService {
return this._sideEffects.onDidStartTurn;
}

private _scheduleExternalSessionPrune(): void {
this._register(disposableTimeout(() => {
void this._pruneStaleExternalSessions().catch(error => {
this._logService.warn('[AgentService] Failed to prune stale external sessions', error);
});
}, EXTERNAL_SESSION_PRUNE_DELAY_MS));
}

private async _pruneStaleExternalSessions(): Promise<void> {
const now = this._now();
const registered = await this._listRegisteredSessions();
const staleExternalSessions: URI[] = [];
for (const entry of registered) {
if (!entry.external) {
continue;
}
const provider = this._providers.get(entry.provider);
if (!provider) {
continue;
}
let metadata: IAgentSessionMetadata | undefined;
try {
metadata = await this._registeredSessionMetadata(provider, entry.session, true);
} catch (error) {
this._logService.warn(`[AgentService] Failed to load metadata while pruning stale external session ${entry.session.toString()}`, error);
continue;
}
if (!metadata) {
continue;
}
if (readSessionEhcliAdoptable(metadata._meta)) {
continue;
}
if (this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, now)) {
staleExternalSessions.push(entry.session);
}
}

for (const session of staleExternalSessions) {
await this._sessionRegistry.unregister(session);
}
if (staleExternalSessions.length > 0) {
this._invalidateSessionList();
this._queueSessionListReconciliation();
}
this._logService.info(`[AgentService] pruned ${staleExternalSessions.length} stale external session row(s) older than ${EXTERNAL_SESSION_MAX_AGE_MS / DAY_MS} days`);
}

// ---- provider registration ----------------------------------------------

/**
Expand Down Expand Up @@ -1607,6 +1658,7 @@ export class AgentService extends Disposable implements IAgentService {
const existing = new Map((await this._listRegisteredSessions()).map(session => [session.session.toString(), session.external]));
const discoveryLimiter = new Limiter<boolean>(4);
let suppressed = 0;
let skippedAsStale = 0;
let registeredExternal = false;
let alreadyRegistered = 0;
let registryChanged = false;
Expand All @@ -1624,6 +1676,10 @@ export class AgentService extends Disposable implements IAgentService {
suppressed++;
return false;
}
if (external && !readSessionEhcliAdoptable(sessionMetadata._meta) && this._isExternalSessionOlderThanMaxAge(sessionMetadata.modifiedTime, this._now())) {
skippedAsStale++;
return false;
}
const identity: IRegisteredSession = { session, provider: provider.id, startTime: metadata.startTime, external, source: external ? 'discovery' : 'restore' };
const registered = await this._retryRegistryMutation(
() => this._sessionRegistry.register(session, identity, { checkTombstone: true }),
Expand Down Expand Up @@ -1656,7 +1712,7 @@ export class AgentService extends Disposable implements IAgentService {
if (registeredExternal) {
this._queueSessionListReconciliation();
}
this._logService.info(`[AgentService] discovery for provider ${provider.id}: ${chats.length} candidate(s) (${chats.filter(chat => chat.external).length} external), ${registered} registered, ${alreadyRegistered} already registered, ${suppressed} suppressed as subagent/chat backing`);
this._logService.info(`[AgentService] discovery for provider ${provider.id}: ${chats.length} candidate(s) (${chats.filter(chat => chat.external).length} external), ${registered} registered, ${alreadyRegistered} already registered, ${suppressed} suppressed as subagent/chat backing, ${skippedAsStale} skipped as older than ${EXTERNAL_SESSION_MAX_AGE_MS / DAY_MS} days`);
return registered > 0;
}

Expand Down Expand Up @@ -1689,10 +1745,13 @@ export class AgentService extends Disposable implements IAgentService {
if (!identity) {
continue;
}
const metadata = sessions[index];
if (identity.external && !readSessionEhcliAdoptable(metadata._meta) && this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, this._now())) {
continue;
}
const registered = await this._sessionRegistry.register(identity.session, identity, { checkTombstone: true });
if (registered) {
this._invalidateSessionList();
const metadata = sessions[index];
if (identity.external && existing.get(identity.session.toString()) !== true) {
await this._initializeExternalSessionReadState(identity.session);
}
Expand Down Expand Up @@ -2101,9 +2160,17 @@ export class AgentService extends Disposable implements IAgentService {
}

private _getExternalSessionsMode(): AgentHostExternalSessionsMode {
const rootValue = this._configurationService.getRootConfigValues()?.[AgentHostShowExternalSessionsConfigKey];
if (rootValue === 'all') {
return AgentHostExternalSessionsMode.Last30Days;
}
return this._configurationService.getRootValue(platformRootSchema, AgentHostShowExternalSessionsConfigKey) ?? AgentHostExternalSessionsMode.None;
}

private _isExternalSessionOlderThanMaxAge(modifiedTime: number, now: number): boolean {
return modifiedTime < now - EXTERNAL_SESSION_MAX_AGE_MS;
}

private _getRecentSessionKeys(sessions: readonly IAgentSessionMetadata[], now: number): ReadonlySet<string> {
const recentExternalSessions = sessions
.filter(session => readSessionExternal(session._meta)
Expand Down Expand Up @@ -2139,12 +2206,12 @@ export class AgentService extends Disposable implements IAgentService {
case AgentHostExternalSessionsMode.Recent:
return session.modifiedTime >= now - 7 * DAY_MS
&& (recentSessionKeys === undefined || recentSessionKeys.has(session.session.toString()));
case AgentHostExternalSessionsMode.All:
return true;
case AgentHostExternalSessionsMode.Last24Hours:
return session.modifiedTime >= now - DAY_MS;
case AgentHostExternalSessionsMode.Last7Days:
return session.modifiedTime >= now - 7 * DAY_MS;
case AgentHostExternalSessionsMode.Last30Days:
return !this._isExternalSessionOlderThanMaxAge(session.modifiedTime, now);
case AgentHostExternalSessionsMode.None:
return false;
}
Expand Down Expand Up @@ -2276,7 +2343,7 @@ export class AgentService extends Disposable implements IAgentService {
previouslyExposed.add(session);
}
const listed = previousMode !== undefined
? this._resolveModeChangeVisibility(await this.listSessions(AgentHostExternalSessionsMode.All), previousMode, previouslyExposed)
? this._resolveModeChangeVisibility(await this.listSessions(AgentHostExternalSessionsMode.Last30Days), previousMode, previouslyExposed)
: await this.listSessions();
const visible = new Set<string>();
let published = 0;
Expand Down Expand Up @@ -2326,7 +2393,7 @@ export class AgentService extends Disposable implements IAgentService {

/**
* Derives both the previous and current mode's visible sets from one catalog
* pass, since {@link AgentHostExternalSessionsMode.All} is a superset of every
* pass, since {@link AgentHostExternalSessionsMode.Last30Days} is a superset of every
* mode and the mode is just a parameter to {@link _shouldIncludeSession}.
* Adds what `previousMode` had exposed into `previouslyExposed`.
*/
Expand All @@ -2350,7 +2417,7 @@ export class AgentService extends Disposable implements IAgentService {
const mode = this._getExternalSessionsMode();
const recentKeys = recentKeysFor(mode);
const visible = superset.filter(session => this._shouldIncludeSession(session, mode, now, recentKeys));
// The pass ran as `All`, so report the mode actually in effect instead.
// The pass ran as `Last30Days`, so report the mode actually in effect instead.
this._logHiddenSessions(superset.length - visible.length, superset.length, mode);
return visible;
}
Expand Down
5 changes: 5 additions & 0 deletions src/vs/platform/agentHost/node/agentSessionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export class AgentSessionRegistry extends Disposable {
return this._database.registerSession(session.toString(), sessionOptions, registerOptions);
}

/** Removes any registry entry for `session` without writing a tombstone. */
async unregister(session: URI): Promise<void> {
await this._database.unregisterSession(session.toString());
}

/**
* Removes any registry entry for `session` (a true delete) and durably
* tombstones it so discovery cannot register it. Used both to delete a
Expand Down
Loading
Loading