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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (options === undefined) {
|
||||
await session.prompt(prompt);
|
||||
@@ -244,13 +273,13 @@ async function promptSessionAndCheck(session: AgentSession, prompt: string, opti
|
||||
const content = m?.content;
|
||||
return {
|
||||
index: i < 0 ? idx : i,
|
||||
role: m?.role,
|
||||
role: coercePreviewValue(m?.role),
|
||||
contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
|
||||
toolName: (m as { toolName?: unknown }).toolName,
|
||||
stopReason: (m as { stopReason?: unknown }).stopReason,
|
||||
toolName: coercePreviewValue((m as { toolName?: unknown }).toolName),
|
||||
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 {
|
||||
piLog.error(`pi state error — state.messages is not an array: ${typeof messages}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user