Skip to content
Merged
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 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 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 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