Fix chat history paging around tool updates (#1247)
* Fix projected timeline paging * Avoid full timeline fetches for plain pages
This commit is contained in:
@@ -547,6 +547,153 @@ describe("processTimelineResponse", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates a fetched in-progress tool call as one item and streams the next update on top", () => {
|
||||
const fetched = processTimelineResponse({
|
||||
...baseTimelineInput,
|
||||
isInitializing: true,
|
||||
hasActiveInitDeferred: true,
|
||||
initRequestDirection: "tail",
|
||||
payload: {
|
||||
...baseTimelineInput.payload,
|
||||
direction: "tail",
|
||||
epoch: "epoch-1",
|
||||
startCursor: { seq: 10 },
|
||||
endCursor: { seq: 250 },
|
||||
entries: [
|
||||
{
|
||||
...makeToolCallTimelineEntry(10, "call-1", "running", {
|
||||
type: "read",
|
||||
filePath: "/tmp/example.ts",
|
||||
}),
|
||||
seqEnd: 250,
|
||||
sourceSeqRanges: [
|
||||
{ startSeq: 10, endSeq: 10 },
|
||||
{ startSeq: 250, endSeq: 250 },
|
||||
],
|
||||
collapsed: ["tool_lifecycle"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAgentToolCalls(fetched.tail)).toHaveLength(1);
|
||||
expect(fetched.cursor).toEqual({ epoch: "epoch-1", startSeq: 10, endSeq: 250 });
|
||||
|
||||
const streamed = processAgentStreamEvent({
|
||||
...baseStreamInput,
|
||||
currentTail: fetched.tail,
|
||||
currentHead: fetched.head,
|
||||
currentCursor: fetched.cursor ?? undefined,
|
||||
seq: 251,
|
||||
epoch: "epoch-1",
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call-1",
|
||||
name: "Read",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "read",
|
||||
filePath: "/tmp/example.ts",
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
} as AgentStreamEventPayload,
|
||||
});
|
||||
|
||||
const tools = getAgentToolCalls(streamed.tail);
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0]?.payload.data.status).toBe("completed");
|
||||
expect(streamed.cursor).toEqual({ epoch: "epoch-1", startSeq: 10, endSeq: 251 });
|
||||
});
|
||||
|
||||
it("accepts an after-page projected tool update whose item started before the cursor", () => {
|
||||
const existingCursor: TimelineCursor = {
|
||||
epoch: "epoch-1",
|
||||
startSeq: 10,
|
||||
endSeq: 249,
|
||||
};
|
||||
const runningTool = hydrateStreamState(
|
||||
[
|
||||
{
|
||||
event: makeToolCallTimelineEvent("call-1"),
|
||||
timestamp: new Date(1010),
|
||||
},
|
||||
],
|
||||
{ source: "canonical" },
|
||||
);
|
||||
|
||||
const result = processTimelineResponse({
|
||||
...baseTimelineInput,
|
||||
currentTail: runningTool,
|
||||
currentCursor: existingCursor,
|
||||
payload: {
|
||||
...baseTimelineInput.payload,
|
||||
direction: "after",
|
||||
epoch: "epoch-1",
|
||||
startCursor: { seq: 250 },
|
||||
endCursor: { seq: 250 },
|
||||
entries: [
|
||||
{
|
||||
...makeToolCallTimelineEntry(10, "call-1", "completed", {
|
||||
type: "read",
|
||||
filePath: "/tmp/example.ts",
|
||||
}),
|
||||
seqEnd: 250,
|
||||
sourceSeqRanges: [
|
||||
{ startSeq: 10, endSeq: 10 },
|
||||
{ startSeq: 250, endSeq: 250 },
|
||||
],
|
||||
collapsed: ["tool_lifecycle"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const catchUp = result.sideEffects.find((effect) => effect.type === "catch_up");
|
||||
const tools = getAgentToolCalls(result.tail);
|
||||
expect(catchUp).toBeUndefined();
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0]?.payload.data.status).toBe("completed");
|
||||
expect(result.cursor).toEqual({ epoch: "epoch-1", startSeq: 10, endSeq: 250 });
|
||||
});
|
||||
|
||||
it("replaces an active assistant head when after-page returns a full projected assistant item", () => {
|
||||
const existingCursor: TimelineCursor = {
|
||||
epoch: "epoch-1",
|
||||
startSeq: 1,
|
||||
endSeq: 3,
|
||||
};
|
||||
const currentHead = [makeAssistantItem("ABC")];
|
||||
|
||||
const result = processTimelineResponse({
|
||||
...baseTimelineInput,
|
||||
currentHead,
|
||||
currentCursor: existingCursor,
|
||||
payload: {
|
||||
...baseTimelineInput.payload,
|
||||
direction: "after",
|
||||
epoch: "epoch-1",
|
||||
startCursor: { seq: 4 },
|
||||
endCursor: { seq: 5 },
|
||||
entries: [
|
||||
{
|
||||
...makeTimelineEntry(1, "ABCDE"),
|
||||
seqEnd: 5,
|
||||
sourceSeqRanges: [{ startSeq: 1, endSeq: 5 }],
|
||||
collapsed: ["assistant_merge"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAssistantTexts(result.tail)).toEqual([]);
|
||||
expect(getAssistantTexts(result.head)).toEqual(["ABCDE"]);
|
||||
expect(result.cursor).toEqual({ epoch: "epoch-1", startSeq: 1, endSeq: 5 });
|
||||
});
|
||||
|
||||
it("detects gap and emits catch-up side effect", () => {
|
||||
const existingCursor: TimelineCursor = {
|
||||
epoch: "epoch-1",
|
||||
|
||||
Reference in New Issue
Block a user