Reduce agent timeline focus catch-up
This commit is contained in:
@@ -1749,7 +1749,7 @@ test("reloadAgentSession cancels active run and resumes existing session once th
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchTimeline returns full timeline with reset when cursor epoch is stale", async () => {
|
||||
test("fetchTimeline returns a bounded reset window when cursor epoch is stale", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-timeline-stale-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
@@ -1798,9 +1798,10 @@ test("fetchTimeline returns full timeline with reset when cursor epoch is stale"
|
||||
expect(result.reset).toBe(true);
|
||||
expect(result.staleCursor).toBe(true);
|
||||
expect(result.gap).toBe(false);
|
||||
expect(result.rows).toHaveLength(3);
|
||||
expect(result.rows[0]?.seq).toBe(1);
|
||||
expect(result.rows).toHaveLength(1);
|
||||
expect(result.rows[0]?.seq).toBe(3);
|
||||
expect(result.rows[result.rows.length - 1]?.seq).toBe(3);
|
||||
expect(result.hasOlder).toBe(true);
|
||||
});
|
||||
|
||||
test("getTimelineRows falls back to the in-memory timeline when no durable store is configured", async () => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { InMemoryAgentTimelineStore } from "./agent-timeline-store.js";
|
||||
|
||||
describe("InMemoryAgentTimelineStore", () => {
|
||||
it("returns a bounded reset window when an after cursor is behind retained history", () => {
|
||||
const store = new InMemoryAgentTimelineStore();
|
||||
store.initialize("agent-1", {
|
||||
epoch: "epoch-1",
|
||||
nextSeq: 8,
|
||||
rows: [
|
||||
{
|
||||
seq: 5,
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
item: { type: "assistant_message", text: "five" },
|
||||
},
|
||||
{
|
||||
seq: 6,
|
||||
timestamp: "2026-01-01T00:00:01.000Z",
|
||||
item: { type: "assistant_message", text: "six" },
|
||||
},
|
||||
{
|
||||
seq: 7,
|
||||
timestamp: "2026-01-01T00:00:02.000Z",
|
||||
item: { type: "assistant_message", text: "seven" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = store.fetch("agent-1", {
|
||||
direction: "after",
|
||||
cursor: { epoch: "epoch-1", seq: 1 },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
epoch: "epoch-1",
|
||||
direction: "after",
|
||||
reset: true,
|
||||
staleCursor: false,
|
||||
gap: true,
|
||||
window: { minSeq: 5, maxSeq: 7, nextSeq: 8 },
|
||||
hasOlder: true,
|
||||
hasNewer: false,
|
||||
rows: [
|
||||
{
|
||||
seq: 7,
|
||||
timestamp: "2026-01-01T00:00:02.000Z",
|
||||
item: { type: "assistant_message", text: "seven" },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,28 @@ function fetchBefore(ctx: FetchContext): AgentTimelineFetchResult {
|
||||
};
|
||||
}
|
||||
|
||||
function fetchReset(
|
||||
ctx: FetchContext,
|
||||
flags: { staleCursor: boolean; gap: boolean },
|
||||
): AgentTimelineFetchResult {
|
||||
const { state, direction, limit, selectAll, minSeq, window } = ctx;
|
||||
const rows =
|
||||
selectAll || limit >= state.rows.length
|
||||
? state.rows.map(cloneRow)
|
||||
: state.rows.slice(state.rows.length - limit).map(cloneRow);
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: true,
|
||||
staleCursor: flags.staleCursor,
|
||||
gap: flags.gap,
|
||||
window,
|
||||
hasOlder: rows.length > 0 && rows[0]!.seq > minSeq,
|
||||
hasNewer: false,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
export class InMemoryAgentTimelineStore {
|
||||
private readonly states = new Map<string, AgentTimelineState>();
|
||||
|
||||
@@ -176,32 +198,23 @@ export class InMemoryAgentTimelineStore {
|
||||
nextSeq: state.nextSeq,
|
||||
};
|
||||
|
||||
const ctx: FetchContext = {
|
||||
state,
|
||||
direction,
|
||||
limit,
|
||||
selectAll,
|
||||
cursor,
|
||||
minSeq,
|
||||
maxSeq,
|
||||
window,
|
||||
};
|
||||
|
||||
if (cursor && typeof cursor.epoch === "string" && cursor.epoch !== state.epoch) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: true,
|
||||
staleCursor: true,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
rows: state.rows.map(cloneRow),
|
||||
};
|
||||
return fetchReset(ctx, { staleCursor: true, gap: false });
|
||||
}
|
||||
|
||||
if (direction === "after" && cursor && state.rows.length > 0 && cursor.seq < minSeq - 1) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: true,
|
||||
staleCursor: false,
|
||||
gap: true,
|
||||
window,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
rows: state.rows.map(cloneRow),
|
||||
};
|
||||
return fetchReset(ctx, { staleCursor: false, gap: true });
|
||||
}
|
||||
|
||||
if (state.rows.length === 0) {
|
||||
@@ -218,17 +231,6 @@ export class InMemoryAgentTimelineStore {
|
||||
};
|
||||
}
|
||||
|
||||
const ctx: FetchContext = {
|
||||
state,
|
||||
direction,
|
||||
limit,
|
||||
selectAll,
|
||||
cursor,
|
||||
minSeq,
|
||||
maxSeq,
|
||||
window,
|
||||
};
|
||||
|
||||
if (direction === "tail") {
|
||||
return fetchTail(ctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user