fix: 保留多次上下文整理记录

- 为同一轮多次整理分配独立状态键

- 防止后续整理覆盖已完成的历史记录
This commit is contained in:
2026-08-20 11:24:55 +08:00
parent 407b85c8a9
commit f63cd9be4d
2 changed files with 114 additions and 1 deletions

View File

@@ -156,6 +156,90 @@ describe('chat timeline builder', () => {
}
});
it('keeps completed memory compression rows immutable across repeated attempts', () => {
const items: ChatTimelineItem[] = [];
const metadata = {
roundId: 'round-1',
statusKey: 'memory-compression:round-1',
};
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
phase: 'started',
status: 'running',
});
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
compressed: true,
phase: 'completed',
status: 'done',
});
ChatTimelineBuilder.appendThinkingDelta(items, '继续处理', metadata);
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
phase: 'started',
status: 'running',
});
const runningStatuses = items.filter((item) => item.type === 'status');
expect(runningStatuses).toHaveLength(2);
expect(runningStatuses[0]).toMatchObject({
label: '已整理上下文',
status: 'done',
statusKey: 'memory-compression:round-1',
});
expect(runningStatuses[1]).toMatchObject({
label: '正在整理上下文',
status: 'running',
statusKey: 'memory-compression:round-1::occurrence:2',
});
const thinkingIndex = items.findIndex(
(item) =>
item.type === 'message' &&
item.parts.some(
(part) => part.type === 'thinking' && part.content === '继续处理',
),
);
const currentStatus = runningStatuses[1];
if (!currentStatus) {
throw new Error('缺少第二次上下文整理状态');
}
expect(items.indexOf(currentStatus)).toBeGreaterThan(thinkingIndex);
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
compressed: true,
phase: 'completed',
status: 'done',
});
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
phase: 'started',
status: 'running',
});
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
...metadata,
compressed: false,
phase: 'completed',
status: 'done',
});
const completedStatuses = items.filter((item) => item.type === 'status');
expect(completedStatuses).toHaveLength(2);
expect(completedStatuses).toEqual([
expect.objectContaining({
label: '已整理上下文',
status: 'done',
statusKey: 'memory-compression:round-1',
}),
expect.objectContaining({
label: '已整理上下文',
status: 'done',
statusKey: 'memory-compression:round-1::occurrence:2',
}),
]);
});
it('updates one Skill invocation row in place and preserves Skill order', () => {
const items: ChatTimelineItem[] = [];

View File

@@ -228,6 +228,31 @@ function findStatusItem(items: ChatTimelineItem[], statusKey: string) {
);
}
function resolveStatusOccurrenceKey(
items: ChatTimelineItem[],
statusKey: string,
roundId?: string,
) {
// 同一 Turn 可多次触发整理;只复用仍在运行的实例,避免改写已完成的历史状态。
const occurrencePrefix = `${statusKey}::occurrence:`;
const occurrences = items.filter(
(item): item is ChatTimelineStatusItem =>
item.type === 'status' &&
(item.statusKey === statusKey ||
item.statusKey.startsWith(occurrencePrefix)) &&
(!roundId || item.roundId === roundId),
);
const running = [...occurrences]
.reverse()
.find((item) => item.status === 'running');
if (running) {
return running.statusKey;
}
return occurrences.length === 0
? statusKey
: `${occurrencePrefix}${occurrences.length + 1}`;
}
function removeStatusItem(items: ChatTimelineItem[], statusKey: string) {
const index = items.findIndex(
(item) => item.type === 'status' && item.statusKey === statusKey,
@@ -680,7 +705,11 @@ export const ChatTimelineBuilder = {
payload?.status === 'done' || payload?.phase === 'completed'
? 'done'
: 'running';
const statusKey = payload?.statusKey || 'memory-compression';
const statusKey = resolveStatusOccurrenceKey(
items,
payload?.statusKey || 'memory-compression',
payload?.roundId,
);
finishAssistantMessage(items, false, payload?.roundId);
if (status === 'done' && payload?.compressed === false) {
removeStatusItem(items, statusKey);