Skip to content

fix(teams): remove chatroom fan-out to prevent agent feedback loops#220

Open
jcenters wants to merge 2 commits intoTinyAGI:mainfrom
jcenters:fix/chatroom-fanout-loop-upstream
Open

fix(teams): remove chatroom fan-out to prevent agent feedback loops#220
jcenters wants to merge 2 commits intoTinyAGI:mainfrom
jcenters:fix/chatroom-fanout-loop-upstream

Conversation

@jcenters
Copy link

Problem

Every [#team: ...] post was fanned out to all teammates as a full queue message, triggering a new Claude invocation per agent. When agents responded with their own [#team: ...] posts, this created an exponential feedback loop.

Real-world impact: In a 4-agent crew, a single chatroom post triggered 3 invocations. Each agent's response triggered 2 more. Within an hour of the crew team going live, this loop exhausted a 5-hour Anthropic API token limit entirely.

Root cause

postToChatRoom() in packages/teams/src/conversation.ts enqueued the chat message for every teammate via enqueueMessage(). Agents processed these as regular tasks and often responded with another [#crew: ...] post, which triggered the next round.

Fix

Chat room messages are now stored as history onlyinsertChatMessage() still runs (history is preserved), but enqueueMessage() fan-out is removed entirely.

Agents already receive recent chat room history as passive context when invoked for real tasks. This preserves team coordination without the runaway invocation cost.

Trade-off

Agents no longer get a push notification for every chatroom post. They see chat history on their next invocation instead. For the intended use case (status broadcasts, findings, flags) this is the correct behavior — chatroom posts are not meant to demand immediate responses.

🤖 Generated with Claude Code

Every [#team: ...] post previously triggered a full queue message for
each teammate, causing a new Claude invocation per agent. When agents
responded with their own [#team: ...] posts, this created an exponential
feedback loop that exhausted API token budgets in minutes.

Chat room messages are now stored as history only. Agents read recent
chat room messages as passive context when next invoked for a real task,
preserving coordination without the runaway cost of active fan-out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR fixes a critical exponential feedback loop in the team chatroom system by removing the per-agent enqueueMessage fan-out from postToChatRoom(), so chatroom messages are now stored as history only. The fix is correct and well-motivated — agents already receive recent chatroom history as passive context on their next invocation, so coordination is preserved without the runaway API cost.

Key changes:

  • postToChatRoom() in packages/teams/src/conversation.ts now only calls insertChatMessage() (history write); the loop over teamAgents that enqueued a full queue job per teammate is removed.
  • Two parameters (teamAgents: string[] and originalData) remain in the function signature but are now entirely unused — they should be removed along with the corresponding arguments at both call sites (conversation.ts and packages/server/src/routes/chatroom.ts).
  • The behavioral change also affects the human-facing REST endpoint POST /api/chatroom/:teamId: a user posting directly to the chatroom will no longer trigger an immediate agent invocation. This is likely intentional but undocumented from the API consumer's perspective.

Confidence Score: 4/5

  • Safe to merge — the loop-prevention fix is correct and isolated; only minor dead-code cleanup remains
  • The core change is a simple, targeted removal of a proven-harmful code path. No new logic is introduced. The only concerns are two now-unused parameters in the public function signature and the undocumented behavior change for the human-facing chatroom REST API, neither of which affects runtime correctness.
  • packages/teams/src/conversation.ts — dead teamAgents and originalData parameters should be removed; packages/server/src/routes/chatroom.ts (unchanged) is implicitly affected by the behavioral change

Important Files Changed

Filename Overview
packages/teams/src/conversation.ts Fan-out loop removed correctly; two parameters (teamAgents, originalData) are now unused dead code in the public signature and all three call sites

Sequence Diagram

sequenceDiagram
    participant Agent as Agent (any)
    participant postToChatRoom
    participant insertChatMessage as insertChatMessage (DB)
    participant enqueueMessage as enqueueMessage (REMOVED)
    participant Teammate as Teammate Agent(s)

    Note over Agent,Teammate: BEFORE (feedback loop)
    Agent->>postToChatRoom: [#team: message]
    postToChatRoom->>insertChatMessage: store history
    postToChatRoom->>enqueueMessage: fan-out to each teammate
    enqueueMessage-->>Teammate: new Claude invocation
    Teammate->>postToChatRoom: [#team: response] (triggers loop)

    Note over Agent,Teammate: AFTER (this PR)
    Agent->>postToChatRoom: [#team: message]
    postToChatRoom->>insertChatMessage: store history only
    Note right of insertChatMessage: No fan-out — teammates\nread history as passive\ncontext on next invocation
Loading

Comments Outside Diff (2)

  1. packages/teams/src/conversation.ts, line 90-96 (link)

    Dead parameters after fan-out removal

    After removing the enqueueMessage fan-out, both teamAgents and originalData are now unused inside the function body. This leaves dangling parameters in the public signature and forces every call site to compute/pass values that are silently discarded (e.g. teams[crMsg.teamId].agents in handleTeamResponse and team.agents in packages/server/src/routes/chatroom.ts).

    Consider cleaning up the signature and all callers:

    Then update the two call sites accordingly:

    • conversation.ts:215 — drop the last two arguments
    • packages/server/src/routes/chatroom.ts:35 — drop the last two arguments
  2. packages/teams/src/conversation.ts, line 214-218 (link)

    User chatroom posts silently dropped without agent notification

    postToChatRoom is also called from packages/server/src/routes/chatroom.ts when a human user posts via POST /api/chatroom/:teamId. With fan-out removed, a user's direct chatroom message is now only visible to agents as passive history context on their next real-task invocation — it no longer triggers any immediate agent response.

    This is likely an intentional trade-off (described in the PR), but worth making explicit: users posting via the REST API should be aware that agents will not respond to chatroom messages proactively. If the intent is to allow human-initiated messages to still trigger an invocation, this call site would need special handling separate from the agent-to-agent path.

Last reviewed commit: a402e76

…removal

The teamAgents and originalData parameters became unused dead code when the
enqueueMessage fan-out loop was removed. Every call site was computing and
passing values that were silently discarded.

Cleaned up the function signature and both call sites:
- packages/teams/src/conversation.ts (agent response handler)
- packages/server/src/routes/chatroom.ts (human REST endpoint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jcenters added a commit to jcenters/tinyclaw-1 that referenced this pull request Mar 16, 2026
Agents in a team could trigger runaway feedback loops by sending each
other messages indefinitely. Two mechanisms failed to prevent this:

1. The chatroom fan-out (fixed separately in TinyAGI#220) escaped the
   conversation tracking system entirely, so totalMessages never
   incremented and the maxMessages guard never fired.
2. Agent-to-agent @mentions via sendInternalMessage had a maxMessages
   guard, but the default was 50 — enough for a 5-hour API limit burn
   before anything stopped.

This PR adds two independent, layered defenses:

**Rate limiter in enqueueMessage (queues.ts)**
Any message where fromAgent is set is agent-generated. Before inserting,
count how many agent-to-agent messages the target agent already has
queued in the last 60 seconds. If at or above the limit, drop the
message and log a [LoopGuard] warning instead of enqueuing.
Default: 10 messages/minute/agent. Configurable via settings.json:

  "protection": { "max_agent_messages_per_minute": 10 }

**Conversation chain depth cap (conversation.ts)**
Lower DEFAULT_MAX_CONVERSATION_MESSAGES from 50 to 10. Read the
effective value from settings.json at conversation creation time so
operators can tune it without a code change:

  "protection": { "max_chain_depth": 10 }

Both limits are independent — the rate limiter catches loops that
escape the conversation system (e.g. chatroom messages, new
conversations spawned by agents), while the chain depth cap limits
depth within a single tracked conversation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jlia0
Copy link
Collaborator

jlia0 commented Mar 18, 2026

Yes this is a real concern and has come up in my testing as well. Thanks for fixing it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants