Fix interrupted run replay state
This commit is contained in:
@@ -35,8 +35,13 @@ type ClawStoredMessage = {
|
||||
name?: string;
|
||||
tool_call_id?: string;
|
||||
tool_calls?: ClawStoredToolCall[];
|
||||
state?: string;
|
||||
stop_reason?: string;
|
||||
metadata?: {
|
||||
elapsed_ms?: unknown;
|
||||
placeholder?: unknown;
|
||||
phase?: unknown;
|
||||
status?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -48,11 +53,22 @@ type ClawStoredSession = {
|
||||
export type ClawRunStatus = {
|
||||
run_id?: string;
|
||||
session_id?: string;
|
||||
status?: "idle" | "queued" | "running" | "completed" | "failed" | "cancelled";
|
||||
status?:
|
||||
| "idle"
|
||||
| "queued"
|
||||
| "running"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "interrupted";
|
||||
current_stage?: string;
|
||||
started_at?: number;
|
||||
};
|
||||
|
||||
type ClawActiveRunStatus = ClawRunStatus & {
|
||||
status: "queued" | "running";
|
||||
};
|
||||
|
||||
type ClawStoredToolCall = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -364,12 +380,12 @@ export function toReplayRepository(
|
||||
runStatus?: ClawRunStatus | null,
|
||||
): ExportedMessageRepository {
|
||||
const messages: ExportedMessageRepository["messages"] = [];
|
||||
const toolResults = collectToolResults(session.messages ?? []);
|
||||
const lastAssistantContentIndex = findLastAssistantContentIndex(
|
||||
session.messages ?? [],
|
||||
);
|
||||
const replayMessages = getReplayMessages(session.messages ?? [], runStatus);
|
||||
const toolResults = collectToolResults(replayMessages);
|
||||
const lastAssistantContentIndex =
|
||||
findLastAssistantContentIndex(replayMessages);
|
||||
let previousId: string | null = null;
|
||||
for (const [index, message] of (session.messages ?? []).entries()) {
|
||||
for (const [index, message] of replayMessages.entries()) {
|
||||
if (message.role === "tool") continue;
|
||||
if (message.role !== "user" && message.role !== "assistant") continue;
|
||||
const content = cleanStoredContent(message.content ?? "");
|
||||
@@ -421,6 +437,37 @@ export function toReplayRepository(
|
||||
messages.push({ message: threadMessage, parentId: previousId });
|
||||
previousId = id;
|
||||
}
|
||||
if (shouldShowInterruptedNotice(session.messages ?? [], runStatus)) {
|
||||
const id = `${fallbackSessionId}-interrupted`;
|
||||
const text =
|
||||
runStatus?.status === "cancelled"
|
||||
? "上一次任务已取消,后台没有正在执行的进程。你可以继续回复,或重新发起任务。"
|
||||
: "上一次任务已中断,后台没有正在执行的进程。你可以继续回复,或重新发起任务。";
|
||||
const parts = [{ type: "text", 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: "complete", reason: "stop" },
|
||||
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;
|
||||
}
|
||||
if (isActiveRunStatus(runStatus)) {
|
||||
const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`;
|
||||
const text =
|
||||
@@ -468,10 +515,65 @@ export function toReplayRepository(
|
||||
|
||||
function isActiveRunStatus(
|
||||
runStatus?: ClawRunStatus | null,
|
||||
): runStatus is ClawRunStatus {
|
||||
): runStatus is ClawActiveRunStatus {
|
||||
return runStatus?.status === "queued" || runStatus?.status === "running";
|
||||
}
|
||||
|
||||
function getReplayMessages(
|
||||
messages: readonly ClawStoredMessage[],
|
||||
runStatus?: ClawRunStatus | null,
|
||||
) {
|
||||
if (isActiveRunStatus(runStatus)) return [...messages];
|
||||
return trimIncompleteReplayTail(messages);
|
||||
}
|
||||
|
||||
function shouldShowInterruptedNotice(
|
||||
messages: readonly ClawStoredMessage[],
|
||||
runStatus?: ClawRunStatus | null,
|
||||
) {
|
||||
if (isActiveRunStatus(runStatus)) return false;
|
||||
return (
|
||||
runStatus?.status === "interrupted" ||
|
||||
runStatus?.status === "cancelled" ||
|
||||
trimIncompleteReplayTail(messages).length !== messages.length
|
||||
);
|
||||
}
|
||||
|
||||
function trimIncompleteReplayTail(messages: readonly ClawStoredMessage[]) {
|
||||
const trimmed = [...messages];
|
||||
while (trimmed.length && isIncompleteReplayMessage(trimmed.at(-1))) {
|
||||
trimmed.pop();
|
||||
}
|
||||
while (trimmed.length && hasTrailingAssistantToolCall(trimmed.at(-1))) {
|
||||
trimmed.pop();
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isIncompleteReplayMessage(message?: ClawStoredMessage) {
|
||||
if (!message) return false;
|
||||
if (message.state && message.state !== "final") return true;
|
||||
if (
|
||||
message.role === "tool" &&
|
||||
(message.metadata?.phase === "starting" ||
|
||||
message.metadata?.phase === "running")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
message.role !== "user" &&
|
||||
message.metadata?.placeholder === true &&
|
||||
message.metadata?.status === "running"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasTrailingAssistantToolCall(message?: ClawStoredMessage) {
|
||||
return message?.role === "assistant" && Boolean(message.tool_calls?.length);
|
||||
}
|
||||
|
||||
function normalizeRunTimestampMs(value: unknown) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
||||
return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value);
|
||||
@@ -499,7 +601,11 @@ function cleanStoredContent(content: string) {
|
||||
function collectToolResults(messages: readonly ClawStoredMessage[]) {
|
||||
const results = new Map<string, string>();
|
||||
for (const message of messages) {
|
||||
if (message.role === "tool" && message.tool_call_id) {
|
||||
if (
|
||||
message.role === "tool" &&
|
||||
message.tool_call_id &&
|
||||
!isIncompleteReplayMessage(message)
|
||||
) {
|
||||
results.set(message.tool_call_id, message.content ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user