fix: 保留多次上下文整理记录
- 为同一轮多次整理分配独立状态键 - 防止后续整理覆盖已完成的历史记录
This commit is contained in:
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user