if (!isSessionsSpawn) { if (!nameChanged) { return block;
} return { ...(block as Record<string, unknown>), name: normalizedName } as RawToolCallBlock;
}
// Redact large/sensitive inline attachment content from persisted transcripts. // Apply redaction to both `.arguments` and `.input` properties since block structures can vary const nextArgs = redactSessionsSpawnAttachmentsArgs(block.arguments); const nextInput = redactSessionsSpawnAttachmentsArgs(block.input); if (nextArgs === block.arguments && nextInput === block.input && !nameChanged) { return block;
}
const next = { ...(block as Record<string, unknown>) }; if (nameChanged && normalizedName) {
next.name = normalizedName;
} if (nextArgs !== block.arguments || Object.hasOwn(block, "arguments")) {
next.arguments = nextArgs;
} if (nextInput !== block.input || Object.hasOwn(block, "input")) {
next.input = nextInput;
} return next as RawToolCallBlock;
}
function countRawToolCallBlocks(content: unknown[]): number {
let count = 0; for (const block of content) { if (isRawToolCallBlock(block)) {
count += 1;
}
} return count;
}
function isReplaySafeThinkingAssistantTurn(
content: unknown[],
allowedToolNames: Set<string> | null,
): boolean {
let sawToolCall = false; const seenToolCallIds = new Set<string>(); for (const block of content) { if (!isRawToolCallBlock(block)) { continue;
}
sawToolCall = true; const toolCallId = typeof block.id === "string" ? block.id.trim() : ""; if (
!hasToolCallInput(block) ||
!toolCallId ||
seenToolCallIds.has(toolCallId) ||
!isAllowedToolCallName(block.name, allowedToolNames)
) { returnfalse;
}
seenToolCallIds.add(toolCallId); if (sanitizeToolCallBlock(block) !== block) { returnfalse;
}
} return sawToolCall;
}
function makeMissingToolResult(params: {
toolCallId: string;
toolName?: string; // OpenAI Responses/Codex replay should match upstream Codex's "aborted" // function_call_output normalization; live coverage in // openai-reasoning-compat.live.test.ts and tool-replay-repair.live.test.ts // sends this repaired history to real models. Other providers keep the older, // explicit OpenClaw diagnostic text unless the caller opts in.
text?: string;
}): Extract<AgentMessage, { role: "toolResult" }> { return {
role: "toolResult",
toolCallId: params.toolCallId,
toolName: params.toolName ?? "unknown",
content: [
{
type: "text",
text:
params.text ?? "[openclaw] missing tool result in session history; inserted synthetic error result for transcript repair.",
},
],
isError: true,
timestamp: Date.now(),
} as Extract<AgentMessage, { role: "toolResult" }>;
}
export type ToolCallInputRepairOptions = {
allowedToolNames?: Iterable<string>;
allowProviderOwnedThinkingReplay?: boolean;
};
export type ErroredAssistantResultPolicy = "preserve" | "drop";
export type ToolUseResultPairingOptions = {
erroredAssistantResultPolicy?: ErroredAssistantResultPolicy;
missingToolResultText?: string;
};
export function stripToolResultDetails(messages: AgentMessage[]): AgentMessage[] {
let touched = false; const out: AgentMessage[] = []; for (const msg of messages) { if (!msg || typeof msg !== "object" || (msg as { role?: unknown }).role !== "toolResult") {
out.push(msg); continue;
} if (!("details" in msg)) {
out.push(msg); continue;
} const sanitized = { ...(msg as object) } as { details?: unknown }; delete sanitized.details;
touched = true;
out.push(sanitized as unknown as AgentMessage);
} return touched ? out : messages;
}
export function repairToolCallInputs(
messages: AgentMessage[],
options?: ToolCallInputRepairOptions,
): ToolCallInputRepairReport {
let droppedToolCalls = 0;
let droppedAssistantMessages = 0;
let changed = false; const out: AgentMessage[] = []; const allowedToolNames = normalizeAllowedToolNames(options?.allowedToolNames); const allowProviderOwnedThinkingReplay = options?.allowProviderOwnedThinkingReplay === true; const claimedReplaySafeToolCallIds = new Set<string>();
for (const msg of messages) { if (!msg || typeof msg !== "object") {
out.push(msg); continue;
}
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
out.push(msg); continue;
}
if (
allowProviderOwnedThinkingReplay &&
msg.content.some((block) => isThinkingLikeBlock(block)) &&
countRawToolCallBlocks(msg.content) > 0
) { // Signed Anthropic thinking blocks must remain byte-for-byte stable on // replay. Preserve the turn only if every sibling tool call is already // valid and requires no redaction or normalization. Otherwise drop the // whole assistant turn rather than mutating provider-owned content. const replaySafeToolCalls = extractToolCallsFromAssistant(msg); if (
isReplaySafeThinkingAssistantTurn(msg.content, allowedToolNames) &&
replaySafeToolCalls.every((toolCall) => !claimedReplaySafeToolCallIds.has(toolCall.id))
) { for (const toolCall of replaySafeToolCalls) {
claimedReplaySafeToolCallIds.add(toolCall.id);
}
out.push(msg);
} else {
droppedToolCalls += countRawToolCallBlocks(msg.content);
droppedAssistantMessages += 1;
changed = true;
} continue;
}
const nextContent: typeof msg.content = [];
let droppedInMessage = 0;
let messageChanged = false;
for (const block of msg.content) { if (
isRawToolCallBlock(block) &&
(!hasToolCallInput(block) ||
!hasToolCallId(block) ||
!isAllowedToolCallName((block as RawToolCallBlock).name, allowedToolNames))
) {
droppedToolCalls += 1;
droppedInMessage += 1;
changed = true;
messageChanged = true; continue;
} if (isRawToolCallBlock(block)) { if (
(block as { type?: unknown }).type === "toolCall" ||
(block as { type?: unknown }).type === "toolUse" ||
(block as { type?: unknown }).type === "functionCall"
) { // Only sanitize (redact) sessions_spawn blocks; all others are passed through // unchanged to preserve provider-specific shapes (e.g. toolUse.input for Anthropic). const blockName = typeof (block as { name?: unknown }).name === "string"
? (block as { name: string }).name.trim()
: undefined; if (normalizeLowercaseStringOrEmpty(blockName) === "sessions_spawn") { const sanitized = sanitizeToolCallBlock(block); if (sanitized !== block) {
changed = true;
messageChanged = true;
}
nextContent.push(sanitized as typeof block);
} else { if (typeof (block as { name?: unknown }).name === "string") { const rawName = (block as { name: string }).name; const trimmedName = rawName.trim(); if (rawName !== trimmedName && trimmedName) { const renamed = { ...(block as object), name: trimmedName } as typeof block;
nextContent.push(renamed);
changed = true;
messageChanged = true;
} else {
nextContent.push(block);
}
} else {
nextContent.push(block);
}
} continue;
}
} else {
nextContent.push(block);
}
}
function shouldDropErroredAssistantResults(options?: ToolUseResultPairingOptions): boolean { return options?.erroredAssistantResultPolicy === "drop";
}
export function repairToolUseResultPairing(
messages: AgentMessage[],
options?: ToolUseResultPairingOptions,
): ToolUseRepairReport { // Anthropic (and Cloud Code Assist) reject transcripts where assistant tool calls are not // immediately followed by matching tool results. Session files can end up with results // displaced (e.g. after user turns) or duplicated. Repair by: // - moving matching toolResult messages directly after their assistant toolCall turn // - inserting synthetic error toolResults for missing ids // - dropping duplicate toolResults for the same id (anywhere in the transcript) const out: AgentMessage[] = []; const added: Array<Extract<AgentMessage, { role: "toolResult" }>> = []; const seenToolResultIds = new Set<string>();
let droppedDuplicateCount = 0;
let droppedOrphanCount = 0;
let moved = false;
let changed = false;
const pushToolResult = (msg: Extract<AgentMessage, { role: "toolResult" }>) => { const id = extractToolResultId(msg); if (id && seenToolResultIds.has(id)) {
droppedDuplicateCount += 1;
changed = true; return;
} if (id) {
seenToolResultIds.add(id);
}
out.push(msg);
};
for (let i = 0; i < messages.length; i += 1) { const msg = messages[i]; if (!msg || typeof msg !== "object") {
out.push(msg); continue;
}
const role = (msg as { role?: unknown }).role; if (role !== "assistant") { // Tool results must only appear directly after the matching assistant tool call turn. // Any "free-floating" toolResult entries in session history can make strict providers // (Anthropic-compatible APIs, MiniMax, Cloud Code Assist) reject the entire request. if (role !== "toolResult") {
out.push(msg);
} else {
droppedOrphanCount += 1;
changed = true;
} continue;
}
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
let j = i + 1; for (; j < messages.length; j += 1) { const next = messages[j]; if (!next || typeof next !== "object") {
remainder.push(next); continue;
}
const nextRole = (next as { role?: unknown }).role; if (nextRole === "assistant") { break;
}
if (nextRole === "toolResult") { const toolResult = next as Extract<AgentMessage, { role: "toolResult" }>; const id = extractToolResultId(toolResult); if (id && toolCallIds.has(id)) { if (seenToolResultIds.has(id)) {
droppedDuplicateCount += 1;
changed = true; continue;
} const normalizedToolResult = normalizeToolResultName(
toolResult,
toolCallNamesById.get(id),
); if (normalizedToolResult !== toolResult) {
changed = true;
} if (!spanResultsById.has(id)) {
spanResultsById.set(id, normalizedToolResult);
} continue;
}
}
// Drop tool results that don't match the current assistant tool calls. if (nextRole !== "toolResult") {
remainder.push(next);
} else {
droppedOrphanCount += 1;
changed = true;
}
}
// Aborted/errored assistant turns should never synthesize missing tool results, but // the replay sanitizer can still legitimately retain real tool results for surviving // tool calls in the same turn after malformed siblings are dropped. const stopReason = (assistant as { stopReason?: string }).stopReason; if (stopReason === "error" || stopReason === "aborted") { if (!shouldDropErroredAssistantResults(options)) {
out.push(msg); for (const toolCall of toolCalls) { const result = spanResultsById.get(toolCall.id); if (!result) { continue;
}
pushToolResult(result);
}
} elseif (spanResultsById.size > 0) {
changed = true;
} else {
changed = true;
} for (const rem of remainder) {
out.push(rem);
}
i = j - 1; continue;
}
out.push(msg);
if (spanResultsById.size > 0 && remainder.length > 0) { // Preserve real late-arriving results before synthesizing missing siblings; // otherwise parallel tool replay can replace useful output with repair noise.
moved = true;
changed = true;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.