refactor(app): fold timeline sequencing helpers into stream reducer (#664)

Three sibling modules (session-stream-lifecycle, session-timeline-seq-gate,
and the bootstrap-tail policy from session-timeline-bootstrap-policy) only
served the stream reducer. Inline them as private helpers in
session-stream-reducers.ts; keep deriveInitialTimelineRequest exported for
the hook caller. Removes shallow seams without changing behavior.
This commit is contained in:
Mohamed Boudra
2026-05-02 15:19:09 +08:00
committed by GitHub
parent 03e54a1a21
commit 1b5d082d1a
7 changed files with 151 additions and 323 deletions
@@ -4,15 +4,6 @@ import type { Agent } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
import {
classifySessionTimelineSeq,
type SessionTimelineSeqDecision,
} from "@/contexts/session-timeline-seq-gate";
import {
deriveBootstrapTailTimelinePolicy,
shouldResolveTimelineInit,
} from "@/contexts/session-timeline-bootstrap-policy";
import { deriveOptimisticLifecycleStatus } from "@/contexts/session-stream-lifecycle";
const AGENT_STREAM_REDUCER_FLUSH_DELAY_MS = 16 * 3;
@@ -46,6 +37,16 @@ export interface AgentStreamReducerSideEffect {
type TimelineDirection = "tail" | "before" | "after";
type InitRequestDirection = "tail" | "after";
type SessionTimelineSeqCursor =
| {
epoch: string;
endSeq: number;
}
| null
| undefined;
type SessionTimelineSeqDecision = "accept" | "drop_stale" | "drop_epoch" | "gap" | "init";
interface TimelineResponseEntry {
seqStart: number;
seqEnd: number;
@@ -99,6 +100,106 @@ interface TimelinePathResult {
sideEffects: TimelineReducerSideEffect[];
}
function classifySessionTimelineSeq({
cursor,
epoch,
seq,
}: {
cursor: SessionTimelineSeqCursor;
epoch: string;
seq: number;
}): SessionTimelineSeqDecision {
if (!cursor) {
return "init";
}
if (cursor.epoch !== epoch) {
return "drop_epoch";
}
if (seq <= cursor.endSeq) {
return "drop_stale";
}
if (seq === cursor.endSeq + 1) {
return "accept";
}
return "gap";
}
function deriveBootstrapTailTimelinePolicy({
direction,
reset,
epoch,
endCursor,
isInitializing,
hasActiveInitDeferred,
}: {
direction: TimelineDirection;
reset: boolean;
epoch: string;
endCursor: { seq: number } | null;
isInitializing: boolean;
hasActiveInitDeferred: boolean;
}): {
replace: boolean;
catchUpCursor: { epoch: string; endSeq: number } | null;
} {
if (reset) {
return { replace: true, catchUpCursor: null };
}
const isBootstrapTailInit = direction === "tail" && isInitializing && hasActiveInitDeferred;
if (!isBootstrapTailInit) {
return { replace: false, catchUpCursor: null };
}
return {
replace: true,
catchUpCursor: endCursor ? { epoch, endSeq: endCursor.seq } : null,
};
}
function shouldResolveTimelineInit({
hasActiveInitDeferred,
isInitializing,
initRequestDirection,
responseDirection,
reset,
}: {
hasActiveInitDeferred: boolean;
isInitializing: boolean;
initRequestDirection: InitRequestDirection;
responseDirection: TimelineDirection;
reset: boolean;
}): boolean {
if (!hasActiveInitDeferred || !isInitializing) {
return false;
}
if (reset) {
return true;
}
return responseDirection === initRequestDirection;
}
function deriveOptimisticLifecycleStatus(
currentStatus: AgentLifecycleStatus,
event: AgentStreamEventPayload,
): AgentLifecycleStatus | null {
if (currentStatus !== "running") {
return null;
}
switch (event.type) {
case "turn_completed":
return "idle";
case "turn_failed":
return "error";
case "turn_canceled":
// A canceled turn can be either a final user cancel or an interrupt before
// a replacement turn starts. The daemon snapshot is authoritative here.
return null;
default:
return null;
}
}
function preserveReplacePathAssistantHead(params: {
tail: StreamItem[];
currentHead: StreamItem[];