Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 5 additions & 1 deletion build/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function shouldErrorOnUnmatchedPattern(args: readonly string[]): boolean
return args.length > 0;
}

export function getEslintConcurrency(args: readonly string[]): 'off' | number {
return args.length > 0 ? 'off' : 4;
}

async function eslint(args: readonly string[]): Promise<void> {
const started = Date.now();
console.log(args.length > 0
Expand All @@ -27,7 +31,7 @@ async function eslint(args: readonly string[]): Promise<void> {
cache: true,
cacheLocation: '.eslintcache',
cacheStrategy: 'content',
concurrency: 'auto',
concurrency: getEslintConcurrency(args),
errorOnUnmatchedPattern: shouldErrorOnUnmatchedPattern(args),
});
const formatter = await linter.loadFormatter('compact');
Expand Down
9 changes: 8 additions & 1 deletion build/lib/test/eslint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import assert from 'assert';
import { suite, test } from 'node:test';
import { getEslintFilePatterns, shouldErrorOnUnmatchedPattern } from '../../eslint.ts';
import { getEslintConcurrency, getEslintFilePatterns, shouldErrorOnUnmatchedPattern } from '../../eslint.ts';
import { eslintFilter } from '../../filters.ts';

suite('eslint', () => {
Expand All @@ -26,4 +26,11 @@ suite('eslint', () => {
shouldErrorOnUnmatchedPattern(['src/vs/base/common/arrays.ts']),
], [false, true]);
});

test('caps worker concurrency', () => {
assert.deepStrictEqual([
getEslintConcurrency([]),
getEslintConcurrency(['src/vs/base/common/arrays.ts']),
], [4, 'off']);
});
});
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 @@ -2085,9 +2144,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 @@ -2123,12 +2190,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 @@ -2245,7 +2312,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 @@ -2295,7 +2362,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 @@ -2319,7 +2386,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