mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Restore canonical timeline fetches
Older clients still request canonical rows for catch-up. Honor that explicit request while leaving projected as the default and projected app sync path. Update the pending 0.1.88 changelog with the fixes already on main.
This commit is contained in:
@@ -10,12 +10,15 @@
|
||||
- **Adjust the interface text size**
|
||||
- **Adjust the code text size**
|
||||
- **Choose a syntax highlighting theme**
|
||||
- **Keep cron schedules aligned to a chosen time zone** ([#1232](https://github.com/getpaseo/paseo/pull/1232) by [@damselem](https://github.com/damselem))
|
||||
|
||||
### Improved
|
||||
|
||||
- Settings now has a flatter sidebar with a host picker
|
||||
- Workspace tab switching is faster
|
||||
- Compact composers now show context usage as a percentage
|
||||
- Agent terminals opened in workspace subdirectories now appear with the rest of the workspace terminals
|
||||
- macOS displays can idle normally while the desktop app is open ([#1242](https://github.com/getpaseo/paseo/pull/1242) by [@fireblue](https://github.com/fireblue))
|
||||
- Large generated diffs now show a clear too-large placeholder instead of trying to render the whole file
|
||||
|
||||
### Fixed
|
||||
@@ -24,6 +27,7 @@
|
||||
- Terminal panes keep the right size after splitting or resizing panes
|
||||
- Restored terminal snapshots reflow correctly after the pane size changes
|
||||
- Workspace scripts menus keep the right size after launching a service
|
||||
- iOS chat messages no longer hide inline links, URLs, or linked file paths ([#1257](https://github.com/getpaseo/paseo/pull/1257) by [@outofrange-consulting](https://github.com/outofrange-consulting))
|
||||
|
||||
## 0.1.87 - 2026-05-30
|
||||
|
||||
|
||||
@@ -9,6 +9,42 @@ function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
||||
}
|
||||
|
||||
async function appendToolLifecycleAcrossCursor(
|
||||
ctx: DaemonTestContext,
|
||||
agentId: string,
|
||||
): Promise<void> {
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agentId, {
|
||||
type: "tool_call",
|
||||
callId: "call_1",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: { cmd: "sleep 10" },
|
||||
output: null,
|
||||
},
|
||||
});
|
||||
for (let seq = 2; seq <= 249; seq += 1) {
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agentId, {
|
||||
type: "assistant_message",
|
||||
text: `background ${seq}`,
|
||||
});
|
||||
}
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agentId, {
|
||||
type: "tool_call",
|
||||
callId: "call_1",
|
||||
name: "shell",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: { cmd: "sleep 10" },
|
||||
output: { stdout: "done" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("daemon E2E - timeline window", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
@@ -124,7 +160,7 @@ describe("daemon E2E - timeline window", () => {
|
||||
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 100,
|
||||
projection: "canonical",
|
||||
projection: "projected",
|
||||
});
|
||||
|
||||
const toolEntries = timeline.entries.filter((entry) => entry.item.type === "tool_call");
|
||||
@@ -142,46 +178,17 @@ describe("daemon E2E - timeline window", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("after fetch returns the full projected tool item for a new lifecycle update", async () => {
|
||||
test("canonical after fetch returns only committed rows after the cursor", async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Timeline Tool Catch-up Test",
|
||||
title: "Timeline Canonical Catch-up Test",
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agent.id, {
|
||||
type: "tool_call",
|
||||
callId: "call_1",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: { cmd: "sleep 10" },
|
||||
output: null,
|
||||
},
|
||||
});
|
||||
for (let seq = 2; seq <= 249; seq += 1) {
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agent.id, {
|
||||
type: "assistant_message",
|
||||
text: `background ${seq}`,
|
||||
});
|
||||
}
|
||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agent.id, {
|
||||
type: "tool_call",
|
||||
callId: "call_1",
|
||||
name: "shell",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: { cmd: "sleep 10" },
|
||||
output: { stdout: "done" },
|
||||
},
|
||||
});
|
||||
await appendToolLifecycleAcrossCursor(ctx, agent.id);
|
||||
|
||||
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
@@ -194,6 +201,43 @@ describe("daemon E2E - timeline window", () => {
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
expect(timeline.projection).toBe("canonical");
|
||||
expect(timeline.entries).toHaveLength(1);
|
||||
expect(timeline.startCursor?.seq).toBe(250);
|
||||
expect(timeline.endCursor?.seq).toBe(250);
|
||||
expect(timeline.entries[0]?.seqStart).toBe(250);
|
||||
expect(timeline.entries[0]?.seqEnd).toBe(250);
|
||||
expect(timeline.entries[0]?.sourceSeqRanges).toEqual([{ startSeq: 250, endSeq: 250 }]);
|
||||
expect(timeline.entries[0]?.item.type).toBe("tool_call");
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("projected after fetch returns the full projected tool item for a new lifecycle update", async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Timeline Tool Catch-up Test",
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
await appendToolLifecycleAcrossCursor(ctx, agent.id);
|
||||
|
||||
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
});
|
||||
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "after",
|
||||
cursor: { epoch: baseline.epoch, seq: 249 },
|
||||
limit: 100,
|
||||
projection: "projected",
|
||||
});
|
||||
|
||||
expect(timeline.projection).toBe("projected");
|
||||
expect(timeline.entries).toHaveLength(1);
|
||||
expect(timeline.startCursor?.seq).toBe(250);
|
||||
expect(timeline.endCursor?.seq).toBe(250);
|
||||
|
||||
@@ -121,7 +121,9 @@ import {
|
||||
emitLiveTimelineItemIfAgentKnown,
|
||||
} from "./agent/timeline-append.js";
|
||||
import {
|
||||
projectTimelineRows,
|
||||
selectProjectedTimelinePage,
|
||||
type TimelineProjectionEntry,
|
||||
type TimelineProjectionMode,
|
||||
} from "./agent/timeline-projection.js";
|
||||
import {
|
||||
@@ -716,6 +718,15 @@ function parseClientCapabilities(
|
||||
return new Set(result);
|
||||
}
|
||||
|
||||
interface AgentTimelineProjectionSelection {
|
||||
timeline: AgentTimelineFetchResult;
|
||||
entries: TimelineProjectionEntry[];
|
||||
startSeq: number | null;
|
||||
endSeq: number | null;
|
||||
hasOlder: boolean;
|
||||
hasNewer: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session represents a single connected client session.
|
||||
* It owns all state management, orchestration logic, and message processing.
|
||||
@@ -7437,11 +7448,70 @@ export class Session {
|
||||
return timeline.rows.some((row) => row.item.type === "tool_call");
|
||||
}
|
||||
|
||||
private selectCanonicalTimelineProjection(input: {
|
||||
timeline: AgentTimelineFetchResult;
|
||||
}): AgentTimelineProjectionSelection {
|
||||
const entries = projectTimelineRows({ rows: input.timeline.rows, mode: "canonical" });
|
||||
return {
|
||||
timeline: input.timeline,
|
||||
entries,
|
||||
startSeq: entries[0]?.seqStart ?? null,
|
||||
endSeq: entries[entries.length - 1]?.seqEnd ?? null,
|
||||
hasOlder: input.timeline.hasOlder,
|
||||
hasNewer: input.timeline.hasNewer,
|
||||
};
|
||||
}
|
||||
|
||||
private selectProjectedTimelineProjection(input: {
|
||||
agentId: string;
|
||||
controlTimeline: AgentTimelineFetchResult;
|
||||
direction: AgentTimelineFetchDirection;
|
||||
cursor?: AgentTimelineCursor;
|
||||
pageLimit: number;
|
||||
}): AgentTimelineProjectionSelection {
|
||||
const timeline = this.shouldUseFullTimelineForProjectedPage({
|
||||
timeline: input.controlTimeline,
|
||||
})
|
||||
? this.agentManager.fetchTimeline(input.agentId, { direction: "tail", limit: 0 })
|
||||
: input.controlTimeline;
|
||||
const page = selectProjectedTimelinePage({
|
||||
rows: timeline.rows,
|
||||
bounds: timeline.window,
|
||||
direction: input.controlTimeline.reset ? "tail" : input.direction,
|
||||
...(input.cursor ? { cursorSeq: input.cursor.seq } : {}),
|
||||
limit: input.pageLimit,
|
||||
});
|
||||
|
||||
return {
|
||||
timeline,
|
||||
entries: page.entries,
|
||||
startSeq: page.startSeq,
|
||||
endSeq: page.endSeq,
|
||||
hasOlder: page.hasOlder || (page.startSeq !== null && page.startSeq > timeline.window.minSeq),
|
||||
hasNewer: page.hasNewer,
|
||||
};
|
||||
}
|
||||
|
||||
private selectTimelineProjection(input: {
|
||||
agentId: string;
|
||||
projection: TimelineProjectionMode;
|
||||
controlTimeline: AgentTimelineFetchResult;
|
||||
direction: AgentTimelineFetchDirection;
|
||||
cursor?: AgentTimelineCursor;
|
||||
pageLimit: number;
|
||||
}): AgentTimelineProjectionSelection {
|
||||
if (input.projection === "canonical") {
|
||||
return this.selectCanonicalTimelineProjection({ timeline: input.controlTimeline });
|
||||
}
|
||||
|
||||
return this.selectProjectedTimelineProjection(input);
|
||||
}
|
||||
|
||||
private async handleFetchAgentTimelineRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>,
|
||||
): Promise<void> {
|
||||
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
|
||||
const projection: TimelineProjectionMode = "projected";
|
||||
const projection: TimelineProjectionMode = msg.projection ?? "projected";
|
||||
const requestedLimit = msg.limit;
|
||||
const pageLimit = requestedLimit ?? (direction === "after" ? 0 : 200);
|
||||
const cursor: AgentTimelineCursor | undefined = msg.cursor
|
||||
@@ -7464,24 +7534,22 @@ export class Session {
|
||||
cursor,
|
||||
limit: pageLimit,
|
||||
});
|
||||
const timeline = this.shouldUseFullTimelineForProjectedPage({
|
||||
timeline: controlTimeline,
|
||||
})
|
||||
? this.agentManager.fetchTimeline(msg.agentId, { direction: "tail", limit: 0 })
|
||||
: controlTimeline;
|
||||
const projectedPage = selectProjectedTimelinePage({
|
||||
rows: timeline.rows,
|
||||
bounds: timeline.window,
|
||||
direction: controlTimeline.reset ? "tail" : direction,
|
||||
...(cursor ? { cursorSeq: cursor.seq } : {}),
|
||||
limit: pageLimit,
|
||||
const selectedTimeline = this.selectTimelineProjection({
|
||||
agentId: msg.agentId,
|
||||
projection,
|
||||
controlTimeline,
|
||||
direction,
|
||||
...(cursor ? { cursor } : {}),
|
||||
pageLimit,
|
||||
});
|
||||
const startCursor =
|
||||
projectedPage.startSeq !== null
|
||||
? { epoch: timeline.epoch, seq: projectedPage.startSeq }
|
||||
selectedTimeline.startSeq !== null
|
||||
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.startSeq }
|
||||
: null;
|
||||
const endCursor =
|
||||
projectedPage.endSeq !== null ? { epoch: timeline.epoch, seq: projectedPage.endSeq } : null;
|
||||
selectedTimeline.endSeq !== null
|
||||
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.endSeq }
|
||||
: null;
|
||||
|
||||
this.emit({
|
||||
type: "fetch_agent_timeline_response",
|
||||
@@ -7491,18 +7559,16 @@ export class Session {
|
||||
agent: agentPayload,
|
||||
direction,
|
||||
projection,
|
||||
epoch: timeline.epoch,
|
||||
epoch: selectedTimeline.timeline.epoch,
|
||||
reset: controlTimeline.reset,
|
||||
staleCursor: controlTimeline.staleCursor,
|
||||
gap: controlTimeline.gap,
|
||||
window: timeline.window,
|
||||
window: selectedTimeline.timeline.window,
|
||||
startCursor,
|
||||
endCursor,
|
||||
hasOlder:
|
||||
projectedPage.hasOlder ||
|
||||
(projectedPage.startSeq !== null && projectedPage.startSeq > timeline.window.minSeq),
|
||||
hasNewer: projectedPage.hasNewer,
|
||||
entries: projectedPage.entries.map((entry) => ({
|
||||
hasOlder: selectedTimeline.hasOlder,
|
||||
hasNewer: selectedTimeline.hasNewer,
|
||||
entries: selectedTimeline.entries.map((entry) => ({
|
||||
provider: snapshot.provider,
|
||||
item: entry.item,
|
||||
timestamp: entry.timestamp,
|
||||
|
||||
Reference in New Issue
Block a user