Cache session replay on client
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "@/components/assistant-ui/activity-panel";
|
} from "@/components/assistant-ui/activity-panel";
|
||||||
import { Thread } from "@/components/assistant-ui/thread";
|
import { Thread } from "@/components/assistant-ui/thread";
|
||||||
import {
|
import {
|
||||||
fetchLatestRunStatus,
|
type fetchLatestRunStatus,
|
||||||
toReplayRepository,
|
toReplayRepository,
|
||||||
} from "@/components/assistant-ui/thread-list";
|
} from "@/components/assistant-ui/thread-list";
|
||||||
import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
|
import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
|
||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
readPendingWorkspaceSessionId,
|
readPendingWorkspaceSessionId,
|
||||||
writeActiveSessionId,
|
writeActiveSessionId,
|
||||||
} from "@/lib/claw-active-session";
|
} from "@/lib/claw-active-session";
|
||||||
|
import {
|
||||||
|
fetchSessionReplaySnapshot,
|
||||||
|
readCachedSessionReplay,
|
||||||
|
} from "@/lib/claw-session-cache";
|
||||||
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
|
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
|
||||||
import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url";
|
import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url";
|
||||||
import {
|
import {
|
||||||
@@ -112,23 +116,46 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
|
|||||||
loadedInitialSessionRef.current = sessionId;
|
loadedInitialSessionRef.current = sessionId;
|
||||||
const targetSessionId = sessionId;
|
const targetSessionId = sessionId;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
async function loadSession() {
|
async function loadSession() {
|
||||||
try {
|
try {
|
||||||
writeActiveSessionId(targetSessionId);
|
writeActiveSessionId(targetSessionId);
|
||||||
const response = await fetch(`/api/claw/sessions/${targetSessionId}`, {
|
const cached = readCachedSessionReplay<
|
||||||
cache: "no-store",
|
Parameters<typeof toReplayRepository>[0],
|
||||||
});
|
Awaited<ReturnType<typeof fetchLatestRunStatus>>
|
||||||
if (!response.ok || cancelled) return;
|
>(targetSessionId);
|
||||||
const payload = await response.json();
|
if (cached && !cancelled) {
|
||||||
const runStatus = await fetchLatestRunStatus(targetSessionId);
|
replaySession(
|
||||||
|
targetSessionId,
|
||||||
|
toReplayRepository(
|
||||||
|
cached.session,
|
||||||
|
targetSessionId,
|
||||||
|
cached.runStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const snapshot = await fetchSessionReplaySnapshot<
|
||||||
|
Parameters<typeof toReplayRepository>[0],
|
||||||
|
Awaited<ReturnType<typeof fetchLatestRunStatus>>
|
||||||
|
>(targetSessionId, { signal: abortController.signal });
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
replaySession(
|
if (!snapshot) return;
|
||||||
targetSessionId,
|
if (!cached || snapshot.signature !== cached.signature) {
|
||||||
toReplayRepository(payload, targetSessionId, runStatus),
|
replaySession(
|
||||||
);
|
targetSessionId,
|
||||||
|
toReplayRepository(
|
||||||
|
snapshot.session,
|
||||||
|
targetSessionId,
|
||||||
|
snapshot.runStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
pushSessionUrl(targetSessionId);
|
pushSessionUrl(targetSessionId);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
loadedInitialSessionRef.current = null;
|
loadedInitialSessionRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -138,6 +165,7 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
|
|||||||
void loadSession();
|
void loadSession();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [account, initialSessionId, replaySession]);
|
}, [account, initialSessionId, replaySession]);
|
||||||
|
|
||||||
@@ -252,8 +280,7 @@ function AssistantWorkspace() {
|
|||||||
const handleChanged = (event: Event) => {
|
const handleChanged = (event: Event) => {
|
||||||
const customEvent = event as CustomEvent<{ sessionId?: string | null }>;
|
const customEvent = event as CustomEvent<{ sessionId?: string | null }>;
|
||||||
const hasExplicitSession =
|
const hasExplicitSession =
|
||||||
customEvent.detail &&
|
customEvent.detail && Object.hasOwn(customEvent.detail, "sessionId");
|
||||||
Object.prototype.hasOwnProperty.call(customEvent.detail, "sessionId");
|
|
||||||
const next = hasExplicitSession
|
const next = hasExplicitSession
|
||||||
? (customEvent.detail.sessionId ?? null)
|
? (customEvent.detail.sessionId ?? null)
|
||||||
: (readSessionIdFromUrl() ?? readActiveSessionId());
|
: (readSessionIdFromUrl() ?? readActiveSessionId());
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ import {
|
|||||||
writeActiveSessionId,
|
writeActiveSessionId,
|
||||||
writePendingWorkspaceSessionId,
|
writePendingWorkspaceSessionId,
|
||||||
} from "@/lib/claw-active-session";
|
} from "@/lib/claw-active-session";
|
||||||
|
import {
|
||||||
|
fetchSessionReplaySnapshot,
|
||||||
|
invalidateCachedSessionReplay,
|
||||||
|
readCachedSessionReplay,
|
||||||
|
type SessionReplaySnapshot,
|
||||||
|
} from "@/lib/claw-session-cache";
|
||||||
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
||||||
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
|
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
|
||||||
|
|
||||||
@@ -113,6 +119,27 @@ type ClawStoredToolCall = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ReplaySessionFn = (
|
||||||
|
sessionId: string,
|
||||||
|
repository: ExportedMessageRepository,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
type ClawReplaySnapshot = SessionReplaySnapshot<
|
||||||
|
ClawStoredSession,
|
||||||
|
ClawRunStatus
|
||||||
|
>;
|
||||||
|
|
||||||
|
function replaySessionSnapshot(
|
||||||
|
replaySession: ReplaySessionFn,
|
||||||
|
sessionId: string,
|
||||||
|
snapshot: ClawReplaySnapshot,
|
||||||
|
) {
|
||||||
|
replaySession(
|
||||||
|
sessionId,
|
||||||
|
toReplayRepository(snapshot.session, sessionId, snapshot.runStatus),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const ThreadList: FC = () => {
|
export const ThreadList: FC = () => {
|
||||||
return (
|
return (
|
||||||
<ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex min-h-0 flex-1 flex-col gap-1">
|
<ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex min-h-0 flex-1 flex-col gap-1">
|
||||||
@@ -168,7 +195,7 @@ function ensureSidebarWorkspaceSessionId(): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workspaceDisplayLabel(entry: JupyterWorkspaceEntry): string {
|
function workspaceDisplayLabel(entry: JupyterWorkspaceEntry): string {
|
||||||
if (entry.label && entry.label.trim()) return entry.label.trim();
|
if (entry.label?.trim()) return entry.label.trim();
|
||||||
if (entry.base_url) {
|
if (entry.base_url) {
|
||||||
try {
|
try {
|
||||||
return new URL(entry.base_url).host;
|
return new URL(entry.base_url).host;
|
||||||
@@ -243,16 +270,15 @@ const JupyterWorkspaceList: FC = () => {
|
|||||||
window.dispatchEvent(new Event("claw-sessions-changed"));
|
window.dispatchEvent(new Event("claw-sessions-changed"));
|
||||||
window.dispatchEvent(new Event("claw-workspaces-changed"));
|
window.dispatchEvent(new Event("claw-workspaces-changed"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(err instanceof Error ? err.message : "切换工作区时网络异常");
|
||||||
err instanceof Error ? err.message : "切换工作区时网络异常",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setBindingId(null);
|
setBindingId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = async (entry: JupyterWorkspaceEntry) => {
|
const remove = async (entry: JupyterWorkspaceEntry) => {
|
||||||
if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」?`)) return;
|
if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」?`))
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}`,
|
`/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}`,
|
||||||
@@ -331,6 +357,8 @@ const ClawSessionList: FC = () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const renameInputRef = useRef<HTMLInputElement | null>(null);
|
const renameInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const openRequestRef = useRef(0);
|
||||||
|
const openAbortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveSessionId(readActiveSessionId());
|
setActiveSessionId(readActiveSessionId());
|
||||||
@@ -394,6 +422,51 @@ const ClawSessionList: FC = () => {
|
|||||||
setDraftTitle("");
|
setDraftTitle("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSession = async (sessionId: string) => {
|
||||||
|
const requestId = openRequestRef.current + 1;
|
||||||
|
openRequestRef.current = requestId;
|
||||||
|
openAbortRef.current?.abort();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
openAbortRef.current = abortController;
|
||||||
|
const cached = readCachedSessionReplay<ClawStoredSession, ClawRunStatus>(
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
writeActiveSessionId(sessionId);
|
||||||
|
pushSessionUrl(sessionId);
|
||||||
|
setActiveSessionId(sessionId);
|
||||||
|
if (cached) {
|
||||||
|
replaySessionSnapshot(replaySession, sessionId, cached);
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
} else {
|
||||||
|
setLoadingSessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snapshot = await fetchSessionReplaySnapshot<
|
||||||
|
ClawStoredSession,
|
||||||
|
ClawRunStatus
|
||||||
|
>(sessionId, { signal: abortController.signal });
|
||||||
|
if (requestId !== openRequestRef.current) return;
|
||||||
|
if (readActiveSessionId() !== sessionId) return;
|
||||||
|
if (!snapshot) return;
|
||||||
|
if (!cached || snapshot.signature !== cached.signature) {
|
||||||
|
replaySessionSnapshot(replaySession, sessionId, snapshot);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||||
|
console.warn("[session-list] failed to open session", error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === openRequestRef.current) {
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
if (openAbortRef.current === abortController) {
|
||||||
|
openAbortRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -443,31 +516,7 @@ const ClawSessionList: FC = () => {
|
|||||||
preview: session.preview,
|
preview: session.preview,
|
||||||
});
|
});
|
||||||
setOpenMenuSessionId(null);
|
setOpenMenuSessionId(null);
|
||||||
writeActiveSessionId(session.session_id);
|
await openSession(session.session_id);
|
||||||
pushSessionUrl(session.session_id);
|
|
||||||
setActiveSessionId(session.session_id);
|
|
||||||
setLoadingSessionId(session.session_id);
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/claw/sessions/${session.session_id}`,
|
|
||||||
);
|
|
||||||
if (!response.ok) return;
|
|
||||||
const payload =
|
|
||||||
(await response.json()) as ClawStoredSession;
|
|
||||||
const runStatus = await fetchLatestRunStatus(
|
|
||||||
session.session_id,
|
|
||||||
);
|
|
||||||
replaySession(
|
|
||||||
session.session_id,
|
|
||||||
toReplayRepository(
|
|
||||||
payload,
|
|
||||||
session.session_id,
|
|
||||||
runStatus,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoadingSessionId(null);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
@@ -525,6 +574,7 @@ const ClawSessionList: FC = () => {
|
|||||||
(item) => item.session_id !== session.session_id,
|
(item) => item.session_id !== session.session_id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
invalidateCachedSessionReplay(session.session_id);
|
||||||
setOpenMenuSessionId(null);
|
setOpenMenuSessionId(null);
|
||||||
if (activeSessionId === session.session_id) {
|
if (activeSessionId === session.session_id) {
|
||||||
clearActiveSessionId();
|
clearActiveSessionId();
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Popover as PopoverPrimitive } from "radix-ui";
|
import { Popover as PopoverPrimitive } from "radix-ui";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { ClawAccount } from "@/app/claw-account-gate";
|
import type { ClawAccount } from "@/app/claw-account-gate";
|
||||||
import { ClawLlmSettings } from "@/app/claw-llm-settings";
|
import { ClawLlmSettings } from "@/app/claw-llm-settings";
|
||||||
import { ClawThemeSwitcher } from "@/app/claw-theme-switcher";
|
import { ClawThemeSwitcher } from "@/app/claw-theme-switcher";
|
||||||
import {
|
import {
|
||||||
fetchLatestRunStatus,
|
type ClawRunStatus,
|
||||||
ThreadList,
|
ThreadList,
|
||||||
toReplayRepository,
|
toReplayRepository,
|
||||||
} from "@/components/assistant-ui/thread-list";
|
} from "@/components/assistant-ui/thread-list";
|
||||||
@@ -49,6 +49,10 @@ import {
|
|||||||
readActiveSessionId,
|
readActiveSessionId,
|
||||||
writeActiveSessionId,
|
writeActiveSessionId,
|
||||||
} from "@/lib/claw-active-session";
|
} from "@/lib/claw-active-session";
|
||||||
|
import {
|
||||||
|
fetchSessionReplaySnapshot,
|
||||||
|
readCachedSessionReplay,
|
||||||
|
} from "@/lib/claw-session-cache";
|
||||||
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
||||||
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
|
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";
|
||||||
|
|
||||||
@@ -384,6 +388,8 @@ function SessionResultButton({
|
|||||||
function useSidebarSessions(open: boolean) {
|
function useSidebarSessions(open: boolean) {
|
||||||
const [sessions, setSessions] = useState<SidebarSession[]>([]);
|
const [sessions, setSessions] = useState<SidebarSession[]>([]);
|
||||||
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
||||||
|
const openRequestRef = useRef(0);
|
||||||
|
const openAbortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -417,22 +423,54 @@ function useSidebarSessions(open: boolean) {
|
|||||||
) {
|
) {
|
||||||
const cleanSessionId = normalizeSessionId(sessionId);
|
const cleanSessionId = normalizeSessionId(sessionId);
|
||||||
if (!cleanSessionId) return;
|
if (!cleanSessionId) return;
|
||||||
|
const requestId = openRequestRef.current + 1;
|
||||||
|
openRequestRef.current = requestId;
|
||||||
|
openAbortRef.current?.abort();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
openAbortRef.current = abortController;
|
||||||
|
const cached = readCachedSessionReplay<ReplaySessionPayload, ClawRunStatus>(
|
||||||
|
cleanSessionId,
|
||||||
|
);
|
||||||
writeActiveSessionId(cleanSessionId);
|
writeActiveSessionId(cleanSessionId);
|
||||||
pushSessionUrl(cleanSessionId);
|
pushSessionUrl(cleanSessionId);
|
||||||
setLoadingSessionId(sessionId);
|
if (cached) {
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/claw/sessions/${cleanSessionId}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
});
|
|
||||||
if (!response.ok) return;
|
|
||||||
const payload = (await response.json()) as ReplaySessionPayload;
|
|
||||||
const runStatus = await fetchLatestRunStatus(cleanSessionId);
|
|
||||||
replaySession(
|
replaySession(
|
||||||
cleanSessionId,
|
cleanSessionId,
|
||||||
toReplayRepository(payload, cleanSessionId, runStatus),
|
toReplayRepository(cached.session, cleanSessionId, cached.runStatus),
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
|
} else {
|
||||||
|
setLoadingSessionId(sessionId);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const snapshot = await fetchSessionReplaySnapshot<
|
||||||
|
ReplaySessionPayload,
|
||||||
|
ClawRunStatus
|
||||||
|
>(cleanSessionId, { signal: abortController.signal });
|
||||||
|
if (requestId !== openRequestRef.current) return;
|
||||||
|
if (readActiveSessionId() !== cleanSessionId) return;
|
||||||
|
if (!snapshot) return;
|
||||||
|
if (!cached || snapshot.signature !== cached.signature) {
|
||||||
|
replaySession(
|
||||||
|
cleanSessionId,
|
||||||
|
toReplayRepository(
|
||||||
|
snapshot.session,
|
||||||
|
cleanSessionId,
|
||||||
|
snapshot.runStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||||
|
console.warn("[sidebar] failed to open session", error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === openRequestRef.current) {
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
if (openAbortRef.current === abortController) {
|
||||||
|
openAbortRef.current = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export type SessionReplaySnapshot<TSession = unknown, TRun = unknown> = {
|
||||||
|
sessionId: string;
|
||||||
|
session: TSession;
|
||||||
|
runStatus: TRun | null;
|
||||||
|
signature: string;
|
||||||
|
fetchedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_REPLAY_CACHE_SIZE = 50;
|
||||||
|
const replayCache = new Map<string, SessionReplaySnapshot>();
|
||||||
|
|
||||||
|
export function readCachedSessionReplay<TSession = unknown, TRun = unknown>(
|
||||||
|
sessionId: string,
|
||||||
|
): SessionReplaySnapshot<TSession, TRun> | null {
|
||||||
|
const cached = replayCache.get(sessionId);
|
||||||
|
if (!cached) return null;
|
||||||
|
// Refresh recency for LRU behavior.
|
||||||
|
replayCache.delete(sessionId);
|
||||||
|
replayCache.set(sessionId, cached);
|
||||||
|
return cached as SessionReplaySnapshot<TSession, TRun>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateCachedSessionReplay(sessionId: string) {
|
||||||
|
replayCache.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSessionReplaySnapshot<
|
||||||
|
TSession = unknown,
|
||||||
|
TRun = unknown,
|
||||||
|
>(
|
||||||
|
sessionId: string,
|
||||||
|
options: { signal?: AbortSignal } = {},
|
||||||
|
): Promise<SessionReplaySnapshot<TSession, TRun> | null> {
|
||||||
|
const [sessionResponse, runStatus] = await Promise.all([
|
||||||
|
fetch(`/api/claw/sessions/${encodeURIComponent(sessionId)}`, {
|
||||||
|
cache: "no-store",
|
||||||
|
signal: options.signal,
|
||||||
|
}),
|
||||||
|
fetchLatestRunStatusForCache<TRun>(sessionId, options.signal),
|
||||||
|
]);
|
||||||
|
if (!sessionResponse.ok) return null;
|
||||||
|
const session = (await sessionResponse.json()) as TSession;
|
||||||
|
return writeCachedSessionReplay(sessionId, session, runStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCachedSessionReplay<TSession, TRun>(
|
||||||
|
sessionId: string,
|
||||||
|
session: TSession,
|
||||||
|
runStatus: TRun | null,
|
||||||
|
): SessionReplaySnapshot<TSession, TRun> {
|
||||||
|
const snapshot: SessionReplaySnapshot<TSession, TRun> = {
|
||||||
|
sessionId,
|
||||||
|
session,
|
||||||
|
runStatus,
|
||||||
|
signature: sessionReplaySignature(session, runStatus),
|
||||||
|
fetchedAt: Date.now(),
|
||||||
|
};
|
||||||
|
replayCache.set(sessionId, snapshot as SessionReplaySnapshot);
|
||||||
|
while (replayCache.size > MAX_REPLAY_CACHE_SIZE) {
|
||||||
|
const oldest = replayCache.keys().next().value;
|
||||||
|
if (!oldest) break;
|
||||||
|
replayCache.delete(oldest);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLatestRunStatusForCache<TRun>(
|
||||||
|
sessionId: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<TRun | null> {
|
||||||
|
try {
|
||||||
|
const url = new URL("/api/claw/runs/latest", window.location.origin);
|
||||||
|
url.searchParams.set("session_id", sessionId);
|
||||||
|
const response = await fetch(url, { cache: "no-store", signal });
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return (await response.json()) as TRun;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionReplaySignature(session: unknown, runStatus: unknown) {
|
||||||
|
const sessionObject = isObject(session) ? session : {};
|
||||||
|
const runObject = isObject(runStatus) ? runStatus : {};
|
||||||
|
const messages = Array.isArray(sessionObject.messages)
|
||||||
|
? sessionObject.messages
|
||||||
|
: [];
|
||||||
|
const lastMessage = messages.at(-1);
|
||||||
|
const lastObject = isObject(lastMessage) ? lastMessage : {};
|
||||||
|
const metadata = isObject(lastObject.metadata) ? lastObject.metadata : {};
|
||||||
|
return [
|
||||||
|
sessionObject.session_id,
|
||||||
|
sessionObject.turns,
|
||||||
|
sessionObject.tool_calls,
|
||||||
|
messages.length,
|
||||||
|
lastObject.role,
|
||||||
|
lastObject.message_id,
|
||||||
|
typeof lastObject.content === "string" ? lastObject.content.length : "",
|
||||||
|
metadata.elapsed_ms,
|
||||||
|
runObject.run_id,
|
||||||
|
runObject.status,
|
||||||
|
runObject.updated_at,
|
||||||
|
runObject.finished_at,
|
||||||
|
runObject.elapsed_ms,
|
||||||
|
].join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user