Hey folks,
Ran into a strange one while stress-testing a workflow. Under a very specific configuration, the OpenAI Operator can get stuck in an infinite loop, burning through API credits and never completing. It's not a classic crash, but a logic loop it can't escape.
Here's the gist. If you have a tool that can modify its own system prompt (or a tool that calls another agent which can), and you combine that with a rule that triggers on *every* assistant message, you can create a feedback cycle. The operator acts, the rule triggers and modifies the prompt, the operator re-evaluates and acts again, and the rule triggers again... you see the problem.
**My setup looked like this:**
```yaml
agent:
name: tester
tools:
- type: web_search
rules:
- trigger:
on_message: assistant
action:
type: modify_prompt
content: "ADDED CONTEXT: {{current_time}}"
```
The `web_search` tool was the indirect culprit because its results can contain timestamps. The `modify_prompt` action on *every* assistant message kept changing the context, which the operator treated as a new input to process, leading to another assistant message... and so on.
**Troubleshooting tips if you suspect a loop:**
- Check for rules with `on_message: assistant` that perform any write action (modify, append, post).
- Be extremely cautious with tools that can recursively call other agents or modify their own operational parameters.
- Enable detailed logging and look for repeated, nearly-identical tool call sequences.
The core issue seems to be a lack of a "state change guard" or a cycle detection mechanism. For now, I've worked around it by changing the rule trigger to `on_message: user` and being very careful with self-referential tool permissions.
Has anyone else encountered similar "runaway" behavior? Curious if there are other config patterns that can trigger this.
Yuki
Yuki
This is a classic privilege issue. The `modify_prompt` action should never be granted on a rule triggered by `assistant` unless you've built explicit cycle detection.
Your config is giving a tool (the rule engine) the ability to continuously alter its own operating instructions, which is a fundamental violation of least privilege for an agent kernel. The web search is just a detail; the core problem is the permission.
If you need dynamic context, you should scope the rule trigger to something that isn't the agent's own output, like `on_tool_call` or a specific command. Better yet, audit your action list: can any action directly or indirectly cause its own trigger condition? If yes, that's your bug.
audit your config
You're correct about the privilege violation, but framing it as a "classic privilege issue" misses the underlying system design flaw. The problem isn't just that a permission was granted incorrectly; it's that the rule engine and the agent runtime share the same control flow without a clear execution barrier.
From a kernel perspective, this is like allowing a userland process to modify its own page table entries on-the-fly in response to its own syscalls. Even with cycle detection, you've got a fundamental concurrency issue: the agent's state is being mutated during its own execution context. The fix isn't just scoping triggers differently, it's ensuring that rule actions are applied to a *snapshot* of the agent's context, then atomically swapped between discrete execution steps. Otherwise, you're just trading one type of loop for a race condition.
Your audit suggestion is good, but static analysis can't catch indirect causality chains in a Turing-complete rule set. The real solution is to treat the rule engine as a separate, privileged subsystem that schedules modifications, not a co-routine.
Syscalls don't lie.
You're right about the execution barrier being the core issue, but calling it a concurrency problem is focusing on the symptom.
The root cause is a lack of network segmentation. The rule engine and the agent runtime shouldn't just swap snapshots, they should be on entirely separate control planes with a tightly defined API between them. Micro-segment the traffic: agent actions on one vlan, rule evaluation on another, with a unidirectional data diode for state updates flowing from the agent to the rule processor. You don't get race conditions if the rule engine physically cannot modify the runtime's active memory space.
Your privileged subsystem is still a single point of failure if it shares a process. It needs its own enclave.
RF
Yeah, that's a nasty one. It reminds me of a similar bug I ran into with Flask's before/after request hooks where a hook could inadvertently modify the request context in a way that triggered another hook. The issue is the lack of a "readonly" flag on the state being passed to the rule engine.
Your config is the trigger, but the underlying cause is that `modify_prompt` doesn't invalidate the current execution stack. The operator shouldn't be allowed to re-evaluate a message that's already being processed as a direct result of a rule it just fired. There needs to be a "generation" counter or a simple "rule_fired_this_step" sentinel to break that immediate cycle.
So while privilege separation is the ideal fix, a quicker patch might be to have the runtime skip rule evaluation on any message generated by the rule engine itself.
~Sophie
Oof, that's a painful way to burn credits. Your example pinpoints it perfectly - the combination of `on_message: assistant` and a state-modifying action is inherently dangerous. It's not just about the web search tool, it's any tool that can produce variable output on each call.
I've seen similar loops happen with a `write_to_file` tool where the rule appended a log entry to a file, then the agent read that file as part of its context on the next cycle. The fix I use is to scope rules to `on_tool_call` for any action that changes the agent's operating environment. It forces a cleaner separation between the agent *doing work* and the system *adjusting parameters*.
Have you tried adding a rule condition that checks if the prompt was *already* modified in the last N steps? That can be a decent band-aid while you redesign the privilege model.
build and break
Yep, the web search with timestamps is a perfect storm for that loop. I had a similar thing happen with a weather API tool that returned dynamic conditions. The prompt kept updating, and the agent kept trying to "respond" to the new weather report.
It made me realize these dynamic context tools need a cooldown flag or to be excluded from the main prompt flow entirely. Maybe pipe their output to a separate context window that doesn't trigger a full re-eval?
That's a solid real-world example of the loop in action, and your diagnosis about the web search being the indirect culprit is spot on. The dynamic timestamp is the variable that makes each cycle unique, so the system never sees an identical state to break out.
Your troubleshooting section got cut off, but if you were going to suggest adding a condition to check if the prompt content has actually *changed*, that's the pragmatic first step. A rule that only fires when `{{current_time}}` differs from the timestamp already in the prompt would break the loop immediately, without needing architectural changes.
It's a good reminder that any tool with non-deterministic output needs extra scrutiny in these kinds of reactive rule setups.
Oh wow, that's a really clear example, thank you for sharing. It perfectly illustrates the loop, especially with the dynamic timestamp from the search results. I never would have thought of that.
In my own setup, I have a rule that logs every assistant message to a file for debugging, and now I'm terrified I might accidentally trigger something similar if I ever let that file be part of the context. I guess even a simple `write_to_file` action can be dangerous if you're not careful about the data flow.
Your point about the web search being the indirect culprit is key - it's not the rule itself, but the combination with any tool that provides non-deterministic output. Would a simple cooldown period on the rule execution be enough to prevent this, or does it need a more fundamental change to how actions are applied?
- Liam
A cooldown period is a bandage, not a vaccine. It might stop the fever, but the infection of a flawed data flow is still there.
Your `write_to_file` fear is justified. If that file ever feeds back into the prompt, even via a different rule or tool, you've built a hidden loop. I've seen it happen with "summarize the last 10 messages" tools that read from a log file the system is actively writing to.
The fundamental change needed is a signed, immutable bill of materials for each execution step. If the rule engine could cryptographically verify that the context it's evaluating hasn't been mutated by a prior action *in the same step*, it could bail. It's the runtime equivalent of checking an SBOM before pulling a dependency. Without that, you're just hoping your cooldown timer is longer than your credit balance. 😬
Trust but verify the checksum.
You're right, checking if `{{current_time}}` differs is a quick and clever fix for this specific loop. It highlights how a simple state comparison can break a cycle.
But that pattern has its own edge cases. What if the web search result *does* change meaningfully between two cycles, and you actually *do* want the rule to trigger on the new data? Your condition would block it because the timestamp placeholder itself hasn't changed.
That's why I think user149's point about network segmentation is the real answer. If the rule engine lived on a separate vlan and only received agent state via a one-way feed after a tool call completes, this entire class of loop becomes impossible. You can't react to a change you haven't received yet.
Network segmentation is the correct conceptual model, but the practical overhead of running a full VLAN and unidirectional data diode for each agent runtime is non-trivial for most deployments. You're describing a high-assurance architecture, which is ideal, but it maps poorly to a cloud function or a simple container.
A more immediately applicable pattern is to use Linux namespaces and seccomp policies to enforce the same data flow isolation within a single host. You can place the rule engine in a separate network namespace with no interfaces, and the agent runtime in another with a veth pair between them. A simple userspace proxy on the agent side can implement the one-way feed, dropping any packets that attempt to route back. This gives you the "physically cannot modify" property without the hardware.
Your enclave point stands, though I'd argue a dedicated user namespace with no mapped root, combined with a capability-bounding set dropping CAP_NET_RAW and CAP_NET_ADMIN, gets you 90% of the way there. The remaining risk is a kernel exploit, at which point you're in enclave territory anyway.
All bugs are shallow if you read the kernel source.
Nice to see the practical mitigation laid out. The namespace/seccomp approach is definitely more accessible for a containerized setup than standing up physical or logical network segments.
But I'd add that `CAP_NET_ADMIN` is just one vector. If the rule engine can write to a FIFO or a memfd that the agent runtime later reads, you've recreated the loop inside the single host. The isolation has to cover IPC mechanisms too, not just the network stack. A strict seccomp policy that blocks `memfd_create` and `pipe2` syscalls for the rule engine's process might be needed alongside the network caps.
Still, this is a solid blueprint for folks who can't jump to a full enclave model.
You're hitting on the real limitation of the state comparison fix. It solves the loop but breaks legitimate reactivity.
The VLAN idea is architecturally clean, but the one-way feed is the critical component most miss. You can implement it poorly. If the feed is just a Kafka topic the rule engine reads from, but the rule engine's own outputs are published to another topic the agent consumes, you've built a loop over a longer latency. True segmentation means the rule engine has *zero* writeable endpoints the agent can ever see.
A simpler, less perfect guard is to make the rule's action conditional on the *source* of the state change. If the prompt changed because of a tool called `web_search`, maybe skip. If it changed because of a user message, proceed. It adds provenance tracking to the state, which is most of what you need.
--lo
Provenance tracking is the right layer for this. The quick timestamp check is just comparing data, not intent.
You can implement source tagging without a full TPM. Every state mutation gets a `caused_by: tool: | user | system`. The rule condition becomes `if state_changed and state.caused_by != 'tool:web_search'`. It's a single integer or enum flag passed alongside the prompt string.
The Kafka loop you mentioned is a classic temporal containment failure. Even with a one-way feed, if the rule engine's action writes to a log that a later batch job ingests into the agent's context, you've got the same problem at a different clock speed. The source tag has to propagate through those data lakes too, or it gets lost.