Skip to content

Commit 8390f66

Browse files
authored
fix: outdated history when trimming happens, add missing metric for compaction (aws#2047)
1 parent df2f71e commit 8390f66

File tree

6 files changed

+146
-35
lines changed

6 files changed

+146
-35
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,11 +1022,8 @@ export class AgenticChatController implements ChatHandlers {
10221022
if (currentMessage) {
10231023
// Get and process the messages from history DB to maintain invariants for service requests
10241024
try {
1025-
const { messages: historyMessages, count: historyCharCount } = this.#chatHistoryDb.fixAndGetHistory(
1026-
tabId,
1027-
currentMessage,
1028-
[]
1029-
)
1025+
const { history: historyMessages, historyCount: historyCharCount } =
1026+
this.#chatHistoryDb.fixAndGetHistory(tabId, conversationIdentifier ?? '', currentMessage, [])
10301027
messages = historyMessages
10311028
characterCount = historyCharCount
10321029
} catch (err) {
@@ -1200,19 +1197,18 @@ export class AgenticChatController implements ChatHandlers {
12001197
if (currentMessage) {
12011198
// Get and process the messages from history DB to maintain invariants for service requests
12021199
try {
1203-
const newUserInputCount = this.#chatHistoryDb.calculateNewMessageCharacterCount(
1200+
const {
1201+
history: historyMessages,
1202+
historyCount: historyCharacterCount,
1203+
currentCount: currentInputCount,
1204+
} = this.#chatHistoryDb.fixAndGetHistory(
1205+
tabId,
1206+
conversationId,
12041207
currentMessage,
12051208
pinnedContextMessages
12061209
)
1207-
const { messages: historyMessages, count: historyCharacterCount } =
1208-
this.#chatHistoryDb.fixAndGetHistory(
1209-
tabId,
1210-
currentMessage,
1211-
pinnedContextMessages,
1212-
newUserInputCount
1213-
)
12141210
messages = historyMessages
1215-
currentRequestCount = newUserInputCount + historyCharacterCount
1211+
currentRequestCount = currentInputCount + historyCharacterCount
12161212
this.#debug(`Request total character count: ${currentRequestCount}`)
12171213
} catch (err) {
12181214
if (err instanceof ToolResultValidationError) {
@@ -1438,6 +1434,10 @@ export class AgenticChatController implements ChatHandlers {
14381434
}
14391435

14401436
if (this.#shouldCompact(currentRequestCount)) {
1437+
this.#telemetryController.emitCompactNudge(
1438+
currentRequestCount,
1439+
this.#features.runtime.serverInfo.version ?? ''
1440+
)
14411441
const messageId = this.#getMessageIdForCompact(uuid())
14421442
const confirmationResult = this.#processCompactConfirmation(messageId, currentRequestCount)
14431443
const cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult)

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,30 @@ describe('ChatDatabase', () => {
9191
})
9292
})
9393

94+
describe('replaceHistory', () => {
95+
it('should replace history with messages', async () => {
96+
await chatDb.databaseInitialize(0)
97+
const tabId = 'tab-1'
98+
const tabType = 'cwc'
99+
const conversationId = 'conv-1'
100+
const messages = [
101+
{ body: 'Test', type: 'prompt' as any, timestamp: new Date() },
102+
{ body: 'Thinking...', type: 'answer', timestamp: new Date() },
103+
]
104+
105+
// Call the method
106+
chatDb.replaceHistory(tabId, tabType, conversationId, messages)
107+
108+
// Verify the messages array contains the summary and a dummy response
109+
const messagesFromDb = chatDb.getMessages(tabId, 250)
110+
assert.strictEqual(messagesFromDb.length, 2)
111+
assert.strictEqual(messagesFromDb[0].body, 'Test')
112+
assert.strictEqual(messagesFromDb[0].type, 'prompt')
113+
assert.strictEqual(messagesFromDb[1].body, 'Thinking...')
114+
assert.strictEqual(messagesFromDb[1].type, 'answer')
115+
})
116+
})
117+
94118
describe('ensureValidMessageSequence', () => {
95119
it('should preserve valid alternating sequence', () => {
96120
const messages: Message[] = [

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,50 @@ export class ChatDatabase {
634634
}
635635
}
636636

637+
/**
638+
* Replace history with summary/dummyResponse pair within a specified tab.
639+
*
640+
* This method manages chat messages by creating a new history with compacted summary and dummy response pairs
641+
*/
642+
replaceHistory(tabId: string, tabType: TabType, conversationId: string, messages: Message[]) {
643+
if (this.isInitialized()) {
644+
const clientType = this.#features.lsp.getClientInitializeParams()?.clientInfo?.name || 'unknown'
645+
const tabCollection = this.#db.getCollection<Tab>(TabCollection)
646+
647+
this.#features.logging.log(
648+
`Update history with new messages: tabId=${tabId}, tabType=${tabType}, conversationId=${conversationId}`
649+
)
650+
651+
const oldHistoryId = this.getOrCreateHistoryId(tabId)
652+
// create a new historyId to start fresh
653+
const historyId = this.createHistoryId(tabId)
654+
655+
const tabData = historyId ? tabCollection.findOne({ historyId }) : undefined
656+
const tabTitle = tabData?.title || 'Amazon Q Chat'
657+
messages = messages.map(msg => this.formatChatHistoryMessage(msg))
658+
this.#features.logging.log(`Overriding tab with new historyId=${historyId}`)
659+
tabCollection.insert({
660+
historyId,
661+
updatedAt: new Date(),
662+
isOpen: true,
663+
tabType: tabType,
664+
title: tabTitle,
665+
conversations: [
666+
{
667+
conversationId,
668+
clientType,
669+
updatedAt: new Date(),
670+
messages: messages,
671+
},
672+
],
673+
})
674+
675+
if (oldHistoryId) {
676+
tabCollection.findAndRemove({ historyId: oldHistoryId })
677+
}
678+
}
679+
}
680+
637681
formatChatHistoryMessage(message: Message): Message {
638682
if (message.type === ('prompt' as ChatItemType)) {
639683
let hasToolResults = false
@@ -663,11 +707,16 @@ export class ChatDatabase {
663707
*/
664708
fixAndGetHistory(
665709
tabId: string,
710+
conversationId: string,
666711
newUserMessage: ChatMessage,
667-
pinnedContextMessages: ChatMessage[],
668-
newUserInputCount?: number
712+
pinnedContextMessages: ChatMessage[]
669713
): MessagesWithCharacterCount {
670-
let messagesWithCount: MessagesWithCharacterCount = { messages: [], count: 0 }
714+
let newUserInputCount = this.calculateNewMessageCharacterCount(newUserMessage, pinnedContextMessages)
715+
let messagesWithCount: MessagesWithCharacterCount = {
716+
history: [],
717+
historyCount: 0,
718+
currentCount: newUserInputCount,
719+
}
671720
if (!this.isInitialized()) {
672721
return messagesWithCount
673722
}
@@ -683,32 +732,32 @@ export class ChatDatabase {
683732
// 3. Fix new user prompt: Ensure lastMessage in history toolUse and newMessage toolResult relationship is valid
684733
this.validateAndFixNewMessageToolResults(allMessages, newUserMessage)
685734

686-
if (!newUserInputCount) {
687-
newUserInputCount = this.calculateNewMessageCharacterCount(newUserMessage, pinnedContextMessages)
688-
}
689-
690735
// 4. NOTE: Keep this trimming logic at the end of the preprocess.
691736
// Make sure max characters ≤ remaining Character Budget, must be put at the end of preprocessing
692-
messagesWithCount = this.trimMessagesToMaxLength(allMessages, newUserInputCount)
693-
allMessages = messagesWithCount.messages
737+
messagesWithCount = this.trimMessagesToMaxLength(allMessages, newUserInputCount, tabId, conversationId)
738+
694739
// Edge case: If the history is empty and the next message contains tool results, then we have to just abandon them.
695740
if (
696-
allMessages.length === 0 &&
741+
messagesWithCount.history.length === 0 &&
697742
newUserMessage.userInputMessage?.userInputMessageContext?.toolResults?.length &&
698743
newUserMessage.userInputMessage?.userInputMessageContext?.toolResults?.length > 0
699744
) {
700745
this.#features.logging.warn('History overflow: abandoning dangling toolResults.')
701746
newUserMessage.userInputMessage.userInputMessageContext.toolResults = []
702747
newUserMessage.userInputMessage.content = 'The conversation history has overflowed, clearing state'
748+
// Update character count for current message
749+
this.#features.logging.debug(`Updating input character with pinnedContext`)
750+
messagesWithCount.currentCount = this.calculateNewMessageCharacterCount(
751+
newUserMessage,
752+
pinnedContextMessages
753+
)
703754
}
704755
}
705756

706757
// Prepend pinned context fake message pair to beginning of history
707758
if (pinnedContextMessages.length === 2) {
708-
messagesWithCount.messages = [
709-
...pinnedContextMessages.map(msg => chatMessageToMessage(msg)),
710-
...allMessages,
711-
]
759+
const pinnedMessages = pinnedContextMessages.map(msg => chatMessageToMessage(msg))
760+
messagesWithCount.history = [...pinnedMessages, ...messagesWithCount.history]
712761
}
713762

714763
return messagesWithCount
@@ -740,11 +789,20 @@ export class ChatDatabase {
740789
return !!ctx && (!ctx.toolResults || ctx.toolResults.length === 0) && message.body !== ''
741790
}
742791

743-
private trimMessagesToMaxLength(messages: Message[], newUserInputCount: number): MessagesWithCharacterCount {
792+
private trimMessagesToMaxLength(
793+
messages: Message[],
794+
newUserInputCount: number,
795+
tabId: string,
796+
conversationId: string
797+
): MessagesWithCharacterCount {
744798
let historyCharacterCount = this.calculateMessagesCharacterCount(messages)
745799
const maxHistoryCharacterSize = Math.max(0, MaxOverallCharacters - newUserInputCount)
746-
this.#features.logging.debug(`Current remaining character budget: ${maxHistoryCharacterSize}`)
800+
let trimmedHistory = false
801+
this.#features.logging.debug(
802+
`Current history character count: ${historyCharacterCount}, remaining history character budget: ${maxHistoryCharacterSize}`
803+
)
747804
while (historyCharacterCount > maxHistoryCharacterSize && messages.length > 2) {
805+
trimmedHistory = true
748806
// Find the next valid user message to start from
749807
const indexToTrim = this.findIndexToTrim(messages)
750808
if (indexToTrim !== undefined && indexToTrim > 0) {
@@ -756,13 +814,20 @@ export class ChatDatabase {
756814
this.#features.logging.debug(
757815
'Could not find a valid point to trim, reset history to reduce character count'
758816
)
759-
return { messages: [], count: 0 }
817+
this.replaceHistory(tabId, 'cwc', conversationId, [])
818+
return { history: [], historyCount: 0, currentCount: newUserInputCount }
760819
}
761820
historyCharacterCount = this.calculateMessagesCharacterCount(messages)
821+
this.#features.logging.debug(`History character count post trimming: ${historyCharacterCount}`)
822+
}
823+
824+
if (trimmedHistory) {
825+
this.replaceHistory(tabId, 'cwc', conversationId, messages)
762826
}
763827
return {
764-
messages,
765-
count: historyCharacterCount,
828+
history: messages,
829+
historyCount: historyCharacterCount,
830+
currentCount: newUserInputCount,
766831
}
767832
}
768833

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ export type TabWithDbMetadata = {
126126
export type DbReference = { collection: Collection<Tab>; db: Loki }
127127

128128
export type MessagesWithCharacterCount = {
129-
messages: Message[]
130-
count: number
129+
history: Message[]
130+
historyCount: number
131+
currentCount: number
131132
}
132133

133134
/**

server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ export class ChatTelemetryController {
226226
data: {
227227
type,
228228
characters,
229+
credentialStartUrl: this.#credentialsProvider.getConnectionMetadata()?.sso?.startUrl,
230+
languageServerVersion: languageServerVersion,
231+
},
232+
})
233+
}
234+
235+
public emitCompactNudge(characters: number, languageServerVersion: string) {
236+
this.#telemetry.emitMetric({
237+
name: ChatTelemetryEventName.CompactNudge,
238+
data: {
239+
characters,
240+
credentialStartUrl: this.#credentialsProvider.getConnectionMetadata()?.sso?.startUrl,
229241
languageServerVersion: languageServerVersion,
230242
},
231243
})

server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export enum ChatTelemetryEventName {
206206
MCPServerInit = 'amazonq_mcpServerInit',
207207
LoadHistory = 'amazonq_loadHistory',
208208
CompactHistory = 'amazonq_compactHistory',
209+
CompactNudge = 'amazonq_compactNudge',
209210
ChatHistoryAction = 'amazonq_performChatHistoryAction',
210211
ExportTab = 'amazonq_exportTab',
211212
UiClick = 'ui_click',
@@ -231,6 +232,7 @@ export interface ChatTelemetryEventMap {
231232
[ChatTelemetryEventName.MCPServerInit]: MCPServerInitializeEvent
232233
[ChatTelemetryEventName.LoadHistory]: LoadHistoryEvent
233234
[ChatTelemetryEventName.CompactHistory]: CompactHistoryEvent
235+
[ChatTelemetryEventName.CompactNudge]: CompactNudgeEvent
234236
[ChatTelemetryEventName.ChatHistoryAction]: ChatHistoryActionEvent
235237
[ChatTelemetryEventName.ExportTab]: ExportTabEvent
236238
[ChatTelemetryEventName.UiClick]: UiClickEvent
@@ -392,6 +394,13 @@ export type LoadHistoryEvent = {
392394
export type CompactHistoryEvent = {
393395
type: CompactHistoryActionType
394396
characters: number
397+
credentialStartUrl?: string
398+
languageServerVersion?: string
399+
}
400+
401+
export type CompactNudgeEvent = {
402+
characters: number
403+
credentialStartUrl?: string
395404
languageServerVersion?: string
396405
}
397406

0 commit comments

Comments
 (0)