Session retention
Retention is a Zuno-owned capability exposed by the two /api/session/prune operations and the matching CLI command.
The one thing to get right
--archive is reversible. --delete is irreversible.
--archivewrites a single column:session.time_archived. Nothing is removed. The reverse operation exists in the library (zuno_db::prune::PruneRequest::restore_archive, which setstime_archivedback toNULL) and is covered bycrates/zuno-db/tests/prune.rs::prune_archive_is_reversible_without_deleting_session_data.--deleteremoves rows from the tables below, sweeps orphaned parts, and switches artifact collection into delete mode. There is no undo. Restore from a backup or from the source of the data; nothing in this binary can bring it back.
Be aware of one asymmetry before you archive at scale: the CLI and HTTP surfaces can set the archive marker but cannot currently clear it. Reversing an archive today means calling restore_archive from Rust or clearing the column yourself. The reversibility is real, but it is not yet a flag.
Preview first, always
With neither --archive nor --delete, the command is a preview and mutates nothing — crates/zuno-db/tests/prune.rs::prune_default_preview_is_inert_across_every_real_table asserts inertness across every table, and prune_preview_counts_exactly_match_the_subsequent_transactional_delete asserts the preview's counts are the counts the delete then produces.
zuno session prune --older-than 90
zuno session prune --older-than 90 --format json2
Flags
| flag | effect |
|---|---|
--older-than DAYS | required; the retention window |
--by updated|created | which timestamp the window applies to; default updated |
--project PATH|ID | scope to one project; default is the current project |
--all-projects | every project; conflicts with --project |
--archive | set the reversible archive marker; conflicts with --delete |
--delete | irreversibly remove; conflicts with --archive |
--include-shared | do not exclude shared sessions from selection |
--include-recent | do not exclude recently active sessions |
--force | proceed when a shared session's remote copy cannot be unshared |
--yes | pre-confirm a delete; requires --delete |
--format table|json | output shape; default table |
The confirmation gate
A delete never proceeds unasked. On a TTY you get a prompt. Without a TTY, and without --yes, the command refuses:
--delete requires --yes when stdin is not a TTY; nothing was changedAnswering anything but yes at the prompt is the same refusal:
session deletion cancelled; nothing was changedBoth are asserted in crates/zuno-cli/src/cmd/session_prune.rs. Note that the refusal happens before stdin is read at all, so a delete in a pipeline cannot be confirmed by whatever bytes happen to arrive.
Shared sessions
A shared session whose remote copy cannot be unshared is refused, not silently deleted locally. --force proceeds and says so verbatim in the report's warnings:
remote unshare failed for shared session <id>: <detail>; local rows were deleted because --force was supplied and the remote copy may surviveThat is the honest statement: the local rows are gone and the remote copy may not be.
What a delete touches
Generated from zuno_db::prune::DELETE_ORDER. The order is pinned by crates/zuno-db/tests/prune.rs::prune_delete_order_and_true_related_table_count_are_pinned, because the order is what keeps foreign keys satisfied mid-transaction.
14 tables, in this order:
| order | table |
|---|---|
| 1 | memory_reflection_job |
| 2 | memory_reflection_delivery |
| 3 | agent_job |
| 4 | work_item |
| 5 | work_plan |
| 6 | session_context_epoch |
| 7 | session_input |
| 8 | session_message |
| 9 | part |
| 10 | message |
| 11 | session_share |
| 12 | session |
| 13 | event_sequence |
| 14 | event |
Regenerate with:
ZUNO_DOCS_REGENERATE=1 cargo test -p zuno-cli --test docsAfter the table deletes, parts with no surviving session are swept, and artifact collection runs in delete mode.
Reading the artifact warning
A report may carry:
`<database>` contains <n> sessions; artifact reclamation is skipped because shared snapshot stores cannot be attributed and may belong to another channel's database.This is not a failure. It says the run could not prove a snapshot store belongs to the sessions being pruned, so it left the bytes alone. The most common cause is running a source build against a release install's data directory — the two select different database files. See migration.md.
An n of 0 here alongside a database you know has sessions is the signal that you are looking at the wrong database, not that your sessions are gone.
Over HTTP
curl 'localhost:PORT/api/session/prune?olderThan=90&by=updated'GET is the preview and is inert. POST mutates and requires apply: true explicitly:
curl -X POST localhost:PORT/api/session/prune \
-H 'content-type: application/json' \
-d '{"olderThan":90,"action":"archive","apply":true}'2
3
Without it:
session prune mutation requires `apply: true`; nothing was changedThe CLI and HTTP previews emit byte-identical JSON — crates/zuno-cli/src/cmd/session_prune.rs::session_prune_cli_and_http_preview_json_are_byte_identical — so an operator can build a policy against one and audit with the other.