Restore active run state on session replay

This commit is contained in:
武阳
2026-05-07 16:37:14 +08:00
parent 9d7d496443
commit 2ba67ead00
4 changed files with 147 additions and 2 deletions
@@ -45,6 +45,13 @@ type ClawStoredSession = {
messages?: ClawStoredMessage[];
};
export type ClawRunStatus = {
run_id?: string;
session_id?: string;
status?: "idle" | "queued" | "running" | "completed" | "failed" | "cancelled";
current_stage?: string;
};
type ClawStoredToolCall = {
id?: string;
name?: string;
@@ -222,9 +229,16 @@ const ClawSessionList: FC = () => {
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),
toReplayRepository(
payload,
session.session_id,
runStatus,
),
);
} finally {
setLoadingSessionId(null);
@@ -323,6 +337,18 @@ function dedupeSessions(payload: unknown[]) {
return [...byId.values()].sort((a, b) => b.modified_at - a.modified_at);
}
export async function fetchLatestRunStatus(sessionId: string) {
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" });
if (!response.ok) return null;
return (await response.json()) as ClawRunStatus;
} catch {
return null;
}
}
function isClawSession(value: unknown): value is ClawSession {
if (!value || typeof value !== "object") return false;
const item = value as Partial<ClawSession>;
@@ -334,6 +360,7 @@ function isClawSession(value: unknown): value is ClawSession {
export function toReplayRepository(
session: ClawStoredSession,
fallbackSessionId: string,
runStatus?: ClawRunStatus | null,
): ExportedMessageRepository {
const messages: ExportedMessageRepository["messages"] = [];
const toolResults = collectToolResults(session.messages ?? []);
@@ -393,12 +420,49 @@ export function toReplayRepository(
messages.push({ message: threadMessage, parentId: previousId });
previousId = id;
}
if (isActiveRunStatus(runStatus)) {
const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`;
const text =
runStatus.status === "queued"
? "当前会话已有任务在执行,本轮正在排队。"
: runStatus.current_stage || "后端正在执行这个会话。";
const parts = [{ type: "reasoning", text }] as UIMessage["parts"];
const uiMessage: UIMessage = {
id,
role: "assistant",
parts,
metadata: { sessionId: session.session_id ?? fallbackSessionId },
} as UIMessage;
const threadMessage = {
id,
role: "assistant",
createdAt: new Date(),
content: toThreadMessageContent(parts),
status: { type: "running" },
metadata: {
unstable_state: null,
unstable_annotations: [],
unstable_data: [],
steps: [],
custom: { sessionId: session.session_id ?? fallbackSessionId },
},
} as ThreadMessage;
bindExternalStoreMessage(threadMessage, uiMessage);
messages.push({ message: threadMessage, parentId: previousId });
previousId = id;
}
return {
headId: previousId,
messages,
};
}
function isActiveRunStatus(
runStatus?: ClawRunStatus | null,
): runStatus is ClawRunStatus {
return runStatus?.status === "queued" || runStatus?.status === "running";
}
function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];