LayerZeroFault
ai agents-api

Fix: ElizaOS JS Runtime Bridge False '[cycle]' Serialization Bug

VV

Written by

Fact-Checked on September 11, 2026

Verified Expert

Fix: ElizaOS JS Runtime Bridge False “[cycle]” Serialization Bug

In autonomous multi-agent frameworks built on ElizaOS (formerly ai16z), the JS Runtime Bridge (packages/agent/src/services/js-runtime-bridge.ts) acts as the high-speed translation layer between the host environment and embedded sandboxed actions. It marshals complex runtime objects, plugin state, and on-chain responses between TypeScript services and agent execution contexts.

However, developers building Web3 arbitrage bots, liquidity rebalancing agents, or complex DeFi plugins have uncovered a destructive serialization defect: valid, non-cyclic data structures are corrupted into the string "[cycle]".

[ElizaOS:RuntimeBridge] Marshaling action parameters for EXECUTE_SWAP...
[ElizaOS:ActionHandler] Fatal RPC Exception:
  Error: invalid address (argument="address", value="[cycle]", code=INVALID_ARGUMENT)
  at Contract.balanceOf (ethers.js:284)
  at executeSwapAction (plugin-defi/swap.ts:114)

When this bug triggers, the agent fails to execute transactions, hallucinates corrupted contract addresses, or corrupts persistent memory stored in Postgres/pgvector.

Placeholder: ElizaOS JS Runtime Bridge Data Corruption Diagram


1. Code-Level Root Cause Analysis

The flaw exists within toJsValue in packages/agent/src/services/js-runtime-bridge.ts. The implementation attempts to protect against infinite recursion caused by circular JavaScript objects:

// VULNERABLE CODE in packages/agent/src/services/js-runtime-bridge.ts
function toJsValue(value: unknown, seen: Set<unknown> = new Set()): unknown {
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // BUG: Checks if the object was visited ANYWHERE earlier in the entire tree!
  if (seen.has(value)) {
    return '[cycle]';
  }

  seen.add(value); // Object added to global set and NEVER removed!

  if (Array.isArray(value)) {
    return value.map((item) => toJsValue(item, seen));
  }

  const result: Record<string, unknown> = {};
  for (const [key, val] of Object.entries(value)) {
    result[key] = toJsValue(val, seen);
  }

  return result;
}

The Flaw: DAG vs. Circular Graph Confusion

In computer science, a Directed Acyclic Graph (DAG) allows multiple distinct paths to reach the same node without containing any cycles:

       Payload
       /     \
    LegA     LegB
       \     /
     Token (USDC)  <-- Shared reference, NOT a cycle!

In the vulnerable implementation:

  1. toJsValue visits LegA.Token (USDC object) and adds its reference to seen.
  2. The traversal backtracks to Payload and enters LegB.
  3. When reaching LegB.Token, seen.has(value) evaluates to true.
  4. Instead of serializing the valid USDC token metadata, the function outputs "[cycle]".

Because the set is shared across all sibling nodes and never pruned upon backtracking, any object referenced more than once in the data payload is systematically mutilated.

(For other settings and serialization anomalies in ElizaOS, see our guide on ElizaOS AgentSkillsService SKILLS_AUTO_LOAD Boolean Setting Bug).

Placeholder: DAG vs Circular Reference Traversal Flowchart


2. Impact on Web3 Autonomous Operations

  • Cross-DEX Arbitrage Quotes: Quotes containing duplicate intermediary token objects (e.g., WETH -> USDC -> DAI -> WETH) have subsequent hops replaced by "[cycle]".
  • Multi-Signature Proposals: Batched Safe transactions sharing the same recipient address fail schema validation.
  • Agent Vector Memory Retain: When the agent stores conversation state containing referenced user profiles, the state database persists [cycle] instead of user IDs, breaking retrieval-augmented generation (RAG) lookups.

3. Production Remediation Strategies

Strategy 1: Branch-Scoped Ancestor Tracking (Upstream Patch)

To correctly identify circular references without penalizing DAGs, track only ancestors along the active call stack.

Replace the naive Set.add() with a scoped Set or backtrack cleanup:

// HARDENED IMPLEMENTATION in packages/agent/src/services/js-runtime-bridge.ts
export function toJsValue(value: unknown, ancestors: Set<unknown> = new Set()): unknown {
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // True cycle check: only triggers if the object is an ancestor of itself
  if (ancestors.has(value)) {
    return '[cycle]';
  }

  // Add to active branch stack
  ancestors.add(value);

  try {
    if (Array.isArray(value)) {
      return value.map((item) => toJsValue(item, ancestors));
    }

    const result: Record<string, unknown> = {};
    for (const [key, val] of Object.entries(value)) {
      result[key] = toJsValue(val, ancestors);
    }

    return result;
  } finally {
    // CRITICAL: Unwind active ancestor stack upon returning up the tree!
    ancestors.delete(value);
  }
}

Strategy 2: Defensive Cloning in Custom Action Plugins

If you are developing plugins for ElizaOS and cannot wait for an upstream package release, deep-clone your parameters using structuredClone() to break shared object references into independent memory instances before passing them to the bridge:

import { Action, IAgentRuntime, Memory } from '@elizaos/core';

export const safeSwapAction: Action = {
  name: 'SAFE_EXECUTE_SWAP',
  description: 'Executes token swap with sanitized non-DAG payload',
  handler: async (runtime: IAgentRuntime, message: Memory, state: any) => {
    // structuredClone produces independent object graphs, bypassing false cycle detection
    const decoupledPayload = structuredClone({
      tokenIn: state.tokenIn,
      tokenOut: state.tokenOut,
      route: state.optimalRoute,
    });

    return await runtime.executeAction('EXECUTE_SWAP', decoupledPayload);
  },
};

4. Verification Test Case

Verify your fix by running a Node.js or Vitest unit test asserting that shared DAG references serialize intact:

import { describe, it, expect } from 'vitest';
import { toJsValue } from './js-runtime-bridge';

describe('toJsValue DAG serialization', () => {
  it('preserves shared references without generating [cycle]', () => {
    const token = { symbol: 'USDC', decimals: 6 };
    const payload = {
      legA: { token },
      legB: { token },
    };

    const result = toJsValue(payload) as any;

    // Both legs must retain true object properties
    expect(result.legA.token.symbol).toBe('USDC');
    expect(result.legB.token.symbol).toBe('USDC');
    expect(result.legB.token).not.toBe('[cycle]');
  });

  it('correctly catches actual circular references', () => {
    const circularObj: any = { name: 'InfiniteLoop' };
    circularObj.self = circularObj;

    const result = toJsValue(circularObj) as any;
    expect(result.self).toBe('[cycle]');
  });
});

Placeholder: Terminal Screenshot of Passing Vitest Suite for toJsValue


5. Summary Checklist for ElizaOS Developers

  1. Audit Custom Bridges: Inspect any custom marshaling wrappers in plugin-* packages for monotonic Set lookups.
  2. Always Implement finally { ancestors.delete(obj); }: Ensure stack unwinding occurs even if an exception is thrown during property enumeration.
  3. Log Sanitized JSON: If an agent suddenly outputs "[cycle]" in production logs, inspect whether shared memory state was passed through toJsValue.
Partner Spotlight: Gate.io

Trade Securely on Gate.io

Don't risk your assets on centralized silos or unverified endpoints. Trade securely on Gate.io with deep liquidity and institutional-grade security protocols.

Claim $100 Sign-up Bonus

Official Partner Referral Link

Related Inquiries

Why does ElizaOS replace valid object parameters with the string '[cycle]'?

In packages/agent/src/services/js-runtime-bridge.ts, the toJsValue() function uses a single persistent Set<unknown> to track visited objects. Because the set is never cleared as the recursion backtracks up the call stack, any directed acyclic graph (DAG)—such as an object referenced twice in an array or shared across action arguments—is mistakenly flagged as a cyclic circular reference and replaced with the literal string '[cycle]'.

How does this bug impact Web3 autonomous trading agents?

When an agent processes multi-token swaps, routing quotes, or portfolio state snapshots, token objects (such as USDC metadata) are shared across input and output legs. When the bridge marshals this payload, the second token reference becomes '[cycle]', resulting in fatal RPC reverts like 'Invalid address [cycle]' or invalid BigInt parse errors.

What is the proper fix for circular reference detection in tree serialization?

Rather than sharing a monotonic global Set across the entire traversal, cycle detection must track only ancestors along the current traversal branch. As the recursion unwinds, visited objects must be removed from the active stack set (ancestors.delete(obj)), or the function must use an ancestor chain array.