fix(agents): land #38935 from @MumuTW
Co-authored-by: MumuTW <MumuTW@users.noreply.github.com>
This commit is contained in:
@@ -249,6 +249,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Models/auth token prompts: guard cancelled manual token prompts so `Symbol(clack:cancel)` values cannot be persisted into auth profiles; adds regression coverage for cancelled `models auth paste-token`. (#38951) Thanks @MumuTW.
|
||||
- Gateway/loopback announce URLs: treat `http://` and `https://` aliases with the same loopback/private-network policy as websocket URLs so loopback cron announce delivery no longer fails secure URL validation. (#39064) Thanks @Narcooo.
|
||||
- Models/default provider fallback: when the hardcoded default provider is removed from `models.providers`, resolve defaults from configured providers instead of reporting stale removed-provider defaults in status output. (#38947) Thanks @davidemanuelDEV.
|
||||
- Agents/cache-trace stability: guard stable stringify against circular references in trace payloads so near-limit payloads no longer crash with `Maximum call stack size exceeded`; adds regression coverage. (#38935) Thanks @MumuTW.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
||||
@@ -144,4 +144,35 @@ describe("createCacheTrace", () => {
|
||||
expect(source.bytes).toBe(6);
|
||||
expect(source.sha256).toBe(crypto.createHash("sha256").update("U0VDUkVU").digest("hex"));
|
||||
});
|
||||
|
||||
it("handles circular references in messages without stack overflow", () => {
|
||||
const lines: string[] = [];
|
||||
const trace = createCacheTrace({
|
||||
cfg: {
|
||||
diagnostics: {
|
||||
cacheTrace: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
writer: {
|
||||
filePath: "memory",
|
||||
write: (line) => lines.push(line),
|
||||
},
|
||||
});
|
||||
|
||||
const parent: Record<string, unknown> = { role: "user", content: "hello" };
|
||||
const child: Record<string, unknown> = { ref: parent };
|
||||
parent.child = child; // circular reference
|
||||
|
||||
trace?.recordStage("prompt:images", {
|
||||
messages: [parent] as unknown as [],
|
||||
});
|
||||
|
||||
expect(lines.length).toBe(1);
|
||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
||||
expect(event.messageCount).toBe(1);
|
||||
expect(event.messageFingerprints).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ function getWriter(filePath: string): CacheTraceWriter {
|
||||
return getQueuedFileWriter(writers, filePath);
|
||||
}
|
||||
|
||||
function stableStringify(value: unknown): string {
|
||||
function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {
|
||||
if (value === null || value === undefined) {
|
||||
return String(value);
|
||||
}
|
||||
@@ -117,30 +117,40 @@ function stableStringify(value: unknown): string {
|
||||
if (typeof value !== "object") {
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
return JSON.stringify("[Circular]");
|
||||
}
|
||||
seen.add(value);
|
||||
if (value instanceof Error) {
|
||||
return stableStringify({
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
});
|
||||
return stableStringify(
|
||||
{
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
},
|
||||
seen,
|
||||
);
|
||||
}
|
||||
if (value instanceof Uint8Array) {
|
||||
return stableStringify({
|
||||
type: "Uint8Array",
|
||||
data: Buffer.from(value).toString("base64"),
|
||||
});
|
||||
return stableStringify(
|
||||
{
|
||||
type: "Uint8Array",
|
||||
data: Buffer.from(value).toString("base64"),
|
||||
},
|
||||
seen,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const serializedEntries: string[] = [];
|
||||
for (const entry of value) {
|
||||
serializedEntries.push(stableStringify(entry));
|
||||
serializedEntries.push(stableStringify(entry, seen));
|
||||
}
|
||||
return `[${serializedEntries.join(",")}]`;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const serializedFields: string[] = [];
|
||||
for (const key of Object.keys(record).toSorted()) {
|
||||
serializedFields.push(`${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
||||
serializedFields.push(`${JSON.stringify(key)}:${stableStringify(record[key], seen)}`);
|
||||
}
|
||||
return `{${serializedFields.join(",")}}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user