Skip to content

Commit 32b97f5

Browse files
Copilotbenibenj
andauthored
Cap External Agent Sessions to 30 Days (replace all, enforce ingest/prune retention) (#331635)
* Initial plan * Replace external session All mode with 30-day retention Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> * Limit ESLint worker concurrency Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> * Revert "Limit ESLint worker concurrency" This reverts commit 9190dc5. Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com>
1 parent c14e0dc commit 32b97f5

11 files changed

Lines changed: 210 additions & 73 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ export const platformRootSchema = createSchema({
818818
type: 'string',
819819
title: localize('agentHost.config.showExternalSessions.title', "Show External Agent Sessions"),
820820
description: localize('agentHost.config.showExternalSessions.description', "Controls whether sessions created outside the Agent Host are included in the session catalog."),
821-
enum: [ChatExternalSessionsMode.None, ChatExternalSessionsMode.Recent, ChatExternalSessionsMode.Last24Hours, ChatExternalSessionsMode.Last7Days, ChatExternalSessionsMode.All],
821+
enum: [ChatExternalSessionsMode.None, ChatExternalSessionsMode.Recent, ChatExternalSessionsMode.Last24Hours, ChatExternalSessionsMode.Last7Days, ChatExternalSessionsMode.Last30Days],
822822
default: ChatExternalSessionsMode.None,
823823
}),
824824
[AgentHostCopilotMultiRootEnabledConfigKey]: schemaProperty<boolean>({

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

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ import { AgentHostCheckpointService } from './agentHostCheckpointService.js';
124124
*/
125125
const SESSION_GC_GRACE_MS = 30_000;
126126
const DAY_MS = 24 * 60 * 60 * 1000;
127+
const EXTERNAL_SESSION_MAX_AGE_MS = 30 * DAY_MS;
128+
const EXTERNAL_SESSION_PRUNE_DELAY_MS = 60_000;
127129
const RECENT_EXTERNAL_SESSION_LIMIT = 2;
128130
/** A catalog pass slower than this is logged at info, since it delays every session-list refresh. */
129131
const SLOW_LIST_SESSIONS_THRESHOLD_MS = 1_000;
@@ -841,6 +843,7 @@ export class AgentService extends Disposable implements IAgentService {
841843
session => this._agentMergeController.getTurnContext(session),
842844
);
843845
this._serverToolHost = new AgentServerToolHost(this._stateManager, buildServerToolGroups(this._createSessionServerToolAccessor(), agentMergeTools, this._createArtifactServerToolAccessor()));
846+
this._scheduleExternalSessionPrune();
844847
}
845848

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

867+
private _scheduleExternalSessionPrune(): void {
868+
this._register(disposableTimeout(() => {
869+
void this._pruneStaleExternalSessions().catch(error => {
870+
this._logService.warn('[AgentService] Failed to prune stale external sessions', error);
871+
});
872+
}, EXTERNAL_SESSION_PRUNE_DELAY_MS));
873+
}
874+
875+
private async _pruneStaleExternalSessions(): Promise<void> {
876+
const now = this._now();
877+
const registered = await this._listRegisteredSessions();
878+
const staleExternalSessions: URI[] = [];
879+
for (const entry of registered) {
880+
if (!entry.external) {
881+
continue;
882+
}
883+
const provider = this._providers.get(entry.provider);
884+
if (!provider) {
885+
continue;
886+
}
887+
let metadata: IAgentSessionMetadata | undefined;
888+
try {
889+
metadata = await this._registeredSessionMetadata(provider, entry.session, true);
890+
} catch (error) {
891+
this._logService.warn(`[AgentService] Failed to load metadata while pruning stale external session ${entry.session.toString()}`, error);
892+
continue;
893+
}
894+
if (!metadata) {
895+
continue;
896+
}
897+
if (readSessionEhcliAdoptable(metadata._meta)) {
898+
continue;
899+
}
900+
if (this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, now)) {
901+
staleExternalSessions.push(entry.session);
902+
}
903+
}
904+
905+
for (const session of staleExternalSessions) {
906+
await this._sessionRegistry.unregister(session);
907+
}
908+
if (staleExternalSessions.length > 0) {
909+
this._invalidateSessionList();
910+
this._queueSessionListReconciliation();
911+
}
912+
this._logService.info(`[AgentService] pruned ${staleExternalSessions.length} stale external session row(s) older than ${EXTERNAL_SESSION_MAX_AGE_MS / DAY_MS} days`);
913+
}
914+
864915
// ---- provider registration ----------------------------------------------
865916

866917
/**
@@ -1607,6 +1658,7 @@ export class AgentService extends Disposable implements IAgentService {
16071658
const existing = new Map((await this._listRegisteredSessions()).map(session => [session.session.toString(), session.external]));
16081659
const discoveryLimiter = new Limiter<boolean>(4);
16091660
let suppressed = 0;
1661+
let skippedAsStale = 0;
16101662
let registeredExternal = false;
16111663
let alreadyRegistered = 0;
16121664
let registryChanged = false;
@@ -1624,6 +1676,10 @@ export class AgentService extends Disposable implements IAgentService {
16241676
suppressed++;
16251677
return false;
16261678
}
1679+
if (external && !readSessionEhcliAdoptable(sessionMetadata._meta) && this._isExternalSessionOlderThanMaxAge(sessionMetadata.modifiedTime, this._now())) {
1680+
skippedAsStale++;
1681+
return false;
1682+
}
16271683
const identity: IRegisteredSession = { session, provider: provider.id, startTime: metadata.startTime, external, source: external ? 'discovery' : 'restore' };
16281684
const registered = await this._retryRegistryMutation(
16291685
() => this._sessionRegistry.register(session, identity, { checkTombstone: true }),
@@ -1656,7 +1712,7 @@ export class AgentService extends Disposable implements IAgentService {
16561712
if (registeredExternal) {
16571713
this._queueSessionListReconciliation();
16581714
}
1659-
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`);
1715+
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`);
16601716
return registered > 0;
16611717
}
16621718

@@ -1689,10 +1745,13 @@ export class AgentService extends Disposable implements IAgentService {
16891745
if (!identity) {
16901746
continue;
16911747
}
1748+
const metadata = sessions[index];
1749+
if (identity.external && !readSessionEhcliAdoptable(metadata._meta) && this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, this._now())) {
1750+
continue;
1751+
}
16921752
const registered = await this._sessionRegistry.register(identity.session, identity, { checkTombstone: true });
16931753
if (registered) {
16941754
this._invalidateSessionList();
1695-
const metadata = sessions[index];
16961755
if (identity.external && existing.get(identity.session.toString()) !== true) {
16971756
await this._initializeExternalSessionReadState(identity.session);
16981757
}
@@ -2101,9 +2160,17 @@ export class AgentService extends Disposable implements IAgentService {
21012160
}
21022161

21032162
private _getExternalSessionsMode(): AgentHostExternalSessionsMode {
2163+
const rootValue = this._configurationService.getRootConfigValues()?.[AgentHostShowExternalSessionsConfigKey];
2164+
if (rootValue === 'all') {
2165+
return AgentHostExternalSessionsMode.Last30Days;
2166+
}
21042167
return this._configurationService.getRootValue(platformRootSchema, AgentHostShowExternalSessionsConfigKey) ?? AgentHostExternalSessionsMode.None;
21052168
}
21062169

2170+
private _isExternalSessionOlderThanMaxAge(modifiedTime: number, now: number): boolean {
2171+
return modifiedTime < now - EXTERNAL_SESSION_MAX_AGE_MS;
2172+
}
2173+
21072174
private _getRecentSessionKeys(sessions: readonly IAgentSessionMetadata[], now: number): ReadonlySet<string> {
21082175
const recentExternalSessions = sessions
21092176
.filter(session => readSessionExternal(session._meta)
@@ -2139,12 +2206,12 @@ export class AgentService extends Disposable implements IAgentService {
21392206
case AgentHostExternalSessionsMode.Recent:
21402207
return session.modifiedTime >= now - 7 * DAY_MS
21412208
&& (recentSessionKeys === undefined || recentSessionKeys.has(session.session.toString()));
2142-
case AgentHostExternalSessionsMode.All:
2143-
return true;
21442209
case AgentHostExternalSessionsMode.Last24Hours:
21452210
return session.modifiedTime >= now - DAY_MS;
21462211
case AgentHostExternalSessionsMode.Last7Days:
21472212
return session.modifiedTime >= now - 7 * DAY_MS;
2213+
case AgentHostExternalSessionsMode.Last30Days:
2214+
return !this._isExternalSessionOlderThanMaxAge(session.modifiedTime, now);
21482215
case AgentHostExternalSessionsMode.None:
21492216
return false;
21502217
}
@@ -2276,7 +2343,7 @@ export class AgentService extends Disposable implements IAgentService {
22762343
previouslyExposed.add(session);
22772344
}
22782345
const listed = previousMode !== undefined
2279-
? this._resolveModeChangeVisibility(await this.listSessions(AgentHostExternalSessionsMode.All), previousMode, previouslyExposed)
2346+
? this._resolveModeChangeVisibility(await this.listSessions(AgentHostExternalSessionsMode.Last30Days), previousMode, previouslyExposed)
22802347
: await this.listSessions();
22812348
const visible = new Set<string>();
22822349
let published = 0;
@@ -2326,7 +2393,7 @@ export class AgentService extends Disposable implements IAgentService {
23262393

23272394
/**
23282395
* Derives both the previous and current mode's visible sets from one catalog
2329-
* pass, since {@link AgentHostExternalSessionsMode.All} is a superset of every
2396+
* pass, since {@link AgentHostExternalSessionsMode.Last30Days} is a superset of every
23302397
* mode and the mode is just a parameter to {@link _shouldIncludeSession}.
23312398
* Adds what `previousMode` had exposed into `previouslyExposed`.
23322399
*/
@@ -2350,7 +2417,7 @@ export class AgentService extends Disposable implements IAgentService {
23502417
const mode = this._getExternalSessionsMode();
23512418
const recentKeys = recentKeysFor(mode);
23522419
const visible = superset.filter(session => this._shouldIncludeSession(session, mode, now, recentKeys));
2353-
// The pass ran as `All`, so report the mode actually in effect instead.
2420+
// The pass ran as `Last30Days`, so report the mode actually in effect instead.
23542421
this._logHiddenSessions(superset.length - visible.length, superset.length, mode);
23552422
return visible;
23562423
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export class AgentSessionRegistry extends Disposable {
6969
return this._database.registerSession(session.toString(), sessionOptions, registerOptions);
7070
}
7171

72+
/** Removes any registry entry for `session` without writing a tombstone. */
73+
async unregister(session: URI): Promise<void> {
74+
await this._database.unregisterSession(session.toString());
75+
}
76+
7277
/**
7378
* Removes any registry entry for `session` (a true delete) and durably
7479
* tombstones it so discovery cannot register it. Used both to delete a

0 commit comments

Comments
 (0)