OceanCode
Session ledger and export
A long session would grow to a huge, ever-slower context. oceancode avoids that with the
session ledger: the model sees a small, fixed window of recent turns, and everything older
is kept verbatim in Markdown files beside the session — never summarized away.
The full concept is in SESSION-LEDGER-CONCEPT.md (apps/ocean-coding-agent-rust/docs/SESSION-LEDGER-CONCEPT.md in the repository); this page is
the operator's view.
What the ledger is
- A window of the raw conversation. The model always sees the newest part of the real
conversation, up to a bounded size — by default 40% of the model's context (400k on a 1M
model, 80k on a 200k model). The window is measured in the provider's real tokens after every
step, and the oldest content leaves it one block at a time — inside a long turn
too (see The tideline below).
tokens: 0instead tracks the model's own ceiling — its context window minus the output allowance; 120k only when the route reports no limit. A turn is your message plus the model's full answer, tool outputs included. - Per-turn transcripts on disk. Every step is written verbatim to its turn's transcript file
as it ends, and the index files are updated when the turn completes. As the window grows the
oldest blocks leave it — they are already on disk, so nothing is lost and no model call is made.
The step's write happens behind the step: the next step does not wait for it.
recalland the session meter settle the ledger first (they wait for every queued write), so they always see the step that just ended. A plainreadof a transcript file does not wait: each append replaces the file atomically, so a read is never torn — at most it is one step behind. A pruned tool output is only ever pointed at a transcript line that is already written. SESSION.md, one living page. Goal, decisions, environment facts, errors and their fixes, a code map, open questions, and the next step — each line stamped with the turn it came from. Facts the database already holds (files touched, commands and exit codes, tasks, the log) are written by the engine. The page is edited, never rewritten, and by one writer; reversed decisions keep the old text struck through rather than deleted.
The ledger ships enabled (ledger.enabled: true), so every session is laddered from its first turn with no setup; its per-turn transcripts are kept as long as the session is: retention.days (default 7 days idle) removes a session and its ledger directory together — see 18 · Operations. See 02 · Configuration to change either. OceanCode never summarizes a session — the legacy compaction (summarization) path runs only when ledger.enabled is false.
How an open session is stored. An open session lives in memory: every step reads its history from there, and its rows in oceancode.db are written behind the step, in batches, by the one database writer — a step never waits on the disk, however long the session grows. Anything that reads the database rather than the open session (an export, opening a session that already left memory, the retention sweep) first settles the writer: it waits for every queued write, so it sees the step that just ended. A session idle for 15 minutes leaves memory and is reloaded from the database on its next use. What a crash loses: only the writes still queued when the process died — normally the last few milliseconds (oceancode_writer_lag_ms on the metrics endpoint, see 18 · Operations). What is on disk is always an in-order prefix of the session — no gap in its event log, no row ahead of the event it came from — so a resume continues from the last committed step.
Find a session's ledger directory with:
oceancode ledger path <session-id>The tideline — /tideline
You set one number: the share of the model's context where the tideline sits
(ledger.tideline.percent, default 40) — the line. After every step the ledger reads the
window's size in the provider's real tokens (the gauge: input + cache read + cache write of the
step that just ended, plus what that step added — its output and tool results; with compress on, plus the
compressor's saved estimate, removed bytes ÷ 4, so the gauge still measures the uncompressed window). Steps are grouped
into blocks: a block closes once the gauge has grown by ledger.tideline.block since it opened — a
token count, or a share of the line written "35%" (unset = 70% of the line, so 70 000 at a
100 000-token line). While the gauge is at or over the line, the oldest closed block leaves the
window, so the window drops to about line − block and grows again. The block still being filled is
never cut, so the window overshoots the line by at most one block. A single turn larger than the
line is cut block by block while it runs; your message in the oldest block leaves with it.
After a cut the window opens with one line that points at the ledger:
[Earlier content of this session is past the tideline: every row up to seq <n> left this window. The verbatim record is <ledger>/transcript/ (one file per turn) and SESSION.md carries the merged state. …].
A turn whose every row is past the tideline is merged into SESSION.md; a turn cut only in part is
not. If the provider still refuses a request as too long, every closed block is cut before the one
retry.
Between cuts every step only appends, so the provider's prompt cache hits; each cut re-writes the
retained window once. The block size sets how often: a 50k block cuts about 20 times per 1M new
tokens past the line, a 20k block 50 times. When the model's context is unknown the fixed tokens
(default 300000) is the line. A line above what the model can hold (its context minus the output
allowance, at most 32k) is clamped to that, with one note in the log.
The status bar's context row measures against the model's own context (…/1.0M on
claude-fable-5-1[1m]); the ┃ on its gauge sits at the tideline — tideline @40% (400k): 238k to go, or tideline @300k: 138k to go under a fixed limit — and it reads tideline off when the ledger is disabled.
Config examples (ledger.shelve and ledger.window are still read as aliases):
{ "ledger": { "tideline": { "percent": 50 } } }
{ "ledger": { "tideline": { "tokens": 500000, "block": 20000 } } }tokens: 0 means the model's own ceiling. The older keys — the floor (low), reserve, the
profile presets (wide · narrow · strict · auto) and ledger.hard_tokens — were removed
with the band geometry: an old config still loads, they are ignored, and the load logs one warning
naming them.
Change the tideline for a session at runtime with /tideline:
| Command | Effect |
|---|---|
/tideline | Show the current tideline state. |
/tideline now | Move the tide now (a cut). |
/tideline 50% | Set the limit as a percent of the model's context. |
/tideline 500k | Set a fixed limit. |
/tideline default | Restore the configured limit. |
Minimum fixed limit 20k; k and m suffixes are accepted. The floor and the presets were removed;
a second number or a preset word is refused with the usage line. A change applies from your next
message on, which the confirmation says. Headless, the same set is
--tideline "<percent>% | <limit> | default". A sub-agent (child) session inherits its parent's
/tideline override — the percentage, or the limit.
The meter's denominator is the model's context window under auto and the effective window
otherwise.
The session meter
Ocean pager has no cockpit panel; its status bar and /context show the session meter:
- the context meter — what the current window costs against the session's window size;
- token counts — input and output for the session;
- spend —
$spent / $capon a metered route. On the Claude Pro/Max subscription route the bill is a list-price estimate, shown as≈$spent list · subscription; alimitsrow appears when a step or time bound is set.
On the Claude Code route the meter's context number is Claude Code's own real context — its
system prompt, CLAUDE.md files and MCP schemas, ~100k+ before any turn. When it reaches the limit
OceanCode drops the --resume id and cold-starts Claude Code with the ledger page plus the retained
turns' text (tool outputs are not re-sent — the page's links carry them), so the number restarts near
that baseline and climbs again.
Recovering a step — /undo and /redo
/undo (/rewind) reverts the conversation to a previous message and puts back every file that
turn and the later ones changed through the file tools (edit, write, apply_patch, sub-agents'
included); /redo re-applies both. It steps a bookmark through the session, so you can back out a
bad turn without losing the history. As in Claude Code, changes a bash command made are not
tracked.
Exporting a session
Write a whole session out as one file:
oceancode export <session-id> --md [--out <path>|-] # one Markdown document
oceancode export <session-id> --atif [--out <path>|-] # one ATIF-v1.8 trajectory (JSON)Pass exactly one of --md or --atif. --out - (the default) writes to stdout; --out <path>
writes a file. The Markdown export carries frontmatter, every turn, and a ## Summary
table, and is built from the ledger's own transcript files when they exist.
Rebuilding the ledger
If a ledger directory is lost or a session predates the ledger, rebuild it from the database:
oceancode ledger rebuild <session-id> # mechanical: transcripts, indexes, page
oceancode ledger rebuild <session-id> --merge # also fold the merge backlog (model calls; asks y/N)
oceancode ledger rebuild <session-id> --merge --yes # skip the confirmationThe plain rebuild makes no model calls; --merge does, so it is never implied.
See also
- 02 · Configuration — the
ledgeranddatabaseconfig sections. - 08 · Bounding a long run — spend and clock bounds.