Sessions & persistence from the SDK
JsonlSessionStore and MemorySessionStore, resuming a branch, forking, and forcing compaction from code.
On this page
Sessions, branching & compaction covers the tree model conceptually.
This page is the code: constructing a store, resuming, branching, and driving
compaction directly from @arcturn/core.
The two stores
Both implement the same SessionStore interface from @arcturn/types
(create, open, append, entries, branch, list, setTitle):
import { JsonlSessionStore, MemorySessionStore } from "@arcturn/core";
const disk = new JsonlSessionStore({ dir: ".arcturn/sessions" }); // one .jsonl file per session
const memory = new MemorySessionStore(); // plain in-process maps — tests, ephemeral runscreateAgent({ ..., sessionDir }) builds a JsonlSessionStore for you and passes it as
sessionStore; passing sessionStore directly (to new Agent(...) or createAgent)
works with either implementation, or your own SessionStore.
JsonlSessionStore serializes writes per session through an internal queue, so
concurrent appends — background tasks and sub-agents can all be emitting events for the
same session at once — never interleave their bytes. create fails with
SessionStoreError (code: "exists") if the id is already taken, and SessionStoreError
(code: "invalidId") if the id contains anything outside [A-Za-z0-9._-]+.
Resuming a session
import { Agent } from "@arcturn/core";
const resumed = await Agent.resume({
...base,
sessionStore: store,
sessionId,
// leafId: olderEntryId, // omit to resume the newest branch tip
});
await resumed.prompt("Continue where we left off");Agent.resume reads every entry for sessionId, walks the path to leafId (or the
newest entry when omitted), and reconstructs conversation, todos, plan, and the model in
effect at that point via materializeBranch — then constructs a normal Agent seeded
with that state. The resumed agent's leafEntryId starts at the branch tip it resumed
from, so its very next appended entry becomes that node's child.
Branching
Passing an older leafId doesn't rewrite anything — it starts a new branch from
that point. Everything after it on the original path stays exactly where it was, still
reachable by resuming from its own leaf:
import { latestEntryId, pathToLeaf } from "@arcturn/core";
const entries = await store.entries(sessionId); // everything, append order
const tip = latestEntryId(entries); // the newest entry — default resume point
const rootFirst = pathToLeaf(entries, someOlderEntryId); // root-first path to any entrybuildTree(entries) turns the flat, append-order entry list into an explicit
parent/child SessionTree, and leafEntries(entries) returns every entry that has no
children — the set of branch tips a "pick a session to resume" UI would list.
Forcing compaction
Compaction runs automatically before a turn when the conversation crosses the
configured threshold (AgentOptions.compaction), and on demand:
const compacted = await agent.compact(); // true when history was folded into a summaryagent.compact() throws if a run is in flight — compact between prompts, not during
one. It returns false (with a notice event, not an error) when there's nothing old
enough to fold: the cut point always lands on a user-message boundary, so a
conversation with no completed turn yet has nothing safe to compact.
CompactionOptions field |
Default | Meaning |
|---|---|---|
enabled |
true |
Set false to disable automatic compaction; agent.compact() still works. |
reserveTokens |
16384 |
Headroom kept free in the context window before compaction triggers. |
keepRecentTokens |
20000 |
Recent conversation preserved verbatim, never folded. |
model |
agent's current model | Model used for the summarization call itself — can be a cheaper model than the one driving turns. |
maxOutputTokens |
4096 |
Output budget for the summary. |
buildPrompt |
built-in | Replace the summarization prompt entirely. |
shape |
per model | "simple" (one summary replaces everything — the default on Claude 4.6+ through anthropic, anthropic-compatible, bedrock, vertex), "keep-tail" (summary plus the recent turns — the default elsewhere; thinking is stripped from the tail on adaptive Claude) or "server" (the provider compacts; agent.compact() then reports nothing to summarize locally). |
resolveCompactionShape(options, model) returns the shape that applies, and
explainCompactionShape the shape with the reason arcturn doctor prints. findCompactionCut,
findSimpleCutPoint and hasPendingToolRound expose the cut logic; no shape folds an open tool
round. agent.compact() returns false with a notice, not an error, when one is waiting.
Why the default differs by model: Context management.
The summary is recorded as its own compaction session entry (summary, upToId,
tokensBefore, tokensAfter, and stripTailThinking when a kept tail was replayed without
its thinking), so a later resume reconstructs the exact folded state — compaction is a tree
node like any other, not a destructive rewrite. compact()'s result also reports shape,
stripTailThinking and keptIndices (each kept message's index in the input, since a
stripped one is a new object).
agent.on("compactionEnd", (event) => {
log(`Compacted ${event.tokensBefore} → ${event.tokensAfter} tokens`);
});Context-edit passes in the session
Local tool-result elision is recorded: each pass appends a state entry whose contextEdit
is a ContextEditRecord — { elided: string[]; withholdThinking?: "afterFirstElided" | "all" },
the tool-call ids now sent as stubs and, on adaptive Claude, how much earlier thinking stops
being replayed. materializeBranch returns the replayed ledger as
MaterializedBranch.contextEdit ({ elided, withheldThinking } — the stubbed tool-call ids
and the indices of assistant messages whose thinking is withheld), and Agent.resume applies it, so a resumed agent sends the
request the live one would have. If you write your own store or branch tooling, keep these
entries and their order: dropping one makes a resume send full results where the original sent
stubs, which misses the prompt cache and, on models that bind thinking to the prefix, the
thinking too. agent.contextEditPasses lists the passes made in this process (requestIndex, elided, charsSaved). See
Context management.
Estimating context usage
console.log(agent.estimatedTokens); // rough token count of the current conversationestimateTokens (also exported directly from @arcturn/core) is the same estimator
shouldCompact uses internally to decide whether a turn needs to trigger compaction
first — useful for a status bar that wants to show "approaching context limit" before
the agent itself acts on it.
What isn't SDK-exposed
Checkpoints & /rewind — the automatic pre-edit file snapshots and
/rewind — are implemented entirely in packages/cli/src, on top of the session tree
primitives documented here. There's no @arcturn/core API for them today: a host that
wants equivalent behavior would need to snapshot files itself (e.g. from a write/edit
tool's permission check or a beforeToolCall hook) and drive branching through
Agent.resume's leafId, the same mechanism /rewind uses.