mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-21 10:01:35 +01:00
Fix cold peer-chat fork to read source chat's own persistence resource
Cold peer-chat fork previously read the shared configurationResource overlay instead of the source chat's own persistence resource, so inherited model/agent/permissionMode came from the wrong scope for any non-default source chat. _chatConfigScopes now records both the configurationResource and the exact resource (IChatScopeBinding) for each chat, and _bindInheritedConversation reads the source overlay by the source's own resource. Also adds a backing-model fallback for a source that was created but never materialized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
cc3c3c265b
commit
aabdead86d
@@ -179,6 +179,18 @@ interface IClaudeChatBacking {
|
||||
readonly sideChat?: IPersistedChat['sideChat'];
|
||||
}
|
||||
|
||||
/**
|
||||
* A chat's exact configuration/persistence-resource pair, recorded so a later
|
||||
* fork or side-chat naming this chat as its source can resolve both without
|
||||
* deriving either from URI shape or from the destination's own context.
|
||||
*/
|
||||
interface IChatScopeBinding {
|
||||
/** The shared, session-wide configuration scope (`IAgentChatContext.configurationResource`). */
|
||||
readonly configurationResource: URI;
|
||||
/** This exact chat's own persistence resource (`IAgentChatContext.resource`) — the key its overlay is written under. */
|
||||
readonly resource: URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* One host-addressed chat operation, resolved against the provider's exact
|
||||
* chat backing.
|
||||
@@ -371,14 +383,16 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
private readonly _chatBackings = new Map<string, IClaudeChatBacking>();
|
||||
|
||||
/**
|
||||
* Maps each host-supplied concrete chat URI to the configuration/persistence
|
||||
* scope Agent Host named for it (`IAgentChatContext.configurationResource`),
|
||||
* recorded whenever that chat is created or (re-)materialized. This is the
|
||||
* only state a fork/side-chat source's own scope is ever resolved from —
|
||||
* never the destination chat's scope, never a sibling catalog grouped by
|
||||
* session.
|
||||
* Maps each host-supplied concrete chat URI to the exact
|
||||
* {@link IChatScopeBinding} — both its configuration scope
|
||||
* (`IAgentChatContext.configurationResource`, shared session-wide) and its
|
||||
* own persistence resource (`IAgentChatContext.resource`, the exact key its
|
||||
* overlay is written under) — recorded whenever that chat is created or
|
||||
* (re-)materialized. This is the only state a fork/side-chat source's own
|
||||
* scope is ever resolved from — never the destination chat's scope, never
|
||||
* a sibling catalog grouped by session.
|
||||
*/
|
||||
private readonly _chatConfigScopes = new Map<string, URI>();
|
||||
private readonly _chatConfigScopes = new Map<string, IChatScopeBinding>();
|
||||
|
||||
/**
|
||||
* Fires when a concrete chat backing's opaque `providerData` changes after creation
|
||||
@@ -493,13 +507,13 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
};
|
||||
}
|
||||
|
||||
/** Records `chat`'s configuration/persistence scope, populated on create and materialize. */
|
||||
private _recordChatScope(chat: URI, configurationResource: URI): void {
|
||||
this._chatConfigScopes.set(chat.toString(), configurationResource);
|
||||
/** Records `chat`'s exact scope binding, populated on create and materialize. */
|
||||
private _recordChatScope(chat: URI, configurationResource: URI, resource: URI): void {
|
||||
this._chatConfigScopes.set(chat.toString(), { configurationResource, resource });
|
||||
}
|
||||
|
||||
/** Resolves the configuration scope recorded for an exact source chat. */
|
||||
private _sourceChatScope(source: URI): URI | undefined {
|
||||
/** Resolves the scope binding recorded for an exact source chat. */
|
||||
private _sourceChatScope(source: URI): IChatScopeBinding | undefined {
|
||||
return this._chatConfigScopes.get(source.toString());
|
||||
}
|
||||
|
||||
@@ -1143,7 +1157,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
|
||||
const canUseTool = this._makeCanUseTool(sessionId, resource);
|
||||
const onElicitation = this._makeOnElicitation(sessionId);
|
||||
this._recordChatScope(context.chat, context.configurationResource);
|
||||
this._recordChatScope(context.chat, context.configurationResource, context.resource);
|
||||
try {
|
||||
await session.materialize({
|
||||
transport,
|
||||
@@ -1257,7 +1271,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
const chatKey = chat.toString();
|
||||
// Record this chat's own scope now — the only place a later fork
|
||||
// naming this chat as its source resolves that source's scope from.
|
||||
this._recordChatScope(chat, context.configurationResource);
|
||||
this._recordChatScope(chat, context.configurationResource, context.resource);
|
||||
return this._sessionSequencer.queue(chatKey, async () => {
|
||||
const existing = this._chatBackings.get(chatKey);
|
||||
const created = existing
|
||||
@@ -1371,26 +1385,31 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
model: ModelSelection | undefined,
|
||||
options?: IAgentCreateChatOptions,
|
||||
): Promise<IAgentCreateChatResult> {
|
||||
// The source's settings live under its own configuration/persistence
|
||||
// scope: the scope this provider recorded when the fork source chat was
|
||||
// itself created or materialized ({@link _sourceChatScope}), or — for a
|
||||
// side chat, which always branches within its own session — this
|
||||
// chat's own scope. A fork whose source scope was never recorded (the
|
||||
// source has no chat backing yet) degrades to this chat's own scope
|
||||
// rather than failing the creation outright.
|
||||
const sourceScope = options?.fork
|
||||
? this._sourceChatScope(options.fork.source) ?? context.configurationResource
|
||||
: context.configurationResource;
|
||||
// The source's settings live under its own exact persistence resource —
|
||||
// the same key its own overlay was written under (see the write below
|
||||
// and `_persistSessionOverlay`) — never the shared configuration scope.
|
||||
// That resource is the one this provider recorded when the source chat
|
||||
// was itself created or materialized ({@link _sourceChatScope}); a
|
||||
// source whose scope was never recorded (no chat backing yet, e.g. a
|
||||
// stale reference) degrades to the source URI itself, which is exactly
|
||||
// its own persistence resource for any non-default chat.
|
||||
const sourceChat = options?.fork?.source ?? options?.sideChat?.source;
|
||||
const sourceBinding = sourceChat ? this._sourceChatScope(sourceChat) : undefined;
|
||||
const sourceResource = sourceBinding?.resource ?? sourceChat ?? context.resource;
|
||||
let sourceOverlay: IClaudeSessionOverlay = {};
|
||||
try {
|
||||
sourceOverlay = await this._metadataStore.read(sourceScope);
|
||||
sourceOverlay = await this._metadataStore.read(sourceResource);
|
||||
} catch (err) {
|
||||
this._logService.warn(`[Claude] createChat: source overlay read failed for ${sourceScope.toString()}; continuing with defaults`, err);
|
||||
this._logService.warn(`[Claude] createChat: source overlay read failed for ${sourceResource.toString()}; continuing with defaults`, err);
|
||||
}
|
||||
const sourceChat = options?.fork?.source ?? options?.sideChat?.source;
|
||||
const sourceSdkId = sourceChat ? this._sourceChatSdkId(sourceChat) : undefined;
|
||||
const liveSource = sourceSdkId ? this._findAnySession(sourceSdkId) : undefined;
|
||||
const inheritedModel = model ?? liveSource?.provisionalModel ?? sourceOverlay.model;
|
||||
// A source that was created (recording a backing model) but never
|
||||
// materialized has no overlay entry yet, so the backing's own model —
|
||||
// the last resort, below the overlay once one exists — is the only
|
||||
// place its intended model survives a cold restart.
|
||||
const backingModel = sourceChat ? this._chatBackings.get(sourceChat.toString())?.model : undefined;
|
||||
const inheritedModel = model ?? liveSource?.provisionalModel ?? sourceOverlay.model ?? backingModel;
|
||||
const agent = options?.agent ?? liveSource?.provisionalAgent ?? sourceOverlay.agent;
|
||||
const permissionMode = narrowClaudePermissionMode(options?.config?.[ClaudeSessionConfigKey.PermissionMode]) ?? liveSource?.permissionModeFallback ?? sourceOverlay.permissionMode;
|
||||
|
||||
@@ -1635,7 +1654,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
const transport = this._ensureAuthenticated(chatSession.provisionalModel);
|
||||
const canUseTool = this._makeCanUseTool(chatSession.sessionId, resource);
|
||||
const onElicitation = this._makeOnElicitation(chatSession.sessionId);
|
||||
this._recordChatScope(chat, configurationResource);
|
||||
this._recordChatScope(chat, configurationResource, resource);
|
||||
try {
|
||||
await chatSession.materialize({
|
||||
transport,
|
||||
@@ -1736,7 +1755,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
additionalDirectories,
|
||||
);
|
||||
this._registerLiveChat(chat, chatSession);
|
||||
this._recordChatScope(chat, configurationResource);
|
||||
this._recordChatScope(chat, configurationResource, resource);
|
||||
// The chat now has a live runtime, so re-apply the contributions of
|
||||
// every client addressed to this exact chat. This replaces nothing —
|
||||
// it only pushes each handle's already-assigned tools/customizations
|
||||
@@ -1782,11 +1801,15 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
|
||||
/**
|
||||
* Re-attach a concrete chat backing from opaque provider data, recording
|
||||
* its configuration/persistence scope so a later fork naming this chat as
|
||||
* its source can resolve that scope without deriving it from URI shape.
|
||||
* its exact scope binding (configuration scope AND own persistence
|
||||
* resource) so a later fork naming this chat as its source can resolve
|
||||
* both without deriving them from URI shape. This is the sole restore
|
||||
* path for a chat that was never (re-)created in this process — a cold
|
||||
* peer chat — so it is the only place that scope binding exists for it.
|
||||
*/
|
||||
async materializeChat(chat: URI, context: URI | IAgentChatContext, providerData: string | undefined): Promise<void> {
|
||||
this._recordChatScope(chat, resolveAgentChatContext(context, chat).configurationResource);
|
||||
const resolved = resolveAgentChatContext(context, chat);
|
||||
this._recordChatScope(chat, resolved.configurationResource, resolved.resource);
|
||||
if (providerData === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -1820,11 +1843,11 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
* orchestrator can persist it additively going forward.
|
||||
*/
|
||||
async recoverLegacyChat(chat: URI, context: URI | IAgentChatContext): Promise<IAgentCreateChatResult> {
|
||||
const { configurationResource } = resolveAgentChatContext(context, chat);
|
||||
const { configurationResource, resource } = resolveAgentChatContext(context, chat);
|
||||
const chatKey = chat.toString();
|
||||
const backing = this._chatBackings.get(chatKey) ?? { sdkSessionId: AgentSession.id(configurationResource) };
|
||||
this._chatBackings.set(chatKey, backing);
|
||||
this._recordChatScope(chat, configurationResource);
|
||||
this._recordChatScope(chat, configurationResource, resource);
|
||||
return { providerData: encodeProviderData(_toPersistedChat(backing)) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user