diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 3a77468a145..5bffc282686 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -36,6 +36,7 @@ import { type UserPromptAdmission, } from './client.js'; import { MESSAGE_DISPLAY_DEBOUNCE_MS } from './message-display-buffer.js'; +import { detectTurnInterruption } from './turn-interruption.js'; import { getRecentGitStatus } from '../utils/gitUtils.js'; import { ChatCompressionService } from '../services/chatCompressionService.js'; import { @@ -121,6 +122,7 @@ import { buildChangedMcpToolsReminder, buildChangedSkillsReminder, buildMcpServerInstructionsReminderFromEntries, + getDirectoryContextString, getInitialChatHistory, wrapSystemReminder, } from './environmentContext.js'; @@ -3709,6 +3711,51 @@ describe('Gemini Client (client.ts)', () => { }); }); + it.each([null, 'Git snapshot'])( + 'does not mistake a working-directory refresh for interrupted input (%s)', + async (gitStatus) => { + await client.startChat(); + vi.mocked(getDirectoryContextString).mockResolvedValue( + 'Mocked directory context', + ); + vi.mocked(getRecentGitStatus).mockReturnValue(gitStatus); + const history: Content[] = [userText('review'), modelText('done')]; + vi.spyOn(client.getChat(), 'addHistory').mockImplementation((entry) => { + history.push(entry); + }); + + await client.addWorkingDirectoryChangedContext('/old', '/new'); + + const serialized = JSON.parse(JSON.stringify(history)) as Content[]; + expect(serialized.slice(2)).toEqual([ + { + role: 'user', + parts: [ + { + text: + "The session's working directory has changed from /old to /new via /cd. " + + 'The startup directory context above is stale. All tool calls and relative paths now resolve from /new.', + }, + ...(gitStatus + ? [ + { + text: `\n${gitStatus}\n`, + }, + ] + : []), + ], + }, + userText('Mocked directory context'), + ]); + expect(detectTurnInterruption(history).kind).toBe('none'); + // The same words submitted as real input must still be recoverable. + expect(detectTurnInterruption(serialized)).toEqual({ + kind: 'interrupted_prompt', + parts: serialized.slice(2).flatMap((entry) => entry.parts ?? []), + }); + }, + ); + describe('getMainSessionSystemInstruction', () => { it('does not collect local git or agent context for an execution environment', async () => { vi.mocked(mockConfig.getExecutionEnvironment).mockReturnValue( diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index fdba76e581f..5155ced8a59 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -188,6 +188,7 @@ import { import type { DeferredToolSummary } from '../tools/tool-registry.js'; import { replayUiTelemetryFromConversation } from '../services/sessionService.js'; import { buildSessionHistoryFromConversation } from '../services/session-api-history.js'; +import { markApiHistoryDirectoryContext } from '../utils/api-history-metadata.js'; import { reportError } from '../utils/errorReporting.js'; import { getErrorMessage, @@ -2019,10 +2020,12 @@ export class LlmClient { return; } - this.getChat().addHistory({ + const chat = this.getChat(); + const context: Content = { role: 'user', parts: [{ text: await getDirectoryContextString(this.config) }], - }); + }; + chat.addHistory(markApiHistoryDirectoryContext(context)); } async addWorkingDirectoryChangedContext( @@ -2040,7 +2043,7 @@ export class LlmClient { this.cachedGitStatus = undefined; await this.refreshSystemInstruction(); const gitStatus = this.getCachedGitStatus(); - this.getChat().addHistory({ + const context: Content = { role: 'user', parts: [ { @@ -2050,7 +2053,8 @@ export class LlmClient { }, ...(gitStatus ? [{ text: wrapSystemReminder(gitStatus) }] : []), ], - }); + }; + this.getChat().addHistory(markApiHistoryDirectoryContext(context)); await this.addDirectoryContext(); } diff --git a/packages/core/src/core/llm-chat.test.ts b/packages/core/src/core/llm-chat.test.ts index 92a3bd59196..0de695fec0e 100644 --- a/packages/core/src/core/llm-chat.test.ts +++ b/packages/core/src/core/llm-chat.test.ts @@ -80,6 +80,10 @@ import { SYSTEM_REMINDER_OPEN } from './environmentContext.js'; import { selectForkHistory } from '../tools/agent/fork-subagent.js'; import { composePostCompactHistory } from '../services/postCompactAttachments.js'; import { detectTurnInterruption } from './turn-interruption.js'; +import { + markApiHistoryDirectoryContext, + markApiHistoryPrompt, +} from '../utils/api-history-metadata.js'; import { formatAgentMessageModelText } from '../agents/session-agents/envelope.js'; import { SessionStartSource } from '../hooks/types.js'; import * as sideQueryModule from '../utils/sideQuery.js'; @@ -5092,6 +5096,167 @@ describe('LlmChat', async () => { throw new Error('unexpected deep clone'); }); + describe('directory context recovery', () => { + it.each([false, true])( + 'only replays context after a structural reminder when real input is pending (%s)', + (realInput) => { + const input = realInput ? [userText('Unanswered real input')] : []; + const contexts = [ + markApiHistoryDirectoryContext(userText('Directory changed.')), + markApiHistoryDirectoryContext(userText('Directory structure.')), + ]; + const history = [ + userText('review'), + modelText('done'), + userText('\nNew MCP tools.\n'), + ...input, + ...contexts, + ]; + const serialized = JSON.stringify(history); + chat.setHistory(history); + expect(detectTurnInterruption(chat.getHistory())).toEqual( + realInput + ? { + kind: 'interrupted_prompt', + parts: [...input, ...contexts].flatMap( + (entry) => entry.parts ?? [], + ), + } + : { kind: 'none' }, + ); + expect(JSON.stringify(chat.getHistory())).toBe(serialized); + }, + ); + + it.each(['before', 'after'])( + 'replays exactly the entries strip removes when real input is %s directory context', + (position) => { + const contexts = [ + markApiHistoryDirectoryContext(userText('Directory changed.')), + markApiHistoryDirectoryContext(userText('Directory structure.')), + ]; + const input = userText('Unanswered real input'); + const entries = + position === 'before' ? [input, ...contexts] : [...contexts, input]; + const kept = [ + userText('review'), + modelText('done'), + userText('\nNew MCP tools.\n'), + ]; + chat.setHistory([...kept, ...entries]); + const interruption = detectTurnInterruption(chat.getHistory()); + expect(interruption.kind).toBe('interrupted_prompt'); + const stripped = chat.stripOrphanedUserEntriesFromHistory(); + expect(stripped).toEqual(entries); + if (interruption.kind === 'interrupted_prompt') { + expect(interruption.parts).toEqual( + stripped.flatMap((entry) => entry.parts ?? []), + ); + } + expect(chat.getHistory()).toEqual(kept); + }, + ); + + it('recovers tool results repaired into a directory entry without duplicating them', () => { + chat.setHistory([ + userText('review'), + content('model', fnCall('read_file', {}, 'pending')), + markApiHistoryDirectoryContext(userText('Directory changed.')), + markApiHistoryDirectoryContext(userText('Directory structure.')), + ]); + expect(chat.repairOrphanedToolUseTurns().injected).toHaveLength(1); + const interruption = detectTurnInterruption(chat.getHistory()); + expect(interruption.kind).toBe('interrupted_prompt'); + if (interruption.kind === 'interrupted_prompt') { + expect( + interruption.parts.filter((part) => part.functionResponse), + ).toEqual([ + expect.objectContaining({ + functionResponse: expect.objectContaining({ id: 'pending' }), + }), + ]); + } + expect(chat.repairOrphanedToolUseTurns().injected).toEqual([]); + }); + + it.each([false, true])( + 'only keeps a merged context marker when all entries are internal (%s)', + (realInput) => { + const following = userText('Following text'); + if (!realInput) markApiHistoryDirectoryContext(following); + chat.setHistory([ + userText('review'), + modelText('done'), + markApiHistoryDirectoryContext(userText('Directory context')), + following, + ]); + expect(detectTurnInterruption(chat.getHistory(true)).kind).toBe( + realInput ? 'interrupted_prompt' : 'none', + ); + }, + ); + + it.each(['clean', 'prompt-before', 'prompt-after', 'tool'] as const)( + 'preserves the %s verdict through recovery copies without changing history', + (scenario) => { + const directories = Array.from({ length: 60 }, (_, index) => + markApiHistoryDirectoryContext( + userText(`Directory context ${index}`), + ), + ); + const input = userText('Unanswered real input'); + const history = [userText('review'), modelText('done')]; + if (scenario === 'prompt-before') history.push(input); + if (scenario === 'tool') { + history.push(content('model', fnCall('read_file', {}, 'pending'))); + } + history.push(...directories); + if (scenario === 'prompt-after') history.push(input); + const serialized = JSON.stringify(history); + chat.setHistory(history); + + for (const [index, copy] of [ + chat.getHistory(), + chat.getHistoryShallow(), + chat.getHistoryForRecovery(50), + ].entries()) { + const boundedPrompt = index === 2 && scenario === 'prompt-after'; + expect(JSON.stringify(copy)).toBe( + boundedPrompt ? JSON.stringify(history.slice(-50)) : serialized, + ); + const result = detectTurnInterruption(copy); + if (scenario === 'clean') { + expect(result.kind).toBe('none'); + } else if (scenario === 'tool') { + expect(result).toEqual({ + kind: 'interrupted_turn', + danglingCalls: [{ callId: 'pending', name: 'read_file' }], + }); + } else { + expect(result).toEqual({ + kind: 'interrupted_prompt', + parts: history + .slice(boundedPrompt ? -50 : 2) + .flatMap((entry) => entry.parts ?? []), + }); + } + } + }, + ); + + it('keeps real input recoverable even if it reuses a marked entry', () => { + const input = markApiHistoryDirectoryContext( + userText('Directory context'), + ); + markApiHistoryPrompt(input, 'real-prompt'); + chat.setHistory([userText('review'), modelText('done'), input]); + expect(detectTurnInterruption(chat.getHistory())).toEqual({ + kind: 'interrupted_prompt', + parts: input.parts, + }); + }); + }); + describe('completed tool boundary', () => { const result = content('user', fnResponse('update_goal', {}, 'ended')); const fresh = () => structuredClone(result); diff --git a/packages/core/src/core/llm-chat.ts b/packages/core/src/core/llm-chat.ts index 0ce0a95fa05..e1f28fa38a8 100644 --- a/packages/core/src/core/llm-chat.ts +++ b/packages/core/src/core/llm-chat.ts @@ -182,6 +182,10 @@ import { cloneApiHistory, } from '../services/session-api-history.js'; import { isAgentEnvelopeContent } from '../agents/session-agents/envelope.js'; +import { + clearApiHistoryDirectoryContext, + isApiHistoryDirectoryContext, +} from '../utils/api-history-metadata.js'; export { InvalidStreamError }; @@ -1781,6 +1785,13 @@ function appendCuratedContent( ...lastContent, parts: [...(lastContent.parts ?? []), ...(content.parts ?? [])], }; + if ( + isApiHistoryDirectoryContext(lastContent) && + !isApiHistoryDirectoryContext(content) + ) { + // Object spread copies the Symbol; real input must not inherit it. + clearApiHistoryDirectoryContext(curatedHistory[lastIndex]!); + } return; } @@ -6219,6 +6230,7 @@ export class LlmChat { if (lastEntry && isAgentEnvelopeContent(lastEntry)) { break; } + // Keeping directory context here would duplicate it on Retry. strippedEntries.unshift(this.history.pop()!); } // Today this is safe even without the reset — only trailing user diff --git a/packages/core/src/core/turn-interruption.ts b/packages/core/src/core/turn-interruption.ts index ef5f8fb376b..314ac9aad65 100644 --- a/packages/core/src/core/turn-interruption.ts +++ b/packages/core/src/core/turn-interruption.ts @@ -10,6 +10,7 @@ import { isSystemReminderContent, } from './environmentContext.js'; import { isAgentEnvelopeContent } from '../agents/session-agents/envelope.js'; +import { isApiHistoryDirectoryContext } from '../utils/api-history-metadata.js'; /** * Classification of how a session's last turn ended, computed from persisted @@ -256,12 +257,10 @@ export function effectiveHistoryEnd( : history.length - trailingSystemNotifications; while (end > 0) { const entry = history[end - 1]!; - // A session multi-agent record (agent reply or @-mention) is durable - // conversation the main model was never asked to answer, so a trailing - // one is not an unfinished turn. + // Agent envelopes and marked directory refreshes do not ask for a reply. // TODO(multi-agent): an agent entry after trailing notifications shifts // the authoritative notification window below; revisit if that mixes. - if (isAgentEnvelopeContent(entry)) { + if (isAgentEnvelopeContent(entry) || isApiHistoryDirectoryContext(entry)) { end--; continue; } @@ -317,7 +316,8 @@ export function detectTurnInterruption( // Walk from the REAL end, not the trimmed one, while the verdict above // still reads `history[end - 1]`. The Retry send path // (`stripOrphanedUserEntriesFromHistory`) pops the ENTIRE trailing user - // run — its only break-guard is `isSystemReminderContent`, which is false + // run, including marked directory context that must be replayed with input. + // Its structural break-guard is `isSystemReminderContent`, which is false // for an envelope — so the re-submission has to carry every entry the // strip removes. Collecting from `end - 1` instead would drop a // recorded-but-undelivered notification from live history permanently: @@ -337,6 +337,12 @@ export function detectTurnInterruption( } trailingUserEntries.unshift(entry); } + if ( + isSystemReminderContent(last) && + trailingUserEntries.every(isApiHistoryDirectoryContext) + ) { + return { kind: 'none' }; + } // Capture every part, including any per-turn system-reminder parts riding // alongside the prompt. The Retry send path does not re-inject per-turn // reminders, so replaying them keeps the continued turn complete. When a diff --git a/packages/core/src/services/chatRecordingService.ts b/packages/core/src/services/chatRecordingService.ts index 45ad51a434e..50d1c0ce485 100644 --- a/packages/core/src/services/chatRecordingService.ts +++ b/packages/core/src/services/chatRecordingService.ts @@ -70,7 +70,10 @@ import { SessionWriterLease, } from './session-writer-lease.js'; import { prepareTranscriptRecords } from '../utils/transcript-records.js'; -import { getApiHistoryPromptId } from '../utils/api-history-metadata.js'; +import { + getApiHistoryPromptId, + isApiHistoryDirectoryContext, +} from '../utils/api-history-metadata.js'; import { getStartupContextLength } from '../core/environmentContext.js'; import { computeInitialTurnFromHistory } from './session-turn-state.js'; import { @@ -675,6 +678,8 @@ export interface ChatCompressionRecordPayload { summaryPrefixLength?: number; /** Prompt identities parallel to compressedHistory. */ promptIds?: Array; + /** Internal directory context, without changing the model-facing snapshot. */ + directoryContextIndices?: number[]; completedToolCallIds?: string[]; } @@ -3262,6 +3267,10 @@ export class ChatRecordingService { const promptIds = compressedHistory.map( (content) => getApiHistoryPromptId(content) ?? null, ); + const directoryContextIndices = compressedHistory.flatMap( + (content, index) => + isApiHistoryDirectoryContext(content) ? [index] : [], + ); const record: ChatRecord = { ...this.createBaseRecord('system'), type: 'system', @@ -3274,6 +3283,9 @@ export class ChatRecordingService { }), summaryPrefixLength, ...(promptIds.some(Boolean) ? { promptIds } : {}), + ...(directoryContextIndices.length + ? { directoryContextIndices } + : {}), }, }; diff --git a/packages/core/src/services/session-api-history.ts b/packages/core/src/services/session-api-history.ts index d20c8d654df..0aa2c83cd5a 100644 --- a/packages/core/src/services/session-api-history.ts +++ b/packages/core/src/services/session-api-history.ts @@ -16,8 +16,10 @@ import type { import { isInternalCodeModeToolResult } from '../utils/transcript-records.js'; import { markCompressedHistory } from '../core/environmentContext.js'; import { + clearApiHistoryDirectoryContext, getApiHistoryPromptId, markApiHistoryPrompt, + markApiHistoryDirectoryContext, } from '../utils/api-history-metadata.js'; export { @@ -221,6 +223,7 @@ function appendApiHistoryRecord( ) ) { previous.parts = [...(previous.parts ?? []), ...(message.parts ?? [])]; + clearApiHistoryDirectoryContext(previous); return; } } @@ -360,10 +363,18 @@ export class SessionApiHistoryAccumulator { if (!isApiHistoryCompressionCandidate(record)) return; const payload = record.systemPayload as ChatCompressionRecordPayload; this.compressionCandidate = payload.compressedHistory; + const directoryContextIndices = new Set( + Array.isArray(payload.directoryContextIndices) + ? payload.directoryContextIndices + : [], + ); this.history = Array.isArray(payload.compressedHistory) ? payload.compressedHistory.map((content, index) => { const copy = copyContentForApiHistory(content); markApiHistoryPrompt(copy, payload.promptIds?.[index]); + if (directoryContextIndices.has(index)) { + markApiHistoryDirectoryContext(copy); + } return copy; }) : []; diff --git a/packages/core/src/services/session-transcript-reader.test.ts b/packages/core/src/services/session-transcript-reader.test.ts index 22202b69dc8..5e07d2e2652 100644 --- a/packages/core/src/services/session-transcript-reader.test.ts +++ b/packages/core/src/services/session-transcript-reader.test.ts @@ -43,6 +43,10 @@ import { Storage } from '../config/storage.js'; import type { Config } from '../config/config.js'; import { CompressionStatus } from '../core/turn.js'; import { detectTurnInterruption } from '../core/turn-interruption.js'; +import { + isApiHistoryDirectoryContext, + markApiHistoryDirectoryContext, +} from '../utils/api-history-metadata.js'; import { SessionSourceService, type SessionSourcesSnapshot, @@ -675,6 +679,81 @@ describe('SessionTranscriptReader', () => { }, ); + it.each(['user', 'mid-turn'] as const)( + 'keeps directory context non-interrupting after saving and reloading a checkpoint (%s)', + async (inputKind) => { + const config = { + storage: new Storage(workspaceDir), + getSessionId: () => sessionId, + getProjectRoot: () => workspaceDir, + getCliVersion: () => 'test', + getModel: () => 'test', + getAuthType: () => undefined, + getApprovalMode: () => ApprovalMode.DEFAULT, + getResumedSessionData: () => undefined, + } as unknown as Config; + const recorder = new ChatRecordingService(config, undefined, false); + const history = [ + userText('review'), + modelText('done'), + markApiHistoryDirectoryContext(userText('Directory changed.')), + markApiHistoryDirectoryContext(userText('Directory structure.')), + ]; + const wireHistory = JSON.parse(JSON.stringify(history)); + recorder.recordChatCompression({ + info: { + compressionStatus: CompressionStatus.COMPRESSED, + originalTokenCount: 100, + newTokenCount: 50, + }, + compressedHistory: history, + }); + await recorder.flush(); + + const service = svc(); + const loaded = await service.loadSession(sessionId); + const checkpoint = loaded!.conversation.messages.find( + (entry) => entry.subtype === 'chat_compression', + ); + expect(checkpoint?.systemPayload).toMatchObject({ + compressedHistory: wireHistory, + directoryContextIndices: [2, 3], + }); + const projection = await service.readRestoreProjection(sessionId, NONE); + const restored = buildSessionHistoryFromConversation( + loaded!.conversation, + ); + for (const apiHistory of [ + restored.apiHistory, + projection!.runtime.apiHistory, + ]) { + expect(JSON.stringify(apiHistory)).toBe(JSON.stringify(wireHistory)); + expect(apiHistory.map(isApiHistoryDirectoryContext)).toEqual([ + false, + false, + true, + true, + ]); + expect(detectTurnInterruption(apiHistory).kind).toBe('none'); + } + + if (inputKind === 'user') { + recorder.recordUserMessage([{ text: 'Unanswered real input' }]); + } else { + recorder.recordMidTurnUserMessage( + [{ text: 'Unanswered real input' }], + 'Unanswered real input', + ); + } + await recorder.flush(); + const next = await service.readRestoreProjection(sessionId, NONE); + expect(detectTurnInterruption(next!.runtime.apiHistory)).toMatchObject({ + kind: 'interrupted_prompt', + parts: expect.arrayContaining([{ text: 'Unanswered real input' }]), + }); + }, + ); + it('round-trips a recorded Goal turn end through selective restore, fork, and rewind', async () => { const config = { storage: new Storage(workspaceDir), diff --git a/packages/core/src/utils/api-history-metadata.ts b/packages/core/src/utils/api-history-metadata.ts index d6b605507ec..95cfc772ebf 100644 --- a/packages/core/src/utils/api-history-metadata.ts +++ b/packages/core/src/utils/api-history-metadata.ts @@ -7,11 +7,31 @@ import type { Content } from '@google/genai'; const API_HISTORY_PROMPT_ID = Symbol('apiHistoryPromptId'); +const DIRECTORY_CONTEXT = Symbol('directoryContext'); type IdentifiedContent = Content & { [API_HISTORY_PROMPT_ID]?: string; + [DIRECTORY_CONTEXT]?: boolean; }; +export function markApiHistoryDirectoryContext(content: Content): Content { + (content as IdentifiedContent)[DIRECTORY_CONTEXT] = true; + return content; +} + +export function clearApiHistoryDirectoryContext(content: Content): void { + delete (content as IdentifiedContent)[DIRECTORY_CONTEXT]; +} + +export function isApiHistoryDirectoryContext(content: Content): boolean { + return ( + content.role === 'user' && + (content as IdentifiedContent)[DIRECTORY_CONTEXT] === true && + content.parts?.every((part) => typeof part.text === 'string') === true && + getApiHistoryPromptId(content) === undefined + ); +} + export function cloneApiHistory(history: Content[]): Content[] { // structuredClone drops the non-serialized prompt and context boundaries. return history.map((content) => ({ diff --git a/packages/core/src/utils/message-group-deletion.test.ts b/packages/core/src/utils/message-group-deletion.test.ts index cfc3a78b296..8032a43edbd 100644 --- a/packages/core/src/utils/message-group-deletion.test.ts +++ b/packages/core/src/utils/message-group-deletion.test.ts @@ -10,6 +10,7 @@ import type { Content } from '@google/genai'; import type { ChatRecord } from '../services/chatRecordingService.js'; import { SessionFileHistoryAccumulator } from '../services/session-file-history-state.js'; import { CompressionStatus } from '../core/turn.js'; +import { detectTurnInterruption } from '../core/turn-interruption.js'; import { buildApiHistoryFromConversation } from '../services/session-api-history.js'; import { prepareTranscriptRecords } from './transcript-records.js'; import { @@ -122,6 +123,65 @@ describe('message group deletion', () => { expect(getTranscriptMessageGroups(kept)).toEqual([]); }); + it('remaps directory context identities across a deleted checkpoint turn', () => { + const u1 = user('u1', null); + const a1 = record('a1', 'u1', 'assistant'); + const u2 = user('u2', 'a1'); + const a2 = record('a2', 'u2', 'assistant'); + const u3 = user('u3', 'a2'); + const a3 = record('a3', 'u3', 'assistant'); + const firstContext: Content = { + role: 'user', + parts: [{ text: 'Directory changed.' }], + }; + const lastContext: Content = { + role: 'user', + parts: [{ text: 'Directory structure.' }], + }; + const compression = record('compressed', 'a3', 'system', { + subtype: 'chat_compression', + systemPayload: { + info: { + compressionStatus: CompressionStatus.COMPRESSED, + originalTokenCount: 100, + newTokenCount: 50, + }, + compressedHistory: [ + u1.message!, + a1.message!, + firstContext, + u2.message!, + a2.message!, + u3.message!, + a3.message!, + lastContext, + ], + promptIds: ['api-u1', null, null, 'api-u2', null, 'api-u3', null, null], + directoryContextIndices: [2, 7], + }, + }); + const { records: kept } = deleteTranscriptMessageGroups( + [u1, a1, u2, a2, u3, a3, compression], + ['u2'], + anchor, + ); + expect( + kept.find((entry) => entry.uuid === 'compressed')?.systemPayload, + ).toMatchObject({ directoryContextIndices: [2, 5] }); + const restored = buildApiHistoryFromConversation({ messages: kept }); + expect(JSON.stringify(restored)).toBe( + JSON.stringify([ + u1.message, + a1.message, + firstContext, + u3.message, + a3.message, + lastContext, + ]), + ); + expect(detectTurnInterruption(restored).kind).toBe('none'); + }); + it('preserves the summary and prunes raw copies and tools from every compression checkpoint', () => { const summary: Content = { role: 'user', diff --git a/packages/core/src/utils/message-group-deletion.ts b/packages/core/src/utils/message-group-deletion.ts index 7d48df9576f..df4e8d304b9 100644 --- a/packages/core/src/utils/message-group-deletion.ts +++ b/packages/core/src/utils/message-group-deletion.ts @@ -403,6 +403,15 @@ function pruneCompression( (index) => payload.compressedHistory[index]!, ), promptIds: retained.map((index) => payload.promptIds![index]!), + ...(Array.isArray(payload.directoryContextIndices) + ? { + directoryContextIndices: retained.flatMap((index, newIndex) => + payload.directoryContextIndices!.includes(index) + ? [newIndex] + : [], + ), + } + : {}), }, }; }