Storage and Retention¶
vornik produces two kinds of data that grow over time: artifacts (files agents read and write) and operational history (task records, execution logs, cost rows). This guide covers where artifacts live, how to keep history from growing without bound, and how to back up and restore a deployment.
All settings here live in your daemon config
(~/.config/vornik/vornik.yaml). See
Configuration reference for the full key
list.
Artifact storage¶
By default vornik stores artifacts on the local filesystem. You can instead point it at an S3-compatible object store — AWS S3, MinIO, or Ceph RGW — which is the better choice for durability and for deployments that may scale beyond a single host.
Filesystem (default)¶
storage:
artifacts_path: /var/lib/vornik/artifacts
# backend: "filesystem" # default; may be omitted
AWS S3¶
storage:
artifacts_path: /var/lib/vornik/artifacts # still used for staging
backend: "s3"
s3:
region: "us-east-1"
bucket: "vornik-prod-artifacts"
prefix: "vornik/prod"
# Leave endpoint and credentials empty to use the AWS SDK's
# default resolver and credential chain (IAM role, ~/.aws, env).
For production, prefer an IAM role over static credentials. Run the daemon
under an instance profile that grants only the permissions it needs:
s3:PutObject, s3:GetObject, s3:DeleteObject, and s3:ListBucket on your
bucket and its contents.
MinIO (local development)¶
storage:
artifacts_path: /var/lib/vornik/artifacts
backend: "s3"
s3:
endpoint: "http://localhost:9000"
region: "us-east-1" # MinIO accepts any non-empty value
bucket: "vornik-dev"
access_key_id: "minioadmin"
secret_access_key: "minioadmin"
use_path_style: true # required for MinIO
force_ssl: false # only safe for localhost development
Credentials via environment¶
You can leave the credential fields blank in YAML and supply them via environment variables instead — the recommended pattern for production:
export VORNIK_STORAGE_S3_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export VORNIK_STORAGE_S3_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Create the bucket first, and mind TLS
vornik does not create buckets for you. The bucket must exist before the
daemon starts, or boot fails with a clear bucket not found error.
Versioning, object lock, and lifecycle policies are yours to configure and
are recommended for production.
force_ssl defaults to true and refuses plain-HTTP endpoints. Only set
it to false for local MinIO development.
Retention¶
The retention sweeper prunes old operational data on a schedule. It is off by default — nothing is deleted until you enable it. Each category of data has its own age threshold in days.
retention:
enabled: true # default: false - opt in
interval: "6h"
task_llm_usage_days: 90 # cost history
tool_audit_days: 180 # tool-call audit (never below 180, see below)
tasks_days: 60 # finished tasks
executions_days: 60 # finished executions
artifacts_days: 60 # artifact records and their files
task_messages_days: 30 # chat history (0 = keep with the parent task)
memory_chunks_days: 0 # long-term memory (0 = keep forever)
What the defaults mean:
-
tool_audit_daysnever resolves below 180 days (six months), whatever the daemon or a project sets. That is the minimum the EU AI Act (Art 26(6)) asks deployers of high-risk AI systems to keep these logs. Raise the floor withtool_audit_floor_days. To keep them for less — because another law requires a shorter period, or because yours is not a high-risk deployment — settool_audit_floor_override: trueand say which intool_audit_floor_override_basis; an override without a basis is ignored. The record of a connected assistant's refused calls (shown on its page under Admin → Assistants) is kept for the same time. -
memory_chunks_days: 0keeps long-term memory forever, which is the right posture for most deployments. Long-term memory is product data, not throwaway logs — set a finite value only if storage pressure forces it. task_messages_days: 0means chat history is pruned only when its parent task is pruned. Set it explicitly to trim chat faster than tasks.
Copies of conversations: diagnostic caches and upgrade dumps¶
Some stores hold copies of conversation text rather than the original.
An erasure request does not reach them directly, so each one has a limit on
how long it can keep a copy. These limits are always on while the sweeper is
enabled; setting a key to 0 means "use the default", not "keep forever".
retention:
blackbox_trace_cache_days: 1 # assembled task traces (default 1 day)
project_gists_days: 7 # project summaries and their narrative (default 7)
upgrade_dump_keep: 2 # database dumps left by vornik-update.sh
upgrade_dump_grace_days: 14 # kept this long after their upgrade succeeded
upgrade_dump_dir: "" # where the backups are; empty = your home directory
- Task traces (the black box's trace view) are a cache: a trace older than a day is rebuilt from the original records when you open it, so nothing is lost by removing it.
- Project summaries are rebuilt every few minutes while a project's memory loop runs. A summary that has not been refreshed for a week, and a narrative that has not been rewritten for a week, are removed. A project page then shows its "no summary yet" state until the loop runs again.
- Upgrade dumps. Each run of
vornik-update.shsaves a full copy of the database in<home>/vornik-upgrade-backup-<date>/. The newest two are kept at most, and each is removed 14 days after the upgrade that wrote it succeeded. The newest dump of an upgrade that did not succeed is kept until a later upgrade runs: it is your way back. Only the dump file is removed; the previous binaries and the run's record stay, and the record notes when and why the dump went.vornik-update.shapplies this at the end of every successful upgrade, and the sweeper applies it between upgrades.
vornikctl retention upgrade-dumps # preview
vornikctl retention upgrade-dumps --apply # delete the dumps past the policy
vornikctl doctor reports dumps past the policy as upgrade_dumps.
With the sweeper off
With retention.enabled: false (the default) the task-trace cache and the
project summaries keep their copies indefinitely. Upgrade dumps are still
pruned, but only when an upgrade runs.
Per-project overrides¶
Any of these thresholds can be set on an individual project's config to override the daemon default. A common pattern is keeping cost data longer for billing while trimming chat sooner:
# In a project config:
retention:
task_llm_usage_days: 365
task_messages_days: 14
The minimum effective retention is one day. Setting a field to 0 means
"inherit the default," not "delete immediately."
Recording a project's model exchanges¶
A project can opt in to keeping every model request and response of its agent steps, redacted, for replay and post-mortem:
# projects/<id>.yaml
recording:
llm_exchanges: true # default false
Off by default because it costs storage: one row per model call carrying the
whole conversation at that point, so a 30-iteration step stores roughly 30
times its final context. The rows live exactly as long as their execution and
are swept with it; vornikctl retention preview reports how many are waiting
under llm_exchanges. Read them back with
vornikctl execution exchanges <executionId> <stepId>, or export a replayable
recording with --export. The switch is read at each model call, so a reload
takes effect at a running step's next request.
Reading what a step was handed and what it handed back¶
Every agent step keeps, beside the prompt its model was first told, the two
files at the container boundary: the task.json the daemon wrote for the
container and the result.json the container wrote back. Both are stored
after secret redaction (a [REDACTED:type] marker stands where a credential
was, so do not read the stored input as exactly what the container saw where
one appears), content-addressed, and pruned with the step outcome that
references them — there is no separate retention setting. A file over 4 MiB is
not stored; vornik_executor_step_io_skipped_total counts it.
vornikctl execution input <executionId> <stepId> [--export task.json]
vornikctl execution result <executionId> <stepId> [--export result.json]
Bare JSON on stdout, so both pipe into jq; --export writes the file with
mode 0600. A step run before the daemon kept these files answers "not
recorded", and one whose file has since been pruned answers "gone".
Previewing and applying¶
vornikctl retention runs in preview mode by default — it shows what would
be pruned without deleting anything. Add --apply to actually delete.
# Preview across every project (no deletes)
vornikctl retention
# Preview a single project
vornikctl retention --project personal-assistant
# Actually prune
vornikctl retention --project personal-assistant --apply
# Machine-readable output
vornikctl retention --json
Artifact pruning is permanent
Pruning artifacts removes both the database record and the underlying file (or object, on S3). It cannot be undone from within vornik — recovery means restoring from a backup. Run a preview first.
On SQLite¶
Retention and vornikctl retention work on both database drivers. On
SQLite every table the SQLite schema has is pruned on the same windows as on
PostgreSQL. Three differences:
- Pruning project memory (
memory_chunks_days, per-class TTLs) and the phone-approved idle-memory flow need PostgreSQL. Project memory itself is PostgreSQL-only, so there is normally nothing to prune; if old memory ever exists on SQLite, the sweep reports it as kept rather than deleting it. - Tables SQLite does not have (the response and embedding caches, the memory-eviction audit) are skipped.
- SQLite runs without foreign-key enforcement, so since 2026.10.6 vornik applies the PostgreSQL cascade itself: deleting or pruning a task, execution or artifact also deletes or clears its child rows. Child rows left behind by deletes before 2026.10.6 are not cleaned up.
- Since 2026.10.6, wiping an archived project (the grace-period wipe or delete-now) works on SQLite too, and a wipe that fails says so instead of reporting success.
Backup and restore¶
vornikctl backup captures a complete snapshot you can restore later.
# Auto-named archive in the current directory
vornikctl backup
# Explicit output path
vornikctl backup --out /backups/vornik-2026-05-19.tar.gz
The archive contains a full database dump, every artifact file (for the filesystem backend), and all project workspaces.
S3 backends are backed up separately
If you use the S3 artifact backend, vornikctl backup does not copy
your S3 objects — back the bucket up with native S3 tooling (for example,
aws s3 sync). The database dump and workspaces are still captured.
To restore, stop the daemon first, then:
vornikctl restore --from /backups/vornik-2026-05-19.tar.gz --force
The restore runs as a single transaction: it either imports completely or rolls back cleanly, never leaving a half-applied database.
As a safety check, restore refuses to overwrite a database that already contains projects or tasks, so you don't accidentally clobber a live system:
error: target database is not empty (1 project, 47 tasks)
(pass --allow-non-empty to override)
If you genuinely intend to re-apply into a populated database, add
--allow-non-empty.
Stop the daemon before restoring
Restoring into a database the daemon is actively using has undefined
results. The --force flag is a contract that you have stopped the
daemon, not a way to skip doing so.
Context budget and tier¶
Separate from disk storage, each chat session has a token budget — the model's context window. As a conversation fills that budget, vornik progressively tightens how many tools it exposes per turn, recovering prompt space exactly when it matters. This keeps long conversations working instead of failing once they approach the window limit.
This behavior only turns on when you tell vornik how large the context window is:
chat:
# Set to your model's context window in tokens to enable tier-aware
# tool management. 0 (the default) leaves it off.
context_size: 200000
With this set, chat replies carry an X-Vornik-Context-Tier header
(peak, good, degrading, or poor) that the UI uses to colour the
session's context badge so you can see at a glance how much headroom remains.
If you want guaranteed full-tool turns, either raise the budget or shorten the
conversation with /summarize.
Where to go next¶
- Cost and caching — keep LLM spend predictable.
- Memory and RAG — what long-term memory stores.
- Configuration reference — every key and its default.
Approval before deleting unused project memory¶
For a personal knowledge project such as companion-personal, add this block to
its project YAML in the configuration tree your daemon reads:
retention:
memory_require_approval: true
memory_idle_days: 365 # optional; 365 is the default in approval mode
Deploy the binary supporting these keys before adding them, then reload the
configuration with vornikctl config reload. This repository change does not
modify a running project's settings. Request production uses the existing
daemon retention sweeper, which must be enabled (retention.enabled: true in
the daemon config; default interval six hours). Pair an approver phone using
the existing approval-device setup; pending requests
appear in the inbox and on the paired phone's approval page. The operator alert
channel sends a link when configured, without the source preview.
In this mode, automatic class-TTL and creation-age chunk deletion are disabled
for the project. A final returned recall hit resets its idle clock, including
memory used through context assembly. A never-used or historical chunk with
no recorded use starts from its creation time. No history of old recalls is
invented. The idle window is the greater of memory_idle_days and the chunk's
stored original TTL duration, so an explicit longer ttl_days is preserved.
Chunks with no expiry remain indefinite and do not receive deletion requests.
After the idle window, the daemon asks on the paired phone before deleting a specific chunk. The phone shows the project, source, bounded content preview and last-use date. Approve deletes that chunk and cleans its dependent links and graph provenance. Reject or leave unanswered to keep it; it remains searchable, and the daemon can ask again after 30 days. At most 20 requests per project are proposed each sweep. Only the paired-device principal can decide; API and companion keys cannot approve. A later recall, content replacement, TTL extension or retention-policy change invalidates the old deletion request.
vornikctl retention --apply also respects this protection; it does not file
phone requests. With no available phone approval service, memory is retained.
Explicit erasure and manual hard eviction remain separate actions. Disabling
approval mode restores legacy TTL and age-based retention, so old expired
chunks can become hidden or be removed by the next sweep. Back up the store
before choosing that policy change.