Skip to main content

Storage Subsystem: StorageManager and StorageMonitor

Overview

The Agent has two separate storage components. They share the [storage] configuration section, but they solve different problems and expose different HTTP APIs.

ComponentResponsibilityPrimary HTTP API
StorageManagerDetects and mounts the SD card, derives eMMC-only or dual-storage mode, routes governed data, migrates older data, safely ejects the card, and formats the card/api/storage/status, /api/storage/eject, /api/storage/format
StorageMonitorSamples persistent-storage capacity, classifies pressure levels, runs cleanup, and degrades non-essential writes when space is low/api/storage/monitor/status, /api/storage/cleanup

StorageManager does not replace StorageMonitor. Adding an SD card provides a migration tier for governed data, while StorageMonitor continues to protect the configured persistent root, which defaults to /userdata.

Related documentation:

StorageManager: SD Card and Dual-Storage Routing

The production Agent runtime creates and starts StorageManager unconditionally. Missing, removed, rejected, or unusable card hardware falls back to eMMC-only operation; there is no user preference that forces a storage mode.

Card Lifecycle and Effective Mode

The manager polls for the configured SD block device and debounces insertion and removal. When a card appears, it:

  1. Prefers the first partition, falling back to the whole device when there is no partition.
  2. Runs a best-effort fsck.
  3. Mounts the card read-write at the configured mount point.
  4. Verifies the mount with a probe write.
  5. Attempts to reject a card with less than the configured minimum free space by unmounting it. If a busy filesystem cannot be unmounted, the manager keeps the card mounted and reports it as usable rather than recording a false unmounted state.

The default mount point is /mnt/sdcard, the default block device base name is mmcblk2, and the default minimum free space is 64 MB. A card normally becomes usable only after it passes the mount and write checks; the busy-unmount case above is the exception to the minimum-free-space rejection.

Effective modeJSON valueCondition
eMMC only1No usable card is mounted
Dual storage2A usable card is mounted

A positively identified blank card is automatically formatted as FAT32 once per insertion. Cards with an existing but unsupported, damaged, or otherwise unmountable filesystem are never automatically erased.

The manager mirrors card, format, migration, and effective-mode state to /run/aiden/storage.state for other device services. This state file is separate from the StorageMonitor level file at /run/agent/storage_level.

Data Routing and Migration

New governed data is always written to eMMC. The current production integration uses this routing for audio archives:

OperationNo mounted SD cardMounted SD card
New audio archive/userdata/audio/userdata/audio
Read audio archives/userdata/audioeMMC first, then /mnt/sdcard/aiden/audio
Retention cleanup roots/userdata/audio/mnt/sdcard/aiden/audio

Keeping new writes on eMMC avoids making active recording depend on removable media. When the SD tier is available and eMMC free space falls below the start watermark, the manager migrates older audio archives to the SD card, oldest first. It stops after eMMC reaches the stop watermark, no eligible files remain, the card becomes unavailable, or an eject or format operation cancels the run.

The default migration watermarks are:

  • Start when eMMC free space is below 10%.
  • Stop when eMMC free space reaches 50%.

Only regular files at least 60 seconds old are eligible. Each file is copied to an .aiden-partial path, synced, size-checked, renamed into place, and only then removed from eMMC. Readers prefer the eMMC copy during the brief window where both copies may exist. Failed or exhausted runs use a ten-minute retry cooldown.

Safe Eject and Formatting

Safe eject cancels any active migration, syncs and unmounts the card, and latches the card as ejected. It is not mounted again until it is physically removed and reinserted.

Manual formatting is destructive: it rewrites the whole card with a fresh MBR, one partition, and a new filesystem labeled AIDEN. Supported filesystems are FAT32, ext4, and exFAT. Formatting runs asynchronously. After FormatDisk succeeds, the manager attempts to mount the new filesystem; check card.mounted and effective_mode to confirm that dual-storage mode was restored, because the post-format mount can still fail.

StorageManager HTTP API

These endpoints report and control the SD/eMMC manager. They are distinct from the StorageMonitor endpoints documented later.

All three endpoints return HTTP 405 for the wrong method and HTTP 503 if the runtime has no StorageManager. A normal production runtime always creates the manager, even when no card is present.

Get Manager Status

GET /api/storage/status

Example response:

{
"effective_mode": 2,
"card": {
"present": true,
"mounted": true,
"device": "/dev/mmcblk2p1",
"total_bytes": 68719476736,
"free_bytes": 64424509440
},
"mount_point": "/mnt/sdcard",
"format_job": {
"status": "idle"
},
"migration": {
"status": "idle"
}
}

format_job.status and migration.status are idle, running, success, or failed. Active and completed jobs also report the applicable filesystem, timestamps, detail, error, moved-file count, and moved-byte count.

Safely Eject the Card

POST /api/storage/eject

The successful response is the updated manager status. The endpoint returns HTTP 409 when the card cannot be ejected, including when no card is mounted, a mount attempt or format is active, migration cannot stop promptly, or the filesystem is busy and cannot be unmounted.

Format the Card

POST /api/storage/format
Content-Type: application/json
{
"fs": "fat32",
"confirm": "format-sd-card"
}

fs may be fat32, ext4, or exfat; an empty value defaults to fat32. The exact confirmation token is required because formatting erases the entire card. HTTP 202 means that the asynchronous job was accepted, not that formatting has finished. Poll GET /api/storage/status and inspect format_job for the result. Invalid requests return HTTP 400.

StorageManager Configuration

[storage]
mount_point = "/mnt/sdcard"
device = "mmcblk2"
min_card_free_mb = 64
migrate_start_free_pct = 10
migrate_stop_free_pct = 50

StorageMonitor: Capacity Protection

StorageMonitor protects the Agent from persistent-storage exhaustion. It is responsible for:

  • Sampling the configured storage root.
  • Classifying the final storage level.
  • Running ordered cleanup stages.
  • Resampling after every cleaner.
  • Applying recovery hysteresis.
  • Disabling non-essential writes under pressure.
  • Triggering remediation after ENOSPC or EROFS errors.
  • Exposing status and manual-cleanup HTTP endpoints.
  • Publishing the final level to deployment-side guards.

Notification consumers, Companion App behavior, LED presentation, and other user-facing delivery policies are outside StorageMonitor's scope.

Capabilities

CapabilityStatusNotes
Startup checkAvailableRuns once when the Agent runtime starts
Periodic monitoringAvailableDefaults to a 300-second interval
Tiered classificationAvailableNormal, Warning, Critical, Emergency
Recovery hysteresisAvailablePrevents level flapping near thresholds
Automatic cleanupAvailableLLM HTTP logs, audio archives, session archives
Post-cleanup resamplingAvailableFinal state always comes from statfs
Degraded write controlsAvailableSuspends non-essential persistence
Write-error remediationAvailableHandles ENOSPC and EROFS
Status endpointAvailableGET /api/storage/monitor/status
Cleanup endpointAvailablePOST /api/storage/cleanup
Deployment state fileAvailable/run/agent/storage_level
Agent log integrationAvailableDefault path is /userdata/agent/log/agent.log
Notification event outputAvailablePublishes through the global VoiceNotificationManager
LED and App presentationOut of scopeConsumers can poll the status endpoint

Storage Scope

Managed Root

StorageMonitor manages one configured persistent-storage root. The default is:

/userdata

The default Agent configuration directory is /userdata/agent. Important persistent paths include:

DataDefault path
Agent main log/userdata/agent/log/agent.log
LLM HTTP logs/userdata/agent/log/llm-http-*.log
Session data/userdata/agent/memory
Session archives/userdata/agent/memory/session_archive
Long-term memory/userdata/agent/memory/long_term
Audio archives/userdata/audio
Agent configuration/userdata/agent/agent.toml

The Agent main log has moved from /var/log/agent/agent.log to <CONFIG_DIR>/log/agent.log. With the default configuration directory, the resolved path is /userdata/agent/log/agent.log. The legacy file is not migrated and is not used as a fallback.

Other services may still write logs under /var/log, including audio_service, frame_service, and adb. Those logs are not managed by the Agent StorageMonitor cleaners.

Protected Data

StorageMonitor does not delete:

  • The active session.
  • Long-term memory.
  • Agent or device configuration.
  • OTA files currently in use.
  • Arbitrary files outside configured cleaner directories.

It also does not manage RAM, CPU, temperature, battery pressure, or volatile /tmp usage.

OTA Partition Interaction

OTA uses a dedicated 300 MiB ext4 partition mounted at /userdata/ota. StorageMonitor still samples the /userdata filesystem, so OTA downloads do not reduce the available-byte value used by the 50/10/5 MiB thresholds.

StorageMonitor cleaners do not target OTA paths. The SD-card StorageManager does not route or migrate OTA downloads and has no OTA lock or capacity coupling. Existing cleanup stages and user-facing storage notifications remain necessary for the non-OTA data stored on /userdata. See OTA Dedicated Storage Partition.

Runtime Flow

Startup check ───────┐
Periodic check ──────┤
ENOSPC / EROFS ──────┼→ StorageMonitor.CheckAndRemediate
Manual cleanup ──────┘ │
├→ statfs(root_path)
├→ classify initial level
├→ run eligible cleaners by priority
├→ statfs after every cleaner
├→ apply recovery hysteresis
└→ publish final StorageMonitorStatus
├→ update write capabilities
├→ update storage_level file
├→ serve HTTP status
└→ optionally publish an event

All entry points share the same remediation path. A monitor-wide mutex serializes checks so startup, periodic, write-failure, and manual cleanup requests cannot delete the same files concurrently.

Check Triggers

Startup

The daemon calls StartStorageMonitor after creating the Agent runtime.

StartStorageMonitor:

  1. Runs CheckAndRemediate with the startup reason.
  2. Starts the periodic monitor loop even if the initial check reports an error.
  3. Stops the loop and removes the deployment state file when the runtime closes.

Periodic Monitoring

The default interval is 300 seconds. Periodic checks can run maintenance cleaners while the storage level is Normal. For example, the normal LLM HTTP log stage removes files beyond the default retention window.

Write Errors

Writers pass storage-related errors to StorageMonitor. An immediate asynchronous check is scheduled only for:

  • ENOSPC: no space left on device.
  • EROFS: read-only filesystem.

Repeated failures are coalesced. At most one write-failure remediation request is pending at a time.

Manual Cleanup

POST /api/storage/cleanup uses the same CheckAndRemediate flow with the manual reason.

  • With force=false, Normal, Warning, and Critical cleanup stages are eligible even when the current level is Normal. Emergency-only stages remain gated unless the effective level is Emergency. The flow still stops after a cleaner resamples at Normal.
  • With force=true, selected cleaners ignore ordinary retention thresholds and run their force-clean behavior.
  • Force cleanup still respects cleaner-directory boundaries and protected-data rules.

Storage Levels

Levels are based on available bytes, not percent used.

LevelDefault available-space rangeBehavior
NormalGreater than 50 MBNormal operation and low-risk maintenance
WarningGreater than 10 MB and at most 50 MBWarning-stage cleanup
CriticalGreater than 5 MB and at most 10 MBAggressive cleanup and non-essential write suspension
EmergencyAt most 5 MBEnables configured emergency cleanup stages while the effective level remains Emergency

Threshold comparisons use exact bytes. Exactly 10 MB is Critical, and exactly 5 MB is Emergency.

Recovery Hysteresis

Escalation takes effect immediately. De-escalation requires available space to exceed the previous level threshold plus recovery_hysteresis_mb.

With the default 5 MB hysteresis:

Previous levelSpace required to de-escalate
WarningGreater than 55 MB
CriticalGreater than 15 MB
EmergencyGreater than 10 MB

This prevents repeated transitions when available space oscillates near a threshold.

Final-State Rule

The initial sample only decides whether remediation should begin. StorageMonitor resamples the filesystem after every cleaner.

The final level, unavailable capabilities, deployment state file, and HTTP response are based on the final statfs sample after hysteresis. A cleaner's reported freed-byte count is retained for audit history but never replaces resampling.

Cleanup Pipeline

Cleaner Interface

type StorageCleaner interface {
Name() string
Priority() int
EstimateReclaimable(ctx context.Context) (uint64, error)
Clean(ctx context.Context) (freed uint64, err error)
}

Cleaners run in ascending Priority order. Non-force checks call EstimateReclaimable first and skip a cleaner when its estimate is zero.

Default Stages

The runtime builds cleanup stages from the configured retention arrays.

DataDefault stageMinimum levelProtection
LLM HTTP logs7 daysNormalProtects the active session log
LLM HTTP logs3 daysWarningProtects the active session log
LLM HTTP logs1 dayCriticalProtects the active session log
LLM HTTP logs0 daysEmergencyRemoves matching logs except the active session log
Audio archives30 days, keep up to 10WarningOnly touches the audio archive directory
Audio archives7 days, keep up to 3CriticalOnly touches the audio archive directory
Audio archives0 days, keep noneEmergencyOnly touches the audio archive directory
Session archives30 daysWarningDoes not touch the active session or long-term memory

A retention value of 0 creates an emergency cleanup stage. The default session_archive_retention_days value is [30], so an all-session-archives emergency stage is not enabled unless 0 is explicitly added.

Stage Eligibility and Stop Conditions

After each cleaner, StorageMonitor resamples and recalculates the effective level.

  • Non-force remediation stops after recovery to Normal.
  • Automatic startup, periodic, and write-error remediation skip a cleaner when its minimum level is above the recalculated effective level.
  • Non-force manual cleanup bypasses the Normal, Warning, and Critical minimum-level gates, but retains the Emergency gate.
  • A cleanup that raises the level from Emergency to Critical prevents later Emergency-only stages from running.
  • Force cleanup continues through all selected cleaners.
  • A cleaner failure is recorded and does not prevent later cleaners from running.

Cleanup Retry Interval

Automatic cleanup has a default minimum retry interval of 60 seconds to avoid repeatedly scanning directories under sustained pressure.

The retry interval is bypassed for:

  • force=true.
  • Manual cleanup.
  • ENOSPC or EROFS remediation.

Degraded Writes

Warning triggers cleanup but does not disable capabilities. Critical and Emergency compute unavailable capabilities from the degraded-mode configuration.

CapabilityDefault behavior
llm_http_logStops new raw LLM HTTP logs
audio_archiveStops new audio archives
session_archiveStops new session archives
session_persistenceKeeps the in-memory session and marks persistence as pending

Capabilities are recalculated after every successful check and recover automatically when storage returns to a safe level.

Agent Main Log

The Agent main log is not completely disabled through AllowWrite. Instead, the S53agent deployment script reads /run/agent/storage_level.

At Critical or Emergency, S53agent trims <CONFIG_DIR>/log/agent.log to storage.degraded_mode.max_agent_log_mb, which defaults to 1 MB. It preserves the newest content so storage pressure does not remove the most useful diagnostics.

Status Model

StorageMonitorStatus is the immutable snapshot produced by a successful check.

FieldMeaning
PathManaged root
TotalBytes, UsedBytes, AvailableBytesFinal statfs values
PercentUsedUsed-space percentage
LevelFinal storage level
UnavailableCapabilitiesWrites currently suspended
RevisionIncrements after every successful check
LastCleanupAtTime of the most recent cleaner execution
LastCleanupFreedBytesBytes reported by the latest cleanup flow
CleanupHistoryMost recent cleaner results, capped at 50 entries
CheckedAtFinal sample time

Status returns copies of slice fields so callers cannot mutate the monitor's internal state.

Deployment State File

Every successful check atomically updates:

/run/agent/storage_level

The file contains normal, warning, critical, or emergency. Deployment-side guards use it to apply the Agent main-log limit.

Disabling or stopping StorageMonitor removes both the state file and its temporary file so deployment scripts do not consume stale state.

StorageMonitor HTTP API

Get Status

GET /api/storage/monitor/status

Example response:

{
"path": "/userdata",
"total_mb": 3072,
"used_mb": 3027,
"available_mb": 45,
"percent_used": 98.5,
"alert_level": "warning",
"degraded_mode": false,
"unavailable_capabilities": [],
"status_revision": 12,
"last_cleanup": "2026-07-22T08:30:00Z",
"last_cleanup_freed_mb": 32,
"cleanup_history": [
{
"timestamp": "2026-07-22T08:30:00Z",
"cleaner": "llm_http_log_7d",
"freed_mb": 32,
"status": "success"
}
]
}

The endpoint provides a consistent snapshot suitable for polling. Real-time push and user-interface presentation are outside this module.

Run Cleanup

POST /api/storage/cleanup
Content-Type: application/json

Example request:

{
"force": false,
"targets": ["llm_http_log", "audio_archive"]
}

targets can be empty or contain these cleanup categories:

  • llm_http_log
  • audio_archive
  • session_archive

Full cleaner names are also accepted. Unknown categories, abbreviations, unknown JSON fields, trailing JSON documents, and request bodies larger than 64 KB return HTTP 400.

Example response:

{
"success": true,
"freed_mb": 45,
"final_alert_level": "normal",
"available_mb": 90,
"cleaners_run": [
{
"name": "llm_http_log_7d",
"freed_mb": 45,
"status": "success"
}
]
}

freed_mb reports only cleaners executed by the current POST request. It does not repeat the previous cleanup's freed-space value.

StorageMonitor Configuration

Default configuration:

[storage]
monitor_enabled = true
root_path = "/userdata"
check_interval_seconds = 300
warning_threshold_mb = 50
critical_threshold_mb = 10
emergency_threshold_mb = 5
recovery_hysteresis_mb = 5

[storage.degraded_mode]
disable_llm_http_log = true
disable_audio_archive = true
disable_session_archive = true
max_agent_log_mb = 1

[storage.cleanup]
enabled = true
llm_http_log_retention_days = [7, 3, 1, 0]
audio_archive_retention_days = [30, 7, 0]
session_archive_retention_days = [30]
cleanup_retry_interval_seconds = 60

Validation rules:

  • root_path is required when storage.monitor_enabled is true.
  • check_interval_seconds must be greater than zero.
  • Thresholds must satisfy emergency < critical < warning.
  • max_agent_log_mb must be greater than zero.
  • cleanup_retry_interval_seconds cannot be negative.
  • Retention values cannot be negative.

Optional Event Output

StorageMonitor exposes a VoiceNotificationSink interface for final storage-state events. The Agent runtime injects the global VoiceNotificationManager as the production consumer.

  • Non-Normal states publish active.
  • Severity changes update the stable storage:device condition.
  • Recovery to Normal publishes resolved.
  • Periodic checks can refresh an equivalent active event.
  • Publish failures do not roll back cleanup or StorageMonitorStatus.

Queueing, presentation, playback, deduplication, and delivery policy remain owned by VoiceNotificationManager.

Testing

The current test suite covers:

  • Exact-byte threshold boundaries.
  • Recovery hysteresis.
  • Post-cleanup resampling and final-state publication.
  • Cleanup retry intervals.
  • Manual and force cleanup behavior.
  • Runtime cleaner order and minimum levels.
  • Capability degradation and recovery.
  • ENOSPC and EROFS remediation.
  • Deployment state-file lifecycle.
  • Event activation, severity updates, resolution, and retry.
  • GET status consistency and cleanup history.
  • POST cleanup target and JSON validation.
  • Agent main-log capping under Critical and Emergency.

Common checks:

cd src/agent
go test ./internal/agent

cd ../..
./scripts/test_agent_log_cap.sh

Current Limitations

  • Only one root_path is managed.
  • Logs for other services under /var/log are not cleaned.
  • The status API supports polling but not real-time push.
  • LED, Companion App, and complete Web UI presentation are outside this module.