Session Lifecycle
Spettro saves your conversation history so you can pause and resume work, clear context when it gets full, and pick up where you left off — even across TUI restarts.
This page covers the full lifecycle: auto-save, debounced writes, session resume, compaction, and the clear/compact distinction.
Auto-save
Spettro automatically saves the current session to disk as you work:
- After every completed agent run (when the assistant message is appended).
- Debounced during a run — tool-stream updates and progress comments are persisted at most once every 2 seconds to avoid thrashing the disk.
- On interrupt — when you press
Escmid-run, the kept-progress summary is saved immediately. - On
/clear,/compact, and session switch — an unconditional save guarantees nothing is lost at these critical points. - On exit — the final turn inside the debounce window is flushed before the TUI shuts down.
Storage path
Sessions live under ~/.spettro/sessions/. Old sessions can be reclaimed
with /storage clean, which never touches the active session
and always keeps the most recent few per project. Each session is identified by a
project-specific hash combined with a timestamp:
~/.spettro/sessions/
└── <session-id>/
├── metadata.json — project hash, start time, goal state
├── messages.json — chat messages (user, assistant, system)
├── tasks.json — session task graph (todos.json kept as legacy alias)
└── events.jsonl — tool traces, approval decisions, agent spawns
Task graph
Session tasks form a persistent dependency graph, not just a flat list. The
agent manages it with the todo-write tool (the retired task-create,
task-update, task-get, task-list and task-delete names still work as
aliases of it). The tool's description tells the model to use it only for
genuinely multi-step work and never as the only call in a step: a step spent
on the list alone does not advance the task. When a step's calls are all
successful todo-write calls anyway, the first such step of a turn gets one
line after its normal result: "Plan updated. Continue with the next concrete
action in the same response next time." Later ones in the same turn get
nothing extra. It is advertised in every host,
headless goal runs included; ACP clients show the list as the session plan.
todosreplaces the whole list; withmerge: trueit inserts or updates only the given tasks byid, and fields left out keep their stored value.deleteremoves tasks by id andclear_completedprunes completed and cancelled ones. A call with none of these only reads the list.Every call returns the full list in dependency order, each task with a derived
blocked_by(its incomplete dependencies) andready(pending, all dependencies met).Sub-agents share their parent's session folder, so below the top level a full replace is merged instead: a worker can add and update tasks but never wipe the orchestrator's list. In that merge a task written without an
idupdates the stored task with exactly the samecontent, so a worker that rewrites its whole list does not add copies.Each task has an
id,content,status(pending,in_progress,completed,blocked,cancelled) and optionaldependencies(IDs of tasks that must be completed first).Dependencies are validated on every change: self-references and cycles are rejected, dependencies on unknown IDs are dropped with a note in the result, and a merged task cannot be moved to
in_progressorcompletedwhile any dependency is incomplete. Tasks written without anidget the next freetask-N.The TUI side panel and
/tasks listrender the graph live during runs; pending tasks gated by incomplete dependencies show as blocked.References to deleted tasks are stripped from other tasks' dependencies so the graph stays valid.
The graph is persisted per session, so a
/resumerestores the plan exactly where it was left.
The session directory is created inside the project-local .spettro/ directory
when one exists, falling back to the global ~/.spettro/sessions/.
What is NOT saved
Transient stream blocks (the live "thinking…" and "answering…" messages that update character-by-character during a run) are stripped before saving. Only the final, authoritative assistant message is persisted.
Resume
You can load a previous session with /resume:
/resume
This opens a picker showing saved sessions for the current project:
Choose a session to resume:
› 2025-01-15 14:30 — implementing the auth middleware
2025-01-14 10:15 — reviewing PR #42
2025-01-12 16:00 — setting up CI pipeline
↑/↓to navigate.Enterto load the selected session.Escto cancel.
When a session is loaded:
- The chat messages are restored: your prompts, the assistant's answers (with their thinking) and system messages. Tool-call rows and plan cards are not saved with the messages, so they do not reappear in the transcript (step 3 shows what the tools did).
- The model's context for the first new turn is the saved transcript as flattened text, since the structured history of that conversation (tool calls and their outputs) is not saved. From that turn on the structured history grows again as usual.
- Session events (tool activity, approval decisions, agent spawns) are replayed into the activity feed and side panel.
- Session tasks (todos) are restored.
- What belonged to the conversation you left is dropped: the context gauge
starts from zero again, and a plan waiting for
/approveis discarded.
If the session had an unfinished goal in progress, Spettro remembers its
state (objective, iteration count, no-progress counter, elapsed time) and
offers /goal resume after loading.
Auto-resume on startup
At startup, Spettro does not auto-resume — you always start with a fresh
transcript. Use /resume explicitly to return to a previous session.
Compact (/compact)
When the conversation grows long, the context window fills up. Compaction shrinks the conversation the model sees, freeing token budget for new work:
/compact
The TUI, ACP and the run loop (every mode, headless and /goal included)
share one compaction core, which always keeps, verbatim:
- the original task: the conversation's first message (with its environment snapshot), even many turns later;
- the latest user messages: up to three of the most recent user requests and steering messages from the compacted span, so the current request is never summarized away however long the run on it has been;
- the most recent tool exchanges: the last three tool calls with their results (two on a forced compaction), with call/result pairing checked before the history is used, so providers never see an orphaned tool result or an unanswered call.
In the TUI the transcript view is replaced by the summary, prefixed with
── conversation compacted ──.
You can focus the compaction on a specific topic:
/compact auth middleware
This gives the LLM a hint about what to prioritise in the summary.
Two-stage compaction: prune, then summarize
Stage 1 is cheap and needs no model call; stage 2 is the summarizer.
Stage 1: prune old tool outputs. Every tool result larger than ~500 tokens is already persisted to the session spool at execution time. Before summarizing anything, compaction replaces old results with a short stub that keeps the size, the spool ID, the tool name, an args digest, the ok/error status, and the first and last line:
[output elided: 48210 chars, spool:7 — re-read with tool-output {"id":"spool:7"}] bash args={"command":"go test ./..."} — 1204 lines, status error, head: "…", tail: "FAIL spettro/internal/agent"Spooled outputs go first, and the most recent ones (about a fifth of the window, up to 40k tokens) are left alone. If that is not enough, every large output before the verbatim tail is stubbed, with a head/tail excerpt for outputs that have no spool copy, and very large strings in old tool-call arguments (such as a whole file passed to
file-write) are elided. The full output stays on disk and the model can re-read it at any time with thetool-outputtool ({"id":"spool:7","offset":0,"limit":4000}). If pruning brings the estimate back under the auto-compact threshold, compaction stops here: no summarizer call and no turn dropped.Stage 2: summarize. If the history is still too large, or on an explicit
/compact, the turns between the task and the verbatim tail are replaced by one structured summary with these sections: Goal, Decisions and findings, Files modified (each path and what changed), Current state (test and build status, with exact failing tests and error text), Next steps and References (spool IDs worth re-reading). The summarizer sees edits, commands and error output (bounded per item to fit its window, keeping the head and tail of long outputs), plus any earlier summary to merge. The list of files changed by the edit tools is also derived straight from the tool log and attached to the summary, so no edited file can be forgotten. If the summarizer fails while the run is recovering from an overflowing context, a summary extracted from the transcript (user messages, files modified, recent commands and errors) is used instead of failing the run (not when the run itself was cancelled: the history is then left as it was). A compaction that would not make the history smaller is discarded.
After compaction:
- Token usage and context pressure are reset to zero.
- The compacted structured history is carried to the next turn (one cache miss on the next request, then the new prefix caches again).
- Session tasks are kept.
Auto-compact
Auto-compaction runs automatically when the context window exceeds a configured threshold:
/compact auto on # enable
/compact auto off # disable
/compact auto status # check current setting
When enabled, Spettro compacts in two places:
- Between turns (TUI and ACP): after an agent turn, if context occupancy is above the threshold percentage. This goes cheapest first like the run loop: pruning before summarizing, and no summarizer call when the pressure comes from the system prompt and tool schemas rather than the history (it then waits, silently, for the history to grow before trying again).
- Inside the run loop (all modes, including headless and
/goal): before each model step, the runtime estimates context pressure and, past the threshold, prunes old tool outputs and, if that is not enough, summarizes older turns as described above. A one-line notice ("compacted 42k → 6k tokens …") appears in the transcript. This is what lets long unattended goal runs survive without anyone watching the gauge.
The threshold percentage is configurable in ~/.spettro/config.json
(default 85 % of the model's effective window). The auto_compact_* settings
below apply to both triggers.
Auto-compact uses a failure budget: if the summarizer fails 3 times in a row
(provider errors), auto-compaction pauses instead of burning a failing call
every step; a successful compaction (e.g. manual /compact) resets the
counter. Failures never abort the run: the runtime notes each one in the
transcript ("failure 1 of 3 before it pauses") and tries again at the next
step while the context is still over the threshold, says so when it pauses,
and an over-budget request still gets one forced compaction as a last
resort.
Configuration
| Config key | Default | Description |
|---|---|---|
auto_compact_enabled |
true |
Enable auto-compaction. |
auto_compact_threshold_pct |
85 |
Context window % at which auto-compact triggers. |
auto_compact_max_failures |
3 |
Consecutive failures before auto-compact gives up. |
Policy
/compact policy
Shows the current thresholds, failure counter, and warning level:
context window: 100000 tokens
threshold: 85000 tokens (85 %)
currently used: 32000 tokens
status: OK (32%)
auto-compact: on
failures: 0 / 3
The context gauge in the status bar turns yellow at ≥75 % and red at ≥90 %.
Live updates during a run
Both the context gauge and the session cost counters update after every LLM request inside a turn, not only when the agent finishes. Multi-step runs (tool loops, goal iterations) therefore show rising occupancy and cost while the agent is still working, so you can interrupt early if a run is burning more context or budget than expected.
Two counters are kept deliberately separate:
| Counter | What it measures | Status-bar role |
|---|---|---|
Context occupancy (contextTokens) |
Largest single LLM request of the current/most recent run — how full the window is | Drives the N / M ctx gauge and auto-compact |
Session cost (totalTokensUsed) |
Sum of every prompt+completion token across the whole session | Goodbye stats, remote status, /stats |
A multi-step run that re-embeds the same history on every step does not inflate the gauge: only the largest request counts as occupancy, while each step still adds its cost. When the run ends, the final totals only add any remainder that live updates missed (for example a dropped event), so cost is never double-counted.
/stats still shows the full provider-reported breakdown (input, output,
cache read/write, per-model) once you want the detailed accounting.
In ACP mode the same live path emits a usage_update session
notification after every request, and the completed turn's aggregated usage
is returned on the session/prompt response.
Clear (/clear)
/clear
- Saves the current conversation to disk (exactly as
/resumewould find it). - Clears the chat transcript, the structured conversation history, the
token counters and context gauge, and a plan waiting for
/approve. - Starts a fresh session.
Use /clear when you want to start a new topic without losing the previous
one. The saved session is available via /resume later.
Full lifecycle example
1. Start Spettro → fresh session
2. Work for a while → auto-save runs in background (debounced)
3. Context is getting
tight (yellow gauge) → auto-compact when crossing 85%
4. Continue working → auto-save continues
5. Switch topics → /clear (saves + starts fresh)
6. Next day → /resume, pick yesterday's session
7. Work more → /compact manually to keep context lean
8. Quit → flushSave writes the last turn
Retention
Sessions are never automatically deleted. They accumulate under
~/.spettro/sessions/. You can remove old sessions manually:
rm -rf ~/.spettro/sessions/<session-id>
There is no built-in session manager or retention policy yet.
Background jobs
Spettro tracks detached shell processes started by the agent with run_in_background
(e.g., dev servers, watch builds, long-running scripts). Jobs are process-wide
session state: they outlive individual agent turns and are killed when the session
ends.
Listing jobs
/jobs
or
/jobs list
Prints every tracked job with its ID, status, command, and elapsed time:
background jobs:
- job-1 [running] npx vite --port 5173 (started 5m23s ago)
- job-2 [exited] go run ./cmd/server (started 2m10s ago)
kill with /jobs kill <id> or /jobs kill all
Killing a job
/jobs kill job-1
Kills the job's entire process group. Accepts any job ID shown in the listing.
/jobs kill all
Terminates every running job at once.
Lifecycle
- Jobs are created when the agent calls
bashwithrun_in_background: true. - Output is captured in a per-job ring buffer (up to 1 MiB of combined stdout/stderr, oldest bytes dropped when exceeded).
- When the session ends (TUI exit,
/exit), all remaining jobs are killed automatically. - When the terminal or tmux pane spettro runs in is closed (SIGHUP), spettro
kills every foreground shell command, background job and PTY session before
it exits. From that moment it also refuses to start new foreground shell
commands (they fail with "spettro is shutting down; command not started"), so
an agent reacting to its killed command cannot leave a new one running.
Under
nohupthe hangup is ignored and everything keeps running. - Jobs survive
/clear(which only resets the conversation). Use/jobs kill allto clean up explicitly.
Tool output spooling
Oversized tool results (from file-read, grep, glob, bash, web-fetch)
are automatically spooled to disk instead of being
hard-truncated. The model receives a truncated head with a footer containing a
spool:N ID and an offset, and can page through the full result with the
tool-output tool ({"id":"spool:N","offset":Z,"limit":M}), which every agent
holding file-read has.
In addition, every tool result over ~500 tokens — even ones small enough to
stay in context untruncated — is written to the spool at execution time. This
backs reference-based compaction (see Compact): when the
context fills up, old oversized results are swapped for [output elided: …] stubs
pointing at their spool IDs rather than being lost to summarization.
Spool files are tied to the conversation, not to a single run: they survive
run end, and are deleted on /clear and when the process exits (TUI exit,
/exit).
The files are written in the background, in the order the results were
produced. Until a file is on disk, tool-output pages through the result from
memory. Only a truncated shell result names its spool file, and it waits for
that one write so the path always exists by the time the result is returned;
every other result returns without waiting for its file. /clear and a
normal exit wait for pending writes before deleting the directory. If a write
fails (for example, the disk is full), the result stays readable through
tool-output from memory, up to 32 MB of such results per session; past that,
tool-output reports the result as lost.
# example: model receives truncated grep output with a footer
[truncated: 12,400 of 13,000 lines omitted; use tool-output {"id":"spool:2","offset":1800} to read more]
# model pages through the omitted portion
~> tool-output {"id":"spool:2","offset":1800}
<~ output=spool:2 size=280000 next_offset=9800 (more available)
# the next chunk of content...
bash (and its retired bash-output alias) also accepts job_id and
offset in place of command, and then reads a background job's output like
job-output.
