Revert the timeline optimistic and pagination rework (#2596)
* Add npm release track invariant and beta dist-tag procedure * Revert "Keep older chat history and image previews stable (#2490)" The timeline optimistic/pagination rework needs more work than main releases can wait for. Reverted together with #2484; both are reapplied on integration/timeline-optimistic-rework. * Revert "Stop completed turns from appearing stuck (#2484)" See the #2490 revert. #2490 built on this submission-authority change, so the two revert and reapply as a pair.
This commit is contained in:
@@ -109,10 +109,7 @@ export const useCreateFlowStore = create<CreateFlowState>((set) => ({
|
||||
set((state) => {
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(state.pendingByDraftId).filter(
|
||||
([, pending]) =>
|
||||
pending.lifecycle !== "sent" ||
|
||||
pending.serverId !== serverId ||
|
||||
pending.agentId !== agentId,
|
||||
([, pending]) => pending.serverId !== serverId || pending.agentId !== agentId,
|
||||
),
|
||||
);
|
||||
if (Object.keys(next).length === Object.keys(state.pendingByDraftId).length) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
persistAttachmentFromDataUrl,
|
||||
persistAttachmentFromFileUri,
|
||||
} from "@/attachments/service";
|
||||
import { collectRetainedAttachmentIds } from "@/attachments/gc-retention";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { useSessionStore, type SessionState } from "@/stores/session-store";
|
||||
import { useWorkspaceAttachmentsStore } from "@/attachments/workspace-attachments-store";
|
||||
@@ -139,9 +138,6 @@ async function runAttachmentGc(): Promise<void> {
|
||||
for (const id of useDraftStore.getState().collectActiveAttachmentIds()) {
|
||||
referencedIds.add(id);
|
||||
}
|
||||
for (const id of collectRetainedAttachmentIds()) {
|
||||
referencedIds.add(id);
|
||||
}
|
||||
|
||||
const pendingByDraftId = useCreateFlowStore.getState().pendingByDraftId;
|
||||
for (const pendingCreate of Object.values(pendingByDraftId)) {
|
||||
|
||||
@@ -5,11 +5,9 @@ import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages";
|
||||
|
||||
import {
|
||||
normalizeWorkspaceDescriptor,
|
||||
selectAgentTimelineState,
|
||||
useSessionStore,
|
||||
type WorkspaceDescriptor,
|
||||
} from "./session-store";
|
||||
import type { StreamItem } from "../types/stream";
|
||||
import { patchWorkspaceScripts } from "../contexts/session-workspace-scripts";
|
||||
|
||||
function createWorkspace(
|
||||
@@ -55,54 +53,6 @@ function getTestSessionReferences() {
|
||||
};
|
||||
}
|
||||
|
||||
describe("agent timeline state", () => {
|
||||
it("commits canonical items, range, and older availability as one synced state", () => {
|
||||
initializeTestSession();
|
||||
const items: StreamItem[] = [
|
||||
{
|
||||
kind: "assistant_message",
|
||||
id: "canonical-row",
|
||||
text: "canonical",
|
||||
timestamp: new Date("2026-07-27T10:00:00.000Z"),
|
||||
},
|
||||
];
|
||||
|
||||
useSessionStore.getState().applyAgentTimelineResponseState("test-server", "agent-1", {
|
||||
items,
|
||||
head: [],
|
||||
range: { epoch: "epoch-1", startSeq: 51, endSeq: 100 },
|
||||
older: "available",
|
||||
synchronized: true,
|
||||
acknowledgedClientMessageIds: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectAgentTimelineState(useSessionStore.getState().sessions["test-server"], "agent-1"),
|
||||
).toEqual({
|
||||
status: "synced",
|
||||
items,
|
||||
range: { epoch: "epoch-1", startSeq: 51, endSeq: 100 },
|
||||
older: "available",
|
||||
});
|
||||
});
|
||||
|
||||
it("represents an empty authoritative timeline without inventing a range", () => {
|
||||
initializeTestSession();
|
||||
useSessionStore.getState().applyAgentTimelineResponseState("test-server", "agent-1", {
|
||||
items: [],
|
||||
head: [],
|
||||
range: null,
|
||||
older: "none",
|
||||
synchronized: true,
|
||||
acknowledgedClientMessageIds: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectAgentTimelineState(useSessionStore.getState().sessions["test-server"], "agent-1"),
|
||||
).toEqual({ status: "synced", items: [], range: null, older: "none" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeWorkspaceDescriptor", () => {
|
||||
it("normalizes workspace scripts and invalid activity timestamps", () => {
|
||||
const scripts = [
|
||||
|
||||
@@ -5,21 +5,10 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { ViewedTimelineUiBridge } from "@/timeline/viewed-timeline-sync";
|
||||
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||
import {
|
||||
appendSubmittedUserMessage,
|
||||
handoffCreatedAgentUserMessageToStream,
|
||||
removeSubmittedUserMessage,
|
||||
type StreamItem,
|
||||
type UserMessageItem,
|
||||
} from "@/types/stream";
|
||||
import {
|
||||
acceptMessageSubmission,
|
||||
beginMessageSubmission,
|
||||
observeAcceptedMessageSubmissionsRunning,
|
||||
observeMessageSubmissionCanonical,
|
||||
rejectMessageSubmission,
|
||||
type MessageSubmissionRecord,
|
||||
type MessageSubmissionRejectionOutcome,
|
||||
} from "@/composer/submission/model";
|
||||
import type { PendingPermission } from "@/types/shared";
|
||||
import type { ComposerAttachment } from "@/attachments/types";
|
||||
import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle";
|
||||
@@ -336,6 +325,8 @@ export interface AgentTimelineCursorState {
|
||||
export interface SessionReplicaTimeline {
|
||||
agentId: string;
|
||||
items: StreamItem[];
|
||||
cursor: AgentTimelineCursorState | null;
|
||||
hasOlder: boolean;
|
||||
}
|
||||
|
||||
export interface SessionReplica {
|
||||
@@ -345,33 +336,6 @@ export interface SessionReplica {
|
||||
timeline: SessionReplicaTimeline | null;
|
||||
}
|
||||
|
||||
export type AgentTimelineState =
|
||||
| { status: "cold" }
|
||||
| { status: "painted"; items: StreamItem[] }
|
||||
| {
|
||||
status: "synced";
|
||||
items: StreamItem[];
|
||||
range: AgentTimelineCursorState | null;
|
||||
older: "available" | "none";
|
||||
};
|
||||
|
||||
export function selectAgentTimelineState(
|
||||
session: SessionState | undefined,
|
||||
agentId: string,
|
||||
): AgentTimelineState {
|
||||
if (!session) return { status: "cold" };
|
||||
const items = session.agentStreamTail.get(agentId) ?? [];
|
||||
if (session.agentAuthoritativeHistoryApplied.get(agentId) === true) {
|
||||
return {
|
||||
status: "synced",
|
||||
items,
|
||||
range: session.agentTimelineCursor.get(agentId) ?? null,
|
||||
older: session.agentTimelineHasOlder.get(agentId) === true ? "available" : "none",
|
||||
};
|
||||
}
|
||||
return items.length > 0 ? { status: "painted", items } : { status: "cold" };
|
||||
}
|
||||
|
||||
export type WorkspaceRestoreStatus = "restoring" | "failed" | "needs-host-upgrade";
|
||||
|
||||
// Per-session state
|
||||
@@ -404,7 +368,6 @@ export interface SessionState {
|
||||
// Stream state (head/tail model)
|
||||
agentStreamTail: Map<string, StreamItem[]>;
|
||||
agentStreamHead: Map<string, StreamItem[]>;
|
||||
messageSubmissions: Map<string, MessageSubmissionRecord[]>;
|
||||
agentTimelineCursor: Map<string, AgentTimelineCursorState>;
|
||||
agentTimelineHasOlder: Map<string, boolean>;
|
||||
agentTimelineOlderFetchInFlight: Map<string, boolean>;
|
||||
@@ -496,28 +459,8 @@ interface SessionStoreActions {
|
||||
setAgentStreamState: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
state: {
|
||||
tail?: StreamItem[];
|
||||
head?: StreamItem[];
|
||||
acknowledgedClientMessageIds?: readonly string[];
|
||||
},
|
||||
state: { tail?: StreamItem[]; head?: StreamItem[] },
|
||||
) => void;
|
||||
beginAgentMessageSubmission: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
message: UserMessageItem,
|
||||
) => void;
|
||||
acceptAgentMessageSubmission: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
clientMessageId: string,
|
||||
outOfBand: boolean | undefined,
|
||||
) => void;
|
||||
rejectAgentMessageSubmission: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
clientMessageId: string,
|
||||
) => MessageSubmissionRejectionOutcome;
|
||||
handoffCreatedAgentUserMessage: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
@@ -545,18 +488,6 @@ interface SessionStoreActions {
|
||||
agentId: string,
|
||||
applied: boolean,
|
||||
) => void;
|
||||
applyAgentTimelineResponseState: (
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
state: {
|
||||
items: StreamItem[];
|
||||
head: StreamItem[];
|
||||
range: AgentTimelineCursorState | null;
|
||||
older: "available" | "none";
|
||||
synchronized: boolean;
|
||||
acknowledgedClientMessageIds: string[];
|
||||
},
|
||||
) => void;
|
||||
|
||||
// Initializing agents
|
||||
setInitializingAgents: (
|
||||
@@ -636,27 +567,6 @@ type SessionStore = SessionStoreState & SessionStoreActions;
|
||||
|
||||
const agentLastActivityCoalescer = createAgentLastActivityCoalescer();
|
||||
|
||||
function applyRunningAgentsToAcceptedSubmissions(input: {
|
||||
previousAgents: Map<string, Agent>;
|
||||
nextAgents: Map<string, Agent>;
|
||||
submissions: Map<string, MessageSubmissionRecord[]>;
|
||||
}): Map<string, MessageSubmissionRecord[]> {
|
||||
let nextSubmissions = input.submissions;
|
||||
for (const [agentId, submissions] of input.submissions) {
|
||||
const previousAgent = input.previousAgents.get(agentId);
|
||||
const nextAgent = input.nextAgents.get(agentId);
|
||||
if (!nextAgent || previousAgent?.status === "running" || nextAgent.status !== "running") {
|
||||
continue;
|
||||
}
|
||||
const remaining = observeAcceptedMessageSubmissionsRunning(submissions);
|
||||
if (remaining === submissions) continue;
|
||||
if (nextSubmissions === input.submissions) nextSubmissions = new Map(input.submissions);
|
||||
if (remaining.length > 0) nextSubmissions.set(agentId, remaining);
|
||||
else nextSubmissions.delete(agentId);
|
||||
}
|
||||
return nextSubmissions;
|
||||
}
|
||||
|
||||
// Helper to create initial session state
|
||||
function createInitialSessionState(
|
||||
serverId: string,
|
||||
@@ -678,7 +588,6 @@ function createInitialSessionState(
|
||||
currentAssistantMessage: "",
|
||||
agentStreamTail: new Map(),
|
||||
agentStreamHead: new Map(),
|
||||
messageSubmissions: new Map(),
|
||||
agentTimelineCursor: new Map(),
|
||||
agentTimelineHasOlder: new Map(),
|
||||
agentTimelineOlderFetchInFlight: new Map(),
|
||||
@@ -795,8 +704,16 @@ export const useSessionStore = create<SessionStore>()(
|
||||
const session = createInitialSessionState(serverId, null);
|
||||
const timeline = replica.timeline;
|
||||
const agentStreamTail = new Map<string, StreamItem[]>();
|
||||
const agentTimelineCursor = new Map<string, AgentTimelineCursorState>();
|
||||
const agentTimelineHasOlder = new Map<string, boolean>();
|
||||
const agentAuthoritativeHistoryApplied = new Map<string, boolean>();
|
||||
const agentHistorySyncGeneration = new Map<string, number>();
|
||||
if (timeline) {
|
||||
agentStreamTail.set(timeline.agentId, timeline.items);
|
||||
agentTimelineHasOlder.set(timeline.agentId, timeline.hasOlder);
|
||||
agentAuthoritativeHistoryApplied.set(timeline.agentId, true);
|
||||
agentHistorySyncGeneration.set(timeline.agentId, session.historySyncGeneration);
|
||||
if (timeline.cursor) agentTimelineCursor.set(timeline.agentId, timeline.cursor);
|
||||
}
|
||||
const agentLastActivity = new Map(prev.agentLastActivity);
|
||||
for (const agent of replica.agents.values()) {
|
||||
@@ -813,6 +730,10 @@ export const useSessionStore = create<SessionStore>()(
|
||||
workspaces: replica.workspaces,
|
||||
emptyProjects: replica.emptyProjects,
|
||||
agentStreamTail,
|
||||
agentTimelineCursor,
|
||||
agentTimelineHasOlder,
|
||||
agentAuthoritativeHistoryApplied,
|
||||
agentHistorySyncGeneration,
|
||||
},
|
||||
},
|
||||
agentLastActivity,
|
||||
@@ -1126,27 +1047,10 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
const currentSubmissions = session.messageSubmissions.get(agentId) ?? [];
|
||||
const observedSubmissions = observeMessageSubmissionCanonical(
|
||||
currentSubmissions,
|
||||
state.acknowledgedClientMessageIds ?? [],
|
||||
);
|
||||
const changedSubmissions = observedSubmissions !== currentSubmissions;
|
||||
|
||||
if (!changedTail && !changedHead && !changedSubmissions) {
|
||||
if (!changedTail && !changedHead) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
let messageSubmissions = session.messageSubmissions;
|
||||
if (changedSubmissions) {
|
||||
messageSubmissions = new Map(session.messageSubmissions);
|
||||
if (observedSubmissions.length > 0) {
|
||||
messageSubmissions.set(agentId, observedSubmissions);
|
||||
} else {
|
||||
messageSubmissions.delete(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1155,129 +1059,12 @@ export const useSessionStore = create<SessionStore>()(
|
||||
...session,
|
||||
agentStreamTail: nextTail,
|
||||
agentStreamHead: nextHead,
|
||||
messageSubmissions,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
beginAgentMessageSubmission: (serverId, agentId, message) => {
|
||||
set((prev) => {
|
||||
const session = prev.sessions[serverId];
|
||||
if (!session) return prev;
|
||||
if (!message.clientMessageId) {
|
||||
throw new Error("Beginning a message submission requires client identity");
|
||||
}
|
||||
const currentTail = session.agentStreamTail.get(agentId) ?? [];
|
||||
const currentHead = session.agentStreamHead.get(agentId) ?? [];
|
||||
const stream = appendSubmittedUserMessage({
|
||||
tail: currentTail,
|
||||
head: currentHead,
|
||||
message,
|
||||
});
|
||||
const submissions = beginMessageSubmission(
|
||||
session.messageSubmissions.get(agentId) ?? [],
|
||||
{ clientMessageId: message.clientMessageId, submittedAt: message.timestamp },
|
||||
);
|
||||
const messageSubmissions = new Map(session.messageSubmissions);
|
||||
messageSubmissions.set(agentId, submissions);
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
...prev.sessions,
|
||||
[serverId]: {
|
||||
...session,
|
||||
agentStreamTail:
|
||||
stream.tail === currentTail
|
||||
? session.agentStreamTail
|
||||
: new Map(session.agentStreamTail).set(agentId, stream.tail),
|
||||
agentStreamHead:
|
||||
stream.head === currentHead
|
||||
? session.agentStreamHead
|
||||
: new Map(session.agentStreamHead).set(agentId, stream.head),
|
||||
messageSubmissions,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
acceptAgentMessageSubmission: (serverId, agentId, clientMessageId, outOfBand) => {
|
||||
set((prev) => {
|
||||
const session = prev.sessions[serverId];
|
||||
if (!session) return prev;
|
||||
const currentSubmissions = session.messageSubmissions.get(agentId) ?? [];
|
||||
const submissions = acceptMessageSubmission(
|
||||
currentSubmissions,
|
||||
clientMessageId,
|
||||
session.agents.get(agentId)?.status === "running",
|
||||
outOfBand,
|
||||
);
|
||||
if (submissions === currentSubmissions) return prev;
|
||||
const messageSubmissions = new Map(session.messageSubmissions);
|
||||
if (submissions.length > 0) {
|
||||
messageSubmissions.set(agentId, submissions);
|
||||
} else {
|
||||
messageSubmissions.delete(agentId);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
...prev.sessions,
|
||||
[serverId]: { ...session, messageSubmissions },
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
rejectAgentMessageSubmission: (serverId, agentId, clientMessageId) => {
|
||||
let outcome: MessageSubmissionRejectionOutcome = "unknown";
|
||||
set((prev) => {
|
||||
const session = prev.sessions[serverId];
|
||||
if (!session) return prev;
|
||||
const currentTail = session.agentStreamTail.get(agentId) ?? [];
|
||||
const currentHead = session.agentStreamHead.get(agentId) ?? [];
|
||||
const currentSubmissions = session.messageSubmissions.get(agentId) ?? [];
|
||||
const result = rejectMessageSubmission(currentSubmissions, clientMessageId);
|
||||
outcome = result.outcome;
|
||||
if (outcome === "unknown") return prev;
|
||||
const stream =
|
||||
outcome === "rejected"
|
||||
? removeSubmittedUserMessage({
|
||||
tail: currentTail,
|
||||
head: currentHead,
|
||||
clientMessageId,
|
||||
})
|
||||
: { tail: currentTail, head: currentHead };
|
||||
const messageSubmissions = new Map(session.messageSubmissions);
|
||||
if (result.submissions.length > 0) {
|
||||
messageSubmissions.set(agentId, result.submissions);
|
||||
} else {
|
||||
messageSubmissions.delete(agentId);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
...prev.sessions,
|
||||
[serverId]: {
|
||||
...session,
|
||||
agentStreamTail:
|
||||
stream.tail === currentTail
|
||||
? session.agentStreamTail
|
||||
: new Map(session.agentStreamTail).set(agentId, stream.tail),
|
||||
agentStreamHead:
|
||||
stream.head === currentHead
|
||||
? session.agentStreamHead
|
||||
: new Map(session.agentStreamHead).set(agentId, stream.head),
|
||||
messageSubmissions,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
return outcome;
|
||||
},
|
||||
|
||||
handoffCreatedAgentUserMessage: (serverId, agentId, message) => {
|
||||
let didHandoff = false;
|
||||
set((prev) => {
|
||||
@@ -1482,61 +1269,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
applyAgentTimelineResponseState: (serverId, agentId, state) => {
|
||||
set((prev) => {
|
||||
const session = prev.sessions[serverId];
|
||||
if (!session) return prev;
|
||||
|
||||
const nextTail = new Map(session.agentStreamTail);
|
||||
nextTail.set(agentId, state.items);
|
||||
const nextHead = new Map(session.agentStreamHead);
|
||||
if (state.head.length > 0) nextHead.set(agentId, state.head);
|
||||
else nextHead.delete(agentId);
|
||||
const nextCursor = new Map(session.agentTimelineCursor);
|
||||
if (state.range) nextCursor.set(agentId, state.range);
|
||||
else nextCursor.delete(agentId);
|
||||
const nextHasOlder = new Map(session.agentTimelineHasOlder);
|
||||
nextHasOlder.set(agentId, state.older === "available");
|
||||
const nextAuthoritative = new Map(session.agentAuthoritativeHistoryApplied);
|
||||
const nextSyncGeneration = new Map(session.agentHistorySyncGeneration);
|
||||
const currentSubmissions = session.messageSubmissions.get(agentId) ?? [];
|
||||
const observedSubmissions = observeMessageSubmissionCanonical(
|
||||
currentSubmissions,
|
||||
state.acknowledgedClientMessageIds,
|
||||
);
|
||||
let messageSubmissions = session.messageSubmissions;
|
||||
if (observedSubmissions !== currentSubmissions) {
|
||||
messageSubmissions = new Map(session.messageSubmissions);
|
||||
if (observedSubmissions.length > 0) {
|
||||
messageSubmissions.set(agentId, observedSubmissions);
|
||||
} else {
|
||||
messageSubmissions.delete(agentId);
|
||||
}
|
||||
}
|
||||
if (state.synchronized) {
|
||||
nextAuthoritative.set(agentId, true);
|
||||
nextSyncGeneration.set(agentId, session.historySyncGeneration);
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
...prev.sessions,
|
||||
[serverId]: {
|
||||
...session,
|
||||
agentStreamTail: nextTail,
|
||||
agentStreamHead: nextHead,
|
||||
agentTimelineCursor: nextCursor,
|
||||
agentTimelineHasOlder: nextHasOlder,
|
||||
agentAuthoritativeHistoryApplied: nextAuthoritative,
|
||||
agentHistorySyncGeneration: nextSyncGeneration,
|
||||
messageSubmissions,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Initializing agents
|
||||
setInitializingAgents: (serverId, state) => {
|
||||
set((prev) => {
|
||||
@@ -1566,12 +1298,7 @@ export const useSessionStore = create<SessionStore>()(
|
||||
return prev;
|
||||
}
|
||||
const nextAgents = typeof agents === "function" ? agents(session.agents) : agents;
|
||||
const messageSubmissions = applyRunningAgentsToAcceptedSubmissions({
|
||||
previousAgents: session.agents,
|
||||
nextAgents,
|
||||
submissions: session.messageSubmissions,
|
||||
});
|
||||
if (session.agents === nextAgents && session.messageSubmissions === messageSubmissions) {
|
||||
if (session.agents === nextAgents) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
@@ -1581,11 +1308,10 @@ export const useSessionStore = create<SessionStore>()(
|
||||
[serverId]: {
|
||||
...session,
|
||||
agents: nextAgents,
|
||||
messageSubmissions,
|
||||
workspaceAgentActivity:
|
||||
nextAgents === session.agents
|
||||
? session.workspaceAgentActivity
|
||||
: buildWorkspaceAgentActivityIndex(nextAgents, session.workspaceAgentActivity),
|
||||
workspaceAgentActivity: buildWorkspaceAgentActivityIndex(
|
||||
nextAgents,
|
||||
session.workspaceAgentActivity,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user