mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(chat): tighten fork boundary handling
This commit is contained in:
@@ -83,4 +83,47 @@ describe("resolveAssistantTurnForkBoundary", () => {
|
||||
boundaryCursor: { epoch: "timeline-1", seq: 42 },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes the provider message id with a supported timeline cursor", () => {
|
||||
const selected = {
|
||||
...assistantMessage("assistant-1", 2, "msg-assistant-1"),
|
||||
timelineCursor: { epoch: "timeline-1", seq: 42 },
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveAssistantTurnForkBoundary({
|
||||
items: [selected],
|
||||
startIndex: 0,
|
||||
supportsTimelineCursor: true,
|
||||
}),
|
||||
).toEqual({
|
||||
boundaryCursor: { epoch: "timeline-1", seq: 42 },
|
||||
boundaryMessageId: "msg-assistant-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the provider message id when timeline cursors are unsupported", () => {
|
||||
const selected = {
|
||||
...assistantMessage("assistant-1", 2, "msg-assistant-1"),
|
||||
timelineCursor: { epoch: "timeline-1", seq: 42 },
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveAssistantTurnForkBoundary({
|
||||
items: [selected],
|
||||
startIndex: 0,
|
||||
supportsTimelineCursor: false,
|
||||
}),
|
||||
).toEqual({ boundaryMessageId: "msg-assistant-1" });
|
||||
});
|
||||
|
||||
it("does not offer an unavailable boundary", () => {
|
||||
expect(
|
||||
resolveAssistantTurnForkBoundary({
|
||||
items: [assistantMessage("assistant-1", 2)],
|
||||
startIndex: 0,
|
||||
supportsTimelineCursor: false,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -398,8 +398,10 @@ second line'`,
|
||||
|
||||
it("selects a synthetic assistant error by its timeline cursor", () => {
|
||||
const result = buildAgentForkContextAttachment({
|
||||
timelineEpoch: "timeline-1",
|
||||
boundaryCursor: { epoch: "timeline-1", seq: 2 },
|
||||
cursorBoundary: {
|
||||
timelineEpoch: "timeline-1",
|
||||
cursor: { epoch: "timeline-1", seq: 2 },
|
||||
},
|
||||
rows: [
|
||||
row(1, { type: "user_message", text: "Try the task", messageId: "user-1" }),
|
||||
row(2, { type: "assistant_message", text: "[System Error] provider failed" }),
|
||||
@@ -417,6 +419,18 @@ second line'`,
|
||||
expect(result.attachment.text).not.toContain("This belongs to a later turn.");
|
||||
});
|
||||
|
||||
it("rejects a cursor from a previous timeline epoch", () => {
|
||||
expect(() =>
|
||||
buildAgentForkContextAttachment({
|
||||
cursorBoundary: {
|
||||
timelineEpoch: "timeline-2",
|
||||
cursor: { epoch: "timeline-1", seq: 2 },
|
||||
},
|
||||
rows: [row(2, { type: "assistant_message", text: "Stale result." })],
|
||||
}),
|
||||
).toThrow("Selected timeline position is no longer available.");
|
||||
});
|
||||
|
||||
it("rejects missing assistant boundaries instead of silently using the wrong context", () => {
|
||||
expect(() =>
|
||||
buildAgentForkContextAttachment({
|
||||
|
||||
@@ -216,17 +216,21 @@ export function curateAgentActivity(
|
||||
: "No activity to display.";
|
||||
}
|
||||
|
||||
interface ForkCursorBoundary {
|
||||
timelineEpoch: string;
|
||||
cursor: { epoch: string; seq: number };
|
||||
}
|
||||
|
||||
function selectForkContextRows(input: {
|
||||
rows: readonly AgentTimelineRow[];
|
||||
timelineEpoch?: string;
|
||||
boundaryCursor?: { epoch: string; seq: number } | null;
|
||||
cursorBoundary?: ForkCursorBoundary | null;
|
||||
boundaryMessageId?: string | null;
|
||||
}): {
|
||||
items: AgentTimelineItem[];
|
||||
boundaryCursor: { epoch: string; seq: number } | null;
|
||||
boundaryMessageId: string | null;
|
||||
} {
|
||||
const boundaryCursor = input.boundaryCursor ?? null;
|
||||
const boundaryCursor = input.cursorBoundary?.cursor ?? null;
|
||||
const boundaryMessageId = input.boundaryMessageId?.trim() || null;
|
||||
if (!boundaryCursor && !boundaryMessageId) {
|
||||
const projected = projectTimelineRows({ rows: input.rows, mode: "projected" });
|
||||
@@ -237,7 +241,10 @@ function selectForkContextRows(input: {
|
||||
};
|
||||
}
|
||||
|
||||
if (boundaryCursor && boundaryCursor.epoch !== input.timelineEpoch) {
|
||||
if (
|
||||
input.cursorBoundary &&
|
||||
input.cursorBoundary.cursor.epoch !== input.cursorBoundary.timelineEpoch
|
||||
) {
|
||||
throw new Error("Selected timeline position is no longer available.");
|
||||
}
|
||||
const boundaryIndex = boundaryCursor
|
||||
@@ -286,8 +293,7 @@ function buildForkContextText(input: {
|
||||
|
||||
export function buildAgentForkContextAttachment(input: {
|
||||
rows: readonly AgentTimelineRow[];
|
||||
timelineEpoch?: string;
|
||||
boundaryCursor?: { epoch: string; seq: number } | null;
|
||||
cursorBoundary?: ForkCursorBoundary | null;
|
||||
boundaryMessageId?: string | null;
|
||||
agentTitle?: string | null;
|
||||
cwd?: string | null;
|
||||
@@ -299,8 +305,7 @@ export function buildAgentForkContextAttachment(input: {
|
||||
} {
|
||||
const selected = selectForkContextRows({
|
||||
rows: input.rows,
|
||||
timelineEpoch: input.timelineEpoch,
|
||||
boundaryCursor: input.boundaryCursor,
|
||||
cursorBoundary: input.cursorBoundary,
|
||||
boundaryMessageId: input.boundaryMessageId,
|
||||
});
|
||||
const entries = curateProjectedActivityEntries(selected.items, {
|
||||
|
||||
@@ -5480,8 +5480,9 @@ export class Session {
|
||||
});
|
||||
const forkContext = buildAgentForkContextAttachment({
|
||||
rows: timeline.rows,
|
||||
timelineEpoch: timeline.epoch,
|
||||
boundaryCursor: msg.boundaryCursor,
|
||||
cursorBoundary: msg.boundaryCursor
|
||||
? { timelineEpoch: timeline.epoch, cursor: msg.boundaryCursor }
|
||||
: null,
|
||||
boundaryMessageId: msg.boundaryMessageId,
|
||||
agentTitle: agentPayload.title,
|
||||
cwd: snapshot.cwd,
|
||||
|
||||
Reference in New Issue
Block a user