fix(status): preserve OAuth JSON diagnostics (#103106)

Co-authored-by: 0668001336 <wang.lizhang@xydigit.com>
This commit is contained in:
Peter Steinberger
2026-07-09 23:03:48 +01:00
committed by GitHub
parent d2163e672c
commit fdde2ecde4
2 changed files with 47 additions and 16 deletions

View File

@@ -24,6 +24,44 @@ describe("summarizeLogTail", () => {
expect(lines).toEqual(["[openai] token refresh 401 invalid_grant · re-auth required"]);
});
it.each([
["closing brace", "Session invalidated } due to signing in again"],
["opening brace", "Session invalidated { due to signing in again"],
["escaped quote before a brace", 'Session invalidated after "}" signing in again'],
])("keeps OAuth JSON diagnostics with a %s", (_caseName, message) => {
const sentinel = "[gateway] synthetic sentinel after OAuth JSON";
const lines = summarizeLogTail([
"[openai] Token refresh failed: 401 {",
`"error":${JSON.stringify({ code: "invalid_grant", message })}`,
"}",
sentinel,
]);
expect(lines).toEqual([
"[openai] token refresh 401 invalid_grant · re-auth required",
sentinel,
]);
});
it("parses the first complete OAuth JSON object before an adjacent object", () => {
const lines = summarizeLogTail([
"[openai] Token refresh failed: 401 {",
'"error":{"code":"invalid_grant","message":"Session invalidated due to signing in again"}}{"ignored":true}',
]);
expect(lines).toEqual(["[openai] token refresh 401 invalid_grant · re-auth required"]);
});
it("consumes an incomplete OAuth JSON block through the end of the tail", () => {
const lines = summarizeLogTail([
"[openai] Token refresh failed: 401 {",
'"error":{"code":"invalid_grant","message":"Session invalidated due to signing in again"}',
"unrelated-looking trailing line",
]);
expect(lines).toEqual(["[openai] token refresh 401"]);
});
it("keeps bounded OAuth diagnostics UTF-16 well-formed", () => {
const lines = summarizeLogTail([
"[openai] Token refresh failed: 500 {",

View File

@@ -5,6 +5,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { classifyOAuthRefreshFailureReason } from "../../agents/auth-profiles/oauth-refresh-failure.js";
import { readGatewayLogTailLines } from "../../daemon/diagnostics.js";
import { extractBalancedJsonPrefix } from "../../shared/balanced-json.js";
/** Reads the last non-empty lines from a gateway log file, returning an empty list on read failure. */
export async function readFileTailLines(filePath: string, maxLines: number): Promise<string[]> {
@@ -16,13 +17,6 @@ export async function readFileTailLines(filePath: string, maxLines: number): Pro
return out.map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
}
function countMatches(haystack: string, needle: string): number {
if (!haystack || !needle) {
return 0;
}
return haystack.split(needle).length - 1;
}
function shorten(message: string, maxLen: number): string {
const cleaned = message.replace(/\s+/g, " ").trim();
if (cleaned.length <= maxLen) {
@@ -51,16 +45,15 @@ function consumeJsonBlock(
return null;
}
const parts: string[] = [startLine.slice(braceAt)];
let depth = countMatches(parts[0] ?? "", "{") - countMatches(parts[0] ?? "", "}");
let i = startIndex;
while (depth > 0 && i + 1 < lines.length) {
i += 1;
const next = lines[i] ?? "";
parts.push(next);
depth += countMatches(next, "{") - countMatches(next, "}");
const raw = [startLine.slice(braceAt), ...lines.slice(startIndex + 1)].join("\n");
const fragment = extractBalancedJsonPrefix(raw);
if (!fragment) {
// A bounded tail can end mid-object. Consume the rest so orphaned JSON
// fields do not escape into the user-facing diagnosis as ordinary lines.
return { json: raw, endIndex: lines.length - 1 };
}
return { json: parts.join("\n"), endIndex: i };
const consumedLineOffset = fragment.json.split("\n").length - 1;
return { json: fragment.json, endIndex: startIndex + consumedLineOffset };
}
/** Summarizes gateway log tail lines, grouping repeated failures and trimming long output. */