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 interface text size**
|
||||||
- **Adjust the code text size**
|
- **Adjust the code text size**
|
||||||
- **Choose a syntax highlighting theme**
|
- **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
|
### Improved
|
||||||
|
|
||||||
- Settings now has a flatter sidebar with a host picker
|
- Settings now has a flatter sidebar with a host picker
|
||||||
- Workspace tab switching is faster
|
- 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
|
- 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
|
- Large generated diffs now show a clear too-large placeholder instead of trying to render the whole file
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
- Terminal panes keep the right size after splitting or resizing panes
|
- Terminal panes keep the right size after splitting or resizing panes
|
||||||
- Restored terminal snapshots reflow correctly after the pane size changes
|
- Restored terminal snapshots reflow correctly after the pane size changes
|
||||||
- Workspace scripts menus keep the right size after launching a service
|
- 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
|
## 0.1.87 - 2026-05-30
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,42 @@ function tmpCwd(): string {
|
|||||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
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", () => {
|
describe("daemon E2E - timeline window", () => {
|
||||||
let ctx: DaemonTestContext;
|
let ctx: DaemonTestContext;
|
||||||
|
|
||||||
@@ -124,7 +160,7 @@ describe("daemon E2E - timeline window", () => {
|
|||||||
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||||
direction: "tail",
|
direction: "tail",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
projection: "canonical",
|
projection: "projected",
|
||||||
});
|
});
|
||||||
|
|
||||||
const toolEntries = timeline.entries.filter((entry) => entry.item.type === "tool_call");
|
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();
|
const cwd = tmpCwd();
|
||||||
try {
|
try {
|
||||||
const agent = await ctx.client.createAgent({
|
const agent = await ctx.client.createAgent({
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd,
|
cwd,
|
||||||
title: "Timeline Tool Catch-up Test",
|
title: "Timeline Canonical Catch-up Test",
|
||||||
modeId: "full-access",
|
modeId: "full-access",
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.daemon.daemon.agentManager.appendTimelineItem(agent.id, {
|
await appendToolLifecycleAcrossCursor(ctx, 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" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||||
direction: "tail",
|
direction: "tail",
|
||||||
@@ -194,6 +201,43 @@ describe("daemon E2E - timeline window", () => {
|
|||||||
projection: "canonical",
|
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.entries).toHaveLength(1);
|
||||||
expect(timeline.startCursor?.seq).toBe(250);
|
expect(timeline.startCursor?.seq).toBe(250);
|
||||||
expect(timeline.endCursor?.seq).toBe(250);
|
expect(timeline.endCursor?.seq).toBe(250);
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ import {
|
|||||||
emitLiveTimelineItemIfAgentKnown,
|
emitLiveTimelineItemIfAgentKnown,
|
||||||
} from "./agent/timeline-append.js";
|
} from "./agent/timeline-append.js";
|
||||||
import {
|
import {
|
||||||
|
projectTimelineRows,
|
||||||
selectProjectedTimelinePage,
|
selectProjectedTimelinePage,
|
||||||
|
type TimelineProjectionEntry,
|
||||||
type TimelineProjectionMode,
|
type TimelineProjectionMode,
|
||||||
} from "./agent/timeline-projection.js";
|
} from "./agent/timeline-projection.js";
|
||||||
import {
|
import {
|
||||||
@@ -716,6 +718,15 @@ function parseClientCapabilities(
|
|||||||
return new Set(result);
|
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.
|
* Session represents a single connected client session.
|
||||||
* It owns all state management, orchestration logic, and message processing.
|
* 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");
|
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(
|
private async handleFetchAgentTimelineRequest(
|
||||||
msg: Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
|
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
|
||||||
const projection: TimelineProjectionMode = "projected";
|
const projection: TimelineProjectionMode = msg.projection ?? "projected";
|
||||||
const requestedLimit = msg.limit;
|
const requestedLimit = msg.limit;
|
||||||
const pageLimit = requestedLimit ?? (direction === "after" ? 0 : 200);
|
const pageLimit = requestedLimit ?? (direction === "after" ? 0 : 200);
|
||||||
const cursor: AgentTimelineCursor | undefined = msg.cursor
|
const cursor: AgentTimelineCursor | undefined = msg.cursor
|
||||||
@@ -7464,24 +7534,22 @@ export class Session {
|
|||||||
cursor,
|
cursor,
|
||||||
limit: pageLimit,
|
limit: pageLimit,
|
||||||
});
|
});
|
||||||
const timeline = this.shouldUseFullTimelineForProjectedPage({
|
const selectedTimeline = this.selectTimelineProjection({
|
||||||
timeline: controlTimeline,
|
agentId: msg.agentId,
|
||||||
})
|
projection,
|
||||||
? this.agentManager.fetchTimeline(msg.agentId, { direction: "tail", limit: 0 })
|
controlTimeline,
|
||||||
: controlTimeline;
|
direction,
|
||||||
const projectedPage = selectProjectedTimelinePage({
|
...(cursor ? { cursor } : {}),
|
||||||
rows: timeline.rows,
|
pageLimit,
|
||||||
bounds: timeline.window,
|
|
||||||
direction: controlTimeline.reset ? "tail" : direction,
|
|
||||||
...(cursor ? { cursorSeq: cursor.seq } : {}),
|
|
||||||
limit: pageLimit,
|
|
||||||
});
|
});
|
||||||
const startCursor =
|
const startCursor =
|
||||||
projectedPage.startSeq !== null
|
selectedTimeline.startSeq !== null
|
||||||
? { epoch: timeline.epoch, seq: projectedPage.startSeq }
|
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.startSeq }
|
||||||
: null;
|
: null;
|
||||||
const endCursor =
|
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({
|
this.emit({
|
||||||
type: "fetch_agent_timeline_response",
|
type: "fetch_agent_timeline_response",
|
||||||
@@ -7491,18 +7559,16 @@ export class Session {
|
|||||||
agent: agentPayload,
|
agent: agentPayload,
|
||||||
direction,
|
direction,
|
||||||
projection,
|
projection,
|
||||||
epoch: timeline.epoch,
|
epoch: selectedTimeline.timeline.epoch,
|
||||||
reset: controlTimeline.reset,
|
reset: controlTimeline.reset,
|
||||||
staleCursor: controlTimeline.staleCursor,
|
staleCursor: controlTimeline.staleCursor,
|
||||||
gap: controlTimeline.gap,
|
gap: controlTimeline.gap,
|
||||||
window: timeline.window,
|
window: selectedTimeline.timeline.window,
|
||||||
startCursor,
|
startCursor,
|
||||||
endCursor,
|
endCursor,
|
||||||
hasOlder:
|
hasOlder: selectedTimeline.hasOlder,
|
||||||
projectedPage.hasOlder ||
|
hasNewer: selectedTimeline.hasNewer,
|
||||||
(projectedPage.startSeq !== null && projectedPage.startSeq > timeline.window.minSeq),
|
entries: selectedTimeline.entries.map((entry) => ({
|
||||||
hasNewer: projectedPage.hasNewer,
|
|
||||||
entries: projectedPage.entries.map((entry) => ({
|
|
||||||
provider: snapshot.provider,
|
provider: snapshot.provider,
|
||||||
item: entry.item,
|
item: entry.item,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
|
|||||||
Reference in New Issue
Block a user