mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:08:37 +00:00
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.
|
- 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.
|
- 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.
|
- 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
|
## 2026.3.2
|
||||||
|
|
||||||
|
|||||||
@@ -144,4 +144,35 @@ describe("createCacheTrace", () => {
|
|||||||
expect(source.bytes).toBe(6);
|
expect(source.bytes).toBe(6);
|
||||||
expect(source.sha256).toBe(crypto.createHash("sha256").update("U0VDUkVU").digest("hex"));
|
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);
|
return getQueuedFileWriter(writers, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stableStringify(value: unknown): string {
|
function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
@@ -117,30 +117,40 @@ function stableStringify(value: unknown): string {
|
|||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
return JSON.stringify(value) ?? "null";
|
return JSON.stringify(value) ?? "null";
|
||||||
}
|
}
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return JSON.stringify("[Circular]");
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
return stableStringify({
|
return stableStringify(
|
||||||
name: value.name,
|
{
|
||||||
message: value.message,
|
name: value.name,
|
||||||
stack: value.stack,
|
message: value.message,
|
||||||
});
|
stack: value.stack,
|
||||||
|
},
|
||||||
|
seen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (value instanceof Uint8Array) {
|
if (value instanceof Uint8Array) {
|
||||||
return stableStringify({
|
return stableStringify(
|
||||||
type: "Uint8Array",
|
{
|
||||||
data: Buffer.from(value).toString("base64"),
|
type: "Uint8Array",
|
||||||
});
|
data: Buffer.from(value).toString("base64"),
|
||||||
|
},
|
||||||
|
seen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const serializedEntries: string[] = [];
|
const serializedEntries: string[] = [];
|
||||||
for (const entry of value) {
|
for (const entry of value) {
|
||||||
serializedEntries.push(stableStringify(entry));
|
serializedEntries.push(stableStringify(entry, seen));
|
||||||
}
|
}
|
||||||
return `[${serializedEntries.join(",")}]`;
|
return `[${serializedEntries.join(",")}]`;
|
||||||
}
|
}
|
||||||
const record = value as Record<string, unknown>;
|
const record = value as Record<string, unknown>;
|
||||||
const serializedFields: string[] = [];
|
const serializedFields: string[] = [];
|
||||||
for (const key of Object.keys(record).toSorted()) {
|
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(",")}}`;
|
return `{${serializedFields.join(",")}}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user