{"record":{"id":"bfb01e7d78bf1dc2","repo":"mastra-ai/mastra","slug":"factory-rule-causal-depth-exceeded","errorCode":null,"errorMessage":"Factory rule causal depth exceeded.","messagePattern":"Factory rule causal depth exceeded\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":207,"sourceCode":"  for (const [event, leaf] of Object.entries(rules.linear)) {\n    enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');\n    if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);\n    assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);\n    if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n      throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);\n    }\n  }\n}\n\nfunction commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {\n  return {\n    idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),\n  };\n}\n\nexport function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {\n  if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n    throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');\n  }\n  if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');\n  const type = value.type;\n  if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');\n\n  switch (type) {\n    case 'reject': {\n      assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');\n      return {\n        type,\n        code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),\n        reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),\n      };\n    }\n    case 'transition': {\n      assertExactKeys(\n        value,\n        ['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L189-L225","documentation":"validateFactoryRuleDecision guards against unbounded recursive/nested causal decisions via a causalDepth counter (default 0). If a decision chain exceeds MAX_FACTORY_RULE_CAUSAL_DEPTH, this error is thrown to stop runaway recursion. Each nested decision validation increments the depth.","triggerScenarios":"A rule's onEvent handler returning a decision that itself references/produces nested decisions recursively, e.g. a decision whose payload contains another decision chain longer than the max depth; mutually recursive rules feeding each other's decisions.","commonSituations":"A transition decision's message or linked data embedding previously produced decisions in a loop; rules that re-trigger themselves (possibly combined with reenter: true) causing ever-deeper causal chains.","solutions":["Break the recursion: stop returning decisions that embed prior decision objects in causal chains","Flatten the decision payload — pass IDs/references instead of nested decision objects","Increase review of reenter flags that let a rule re-fire on its own transition","Inspect MAX_FACTORY_RULE_CAUSAL_DEPTH in validation.ts to know the limit and count your chain depth"],"exampleFix":"// before\nconst decision = { type: 'transition', ..., causal: parentDecision } // nests prior decision\n// after\nconst decision = { type: 'transition', ..., causalId: parentDecision.idempotencyKey }","handlingStrategy":"validation","validationCode":"function assertCausalChainDepth(decision, max = MAX_DEPTH) {\n  let d = decision, depth = 0;\n  while (d?.causal) { d = d.causal; if (++depth > max) throw new Error('causal chain too deep'); }\n}","typeGuard":"const isShallowDecision = (d, max = 3) => {\n  let cur = d, depth = 0;\n  while (cur?.causal) { cur = cur.causal; if (++depth > max) return false; }\n  return true;\n};","tryCatchPattern":"try {\n  return decision(input);\n} catch (e) {\n  if (e instanceof FactoryRuleValidationError && e.message === 'Factory rule causal depth exceeded.') {\n    logger.error('Runaway rule recursion; flattening decision chain');\n    return decision(flattenDecision(input));\n  }\n  throw e;\n}","preventionTips":["Reference prior decisions by idempotencyKey, not by embedding them","Audit rules combined with reenter: true for self-triggering loops","Add a depth counter in your own rule handlers","Keep decision payloads flat and id-based"],"tags":["validation","recursion","factory-rules"],"backgroundTag":"infinite-recursion","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}