Skip to content

Sessions and Persistence

Agents-fleet persists every interactive run as a session so you can resume later with agents-fleet --resume <id> (or pick from the list with agents-fleet --resume).

This page documents where sessions live on disk, the schema versions you may encounter, the NDJSON transcript layout, and how the different exit paths interact with shutdown.

Audience: power users who want to inspect, back up, or troubleshoot their session files. Day-to-day usage does not require any of this.


Where sessions live

All session state is written under your home directory:

~/.fleet/
└── sessions/
    ├── <sessionId>.json                 ← session metadata + transcript (v1/v2)
    ├── <sessionId>.workers/             ← per-worker NDJSON transcripts
    │   ├── <agentId-1>.ndjson
    │   ├── <agentId-2>.ndjson
    │   └── …
    └── <sessionId>.transcript.ndjson    ← coordinator transcript (future split)
  • The <sessionId>.json file is small (metadata, fleet roster, worker index) and loaded fully on resume.
  • The <sessionId>.workers/ directory is created the first time a worker is spawned in a session.
  • The <sessionId>.transcript.ndjson sibling holds the coordinator transcript when the coordinator-transcript split is enabled (see Schema versions below).
  • All session files (the JSON metadata, per-worker .ndjson transcripts, and the future coordinator transcript) are written with mode 0o600 — owner read/write only — so other users on the same machine cannot read your session history.

Session ids are validated on every read/write. Filenames containing path separators or .. segments are rejected.


Schema versions

The persisted JSON file carries an optional schemaVersion field. The runtime understands three versions:

VersionMarkerWhat it adds
v1no schemaVersion keyInline transcript[] array on the JSON file. Pre-Worker-View-Switcher.
v2schemaVersion: 2Optional workers[] roster with per-worker NDJSON paths (Worker View v1).
v3schemaVersion: 3Adds optional msgCount (cached transcript line count) and transcriptPath (relative path to the coordinator NDJSON, default <sessionId>.transcript.ndjson).

All three versions are accepted by the loader. v1 and v2 files are upgraded to v3 the next time the session is saved; the upgrade is additive — no existing fields are removed by this revision.

A future revision will move the coordinator transcript out of the inline JSON transcript field and into the sibling NDJSON file. When that ships, sessions written by the new code will appear to older versions as having an empty transcript history (the JSON file loads cleanly, the NDJSON sibling is simply unknown to them). Resume from within the new code is unaffected.


Transcript layout (NDJSON)

Per-worker transcripts (and, in a follow-up, the coordinator transcript) use append-only newline-delimited JSON — one entry per line, no trailing comma, no surrounding array.

jsonl
{"role":"system","content":"Worker spawned",}
{"role":"assistant","content":"Reading src/index.ts…",}
{"role":"tool","name":"view","args":{},}

Why NDJSON?

  • Cheap appends — adding one entry is a single appendFile call, not a full-file rewrite.
  • Cheap tail reads — the resume path needs only the last N entries. A reverse byte-scan from EOF returns the tail in ~1 ms even on a 24 MB / 100k-entry file (per the BR2 benchmark).
  • Cheap older-load — when you scroll up past the initial tail, loadRange(beforeIndex, count) reads a contiguous slice without touching the rest of the file.

The shared store is at src/sessions/transcriptStore.ts and is reused by every transcript-bearing subsystem: per-worker transcripts today via WorkerTranscriptStore (one .ndjson file per worker under <sessionId>.workers/), and the coordinator transcript plus any future debug captures share the same code path.


Loops are not persisted

Loops created by /start, /loop, and /loop-target are not part of the session schema. They live in the LoopScheduler for the lifetime of the process and are dropped when the session exits, regardless of how the exit was triggered.

When you --resume a session, any active loops need to be re-issued manually:

agents-fleet --resume <id>
> /loop "<goal>" <interval>

This applies equally to task-driven loops (/start, /loop-target with no prompt) and to goal-driven loops (/loop "<goal>", /loop-target <interval> "<goal>"). The session itself emits a one-line chalk.dim warning on resume reminding you to restart the loop if one was previously active.


Exit paths

Agents-fleet has four ways to leave a session, and they should all produce the same user-visible feedback:

PathStatusNotes
/exit, /quit, /q✅ unifiedRoutes through onShutdown, runs steering extraction + insight synthesis + session save.
Ctrl+C (SIGINT)✅ unifiedSame shutdown pipeline; messages render via console.log (Ink still alive).
Ctrl+D (Ink EOF)⚠️ partialCurrently bypasses shutdown. The Ink handleRequestExit wrapper ships in a follow-up release.
Process kill / SIGKILL❌ noneNo cleanup possible by design. Last debounced 2 s save is the recovery point.

When shutdown is triggered from inside the Ink REPL, status messages — "Extracting learnings from N message(s)…", "Found N steering insight(s)", "Insight synthesis complete", "Session saved: <id>", and the Resume: agents-fleet --resume <id> hint — render through the live Ink transcript so they appear before the process dies.

Time budgets:

  • Insight synthesis is capped at 20 s.
  • Overall shutdown is capped at 5 s on top of synthesis (worst-case ~26 s wall-clock from exit trigger to process death).
  • A second exit signal during shutdown forces an immediate exit (idempotency preserved).

Forward-compatibility note

Sessions written by v0.17.0+ can still be opened by older versions:

  • The JSON metadata file loads cleanly — the new msgCount and transcriptPath fields are silently ignored by older readers.
  • Once the coordinator-transcript split lands, older versions will see the inline transcript field as empty and will not display history; no errors, no crashes.
  • Worker transcripts (NDJSON) were already external in v0.16.0 (Worker View Switcher v1), so worker chip transcripts continue to work identically across versions.

If you need to keep working with an old client, set AGENTS_FLEET_NO_MIGRATE=1 (reserved env var) before the coordinator-transcript split lands to skip the eventual migration on load.


Troubleshooting

  • Resume picker shows the wrong message count — the cached msgCount is best-effort. If it appears stale after an unclean exit, open the session once; the next save corrects the cache.
  • NDJSON file has a partial trailing line — the tail reader tolerates a partial trailing line at EOF (a common artifact of process kill mid-append). The partial line is dropped on read; the next append starts on a fresh line.
  • Invalid transcript path on resume — a worker entry in the session JSON has a transcript path that fails the safety checks (absolute, contains .., contains a path separator, or escapes the <sessionId>.workers/ directory). The offending worker entry is silently skipped so the rest of the session can resume; remove the bad entry from the JSON manually if you want to suppress the warning.