refactor(timeline): remove submission timing state

This commit is contained in:
Mohamed Boudra
2026-08-01 01:22:45 +02:00
parent 4e34668a19
commit 2b2012e8e6
10 changed files with 30 additions and 46 deletions
+11 -9
View File
@@ -138,15 +138,17 @@ footer, and permanently drops attachments because the daemon does not echo them
A submitted prompt is one `UserMessageItem` row. That row is the authoritative local presentation:
its stable identity, text, timestamp, images, and attachments do not change when the provider
acknowledges it. Submission lifecycle is a separate record keyed by agent, not another row shape.
The transaction registry exists only until the daemon accepts or rejects the send. An accepted RPC
settles it because capable daemons guarantee the prompt is already durable. Canonical acknowledgement
can arrive first and prevents a later transport error from rolling back a prompt already observed.
The transaction registry records two independent settlement facts: canonical acknowledgement and RPC
settlement. Canonical acknowledgement retires optimistic activity immediately. When both facts are
known, whichever arrives second deletes the record. A canonical acknowledgement that arrives first
prevents a later transport error from rolling back a prompt already observed.
The daemon's accepted response waits for the correlated run start and guarantees that the canonical
submitted row has been recorded. It publishes the accepted turn's liveness before that row, so the
client applies authoritative activity before retiring optimistic activity on RPC acceptance. Timeline
render batching does not delay lifecycle application. Directory status never settles a submission.
Overlapping sends settle independently rather than collapsing to one newest pending message.
client applies authoritative activity before canonical acknowledgement retires optimistic activity.
Timeline render batching does not delay lifecycle application. Directory status never settles a
submission. Overlapping sends settle independently rather than collapsing to one newest pending
message.
Daemons advertising `server_info.features.canonicalSubmittedPrompts` guarantee that every accepted
prompt carrying a client message id is recorded and streamed as a canonical `user_message` with that
@@ -167,9 +169,9 @@ remain a separate pre-turn registry and retire on canonical acknowledgement.
The compatibility boundary for older daemons is snapshot normalization: running/idle status becomes an
anonymous active turn or idle state once, and downstream code consumes the same activity shape. The app
does not combine anonymous lifecycle events, timestamps, timeline rows, and resume coverage to infer a
second running state. Disconnect and replica removal remain destructive close boundaries. Running time
comes from the daemon snapshot/event or the pending submission, never from whichever timeline rows happen
to be mounted.
second running state. Disconnect and replica removal remain destructive close boundaries. Elapsed time
comes only from turn liveness, never from submission records or whichever timeline rows happen to be
mounted.
The daemon records one canonical submitted user row with Paseo's `clientMessageId` as soon as the
provider accepts the turn. A correlated provider echo enriches that same sequence with its provider
@@ -215,7 +215,6 @@ function createFakeStream(initialHead: Map<string, StreamItem[]> = new Map()): F
...stream,
submissions: beginMessageSubmission(current.submissions, {
clientMessageId: message.clientMessageId!,
submittedAt: message.timestamp,
}),
});
},
@@ -159,7 +159,6 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
return [
{
clientMessageId: machine.attempt.clientMessageId,
submittedAt: machine.attempt.timestamp,
},
];
}, [machine]);
@@ -545,7 +545,7 @@ export function WorkspaceDraftAgentTab({
},
});
const turnPresentation = useMemo(
() => resolveTurnPresentation(TURN_LIVENESS_IDLE, pendingMessageSubmissions),
() => resolveTurnPresentation(TURN_LIVENESS_IDLE, pendingMessageSubmissions.length > 0),
[pendingMessageSubmissions],
);
useCommandCenterActions({
@@ -8,15 +8,10 @@ import {
rejectMessageSubmission,
} from "./model";
const submittedAt = new Date("2026-07-26T10:00:00.000Z");
describe("message submission transactions", () => {
it("tracks every in-flight submission independently", () => {
const first = beginMessageSubmission([], { clientMessageId: "client-1", submittedAt });
const both = beginMessageSubmission(first, {
clientMessageId: "client-2",
submittedAt: new Date(submittedAt.getTime() + 1),
});
const first = beginMessageSubmission([], { clientMessageId: "client-1" });
const both = beginMessageSubmission(first, { clientMessageId: "client-2" });
expect(getActiveMessageSubmissions(both).map((item) => item.clientMessageId)).toEqual([
"client-1",
@@ -27,15 +22,14 @@ describe("message submission transactions", () => {
it("settles only the accepted transaction on RPC acceptance", () => {
const both = beginMessageSubmission(
beginMessageSubmission([], { clientMessageId: "client-1", submittedAt }),
{ clientMessageId: "client-2", submittedAt },
beginMessageSubmission([], { clientMessageId: "client-1" }),
{ clientMessageId: "client-2" },
);
const acknowledged = observeMessageSubmissionCanonical(both, ["client-1"]);
expect(acceptMessageSubmission(acknowledged, "client-1")).toEqual([
{
clientMessageId: "client-2",
submittedAt,
providerAcknowledged: false,
rpcSettled: false,
},
@@ -43,13 +37,12 @@ describe("message submission transactions", () => {
});
it("keeps activity until an accepted RPC's canonical row is observed", () => {
const sending = beginMessageSubmission([], { clientMessageId: "client-1", submittedAt });
const sending = beginMessageSubmission([], { clientMessageId: "client-1" });
const accepted = acceptMessageSubmission(sending, "client-1");
expect(accepted).toEqual([
{
clientMessageId: "client-1",
submittedAt,
providerAcknowledged: false,
rpcSettled: true,
},
@@ -62,21 +55,19 @@ describe("message submission transactions", () => {
it("records provider acknowledgement without settling another transaction", () => {
const both = beginMessageSubmission(
beginMessageSubmission([], { clientMessageId: "client-1", submittedAt }),
{ clientMessageId: "client-2", submittedAt },
beginMessageSubmission([], { clientMessageId: "client-1" }),
{ clientMessageId: "client-2" },
);
const observed = observeMessageSubmissionCanonical(both, ["client-1"]);
expect(observed).toEqual([
{
clientMessageId: "client-1",
submittedAt,
providerAcknowledged: true,
rpcSettled: false,
},
{
clientMessageId: "client-2",
submittedAt,
providerAcknowledged: false,
rpcSettled: false,
},
@@ -85,7 +76,7 @@ describe("message submission transactions", () => {
});
it("does not roll back a provider-acknowledged prompt on a later transport error", () => {
const sending = beginMessageSubmission([], { clientMessageId: "client-1", submittedAt });
const sending = beginMessageSubmission([], { clientMessageId: "client-1" });
const observed = observeMessageSubmissionCanonical(sending, ["client-1"]);
expect(rejectMessageSubmission(observed, "client-1")).toEqual({
@@ -95,7 +86,7 @@ describe("message submission transactions", () => {
});
it("rejects an unacknowledged transaction", () => {
const sending = beginMessageSubmission([], { clientMessageId: "client-1", submittedAt });
const sending = beginMessageSubmission([], { clientMessageId: "client-1" });
expect(rejectMessageSubmission(sending, "client-1")).toEqual({
outcome: "rejected",
@@ -104,10 +95,10 @@ describe("message submission transactions", () => {
});
it("does not create duplicate transaction identity", () => {
const sending = beginMessageSubmission([], { clientMessageId: "client-1", submittedAt });
const sending = beginMessageSubmission([], { clientMessageId: "client-1" });
expect(() =>
beginMessageSubmission(sending, { clientMessageId: "client-1", submittedAt }),
).toThrow("Message submission already exists");
expect(() => beginMessageSubmission(sending, { clientMessageId: "client-1" })).toThrow(
"Message submission already exists",
);
});
});
@@ -1,6 +1,5 @@
export interface PendingMessageSubmission {
clientMessageId: string;
submittedAt: Date;
}
export type MessageSubmissionRecord = PendingMessageSubmission & {
+2 -2
View File
@@ -387,7 +387,7 @@ export function selectAgentTurnPresentation(
): TurnPresentation {
return resolveTurnPresentation(
session?.agentTurnLiveness.get(agentId) ?? TURN_LIVENESS_IDLE,
getActiveMessageSubmissions(session?.messageSubmissions.get(agentId)),
getActiveMessageSubmissions(session?.messageSubmissions.get(agentId)).length > 0,
);
}
@@ -1237,7 +1237,7 @@ export const useSessionStore = create<SessionStore>()(
});
const submissions = beginMessageSubmission(
session.messageSubmissions.get(agentId) ?? [],
{ clientMessageId: message.clientMessageId, submittedAt: message.timestamp },
{ clientMessageId: message.clientMessageId },
);
const messageSubmissions = new Map(session.messageSubmissions);
messageSubmissions.set(agentId, submissions);
@@ -68,11 +68,7 @@ describe("turn activity", () => {
});
it("keeps submission-only activity untimed", () => {
expect(
resolveTurnPresentation(TURN_LIVENESS_IDLE, [
{ clientMessageId: "message-1", submittedAt: startedAt },
]),
).toEqual({
expect(resolveTurnPresentation(TURN_LIVENESS_IDLE, true)).toEqual({
isActive: true,
isCancelling: false,
startedAt: null,
+2 -3
View File
@@ -56,9 +56,8 @@ export interface TurnPresentation {
export function resolveTurnPresentation(
liveness: TurnLiveness,
submissions: readonly { clientMessageId: string; submittedAt: Date }[],
hasActiveSubmission: boolean,
): TurnPresentation {
const latestSubmission = submissions.at(-1);
if (liveness.phase === "open") {
return {
isActive: true,
@@ -68,7 +67,7 @@ export function resolveTurnPresentation(
};
}
return {
isActive: latestSubmission !== undefined,
isActive: hasActiveSubmission,
isCancelling: liveness.cancellationRequestId !== null,
startedAt: null,
turnId: null,
@@ -157,7 +157,6 @@ describe("message submission authority", () => {
expect(useSessionStore.getState().sessions[serverId]?.messageSubmissions.get(agentId)).toEqual([
{
clientMessageId,
submittedAt: new Date("2026-07-27T10:00:00.000Z"),
providerAcknowledged: false,
rpcSettled: false,
},