fix: tighten mutating error retry matching

This commit is contained in:
Gustavo Madeira Santana
2026-02-14 16:53:55 -05:00
parent 99e7e054ee
commit 0e721e6665
2 changed files with 99 additions and 2 deletions

View File

@@ -169,8 +169,13 @@ function sameToolAction(
meta?: string,
actionFingerprint?: string,
): boolean {
if (existing.actionFingerprint && actionFingerprint) {
return existing.actionFingerprint === actionFingerprint;
if (existing.actionFingerprint != null || actionFingerprint != null) {
// For mutating flows, fail closed: only clear when both fingerprints exist and match.
return (
existing.actionFingerprint != null &&
actionFingerprint != null &&
existing.actionFingerprint === actionFingerprint
);
}
return existing.toolName === toolName && (existing.meta ?? "") === (meta ?? "");
}

View File

@@ -412,6 +412,98 @@ describe("subscribeEmbeddedPiSession", () => {
expect(subscription.getLastToolError()).toBeUndefined();
});
it("keeps unresolved mutating failure when same tool succeeds on a different target", () => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {
handler = fn;
return () => {};
},
};
const subscription = subscribeEmbeddedPiSession({
session: session as unknown as Parameters<typeof subscribeEmbeddedPiSession>[0]["session"],
runId: "run-tools-3",
sessionKey: "test-session",
});
handler?.({
type: "tool_execution_start",
toolName: "write",
toolCallId: "w1",
args: { path: "/tmp/a.txt", content: "first" },
});
handler?.({
type: "tool_execution_end",
toolName: "write",
toolCallId: "w1",
isError: true,
result: { error: "disk full" },
});
handler?.({
type: "tool_execution_start",
toolName: "write",
toolCallId: "w2",
args: { path: "/tmp/b.txt", content: "second" },
});
handler?.({
type: "tool_execution_end",
toolName: "write",
toolCallId: "w2",
isError: false,
result: { ok: true },
});
expect(subscription.getLastToolError()?.toolName).toBe("write");
});
it("keeps unresolved session_status model-mutation failure on later read-only status success", () => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {
handler = fn;
return () => {};
},
};
const subscription = subscribeEmbeddedPiSession({
session: session as unknown as Parameters<typeof subscribeEmbeddedPiSession>[0]["session"],
runId: "run-tools-4",
sessionKey: "test-session",
});
handler?.({
type: "tool_execution_start",
toolName: "session_status",
toolCallId: "s1",
args: { sessionKey: "agent:main:main", model: "openai/gpt-4o" },
});
handler?.({
type: "tool_execution_end",
toolName: "session_status",
toolCallId: "s1",
isError: true,
result: { error: "Model not allowed." },
});
handler?.({
type: "tool_execution_start",
toolName: "session_status",
toolCallId: "s2",
args: { sessionKey: "agent:main:main" },
});
handler?.({
type: "tool_execution_end",
toolName: "session_status",
toolCallId: "s2",
isError: false,
result: { ok: true },
});
expect(subscription.getLastToolError()?.toolName).toBe("session_status");
});
it("emits lifecycle:error event on agent_end when last assistant message was an error", async () => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {