fix(console): preserve exact adopted history boundaries
This commit is contained in:
@@ -151,7 +151,12 @@ timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<stri
|
||||
|
||||
export type SessionConversationTurn = { turn_id: string, entries: Array<SessionSnapshotEntry>, };
|
||||
|
||||
export type SessionHistoryLineageBoundary = { lineage_id: string, adopted_through_turn_id?: string | null, };
|
||||
export type SessionHistoryLineageBoundary = { lineage_id: string,
|
||||
/**
|
||||
* Canonical public form of the final user-visible turn at the exact
|
||||
* provider boundary. Entries after that boundary are excluded.
|
||||
*/
|
||||
adopted_through_turn?: SessionConversationTurn | null, };
|
||||
|
||||
export type SessionHistoryPage = { session_id: string,
|
||||
/**
|
||||
|
||||
@@ -58,7 +58,7 @@ function page(
|
||||
compactAncestorLineageIds: string[] = [],
|
||||
parentLineage?: {
|
||||
lineage_id: string;
|
||||
adopted_through_turn_id?: string | null;
|
||||
adopted_through_turn?: SessionConversationTurn | null;
|
||||
},
|
||||
): SessionHistoryPage {
|
||||
return {
|
||||
@@ -256,6 +256,10 @@ Deno.test("fork lineage boundary preserves an adopted prefix without page overla
|
||||
status: "loading",
|
||||
requestedCursor: "__initial__",
|
||||
};
|
||||
state.turns[24]!.entries.push(
|
||||
message("a-unadopted-25", "assistant", "stale sibling response"),
|
||||
);
|
||||
const adoptedBoundary = turn(25);
|
||||
state = applyConsoleHistoryPage(
|
||||
state,
|
||||
page(
|
||||
@@ -265,7 +269,7 @@ Deno.test("fork lineage boundary preserves an adopted prefix without page overla
|
||||
[],
|
||||
{
|
||||
lineage_id: "lineage-a",
|
||||
adopted_through_turn_id: "u-25",
|
||||
adopted_through_turn: adoptedBoundary,
|
||||
},
|
||||
),
|
||||
null,
|
||||
@@ -279,6 +283,10 @@ Deno.test("fork lineage boundary preserves an adopted prefix without page overla
|
||||
].join(","),
|
||||
"fork refresh must retain only the authoritative adopted prefix",
|
||||
);
|
||||
assert(
|
||||
historyEntries(state).every((entry) => entry.entry_id !== "a-unadopted-25"),
|
||||
"the canonical provider boundary must trim a sibling entry in the same visible turn",
|
||||
);
|
||||
assert(
|
||||
state.cursor === "cursor-101" && state.hasMore,
|
||||
"fork refresh must replace the old paging boundary",
|
||||
|
||||
@@ -102,7 +102,7 @@ export function applyConsoleHistoryPage(
|
||||
? reconcileParentLineage(
|
||||
state.turns,
|
||||
incoming,
|
||||
page.parent_lineage?.adopted_through_turn_id ?? null,
|
||||
page.parent_lineage?.adopted_through_turn ?? null,
|
||||
)
|
||||
: refreshingCompactDescendant
|
||||
? mergeRefreshedTurns(state.turns, incoming)
|
||||
@@ -230,14 +230,21 @@ function previewAssistant(value: string): string[] {
|
||||
function reconcileParentLineage(
|
||||
retained: readonly SessionConversationTurn[],
|
||||
refreshed: readonly SessionConversationTurn[],
|
||||
adoptedThroughTurnId: string | null,
|
||||
adoptedThroughTurn: SessionConversationTurn | null,
|
||||
): SessionConversationTurn[] {
|
||||
if (adoptedThroughTurnId === null) return dedupeTurns(refreshed);
|
||||
if (adoptedThroughTurn === null) return dedupeTurns(refreshed);
|
||||
const boundary = retained.findIndex((turn) =>
|
||||
turn.turn_id === adoptedThroughTurnId
|
||||
turn.turn_id === adoptedThroughTurn.turn_id
|
||||
);
|
||||
if (boundary < 0) return dedupeTurns(refreshed);
|
||||
return dedupeTurns([...retained.slice(0, boundary + 1), ...refreshed]);
|
||||
const refreshedIncludesBoundary = refreshed.some((turn) =>
|
||||
turn.turn_id === adoptedThroughTurn.turn_id
|
||||
);
|
||||
return dedupeTurns([
|
||||
...retained.slice(0, boundary),
|
||||
...(refreshedIncludesBoundary ? [] : [adoptedThroughTurn]),
|
||||
...refreshed,
|
||||
]);
|
||||
}
|
||||
|
||||
function reconcileChangedLineage(
|
||||
|
||||
@@ -3127,6 +3127,21 @@ Deno.test("committed history places omitted retained rows between snapshot ancho
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("committed history places current context before an interior first anchor", () => {
|
||||
const makeLine = (prefix: string, turn: number) => {
|
||||
const line = consoleLine(`${prefix}-${turn}`, "user");
|
||||
line.entryId = `user-${turn}`;
|
||||
return line;
|
||||
};
|
||||
const history = [6, 7, 8, 9, 10].map((turn) => makeLine("history", turn));
|
||||
const current = [1, 8, 10].map((turn) => makeLine("current", turn));
|
||||
|
||||
assertEquals(
|
||||
mergeCommittedHistoryLines(history, current).map((line) => line.entryId),
|
||||
[1, 6, 7, 8, 9, 10].map((turn) => `user-${turn}`),
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("committed history places a retained suffix before newer current-only rows", () => {
|
||||
const makeLine = (prefix: string, turn: number) => {
|
||||
const line = consoleLine(`${prefix}-${turn}`, "user");
|
||||
|
||||
@@ -334,9 +334,16 @@ export function mergeCommittedHistoryLines(
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const anchor of anchors) {
|
||||
appendHistoryInterval(anchor.history);
|
||||
merged.push(...current.slice(currentCursor, anchor.current));
|
||||
for (const [index, anchor] of anchors.entries()) {
|
||||
if (index === 0) {
|
||||
// Current rows before the first shared entry can be older retained
|
||||
// context that the newest history page starts after.
|
||||
merged.push(...current.slice(currentCursor, anchor.current));
|
||||
appendHistoryInterval(anchor.history);
|
||||
} else {
|
||||
appendHistoryInterval(anchor.history);
|
||||
merged.push(...current.slice(currentCursor, anchor.current));
|
||||
}
|
||||
merged.push(current[anchor.current]!);
|
||||
historyCursor = anchor.history + 1;
|
||||
currentCursor = anchor.current + 1;
|
||||
|
||||
+4
-2
@@ -11,6 +11,7 @@ import {
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import type {
|
||||
Event as ProtocolEvent,
|
||||
SessionConversationTurn,
|
||||
SessionSnapshot,
|
||||
} from "$lib/generated/protocol";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
@@ -127,7 +128,7 @@ function historyPage(
|
||||
lineageId = "lineage-a",
|
||||
parentLineage?: {
|
||||
lineage_id: string;
|
||||
adopted_through_turn_id?: string | null;
|
||||
adopted_through_turn?: SessionConversationTurn | null;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
@@ -506,7 +507,8 @@ test("fork refresh reconciles an in-flight older page to the adopted prefix", as
|
||||
"lineage-fork",
|
||||
{
|
||||
lineage_id: "lineage-a",
|
||||
adopted_through_turn_id: "user-48",
|
||||
adopted_through_turn: historyPage([48], null).page
|
||||
.turns[0] as SessionConversationTurn,
|
||||
},
|
||||
)));
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user