Skip to content

Commit 82e64bd

Browse files
sandy081Copilot
andcommitted
agentHost: bound catalog reconciliation work
Publish passive metadata without awaiting full catalog synchronization, batch discovery timestamp and dirty-marker updates, and rotate persisted bounded verification samples instead of rescanning every catalog row on each startup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9c0669c commit 82e64bd

9 files changed

Lines changed: 431 additions & 42 deletions

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

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
2121
const DEFAULT_FULL_VERIFICATION_INTERVAL_MS = 60 * 60 * 1000;
2222
const DEFAULT_BACKGROUND_DELAY_MS = 1000;
2323
const RECONCILIATION_CURSOR_STORAGE_KEY = 'agentHost.catalogReconciliation.cursor';
24+
const VERIFICATION_CURSOR_STORAGE_KEY = 'agentHost.catalogReconciliation.verificationCursor';
25+
const VERIFICATION_VERSION_STORAGE_KEY = 'agentHost.catalogReconciliation.verificationVersion';
26+
const LAST_VERIFICATION_STORAGE_KEY = 'agentHost.catalogReconciliation.lastVerification';
27+
const CATALOG_VERIFICATION_VERSION = 1;
2428
type AgentHostCatalogSyncPendingReason = Extract<AgentHostCatalogSyncResult, { status: 'pending' }>['reason'];
2529
type ScheduledPassKind = 'background' | 'periodic';
2630

@@ -80,8 +84,9 @@ export class AgentHostCatalogReconciliationService extends Disposable {
8084
private readonly _scheduledPass = this._register(new MutableDisposable<IDisposable>());
8185
private _scheduledPassKind: ScheduledPassKind | undefined;
8286
private _payloadDirtyMark: Promise<void> | undefined;
83-
private _initialPayloadDirtyMarkPending = true;
84-
private _lastFullVerification = 0;
87+
private _initialPayloadDirtyMarkPending: boolean;
88+
private _lastCompatibilityVerification: number;
89+
private _verificationCursor: string | undefined;
8590
private _running: Promise<IAgentHostCatalogReconciliationReport> | undefined;
8691
private _rerunRequested = false;
8792
private _periodic = false;
@@ -104,6 +109,11 @@ export class AgentHostCatalogReconciliationService extends Disposable {
104109
this._backgroundDelayMs = this._nonNegativeInteger(options.backgroundDelayMs, DEFAULT_BACKGROUND_DELAY_MS, 'backgroundDelayMs');
105110
this._schedule = options.schedule ?? ((callback, delay) => disposableTimeout(callback, delay));
106111
this._now = options.now ?? Date.now;
112+
this._initialPayloadDirtyMarkPending = this._storageService.get<number>(VERIFICATION_VERSION_STORAGE_KEY) !== CATALOG_VERIFICATION_VERSION;
113+
const lastVerification = this._storageService.get<number>(LAST_VERIFICATION_STORAGE_KEY);
114+
this._lastCompatibilityVerification = typeof lastVerification === 'number' && Number.isFinite(lastVerification) && lastVerification <= this._now() ? lastVerification : 0;
115+
const verificationCursor = this._storageService.get<string>(VERIFICATION_CURSOR_STORAGE_KEY);
116+
this._verificationCursor = typeof verificationCursor === 'string' ? verificationCursor : undefined;
107117
}
108118

109119
schedule(): void {
@@ -207,21 +217,20 @@ export class AgentHostCatalogReconciliationService extends Disposable {
207217

208218
private async _runSinglePass(token: CancellationToken): Promise<IAgentHostCatalogReconciliationReport> {
209219
await this._ensureInitialPayloadDirtyMark();
210-
if (this._now() - this._lastFullVerification >= this._fullVerificationIntervalMs) {
211-
await this._markAllPayloadsDirty();
212-
this._lastFullVerification = this._now();
220+
if (this._now() - this._lastCompatibilityVerification >= this._fullVerificationIntervalMs) {
221+
await this._markVerificationSampleDirty(token);
213222
}
214223
const { sessions, receiptBySession } = await this._listDirtySessions();
215224
if (sessions.length === 0) {
216-
this._storageService.delete(this._cursorStorageKey);
225+
this._tryDeleteStorage(this._cursorStorageKey);
217226
return { outcomes: [], cursor: undefined };
218227
}
219228

220229
const selected = this._selectBatch(sessions, this._readCursor());
221230
const outcomes = await this._runBatch(selected, receiptBySession, token);
222231
const cursor = selected.at(-1)?.session.toString();
223232
if (cursor && !token.isCancellationRequested) {
224-
this._storageService.set(this._cursorStorageKey, cursor);
233+
this._trySetStorage(this._cursorStorageKey, cursor);
225234
}
226235
return { outcomes, cursor };
227236
}
@@ -235,11 +244,11 @@ export class AgentHostCatalogReconciliationService extends Disposable {
235244
outcomes.push(...await this._runBatch(selected, receiptBySession, token));
236245
cursor = selected.at(-1)?.session.toString();
237246
if (cursor && !token.isCancellationRequested) {
238-
this._storageService.set(this._cursorStorageKey, cursor);
247+
this._trySetStorage(this._cursorStorageKey, cursor);
239248
}
240249
}
241250
if (sessions.length === 0) {
242-
this._storageService.delete(this._cursorStorageKey);
251+
this._tryDeleteStorage(this._cursorStorageKey);
243252
}
244253
return { outcomes, cursor };
245254
}
@@ -556,13 +565,65 @@ export class AgentHostCatalogReconciliationService extends Disposable {
556565
}
557566
await this._markAllPayloadsDirty();
558567
this._initialPayloadDirtyMarkPending = false;
559-
this._lastFullVerification = this._now();
568+
this._trySetStorage(VERIFICATION_VERSION_STORAGE_KEY, CATALOG_VERIFICATION_VERSION);
569+
this._recordCompatibilityVerification();
560570
}
561571

562572
private async _prepareFullVerification(): Promise<void> {
563573
await this._markAllPayloadsDirty();
564574
this._initialPayloadDirtyMarkPending = false;
565-
this._lastFullVerification = this._now();
575+
this._trySetStorage(VERIFICATION_VERSION_STORAGE_KEY, CATALOG_VERIFICATION_VERSION);
576+
this._recordCompatibilityVerification();
577+
}
578+
579+
private async _markVerificationSampleDirty(token: CancellationToken): Promise<void> {
580+
if (token.isCancellationRequested) {
581+
return;
582+
}
583+
const receipts = (await this._catalogDatabase.listSessionsV2Receipts())
584+
.filter(receipt => receipt.payloadDirty === 0)
585+
.sort((first, second) => compareSessionKeys(first.session, second.session));
586+
const selected = this._selectVerificationSample(receipts, this._verificationCursor);
587+
await this._catalogDatabase.markSessionsV2PayloadsDirty(selected.map(receipt => receipt.session));
588+
if (token.isCancellationRequested) {
589+
return;
590+
}
591+
const cursor = selected.at(-1)?.session;
592+
if (cursor) {
593+
this._verificationCursor = cursor;
594+
this._trySetStorage(VERIFICATION_CURSOR_STORAGE_KEY, cursor);
595+
}
596+
this._recordCompatibilityVerification();
597+
}
598+
599+
private _selectVerificationSample(receipts: readonly IAgentHostDatabaseSessionV2Receipt[], cursor: string | undefined): readonly IAgentHostDatabaseSessionV2Receipt[] {
600+
if (receipts.length === 0) {
601+
return [];
602+
}
603+
const start = cursor === undefined ? 0 : Math.max(0, receipts.findIndex(receipt => compareSessionKeys(receipt.session, cursor) > 0));
604+
const ordered = start === 0 ? receipts : [...receipts.slice(start), ...receipts.slice(0, start)];
605+
return ordered.slice(0, this._batchSize);
606+
}
607+
608+
private _recordCompatibilityVerification(): void {
609+
this._lastCompatibilityVerification = this._now();
610+
this._trySetStorage(LAST_VERIFICATION_STORAGE_KEY, this._lastCompatibilityVerification);
611+
}
612+
613+
private _trySetStorage<T>(key: string, value: T): void {
614+
try {
615+
this._storageService.set(key, value);
616+
} catch (error) {
617+
this._logService.warn(`[AgentHostCatalogReconciliation] Failed to persist '${key}'`, error);
618+
}
619+
}
620+
621+
private _tryDeleteStorage(key: string): void {
622+
try {
623+
this._storageService.delete(key);
624+
} catch (error) {
625+
this._logService.warn(`[AgentHostCatalogReconciliation] Failed to delete '${key}'`, error);
626+
}
566627
}
567628

568629
private _markAllPayloadsDirty(): Promise<void> {

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export interface IAgentHostDatabase extends IDisposable {
123123
updateSessionExternal(updates: readonly IAgentHostDatabaseExternalUpdate[]): Promise<void>;
124124
/** Advances the durable last-observed modification time. */
125125
updateSessionModifiedTime(session: string, modifiedTime: number): Promise<boolean>;
126-
/** Advances the durable last-observed modification time for many sessions in one transaction. */
126+
/** Advances modification times and marks changed catalog payloads dirty in one transaction. */
127127
updateSessionModifiedTimes(updates: readonly IAgentHostDatabaseModifiedTimeUpdate[]): Promise<void>;
128128
getSession(session: string): Promise<IAgentHostDatabaseSession | undefined>;
129129
listSessions(): Promise<readonly IAgentHostDatabaseSession[]>;
@@ -207,6 +207,8 @@ export interface IAgentHostDatabase extends IDisposable {
207207
getSessionV2PayloadDirty(session: string): Promise<number | undefined>;
208208
/** Marks every cached payload dirty once so mutations made by older builds are rechecked. */
209209
markAllSessionsV2PayloadsDirty(): Promise<void>;
210+
/** Marks selected cached payloads dirty in one transaction. */
211+
markSessionsV2PayloadsDirty(sessions: readonly string[]): Promise<void>;
210212
/** Clears a dirty marker only when no newer mutation superseded it. */
211213
markSessionV2PayloadClean(session: string, expectedDirty: number): Promise<boolean>;
212214
upsertSessionV2(envelope: IAgentHostDatabaseSessionV2Envelope, expectedSessionGeneration: string | undefined): Promise<AgentHostDatabaseSessionV2UpsertResult>;
@@ -389,6 +391,7 @@ function sessionsV2BackfillKey(provider: AgentProvider, payloadVersion: number):
389391

390392
const sessionsV2ExcludedKeyPrefix = 'sessionsV2Excluded:';
391393
const sessionsV2PayloadDirtyKeyPrefix = 'sessionsV2PayloadDirty:';
394+
const MODIFIED_TIME_UPDATE_BATCH_SIZE = 400;
392395
const sessionChatCatalogLegacyMirrorKeyPrefix = 'sessionChatCatalogLegacyMirror:';
393396

394397
function sessionsV2ExcludedProviderPrefix(provider: AgentProvider): string {
@@ -558,10 +561,36 @@ export class AgentHostDatabase implements IAgentHostDatabase {
558561
const database = await this._ensureDatabase();
559562
await exec(database, 'BEGIN IMMEDIATE');
560563
try {
561-
for (const { session, modifiedTime } of updates) {
562-
await run(database, 'UPDATE sessions_v2 SET modified_time = ? WHERE session_uri = ? AND modified_time < ?', [modifiedTime, session, modifiedTime]);
563-
await run(database, 'UPDATE sessions SET modified_time = ? WHERE session_uri = ? AND modified_time < ?', [modifiedTime, session, modifiedTime]);
564+
await exec(database, `CREATE TEMP TABLE IF NOT EXISTS session_modified_time_updates (
565+
session_uri TEXT PRIMARY KEY NOT NULL,
566+
modified_time INTEGER NOT NULL
567+
);
568+
DELETE FROM session_modified_time_updates`);
569+
for (let offset = 0; offset < updates.length; offset += MODIFIED_TIME_UPDATE_BATCH_SIZE) {
570+
const batch = updates.slice(offset, offset + MODIFIED_TIME_UPDATE_BATCH_SIZE);
571+
await run(database, `INSERT OR REPLACE INTO session_modified_time_updates (session_uri, modified_time) VALUES ${batch.map(() => '(?, ?)').join(', ')}`,
572+
batch.flatMap(({ session, modifiedTime }) => [session, modifiedTime]));
564573
}
574+
await run(database, `INSERT INTO metadata (key, value)
575+
SELECT '${sessionsV2PayloadDirtyKeyPrefix}' || sessions_v2.session_uri, '1'
576+
FROM sessions_v2
577+
INNER JOIN session_modified_time_updates AS updates ON updates.session_uri = sessions_v2.session_uri
578+
WHERE sessions_v2.modified_time < updates.modified_time
579+
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`, []);
580+
await run(database, `UPDATE sessions_v2 SET modified_time = (
581+
SELECT updates.modified_time FROM session_modified_time_updates AS updates
582+
WHERE updates.session_uri = sessions_v2.session_uri
583+
) WHERE EXISTS (
584+
SELECT 1 FROM session_modified_time_updates AS updates
585+
WHERE updates.session_uri = sessions_v2.session_uri AND sessions_v2.modified_time < updates.modified_time
586+
)`, []);
587+
await run(database, `UPDATE sessions SET modified_time = (
588+
SELECT updates.modified_time FROM session_modified_time_updates AS updates
589+
WHERE updates.session_uri = sessions.session_uri
590+
) WHERE EXISTS (
591+
SELECT 1 FROM session_modified_time_updates AS updates
592+
WHERE updates.session_uri = sessions.session_uri AND sessions.modified_time < updates.modified_time
593+
)`, []);
565594
await exec(database, 'COMMIT');
566595
} catch (error) {
567596
await this._rollback(database, error, 'Failed to update session modified times');
@@ -1108,8 +1137,7 @@ export class AgentHostDatabase implements IAgentHostDatabase {
11081137
try {
11091138
const exists = await get(database, 'SELECT 1 AS present FROM sessions_v2 WHERE session_uri = ?', [session]);
11101139
if (exists) {
1111-
await run(database, `INSERT INTO metadata (key, value) VALUES (?, '1')
1112-
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`, [sessionsV2PayloadDirtyKey(session)]);
1140+
await this._markSessionV2PayloadDirty(database, session);
11131141
}
11141142
const row = exists
11151143
? await get(database, 'SELECT CAST(value AS INTEGER) AS payload_dirty FROM metadata WHERE key = ?', [sessionsV2PayloadDirtyKey(session)])
@@ -1144,6 +1172,32 @@ export class AgentHostDatabase implements IAgentHostDatabase {
11441172
});
11451173
}
11461174

1175+
async markSessionsV2PayloadsDirty(sessions: readonly string[]): Promise<void> {
1176+
if (sessions.length === 0) {
1177+
return;
1178+
}
1179+
return this._transactionSequencer.queue(async () => {
1180+
const database = await this._ensureDatabase();
1181+
await exec(database, 'BEGIN IMMEDIATE');
1182+
try {
1183+
for (const session of sessions) {
1184+
const exists = await get(database, 'SELECT 1 AS present FROM sessions_v2 WHERE session_uri = ?', [session]);
1185+
if (exists) {
1186+
await this._markSessionV2PayloadDirty(database, session);
1187+
}
1188+
}
1189+
await exec(database, 'COMMIT');
1190+
} catch (error) {
1191+
await this._rollback(database, error, 'Failed to mark selected sessions_v2 payloads dirty');
1192+
}
1193+
});
1194+
}
1195+
1196+
private _markSessionV2PayloadDirty(database: Database, session: string): Promise<void> {
1197+
return run(database, `INSERT INTO metadata (key, value) VALUES (?, '1')
1198+
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`, [sessionsV2PayloadDirtyKey(session)]);
1199+
}
1200+
11471201
async markSessionV2PayloadClean(session: string, expectedDirty: number): Promise<boolean> {
11481202
this._validatePayloadDirty(expectedDirty);
11491203
return this._transactionSequencer.queue(async () => {

0 commit comments

Comments
 (0)