Skip to content

AI Advisor Audit Trail & Privacy

The advisor uses two stores with opposite properties, and a privacy model designed so that the only outbound traffic is the model call itself.


🗄️ Persistence: Memory vs. Audit Trail

Section titled “🗄️ Persistence: Memory vs. Audit Trail”
ai-advisor.db (conversation memory) ai_advisor_runs (audit trail)
Engine libsql through @mastra/libsql, created by createAdvisorMemory SQLite ledger (kryptofolio_ledger.db), STRICT table
Content Full conversation, including every figure stated or returned by a tool Provenance only, never content
Lifetime Disposable; deleting the file erases conversations only Durable; unaffected by a wipe
Written by Mastra Memory SqliteAdvisorRunLogAdapter via IAdvisorRunLogPort

Migration 009_ai_advisor_runs.sql created ai_advisor_runs; 010_nullable_advisor_execution_profile.sql made execution_profile nullable. See SQLite Transactional Ledger for the surrounding schema conventions.

CREATE TABLE ai_advisor_runs (
id TEXT PRIMARY KEY,
thread_id TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT,
outcome TEXT NOT NULL CHECK (outcome IN ('completed','refused','failed','aborted')),
provider_id TEXT,
model_id TEXT,
tools_called TEXT NOT NULL DEFAULT '[]',
input_tokens INTEGER,
output_tokens INTEGER,
failure_code TEXT,
execution_profile TEXT CHECK (execution_profile IS NULL OR execution_profile IN ('local','metered','mixed')),
steps_used INTEGER,
max_steps INTEGER
) STRICT;
Column Meaning
id The run id; the write is an upsert on it, so a completion and a cancellation cleanup can never double-write.
thread_id The conversation thread.
started_at, finished_at ISO timestamps; finished_at is NULL for an aborted run.
outcome completed, refused, failed, or aborted.
provider_id, model_id The entry that answered, or the last entry attempted; NULL when no entry was tried.
tools_called JSON array of tool names, appended when a tool result or error arrives.
input_tokens, output_tokens Usage reported on finish; NULL for aborted runs.
failure_code Set for failed runs only; stays ALL_PROVIDERS_FAILED and does not record the cause kind.
execution_profile local, metered, mixed, or NULL.
steps_used, max_steps Steps consumed (counted from step-finish chunks) against the ceiling; NULL when unknown.

Rules that follow from the code:

  • The reason of a refused run is deliberately never written.
  • A failure before any chain entry was tried (NO_MODEL_AVAILABLE, VAULT_LOCKED) is a pre-run receipt: profile and step columns are NULL, not guessed.
  • An aborted run is written from the use case’s finally with only run id, thread id, start time, and an empty tool list, so it has no provider, model, profile, tokens, or step counts.
  • stepsUsed and maxSteps for a profiled receipt are required together with executionProfile; the type makes “profile without steps” unrepresentable.

[!WARNING] Migration 010 rebuilds the table with DROP TABLE IF EXISTS ai_advisor_runs (SQLite cannot drop NOT NULL via ALTER) and states a clean-slate assumption: the project had no production deployment and the table held no rows worth preserving. Applying it to a database with real audit rows discards them.

  • Location: ai-advisor.db in the data root, overridable with ADVISOR_DB_PATH (a relative value is anchored to the data root, :memory: is honoured). KRYPTOFOLIO_DATA_DIR moves the whole data root.
  • Recall: each turn replays the last lastMessages messages of the thread (resource: 'local'). Semantic recall, working memory, and observational memory are all off, so there is no embedder and no vector store. Tool-call and tool-result pairs from earlier turns are filtered out (ToolCallFilter with preserveModelOutput: true): the model keeps its own earlier prose but must re-query a tool rather than trust a stale figure.
  • Wiping: stop the backend and delete ai-advisor.db plus any -wal, -shm, or -journal files beside it. The file is recreated on the next run. Nothing else is erased.
  • Lifecycle: the file is opened lazily on the first advisor request, never at import.

Property Mechanism
Read-only No write tool; structural test rejects insert/update/delete in tool files.
No secrets in the model path Keys are decrypted per request, held only in the resolved chain, and redacted from logged error messages.
Model input is bounded Strict tool schemas; no account id or internal identifier is ever a model input; symbol regex.
Model output is untrusted Markdown with raw HTML, images, and non-app links disabled.
Content stays local at rest Conversation content only in ai-advisor.db; the audit table holds none.
No telemetry No Mastra instance or exporter is registered; @mastra/observability is not installed (noTelemetry.spec.ts).
Local vs metered is not user-labelled Derived from provider and model id (see Providers & Models).
Error detail The wire carries a closed cause.kind, never provider text, URLs, headers, keys, or bodies.
No generic agent surface Exactly six routes; no Mastra server adapter mounted.

[!WARNING] When the answering model is cloud, the question and the figures returned by tools are sent to that provider. The chat panel shows a cloud badge for exactly this reason. Use an all-local chain if the data must not leave the machine. Treat ai-advisor.db as sensitive as the ledger.

The only outbound traffic is the model call itself: to a hosted provider for cloud entries, or to OLLAMA_BASE_URL for ollama. Provider keys live in the Local Encrypted Vault, never in environment variables.