Reduce agent timeline focus catch-up
This commit is contained in:
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.1.65-beta.4 - 2026-05-02
|
## 0.1.65-beta.5 - 2026-05-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
- Cursor CLI and other ACP custom providers launch reliably. ([#628](https://github.com/getpaseo/paseo/pull/628))
|
- Cursor CLI and other ACP custom providers launch reliably. ([#628](https://github.com/getpaseo/paseo/pull/628))
|
||||||
- Daemon stays up when WebSocket clients disconnect mid-stream, and crashes now write a fatal log entry instead of disappearing silently. ([#613](https://github.com/getpaseo/paseo/pull/613) by [@yuruiz](https://github.com/yuruiz))
|
- Daemon stays up when WebSocket clients disconnect mid-stream, and crashes now write a fatal log entry instead of disappearing silently. ([#613](https://github.com/getpaseo/paseo/pull/613) by [@yuruiz](https://github.com/yuruiz))
|
||||||
- Long agent timelines reconnect cleanly over the relay instead of looping through disconnects while catching up. ([#657](https://github.com/getpaseo/paseo/pull/657) by [@fireblue](https://github.com/fireblue))
|
- Long agent timelines reconnect cleanly over the relay instead of looping through disconnects while catching up. ([#657](https://github.com/getpaseo/paseo/pull/657) by [@fireblue](https://github.com/fireblue))
|
||||||
|
- Agent timelines refresh with smaller catch-up requests when you reopen an agent.
|
||||||
|
- Terminal snapshots flush reliably before clients reconnect.
|
||||||
|
- Workspace reconnects avoid unnecessary refresh work when the focused workspace is already current.
|
||||||
- Voice dictation keeps recording when the agent tab loses focus.
|
- Voice dictation keeps recording when the agent tab loses focus.
|
||||||
- OpenCode mode picker now lists agents available in every mode. ([#606](https://github.com/getpaseo/paseo/pull/606) by [@thisisryanswift](https://github.com/thisisryanswift))
|
- OpenCode mode picker now lists agents available in every mode. ([#606](https://github.com/getpaseo/paseo/pull/606) by [@thisisryanswift](https://github.com/thisisryanswift))
|
||||||
- Codex plan approval panels no longer duplicate.
|
- Codex plan approval panels no longer duplicate.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("useAgentInitialization", () => {
|
describe("useAgentInitialization", () => {
|
||||||
it("always requests a canonical tail bootstrap even with an authoritative cursor", () => {
|
it("requests bounded canonical catch-up after the current cursor when authoritative history is loaded", () => {
|
||||||
const client = makeClient();
|
const client = makeClient();
|
||||||
useSessionStore.getState().initializeSession(serverId, client as never);
|
useSessionStore.getState().initializeSession(serverId, client as never);
|
||||||
useSessionStore
|
useSessionStore
|
||||||
@@ -43,6 +43,27 @@ describe("useAgentInitialization", () => {
|
|||||||
void result.current.ensureAgentIsInitialized(agentId);
|
void result.current.ensureAgentIsInitialized(agentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
|
||||||
|
direction: "after",
|
||||||
|
cursor: { epoch: "epoch-1", seq: 42 },
|
||||||
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
||||||
|
projection: "canonical",
|
||||||
|
});
|
||||||
|
expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("after");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests a bounded canonical tail when no authoritative cursor is available", () => {
|
||||||
|
const client = makeClient();
|
||||||
|
useSessionStore.getState().initializeSession(serverId, client as never);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAgentInitialization({ serverId, client: client as never }),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
void result.current.ensureAgentIsInitialized(agentId);
|
||||||
|
});
|
||||||
|
|
||||||
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
|
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
|
||||||
direction: "tail",
|
direction: "tail",
|
||||||
limit: TIMELINE_FETCH_PAGE_SIZE,
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
||||||
|
|||||||
@@ -42,13 +42,25 @@ export function useAgentInitialization({
|
|||||||
return existing.promise;
|
return existing.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timelineRequest = {
|
const session = useSessionStore.getState().sessions[serverId];
|
||||||
direction: "tail" as const,
|
const cursor = session?.agentTimelineCursor.get(agentId);
|
||||||
limit: TIMELINE_FETCH_PAGE_SIZE,
|
const hasAuthoritativeHistory =
|
||||||
projection: "canonical" as const,
|
session?.agentAuthoritativeHistoryApplied.get(agentId) === true;
|
||||||
};
|
const timelineRequest =
|
||||||
|
hasAuthoritativeHistory && cursor
|
||||||
|
? {
|
||||||
|
direction: "after" as const,
|
||||||
|
cursor: { epoch: cursor.epoch, seq: cursor.endSeq },
|
||||||
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
||||||
|
projection: "canonical" as const,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
direction: "tail" as const,
|
||||||
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
||||||
|
projection: "canonical" as const,
|
||||||
|
};
|
||||||
|
|
||||||
const deferred = createInitDeferred(key, "tail");
|
const deferred = createInitDeferred(key, timelineRequest.direction);
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
setAgentInitializing(agentId, false);
|
setAgentInitializing(agentId, false);
|
||||||
rejectInitDeferred(
|
rejectInitDeferred(
|
||||||
|
|||||||
@@ -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 workdir = mkdtempSync(join(tmpdir(), "agent-manager-timeline-stale-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
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.reset).toBe(true);
|
||||||
expect(result.staleCursor).toBe(true);
|
expect(result.staleCursor).toBe(true);
|
||||||
expect(result.gap).toBe(false);
|
expect(result.gap).toBe(false);
|
||||||
expect(result.rows).toHaveLength(3);
|
expect(result.rows).toHaveLength(1);
|
||||||
expect(result.rows[0]?.seq).toBe(1);
|
expect(result.rows[0]?.seq).toBe(3);
|
||||||
expect(result.rows[result.rows.length - 1]?.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 () => {
|
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 {
|
export class InMemoryAgentTimelineStore {
|
||||||
private readonly states = new Map<string, AgentTimelineState>();
|
private readonly states = new Map<string, AgentTimelineState>();
|
||||||
|
|
||||||
@@ -176,32 +198,23 @@ export class InMemoryAgentTimelineStore {
|
|||||||
nextSeq: state.nextSeq,
|
nextSeq: state.nextSeq,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ctx: FetchContext = {
|
||||||
|
state,
|
||||||
|
direction,
|
||||||
|
limit,
|
||||||
|
selectAll,
|
||||||
|
cursor,
|
||||||
|
minSeq,
|
||||||
|
maxSeq,
|
||||||
|
window,
|
||||||
|
};
|
||||||
|
|
||||||
if (cursor && typeof cursor.epoch === "string" && cursor.epoch !== state.epoch) {
|
if (cursor && typeof cursor.epoch === "string" && cursor.epoch !== state.epoch) {
|
||||||
return {
|
return fetchReset(ctx, { staleCursor: true, gap: false });
|
||||||
epoch: state.epoch,
|
|
||||||
direction,
|
|
||||||
reset: true,
|
|
||||||
staleCursor: true,
|
|
||||||
gap: false,
|
|
||||||
window,
|
|
||||||
hasOlder: false,
|
|
||||||
hasNewer: false,
|
|
||||||
rows: state.rows.map(cloneRow),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction === "after" && cursor && state.rows.length > 0 && cursor.seq < minSeq - 1) {
|
if (direction === "after" && cursor && state.rows.length > 0 && cursor.seq < minSeq - 1) {
|
||||||
return {
|
return fetchReset(ctx, { staleCursor: false, gap: true });
|
||||||
epoch: state.epoch,
|
|
||||||
direction,
|
|
||||||
reset: true,
|
|
||||||
staleCursor: false,
|
|
||||||
gap: true,
|
|
||||||
window,
|
|
||||||
hasOlder: false,
|
|
||||||
hasNewer: false,
|
|
||||||
rows: state.rows.map(cloneRow),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.rows.length === 0) {
|
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") {
|
if (direction === "tail") {
|
||||||
return fetchTail(ctx);
|
return fetchTail(ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user