mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix: reserve preserved signed replay ids by owner
This commit is contained in:
@@ -316,9 +316,10 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
||||
});
|
||||
|
||||
expect(out).toBe(input);
|
||||
expect(((out[0] as Extract<AgentMessage, { role: "assistant" }>).content?.[1] as { id?: string }).id).toBe(
|
||||
"call_1",
|
||||
);
|
||||
expect(
|
||||
((out[0] as Extract<AgentMessage, { role: "assistant" }>).content?.[1] as { id?: string })
|
||||
.id,
|
||||
).toBe("call_1");
|
||||
expect((out[1] as Extract<AgentMessage, { role: "toolResult" }>).toolCallId).toBe("call_1");
|
||||
});
|
||||
|
||||
@@ -368,6 +369,55 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
||||
expect((out[3] as Extract<AgentMessage, { role: "toolResult" }>).toolCallId).toBe("call1");
|
||||
});
|
||||
|
||||
it("rewrites later signed turns when an earlier signed turn already owns the raw id", () => {
|
||||
const input = castAgentMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
|
||||
{ type: "toolCall", id: "call1", name: "read", arguments: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call1",
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "first" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_2" },
|
||||
{ type: "toolCall", id: "call1", name: "read", arguments: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call1",
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "second" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict", {
|
||||
preserveReplaySafeThinkingToolCallIds: true,
|
||||
allowedToolNames: ["read"],
|
||||
});
|
||||
|
||||
expect(out).not.toBe(input);
|
||||
const firstAssistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const secondAssistant = out[2] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const firstToolCall = firstAssistant.content?.[1] as { id?: string };
|
||||
const secondToolCall = secondAssistant.content?.[1] as { id?: string };
|
||||
expect(firstToolCall.id).toBe("call1");
|
||||
expect(secondToolCall.id).not.toBe("call1");
|
||||
expect(secondToolCall.id).not.toBe(firstToolCall.id);
|
||||
expect((out[1] as Extract<AgentMessage, { role: "toolResult" }>).toolCallId).toBe("call1");
|
||||
expect((out[3] as Extract<AgentMessage, { role: "toolResult" }>).toolCallId).toBe(
|
||||
secondToolCall.id,
|
||||
);
|
||||
});
|
||||
|
||||
it("avoids collisions with alphanumeric-only suffixes", () => {
|
||||
const input = buildDuplicateIdCollisionInput();
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ function isRedactedSessionsSpawnAttachment(item: unknown): boolean {
|
||||
if (!(SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS as readonly string[]).includes(key)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof attachment[key] !== "string" || (attachment[key] as string).trim().length === 0) {
|
||||
if (typeof attachment[key] !== "string" || attachment[key].trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -218,10 +218,7 @@ function isReplaySafeThinkingAssistantMessage(
|
||||
continue;
|
||||
}
|
||||
const typedBlock = block as ReplaySafeToolCallBlock;
|
||||
if (
|
||||
typeof typedBlock.type !== "string" ||
|
||||
!TOOL_CALL_TYPES.has(typedBlock.type)
|
||||
) {
|
||||
if (typeof typedBlock.type !== "string" || !TOOL_CALL_TYPES.has(typedBlock.type)) {
|
||||
continue;
|
||||
}
|
||||
sawToolCall = true;
|
||||
@@ -243,21 +240,28 @@ function isReplaySafeThinkingAssistantMessage(
|
||||
function collectReplaySafeThinkingToolIds(
|
||||
messages: AgentMessage[],
|
||||
allowedToolNames: Set<string> | null,
|
||||
): Set<string> {
|
||||
): { reservedIds: Set<string>; preservedIndexes: Set<number> } {
|
||||
const reserved = new Set<string>();
|
||||
for (const message of messages) {
|
||||
const preservedIndexes = new Set<number>();
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const message = messages[index];
|
||||
if (!message || typeof message !== "object" || message.role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
const assistant = message as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const assistant = message;
|
||||
if (!isReplaySafeThinkingAssistantMessage(assistant, allowedToolNames)) {
|
||||
continue;
|
||||
}
|
||||
for (const toolCall of extractToolCallsFromAssistant(assistant)) {
|
||||
const toolCalls = extractToolCallsFromAssistant(assistant);
|
||||
if (toolCalls.some((toolCall) => reserved.has(toolCall.id))) {
|
||||
continue;
|
||||
}
|
||||
preservedIndexes.add(index);
|
||||
for (const toolCall of toolCalls) {
|
||||
reserved.add(toolCall.id);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
return { reservedIds: reserved, preservedIndexes };
|
||||
}
|
||||
|
||||
export function isValidCloudCodeAssistToolId(id: string, mode: ToolCallIdMode = "strict"): boolean {
|
||||
@@ -505,27 +509,24 @@ export function sanitizeToolCallIdsForCloudCodeAssist(
|
||||
const allowedToolNames = normalizeAllowedToolNames(options?.allowedToolNames);
|
||||
const preserveReplaySafeThinkingToolCallIds =
|
||||
options?.preserveReplaySafeThinkingToolCallIds === true;
|
||||
const reservedIds = preserveReplaySafeThinkingToolCallIds
|
||||
const replaySafeThinking = preserveReplaySafeThinkingToolCallIds
|
||||
? collectReplaySafeThinkingToolIds(messages, allowedToolNames)
|
||||
: undefined;
|
||||
const { resolveAssistantId, resolveToolResultId, preserveAssistantId } =
|
||||
createOccurrenceAwareResolver(mode, {
|
||||
...options,
|
||||
reservedIds,
|
||||
reservedIds: replaySafeThinking?.reservedIds,
|
||||
});
|
||||
|
||||
let changed = false;
|
||||
const out = messages.map((msg) => {
|
||||
const out = messages.map((msg, index) => {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return msg;
|
||||
}
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
if (role === "assistant") {
|
||||
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||
if (
|
||||
preserveReplaySafeThinkingToolCallIds &&
|
||||
isReplaySafeThinkingAssistantMessage(assistant, allowedToolNames)
|
||||
) {
|
||||
if (replaySafeThinking?.preservedIndexes.has(index)) {
|
||||
for (const toolCall of extractToolCallsFromAssistant(assistant)) {
|
||||
preserveAssistantId(toolCall.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user