Version: 0.10.0
Last Updated: April 2026
Developer: Kartik (NullVoider)
📖 Documentation in progress — Extended documentation covering in-depth deployment guides, architecture deep-dives, and operational runbooks for research teams, AI labs, and enterprise users is currently being written and will be published separately. This README serves as the primary reference in the meantime.
Additionally, a research document covering the memory-grounded CUA training paradigm enabled by Memory Archive — including pre-training, SFT, post-training RL, and inference-time retrieval — is being revised to reflect recent changes to the tooling and will be published alongside the extended documentation.
Memory Archive is a production-grade, open-source Rust + Python monorepo that serves as the observational backbone for Computer Use Agent (CUA) training data collection. It monitors a running duo or trio of CUA tools — The-Eyes (screen capture) and Control-Center (actuation agent) — records every action with its full visual context (three screenshots per captured step), routes each step to either a human annotator or a Vision Language Model (VLM) for natural-language reasoning, and compiles the result into a structured memory.md file: a replayable, step-by-step document that a CUA can follow to autonomously repeat the task. Memory Archive is read-only by design: it never sends commands to the OS environment, never modifies the tools it monitors, and treats data integrity as its first priority — every write is atomic, every session is either complete or explicitly marked incomplete, and no state is ever batched or deferred.
- Overview
- Key and Critical Features
- Capabilities Summary
- Technical Specification
- Quick Start Guide
- Security Features
- Usage Modes
- CLI Command Reference
- TUI Usage and Features
- Configuration Reference
- Session Management
- Metrics, Monitoring, and Observability
- Advanced Features
Memory Archive is a training data collection engine purpose-built for the CUA (Computer Use Agent) development and inference lifecycle. It sits alongside running CUA tools — specifically The-Eyes and Control-Center — and silently records every action event the actuation agent performs, paired with the precise visual state of the screen at the moment of that action. After capture, each recorded step is annotated with natural-language reasoning, either by a human reviewer using the built-in TUI or by a VLM in fully automated mode. The final output is memory.md: a structured, step-annotated document that a CUA can ingest and replay to autonomously repeat the captured task.
| Audience | Use Case |
|---|---|
| Individual developers | Capture personal workflow memories on a single machine using local storage; annotate manually; build a private library of CUA-replayable task recordings |
| Research teams | Shared ma-core with cloud-primary storage; multiple annotators claiming sessions from a queue via the remote TUI; consistent provenance tracking across all recordings |
| AI labs | Fully automated mode with multi-provider VLM routing; per-session circuit breakers; signed pricing registry; Prometheus observability; Kafka-backed crash recovery; 100,000+ concurrent sessions per ma-core instance |
| Enterprises | Multi-tenant storage routing, per-annotator access control, audit trails, Ed25519-signed cost manifests, zero-credential annotator machines, TLS 1.3 IPC with fingerprint pinning |
| CUA platforms | Programmatic session registration via IPC; streaming StepReadyForReasoning push events; fully automated pipeline from capture to finalized memory.md without human intervention |
| It is NOT… | Why this matters |
|---|---|
| An actuation tool | It never sends commands to the OS environment; it only reads from Control-Center's stream |
| An orchestration layer | It does not schedule, plan, or coordinate CUA actions |
| A reasoning engine | It calls external VLM APIs; it does not perform its own inference |
| A storage backend | It writes to local disk or delegates to AWS S3, Azure, or GCP — it does not implement its own storage |
| An agent that lives inside the OS | It runs alongside the environment, not inside it |
| A file server | It proxies files to authorized remote annotators; it is not a general-purpose file server |
| A secrets manager | Cloud credentials and API keys are resolved from environment variables, never stored in config.json or transmitted over IPC |
| A certificate authority | It generates a self-signed CA and server cert for its own TLS IPC transport, not for general PKI use |
| A multi-cloud replication layer | It routes each session to one backend at registration time; it does not synchronize across providers |
| A pricing authority | Token cost rates come from an external, cryptographically signed manifest; Memory Archive does not determine pricing |
| Principle | Description |
|---|---|
| Read-only by design | Memory Archive never issues commands to the OS environment, never modifies the tools it monitors, and has no write path into Control-Center or The-Eyes |
| Data integrity above all | Every write to disk or cloud is atomic (temp-file-then-rename); a crash at any point produces an explicitly flagged incomplete session, never a silently corrupted one |
| Incremental persistence | Every event, image, and annotation is persisted immediately and individually; nothing is batched, buffered, or flushed only at session end |
| Capture-first, reasoning-async | In automated mode, VLM API calls are dispatched asynchronously and never pause or delay the capture loop |
| Scale-first | A single ma-core instance is designed to handle 100,000+ concurrent active sessions without architectural changes |
| Secrets never cross the wire | Cloud credentials and VLM API keys never appear in IPC messages, config files, or log output; they are resolved from environment variables at runtime |
| Zero-credential remote annotation | Annotator machines hold no cloud credentials; all file reads and writes are proxied through ma-core over an authenticated, TLS-encrypted channel |
| Per-session circuit breaker isolation | Circuit breaker state lives in per-session objects; one session's VLM failure cannot degrade, open, or affect the circuit breaker of any other session |
| Pricing truth is external and signed | Token cost rates are sourced from a cryptographically signed manifest whose public key and URL are hardcoded in the binary; no user-entered or config-file rate is authoritative |
| Storage backend is pinned at registration | The storage backend for a session is selected and written to Redis when the session is registered; it is never re-evaluated mid-capture |
| Single-instance enforcement | ma-core writes a PID file on startup and refuses to start if a live process already holds that PID, preventing silent data corruption from duplicate writers |
Memory Archive operates in two distinct capture modes. In manual mode, a user performs a task live in the OS environment while Memory Archive records every non-position command event in real time. Annotation is done afterward using the built-in TUI. In automated mode, an orchestration layer registers sessions programmatically, and a VLM pipeline handles all reasoning asynchronously without pausing capture. Both modes produce identical output structures; the source field in reasoning.jsonl distinguishes human from model annotations.
ma-core is a Rust async daemon built on Tokio. All session state — status, metadata, step counters, Kafka offsets — is stored in Redis, not in process memory. This means session count is bounded by Redis capacity, not by ma-core RAM. The capture loop, IPC server, cloud upload queue, VLM pipeline, and Prometheus endpoint all run concurrently without blocking each other. The architecture is validated for 100,000+ concurrent sessions per process.
Every file write — whether to local disk or cloud storage — goes through an atomic path: data is written to a uniquely named temporary file, then renamed into place. On any crash, the temp file is either absent (write never started) or present (renamed to an incomplete marker). No partial-write corruption of finalized files is possible. Sessions that disconnect without a done command are renamed to {memory_name} (incomplete)/ and flagged in Redis with status incomplete.
For each captured step, Memory Archive fetches three frames from The-Eyes: a before frame (1000ms before the event), an at frame (exact event timestamp), and an after frame (1000ms after the event). For mouse events (click, double-click, right-click), the at frame is programmatically annotated in Rust by marker: a filled red circle (radius 6px, #dc2626) is drawn at the exact click coordinates, a directional arrow points from the circle edge toward the quadrant with the most available space, and a coordinate box displays the numeric X: and Y: values. The annotated frame is re-encoded to PNG. For keyboard events, the at frame is not annotated and is passed to the VLM or human annotator for contextual reasoning.
Remote annotators connect to ma-core over TLS 1.3 TCP using only an annotator_id and a 256-bit random key. They never hold cloud credentials. All file reads (FetchFile) and writes (UploadFile) are proxied through ma-core, which verifies the annotator's identity, checks write authority, and performs the actual cloud operation. Annotators receive only the files they are authorized to access for the sessions they have claimed.
Each annotator is registered with a unique identity (annotator_id), a 256-bit random key (only the SHA-256 hash is stored in Redis), a list of allowed tenant ID prefixes (allowed_tenant_ids), and a maximum concurrent claim limit (max_concurrent_claims). Auth failure attempts are rate-limited per annotator. The annotation queue visible to an annotator is filtered by their allowed_tenant_ids. Deactivation preserves full audit history. Key rotation takes effect immediately, invalidating the old key.
A single ma-core instance can route different sessions to different storage backends — AWS S3, Azure Blob/ADLS/Files, GCP Cloud Storage, or local disk — based on configurable routing rules (tenant prefix, region tag, storage mode). The selected backend is pinned to the session at registration time and stored in Redis. Multiple named backends (e.g., aws-us-east, azure-eu-west, gcp-asia) can coexist in a single deployment. All backends support MD5 integrity verification on upload.
ma-app's ModelRouter applies one of three stateless routing policies — pinned, fallback, or load_balance — to select a VLM provider per request. Each session has its own ReasoningPipeline with an independent circuit breaker. A circuit opens after 5 consecutive non-retryable failures and enters a half-open trial after 60 seconds. When both primary and fallback circuits open, the session transitions to reasoning_degraded status, capture continues, and steps are marked source: model_degraded. On session completion, the session auto-enters the human annotation queue. At most two providers per session are permitted — one primary and one fallback — to preserve dataset clarity.
Token cost rates are sourced from an MA-hosted Ed25519-signed JSON manifest. The public key and manifest URL are both hardcoded in the binary and cannot be overridden by config or environment variables. The manifest is cached locally for 24 hours; signature verification is performed on every cache load. A failed signature check deletes the cache and triggers a fresh fetch, and fires an ERROR alert. Fallback tiers include the AWS Bedrock Pricing API and user-configured cost rates in ProviderConfig. Model aliases (e.g., gpt-4o-2024-11-20 → gpt-4o) are resolved via the manifest's aliases[] array.
Each session registration can specify its own capture_server_addr (The-Eyes) and actuation_server_addr (Control-Center), overriding the global config fallback. These per-session addresses are stored in Redis and used throughout the capture lifecycle. This enables a single ma-core instance to serve sessions spread across many different capture and actuation servers — a requirement at scale where The-Eyes and Control-Center may run on different hosts per OS environment.
ma-core exposes a Prometheus metrics endpoint (default port 9091, optional Bearer token auth) with 25+ counters and gauges covering: active session count, steps captured per second, cloud upload queue depth and permanent failures, IPC connection counts and push queue depth, Kafka consumer lag, VLM request latency histograms (p50/p95/p99), per-provider error counts by type, circuit breaker state per session, pricing registry fetch status and manifest age, per-annotator active claims and auth failures, and storage routing decisions. Alert conditions are delivered via configurable webhook.
The built-in TUI (built on Textual) provides a two-pane interface: a virtual-scrolling step list on the left and an image preview on the right, with a multi-line reasoning editor, word counter, undo/redo, clipboard support, and autosave every 2.5 seconds. Steps are resumable — previously saved reasoning is loaded from reasoning.jsonl on open. The TUI is followed automatically by CompilerApp, a full-screen terminal text editor for producing and finalizing memory.md. In remote annotator mode, the TUI sends claim heartbeats every 5 minutes and prefetches the next step's images in the background.
Memory Archive ships with a one-line installer for Linux/macOS (install.sh) and Windows (install.ps1), covering both the Rust binaries and the Python wheel. The memory-archive update command checks the GitHub Releases API, downloads the latest platform archive, and replaces binaries in place. The memory-archive uninstall command removes binaries and optionally purges all local state (--purge). All three paths handle version transitions cleanly without leaving orphaned files.
Session lifecycle:
- Register a new session (manual or automated mode) via IPC
- Start watching a session's command stream
- Signal session completion (
done) - Query full session status from Redis
- Automatic transition from
reasoning_degraded→pending_human_annotationon session completion - Startup sweep: mark sessions with stale heartbeats as
incompleteonma-corerestart - Reconcile sweep: re-queue orphaned
annotatingsessions onma-corerestart
Capture:
- Stream CommandEvent messages from Control-Center WatchCommands gRPC endpoint
- Consume CommandEvent messages from Kafka (cloud_primary mode)
- Drop position-only events silently (no file write, no step counter increment)
- Write
raw_input.md,converted_input.md,actuation_commands.json,cc_commands.jsonatomically per step - Mark failed commands with
[FAILED]prefix inraw_input.md - Detect tool silence beyond configurable timeout and trigger graceful disconnect
- Filter Control-Center heartbeat-only messages from the command stream
- Track Kafka partition and offset per session for crash recovery
Image capture:
- Fetch
before,at, andafterframes from The-Eyes per step - Route fetch using
FetchDecisionbased on action type and configurable timing offsets - Skip image fetches for position events and failed commands
- Fetch
closing_state.webpon sessiondone - Log skipped image fetches in
metadata.jsonwithout blocking step record - Annotate mouse
atframes in Rust (marker.rs): filled circle, directional arrow, coordinate box - Re-encode annotated frames to PNG
- Check The-Eyes liveness via configurable poll interval
Storage:
- Write to local disk with atomic temp-file-then-rename
- Upload to AWS S3 (multipart >5MB, MD5 + ETag integrity verification)
- Upload to Azure Blob Storage, ADLS Gen2, and Azure Files (auto-detection, MD5 integrity)
- Upload to GCP Cloud Storage (MD5 integrity)
- Route sessions to named storage backends via routing rules
- Pin storage backend per session in Redis at registration
- Retry uploads with exponential backoff (SyncWorker)
- Permanently fail uploads after max retries and fire an ERROR alert
- Track per-session upload state in
sync_log.json - Flush
metadata.jsonto cloud every N steps (metadata_flush_interval) - Proxy file reads and writes for remote annotators via IPC (zero cloud credentials on annotator machine)
- Download session files for annotation in local mode (
fetch_session_if_missing) - Download session files to temp dir for cloud_primary annotation (
RemoteFetcher)
Conversion:
- Convert raw command strings to human-readable descriptions
- Normalize key names per OS (
humanize_key) - Produce OS-specific human-readable output for keyboard and mouse events
Reasoning — manual/human:
- Open TUI for step-by-step human annotation
- Load existing reasoning from
reasoning.jsonlon TUI open (resumable) - Write reasoning atomically to
reasoning.jsonlviaReasoningWriter - Send
UpdateAnnotationProgressIPC notification per save - Support step skipping (skipped steps logged in
reasoning.jsonl) - Send claim heartbeat every 5 minutes in remote annotator mode
Reasoning — automated/VLM:
- Receive
StepReadyForReasoningpush fromma-coreper step - Route to primary or fallback VLM per session routing policy
- Call any OpenAI-compatible API endpoint (
GenericApiModelBackend) - Call Proprietary VLM API (
InternalModelBackend) - Apply sliding-window rate limiting (requests/min + token budget/hour)
- Track per-session circuit breaker state (threshold, reset interval)
- Transition session to
reasoning_degradedwhen circuit opens - Return
ReasoningResulttoma-corevia IPC - Send
automatedcommand to start VLM daemon per session
Compilation:
- Generate
memory.mdscaffold fromreasoning.jsonl(run_compile) - Open
CompilerAppfor full-screen terminal editing - Finalize session (status →
complete, Redis 90-day TTL,FinalizeMemoryIPC) on editor save
Remote annotation:
- Register annotators via admin CLI or REST API
- Deactivate annotators (audit history preserved)
- Rotate annotator keys (immediate effect)
- List all annotators with status and claim counts
- Generate base64 connection profile strings for annotator onboarding
- Set up annotator config from profile string on annotator machine
- List annotation queue (filtered by
allowed_tenant_ids) - Claim specific or oldest available session
- Release session claim
- Auto-heartbeat claim TTL renewal
- Detect and handle claim expiry mid-annotation
Observability:
- Expose Prometheus metrics on configurable port with optional Bearer token auth
- Emit structured JSON logs (capture, IPC, storage, VLM subsystems)
- Deliver webhook alerts for all defined alert conditions
- Track pricing registry fetch status, manifest age, and signature verification result
- Track per-annotator active claims and auth failure rates
- Track storage routing decisions and backend errors
Server management:
- Start
ma-corein foreground or daemon mode - Stop
ma-corevia SIGTERM / taskkill - Stream or tail
ma-corelogs - Start
ma-kafka-producerKafka bridge for a session - Generate and display TLS cert fingerprint
- Enforce single-instance via PID file
Install/update/uninstall:
- One-line install on Linux/macOS and Windows
- Check for updates without downloading
- In-place binary update
- Uninstall with optional full state purge
Session Lifecycle Management. Every Memory Archive recording begins with a session registration IPC call that creates a Redis hash for the session and initializes the memory directory structure on disk or cloud. The session moves through a well-defined state machine — active → pending_annotation → annotating → pending_compilation → complete — with defined exception paths for incomplete disconnects and VLM degradation. Redis Set indexes (sessions:active, sessions:pending, etc.) allow O(1) membership checks and fast queue operations. On ma-core startup, a startup sweep inspects all sessions with active or annotating status and marks those whose associated processes are no longer running as incomplete or re-queues them. A reconcile sweep re-queues any orphaned annotating sessions that have no live heartbeat. TTLs are enforced in Redis: incomplete sessions expire after 7 days, complete sessions after 90 days.
Capture and Command Processing. The capture loop (run_watch_loop) supports two event sources — direct gRPC streaming from Control-Center or Kafka consumption (cloud_primary mode). In gRPC mode, the WatchStream connects directly to the Control-Center WatchCommands endpoint. In Kafka mode, StreamConsumer subscribes to the control-center-events topic, consuming from the partition keyed to the session's session_id. Position-only events are silently dropped; heartbeat-only messages from Control-Center are filtered before any processing. For each non-position event, the capture loop writes four files atomically: raw_input.md (raw command string), converted_input.md (human-readable), actuation_commands.json (full CommandEvent JSON), and cc_commands.json (Control-Center replay format). The convert module normalizes key names per OS (humanize_key) and generates grammatically correct human-readable descriptions. Silence detection monitors the time since the last non-position event; if the configurable silence_timeout_seconds elapses without activity, the session is gracefully disconnected.
Three-Frame Image Capture and Annotation. For each step, VisionPipeline evaluates a FetchDecision — based on action type, event timestamp, and per-type timing offsets — and issues three HTTP requests to The-Eyes' /frames/closest endpoint. The before frame is fetched at timestamp - 1000ms, the at frame at the exact event timestamp (plus a press or type delay for keyboard events), and the after frame at timestamp + 1000ms. For mouse events, marker.rs annotates the at frame in Rust: a filled red circle is drawn at the click coordinates, a directional arrow is drawn from the circle edge toward the least-crowded screen quadrant, and a coordinate box is rendered with the numeric X/Y values. The annotated frame is re-encoded to PNG. Before/after frame fetch failures are logged in metadata.json under skipped_image_fetches[] but do not block the at frame or the step record. A closing_state.webp is fetched on the done command.
Multi-Cloud Storage with Routing. StorageRouter selects a named backend at session registration time by evaluating routing rules against session attributes (tenant prefix, region tag, mode). The backend name is stored in the Redis session hash and is never re-evaluated. Each named backend (LocalBackend, S3Backend, AzureBackend, GcpBackend) implements the StorageBackend trait and handles its own credential resolution, integrity verification, and retry logic. In local mode, SyncWorker runs as a background thread, dequeuing FileWritten events and uploading to cloud with exponential backoff. Permanent upload failures fire an ERROR webhook alert. Cloud session read-back for annotation (fetching a session from cloud to a temp dir) is handled by RemoteFetcher for remote annotators and fetch_session_if_missing for local annotation.
VLM Reasoning Pipeline. In automated mode, ma-core emits a StepReadyForReasoning IPC push event per step. ma-app's ReasoningPipeline receives the push, applies the session's routing policy via ModelRouter, and dispatches the request to the selected ModelBackend — either GenericApiModelBackend (any OpenAI-compatible endpoint) or InternalModelBackend (Proprietary VLM). Rate limiting is applied via a sliding-window RateLimiter across both requests_per_minute and token_budget_per_hour. The per-session circuit breaker in ReasoningPipeline counts non-retryable failures; at the threshold, it opens, all subsequent requests for that session return model_degraded, and after circuit_breaker_reset_seconds, a single half-open trial request is sent. If the trial succeeds, the circuit closes. On ReasoningResult return, ma-core writes the reasoning to reasoning.jsonl, updates metadata.json token counts, and advances the session.
Human Annotation TUI. AnnotationApp is a Textual-based terminal application providing a two-pane interface: a virtual-scrolling StepList on the left (with step status icons and accordion expansion) and an image preview pane on the right. The ReasoningEditor widget supports multi-line input, word counting, undo/redo with Ctrl+Z/Y, clipboard, and autosave every 2.5 seconds. SessionLoader reads all existing reasoning.jsonl entries and metadata.json on open, pre-populating the step list. ReasoningWriter performs atomic upserts to reasoning.jsonl. After all steps are annotated, AnnotationComplete overlay offers to launch CompilerApp immediately. CompilerApp is a full-screen terminal text editor with autosave, a CompilerStatusBar showing word count and save state, and a quit overlay. On save, it calls FinalizeMemory IPC, setting session status to complete and applying a 90-day Redis TTL.
| Tier | Description | ma-core RAM | Redis | Kafka | Storage | Network |
|---|---|---|---|---|---|---|
| Development | Single developer, local mode, no cloud | 512 MB | Single node, in-memory | Not required | Local disk | LAN |
| Team | Small team, cloud sync, remote annotation | 2 GB | Persistent (AOF + RDB snapshots) | Not required | One cloud provider | 1 Gbps |
| Production | AI lab / enterprise, 100k+ sessions, automated | 16–64 GB per ma-core |
Redis Cluster or Redis Enterprise | Kafka 3.0+ KRaft, 200-partition topics | Multi-region cloud | 10 Gbps+ to cloud storage |
| Component | Requirement |
|---|---|
ma-core |
Rust 1.85+ (build only); Redis 7.0+; Kafka 3.0+ (cloud_primary mode); cloud credentials in env (cloud_primary); MA_IPC_TOKEN env var (if ipc_port set); MA_ANNOTATOR_MGMT_TOKEN env var (if annotator_mgmt_port set) |
ma-app |
Python 3.13; pip; network access to ma-core (Unix socket locally, TLS TCP remotely) |
ma-kafka-producer |
Rust 1.85+ (build only); access to Control-Center gRPC endpoint; access to Kafka broker |
| Annotator machine | Python 3.13; pip; network access to ma-core TLS TCP port; no cloud credentials required |
| Platform | Architecture | Notes |
|---|---|---|
| Linux | x86_64 | Ubuntu 22.04+; glibc 2.35+ |
| Linux | arm64 | Ubuntu 22.04+; cross-compiled via cross crate |
| macOS | x86_64 | macOS 12 (Monterey)+ |
| macOS | arm64 (Apple Silicon) | macOS 12 (Monterey)+ |
| Windows | x86_64 | Windows 10 1809+; Windows 11 |
| Backend | Authentication | Multipart | Integrity | Min SDK/API |
|---|---|---|---|---|
| Local disk | OS filesystem permissions | N/A | Atomic rename | N/A |
| AWS S3 | Env vars / IAM role / ~/.aws credential chain |
Yes (>5 MB) | MD5 pre-upload + ETag verification | aws-sdk-s3 (Rust) |
| Azure Blob | Service principal / IMDS managed identity / Azure CLI | N/A | MD5 | REST API 2026-02-06 |
| Azure ADLS Gen2 | Service principal / IMDS managed identity / Azure CLI | N/A | MD5 | REST API 2026-02-06 |
| Azure Files | Service principal / IMDS managed identity / Azure CLI | N/A | MD5 | REST API 2026-02-06 |
| GCP Cloud Storage | Application Default Credentials | N/A | MD5 | google-cloud-storage (Rust) |
| Backend type | API compatibility |
|---|---|
GenericApiModelBackend |
Any OpenAI-compatible /v1/chat/completions endpoint |
InternalModelBackend |
Proprietary VLM API |
| AWS Bedrock (pricing only) | pricing:GetProducts IAM action |
| Dependency | Version | Component | Purpose |
|---|---|---|---|
| Redis | 7.0+ | ma-core |
Session state, annotator registry, claim TTLs |
| Kafka | 3.0+ KRaft | ma-core (cloud_primary) |
Event streaming, crash recovery |
tokio |
1.x | ma-core |
Async runtime |
tonic |
0.12+ | ma-core, ma-proto |
gRPC client |
rcgen |
0.13+ | ma-core |
Self-signed TLS cert generation |
reqwest |
0.12+ | ma-core |
HTTP client (The-Eyes, Azure REST) |
aws-sdk-s3 |
latest | ma-core |
S3 operations |
google-cloud-storage |
latest | ma-core |
GCS operations |
rdkafka |
0.36+ | ma-core, ma-kafka-producer |
Kafka client |
prometheus |
0.13+ | ma-core |
Metrics exposition |
tracing |
0.1+ | ma-core |
Structured logging |
Textual |
0.60+ | ma-app |
TUI framework |
Typer |
0.12+ | ma-app |
CLI framework |
boto3 |
1.34+ | ma-app |
Python S3 client (SyncWorker) |
azure-storage-blob |
12.19+ | ma-app |
Python Azure client (SyncWorker) |
google-cloud-storage |
2.16+ | ma-app |
Python GCS client (SyncWorker) |
protoc |
any stable | build | Protobuf compiler |
| Message direction | Transport | Auth tier | Purpose |
|---|---|---|---|
CLI → ma-core (local) |
Unix domain socket | None (file permission) | All local admin commands |
CLI → ma-core (remote admin) |
TLS 1.3 TCP | MA_IPC_TOKEN Bearer |
Remote admin operations |
Annotator machine → ma-core |
TLS 1.3 TCP | AnnotatorAuth JSON (first message) |
Annotation session operations |
ma-core → ma-app (push) |
Same transport as initiating connection | N/A (push on established conn) | StepReadyForReasoning, FileWritten, session events |
curl -fsSL https://raw.githubusercontent.com/nullvoider07/memory-archive/master/install/install.sh | bashThe installer downloads the platform-appropriate release archive from GitHub Releases, extracts ma-core and ma-kafka-producer to ~/.local/bin/ (or /usr/local/bin/ with sudo), installs the Python wheel into your active Python environment, and verifies the installation with a memory-archive ping.
irm https://raw.githubusercontent.com/nullvoider07/memory-archive/master/install/install.ps1 | iexThe installer downloads ma-windows-x64.zip, extracts binaries to %LOCALAPPDATA%\MemoryArchive\bin\, installs the Python wheel, and adds the bin directory to PATH for the current user.
# Prerequisites: Rust 1.85+, Python 3.13, protoc on PATH
git clone https://github.com/nullvoider07/memory-archive.git
cd memory-archive
# Build Rust binaries
cargo build --release
# Install Python package
cd ma-app
pip install .
# Verify
ma-core --version
memory-archive pingFor cross-compilation to Linux arm64:
cargo install cross
cross build --release --target aarch64-unknown-linux-gnuBefore using Memory Archive, run the config command to set your storage path and, if using cloud sync, your cloud provider:
# Local-only setup
memory-archive config --storage-path ~/memories --storage-mode local
# With cloud sync (AWS S3 example)
memory-archive config \
--storage-path ~/memories \
--storage-mode local \
--cloud aws \
--aws-bucket my-memory-archive-bucket \
--aws-region us-east-1
# Set Control-Center and The-Eyes addresses
memory-archive config \
--control-center-addr localhost:50051 \
--the-eyes-addr http://localhost:8080
# Verify config
memory-archive config --showCloud credentials are not set in config — they are read from environment variables at runtime:
# AWS
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
# Azure (service principal)
export AZURE_TENANT_ID=...
export AZURE_CLIENT_ID=...
export AZURE_CLIENT_SECRET=...
# GCP
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.jsonThis walkthrough covers the full pipeline from installation to a finalized memory.md.
Step 1 — Start ma-core
memory-archive server start --daemonma-core starts in the background, creates ~/.memory-archive/ma-core.pid, opens the Unix socket, and writes initial logs to ~/.memory-archive/ma-core.log.
Step 2 — Verify connectivity
memory-archive ping
# → ma-core v0.x.x — OKStep 3 — Register a session
SESSION_ID=$(memory-archive session register \
--mode manual \
--os-type LINUX \
--os-version "Ubuntu 24.04 LTS" \
--os-arch x86_64 \
--capture-server the-eyes-local \
--actuation-server cc-local \
--memory-name "open-browser-and-search")
echo "Session: $SESSION_ID"This creates the memory directory at ~/memories/open-browser-and-search/ and registers the session in Redis.
Step 4 — Start watching
memory-archive start --session "$SESSION_ID"This command blocks. Memory Archive is now recording every non-position command event from Control-Center and fetching frames from The-Eyes.
Step 5 — Perform the task
Perform the task you want to record in your OS environment. Control-Center streams each command event to Memory Archive. Each step is written atomically to disk.
Step 6 — Signal completion
When you have finished the task, in a new terminal:
memory-archive done --session "$SESSION_ID"The start command returns. Session status advances to pending_annotation.
Step 7 — Check status
memory-archive status --session "$SESSION_ID"Step 8 — Annotate
memory-archive annotate --session "$SESSION_ID"The TUI opens. For each step:
- Navigate with
j/kor arrow keys - Press
eto edit the reasoning for the selected step - Write your natural-language reasoning in the editor
- Press
Ctrl+Nto save and advance to the next step - Press
?for the full keyboard shortcut reference
When all steps are annotated, the AnnotationComplete overlay appears. Choose "compile now" to proceed directly.
Step 9 — Compile
If you did not compile from the AnnotationComplete overlay, run:
memory-archive compile --session "$SESSION_ID"CompilerApp opens with a scaffold of memory.md generated from your reasoning. Edit the document to your satisfaction, then press Ctrl+Q and save. FinalizeMemory IPC is called: session status → complete, Redis 90-day TTL applied.
Step 10 — View results
ls ~/memories/open-browser-and-search/
# memory.md metadata.json commands/ vision/ reasoning/
cat ~/memories/open-browser-and-search/memory.mdStep 11 — Check cost (automated mode only)
memory-archive cost --session "$SESSION_ID" --detailedThis walkthrough sets up a shared ma-core with remote annotators.
On the server (operator):
# 1. Configure cloud storage and Redis
memory-archive config \
--storage-mode cloud_primary \
--cloud aws \
--aws-bucket team-memories \
--aws-region eu-west-1 \
--redis-url redis://redis-host:6379 \
--ipc-port 9001
# 2. Set the admin IPC token
export MA_IPC_TOKEN=<strong-random-token>
export MA_ANNOTATOR_MGMT_TOKEN=<strong-random-token>
# 3. Start ma-core
memory-archive server start --daemon
# 4. Get the TLS fingerprint
memory-archive tls fingerprint
# → AA:BB:CC:DD:...
# 5. Register an annotator
memory-archive annotator-admin register \
--annotator-id alice \
--allowed-tenants acme-corp \
--max-claims 3
# → Key: <plaintext key shown once — distribute securely>
# 6. Generate a connection profile for Alice
memory-archive annotator-admin generate-profile --annotator-id alice
# → base64 profile stringOn the annotator machine (Alice):
# 1. Install Memory Archive
curl -fsSL https://raw.githubusercontent.com/nullvoider07/memory-archive/master/install/install.sh | bash
# 2. Configure from profile
memory-archive annotator setup <base64-profile-from-operator>
# 3. View annotation queue
memory-archive annotator queue
# 4. Claim and annotate a session
memory-archive annotator claim
# TUI opens for the oldest available session
# Or claim a specific session:
memory-archive annotator claim --session "$SESSION_ID"Automated mode requires cloud_primary storage, Kafka, Redis, and a running VLM endpoint.
# 1. Configure
memory-archive config \
--storage-mode cloud_primary \
--cloud aws \
--aws-bucket prod-memories \
--aws-region us-east-1 \
--redis-url redis://redis-cluster:6379 \
--kafka-broker kafka-host:9092
# 2. Add VLM provider config to config.json (see Section 10)
# 3. Start ma-core
export MA_IPC_TOKEN=<token>
memory-archive server start --daemon
# 4. From orchestration layer: register session with --mode automated
SESSION_ID=$(memory-archive session register \
--mode automated \
--os-type LINUX \
--memory-name "automated-task-001" \
--tenant-id my-org)
# 5. Start VLM reasoning daemon
memory-archive automated --session "$SESSION_ID" &
# 6. Start watching
memory-archive start --session "$SESSION_ID"
# 7. Signal done when orchestration layer completes task
memory-archive done --session "$SESSION_ID"
# 8. Compile (or compile programmatically via FinalizeMemory IPC)
memory-archive compile --session "$SESSION_ID"All local IPC communication uses a Unix domain socket at ~/.memory-archive/ma.sock. The socket file has permissions 600, and its parent directory (~/.memory-archive/) has permissions 700. Only the owner process and processes running as the same user can connect.
All remote IPC communication (annotators, remote admin) uses TLS 1.3 exclusively. TLS 1.2 and below are rejected at the handshake level. A self-signed CA is generated by rcgen on the first ma-core start and stored at ~/.memory-archive/ca/ca-cert.pem with a 10-year validity. The server certificate is generated from this CA, stored at ~/.memory-archive/ipc-cert.pem with a 1-year validity, and has file permissions 600. Clients do not use standard CA trust chains — instead, the SHA-256 fingerprint of the server certificate is pinned on the client side. No Trust On First Use (TOFU): the fingerprint must be explicitly configured before any connection is accepted. The fingerprint is distributed via a base64 connection profile string.
The admin IPC token is read exclusively from the MA_IPC_TOKEN environment variable. It is never written to config.json, never logged, and never transmitted in IPC messages (it is verified at connection establishment and then discarded). If ipc_port is configured in config.json but MA_IPC_TOKEN is not set in the environment, ma-core refuses to start and exits with a CRITICAL error.
Each annotator is provisioned with a 256-bit cryptographically random key generated server-side. Only the SHA-256 hash of the key is stored in Redis (key_hash field on the annotator:{annotator_id} hash). The plaintext key is returned once in the AnnotatorRegistered IPC response and never stored or logged afterward; the operator is responsible for distributing it to the annotator securely. Hash comparison at authentication time uses constant-time comparison to prevent timing oracle attacks. Authentication failures are tracked per annotator with a 60-second rolling window; more than 10 failures in 60 seconds fires a WARNING alert for brute-force detection. The last_auth_at timestamp is updated on every successful authentication for audit purposes.
Annotator deactivation sets the status field in the Redis hash to "deactivated". The record and its full audit history are preserved; the annotator simply can no longer authenticate. Key rotation generates a new 256-bit key, updates key_hash in Redis, and immediately invalidates any session using the old key.
The UploadFile IPC message from an annotator connection is subject to server-side path validation and write authority checks in the Rust handler — not on the client. Only two paths are permitted: reasoning/reasoning.jsonl and metadata.json. Any other path returns a WRITE_FORBIDDEN error. For reasoning.jsonl uploads, the Rust handler forcibly overwrites the source field in every entry to "human" regardless of the value supplied by the client. This prevents training data poisoning where a rogue annotator client could submit reasoning entries falsely attributed to a VLM. For metadata.json uploads, only counter fields (annotated_steps, skipped_steps) are permitted; status changes are rejected server-side. The validate_relative_path() function normalizes all submitted paths and rejects any path containing .. sequences, preventing directory traversal attacks. The payload size limit for UploadFile is 50 MB.
Cloud credentials (AWS access keys, Azure service principal credentials, GCP Application Default Credentials) exist only on the ma-core server. They are never transmitted over any IPC channel, never written to config files, and never logged. Remote annotator machines hold zero cloud credentials. All file access for remote annotators — both reads (FetchFile) and writes (UploadFile) — is proxied through ma-core, which performs the actual cloud storage operation after verifying annotator identity and write authority.
The Ed25519 public key used to verify the pricing manifest and the manifest URL are both hardcoded in the ma-core binary at compile time. They cannot be overridden by environment variables, config files, or IPC messages. This prevents DNS spoofing, BGP hijack, and config manipulation attacks from substituting a fraudulent manifest. If signature verification fails for any reason — including a corrupt cache or a compromised network response — the manifest is discarded, the local cache is deleted, an ERROR alert is fired, and the system falls back to the next pricing tier (Bedrock Pricing API or configured cost rates).
The AWS Bedrock Pricing API (pricing:GetProducts) is accessed under a separate IAM policy from the inference policy. This prevents a confused deputy scenario where an attacker could use ma-core's inference credentials to query pricing data and infer usage patterns or billing information.
Circuit breaker state is stored in per-session _SessionState objects held by ReasoningPipeline. It is never held in ModelRouter, which is stateless. One session's VLM failures cannot affect any other session's circuit breaker. Storage backends are pinned per session in Redis at registration and are never re-evaluated mid-capture; a routing rule change after session start does not affect in-flight sessions. Kafka partition key is session_id, ensuring that all events for a session are consumed in order and that events for different sessions never mix. The allowed_tenant_ids list on each annotator record is evaluated server-side when building the annotation queue; annotators cannot access sessions outside their permitted tenant scope.
ma-core writes its PID to ~/.memory-archive/ma-core.pid on startup. On every subsequent startup, it reads this file and checks whether the recorded PID corresponds to a live process. If a live process is found, ma-core exits with an error. This prevents two concurrent ma-core instances from writing to the same session directories, which could produce corrupted state even with atomic individual writes.
| Aspect | Manual (duo) | Automated (trio) | Remote Human Annotation | Degraded → Human Fallback |
|---|---|---|---|---|
| Who annotates | Local human user via TUI | VLM API (async) | Remote human annotator via TUI | Human annotator (after VLM failure) |
| Requires Kafka | No | Yes | No (local capture); yes if cloud_primary | No |
| Requires cloud storage | Optional | Yes (cloud_primary required) | Yes (cloud_primary required) | No |
| Requires Redis | Yes | Yes | Yes | Yes |
| Session registration | Manual CLI | Programmatic (orchestration layer) | Any | Any |
| Annotation timing | After capture | During capture (async) | After capture | After capture |
| Circuit breaker | N/A | Per-session | N/A | N/A |
| VLM providers | None | 1 primary + 1 optional fallback | None | None |
| Annotator credentials | Local only | N/A | TLS + annotator key | Local or remote |
In manual mode, the operator works directly in the OS environment while Memory Archive watches the Control-Center gRPC stream. The system captures every non-position, non-failed command event, fetches three frames per step from The-Eyes, and annotates mouse frames programmatically. No VLM is involved during capture. After the task is complete, the operator runs memory-archive done, then memory-archive annotate to open the TUI and write natural-language reasoning for each step, then memory-archive compile to produce and finalize memory.md. This mode works entirely without Kafka and optionally without cloud storage.
Mode-specific requirements: Redis 7.0+; Control-Center running locally; The-Eyes running locally; MA_IPC_TOKEN not required unless ipc_port is configured.
In automated mode, an orchestration layer registers sessions programmatically via IPC, the OS environment runs the task while Memory Archive captures, and a VLM pipeline handles reasoning in real time without pausing capture. ma-app's automated command starts a reasoning daemon per session that receives StepReadyForReasoning push events, calls the configured VLM API, and returns ReasoningResult IPC messages. When all steps are captured and the orchestration layer sends done, the session proceeds directly to compilation (via compile or FinalizeMemory IPC). No human annotation is required in the happy path.
Mode-specific requirements: Redis 7.0+; Kafka 3.0+ KRaft; cloud_primary storage; VLM API endpoint and credentials; MA_IPC_TOKEN env var.
In remote human annotation mode, annotators connect to ma-core over TLS 1.3 TCP from machines that hold no cloud credentials. An operator registers annotators with annotator-admin register, distributes the plaintext key and connection profile, and annotators use memory-archive annotator claim to pick sessions from the queue and annotate them via the TUI. The TUI sends a HeartbeatClaim IPC every 5 minutes to refresh the claim TTL. File reads (images) and writes (reasoning.jsonl) are proxied through ma-core. This mode is compatible with both manually captured and automated-then-degraded sessions.
Mode-specific requirements: Redis 7.0+; ipc_port configured; MA_IPC_TOKEN and MA_ANNOTATOR_MGMT_TOKEN env vars; TLS cert fingerprint distributed to annotators; annotator records registered in Redis.
When both the primary and fallback VLM circuits open for a session, ma-core transitions the session to reasoning_degraded status. Capture continues normally; steps after the degradation point are marked source: model_degraded in reasoning.jsonl. When the session receives a done command, it automatically transitions to pending_human_annotation and appears in the annotation queue. In the TUI, steps annotated before degradation appear as complete with their VLM reasoning pre-populated; degraded steps appear as empty and require human annotation. The final memory.md reflects the mix of VLM and human reasoning, with each step's source field distinguishing the two.
Mode-specific requirements: Same as automated mode for the capture phase; same as remote human annotation for the annotation phase.
All commands are subcommands of the memory-archive entry point.
Register a new capture session in Redis and initialize the memory directory.
memory-archive session register [OPTIONS]
Options:
--mode TEXT Capture mode: manual | automated [required]
--os-type TEXT OS type: LINUX | WINDOWS | MACOS [required]
--os-version TEXT OS version string (e.g., "Ubuntu 24.04 LTS")
--os-arch TEXT OS architecture (e.g., x86_64)
--os-env-id TEXT Opaque OS environment identifier
--capture-server TEXT The-Eyes server ID
--actuation-server TEXT Control-Center server ID
--memory-name TEXT Directory name for this memory [required]
--tenant-id TEXT Tenant identifier (optional, overrides config)
Returns: session ID (UUID string) printed to stdout.
# Example
SESSION_ID=$(memory-archive session register \
--mode manual \
--os-type LINUX \
--os-version "Ubuntu 24.04 LTS" \
--os-arch x86_64 \
--capture-server eyes-01 \
--actuation-server cc-01 \
--memory-name "fill-web-form")Start watching a registered session's command stream. Blocks until done is received or the tool disconnects.
memory-archive start [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
In local mode, also starts SyncWorker for cloud uploads and handles FileWritten push events from ma-core.
memory-archive start --session "$SESSION_ID"Signal that the capture is complete. Flushes all in-memory state, fetches the closing state image, transitions Redis status to pending_annotation (manual) or the appropriate automated state.
memory-archive done [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
memory-archive done --session "$SESSION_ID"Start the VLM reasoning daemon for an automated session. Receives StepReadyForReasoning push events from ma-core, calls the configured VLM, and returns ReasoningResult via IPC. Runs until the session is complete or disconnected.
memory-archive automated [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
Requires storage_mode = cloud_primary and VLM provider config in config.json.
memory-archive automated --session "$SESSION_ID"Open the TUI annotation interface for a session. In local mode, also starts SyncWorker. Resumes from the last annotated step if partially complete. After annotation, launches CompilerApp automatically.
memory-archive annotate [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
memory-archive annotate --session "$SESSION_ID"Standalone command to regenerate the memory.md scaffold from reasoning.jsonl and open CompilerApp. Calls FinalizeMemory IPC on editor save.
memory-archive compile [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
memory-archive compile --session "$SESSION_ID"Print all Redis session hash fields for a session.
memory-archive status [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
memory-archive status --session "$SESSION_ID"Print token usage and estimated cost for a session.
memory-archive cost [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
--detailed Scan reasoning.jsonl for per-step breakdown
(default: reads metadata.json summary only)
Output includes: total input tokens, total output tokens, total tokens, estimated cost in USD, per-provider breakdown, and rate source (manifest / Bedrock API / configured).
memory-archive cost --session "$SESSION_ID"
memory-archive cost --session "$SESSION_ID" --detailedCheck ma-core connectivity. Sends a Ping IPC message and prints the ma-core version string from the Pong response.
memory-archive ping
# → ma-core v0.3.0 — OKCheck for and apply updates. Downloads the latest release archive for the current platform from GitHub Releases and replaces binaries and wheel in place.
memory-archive update [OPTIONS]
Options:
--check-only Only check for a newer version; do not download
memory-archive update
memory-archive update --check-onlyRemove Memory Archive binaries and optionally all local state.
memory-archive uninstall [OPTIONS]
Options:
--purge Also remove ~/.memory-archive/ (config, TLS certs, session data)
-y, --yes Skip confirmation prompt
memory-archive uninstall
memory-archive uninstall --purge --yesList all sessions in pending_human_annotation that are visible to the current annotator (filtered by allowed_tenant_ids). Sorted oldest-first.
memory-archive annotator queueClaim a session from the annotation queue and open the TUI. Sends a HeartbeatClaim every 5 minutes while the TUI is open. Releases the claim on TUI exit.
memory-archive annotator claim [OPTIONS]
Options:
-s, --session TEXT Specific session ID to claim (omit to auto-claim oldest)
memory-archive annotator claim
memory-archive annotator claim --session "$SESSION_ID"Configure this machine as a remote annotator from a base64 connection profile string provided by the operator.
memory-archive annotator setup <PROFILE>
Arguments:
PROFILE Base64 connection profile string from operator [required]
Decodes the profile and writes ma_core_addr, ipc_server_fingerprint, annotator_id, and annotator_key to config.json.
memory-archive annotator setup eyJtYV9jb3JlX2FkZHIiOiAi...Register a new annotator. Generates a 256-bit random key, stores its SHA-256 hash in Redis, and returns the plaintext key once.
memory-archive annotator-admin register [OPTIONS]
Options:
--annotator-id TEXT Unique annotator identifier [required]
--allowed-tenants TEXT Comma-separated tenant ID prefixes (empty = all tenants)
--max-claims INT Maximum concurrent claims (0 = unlimited) [default: 0]
memory-archive annotator-admin register \
--annotator-id alice \
--allowed-tenants acme-corp,beta-labs \
--max-claims 5Deactivate an annotator. Sets status = deactivated in Redis; audit history preserved.
memory-archive annotator-admin deactivate [OPTIONS]
Options:
--annotator-id TEXT Annotator ID to deactivate [required]
memory-archive annotator-admin deactivate --annotator-id aliceRotate an annotator's key. Generates a new 256-bit key, invalidates the old key immediately, and returns the new plaintext key once.
memory-archive annotator-admin rotate-key [OPTIONS]
Options:
--annotator-id TEXT Annotator whose key to rotate [required]
memory-archive annotator-admin rotate-key --annotator-id aliceList all registered annotators with their current status, claim counts, allowed tenants, and last authentication timestamp.
memory-archive annotator-admin listGenerate a base64 connection profile string for an annotator. Looks up the annotator in Redis (must be active), prompts for the plaintext key, and produces the encoded profile.
memory-archive annotator-admin generate-profile [OPTIONS]
Options:
--annotator-id TEXT Annotator to generate profile for [required]
memory-archive annotator-admin generate-profile --annotator-id aliceSet or view configuration. All options are optional; only specified flags are updated.
memory-archive config [OPTIONS]
Storage:
--storage-path PATH Local memory directory [default: ~/memories]
--storage-mode TEXT local | cloud_primary [default: local]
Infrastructure:
--redis-url TEXT Redis connection URL [default: redis://localhost:6379]
--kafka-broker TEXT Kafka broker address
--ipc-port INT TCP IPC listener port (enables remote mode)
--ipc-bind-addr TEXT TCP IPC bind address [default: 0.0.0.0]
Tool addresses:
--control-center-addr TEXT CC gRPC address (global fallback)
--the-eyes-addr TEXT The-Eyes HTTP address (global fallback)
--the-eyes-poll-interval INT Liveness poll interval in seconds [default: 10]
--silence-timeout INT CC silence timeout in seconds [default: 30]
Cloud — general:
--cloud TEXT aws | azure | gcp
Cloud — AWS:
--aws-bucket TEXT S3 bucket name
--aws-region TEXT S3 region (e.g., us-east-1)
Cloud — Azure:
--azure-container TEXT Azure container or file share name
--azure-account TEXT Azure storage account name
--azure-storage-type TEXT auto | blob | adls | files [default: auto]
Cloud — GCP:
--gcp-bucket TEXT GCP bucket name
--gcp-project TEXT GCP project ID (optional)
Remote / annotator:
--ma-core-addr TEXT Remote ma-core address for annotator machines (host:port)
--ipc-server-fingerprint TEXT SHA-256 TLS cert fingerprint (AA:BB:CC:...)
--annotator-id TEXT Annotator identity for this machine
--annotator-key TEXT Annotator key for this machine
--tenant-id TEXT Tenant identifier for cost attribution
Advanced:
--metadata-flush-interval INT Steps between cloud metadata flushes [default: 10]
--temp-session-dir PATH Temp dir for cloud_primary read-back [default: system temp]
View:
--show Print current configuration and exit
memory-archive config --show
memory-archive config --storage-mode cloud_primary --cloud aws --aws-bucket my-bucket --aws-region us-east-1Start ma-core.
memory-archive server start [OPTIONS]
Options:
--daemon Run ma-core in the background
--log-file PATH Log file path for daemon mode [default: ~/.memory-archive/ma-core.log]
--release Use release binary tier [default]
--debug Use debug binary tier
Binary resolution order: MA_CORE_BIN env var → ma-core on PATH → target/release/ma-core in workspace root → cargo run.
memory-archive server start --daemon
memory-archive server start --daemon --log-file /var/log/ma-core.logStop a running ma-core daemon. Reads the PID from ~/.memory-archive/ma-core.pid and sends SIGTERM (Linux/macOS) or calls taskkill (Windows).
memory-archive server stopView or stream ma-core logs.
memory-archive server logs [OPTIONS]
Options:
-n, --lines INT Number of recent lines to show [default: 50]
-f, --follow Stream new log output (like tail -f)
--log-file PATH Log file path [default: ~/.memory-archive/ma-core.log]
memory-archive server logs
memory-archive server logs -n 200 -f
memory-archive server logs --log-file /var/log/ma-core.log -fStart ma-kafka-producer for a session. Connects to Control-Center's WatchCommands gRPC stream and publishes events to the control-center-events Kafka topic. Development tool — not for production use.
memory-archive server kafka-bridge [OPTIONS]
Options:
-s, --session TEXT Session ID [required]
--cc-addr TEXT CC gRPC address (overrides config)
--kafka-broker TEXT Kafka broker address (overrides config)
--release Use release binary [default]
--debug Use debug binary
memory-archive server kafka-bridge \
--session "$SESSION_ID" \
--cc-addr localhost:50051 \
--kafka-broker localhost:9092Read the ma-core TLS server certificate from ~/.memory-archive/ipc-cert.pem and print the SHA-256 fingerprint in colon-separated uppercase hex (e.g., AA:BB:CC:DD:...).
memory-archive tls fingerprintThe image pane (top-left) shows the at-frame for the selected step along with its filename, pixel dimensions, and action type. An "Open fullscreen" button launches the frame in an external viewer (feh on Linux, open on macOS). The steps list (top-right) is a virtual-scrolling list of all captured steps with their checkbox status and converted command. The reasoning editor (bottom-left) contains a multi-line text input with a live word and character counter. The stats pane (bottom-right) shows the session name, truncated session ID, annotated and skipped counts, and a PillProgressBar with completion percentage.
| Icon | Status | Meaning |
|---|---|---|
⬜ |
Pending | Not yet visited in this session |
🔵 |
In Progress | Currently open for editing |
✅ |
Complete | Reasoning saved and step confirmed |
[-] |
Skipped | User explicitly chose to skip this step |
| Key | Action |
|---|---|
j / ↓ |
Move to next step |
k / ↑ |
Move to previous step |
PgDn |
Fast scroll down (5 steps) |
PgUp |
Fast scroll up (5 steps) |
e / Enter |
Open selected step for editing |
Space |
Toggle step accordion (expand/collapse) |
Ctrl+N |
Save current reasoning, mark step complete, advance to next step |
Ctrl+S |
Save current reasoning without advancing |
Ctrl+Z |
Undo last edit in reasoning editor |
Ctrl+Y |
Redo last undone edit |
u |
Revert reasoning editor to last saved content |
Tab |
Cycle focus between StepList and ReasoningEditor |
+ |
Zoom in on image preview |
- |
Zoom out on image preview |
f |
Fit image to pane dimensions |
? |
Open HelpOverlay (full keyboard shortcut reference) |
Ctrl+Q |
Quit (QuitConfirm overlay if unsaved changes exist) |
The ReasoningEditor triggers an autosave to reasoning.jsonl via ReasoningWriter every 2.5 seconds whenever the content has changed since the last save. Autosave fires the UpdateAnnotationProgress IPC notification, which updates annotated_steps in the Redis session hash. Autosave does not advance step status to complete; only Ctrl+N marks a step complete.
When the TUI opens for a session that has already been partially annotated, SessionLoader reads all entries from reasoning.jsonl and pre-populates each step's status:
- Steps with non-empty
reasoningand noskippedflag →✅ Complete - Steps with
skipped: true→[-] Skipped - Steps with an empty or missing entry →
⬜ Pending
The TUI automatically positions the cursor on the first ⬜ Pending step. This makes the TUI fully resumable after any interruption.
| Overlay | Trigger | Description |
|---|---|---|
JumpToStep |
Internal navigation | Numeric input to jump directly to any step number |
QuitConfirm |
Ctrl+Q with unsaved changes |
Prompts to confirm quit; offers discard, save, or cancel |
CrashRecovery |
TUI open with an in_progress step detected |
Prompts to resume or reset the in-progress step |
AnnotationComplete |
All steps have status Complete or Skipped | Offers "compile now" (launches CompilerApp immediately) or "compile later" |
HelpOverlay |
? |
Full keyboard shortcut reference panel |
The right pane displays the at frame for the selected step inline in the terminal. For mouse steps, the annotated PNG (with the red circle, arrow, and coordinate box) is shown. Controls:
+/-: zoom in/outf: fit image to pane
An external image viewer can be opened: feh on Linux, open on macOS, and os.startfile() on Windows (opens in the system default image viewer). If none of these are available, the "Open fullscreen" button is disabled and labelled "no image viewer available". In remote annotator mode, images are fetched from ma-core via RemoteFetcher over the TLS channel and cached in a local temp directory. While the annotator is writing reasoning for step N, the next step's images are prefetched in a background thread to eliminate visible fetch latency on step advance.
After annotation is complete (either via AnnotationComplete overlay or memory-archive compile), CompilerApp opens. It is a full-screen terminal text editor built as a CompilerScreen Textual widget. On open, run_compile generates a memory.md scaffold from reasoning.jsonl, inserting the step reasoning in document order with headers for each step. The editor supports all standard text editing operations.
CompilerStatusBar is displayed at the bottom, showing the current word count and save state (Saved / Unsaved). Autosave fires every 2.5 seconds when content has changed. On Ctrl+Q, the CompilerQuitOverlay prompts to save or discard. If the user saves and exits, the FinalizeMemory IPC message is sent to ma-core, which sets the session status to complete and applies a 90-day Redis TTL. The finalized memory.md and all session files remain accessible until the TTL expires.
When memory-archive annotator claim is used (remote mode), the TUI starts a daemon thread that fires HeartbeatClaim IPC every 5 minutes while the TUI is open. This refreshes the claim:{session_id} Redis key TTL (30-minute rolling window). If the HeartbeatClaim response is CLAIM_LOST (e.g., the claim expired due to network interruption), the TUI logs a warning but does not immediately close — work in progress is not discarded. The claim can be reclaimed via memory-archive annotator claim --session "$SESSION_ID" if the session is still available in the queue.
| Config key | CLI flag | Description | Mode | Default |
|---|---|---|---|---|
storage_path |
--storage-path |
Local memory storage directory | both | ~/memories |
storage_mode |
--storage-mode |
local or cloud_primary |
both | local |
redis_url |
--redis-url |
Redis connection URL | both | redis://localhost:6379 |
kafka_broker |
--kafka-broker |
Kafka broker address | cloud_primary | — |
ipc_port |
--ipc-port |
TCP IPC listener port (enables remote mode) | remote mode | — |
ipc_bind_addr |
--ipc-bind-addr |
TCP IPC bind address | remote mode | 0.0.0.0 |
ma_core_addr |
--ma-core-addr |
Remote ma-core address (host:port) for annotator machines | remote | — |
ipc_server_fingerprint |
--ipc-server-fingerprint |
SHA-256 TLS cert fingerprint (AA:BB:CC:...) | remote | — |
control_center_addr |
--control-center-addr |
CC gRPC global fallback address | both | — |
the_eyes_addr |
--the-eyes-addr |
The-Eyes HTTP global fallback address | both | — |
the_eyes_poll_interval_seconds |
--the-eyes-poll-interval |
The-Eyes liveness poll interval (seconds) | both | 10 |
silence_timeout_seconds |
--silence-timeout |
CC silence before graceful disconnect (seconds) | both | 30 |
metadata_flush_interval |
--metadata-flush-interval |
Steps between cloud metadata.json flushes | cloud_primary | 10 |
temp_session_dir |
--temp-session-dir |
Temp dir for cloud_primary annotation read-back | cloud_primary | system temp |
cloud.provider |
--cloud |
Cloud provider: aws / azure / gcp |
both | — |
cloud.aws.bucket |
--aws-bucket |
S3 bucket name | both | — |
cloud.aws.region |
--aws-region |
S3 region (e.g., us-east-1) |
both | — |
cloud.azure.account |
--azure-account |
Azure storage account name | both | — |
cloud.azure.container |
--azure-container |
Azure container or file share name | both | — |
cloud.azure.storage_type |
--azure-storage-type |
auto / blob / adls / files |
both | auto |
cloud.gcp.bucket |
--gcp-bucket |
GCP bucket name | both | — |
cloud.gcp.project |
--gcp-project |
GCP project ID (optional) | both | — |
annotator_id |
--annotator-id |
Annotator identity for this machine | annotator | — |
annotator_key |
--annotator-key |
Annotator key for this machine | annotator | — |
tenant_id |
--tenant-id |
Tenant ID for cost attribution | automated | — |
model.routing_policy |
config.json only | VLM routing policy: pinned / fallback / load_balance |
automated | pinned |
model.providers[] |
config.json only | Named provider configs with endpoint, key ref, priority, cost rates | automated | — |
model.requests_per_minute |
config.json only | VLM API rate limit (requests per minute) | automated | 60 |
model.token_budget_per_hour |
config.json only | Token budget per hour across all VLM calls | automated | 1000000 |
model.circuit_breaker_threshold |
config.json only | Non-retryable failures before circuit opens | automated | 5 |
model.circuit_breaker_reset_seconds |
config.json only | Seconds before half-open trial | automated | 60 |
model.max_retries |
config.json only | Retries on retryable errors | automated | 3 |
observability.metrics_port |
config.json only | Prometheus metrics endpoint port | both | 9091 |
observability.metrics_token |
config.json only | Bearer token for metrics endpoint | both | — |
observability.log_level |
config.json only | Log level per component | both | info |
observability.alert_webhook_url |
config.json only | Webhook URL for alert delivery | both | — |
observability.memory_warn_mb |
config.json only | ma-core RSS memory warning threshold (MB) | both | — |
observability.kafka_lag_warn |
config.json only | Kafka consumer lag alert threshold | cloud_primary | — |
observability.upload_queue_warn |
config.json only | Cloud upload queue depth alert threshold | both | — |
observability.ipc_push_queue_warn |
config.json only | IPC push queue depth alert threshold | both | — |
annotator_mgmt_port |
config.json only | Annotator management REST API port | both | 9002 |
These values are never stored in config.json and are read exclusively from the environment at runtime:
| Variable | Required when | Description |
|---|---|---|
MA_IPC_TOKEN |
ipc_port is configured |
Admin IPC authentication token |
MA_ANNOTATOR_MGMT_TOKEN |
annotator_mgmt_port is configured |
Annotator management REST API bearer token |
MEMORY_ARCHIVE_CONFIG |
Always (optional) | Override path to config.json |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
AWS storage, if not using IAM role | AWS credentials |
AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET |
Azure storage, if not using managed identity | Azure service principal credentials |
GOOGLE_APPLICATION_CREDENTIALS |
GCP storage, if not using ADC | Path to GCP service account JSON key |
{
"model": {
"routing_policy": "fallback",
"requests_per_minute": 120,
"token_budget_per_hour": 2000000,
"circuit_breaker_threshold": 5,
"circuit_breaker_reset_seconds": 60,
"max_retries": 3,
"providers": [
{
"id": "primary-gpt4o",
"backend": "generic",
"endpoint": "https://api.openai.com/v1/chat/completions",
"api_key_ref": "env:OPENAI_API_KEY",
"model_id": "gpt-4o",
"priority": 1,
"cost_per_million_input_tokens": 2.50,
"cost_per_million_output_tokens": 10.00
},
{
"id": "fallback-claude",
"backend": "generic",
"endpoint": "https://api.anthropic.com/v1/messages",
"api_key_ref": "env:ANTHROPIC_API_KEY",
"model_id": "claude-sonnet-4-6",
"priority": 2,
"cost_per_million_input_tokens": 3.00,
"cost_per_million_output_tokens": 15.00
}
]
}
}API key references use the env:VARIABLE_NAME syntax. The secrets.py module resolves these at runtime. Plaintext keys in config are not supported.
{
"storage_backends": [
{
"name": "aws-us-east",
"provider": "aws",
"bucket": "memories-us-east",
"region": "us-east-1"
},
{
"name": "azure-eu-west",
"provider": "azure",
"account": "memoriesstorage",
"container": "memories-eu",
"storage_type": "blob"
},
{
"name": "gcp-asia",
"provider": "gcp",
"bucket": "memories-asia",
"project": "my-gcp-project"
},
{
"name": "local-fallback",
"provider": "local",
"path": "~/memories"
}
],
"storage_routing_rules": [
{
"match": { "tenant_prefix": "eu-" },
"backend": "azure-eu-west"
},
{
"match": { "tenant_prefix": "asia-" },
"backend": "gcp-asia"
},
{
"match": { "mode": "automated" },
"backend": "aws-us-east"
},
{
"match": {},
"backend": "local-fallback"
}
]
}Rules are evaluated top-to-bottom; the first matching rule wins. An empty match object is a wildcard (default route).
| Key pattern | Type | Description | TTL |
|---|---|---|---|
session:{session_id} |
Hash | All session fields (see below) | None (managed by status transitions) |
claim:{session_id} |
String | {annotator_id}:{claim_id} |
30 min (refreshed by heartbeat) |
annotator:{annotator_id} |
Hash | Credential hash, status, claim counts, allowed tenants | None |
sessions:active |
Set | Session IDs with status active |
None |
sessions:pending |
Set | Session IDs with status pending_annotation |
None |
sessions:pending_human_annotation |
Set | Session IDs with status pending_human_annotation |
None |
sessions:annotating |
Set | Session IDs with status annotating |
None |
sessions:pending_compilation |
Set | Session IDs with status pending_compilation |
None |
sessions:by_os:{OS_TYPE} |
Set | Session IDs indexed by OS type | None |
sessions:by_mode:{mode} |
Set | Session IDs indexed by capture mode | None |
session_id UUID string
mode manual | automated
status active | pending_annotation | pending_human_annotation |
annotating | pending_compilation | complete |
incomplete | reasoning_degraded
ma_core_addr ma-core IPC address that owns this session
tenant_id tenant identifier (optional)
os_type LINUX | WINDOWS | MACOS
os_version string (e.g., "Ubuntu 24.04 LTS")
os_architecture e.g., x86_64
os_environment_id opaque environment identifier
capture_server_id The-Eyes server identifier
capture_server_addr The-Eyes HTTP address (per-session override or global config)
actuation_server_id Control-Center server identifier
the_eyes_addr The-Eyes HTTP address
reasoning_model_id VLM model ID used (automated mode)
memory_name directory name for this memory
memory_path full path to memory directory
created_at ISO 8601 timestamp
updated_at ISO 8601 timestamp
total_steps integer
annotated_steps integer
skipped_steps integer
kafka_partition integer (cloud_primary mode)
kafka_offset integer (cloud_primary mode, last committed)
storage_backend name of pinned storage backend
model_provider primary VLM provider ID
model_endpoint primary VLM endpoint
model_api_key_ref primary VLM API key reference (env:VAR_NAME)
fallback_model_provider fallback VLM provider ID (optional)
fallback_model_endpoint fallback VLM endpoint (optional)
fallback_api_key_ref fallback VLM API key reference (optional)
context_window_steps steps of context sent to VLM per request
| Status | TTL | Notes |
|---|---|---|
active |
None | Removed from set on status transition |
pending_annotation |
None | Removed from set on annotating transition |
pending_human_annotation |
None | Removed from set when claimed |
annotating |
None (claim TTL: 30 min rolling) | Claim expires if heartbeat stops |
pending_compilation |
None | Removed on FinalizeMemory |
complete |
90 days | Set at FinalizeMemory |
incomplete |
7 days | Set at disconnect without done |
reasoning_degraded |
None | Transitions to pending_human_annotation on completion |
When a session is registered, per-session capture_server_addr and actuation_server_addr fields can override the global control_center_addr and the_eyes_addr from config.json. These per-session addresses are stored in the Redis hash at registration. All subsequent capture loop operations (gRPC stream connection, The-Eyes HTTP requests) use the per-session address. This allows a single ma-core to serve sessions on different hosts — for example, 10,000 sessions each on a different VM/container running its own The-Eyes and Control-Center. The ma_session_server_address_source Prometheus counter distinguishes per-session overrides from global config usage.
In cloud_primary mode, each session is assigned a Kafka partition based on hash(session_id) % num_partitions. The partition and the last-committed offset are stored in the Redis session hash as kafka_partition and kafka_offset. Offsets are committed only after the corresponding step has been successfully written to the storage backend — guaranteeing at-least-once processing. On crash recovery, replay_session_events() creates a dedicated BaseConsumer with a unique consumer group, manually assigns the stored partition, seeks to kafka_offset + 1, and replays from that point. Events already processed (before the crash) are re-received but are idempotent to write (same content, same path, atomic rename).
A claim is created by a ClaimSession IPC message. ma-core atomically sets claim:{session_id} in Redis with {annotator_id}:{claim_id} and a 30-minute TTL, and moves the session from sessions:pending_human_annotation to sessions:annotating. The claiming annotator must send HeartbeatClaim IPC every 5 minutes to refresh the TTL. If the claim expires (no heartbeat for 30 minutes), the session returns to sessions:pending_human_annotation and becomes available for other annotators. A ClaimConflict response is returned if another annotator claims the same session concurrently (Redis atomic SETNX). ReleaseSession IPC explicitly removes the claim and returns the session to the queue.
On ma-core startup, two sweeps run serially before the IPC server accepts connections:
-
Startup sweep: Iterates over all sessions in
sessions:active. For each, checks whether the PID that registered the session is still alive. If not, transitions the session toincompleteand renames the memory directory (local mode only). -
Reconcile sweep: Iterates over all sessions in
sessions:annotating. For each, checks whether the associatedclaim:{session_id}key still exists in Redis. If the claim has expired (TTL elapsed), returns the session tosessions:pending_human_annotation. This handles the case wherema-corewas restarted while an annotator had an active claim.
The Prometheus metrics endpoint is available at http://{host}:{metrics_port}/metrics (default port 9091). If observability.metrics_token is set in config.json, requests must include an Authorization: Bearer <token> header.
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_active_sessions |
Gauge | — | Currently active sessions |
ma_steps_total |
Counter | — | Total steps captured (use rate() for steps/sec) |
ma_cloud_upload_queue_depth |
Gauge | — | Files pending upload in SyncWorker queue |
ma_cloud_upload_errors_total |
Counter | — | Permanent upload failures |
ma_ipc_push_queue_depth |
Gauge | — | Pending outbound IPC push messages |
ma_ipc_tcp_connections_active |
Gauge | — | Active remote TLS TCP IPC connections |
ma_kafka_consumer_lag |
Gauge | — | Kafka consumer lag (cloud_primary mode only) |
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_vlm_requests_total |
Counter | provider |
VLM API requests dispatched |
ma_vlm_request_latency_ms |
Histogram | provider, quantile |
Latency at p50, p95, p99 |
ma_vlm_errors_total |
Counter | provider, error_type |
VLM API errors by provider and error type |
ma_vlm_circuit_breaker_open |
Gauge | — | 1 if any session circuit is open, 0 otherwise |
ma_vlm_tokens_consumed_total |
Counter | — | Total tokens consumed across all VLM calls |
ma_sessions_reasoning_degraded |
Gauge | — | Sessions currently in reasoning_degraded status |
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_storage_backend_errors_total |
Counter | backend |
Storage backend operation errors |
ma_storage_routing_decisions_total |
Counter | backend |
Sessions routed to each named backend |
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_pricing_registry_fetch_status |
Counter | status |
Fetch outcomes: success, signature_failure, network_failure, cache_hit |
ma_pricing_registry_age_seconds |
Gauge | — | Age of the current pricing manifest in seconds |
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_annotator_active_claims |
Gauge | annotator_id |
Current concurrent claims per annotator |
ma_annotator_auth_failures_total |
Counter | annotator_id |
Cumulative authentication failures per annotator |
| Metric | Type | Labels | Description |
|---|---|---|---|
ma_session_server_address_source |
Counter | source |
Sessions using per_session vs. global_config addresses |
All alerts are delivered via the observability.alert_webhook_url webhook as JSON POST requests.
| Condition | Severity | Trigger |
|---|---|---|
| Cloud upload permanently failed | ERROR | Upload exhausts all retries |
| VLM circuit breaker opened | WARNING | Per-session circuit opens |
| All fallback chain providers degraded | ERROR | Primary + fallback both in open state |
| Redis connection lost | CRITICAL | Redis ping fails |
ma-core RSS memory above memory_warn_mb |
WARNING | RSS exceeds configured threshold |
Kafka consumer lag above kafka_lag_warn |
WARNING | ma_kafka_consumer_lag exceeds threshold |
Cloud upload queue above upload_queue_warn |
WARNING | ma_cloud_upload_queue_depth exceeds threshold |
IPC push queue above ipc_push_queue_warn |
WARNING | ma_ipc_push_queue_depth exceeds threshold |
| Storage backend unreachable at registration | ERROR | Health check at registration time fails |
| Pricing registry signature verification failed | ERROR | Ed25519 verification fails on manifest |
| Pricing registry manifest older than 7 days | WARNING | ma_pricing_registry_age_seconds > 604800 |
| Annotator auth failures > 10 in 60 seconds | WARNING | Possible brute-force attempt |
ma-core emits structured JSON logs via the tracing crate. Each log entry includes:
{
"timestamp": "2026-04-09T12:01:04.286Z",
"level": "INFO",
"target": "ma_core::capture::stream",
"session_id": "f83a1d2c-...",
"message": "Step 0042 captured",
"fields": {
"action_type": "mouse_click",
"step_id": 42,
"upload_queued": true
}
}Log destinations: stdout (foreground mode) or the configured --log-file path (daemon mode). Per-component log levels are set in config.json under observability.log_level:
{
"observability": {
"log_level": {
"ma_core::capture": "debug",
"ma_core::storage": "info",
"ma_core::ipc": "warn"
}
}
}Alerts are delivered as HTTP POST requests to observability.alert_webhook_url:
{
"severity": "ERROR",
"condition": "cloud_upload_permanent_failure",
"session_id": "f83a1d2c-...",
"detail": "File vision/frames/step_0042_at.png failed after 5 retries",
"timestamp": "2026-04-09T12:01:04.286Z"
}The webhook target can be any HTTP endpoint — Slack incoming webhook, PagerDuty Events API, custom alertmanager, etc.
StorageRouter (storage/router.rs) is evaluated once per session registration. It receives a SessionRegistrationRequest containing tenant_id, mode, os_type, and any routing hints from the registration call. It evaluates the routing rules array top-to-bottom and returns the name of the first matching backend. The backend name is written to storage_backend in the Redis session hash and is never re-evaluated for the lifetime of the session.
Routing rule DSL (config.json):
{
"match": {
"tenant_prefix": "eu-", // tenant_id must start with this string
"region_tag": "eu-west", // session-level region hint
"mode": "automated", // session mode
"os_type": "LINUX" // OS type
},
"backend": "azure-eu-west"
}All conditions in a match object are ANDed. An empty match object matches any session and acts as a default route. Multiple named backends (e.g., aws-us-east, azure-eu-west, gcp-asia, local-dev) can coexist per ma-core instance. Each backend has its own credential configuration, resolved from separate environment variables.
ma_storage_routing_decisions_total{backend} tracks which backend each session was routed to. ma_storage_backend_errors_total{backend} tracks errors per backend.
ModelRouter is stateless — it holds no per-session state. It applies one of three routing policies:
pinned: Always selects the provider with the lowestpriorityvalue (highest priority). Ignores all other providers.fallback: Selects the highest-priority provider. If the per-session circuit breaker for that provider is open (tracked inReasoningPipeline, notModelRouter), selects the next-highest-priority provider. Maximum two providers per session.load_balance: Distributes requests across providers using weighted round-robin based onpriorityvalues. All providers share a single per-session circuit breaker.
Per-session circuit breaker state lives in ReasoningPipeline._SessionState. State transitions:
- Closed: normal operation; failures counted.
- Open: circuit tripped (threshold reached); all requests immediately return
model_degraded; timer starts. - Half-open: after
circuit_breaker_reset_seconds, one trial request is sent. If it succeeds: → Closed, failure count reset. If it fails: → Open, timer resets.
When both the primary and fallback circuits open for a session, ReasoningPipeline sends a ReasoningDegraded IPC message to ma-core, which transitions the session to reasoning_degraded in Redis. Capture continues; steps written after degradation carry source: model_degraded in reasoning.jsonl.
The pricing registry (model/pricing.py) resolves token cost rates in three tiers:
Tier 1 — MA-hosted Ed25519-signed manifest:
{
"generated_at": "2026-04-01T00:00:00Z",
"models": [
{
"model_id": "gpt-4o",
"aliases": ["gpt-4o-2024-11-20", "gpt-4o-2025-01-01"],
"input_cost_per_million": 2.50,
"output_cost_per_million": 10.00
}
],
"signature": "<Ed25519 signature over the JSON body>"
}The manifest is fetched from a hardcoded URL at startup, cached in ~/.memory-archive/pricing-cache.json, and re-used for 24 hours. On cache load, the signature is verified against the hardcoded public key. A failed verification deletes the cache, fires an ERROR alert, and triggers a fresh fetch. If the fresh fetch also fails verification, the system falls through to Tier 2 or Tier 3. The manifest age is tracked by ma_pricing_registry_age_seconds.
Tier 2 — AWS Bedrock Pricing API:
For models hosted on AWS Bedrock, pricing:GetProducts is queried against the Bedrock service code. This IAM action must be in a separate policy from inference permissions. Bedrock pricing data is used when the model ID matches a known Bedrock model pattern and the manifest does not contain an entry for it.
Tier 3 — Configured cost rates:
cost_per_million_input_tokens and cost_per_million_output_tokens fields in ProviderConfig (config.json). Used as a final fallback for any model not found in the manifest or Bedrock pricing.
Model alias resolution: the pricing registry first checks model_id exactly, then checks aliases[] for each manifest entry. First match wins. This handles version suffixes (e.g., gpt-4o-2024-11-20 resolves to the gpt-4o entry).
In large deployments where each OS environment runs its own The-Eyes and Control-Center on a distinct host, every session registration call includes --capture-server-addr and --actuation-server-addr overrides. These values are stored in Redis at capture_server_addr and the_eyes_addr fields. When the capture loop starts for a session, it reads the per-session address from Redis rather than the global config. This requires no ma-core reconfiguration as new OS environments are added — each session simply declares its own endpoints.
ma_session_server_address_source{source="per_session"} vs. {source="global_config"} lets operators verify that all production sessions are using per-session addressing and that no session is accidentally falling back to a stale global config address.
ma-core exposes a REST API on annotator_mgmt_port (default 9002) for annotator lifecycle management. All endpoints require Authorization: Bearer <MA_ANNOTATOR_MGMT_TOKEN>.
| Method | Path | Description |
|---|---|---|
POST |
/v1/annotators |
Register a new annotator |
GET |
/v1/annotators |
List all annotators |
GET |
/v1/annotators/{annotator_id} |
Get a single annotator |
DELETE |
/v1/annotators/{annotator_id} |
Deactivate an annotator |
POST |
/v1/annotators/{annotator_id}/rotate-key |
Rotate annotator key |
POST /v1/annotators request:
{
"annotator_id": "alice",
"allowed_tenant_ids": ["acme-corp", "beta-labs"],
"max_concurrent_claims": 3
}POST /v1/annotators response:
{
"annotator_id": "alice",
"key": "<plaintext key — shown once>",
"status": "active"
}The REST API is an alternative to the annotator-admin CLI commands and enables programmatic annotator lifecycle management from CI/CD systems, orchestration layers, or admin dashboards.
ma-kafka-producer is a Rust binary in the workspace that bridges a single Control-Center WatchCommands gRPC stream to the control-center-events Kafka topic. It is intended for development and testing — specifically, for running automated mode tests against a live Control-Center without a full production Kafka-native CC deployment. In production, Control-Center publishes directly to Kafka.
memory-archive server kafka-bridge \
--session "$SESSION_ID" \
--cc-addr localhost:50051 \
--kafka-broker localhost:9092Each event published by ma-kafka-producer is wrapped in a KafkaEnvelope:
{
"schema_version": "1.0",
"session_id": "<session_id>",
"event_type": "command_event",
"timestamp": "2026-04-09T12:01:04.286Z",
"payload": { ... }
}The partition key is session_id, guaranteeing ordered delivery within a session on the consumer side.
In cloud_primary mode, ma-core tracks kafka_partition and kafka_offset in Redis per session. The offset is committed after each step is successfully written to the storage backend. If ma-core crashes mid-session, on restart it runs the startup sweep, identifies the session as active with a stale PID, and invokes replay_session_events():
- A dedicated
BaseConsumeris created with a unique consumer group ID (not the sharedmemory-archive-workersgroup) to avoid disrupting other sessions. - The consumer manually assigns the stored Kafka partition.
- It seeks to
kafka_offset + 1(the first unprocessed offset). - It replays events from that point forward, writing each step atomically to storage.
- Each successful write updates
kafka_offsetin Redis. - When the end of the partition is reached (or the
doneevent is encountered), the replay completes and the session resumes its normal lifecycle.
Because all storage writes use atomic temp-file-then-rename, re-processing an event that was already written (in the window between the write and the offset commit) is safe — the rename is idempotent for the same content.
{memory_name}/
├── memory.md # Compiled output — the final deliverable
├── metadata.json # Full session metadata, token counts, step index
├── commands/
│ ├── raw_input.md # Raw command strings, [FAILED] prefix on failed
│ ├── converted_input.md # Human-readable (e.g., "Left-click at (960, 540)")
│ ├── actuation_commands.json # Full CommandEvent JSON array
│ └── cc_commands.json # Control-Center replay format
├── vision/
│ └── frames/
│ ├── step_0001_20260409_120104_before.webp
│ ├── step_0001_20260409_120104_at.png # Always PNG (re-encoded after marker.rs annotation)
│ ├── step_0001_20260409_120104_after.webp
│ └── closing_state.webp # Final screen state on session done
└── reasoning/
└── reasoning.jsonl # One JSON entry per step
An incomplete session (disconnect without done) has its directory renamed to {memory_name} (incomplete)/ in local mode. In cloud_primary mode, the incomplete flag is stored in metadata.json and in the Redis session hash.
Last Updated: April 2026
Developer: Kartik (NullVoider)
Memory Archive is released under the Apache 2.0 License.
Memory Archive was built from scratch as the data collection and annotation layer for Computer Use Agent (CUA) workflows. Every file format, and every pipeline decision — three frames per step, atomic writes, per-session circuit breakers, signed pricing — was designed around the real constraints of producing reliable training data at scale: data that is structurally consistent, crash-recoverable, and usable from pre-training through inference without format translation.
The tool operates as one part of a three-component CUA stack alongside The-Eyes (vision capture) and Control-Center (desktop actuation), with each component designed to be independently deployable and observable. Memory Archive sits outside the OS environment entirely — read-only by design — recording what the other two components do and turning those recordings into memories a CUA can follow.
Memory Archive — Training data infrastructure for the AI age 🗄️

