























@@ -254,6 +254,16 @@ describe("tool-loop-detection", () => {
254254expect(timestamp).toBeLessThanOrEqual(after);
255255});
256256257+it("records run id when provided", () => {
258+const state = createState();
259+260+recordToolCall(state, "tool", { arg: 1 }, "call-run", enabledLoopDetectionConfig, {
261+runId: "run-1",
262+});
263+264+expect(state.toolCallHistory?.[0]?.runId).toBe("run-1");
265+});
266+257267it("respects configured historySize", () => {
258268const state = createState();
259269@@ -294,6 +304,59 @@ describe("tool-loop-detection", () => {
294304expect(result.stuck).toBe(false);
295305});
296306307+it("ignores repeated history from other runs", () => {
308+const state = createState();
309+const params = { path: "/same.txt" };
310+311+for (let i = 0; i < WARNING_THRESHOLD; i += 1) {
312+recordToolCall(state, "read", params, `old-run-${i}`, enabledLoopDetectionConfig, {
313+runId: "heartbeat-1",
314+});
315+}
316+317+const result = detectToolCallLoop(state, "read", params, enabledLoopDetectionConfig, {
318+runId: "heartbeat-2",
319+});
320+321+expect(result.stuck).toBe(false);
322+});
323+324+it("detects repeated history within the same run", () => {
325+const state = createState();
326+const params = { path: "/same.txt" };
327+328+for (let i = 0; i < WARNING_THRESHOLD; i += 1) {
329+recordToolCall(state, "read", params, `same-run-${i}`, enabledLoopDetectionConfig, {
330+runId: "run-1",
331+});
332+}
333+334+const result = detectToolCallLoop(state, "read", params, enabledLoopDetectionConfig, {
335+runId: "run-1",
336+});
337+338+expect(result.stuck).toBe(true);
339+if (result.stuck) {
340+expect(result.detector).toBe("generic_repeat");
341+expect(result.count).toBe(WARNING_THRESHOLD);
342+}
343+});
344+345+it("keeps scoped and unscoped history isolated", () => {
346+const state = createState();
347+const params = { path: "/same.txt" };
348+349+for (let i = 0; i < WARNING_THRESHOLD; i += 1) {
350+recordToolCall(state, "read", params, `scoped-${i}`, enabledLoopDetectionConfig, {
351+runId: "run-1",
352+});
353+}
354+355+const result = detectToolCallLoop(state, "read", params, enabledLoopDetectionConfig);
356+357+expect(result.stuck).toBe(false);
358+});
359+297360it("warns on generic repeated tool+args calls", () => {
298361const state = createState();
299362for (let i = 0; i < WARNING_THRESHOLD; i += 1) {
@@ -748,6 +811,28 @@ describe("tool-loop-detection", () => {
748811expect(entry?.resultHash?.length).toBe(64);
749812});
750813814+it("does not attach outcomes to matching calls from other runs", () => {
815+const state = createState();
816+const params = { path: "/same.txt" };
817+recordToolCall(state, "read", params, "call-1", enabledLoopDetectionConfig, {
818+runId: "run-1",
819+});
820+821+recordToolCallOutcome(state, {
822+toolName: "read",
823+toolParams: params,
824+toolCallId: "call-1",
825+result: { content: [{ type: "text", text: "same output" }] },
826+config: enabledLoopDetectionConfig,
827+runId: "run-2",
828+});
829+830+expect(state.toolCallHistory).toHaveLength(2);
831+expect(state.toolCallHistory?.[0]?.resultHash).toBeUndefined();
832+expect(state.toolCallHistory?.[1]?.runId).toBe("run-2");
833+expect(state.toolCallHistory?.[1]?.resultHash).toBeTypeOf("string");
834+});
835+751836it("handles empty history", () => {
752837const state = createState();
753838此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。