Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
49f24d8
agentHost: centralize session list metadata
sandy081 Aug 22, 2026
bcad4c9
Merge origin/main into session metadata migration
sandy081 Aug 24, 2026
7453d67
agentHost: migrate sessions directly to v2
sandy081 Aug 24, 2026
1f4d679
agentHost: extract session catalog helpers
sandy081 Aug 24, 2026
b1f2a96
agentHost: simplify session catalog persistence
sandy081 Aug 26, 2026
6a71e16
Merge origin/main into session metadata migration
sandy081 Aug 26, 2026
e7baa3e
agentHost: remove live compatibility test harness
sandy081 Aug 26, 2026
f22e651
agentHost: make catalog payload rebuildable
sandy081 Aug 27, 2026
e2839cd
Merge origin/main into session metadata migration
sandy081 Aug 27, 2026
495bfe4
agentHost: centralize session chat membership
sandy081 Aug 28, 2026
a502e17
agentHost: address catalog review feedback
sandy081 Aug 28, 2026
ad19313
Merge origin/main into session database migration
sandy081 Aug 31, 2026
963a6f9
Merge origin/main into session database migration
sandy081 Sep 1, 2026
9763af2
agentHost: bridge session catalog migration collision
sandy081 Sep 1, 2026
3c990ee
agentHost: migrate catalog after first listing
sandy081 Sep 1, 2026
57e9b90
Merge origin/main into session database migration
sandy081 Sep 1, 2026
8513b0e
Merge remote-tracking branch 'origin/main' into sandy081/agents/sessi…
sandy081 Sep 1, 2026
91ee991
Merge remote-tracking branch 'origin/main' into sandy081/agents/sessi…
sandy081 Sep 1, 2026
65e2431
agentHost: preserve titles during catalog migration
sandy081 Sep 1, 2026
84ffc4d
agentHost: bound catalog summaries
sandy081 Sep 2, 2026
e6bdcba
agentHost: address catalog persistence feedback
sandy081 Sep 2, 2026
0f234f3
Merge origin/main into session database migration
sandy081 Sep 2, 2026
49b315e
Merge remote-tracking branch 'origin/main' into sandy081/agents/sessi…
sandy081 Sep 2, 2026
4920195
agentHost: reuse keyed catalog sequencer
sandy081 Sep 2, 2026
6966b90
agentHost: harden central catalog synchronization
sandy081 Sep 2, 2026
1e29441
Merge remote-tracking branch 'origin/main' into sandy081/agents/sessi…
sandy081 Sep 2, 2026
f907524
agentHost: persist unloaded session flags centrally
sandy081 Sep 3, 2026
32714bc
agentHost: harden peer chat catalog lifecycle
sandy081 Sep 3, 2026
76757e1
agentHost: preserve peer backing during recovery
sandy081 Sep 3, 2026
e1bc2e3
agentHost: harden catalog reconciliation
sandy081 Sep 3, 2026
d26ff10
agentHost: avoid claiming unopened legacy sessions
sandy081 Sep 3, 2026
92d653f
agentHost: preserve catalog fallback metadata
sandy081 Sep 3, 2026
feffc55
agentHost: fence catalog sync during deletion
sandy081 Sep 3, 2026
d3fc54d
Merge origin/main into session database migration
sandy081 Sep 3, 2026
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
10 changes: 10 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,16 @@ export interface IAgentChatAdoptionResult {
readonly eligible: boolean;
/** Whether the chat already has Agent Host metadata, i.e. it is ours regardless of adoption. */
readonly native?: boolean;
/** Host-owned list-visible values recovered from the predecessor format. */
readonly listVisible?: ({
readonly title: string;
readonly titleSource: 'user' | 'agent' | 'auto';
} | {
readonly title?: undefined;
readonly titleSource?: undefined;
}) & {
readonly isRead?: boolean;
};
/** Set when the adopted chat ran in a worktree that no longer exists and can be recreated. */
readonly worktree?: IAgentAdoptedWorktree;
/** Diagnostic reason behind {@link adopted}. */
Expand Down
54 changes: 54 additions & 0 deletions src/vs/platform/agentHost/common/sessionDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,40 @@ export interface ILocalTurnRecord {
payload: string;
}

interface ISessionCatalogSyncIdentity {
readonly sessionGeneration: string;
readonly sourceRevision: number;
readonly projectionVersion: number;
}

/** Durable canonical catalog projection awaiting central acknowledgement. */
export interface ISessionCatalogSyncPendingSnapshot extends ISessionCatalogSyncIdentity {
readonly payload: string;
readonly payloadHash: string;
readonly acknowledgedHash?: string;
readonly state: 'pending';
}

/** Compact receipt retained after the pending payload has been acknowledged. */
export interface ISessionCatalogSyncAcknowledgedSnapshot extends ISessionCatalogSyncIdentity {
readonly payload: undefined;
readonly payloadHash: string;
readonly acknowledgedHash: string;
readonly state: 'acknowledged';
}

export type ISessionCatalogSyncSnapshot = ISessionCatalogSyncPendingSnapshot | ISessionCatalogSyncAcknowledgedSnapshot;

/** Identity fields required to acknowledge exactly one catalog synchronization snapshot. */
export interface ISessionCatalogSyncAcknowledgement {
readonly sessionGeneration: string;
readonly sourceRevision: number;
readonly projectionVersion: number;
readonly payloadHash: string;
}

/** Outcome of atomically storing metadata with a catalog synchronization snapshot. */
export type SessionCatalogSyncWriteResult = 'applied' | 'replayed';

/**
* A disposable handle to a per-session SQLite database backed by
Expand Down Expand Up @@ -308,6 +342,26 @@ export interface ISessionDatabase extends IDisposable {
*/
setMetadataValuesIfAbsent(key: string, values: Readonly<Record<string, string>>, copies?: Readonly<Record<string, string>>): Promise<boolean>;

/**
* Atomically stores metadata and advances the durable catalog relay snapshot.
*/
setMetadataValuesAndCatalogSyncSnapshot(values: Readonly<Record<string, string>>, snapshot: ISessionCatalogSyncPendingSnapshot): Promise<SessionCatalogSyncWriteResult>;

/**
* Atomically transitions to a new session generation when the stored generation matches.
*/
transitionMetadataValuesAndCatalogSyncSnapshot(values: Readonly<Record<string, string>>, expectedSessionGeneration: string, snapshot: ISessionCatalogSyncPendingSnapshot): Promise<boolean>;

/**
* Returns the durable catalog relay snapshot, if one has been stored.
*/
getCatalogSyncSnapshot(): Promise<ISessionCatalogSyncSnapshot | undefined>;

/**
* Acknowledges the snapshot only when every supplied identity field still matches.
*/
acknowledgeCatalogSyncSnapshot(acknowledgement: ISessionCatalogSyncAcknowledgement): Promise<boolean>;

/**
* Store or clear the draft for a chat in this session.
*/
Expand Down
95 changes: 95 additions & 0 deletions src/vs/platform/agentHost/node/agentHostCatalogListReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AgentSession, type IAgentSessionMetadata } from '../common/agent.js';
import { SessionStatus, withSessionExternal, withSessionStatusFlag } from '../common/state/sessionState.js';
import { AGENT_HOST_CATALOG_PAYLOAD_VERSION, decodeAgentHostCatalogPayload, reviveAgentHostCatalogData, type AgentHostCatalogRevivedData } from './agentHostCatalogProjection.js';
import type { IAgentHostDatabase } from './agentHostDatabase.js';
import type { IRegisteredSession } from './agentSessionRegistry.js';

export type AgentHostCatalogListResult =
/** The central row is authoritative for this session's listing. */
| { readonly eligible: true; readonly metadata: IAgentSessionMetadata; readonly data: AgentHostCatalogRevivedData }
/**
* The central row marks the session as a chat backing. It is deliberately
* hidden and must never fall back into the top-level list.
*/
| { readonly eligible: false; readonly chatBacking: true }
/** The central row is missing, stale or unusable; the caller falls back and schedules a repair. */
| { readonly eligible: false; readonly chatBacking: false; readonly detail: string; readonly error?: Error };

/**
* Eligibility boundary between the `sessions_v2` catalog and the session list:
* it checks that a stored row still describes the registered session, then
* hands the payload's own decoded data to the caller without re-parsing it.
*/
export class AgentHostCatalogListReader {

constructor(private readonly _catalogDatabase: IAgentHostDatabase) { }

async read(registered: IRegisteredSession): Promise<AgentHostCatalogListResult> {
const session = registered.session.toString();
try {
const catalog = await this._catalogDatabase.getSessionV2(session);
if (!catalog) {
return ineligible('no central row');
}
if (catalog.session !== session) {
return ineligible(`central row identity ${catalog.session} does not match`);
}
if (catalog.isChatBacking) {
return { eligible: false, chatBacking: true };
}
if (AgentSession.provider(registered.session) !== registered.provider || catalog.provider !== registered.provider) {
return ineligible(`central row provider ${catalog.provider} does not match ${registered.provider}`);
}
if (catalog.payloadVersion !== AGENT_HOST_CATALOG_PAYLOAD_VERSION) {
return ineligible(`central row payload version ${catalog.payloadVersion} is outdated`);
}
const decoded = decodeAgentHostCatalogPayload(catalog.payload);
if (!decoded.ok) {
return ineligible(`central payload is ${decoded.reason}: ${decoded.error}`);
}
// A payload can only become chat-backing through a write that also
// updates the row marker, but an inconsistent row must still hide
// the session rather than surface a backing as a top-level entry.
if (decoded.value.data.isChatBacking) {
return { eligible: false, chatBacking: true };
}
const data = reviveAgentHostCatalogData(decoded.value.data);
return { eligible: true, metadata: this._toSessionMetadata(registered, data), data };
} catch (error) {
return {
eligible: false,
chatBacking: false,
detail: 'central row read failed',
error: error instanceof Error ? error : new Error(String(error)),
};
}
}

private _toSessionMetadata(registered: IRegisteredSession, data: AgentHostCatalogRevivedData): IAgentSessionMetadata {
let status = withSessionStatusFlag(SessionStatus.Idle, SessionStatus.IsRead, data.isRead);
status = withSessionStatusFlag(status, SessionStatus.IsArchived, data.isArchived);
const meta = withSessionExternal(data._meta, registered.external);
return {
session: registered.session,
startTime: registered.startTime,
// The registry owns durable recency: a live advance can outrun the
// payload's own timestamp until the next reconciliation writes it back.
modifiedTime: Math.max(data.modifiedTime, registered.modifiedTime),
summary: data.summary,
status,
project: data.project,
workingDirectories: [...data.workingDirectories],
changes: data.changes,
...(meta !== undefined ? { _meta: meta } : {}),
};
}
}

function ineligible(detail: string): AgentHostCatalogListResult {
return { eligible: false, chatBacking: false, detail };
}
Loading