Skip to content
Pilotoo
v0.1

Storage and restarts

The local backend keeps everything on the user’s machine: one SQLite file for conversations and runs, and plain files for configuration and uploads. This page says where each thing lives, so you know what to back up, what a user may edit by hand, and what a restart does to a run.

One SQLite file, <dataDir>/local.db, holds nearly everything:

Data Tables
LangGraph checkpoints (the stock SqliteSaver of @langchain/langgraph-checkpoint-sqlite) checkpoints, writes
Conversation catalog conversations
Run events run_events
Background tasks background_tasks
Token usage, served at /usage as on the gateway usage_calls
Schedules and their runs, served at /schedules schedules, schedule_runs

The server keeps the same data in PostgreSQL; see Storage for every store and its port.

Deleting a conversation removes its row, its checkpoint rows, its schedule run row and the conversations its background tasks run on, in one transaction.

Agents, profiles, providers and skills are files under configDir. Uploaded images are files under <dataDir>/attachments. Fork, edit-and-regenerate and interrupts behave as on the gateway.

createFileAgentDefinitionStore(dir) (@pilotoo/agent-runtime) is a source and a store in one. It reads <dir>/<id>.yaml (or .yml) and writes the same format, so any host with a folder of agents can offer the editor by passing it as both agentDefinitionSource and agentDefinitionStore.

The local module does exactly that over <configDir>/agents, and sets AGENT_MANAGER_ROLES=owner so the fixed local identity manages agents. It also turns on every config tool over the same folder (a host’s configTools replaces them) and sets SCHEDULE_ROLES=owner so the local owner schedules runs.

  • The file is the truth. The version is the file’s modification time, so a hand edit and an editor save bump it alike, and nothing needs a restart.
  • No system agents here. The store reports its agents with source: 'database' (the wire word for an editable record), so the editor treats every file as the user’s own, shipped ones included.
  • Writes go to a temporary file in the folder and are renamed over the target. Writes of one process are serialised. Deleting or drafting the last active agent is LastActiveAgentError (409). Another process writing the folder at the same moment is not covered.
  • Ids are file names: [A-Za-z0-9][A-Za-z0-9._-]*, at most 200 characters, no ... Create refuses anything else (InvalidAgentFileNameError); update and delete answer 404.

createFileAttachmentStore(dir) (@pilotoo/agent-runtime, beside createS3AttachmentStore) is the AttachmentStore over a folder. The local module passes it over <dataDir>/attachments, so uploads work with no bucket. A host’s own attachmentStore, null included, replaces it. The contract, limits and HTTP answers are the same as on the server (Attachments).

  • One file per attachment, <id>.att: a JSON line (owner, display name, type) and then the bytes. A single rename publishes both, so no file is ever served without an owner. Uploads write a temp file in the folder (mode 600), fsync, then rename; a failure removes the temp file. The folder (mode 700) is created on the first upload.
  • Ids are lowercase UUIDs. Any other id answers as unknown before the disk is touched, so no id can name a path. Another owner’s id answers like a missing one. An error other than “missing” (permissions, a directory in the way) is raised, not hidden.
  • Nothing expires. Deleting a conversation does not delete its images.

Conversations and checkpoints survive a restart.

  • A run in flight when the process stopped is not resumed. The next reader of its thread gets its replay, closed by exactly one RUN_ERROR with code RUN_INTERRUPTED, which a UI should render as an interrupted turn.
  • A run paused on ask_human or a client tool stays pending and resumes after the restart.
  • A background task that was running is marked interrupted, and its parent agent is told in a turn of its own.
  • The protocol has no “last seen sequence”, so a reader receives the whole replay and finds what it already showed as its prefix.