fix(FN-4930): make promptSessionAndCheck transcript diagnostics recursion-safe
Fusion-Task-Id: FN-4930 Fusion-Task-Lineage: 5fb64145-a526-4aec-846e-5771b5844b18
This commit is contained in:
committed by
gsxdsm
parent
0e4fda59b8
commit
24f5c238a3
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make `promptSessionAndCheck` transcript diagnostics circular-safe to prevent stack overflows on malformed message metadata.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const warnMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../logger.js", () => ({
|
||||||
|
createLogger: () => ({
|
||||||
|
log: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: warnMock,
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("promptSessionAndCheck recursion guard (FN-4930)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
warnMock.mockReset();
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not overflow when transcript-inspection metadata has pathological toJSON", async () => {
|
||||||
|
const { promptSessionAndCheck } = await import("../pi.js");
|
||||||
|
const recursive = {
|
||||||
|
toJSON() {
|
||||||
|
return { recursive };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
errorMessage: "",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "ok",
|
||||||
|
toolName: recursive,
|
||||||
|
stopReason: recursive,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn(async () => {
|
||||||
|
state.errorMessage = "Cannot read properties of undefined (reading 'foo')";
|
||||||
|
}),
|
||||||
|
state,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await expect(promptSessionAndCheck(session, "hello")).rejects.not.toThrow(/Maximum call stack/i);
|
||||||
|
expect(warnMock).not.toHaveBeenCalledWith(expect.stringContaining("failed to inspect transcript"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -221,7 +221,36 @@ function isThinkingReasoningConflictError(message: string): boolean {
|
|||||||
return /cannot specify both\s+['"]?thinking['"]?\s+and\s+['"]?reasoning_effort['"]?/i.test(message);
|
return /cannot specify both\s+['"]?thinking['"]?\s+and\s+['"]?reasoning_effort['"]?/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptSessionAndCheck(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
|
function coercePreviewValue(value: unknown): string {
|
||||||
|
if (value === null) return "null";
|
||||||
|
if (value === undefined) return "undefined";
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
if (typeof value === "symbol") {
|
||||||
|
return value.description ? `symbol(${value.description})` : "symbol";
|
||||||
|
}
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return `[function ${value.name || "anonymous"}]`;
|
||||||
|
}
|
||||||
|
return Object.prototype.toString.call(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safePreviewJson(value: unknown): string {
|
||||||
|
const seen = new WeakSet<object>();
|
||||||
|
return JSON.stringify(value, (_key, candidate) => {
|
||||||
|
if (typeof candidate === "object" && candidate !== null) {
|
||||||
|
if (seen.has(candidate)) {
|
||||||
|
return "[Circular]";
|
||||||
|
}
|
||||||
|
seen.add(candidate);
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promptSessionAndCheck(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
|
||||||
clearSessionStateError(session);
|
clearSessionStateError(session);
|
||||||
if (options === undefined) {
|
if (options === undefined) {
|
||||||
await session.prompt(prompt);
|
await session.prompt(prompt);
|
||||||
@@ -244,13 +273,13 @@ async function promptSessionAndCheck(session: AgentSession, prompt: string, opti
|
|||||||
const content = m?.content;
|
const content = m?.content;
|
||||||
return {
|
return {
|
||||||
index: i < 0 ? idx : i,
|
index: i < 0 ? idx : i,
|
||||||
role: m?.role,
|
role: coercePreviewValue(m?.role),
|
||||||
contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
|
contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
|
||||||
toolName: (m as { toolName?: unknown }).toolName,
|
toolName: coercePreviewValue((m as { toolName?: unknown }).toolName),
|
||||||
stopReason: (m as { stopReason?: unknown }).stopReason,
|
stopReason: coercePreviewValue((m as { stopReason?: unknown }).stopReason),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
piLog.error(`pi state error — transcript tail (${messages.length} msgs total): ${JSON.stringify(recent)}`);
|
piLog.error(`pi state error — transcript tail (${messages.length} msgs total): ${safePreviewJson(recent)}`);
|
||||||
} else {
|
} else {
|
||||||
piLog.error(`pi state error — state.messages is not an array: ${typeof messages}`);
|
piLog.error(`pi state error — state.messages is not an array: ${typeof messages}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user