{"record":{"id":"605b0a21ff21a20d","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-subagent-cycle","errorCode":"cave_sandbox_subagent_cycle","errorMessage":"cave_sandbox_subagent_cycle","messagePattern":"cave_sandbox_subagent_cycle","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/tool-worker.ts","lineNumber":121,"sourceCode":"  if (request.allowNetwork !== true) installNetworkDeny();\n  const imported = await import(request.entry) as { default?: AgentDefinition; agent?: AgentDefinition };\n  let definition = imported.default ?? imported.agent;\n  if (!definition || definition.kind !== \"agent\") throw new Error(\"cave_sandbox_agent_export_missing\");\n  validateAgentGraph(definition);\n  if (agentDefinitionSHA256(definition) !== request.rootDefinitionSha256) {\n    throw new Error(\"cave_sandbox_definition_mismatch\");\n  }\n  const visited = new Set<AgentDefinition>([definition]);\n  for (const name of request.agentPath) {\n    const delegated = definition.tools.filter((item) =>\n      item.name === name && item.runtime?.kind === \"subagent\"\n    );\n    if (delegated.length !== 1) throw new Error(\"cave_sandbox_unknown_subagent\");\n    const child = delegated[0]!.runtime!.definition as AgentDefinition;\n    if (!child || child.kind !== \"agent\") {\n      throw new Error(\"cave_sandbox_subagent_definition_invalid\");\n    }\n    if (visited.has(child)) throw new Error(\"cave_sandbox_subagent_cycle\");\n    visited.add(child);\n    definition = child;\n  }\n  const selectedTools = definition.tools.filter((item) => item.name === request.tool);\n  if (selectedTools.length !== 1 || selectedTools[0]!.runtime?.kind === \"subagent\") {\n    throw new Error(\"cave_sandbox_unknown_tool\");\n  }\n  const selected = selectedTools[0]!;\n  if (toolDefinitionSHA256(selected) !== request.toolDefinitionSha256) {\n    throw new Error(\"cave_sandbox_tool_definition_mismatch\");\n  }\n  if (selected.effect !== \"read\" && request.allowSideEffects !== true) {\n    throw new Error(\"cave_sandbox_side_effect_denied\");\n  }\n  const value = await selected.execute(request.params as never, AbortSignal.timeout(selected.timeoutMs));\n  writeResult({ ok: true, value });\n} catch (error) {\n  writeResult({ ok: false, code: failureCode(error) });","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/tool-worker.ts#L103-L139","documentation":"The worker keeps a `visited` Set of AgentDefinition objects (identity-based) as it descends agentPath. If the next child definition is reference-identical to one already on the current chain, the delegation graph contains a cycle and the worker aborts instead of recursing infinitely.","triggerScenarios":"Agent A delegates to agent B, and B (or a deeper descendant) delegates back to the same A definition object; a self-referential definition that includes itself as a subagent; mutually recursive builder functions that return the same object instance.","commonSituations":"Composing recursive agent graphs (reviewer delegates to worker, worker back to reviewer) without introducing distinct wrapper definitions; a builder memoizing a definition and reusing the instance in two places on one chain.","solutions":["Break the cycle: give each level a distinct definition object (wrap or clone) so the chain is a DAG, or terminate recursion with a leaf agent.","Audit the graph with validateAgentGraph in the parent — cycle detection there catches this before spawn.","If recursive behavior is intended, restructure so depth is bounded by distinct definitions per level rather than object reuse."],"exampleFix":"// before\nconst a = agent({ tools: [subagent(\"b\", b)] });\nconst b = agent({ tools: [subagent(\"a\", a)] }); // a -> b -> a cycle\n\n// after\nconst b = agent({ tools: [leafTool] }); // b terminates; recursion handled at orchestration layer","handlingStrategy":"validation","validationCode":"function pathIsAcyclic(root: AgentDefinition, path: string[]): boolean {\n  const visited = new Set<object>([root]);\n  let def = root;\n  for (const name of path) {\n    const child = def.tools.find((t) => t.name === name && t.runtime?.kind === \"subagent\")?.runtime?.definition;\n    if (!child || visited.has(child as object)) return false;\n    visited.add(child as object);\n    def = child as AgentDefinition;\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Model delegation graphs as DAGs; never reuse one definition instance twice on a chain.","Rely on validateAgentGraph's recursive checks before spawning."],"tags":["sandbox","cycle","subagent","graph","cave"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}