


















@@ -1,6 +1,8 @@
1-import type { v2 } from "./protocol.js";
1+import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
2+import type { JsonValue, v2 } from "./protocol.js";
2334export const CODEX_APP_INVENTORY_CACHE_TTL_MS = 60 * 60 * 1_000;
5+const MAX_SERIALIZED_ERROR_MESSAGE_LENGTH = 500;
4657export type CodexAppInventoryRequest = (
68method: "app/list",
@@ -50,12 +52,15 @@ type RefreshParams = {
5052request: CodexAppInventoryRequest;
5153nowMs?: number;
5254forceRefetch?: boolean;
55+suppressRefresh?: boolean;
5356};
54575558export class CodexAppInventoryCache {
5659private readonly ttlMs: number;
5760private readonly entries = new Map<string, CacheEntry>();
5861private readonly inFlight = new Map<string, Promise<CodexAppInventorySnapshot>>();
62+// Per-key refresh generation. Each refresh attempt claims the next token so
63+// an older request that finishes late cannot overwrite a newer snapshot.
5964private readonly refreshTokens = new Map<string, number>();
6065private readonly diagnostics = new Map<string, CodexAppInventoryCacheDiagnostic>();
6166private revision = 0;
@@ -68,7 +73,7 @@ export class CodexAppInventoryCache {
6873const nowMs = params.nowMs ?? Date.now();
6974const entry = this.entries.get(params.key);
7075if (!entry) {
71-const refreshScheduled = this.scheduleRefresh(params);
76+const refreshScheduled = params.suppressRefresh ? false : this.scheduleRefresh(params);
7277return {
7378state: "missing",
7479key: params.key,
@@ -168,26 +173,49 @@ export class CodexAppInventoryCache {
168173expiresAtMs: nowMs + this.ttlMs,
169174revision: this.revision,
170175};
176+// Only publish this snapshot if no newer refresh started for the same key
177+// while this request was in flight.
171178if (this.refreshTokens.get(params.key) === refreshToken) {
172179this.entries.set(params.key, { ...snapshot, invalidated: false });
173180this.diagnostics.delete(params.key);
174181}
175182return snapshot;
176183} catch (error) {
177184const diagnostic = {
178-message: error instanceof Error ? error.message : String(error),
185+message: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
179186atMs: nowMs,
180187};
181188this.diagnostics.set(params.key, diagnostic);
182189const entry = this.entries.get(params.key);
183190if (entry) {
184191entry.lastError = diagnostic;
185192}
193+embeddedAgentLog.warn("codex app inventory refresh failed", {
194+forceRefetch: params.forceRefetch === true,
195+keyFingerprint: fingerprintInventoryCacheKey(params.key),
196+error: serializeCodexAppInventoryError(error),
197+});
186198throw error;
187199}
188200}
189201}
190202203+export function serializeCodexAppInventoryError(error: unknown): Record<string, unknown> {
204+const record = isRecord(error) ? error : undefined;
205+const data = record && "data" in record ? redactErrorData(record.data) : undefined;
206+return {
207+name:
208+error instanceof Error
209+ ? error.name
210+ : typeof record?.name === "string"
211+ ? record.name
212+ : undefined,
213+message: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
214+ ...(typeof record?.code === "number" ? { code: record.code } : {}),
215+ ...(data !== undefined ? { data } : {}),
216+};
217+}
218+191219export const defaultCodexAppInventoryCache = new CodexAppInventoryCache();
192220193221export function buildCodexAppInventoryCacheKey(input: CodexAppInventoryCacheKeyInput): string {
@@ -223,3 +251,74 @@ function stripEntryState(entry: CacheEntry): CodexAppInventorySnapshot {
223251const { invalidated: _invalidated, ...snapshot } = entry;
224252return snapshot;
225253}
254+255+function fingerprintInventoryCacheKey(key: string): string {
256+let hash = 0;
257+for (let index = 0; index < key.length; index += 1) {
258+hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
259+}
260+return hash.toString(16).padStart(8, "0");
261+}
262+263+function isRecord(value: unknown): value is Record<string, unknown> {
264+return Boolean(value && typeof value === "object" && !Array.isArray(value));
265+}
266+267+function redactErrorData(value: unknown, depth = 0): JsonValue | undefined {
268+if (value === undefined) {
269+return undefined;
270+}
271+if (value === null || typeof value === "boolean" || typeof value === "number") {
272+return value;
273+}
274+if (depth > 6) {
275+return "[truncated]";
276+}
277+if (Array.isArray(value)) {
278+return value.map((entry) => redactErrorData(entry, depth + 1) ?? null);
279+}
280+if (isRecord(value)) {
281+const redacted: Record<string, JsonValue> = {};
282+for (const [key, entry] of Object.entries(value)) {
283+redacted[key] = isSensitiveErrorDataKey(key)
284+ ? "<redacted>"
285+ : (redactErrorData(entry, depth + 1) ?? null);
286+}
287+return redacted;
288+}
289+if (typeof value === "string" && value.length > 500) {
290+return `${value.slice(0, 500)}...`;
291+}
292+if (typeof value === "string") {
293+return value;
294+}
295+if (typeof value === "bigint") {
296+return value.toString();
297+}
298+if (typeof value === "symbol") {
299+return value.description ? `Symbol(${value.description})` : "Symbol()";
300+}
301+if (typeof value === "function") {
302+return value.name ? `[function ${value.name}]` : "[function]";
303+}
304+return "[unserializable]";
305+}
306+307+function sanitizeErrorMessage(message: string): string {
308+const htmlStart = message.search(/<html[\s>]/i);
309+const withoutHtml =
310+htmlStart >= 0
311+ ? `${message.slice(0, htmlStart).trimEnd()} [HTML response body omitted]`
312+ : message;
313+const redacted = withoutHtml.replace(
314+/([?&][^=\s"'<>]*(?:api[_-]?key|authorization|cookie|credential|password|secret|token|tk)[^=\s"'<>]*=)[^&\s"'<>]+/gi,
315+"$1<redacted>",
316+);
317+return redacted.length > MAX_SERIALIZED_ERROR_MESSAGE_LENGTH
318+ ? `${redacted.slice(0, MAX_SERIALIZED_ERROR_MESSAGE_LENGTH)}...`
319+ : redacted;
320+}
321+322+function isSensitiveErrorDataKey(key: string): boolean {
323+return /api[_-]?key|authorization|cookie|credential|password|secret|token/i.test(key);
324+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。