> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# State and storage

> Where a plugin keeps state: in-memory and per-plugin disk. Paths, atomicity, cleanup on uninstall.

An agent plugin has two durable places to keep state today: process
memory and its per-plugin data directory. Pick the right one for the
data; do not pile everything into one.

## The two layers

| Layer           | Where                         | Survives                                       | Use for                                              |
| --------------- | ----------------------------- | ---------------------------------------------- | ---------------------------------------------------- |
| Ephemeral       | RAM, in your plugin process   | Until next restart                             | Counters, caches, in-flight subscriptions.           |
| Per-plugin disk | `/var/ados/plugin-data/<id>/` | Restarts and upgrades. Persists across remove. | Local config, calibration, log files, cached models. |

## Layer 1: ephemeral

Just normal Python module state. The supervisor runs one process per
plugin, so a `dict` or a `list` is your ephemeral store.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class MyPlugin:
    def __init__(self) -> None:
        self._last_voltage: float | None = None
```

This is the fastest layer and the least durable. Use it for anything you
can recompute on restart.

## Layer 2: per-plugin disk

Every plugin gets a writable data directory. The runner hands the path
to your plugin on the context:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def on_start(self, ctx) -> None:
    data_dir = ctx.data_dir          # pathlib.Path, /var/ados/plugin-data/<id>/
    sample_log = data_dir / "samples.jsonl"
```

The context also carries `ctx.config_dir`
(`/var/ados/plugin-data/<id>/config/`) and `ctx.temp_dir`
(`/run/ados/plugins/<id>`). When the plugin is bound to a specific drone,
`ctx.data_dir` is scoped per drone
(`/var/ados/plugin-data/<id>/drones/<agent-id>/`), while `ctx.config_dir`
stays shared and `ctx.agent_id` carries the drone's id.

The systemd unit grants write access only to `/var/ados/plugin-data`,
`/var/log/ados/plugins`, and `/run/ados/plugins`. Writes elsewhere fail
with `EROFS`.

### Atomicity

There is no transaction primitive. For a safe single-file write, write to
a `<name>.tmp`, fsync, and rename over the target. For anything needing
ACID, use a single JSON file you rewrite atomically or SQLite (`sqlite3`
is in the Python standard library).

There is only one plugin process, so the only concurrency is your own
asyncio tasks. Add a lock if you fan out writes.

### Format guidance

* Configuration: JSON or YAML, typed keys.
* Time-series: append-only `.jsonl` (one JSON object per line).
* Binary: plain files. Many small files beat one large blob for the page
  cache.
* Database: SQLite is fine for non-trivial state.

## Configuration

The agent context exposes config two ways. `ctx.config` is the static
config dict the host delivered at start (read from
`/var/ados/plugin-data/<id>/config.yaml`, or
`config/<agent-id>.yaml` when the plugin is bound to a drone).
`ctx.config_kv` is the live key-value facade. Its read order is drone
scope when bound, then global, then your default:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Live read and write (async).
value = await ctx.config_kv.get("threshold_v", default=3.5)
await ctx.config_kv.set("threshold_v", 3.4, scope="drone")

# Synchronous read of the static dict the host delivered at start.
limit = ctx.config_kv.static("threshold_v", 3.5)
```

`set` defaults to `scope="drone"`; pass `scope="global"` to write the
fleet-wide value. On the GCS half, the host pushes a `config.changed`
event the plugin subscribes to with `ctx.config.onChange`.

### Per-drone plugins

Set `per_drone_config: true` in the `agent:` block of `manifest.yaml`
(schema v2) and the supervisor runs one process instance per connected
drone, each with its own `ctx.agent_id` and its own scoped `ctx.data_dir`.
Read each drone's live settings through `ctx.config_kv` and fall back to
the static dict for unset keys:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# manifest.yaml (excerpt)
agent:
  runtime: python
  entrypoint: "follow_me:FollowMePlugin"
  isolation: subprocess
  per_drone_config: true
```

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def on_start(self, ctx) -> None:
    # ctx.agent_id is the bound drone; ctx.data_dir is scoped to it.
    live = {}
    for key in ("follow_distance_m", "follow_height_m"):
        value = await ctx.config_kv.get(key, None)
        if value is not None:
            live[key] = value
    static = dict(getattr(ctx, "config", {}) or {})
    self._config = {**static, **live}
```

The shipped Follow-Me extension uses this pattern: live per-drone keys
override the static defaults each control loop. A write with the default
`scope="drone"` updates only the bound drone; `scope="global"` updates
the fleet-wide default the other instances fall back to.

## Cloud-visible state

The GCS capability catalog includes `cloud.read` and `cloud.write` for
plugins that need fleet-visible state. A dedicated cloud-storage facade
is not yet part of the plugin SDK, so cloud-backed plugin state is a
forward-looking surface rather than a current API. For now, keep durable
plugin state in the per-plugin data directory and surface readings to the
operator through telemetry and notifications.

## Cleanup on remove

`ados plugin remove <id>` does the following:

1. Disable the plugin if it is running or enabled. This stops the unit.
2. Delete the unit file (and any declared extra-service units). Reload
   the daemon.
3. Remove the install directory `/var/ados/plugins/<id>/`.
4. Delete the plugin's log file under `/var/log/ados/plugins/`, unless
   `--keep-data` is passed.
5. Drop the install entry from the state file.

`--keep-data` preserves the plugin's log file. The data directory under
`/var/ados/plugin-data/<id>/` (including any per-drone subdirectories and
`config/<agent-id>.yaml` files) is never deleted by remove, with or
without the flag, so reinstalling the same plugin id can pick up its
prior data. If you want a clean slate, clear the data directory yourself.

## Migration between versions

Plugins own their own data migrations. There is no SDK migration helper,
so carry a small version marker in your data directory and migrate
forward on start:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def on_start(self, ctx) -> None:
    marker = ctx.data_dir / ".data_version"
    version = int(marker.read_text()) if marker.exists() else 1
    if version < 2:
        await self._migrate_v1_to_v2(ctx)
        marker.write_text("2")
```

Handle skipped versions; an upgrade can jump more than one version at
once. Forward migration is the contract; reading new data with an older
plugin is best-effort.

## What not to put on disk

* Secrets you did not generate yourself. The host manages pairing keys,
  MAVLink session keys, and signer keys.
* Large media. Anything over a gigabyte belongs in a recording, not the
  data dir.
* Operator PII without consent. The data dir is not encrypted at rest by
  default.

## See also

* [Manifest reference](/developers/manifest)
* [Permissions](/developers/permissions)
* [Performance and budgets](/developers/performance-and-budgets)
