Sessions and goals
Resume, fork, inspect and export your work.
Manual for v0.9.5. Stable release: v0.9.5.
Documentation · Context · Commands
octet --continueSessions are append-only JSONL files, grouped by workspace under the session
directory (--session-dir PATH). Only completed steps are saved, not streaming
deltas. Parent links keep branches without rewriting history. Names and tags
live in .metadata/ sidecars, so the conversation file stays unchanged.
Resume and branch#
octet --resume # pick from a list
octet --resume SESSION_ID
octet --fork SESSION_ID # new session from that head
octet --fork # pick, then forkResume restores the model, reasoning, prompt identity, tool panels, branches and
prompt colors. An explicit --model or --reasoning overrides the recovered
value.
Inside a session, /resume, /fork, /clone, /name and /export do the
same (commands). /tree navigates branches inside the same
session file; /fork creates a distinct new file. Choose no summary, a normal
branch summary, or a summary with custom instructions. A summary covers only the
abandoned span since the common ancestor, not sibling branches; it adds handoff
context without compacting the selected ancestry. Selecting a user/custom message
restores its text for editing rather than submitting it. Cancellation, veto or
summary-provider failure leaves the branch selection unpublished. In the interactive TUI, a name you give a
session shows in the terminal window title as octet · <name>, and unnamed
sessions use octet. Renaming, resuming or starting a session updates the
title. Plain, print and RPC modes don't set a window title.
In the resume picker, type to filter by fuzzy match, "quoted phrase" or re:
regex. Tab switches between this workspace and all workspaces. Ctrl+S cycles the
sort (recent, title, message count), Ctrl+N shows named sessions only, Ctrl+P
toggles paths, and Ctrl+R renames. Ctrl+X (or Delete) asks to move the selected
session to trash, naming it: Enter confirms and Esc cancels. Trash is
recoverable. The session stays on disk, hidden from the picker, and the web UI's
trash can restore it. You can't trash the current session. A session from
another workspace can't be resumed into the same live app.
The session goal#
/goal <objective> stores a durable objective for the current session;
/goal status reads it back and /goal pause, /goal resume and /goal clear
walk the rest of its lifecycle. The goal lives in a private per-session store and
survives resume, but it is addressed by the session key, so a new session starts
without one. While a goal is active, a settled turn is followed by an automatic
continuation toward the objective after a short grace period. Continuation ends
on GOAL_COMPLETE, on GOAL_BLOCKED with a reason, or when the stored turn
budget is exhausted. Unlike the other commands, /goal is never queued: it
applies immediately, and a run that cannot address the goal store fails closed
with a visible reason.
Commands#
octet sessions list
octet sessions list --query parser
octet sessions inspect SESSION_ID
octet sessions rename SESSION_ID "parser hardening"
octet sessions tag SESSION_ID rust local-model
octet sessions export SESSION_ID
octet sessions export SESSION_ID --output ./handoff.octet-session.json
octet sessions export SESSION_ID --format jsonl --output ./handoff.jsonl
octet sessions import ./handoff.octet-session.json
octet sessions share SESSION_ID
octet sessions delete SESSION_ID
octet sessions repair SESSION_ID
octet doctorlist is read-only. It searches IDs, names, titles, tags, JSONL paths and dates
in this workspace's store, and shows modified times as relative ages. inspect
reports title, size, entries, head, checkpoints, usage, and branch roots and
leaves, including abandoned forks (which never count as active context).
doctor runs read-mostly checks without starting an agent or an extension.
Recovery#
delete moves the files to .trash/. repair makes an owner-private backup,
then removes only an interrupted final append. A corrupted completed record is
reported, never rewritten. A dropped run never silently replays a mutating call,
so its outcome is indeterminate. Cancelling can't undo what already ran. See
network and effect recovery.
Portable export and redaction#
export writes an owner-private octet-session-export version 1 JSON file. It
won't replace an existing path without --force. Redaction is on by default and
scans prompts, tool arguments and results for credential-like keys and strings.
It's a safety filter, not proof that arbitrary prose or media has no secret.
--include-secrets asks for raw export-eligible values, with a warning. Use it
only for a trusted destination. Private extension metadata (including
annotations with no public flag) is always left out, in both JSON and HTML,
even with --include-secrets. That filtering doesn't change the private source
transcript. Private extension JSON string values may contain ordinary LF, CR,
and TAB; JSONL stores them escaped. Object keys, namespaces and entry types
remain control-free, and NUL, ESC and other controls remain rejected. Public
metadata retains its control-free contract. Existing metadata byte, depth,
node-count and key-size limits still apply.
For a shareable page, run
octet sessions export SESSION_ID --format html --output ./session.html. It
writes one owner-private, script-free file with formatted Markdown, bounded code
highlighting, branch links and validated inline image previews. Author HTML,
external links and terminal control characters stay inert, and audio and remote
media aren't fetched. Metadata keys named Image or Audio stay literal data,
not previews. See media privacy.
JSON and native JSONL preserve the whole durable graph and selected head; JSONL
is Octet data, not Pi output. /export defaults to HTML, while an explicit
.json or .jsonl path selects the corresponding data format. HTML cannot be
imported. octet sessions import PATH or interactive /import PATH creates a
new private session from supported Octet exports/native JSONL or Pi v3 JSONL.
The source is never rewritten; unsupported or lossy semantics refuse import.
Imported extension metadata and effect-replay claims do not grant authority.
Interactive import asks before making the copy/switch and preserves the draft.
/share or octet sessions share ID prepares a bounded, redacted snapshot of
all branches, not just visible messages, and shows a local preview path and
SHA-256. Review it: automatic redaction cannot guarantee removal of private prose
or media. Default confirmation is cancel. Publication starts only after explicit
approval of that snapshot, using your separately configured GitHub CLI to create
an unlisted gist. Unlisted is not private: anyone with the link can read, copy
or redistribute it. --yes is explicit CLI publication approval. The verified
snapshot bytes, not a later source reread, are sent; preview changes require fresh
review. Cancellation cannot undo a publication GitHub already accepted. Radius
organization sharing is not supported.
What the scanner covers
Authorization and cookie headers, common API-token prefixes, credential assignments and URL queries, URL userinfo, private-key blocks, and JSON objects or arrays serialized inside strings. It keeps surrounding prose and UTF-8 intact and reports what it replaced. The scan is bounded and deterministic.
Discovery catalog#
Each workspace store keeps a disposable SQLite index,
.catalog/sessions-v1.sqlite3, so listing is fast. JSONL and .metadata/ stay
authoritative, and deleting .catalog/ is safe. It's recreated on demand.
How the index is kept
It holds titles, active-branch message counts and transcript size and mtime
fingerprints. Only missing or stale entries are streamed. The active row
refreshes on normal app close, and sessions that no longer exist lose their
rows. The picker lists the workspace-key directories under the shared root and
shows their .workspace markers. If the index is unavailable, locked, corrupt,
oversized or from a newer version, octet scans the JSONL instead and rebuilds a
corrupt one.
JSONL schema#
Session files are JSONL with entry, head, checkpoint, usage and
usage_uncertainty records, plus optional creation metadata in a leading
header. The schema is here for tools that read them.
Record types, entry and head examples, and rules
Each line is one JSON object with a type:
| Record | Meaning |
|---|---|
header |
Optional immutable creation identity, workspace, timestamp and parent-session reference; not a conversation entry. |
entry |
Immutable, parent-linked conversation or state. |
head |
The active branch and cumulative cost. |
checkpoint |
A completed prompt and its exact restorable head. |
usage |
Provider, model, token and cost accounting for one operation. |
usage_uncertainty |
Accepted-attempt exposure whose usage and cost are unknown. |
An entry, where parent: null marks a root:
{
"type": "entry",
"id": "entry-id",
"parent": "previous-entry-id",
"metadata": {
"prompt_model": "local-model-id",
"prompt_model_source": "local",
"prompt_color": "#5a36d6"
},
"value": { "type": "message" }
}Entry values include message, compaction, branch_summary, config, prompt_template_selected,
skill_activated, skill_resource_read and skill_deactivated. Template name
and hash stay outside the model's context. Compaction snapshots the active
skills and the cumulative Pi-compatible details.readFiles and
details.modifiedFiles. Older records without those fields get empty lists.
metadata.prompt_color is a normalized sRGB value set when the prompt is first
appended. It's never sent to the provider, and it survives resume, checkout,
branching, compaction, model switches and theme reloads. A legacy prompt without
it may get a fallback from its historical prompt_model, never the currently
selected model.
usage kinds include assistant turns, rejected Responses turns, compaction,
terminal gates, cache_warm and delegated_agent. Cache refresh usage counts
toward exact session totals and ceilings, never assistant-turn statistics or
model-visible context. Separate payload-free cache_warm lifecycle records
store attempt, route/model, timestamp, state, optional prefix anchor, and
extension_override; old records default missing metadata. An unsettled
started record remains uncertain on reopen. /session also reports the live
user-selected warming mode, economic decision and known refresh subtotal.
See cache warming.
A delegated record names the child, its
turn and tool-call counts, token totals (counted separately), route and model,
and exact cost. The child's JSONL stays the detailed transcript. A root mirror
is written once before the owning checkpoint, so session cost, /cost,
export, resume and cost limits all include child spend.
An accepted inference attempt that's interrupted before authoritative usage is
available is recorded as usage_uncertainty, not as zero tokens or zero
cost. Its record holds only trusted endpoint, model and operation
identifiers, each at most 128 ASCII identifier bytes. No endpoint URLs, request
or response bodies, prompts, credentials or error text go in it. Unknown
exposure uses the same owner-private, locked, synced append path as known usage,
and a failed append must stop automatic replacement. The writer records each
failed physical attempt once, not each retry notification, so the records are
evidence, not provider-confirmed billable-attempt counts.
Session::has_uncertain_usage() stays true after later successful turns,
checkpoints, compaction, checkout (to another branch or the root) and reopening.
It's session-wide accounting, never model-visible context or head state.
Existing usage and cost totals are known subtotals, not complete spend. If
usage_uncertainty.bound is present, the ledger includes its recorded token
and worst-case cost exposure when checking limits, including after resuming.
A historical bounded record does not authorize a new provider request. Current
transports lack a trusted input-token admission bound, so hard token or cost
ceilings refuse new inference before dispatch, even when an output cap exists.
Planning estimates, prior usage and model context windows are not input bounds.
New ambiguous attempts therefore retain unbounded uncertainty; missing bounds
fail both ceilings, and missing prices fail the cost ceiling.
usage_uncertainty_records() exposes the bounded identifiers separately from
the optional admission bounds, and neither invents
provider-confirmed usage. Delegated exposure is mirrored into the owning root
ledger by child, recording only increases.
Older sessions without these records keep their known ledger, and the reader doesn't invent evidence about past failures. Fork and clone start a new accounting session: like known usage telemetry, uncertainty isn't copied by the active-branch conversation projection. Restoring within the original session never clears its exposure.
The head record is the only thing that changes the selected branch:
{
"type": "head",
"id": "entry-id",
"total_cost_microdollars": 0,
"total_cost_picodollars_remainder": 0
}Branching and compaction never rewrite entries. Context walks parents from the selected head and applies compaction boundaries. Completed records are strict UTF-8 and strict JSON, and only a final unterminated record counts as a recoverable torn append. Reads are bounded by bytes and record count. Persistence invariants.