I/D/E · flue-framework

Session Tree, Leaf, And Replay Safety

Summary

A source-level walkthrough of why Flue stores session history as a tree with an active path instead of a flat transcript.

The first runtime mistake to avoid is reading Flue’s session state as a chat transcript. A transcript is flat. Flue’s session state is a parent-linked tree with a current leaf and an active path.

That shape is not ornamental. It is what lets Flue rebuild model-visible context, insert compaction summaries, create child task sessions, aggregate usage over the exact work a call appended, and delete a task tree without pretending every branch is part of the current conversation.

The source pin for this chapter is withastro/flue@dbaa9effa305561c627c6836559f8a0cbce67875.

Active Path Lens

Stored history can branch, but replay safety comes from projecting the active path from a known leaf.

Domain Word

In Flue, session history is the persisted tree of session entries. Active path is the path from the root to the current leaf. The model does not see the whole tree; it sees the context built from the active path.

The invariant is: stored history may be richer than runnable context, but runnable context must be derivable from a known leaf.

The twelve-factor pressure is processes. A running handler cannot be the only memory of the work. A headless runtime needs enough durable state to reconstruct what matters after a turn, a retry, a task, or a restart.

The State Shape

packages/runtime/src/session.ts defines SessionHistory. It stores three things:

FieldMeaning
entriesAll session entries persisted for the session.
byIdIn-memory lookup map from entry ID to entry.
leafIdCurrent end of the active path.

Every entry has enough parent information for getActivePath() to walk backward from the leaf and reverse the result.

SESSION TREE
stored entries:

root
 A user prompt
    B assistant tool call
        C old branch
        D compaction summary
            E retry assistant answer   leaf

active path:
root -> A -> B -> D -> E

model-visible context:
messages produced from active path,
with the latest compaction summary replacing older detail

This is why “session memory” is too vague a phrase for Flue. Session history is not provider memory, not a run log, and not a raw transcript. It is a harness-owned data structure that can produce a transcript when the model needs one.

Entry Types

The pinned source uses three session entry shapes in types.ts and session.ts:

EntryPurpose
Message entryStores user, assistant, and tool-result messages produced by prompts, skills, shell calls, tasks, retries, or tools.
Compaction entryStores a context summary plus firstKeptEntryId, token count, file-operation details, and optional usage.
Branch summary entryStores a summary of a branch that should remain understandable without replaying the whole branch.

The important part is that compaction is not represented by deleting history and hoping the summary is enough. It is represented as its own entry on the active path. That gives the history a point where the model-visible context can change without erasing the fact that the session had earlier detail.

Active Path Is The Runnable Conversation

getActivePath() starts with leafId, follows each parentId, and returns the reversed path. buildContextEntries() then converts that active path into model-visible messages.

The compaction branch is the subtle part:

  1. Find the latest compaction entry on the active path.
  2. Convert that compaction summary into a user-context message.
  3. Find the first entry that should be kept after the summary.
  4. Add kept messages after the compaction.

That means old detail can remain in stored history while the model sees the summary plus recent tail. The two are intentionally different.

Appending Work

Most session work eventually appends messages:

MethodWhat it adds
appendMessage(...)One parent-linked message entry.
appendMessages(...)A sequence of message entries, each linked after the prior leaf.
appendCompaction(...)A compaction entry linked after the current leaf.
appendBranchSummary(...)A branch summary entry linked after the current leaf.

The shell path is a good example. session.shell(...) is programmatic, not model-decided, but Flue records it as an LLM-shaped bash tool exchange: user command, assistant tool call, and tool result. That keeps later context, compaction, and run inspection aligned with what the model would have seen if it had chosen the bash tool itself.

Task Sessions Are Child State, Not Inline Text

The task tool creates a detached child session with its own context. The parent records task session metadata in metadata.taskSessions, including the child session name, task ID, and storage key.

That design keeps delegation scoped:

Parent session ownsChild session owns
The fact that a task was started.The task conversation.
The final task result returned to parent context.Intermediate tool calls and retries.
Cleanup relationship through metadata.Its own session history and active path.

deleteSessionTree(...) proves the model. It loads a session, finds child task sessions in metadata, recursively deletes each child by storage key, and then deletes the parent. Deletion is tree-shaped because delegation is tree-shaped.

Usage Windows Depend On The Leaf

aggregateUsageSince(beforeLeafId) walks getActivePathSince(beforeLeafId). That method returns entries appended after a known leaf if the leaf is still on the current active path. If the old leaf is not on the active path, it returns [] instead of falling back to unrelated history.

That is a cautious answer. Silent overcounting would make compaction, retries, and task boundaries look cheaper or more expensive than they were. Returning zero when the window no longer matches the path is louder and safer.

The usage aggregator counts assistant message usage and compaction entry usage. That second case matters because compaction can make internal summarization calls, and Flue persists their cost on the compaction entry so the triggering public call can report the real cost.

Replay Safety In This Chapter’s Narrow Sense

This chapter is not claiming Flue has a general idempotency framework for every external side effect. The narrower claim is about reconstructing runnable context safely inside the session boundary.

Replay safety here means:

  • The model-visible conversation is derived from the active path, not from every stored entry.
  • Compaction can replace old active-path detail with a summary without losing all stored evidence.
  • Overflow recovery can remove a failed assistant leaf before retry.
  • Task sessions can be cleaned up with the parent tree.
  • Usage can be calculated over the exact entries appended after a sampled leaf.

Those are harness-level guarantees. They do not replace application-level idempotency keys for external APIs.

What Breaks If This Boundary Drifts

DriftFailure
Treating history as a flat transcriptCompaction becomes destructive summarization, not a reversible state transition.
Treating the whole tree as model contextBranches and child task detail leak into unrelated turns.
Forgetting the leafUsage windows and retry recovery lose their anchor.
Recording task work inline onlyParent sessions inherit noisy intermediate context and cannot clean child state coherently.
Treating provider retention as replay sourceSession recovery depends on external hosted state outside Flue’s store.

What To Copy

The useful pattern is not “use a tree because trees are elegant.” The useful pattern is: if a runtime needs compaction, delegation, deletion, and usage windows, give each transition a durable identity and derive runnable context from an explicit active path.

That pattern generalizes to other agent harnesses. A production harness should know the difference between stored evidence and the conversation slice handed to the model.

Verify In Source

  • SessionHistory.getActivePath() walks parent links from leafId.
  • SessionHistory.buildContextEntries() treats the latest compaction entry as a context summary boundary.
  • SessionHistory.removeLeafMessage(...) only removes the current leaf when it matches the failed message.
  • Session.recordTaskSession(...) stores child task session metadata.
  • deleteSessionTree(...) recursively deletes task sessions before deleting the parent storage key.
  • aggregateUsageSince(...) walks active-path entries after a sampled leaf.

References

Flue-framework Ch 2/8
  1. 1 Runtime Map 24m
  2. 2 Session Tree, Leaf, And Replay Safety 26m
  3. 3 The Pi-ai Seam 22m
  4. 4 Compaction As Failure Recovery 28m
  5. 5 Tool Contracts And Sandbox Reality 25m
  6. 6 Runs, Registries, Logs, And APIs 27m
  7. 7 Build Targets And Deployment Shape 26m
  8. 8 Extending Flue Safely 24m