fix(acp): confirm pending steer message on queuedSteer notification (#10532)
This commit is contained in:
@@ -170,6 +170,20 @@ function activeRunNotification(sessionId: string, activeRunId: string | null): S
|
||||
};
|
||||
}
|
||||
|
||||
function queuedSteerNotification(sessionId: string, messageId: string): SessionNotification {
|
||||
return {
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: 'session_info_update',
|
||||
_meta: {
|
||||
goose: {
|
||||
queuedSteer: { messageId, runId: 'run-1' },
|
||||
},
|
||||
},
|
||||
} as SessionNotification['update'],
|
||||
};
|
||||
}
|
||||
|
||||
describe('acpChatSessionStore', () => {
|
||||
const sessionIds = new Set<string>();
|
||||
const sessionId = (id: string): string => {
|
||||
@@ -417,6 +431,36 @@ describe('acpChatSessionStore', () => {
|
||||
expect(firstMessage?.content[0]).toMatchObject({ type: 'text', text: 'hello' });
|
||||
});
|
||||
|
||||
it('keeps steer message confirmed when queuedSteer arrives before addPendingLocalSteerMessage', () => {
|
||||
const currentSessionId = sessionId('session-1');
|
||||
const localSteerMessage = {
|
||||
...message('steer-1', 'hello'),
|
||||
metadata: { userVisible: true, agentVisible: true, steer: true },
|
||||
};
|
||||
|
||||
acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-1');
|
||||
|
||||
// queuedSteer notification arrives before addPendingLocalSteerMessage (race condition)
|
||||
acpChatSessionActions.applyAcpSessionNotification(
|
||||
queuedSteerNotification(currentSessionId, 'steer-1')
|
||||
);
|
||||
|
||||
// Now the UI adds the pending message after the RPC response returns
|
||||
acpChatSessionActions.addPendingLocalSteerMessage(currentSessionId, localSteerMessage);
|
||||
|
||||
// Message should be present and NOT in pending (already confirmed via queuedSteer)
|
||||
const snapshot = acpChatSessionStore.getSnapshot(currentSessionId);
|
||||
expect(snapshot?.messages).toHaveLength(1);
|
||||
|
||||
// Cancellation should keep it because it's confirmed, not pending
|
||||
const cancellationSnapshot = acpChatSessionActions.startPromptCancellation(
|
||||
currentSessionId,
|
||||
'attempt-1'
|
||||
);
|
||||
expect(cancellationSnapshot?.messages).toHaveLength(1);
|
||||
expect(cancellationSnapshot?.messages[0].id).toBe('steer-1');
|
||||
});
|
||||
|
||||
it('stores active run ids from session info notifications', () => {
|
||||
const currentSessionId = sessionId('session-1');
|
||||
|
||||
|
||||
@@ -633,4 +633,47 @@ describe('createAcpSessionNotificationAdapter', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('session_info_update with queuedSteer', () => {
|
||||
it('emits localSteerConfirmed when queuedSteer meta is present', () => {
|
||||
const adapter = createAcpSessionNotificationAdapter();
|
||||
const changes = adapter.apply(
|
||||
acpUpdate({
|
||||
sessionUpdate: 'session_info_update',
|
||||
_meta: {
|
||||
goose: {
|
||||
queuedSteer: { messageId: 'steer-msg-1', runId: 'run-1' },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(changes).toEqual([{ type: 'localSteerConfirmed', messageId: 'steer-msg-1' }]);
|
||||
});
|
||||
|
||||
it('emits both sessionInfo and localSteerConfirmed when both are present', () => {
|
||||
const adapter = createAcpSessionNotificationAdapter();
|
||||
const changes = adapter.apply(
|
||||
acpUpdate({
|
||||
sessionUpdate: 'session_info_update',
|
||||
title: 'New Title',
|
||||
_meta: {
|
||||
goose: {
|
||||
queuedSteer: { messageId: 'steer-msg-2', runId: 'run-2' },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(changes).toHaveLength(2);
|
||||
expect(changes[0]).toEqual({ type: 'sessionInfo', name: 'New Title' });
|
||||
expect(changes[1]).toEqual({ type: 'localSteerConfirmed', messageId: 'steer-msg-2' });
|
||||
});
|
||||
|
||||
it('returns empty array when session_info_update has no relevant fields', () => {
|
||||
const adapter = createAcpSessionNotificationAdapter();
|
||||
const changes = adapter.apply(acpUpdate({ sessionUpdate: 'session_info_update' }));
|
||||
expect(changes).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export type AcpChatStateChange =
|
||||
type: 'sessionInfo';
|
||||
name?: string;
|
||||
activeRunId?: string | null;
|
||||
gooseMode?: string;
|
||||
}
|
||||
| { type: 'localSteerConfirmed'; messageId: string }
|
||||
| { type: 'notification'; notification: NotificationEvent };
|
||||
@@ -79,6 +80,13 @@ export function getGooseActiveRunId(update: { _meta?: unknown }): string | null
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function getGooseQueuedSteer(update: { _meta?: unknown }): string | undefined {
|
||||
if (!isRecord(update._meta)) return undefined;
|
||||
const goose = update._meta.goose;
|
||||
if (!isRecord(goose) || !isRecord(goose.queuedSteer)) return undefined;
|
||||
return typeof goose.queuedSteer.messageId === 'string' ? goose.queuedSteer.messageId : undefined;
|
||||
}
|
||||
|
||||
export function rawInputToArguments(rawInput: unknown): Record<string, unknown> {
|
||||
return isRecord(rawInput) ? rawInput : {};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ interface StoreEntry extends AcpChatSessionSnapshot {
|
||||
} | null;
|
||||
pendingUserInputRequestIds: Set<string>;
|
||||
pendingLocalSteerMessageIds: Set<string>;
|
||||
preConfirmedSteerMessageIds: Set<string>;
|
||||
}
|
||||
|
||||
const initialTokenState: TokenState = {
|
||||
@@ -165,6 +166,7 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal {
|
||||
promptCancellationRestoreState: null,
|
||||
pendingUserInputRequestIds: new Set(),
|
||||
pendingLocalSteerMessageIds: new Set(),
|
||||
preConfirmedSteerMessageIds: new Set(),
|
||||
adapter: createAcpSessionNotificationAdapter(),
|
||||
};
|
||||
sessionsById.set(sessionId, entry);
|
||||
@@ -235,7 +237,9 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal {
|
||||
}
|
||||
|
||||
entry.messages = [...entry.messages, cloneMessage(message)];
|
||||
entry.pendingLocalSteerMessageIds.add(message.id);
|
||||
if (!entry.preConfirmedSteerMessageIds.delete(message.id)) {
|
||||
entry.pendingLocalSteerMessageIds.add(message.id);
|
||||
}
|
||||
entry.adapter = createAdapterForEntry(entry);
|
||||
return notify(sessionId, entry);
|
||||
};
|
||||
@@ -606,7 +610,9 @@ function applyChatStateChanges(entry: StoreEntry, changes: AcpChatStateChange[])
|
||||
}
|
||||
break;
|
||||
case 'localSteerConfirmed':
|
||||
entry.pendingLocalSteerMessageIds.delete(change.messageId);
|
||||
if (!entry.pendingLocalSteerMessageIds.delete(change.messageId)) {
|
||||
entry.preConfirmedSteerMessageIds.add(change.messageId);
|
||||
}
|
||||
break;
|
||||
case 'notification':
|
||||
entry.notifications = [...entry.notifications, change.notification];
|
||||
@@ -636,6 +642,7 @@ function resetReplayState(entry: StoreEntry): void {
|
||||
entry.promptCancellationRestoreState = null;
|
||||
entry.pendingUserInputRequestIds.clear();
|
||||
entry.pendingLocalSteerMessageIds.clear();
|
||||
entry.preConfirmedSteerMessageIds.clear();
|
||||
entry.adapter = createAcpSessionNotificationAdapter();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type AdapterState,
|
||||
cloneMessage,
|
||||
getGooseActiveRunId,
|
||||
getGooseQueuedSteer,
|
||||
} from './adapter/shared';
|
||||
import { applyToolCall, applyToolCallUpdate } from './adapter/tools';
|
||||
import type { AcpElicitationRequest } from './elicitationRequests';
|
||||
@@ -79,17 +80,22 @@ function applyAcpSessionNotification(
|
||||
return applyToolCallUpdate(state, update);
|
||||
case 'session_info_update': {
|
||||
const activeRunId = getGooseActiveRunId(update);
|
||||
if (!update.title && activeRunId === undefined) {
|
||||
return [];
|
||||
}
|
||||
const queuedSteerMessageId = getGooseQueuedSteer(update);
|
||||
const changes: AcpChatStateChange[] = [];
|
||||
|
||||
return [
|
||||
{
|
||||
if (update.title || activeRunId !== undefined) {
|
||||
changes.push({
|
||||
type: 'sessionInfo',
|
||||
...(update.title ? { name: update.title } : {}),
|
||||
...(activeRunId !== undefined ? { activeRunId } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (queuedSteerMessageId) {
|
||||
changes.push({ type: 'localSteerConfirmed', messageId: queuedSteerMessageId });
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
case 'usage_update':
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user