fix: sync dev branch with main timeline/schema changes and remove stale providers

Forward-port main's timeline epoch tracking, projected window selection,
and schema updates into dev. Remove aider/amp/gemini providers and
provider-history-compatibility layers that are no longer needed.
This commit is contained in:
Mohamed Boudra
2026-04-15 10:52:28 +07:00
parent 326dad93e4
commit 9d30b7e85a
42 changed files with 1251 additions and 2051 deletions
@@ -9,9 +9,13 @@ import {
type TimelineCursor,
} from "./session-stream-reducers";
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
function makeTimelineEntry(seq: number, text: string, type: string = "assistant_message") {
return {
seq,
seqStart: seq,
provider: "claude",
item: { type, text },
timestamp: new Date(1000 + seq).toISOString(),
@@ -29,30 +33,22 @@ function makeTimelineEvent(
} as AgentStreamEventPayload;
}
function makeToolCallEvent(status: "running" | "completed"): AgentStreamEventPayload {
function makeUserTimelineEvent(text: string): AgentStreamEventPayload {
return {
type: "timeline",
provider: "claude",
item: {
type: "tool_call",
callId: "call-1",
name: "shell",
status,
detail: {
type: "shell",
command: "pwd",
},
error: null,
},
};
item: { type: "user_message", text },
} as AgentStreamEventPayload;
}
const baseTimelineInput: ProcessTimelineResponseInput = {
payload: {
agentId: "agent-1",
direction: "after",
startSeq: null,
endSeq: null,
reset: false,
epoch: "epoch-1",
startCursor: null,
endCursor: null,
entries: [],
error: null,
},
@@ -67,6 +63,7 @@ const baseTimelineInput: ProcessTimelineResponseInput = {
const baseStreamInput: ProcessAgentStreamEventInput = {
event: makeTimelineEvent("hello"),
seq: undefined,
epoch: undefined,
currentTail: [],
currentHead: [],
currentCursor: undefined,
@@ -74,6 +71,10 @@ const baseStreamInput: ProcessAgentStreamEventInput = {
timestamp: new Date(2000),
};
// ---------------------------------------------------------------------------
// processTimelineResponse
// ---------------------------------------------------------------------------
describe("processTimelineResponse", () => {
it("returns error path when payload.error is set", () => {
const result = processTimelineResponse({
@@ -92,10 +93,35 @@ describe("processTimelineResponse", () => {
expect(result.tail).toBe(baseTimelineInput.currentTail);
expect(result.head).toBe(baseTimelineInput.currentHead);
expect(result.cursorChanged).toBe(false);
expect(result.sideEffects).toEqual([]);
});
it("replaces tail during bootstrap tail init and schedules committed catch-up", () => {
const provisionalHead: StreamItem[] = [
it("returns error with no init resolution when no deferred exists", () => {
const result = processTimelineResponse({
...baseTimelineInput,
isInitializing: true,
hasActiveInitDeferred: false,
payload: {
...baseTimelineInput.payload,
error: "timeout",
},
});
expect(result.error).toBe("timeout");
expect(result.initResolution).toBe(null);
expect(result.clearInitializing).toBe(true);
});
it("replaces tail and clears head when reset=true", () => {
const existingTail: StreamItem[] = [
{
kind: "user_message",
id: "old",
text: "old message",
timestamp: new Date(500),
},
];
const existingHead: StreamItem[] = [
{
kind: "assistant_message",
id: "head-1",
@@ -106,325 +132,518 @@ describe("processTimelineResponse", () => {
const result = processTimelineResponse({
...baseTimelineInput,
currentHead: provisionalHead,
currentTail: existingTail,
currentHead: existingHead,
payload: {
...baseTimelineInput.payload,
reset: true,
startCursor: { seq: 1 },
endCursor: { seq: 3 },
entries: [
makeTimelineEntry(1, "first"),
makeTimelineEntry(2, "second"),
makeTimelineEntry(3, "third"),
],
},
});
expect(result.tail).not.toBe(existingTail);
expect(result.tail.length).toBeGreaterThan(0);
expect(result.head).toEqual([]);
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 3,
});
expect(result.error).toBe(null);
expect(result.sideEffects.some((e) => e.type === "flush_pending_updates")).toBe(true);
});
it("sets cursor to null when reset=true but no cursors in payload", () => {
const result = processTimelineResponse({
...baseTimelineInput,
currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 5 },
payload: {
...baseTimelineInput.payload,
reset: true,
entries: [],
},
});
expect(result.cursor).toBe(null);
expect(result.cursorChanged).toBe(true);
});
it("performs bootstrap tail init with catch-up side effect", () => {
const result = processTimelineResponse({
...baseTimelineInput,
isInitializing: true,
hasActiveInitDeferred: true,
initRequestDirection: "tail",
payload: {
...baseTimelineInput.payload,
direction: "tail",
startSeq: 1,
endSeq: 5,
epoch: "epoch-1",
startCursor: { seq: 1 },
endCursor: { seq: 5 },
entries: [makeTimelineEntry(1, "first"), makeTimelineEntry(5, "last")],
},
});
// Bootstrap tail replaces
expect(result.tail.length).toBeGreaterThan(0);
expect(result.head).toEqual([]);
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 5,
});
const catchUp = result.sideEffects.find((effect) => effect.type === "catch_up");
expect(catchUp).toEqual({
type: "catch_up",
cursor: { endSeq: 5 },
// Should have catch-up side effect
const catchUp = result.sideEffects.find((e) => e.type === "catch_up");
expect(catchUp).toBeDefined();
expect(catchUp!.type === "catch_up" && catchUp!.cursor).toEqual({
epoch: "epoch-1",
endSeq: 5,
});
});
it("prepends older committed history for before pagination", () => {
const currentTail: StreamItem[] = [
{
kind: "assistant_message",
id: "tail-3",
text: "newer",
timestamp: new Date(3000),
},
];
const currentCursor: TimelineCursor = { startSeq: 3, endSeq: 4 };
it("appends incrementally for contiguous seqs", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 3,
};
const result = processTimelineResponse({
...baseTimelineInput,
currentTail,
currentCursor,
currentCursor: existingCursor,
payload: {
...baseTimelineInput.payload,
direction: "before",
startSeq: 1,
endSeq: 2,
entries: [makeTimelineEntry(1, "hello", "user_message"), makeTimelineEntry(2, "older")],
epoch: "epoch-1",
entries: [makeTimelineEntry(4, "next-1"), makeTimelineEntry(5, "next-2")],
},
});
expect(result.tail.length).toBeGreaterThan(0);
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 4,
endSeq: 5,
});
expect(result.tail).toHaveLength(3);
expect(result.tail[0]?.kind).toBe("user_message");
expect(result.tail[1]?.kind).toBe("assistant_message");
expect(result.tail[2]).toBe(currentTail[0]);
expect(result.error).toBe(null);
});
it("replaces stale provisional assistant UI when fetch-after returns committed row 121", () => {
const currentHead: StreamItem[] = [
{
kind: "assistant_message",
id: "head-assistant",
text: "partial",
timestamp: new Date(120000),
},
];
const currentCursor: TimelineCursor = { startSeq: 1, endSeq: 120 };
it("detects gap and emits catch-up side effect", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 3,
};
const result = processTimelineResponse({
...baseTimelineInput,
currentHead,
currentCursor,
currentCursor: existingCursor,
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [makeTimelineEntry(10, "far ahead")],
},
});
// Gap should trigger catch-up
const catchUp = result.sideEffects.find((e) => e.type === "catch_up");
expect(catchUp).toBeDefined();
expect(catchUp!.type === "catch_up" && catchUp!.cursor).toEqual({
epoch: "epoch-1",
endSeq: 3,
});
});
it("drops stale entries silently", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 8,
};
const result = processTimelineResponse({
...baseTimelineInput,
currentCursor: existingCursor,
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [makeTimelineEntry(5, "old"), makeTimelineEntry(7, "also old")],
},
});
// No new items appended (all dropped as stale)
expect(result.tail).toBe(baseTimelineInput.currentTail);
expect(result.cursorChanged).toBe(false);
});
it("drops entries with epoch mismatch", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 5,
};
const result = processTimelineResponse({
...baseTimelineInput,
currentCursor: existingCursor,
payload: {
...baseTimelineInput.payload,
epoch: "epoch-2",
entries: [makeTimelineEntry(6, "different epoch")],
},
});
expect(result.tail).toBe(baseTimelineInput.currentTail);
expect(result.cursorChanged).toBe(false);
});
it("resolves init when deferred matches direction", () => {
const result = processTimelineResponse({
...baseTimelineInput,
isInitializing: true,
hasActiveInitDeferred: true,
initRequestDirection: "after",
payload: {
...baseTimelineInput.payload,
direction: "after",
startSeq: 121,
endSeq: 121,
entries: [makeTimelineEntry(121, "finalized reply")],
},
});
expect(result.head).toEqual([]);
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
startSeq: 1,
endSeq: 121,
});
expect(result.tail[result.tail.length - 1]).toMatchObject({
kind: "assistant_message",
text: "finalized reply",
});
});
it("keeps provisional head when reconnect catch-up has no new committed rows yet", () => {
const currentHead: StreamItem[] = [
{
kind: "assistant_message",
id: "head-assistant",
text: "still streaming",
timestamp: new Date(120000),
},
];
const currentCursor: TimelineCursor = { startSeq: 1, endSeq: 120 };
const result = processTimelineResponse({
...baseTimelineInput,
currentHead,
currentCursor,
payload: {
...baseTimelineInput.payload,
direction: "after",
startSeq: null,
endSeq: null,
entries: [],
},
});
expect(result.head).toBe(currentHead);
expect(result.cursorChanged).toBe(false);
expect(result.tail).toBe(baseTimelineInput.currentTail);
expect(result.initResolution).toBe("resolve");
expect(result.clearInitializing).toBe(true);
});
it("requests catch-up when committed rows arrive with a forward gap", () => {
const currentCursor: TimelineCursor = { startSeq: 1, endSeq: 120 };
it("does not resolve init when directions differ (before vs after)", () => {
const result = processTimelineResponse({
...baseTimelineInput,
currentCursor,
isInitializing: true,
hasActiveInitDeferred: true,
initRequestDirection: "after",
payload: {
...baseTimelineInput.payload,
direction: "before",
entries: [],
},
});
// "before" direction doesn't match "after" initRequestDirection,
// and "before" is not a bootstrap tail path, so init should NOT resolve
expect(result.initResolution).toBe(null);
expect(result.clearInitializing).toBe(false);
});
it("clears initializing even without deferred", () => {
const result = processTimelineResponse({
...baseTimelineInput,
isInitializing: true,
hasActiveInitDeferred: false,
payload: {
...baseTimelineInput.payload,
direction: "after",
startSeq: 125,
endSeq: 125,
entries: [makeTimelineEntry(125, "far ahead")],
entries: [],
},
});
expect(result.cursorChanged).toBe(false);
expect(result.tail).toBe(baseTimelineInput.currentTail);
expect(result.sideEffects).toContainEqual({
type: "catch_up",
cursor: { endSeq: 120 },
expect(result.clearInitializing).toBe(true);
expect(result.initResolution).toBe(null);
});
it("always includes flush_pending_updates side effect on success", () => {
const result = processTimelineResponse({
...baseTimelineInput,
payload: {
...baseTimelineInput.payload,
entries: [],
},
});
expect(result.sideEffects.some((e) => e.type === "flush_pending_updates")).toBe(true);
});
it("initializes cursor when no existing cursor on first entries", () => {
const result = processTimelineResponse({
...baseTimelineInput,
currentCursor: undefined,
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [makeTimelineEntry(1, "first"), makeTimelineEntry(2, "second")],
},
});
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 2,
});
});
});
// ---------------------------------------------------------------------------
// processAgentStreamEvent
// ---------------------------------------------------------------------------
describe("processAgentStreamEvent", () => {
it("treats seq-less timeline events as provisional head updates", () => {
it("passes through non-timeline events without cursor changes", () => {
const turnEvent: AgentStreamEventPayload = {
type: "turn_completed",
provider: "claude",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("partial"),
event: turnEvent,
seq: undefined,
epoch: undefined,
});
expect(result.changedHead).toBe(true);
expect(result.changedTail).toBe(false);
expect(result.head).toHaveLength(1);
expect(result.head[0]).toMatchObject({
kind: "assistant_message",
text: "partial",
});
expect(result.cursorChanged).toBe(false);
expect(result.cursor).toBe(null);
expect(result.sideEffects).toEqual([]);
});
it("keeps tool call rows anchored in tail across live and committed updates", () => {
const running = processAgentStreamEvent({
it("accepts timeline event with cursor advance", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 4,
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("running"),
seq: undefined,
});
expect(running.head).toEqual([]);
expect(running.tail).toHaveLength(1);
expect(running.tail[0]).toMatchObject({
kind: "tool_call",
payload: {
source: "agent",
data: {
callId: "call-1",
status: "running",
},
},
event: makeTimelineEvent("new chunk"),
seq: 5,
epoch: "epoch-1",
currentCursor: existingCursor,
});
const completedLive = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: undefined,
currentHead: running.head,
currentTail: running.tail,
currentCursor: { startSeq: 1, endSeq: 7 },
});
expect(completedLive.head).toEqual([]);
expect(completedLive.tail).toHaveLength(1);
expect(completedLive.tail[0]).toMatchObject({
kind: "tool_call",
payload: {
source: "agent",
data: {
callId: "call-1",
status: "completed",
},
},
});
const committed = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: 8,
currentHead: completedLive.head,
currentTail: completedLive.tail,
currentCursor: { startSeq: 1, endSeq: 7 },
});
expect(committed.head).toEqual([]);
expect(committed.tail).toHaveLength(1);
expect(committed.tail[0]).toMatchObject({
kind: "tool_call",
payload: {
source: "agent",
data: {
callId: "call-1",
status: "completed",
},
},
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 5,
});
expect(result.sideEffects).toEqual([]);
});
it("preserves assistant/tool interleaving while a turn is streaming", () => {
const assistantBeforeTool = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("before"),
seq: undefined,
});
it("detects gap and emits catch-up side effect", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 4,
};
const runningTool = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("running"),
seq: undefined,
currentHead: assistantBeforeTool.head,
currentTail: assistantBeforeTool.tail,
});
const assistantAfterTool = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("after"),
seq: undefined,
currentHead: runningTool.head,
currentTail: runningTool.tail,
});
const completedTool = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: undefined,
currentHead: assistantAfterTool.head,
currentTail: assistantAfterTool.tail,
});
expect(completedTool.head).toEqual([]);
expect(completedTool.tail.map((item) => item.kind)).toEqual([
"assistant_message",
"tool_call",
"assistant_message",
]);
expect(
completedTool.tail[0]?.kind === "assistant_message" ? completedTool.tail[0].text : null,
).toBe("before");
expect(
completedTool.tail[2]?.kind === "assistant_message" ? completedTool.tail[2].text : null,
).toBe("after");
});
it("requests catch-up when a committed live row skips ahead", () => {
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("far ahead"),
seq: 125,
currentCursor: { startSeq: 1, endSeq: 120 },
seq: 10,
epoch: "epoch-1",
currentCursor: existingCursor,
});
expect(result.cursorChanged).toBe(false);
expect(result.changedTail).toBe(false);
expect(result.changedHead).toBe(false);
expect(result.cursorChanged).toBe(false);
expect(result.sideEffects).toContainEqual({
type: "catch_up",
cursor: { endSeq: 120 },
const catchUp = result.sideEffects.find((e) => e.type === "catch_up");
expect(catchUp).toBeDefined();
expect(catchUp!.cursor).toEqual({
epoch: "epoch-1",
endSeq: 4,
});
});
it("flushes provisional head into tail on terminal turn events", () => {
const withHead = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("streaming"),
seq: undefined,
});
it("drops stale timeline event", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 8,
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: {
type: "turn_completed",
provider: "claude",
},
currentHead: withHead.head,
currentTail: withHead.tail,
event: makeTimelineEvent("old"),
seq: 5,
epoch: "epoch-1",
currentCursor: existingCursor,
});
expect(result.changedHead).toBe(true);
expect(result.changedTail).toBe(true);
expect(result.head).toEqual([]);
expect(result.tail).toHaveLength(1);
expect(result.tail[0]).toMatchObject({
kind: "assistant_message",
text: "streaming",
expect(result.cursorChanged).toBe(false);
expect(result.changedTail).toBe(false);
expect(result.changedHead).toBe(false);
expect(result.sideEffects).toEqual([]);
});
it("drops timeline event with epoch mismatch", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 5,
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("wrong epoch"),
seq: 6,
epoch: "epoch-2",
currentCursor: existingCursor,
});
expect(result.cursorChanged).toBe(false);
expect(result.changedTail).toBe(false);
expect(result.changedHead).toBe(false);
expect(result.sideEffects).toEqual([]);
});
it("initializes cursor when none exists", () => {
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("first"),
seq: 1,
epoch: "epoch-1",
currentCursor: undefined,
});
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 1,
});
});
it("derives optimistic idle status on turn_completed for running agent", () => {
const turnCompletedEvent: AgentStreamEventPayload = {
type: "turn_completed",
provider: "claude",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: turnCompletedEvent,
currentAgent: {
status: "running",
updatedAt: new Date(1000),
lastActivityAt: new Date(1000),
},
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(true);
expect(result.agent).not.toBe(null);
expect(result.agent!.status).toBe("idle");
expect(result.agent!.updatedAt.getTime()).toBe(2000);
expect(result.agent!.lastActivityAt.getTime()).toBe(2000);
});
it("derives optimistic error status on turn_failed for running agent", () => {
const turnFailedEvent: AgentStreamEventPayload = {
type: "turn_failed",
provider: "claude",
error: "something broke",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: turnFailedEvent,
currentAgent: {
status: "running",
updatedAt: new Date(1000),
lastActivityAt: new Date(1000),
},
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(true);
expect(result.agent!.status).toBe("error");
});
it("does not change agent when status is not running", () => {
const turnCompletedEvent: AgentStreamEventPayload = {
type: "turn_completed",
provider: "claude",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: turnCompletedEvent,
currentAgent: {
status: "idle",
updatedAt: new Date(1000),
lastActivityAt: new Date(1000),
},
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(false);
expect(result.agent).toBe(null);
});
it("does not change agent when no agent is provided", () => {
const turnCompletedEvent: AgentStreamEventPayload = {
type: "turn_completed",
provider: "claude",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: turnCompletedEvent,
currentAgent: null,
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(false);
expect(result.agent).toBe(null);
});
it("preserves updatedAt when agent timestamp is newer than event", () => {
const turnCompletedEvent: AgentStreamEventPayload = {
type: "turn_completed",
provider: "claude",
};
const result = processAgentStreamEvent({
...baseStreamInput,
event: turnCompletedEvent,
currentAgent: {
status: "running",
updatedAt: new Date(5000),
lastActivityAt: new Date(5000),
},
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(true);
expect(result.agent!.updatedAt.getTime()).toBe(5000);
expect(result.agent!.lastActivityAt.getTime()).toBe(5000);
});
it("does not produce agent patch for non-terminal events", () => {
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("just text"),
currentAgent: {
status: "running",
updatedAt: new Date(1000),
lastActivityAt: new Date(1000),
},
seq: 1,
epoch: "epoch-1",
timestamp: new Date(2000),
});
expect(result.agentChanged).toBe(false);
expect(result.agent).toBe(null);
});
});