Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Draft
Show file tree
Hide file tree
Changes from all 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
17 changes: 8 additions & 9 deletions 17 src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,8 @@ export class AgentSideEffects extends Disposable {
this._publishAgentInfos(agents, reader);
}));

// Server-dispatched ChatToolCallComplete actions (e.g. from
// the disconnect timeout in ProtocolServerHandler) bypass
// handleAction, so the agent's SDK deferred never resolves.
// Listen for these envelopes and notify the agent directly.
// Observe envelopes for side effects that must include server-dispatched
// actions, which bypass handleAction.
this._register(this._stateManager.onDidEmitEnvelope(envelope => {
if (isAhpChatChannel(envelope.channel) && isChatAction(envelope.action)) {
if (envelope.action.type === ActionType.ChatTurnCancelled) {
Expand Down Expand Up @@ -230,6 +228,12 @@ export class AgentSideEffects extends Disposable {
if (envelope.action.type === ActionType.ChatDraftChanged) {
this._persistChatDraft(envelope.channel, envelope.action.draft);
}
if (envelope.action.type === ActionType.SessionConfigChanged) {
const values = this._stateManager.getSessionState(envelope.channel)?.config?.values;
if (values) {
this._persistSessionFlag(envelope.channel, 'configValues', JSON.stringify(values));
}
}
Comment thread
roblourens marked this conversation as resolved.
}));
}

Expand Down Expand Up @@ -1320,13 +1324,8 @@ export class AgentSideEffects extends Disposable {
break;
}
case ActionType.SessionConfigChanged: {
// Persist merged values so a future `restoreSession` can re-hydrate
// the user's previous selections (e.g. autoApprove).
const sessionState = this._stateManager.getSessionState(channel);
const values = sessionState?.config?.values;
if (values) {
this._persistSessionFlag(channel, 'configValues', JSON.stringify(values));
}
if (this._worktree && sessionState?.lifecycle === SessionLifecycle.Creating) {
const sessionId = AgentSession.id(channel);
const isolation = values?.[SessionConfigKey.Isolation];
Expand Down
15 changes: 10 additions & 5 deletions 15 src/vs/platform/agentHost/node/shared/persistSessionMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ import type { ISessionDataService } from '../../common/sessionDataService.js';
* re-implement the open/write/dispose dance.
*/
export function persistSessionMetadata(sessionDataService: ISessionDataService, logService: ILogService, session: string, key: string, value: string): void {
const ref = sessionDataService.openDatabase(URI.parse(session));
ref.object.setMetadata(key, value).catch(err => {
const onError = (err: unknown) => {
logService.warn(`[AgentHost] Failed to persist session metadata '${key}'`, err);
}).finally(() => {
ref.dispose();
});
};
try {
const ref = sessionDataService.openDatabase(URI.parse(session));
ref.object.setMetadata(key, value).catch(onError).finally(() => {
ref.dispose();
});
} catch (err) {
onError(err);
}
}
53 changes: 45 additions & 8 deletions 53 src/vs/platform/agentHost/test/node/agentSideEffects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3695,6 +3695,17 @@ suite('AgentSideEffects', () => {
sessionDb = disposables.add(await SessionDatabase.open(':memory:'));
});

async function waitForMetadata(key: string): Promise<string> {
for (let attempt = 0; attempt < 100; attempt++) {
const value = await sessionDb.getMetadata(key);
if (value !== undefined) {
return value;
}
await timeout(10);
}
throw new Error(`Session metadata '${key}' was not persisted`);
}

teardown(async () => {
await sessionDb.close();
});
Expand Down Expand Up @@ -3726,10 +3737,7 @@ suite('AgentSideEffects', () => {
title: 'Custom Title',
});

// Wait for the async persistence
await new Promise(r => setTimeout(r, 50));

assert.strictEqual(await sessionDb.getMetadata('customTitle'), 'Custom Title');
assert.strictEqual(await waitForMetadata('customTitle'), 'Custom Title');
});

test('handleListSessions returns persisted custom title', async () => {
Expand Down Expand Up @@ -3847,11 +3855,40 @@ suite('AgentSideEffects', () => {
config: { autoApprove: 'autoApprove' },
});

await new Promise(r => setTimeout(r, 50));
const persisted = await waitForMetadata('configValues');
assert.deepStrictEqual(JSON.parse(persisted), { autoApprove: 'autoApprove' });
});

test('server-dispatched SessionConfigChanged persists merged config values to the database', async () => {
const sessionDataService = createSessionDataService(sessionDb);
const localStateManager = disposables.add(new AgentHostStateManager(new NullLogService()));
const localAgent = new MockAgent();
disposables.add(toDisposable(() => localAgent.dispose()));
createTestSideEffects(disposables, localStateManager, {
getAgent: () => localAgent,
agents: observableValue<readonly IAgent[]>('agents', [localAgent]),
sessionDataService,
onTurnComplete: () => { },
});

const session = localStateManager.createSession({
resource: sessionUri.toString(),
provider: 'mock',
title: 'Initial',
status: SessionStatus.Idle,
createdAt: new Date().toISOString(),
modifiedAt: new Date().toISOString(),
project: { uri: 'file:///test-project', displayName: 'Test Project' },
});
session.config = { schema: { type: 'object', properties: {} }, values: { mode: 'plan', autoApprove: 'default' } };

localStateManager.dispatchServerAction(sessionUri.toString(), {
type: ActionType.SessionConfigChanged,
config: { mode: 'interactive' },
});

const persisted = await sessionDb.getMetadata('configValues');
assert.ok(persisted);
assert.deepStrictEqual(JSON.parse(persisted!), { autoApprove: 'autoApprove' });
const persisted = await waitForMetadata('configValues');
assert.deepStrictEqual(JSON.parse(persisted), { mode: 'interactive', autoApprove: 'default' });
});

test('SessionConfigChanged notifies the agent with the post-reducer merged values', async () => {
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.