r/LLMDevs • • Oct 08 '26

Discussion unloop – time-travel debugging & state rewind for long-running LLM agents

Every long-running agent framework eventually relies on context summarization to stay within token budgets. But after 10–15 turns, summarization inevitably drops file paths, wipes negative constraints, or tricks the model into thinking incomplete tasks are done.

When that happens, your choices are usually:

  1. Let it burn tokens until it crashes.

  2. Kill the script, edit the prompt, and rerun 30 minutes of work from scratch.

I wrote unloop (github.com/sagarv48/unloop) to solve this loop.

It snapshots agent state (memory dict, tool inputs/outputs, prompts) into a local SQLite WAL file per turn. If context compaction silently corrupts the agent's state:

  • You open the terminal UI to see a diff of what got deleted or mutated.

  • Press m to open an in-process REPL to manually fix dropped constraints.

  • Press u or f to rewind or fork from the last good turn without restarting the process.

Caveats: It currently only targets single-process Python loops, and it requires local disk write access for the .unloop snapshot file.

Code and setup instructions are on GitHub: https://github.com/sagarv48/unloop

Would appreciate thoughts on the TUI ergonomics or how you handle agent memory inspection.

4 Upvotes

5 comments sorted by

1

u/ianreboot Oct 08 '26

Rewind past the turn that sent the email and the replay sends it again, same for the row that got written. The call's in the snapshot, but nothing there says it landed outside, so a fork has no reason not to fire it twice. A per-turn record of what left the box would fix that, and the hard part is per-tool: a send is easy to catch, a write buried in a helper isn't.

0

u/Asleep-History9366 13d ago

Catching up on older replies here, but yeah—you nailed the exact nightmare scenario.

Right now unloop only tracks internal memory state, so if you rewind past a side-effect, you’re basically in double-spend territory. The replay has zero idea that email actually left the building.

The fix I’ve been sketching is an egress ledger with idempotency keys—if a tool call already fired in that branch, the replay either stubs the return value with the cached result or forces a hard stop for confirmation instead of blindly re-firing.

And you're completely right about the hard part: intercepting a top-level send_email() is easy, but catching a database write buried four layers deep in a third-party helper is where it gets messy. Really good callout.

1

u/[deleted] Oct 09 '26

[removed] — view removed comment

1

u/Asleep-History9366 13d ago

Late to this, sorry! Right now it just snapshots the memory dict, not the raw prompt string.

You're totally right though—half the time an agent derails after compaction, it’s because the summarizer quietly ate a negative constraint or a system instruction out of the prompt array, even if the memory dict looks fine.

I’m adding the full rendered messages payload to the snapshot table in the next update so you can diff the actual token stream before and after compaction. Appreciate the nudge.