OceanCode
Bounding a long run
A run is bounded by the money and time you set, not by an arbitrary step count. Set nothing and a run keeps going until the work is done (or the no-progress guard stops it, below).
The bounds
| Bound | Environment variable | oceancode run flag | Slash command (this session) |
|---|---|---|---|
| Spend cap (USD) | OC_MAX_USD | --max-usd 25 | /budget 25 |
| No spend cap | — | --max-usd off | /budget off |
| Subscription (no cap; spend is a list-price estimate) | — | --max-usd subscription | /budget subscription |
| Wall clock | OC_MAX_MINUTES | --max-minutes 8h (also 45, 90m, 2d, off) | /limits time 8h |
| Steps per turn (root), 1–1000 | OC_MAX_STEPS | --max-steps 100 (default clears) | /limits steps 100 |
| Steps per sub-agent turn | OC_MAX_SUBAGENT_STEPS | — | — |
| Tideline (% of context) | ledger.tideline.percent (config) | --tideline 50% | /tideline 50% |
| Tideline (fixed tokens) | ledger.tideline.tokens (config) | --tideline 60k | /tideline 60k |
| Tideline block (what one cut removes) | ledger.tideline.block (config: a token count, or a share of the line as "35%"; unset = 70 % of the line) | — | — |
Precedence: slash command / flag > environment variable > built-in default. Flag and slash
values are stored on the session, so they survive --continue.
The tideline's old names still work as aliases: --shelve, /shelve and ledger.shelve.
The step cap is a boundary, not a stop
OC_MAX_STEPS (default 300) caps the steps in one root turn, but a root turn that reaches it
auto-continues into a fresh turn — no budget or clock needs to be set. The ledger cuts by
blocks after every step, so even one long turn stays under the tideline (see
06 · Session ledger and export). 0 is
rejected: bound a long run with a budget or a clock instead.
Sub-agents are different: they simply stop at OC_MAX_SUBAGENT_STEPS (default 300), so a
raised root cap does not deepen them.
Output tokens
OC_MAX_OUTPUT_TOKENS overrides the per-request output-token cap in either direction, clamped
to the model's own published maximum. It bounds how much a single model response may emit, not
the run as a whole; leave it unset to use the model's full maximum.
What happens at a bound
Exceeding a bound is a clean, non-error stop, distinct from a crash, and the ledger records
it as a step-limited turn. The internal stop reasons are BudgetExceeded and TimeExceeded.
Over the Ocean tunnel the response carries a typed stopped_by:
| Bound reached | stopped_by |
|---|---|
OC_MAX_MINUTES / /limits time | time-exceeded |
OC_MAX_USD / /budget | budget-exceeded |
| step cap | step-limit |
The plan drives the run
The model keeps its plan as a todo list (see 05 · Plan mode and todos). A root turn that settles while any todo is still open does not end the run — the engine works off the next open todos and drains again. A run ends only when:
- the work settles with no open todo; or
- a spend or clock bound trips; or
- you interrupt it; or
- the no-progress guard fires — two consecutive continued turns that leave the open and completed counts unchanged stop the run cleanly with a "Run stopped" toast.
So a model that never plans and never settles runs a few turns and stops; one that plans and makes progress runs until its list is done or a bound trips.
While a background sub-agent of the session is still working, a settled turn is not worked off: the sub-agent is what moves the plan, and its result re-drives the session when it lands. The work-off resumes in that re-driven turn.
Such a turn ends as waiting, not finished, and says on what. The pager's end-of-turn line
reads Worked for 6m38s · waiting for sub-agent "Build the mock" · 2 plan rows open · continues when it lands; an ACP client gets _meta.oceanStopReason = "waiting" and _meta.waitingNote
on the prompt response, and the session's log holds a durable session.waiting row. When the
sub-agent lands, the engine starts the next turn itself (a durable session.resumed row) and
the pager draws it as it runs; the sub-agent's result row tells the model how long the run
paused, so it can say so when asked. That turn shows as running from its first instant: an ACP
client gets ocean/queue/changed naming it as running before its first frame, and the pager's
status row reads what a turn you started would (Waiting for response… during a silent think,
Thinking…, the running tool, the sub-agent it waits on). Every turn the engine starts this way
gets its session.resumed row, including the follow-up that answers a result which landed while
a turn was running. A message you send while a turn is ending as waiting is answered as its own
turn and streamed under its own prompt id. The restart is guaranteed:
- a sub-agent that fails or crashes still reports, as an error result, and the run goes on;
- a sub-agent you stop (
ocean/subagent/cancel, orescescon the session) does not restart the run; if plan rows are left open, the session records why (reason: user_stopped_child) and the next message you send continues it; - if the engine exits while a sub-agent works (a quit, a crash), the next time the session is opened its lost sub-agents are reported as errors ("the engine restarted while it worked") and the run goes on. A session whose engine is still running elsewhere is left to it.
oceancode run waits for its background sub-agents and the turns their results start before
it exits.
The Jev supervisor (opt-in)
With the Jev decision layer on (see 13 · Jev decisions), a supervisor
reads a root turn's clean finish and, before a bash call whose exact command already ran twice
among the prompt's last six, asks whether it would bring anything new (the no-progress check).
With jev.supervisor = "act" it may keep a finish from ending the turn —
a synthesized continuation, at most jev.supervisor_max_continuations per human prompt
(default 1), only when the goal is clearly not complete and progress is real. Two consecutive
no-progress answers send one steer message; four stop the run — under headless --auto it ends
with stop reason no_progress (exit 5), elsewhere you are asked Continue or Stop. With
jev.check = "act", headless --auto also ends a run right after a check verified a pass. It
never moves a bound: the spend, clock and step bounds above, the exact-repeat doom-loop trip, the
hook cap and the no-progress guard decide exactly as they do without it. In shadow, the default
once the layer is enabled, it only records what it would have done.
Stopping a run stops all of it
esc esc (or the palette's abort) stops the session's own drain at its next step boundary,
and every sub-agent stops with it — foreground ones immediately, background ones cancelled by
the parent. A late background result is recorded as cancelled and never restarts the run.
Background jobs and long waits have no clock of their own
A job the model starts — bash with background: true, a monitor, a loop, a scheduler —
runs until it ends or the model calls job_kill. It dies with the run only when the run has a
wall clock (OC_MAX_MINUTES / /limits time); without one a build, a watch or a server runs for
hours or days, and job_output reads it back whenever the model asks. (Until 2026-10-05 every
job was reaped ten minutes after launch while the run had no clock at all.)
A job that ends on its own — done or failed, not killed — reports back. Its result (state,
command, the last 2,000 bytes of output) is posted to the session that started it as a
<job id=… state=…> message and a new turn starts, the way a background task reports; a turn
already running gets exactly one follow-up turn. So the model does not have to sleep and poll to
learn a job finished, and a turn that stopped polling still hears about it. A job a sub-agent
starts wakes no one.
Polling is not a loop either. The doom-loop trip ends a turn only after three consecutive fast
calls with the same input and the same result. A repeat whose output moved (a row count, a
timestamp, a log line) or a call that ran 30 seconds or more (a sleep 300; check) resets the
count to zero. When the trip does fire, the turn ends with stop reason doom_loop and the final
text Stopped by the doom-loop guard: … stays in the transcript, not only in a passing toast.
(Until 2026-10-24 three identical sleep 298; wc -l results.jsonl polls of a live benchmark
tripped it and the turn went quiet with no visible reason.)
A long wait does not lose the prompt cache either. Every request's cache markers carry the
provider's one-hour TTL, and while a step waits — on a sub-agent, on a long bash — or the
session idles, the engine re-reads the last request every OC_CACHE_KEEPALIVE_MINUTES
(default 50) so that clock restarts: a cache read of the prefix (session.cache.touched, cents)
instead of re-writing it when the wait ends. It stops after OC_CACHE_KEEPALIVE_MAX touches (default 48, ≈ 40 h; past a model's break-even — 80 touches on Fable 5.1, 20 on a 0.1× model — a re-write is the cheaper bet). A touch that fails is sent once more a few minutes later before the keep-alive gives up, and pressing Esc on a running turn does not stop it — the next prompt reads the same prefix. See 02 · Configuration.
See also
- 02 · Configuration — the
OC_MAX_*variables in one table. - 07 · Headless and server — the
--max-*flags onoceancode run.