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', () => {
|
it('updates one Skill invocation row in place and preserves Skill order', () => {
|
||||||
const items: ChatTimelineItem[] = [];
|
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) {
|
function removeStatusItem(items: ChatTimelineItem[], statusKey: string) {
|
||||||
const index = items.findIndex(
|
const index = items.findIndex(
|
||||||
(item) => item.type === 'status' && item.statusKey === statusKey,
|
(item) => item.type === 'status' && item.statusKey === statusKey,
|
||||||
@@ -680,7 +705,11 @@ export const ChatTimelineBuilder = {
|
|||||||
payload?.status === 'done' || payload?.phase === 'completed'
|
payload?.status === 'done' || payload?.phase === 'completed'
|
||||||
? 'done'
|
? 'done'
|
||||||
: 'running';
|
: 'running';
|
||||||
const statusKey = payload?.statusKey || 'memory-compression';
|
const statusKey = resolveStatusOccurrenceKey(
|
||||||
|
items,
|
||||||
|
payload?.statusKey || 'memory-compression',
|
||||||
|
payload?.roundId,
|
||||||
|
);
|
||||||
finishAssistantMessage(items, false, payload?.roundId);
|
finishAssistantMessage(items, false, payload?.roundId);
|
||||||
if (status === 'done' && payload?.compressed === false) {
|
if (status === 'done' && payload?.compressed === false) {
|
||||||
removeStatusItem(items, statusKey);
|
removeStatusItem(items, statusKey);
|
||||||
|
|||||||
Reference in New Issue
Block a user