{"record":{"id":"faa2ae8bdf427e40","repo":"jackwener/OpenCLI","slug":"pipeline-template-sanitizecontext-failed-err","errorCode":null,"errorMessage":"[pipeline/template] sanitizeContext failed: ${err instanceof Error ? err.message : String(err)}. Returning {} for this branch. Likely cause: circular reference, Symbol, or other non-serializable value in pipeline context.","messagePattern":"\\[pipeline/template\\] sanitizeContext failed: (.+?)\\. Returning (.+?) for this branch\\. Likely cause: circular reference, Symbol, or other non-serializable value in pipeline context\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/pipeline/template.ts","lineNumber":222,"sourceCode":"const _sanitizeCache = new WeakMap<object, string>();\n\nfunction sanitizeContext(obj: unknown): unknown {\n  if (obj === null || obj === undefined) return obj;\n  if (typeof obj !== 'object' && typeof obj !== 'function') return obj;\n  const objRef = obj as object;\n  const cached = _sanitizeCache.get(objRef);\n  if (cached !== undefined) return JSON.parse(cached);\n  try {\n    // BigInt is non-serializable by default but is the most common cause of\n    // sanitizeContext failures (e.g. GraphQL 64-bit IDs). Coerce to string\n    // so callers see the value instead of a silent {}.\n    const jsonStr = JSON.stringify(obj, (_key, value) =>\n      typeof value === 'bigint' ? value.toString() : value,\n    );\n    _sanitizeCache.set(objRef, jsonStr);\n    return JSON.parse(jsonStr);\n  } catch (err) {\n    log.warn(\n      `[pipeline/template] sanitizeContext failed: ${err instanceof Error ? err.message : String(err)}. ` +\n      `Returning {} for this branch. Likely cause: circular reference, Symbol, or other non-serializable value in pipeline context.`,\n    );\n    return {};\n  }\n}\n\n/** LRU-bounded cache for compiled VM scripts — prevents unbounded memory growth. */\nconst MAX_VM_CACHE_SIZE = 256;\nconst _vmCache = new Map<string, vm.Script>();\n\nfunction getOrCompileScript(expr: string): vm.Script {\n  let script = _vmCache.get(expr);\n  if (script) return script;\n\n  // Evict oldest entry when cache is full\n  if (_vmCache.size >= MAX_VM_CACHE_SIZE) {\n    const firstKey = _vmCache.keys().next().value;","sourceCodeStart":204,"sourceCodeEnd":240,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/pipeline/template.ts#L204-L240","documentation":"sanitizeContext serializes the pipeline context to plain JSON (handling BigInt) with a cache, so it can be safely passed into sandboxed template/JS evaluation. If JSON.stringify/parse fails — typically circular references, Symbols, functions, or other non-serializable values — the function warns and returns {} for this branch instead of throwing, so evaluation proceeds with an empty context.","triggerScenarios":"JSON.stringify throws or JSON.parse produces invalid state because obj contains a circular reference, a Symbol key/value, a function that toJSON turns into invalid JSON, or a getter that throws during serialization.","commonSituations":"Storing DOM nodes, page handles, or plugin objects in pipeline context; a BigInt left in a nested field (top-level is handled, nested replacer handles it too, but toJSON throwing is not); recursive data built by a previous pipeline step; adding class instances with circular back-references.","solutions":["Find the circular/non-serializable value in the pipeline context and remove it or convert it to plain data (id strings instead of object references).","Store only JSON-safe values in context: strings, numbers, booleans, arrays, plain objects.","Wrap risky values with a toJSON() method returning a serializable representation.","Note the branch will now receive {} — check whether downstream templates expected those fields and provide defaults."],"exampleFix":"// before\ncontext.parent = context; // circular\n// after\ncontext.parentId = 'root'; // store a reference key, not the object itself","handlingStrategy":"validation","validationCode":"function isJsonSafe(value: unknown, seen = new Set()): boolean {\n  if (value === null || typeof value !== 'object') return typeof value !== 'function' && typeof value !== 'symbol';\n  if (seen.has(value)) return false;\n  seen.add(value);\n  return Object.values(value).every((v) => isJsonSafe(v, seen));\n}\n// before eval: if (!isJsonSafe(ctx)) console.warn('context has circular/non-serializable values');","typeGuard":"function isPlainObject(v: unknown): v is Record<string, unknown> {\n  if (typeof v !== 'object' || v === null || Array.isArray(v)) return false;\n  return Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null;\n}","tryCatchPattern":"let safeCtx: Record<string, unknown> = {};\ntry {\n  safeCtx = sanitizeContext(ctx);\n} catch {\n  safeCtx = {}; // branch will evaluate with empty context; log which keys were expected\n}","preventionTips":["Store only JSON-safe primitives/plain objects in pipeline context.","Replace object back-references with id strings.","Give non-serializable values a toJSON() method.","Don't leak DOM nodes/page handles into context from earlier steps."],"tags":["serialization","json","template"],"backgroundTag":"circular-json-reference","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}