A resumed conversation looked brand-new — history was orphaned, not lost
A conversation created under agent-server 1.29.3 (a flat event log) was resumed under 1.33.0 (a parent-linked event tree). All 5,814 events were intact on disk, but the agent's LLM context was rebuilt from only the last ~165 — because the first event written after resume was stamped as a hard tree root, cutting the walk off from the 5,566 older events.
1One-sentence answer
The agent rebuilds its context by walking a linked list of events from the newest back to a root, following each
event's parent_id. On resume across the 1.29.3→1.33.0 boundary, the first new event got
parent_id="__root__" (an explicit "stop here"), so the walk halted there and never reached — or fell
back to — the older flat-log events. The fix is a one-field re-link; no data was lost.
parent_id). Anyone upgrading across that boundary and resuming can hit it.2The symptom
After resume, asked what it remembered, the agent reported only the two most recent user messages — as if the conversation had just begun. Meanwhile the browser UI still showed the full history. That split is the tell: the UI reads the flat directory of event files; the agent reads the tree. They disagreed.
| Reader | Source of truth | Saw |
|---|---|---|
| Browser UI | flat list of events/event-*.json | all 5,814 events ✔ |
| Agent (LLM context) | path_to_root(leaf) — tree walk | ~165 events, 2 user messages ✗ |
3Two storage models: linear → tree
1.29.3 stored events as a plain sequence: file N follows file N-1, implicitly. 1.33.0
kept the same files but added an explicit link: every event now carries a parent_id, so history is a
tree (which enables branching / forking / navigation). Old files simply have no parent_id field.
parent_id. Sequence = order on disk.Crucially, 1.33.0 is designed to read old flat logs. When an event has no parent_id, the
tree walk falls back to "the previous file" — so legacy conversations should load unbranched, with no rewrite:
# openhands-sdk/openhands/sdk/conversation/event_store.py:91
def _effective_parent_id(self, idx, event):
if event.parent_id == ROOT_PARENT_ID: # "__root__" → hard stop
return None
if event.parent_id is not None: # explicit link (new events)
return event.parent_id
if idx == 0:
return None # genuine first event
return self.get_id(idx - 1) # LEGACY fallback: chain to previous file
4How the agent's context is rebuilt
The agent's working view is path_to_root(leaf): start at the current leaf and follow
_effective_parent_id until it returns None. Whatever the walk collects is the
conversation the LLM sees.
# event_store.py:106 — walk leaf → … → root, then reverse to root-first
def path_to_root(self, leaf_id, limit=None):
chain, cur = [], leaf_id
while cur is not None:
idx = self.get_index(cur)
chain.append(self[idx])
cur = self._effective_parent_id(idx, self[idx]) # ← stops on None
return chain[::-1]
event_store.py · path_to_root · state.py · view / active_branch
None too early, everything below it is invisible — even though it is right there on disk.5Where it broke: the resume stamped a false root
When 1.33.0 resumes, it must decide the parent for the first new event. Two functions cooperate:
# state.py:254
def _resolve_active_leaf(self):
if self.leaf_event_id is not None: return self.leaf_event_id
if self.head_is_empty: return None
if (n := len(self._events)) == 0: return None
if self._events[n - 1].parent_id is not None: # ← the trap
return None
return self._events.get_id(n - 1) # legacy tail → chain onto it
# state.py:281
def _stamp_parent_id(self, event):
if event.parent_id is not None: return event
parent = self._resolve_active_leaf()
if parent is None and len(self._events) > 0:
parent = ROOT_PARENT_ID # ← "deliberate new root" — but here it's wrong
return event.model_copy(update={"parent_id": parent})
state.py · _resolve_active_leaf / _stamp_parent_id
On this resume, leaf_event_id was None (not stored by 1.29.3). The last file on
disk was not a bare legacy event — it already carried a parent_id (a state-update/error event
written during the failed 1.29.3 startup). So the guard self._events[n-1].parent_id is not None was
true, _resolve_active_leaf returned None, and _stamp_parent_id concluded
"empty head over a non-empty log ⇒ start a new root" and stamped __root__. That is the bug: it should
have chained onto the legacy tail, not declared a new root.
6Worked trace (the real conversation)
5,814 events on disk. Indices 0–5565 are legacy (no parent_id); the resume began at 5567.
| Event | parent_id | What the walk does |
|---|---|---|
05811 leaf (agent reply) | 06d0358e (5808) | follow ↑ |
05808 user msg | fa1f595d (5805) | follow ↑ |
| … 5805 → 5567 … | explicit links | follow ↑ (165 events) |
05567 first post-resume msg | __root__ | STOP → returns None 🛑 |
05566 … 00000 (5,566 events) | none / legacy | never reached — orphaned |
7Recovery
The data is intact; this is a one-field pointer repair. In the offending event file, change the parent from the false root to the last pre-resume event:
# events/event-05567-ae760ea7-….json
- "parent_id": "__root__"
+ "parent_id": "eabf9384-55d0-473a-a314-514372203b5d" # id of event 05565
ConversationStateUpdateEvents
and abandoned nodes aren't on the linear path, which is expected.)parent_id across all legacy events, but the
single re-link is enough because _effective_parent_id already chains legacy events by position.8What did not happen
- No data loss. All 5,814 event files are present and unmodified; the browser still renders them.
- Not an encryption/key problem. Settings decrypt fine; same secret key. This is purely the event-tree walk.
- Not a condenser/summarization drop. The events were never in the context to be condensed — the walk stopped before reaching them.
- Not specific to one conversation. Any pre-1.33.0 conversation resumed with a non-bare tail can be
stamped with a false
__root__and present as brand-new.
leaf_event_id is unset, resolve the active
leaf to the last real event (skipping ConversationStateUpdateEvent/error artifacts) rather
than treating "last event already has a parent_id" as grounds for a brand-new root. That keeps legacy tails linked.