Admin APIs

Admin APIs give platform administrators and auditors elevated access to Memory Service data — across all users, including archived resources. Most administrative resources are available under the /v1/admin/ path prefix. Episodic memory administration uses /admin/v1/... routes because memory APIs already reserve /v1/memories for agent-facing memory operations. Admin APIs require the admin or auditor role unless an endpoint documents a stricter role.

How Admin APIs Differ from Regular APIs

The regular Agent APIs are scoped to the authenticated caller: a user can only see their own conversations, their own attachments, and resources they have been explicitly shared. Admin APIs remove those ownership boundaries.

Key differences:

AspectRegular APIsAdmin APIs
ScopeCaller’s own resourcesAll users
Archived resourcesHiddenVisible (with archived=include)
Role requiredAny authenticated useradmin or auditor
Audit loggingNoYes — every call is logged
JustificationNot applicableOptional (or required, if configured)

Roles

There are three admin roles, each granting a different level of privilege:

RoleAccessDescription
adminRead + WriteFull administrative access across all users. Implies auditor and indexer.
auditorRead-onlyView any user’s conversations, attachments, and memories; search system-wide. Cannot modify data.
indexerIndex onlyRead full unindexed history entries system-wide and set or replace their searchable indexed text. Does not grant other Admin APIs.

Roles can be assigned via OIDC token roles, explicit user lists, or API key client IDs. See the Admin Access Configuration section of the configuration guide for details.

Justification and Audit Logging

Every admin API call is written to a structured audit log entry. This provides a tamper-evident record of who accessed what data and why.

Providing a Justification

All admin endpoints accept an optional justification parameter. For GET endpoints it is a query parameter; for write endpoints it may also appear in the request body.

# Query parameter (GET requests)
curl "http://localhost:8080/v1/admin/conversations?justification=Support+ticket+%231234" \
  -H "Authorization: Bearer <admin-token>"

# Request body (write requests)
curl -X PATCH "http://localhost:8080/v1/admin/conversations/{id}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{"archived": true, "justification": "User requested account cleanup per support ticket #789"}'

Requiring Justification

By default, justification is optional. Set --admin-require-justification (or MEMORY_SERVICE_ADMIN_REQUIRE_JUSTIFICATION=true) to make it mandatory — any admin call without a justification will receive a 400 Bad Request response. This is recommended for regulated environments.

Audit Log Format

Each audit log entry is emitted as a structured INFO-level log message with the prefix Admin audit and the following fields:

FieldDescription
callerThe admin’s user ID (or API key client ID)
roleThe admin role used (admin, auditor, indexer)
methodThe HTTP method (GET, POST, DELETE, etc.)
pathThe request path
statusThe HTTP response status code
clientIPThe client’s IP address
justificationThe justification string (if provided)

The log level is controlled via the MEMORY_SERVICE_LOG_LEVEL environment variable (default: info). To route admin audit entries to a separate sink, filter on the Admin audit message prefix in your log aggregation pipeline.

Reading Data Across Users

Admin and auditor endpoints mirror most regular APIs but operate system-wide. For example, listing conversations:

# List all conversations across all users
curl "http://localhost:8080/v1/admin/conversations" \
  -H "Authorization: Bearer <admin-token>"

# Filter to a specific user
curl "http://localhost:8080/v1/admin/conversations?userId=alice" \
  -H "Authorization: Bearer <admin-token>"

# Include archived conversations alongside active ones
curl "http://localhost:8080/v1/admin/conversations?archived=include" \
  -H "Authorization: Bearer <admin-token>"

# Show only archived conversations
curl "http://localhost:8080/v1/admin/conversations?archived=only" \
  -H "Authorization: Bearer <admin-token>"

# Filter by repeatable metadata predicates (combined with AND, up to 5)
curl "http://localhost:8080/v1/admin/conversations?metadata=status=waiting&metadata=env!=prod" \
  -H "Authorization: Bearer <admin-token>"

A request accepts at most five metadata filter expressions. Supported operators are = and !=. Metadata filter keys may contain alphanumeric characters, underscores, and hyphens ([A-Za-z0-9_-]); dots are rejected. Matching is exact and string-only, so missing, null, numeric, boolean, array, and object metadata values do not match either operator.

Admin gRPC metadata filtering

The admin gRPC API accepts the same typed ConversationMetadataFilter predicates as the agent API. The same capability check is required before sending metadata_filters — see the Quarkus gRPC client or Spring gRPC client for the capability check pattern. Once the version guard passes, build the admin request with equality and not-equal predicates:

import java.util.List;
import io.github.chirino.memory.grpc.v1.*;

AdminListConversationsRequest request = AdminListConversationsRequest.newBuilder()
    .setPage(PageRequest.newBuilder().setPageSize(20).build())
    .setJustification("Support ticket #1234")
    .addAllMetadataFilters(List.of(
        ConversationMetadataFilter.newBuilder()
            .setKey("status")
            .setComparison(ConversationMetadataComparison.CONVERSATION_METADATA_COMPARISON_EQUAL)
            .setValue("waiting")
            .build(),
        ConversationMetadataFilter.newBuilder()
            .setKey("env")
            .setComparison(ConversationMetadataComparison.CONVERSATION_METADATA_COMPARISON_NOT_EQUAL)
            .setValue("prod")
            .build()
    ))
    .build();

AdminListConversationsResponse response = adminConversationsClient.listConversations(request);

Similarly, entries, memberships, forks, attachments, episodic memories, and semantic search all have admin counterparts that bypass ownership checks. Admin conversation payloads expose the metadata map and an archived boolean so clients can inspect conversation state without depending on storage timestamps.

Getting Entries

Admins can retrieve entries from any conversation or fetch a specific entry by its ID:

# Get all entries from a conversation (including archived)
curl "http://localhost:8080/v1/admin/conversations/{conversationId}/entries" \
  -H "Authorization: Bearer <admin-token>"

# Get a specific entry by ID (works across all conversations)
curl "http://localhost:8080/v1/admin/entries/{entryId}" \
  -H "Authorization: Bearer <admin-token>"

The entry-by-ID endpoint is useful for audit trails, debugging, or when you have an entry UUID but don’t know which conversation it belongs to.

Episodic Memory Administration

Episodic memory admin routes use /admin/v1/... instead of /v1/admin/.... Read-only exploration endpoints allow admin or auditor; operational and destructive endpoints require admin.

EndpointRolePurpose
GET /admin/v1/memoriesadmin or auditorList latest memory rows across users
PUT /admin/v1/memoriesadminUpsert a memory in any namespace
PATCH /admin/v1/memoriesadminArchive a memory by namespace and key
GET /admin/v1/memories/{id}admin or auditorRead a retained memory row by UUID
POST /admin/v1/memories/searchadmin or auditorSearch memories across users or as a target user
GET /admin/v1/memory-namespacesadmin or auditorBrowse memory namespace trees
DELETE /admin/v1/memories/{id}adminDelete or tombstone a memory row
GET /admin/v1/memory-index/statusadminInspect memory vector index status
POST /admin/v1/memory-index/triggeradminTrigger a memory vector index run
GET /admin/v1/memory-usageadminInspect memory usage counters
GET /admin/v1/memory-usage/topadminTop memory usage ranked by fetch count or recency

These dedicated admin endpoints bypass the user-facing memories.filter Rego policy. The exception is POST /admin/v1/memories/search with as_user_id: that mode deliberately evaluates the filter as the target user with no administrative roles. Having an admin role does not broaden searches made through the public /v1/memories/search endpoint; public calls remain scoped to the authenticated caller’s own user namespace.

See Memories for request parameters and examples.

Memory Kind Versions and Migrations

Administrators create immutable memory kind versions that define how a memory value is projected into plaintext filter attributes. Each version owns one Rego program; the projected attributes are typed, replayable, and stored per-memory row. Operators can then migrate existing memories to a new version online, one row at a time, without stopping writes.

Kind Version Lifecycle

# 1. Create an immutable schema version
curl -X POST http://localhost:8080/admin/v1/memory-kinds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "name": "customer-profile/v2",
    "attributes": {
      "observedAt": "timestamp",
      "channel":    "string",
      "score":      "number",
      "active":     "boolean",
      "tags":       "string[]"
    },
    "projectionRego": "package memories.attributes\nattributes := {\"channel\": input.value.channel, \"score\": input.value.score}"
  }'

Response:

{
  "name": "customer-profile/v2",
  "attributes": {
    "observedAt": "timestamp",
    "channel": "string",
    "score": "number",
    "active": "boolean",
    "tags": "string[]"
  },
  "projectionRego": "package memories.attributes\nattributes := {\"channel\": input.value.channel, \"score\": input.value.score}",
  "writable": true,
  "createdAt": "2026-08-01T00:00:00Z"
}

Creation is idempotent: repeating the same request with an identical type map and source returns the existing resource. Any change to the type map or source under the same name returns 409 Conflict.

The projectionRego program receives only input.namespace, input.key, input.value, and input.index. Transient caller context such as JWT claims is not available, making the projection replayable from persisted data. All returned attribute fields must be declared in attributes; undeclared fields, explicit nulls, and non-finite numbers are rejected.

Supported attribute types:

TypeStored asSupported filtersSortable
stringJSON string$eq, $in, $existsYes
numberJSON number$eq, $in, $exists, $gte/$lteYes
booleanJSON boolean$eq, $in, $existsYes
timestampUTC RFC 3339 string$eq, $in, $exists, $gte/$lteYes (chronological)
string[]JSON string arraymembership $eq/$in, $existsNo
# 2. List all versions (filter by family with ?family=customer-profile)
curl "http://localhost:8080/admin/v1/memory-kinds?family=customer-profile" \
  -H "Authorization: Bearer <admin-token>"

# 3. Get a specific version (exact GET includes projectionRego; list responses omit it)
curl "http://localhost:8080/admin/v1/memory-kinds/customer-profile/v2" \
  -H "Authorization: Bearer <admin-token>"

Online Migration

A migration moves all memories currently on a source schema version to a target version, one row at a time, while the service continues accepting writes. The migration re-evaluates the target schema’s Rego program against each memory’s persisted namespace, key, value, and index and writes the new attributes atomically.

# Create a migration from v1 to v2 (optionally scoped by namespace prefix)
curl -X POST http://localhost:8080/admin/v1/memory-kind-migrations \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "source": "customer-profile/v1",
    "target": "customer-profile/v2",
    "namespace_prefix": ["user"]
  }'

Response:

{
  "id": "f1a2b3c4-d5e6-7890-abcd-111122223333",
  "source": "customer-profile/v1",
  "target": "customer-profile/v2",
  "state": "queued",
  "migrated_count": 0,
  "skipped_tombstone_count": 0,
  "vector_pending_count": 0,
  "created_at": "2026-08-01T00:00:00Z"
}

States: queuedrunningsucceeded / failed / canceled / canceling.

Only one active migration is allowed per source version at a time; a second POST for the same source returns 409 Conflict.

succeeded means all replayable rows with the source schema were cut over. vector_pending_count reports rows whose vector payload update was deferred to the background indexer.

# Poll migration progress
curl "http://localhost:8080/admin/v1/memory-kind-migrations/f1a2b3c4-d5e6-7890-abcd-111122223333" \
  -H "Authorization: Bearer <admin-token>"

# List all migrations
curl "http://localhost:8080/admin/v1/memory-kind-migrations" \
  -H "Authorization: Bearer <admin-token>"

# Cancel a running migration (already-migrated rows are kept on the target)
curl -X DELETE "http://localhost:8080/admin/v1/memory-kind-migrations/f1a2b3c4-d5e6-7890-abcd-111122223333" \
  -H "Authorization: Bearer <admin-token>"

To resume a canceled or failed migration, create a new migration with the same source and target. Retrying is naturally idempotent because only rows still on the source version are scanned.

Schema Version Admin Endpoint Reference

EndpointRolePurpose
POST /admin/v1/memory-kindsadminCreate an immutable schema version
GET /admin/v1/memory-kindsadminList versions (optional ?family=)
GET /admin/v1/memory-kinds/{family}/{version}adminGet one version (includes Rego source)
POST /admin/v1/memory-kind-migrationsadminCreate a migration job
GET /admin/v1/memory-kind-migrationsadminList all migration jobs
GET /admin/v1/memory-kind-migrations/{id}adminGet one migration job
DELETE /admin/v1/memory-kind-migrations/{id}adminRequest cancellation

Updating Conversations

Admins can update a conversation’s title and metadata by patching the conversation resource. Metadata uses top-level merge-patch semantics: provided non-null values replace complete top-level values, omitted keys remain unchanged, and keys set to null are removed.

curl -X PATCH "http://localhost:8080/v1/admin/conversations/{id}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "title": "Escalated support case",
    "metadata": {"status": "reviewed", "queue": null},
    "justification": "Support ticket #456"
  }'

The same endpoint archives and unarchives the conversation fork tree:

curl -X PATCH "http://localhost:8080/v1/admin/conversations/{id}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{"archived": false, "justification": "User accidentally archived the conversation — support ticket #456"}'

Set archived to true to archive the fork tree or false to unarchive it. All updates require the admin role, and the response is the updated conversation object.

Eviction: Permanently Removing Archived Data

Archiving keeps data recoverable. Eviction permanently removes resources that have been archived for longer than a specified retention period. This operation is irreversible and requires the admin role.

When to Use Eviction

  • Enforcing a data retention policy (e.g., purge conversations archived more than 90 days ago)
  • Satisfying a GDPR/right-to-erasure request
  • Reclaiming storage after bulk deletes

Synchronous Eviction

By default, the eviction call blocks until complete and returns 204 No Content:

curl -X POST "http://localhost:8080/v1/admin/evict" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "retentionPeriod": "P90D",
    "resourceTypes": ["conversations"],
    "justification": "Quarterly data cleanup per retention policy"
  }'

The retentionPeriod is an ISO 8601 duration. Common values:

ValueMeaning
P90D90 days
P1Y1 year
PT24H24 hours
P0DImmediately (all archived)

Streaming Eviction (SSE)

For large datasets, use ?async=true (or set Accept: text/event-stream) to receive progress updates as Server-Sent Events instead of blocking:

curl -X POST "http://localhost:8080/v1/admin/evict?async=true" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "retentionPeriod": "P90D",
    "resourceTypes": ["conversations"],
    "justification": "Quarterly data cleanup per retention policy"
  }'

The response stream emits progress events from 0 to 100:

data: {"progress": 0}

data: {"progress": 25}

data: {"progress": 75}

data: {"progress": 100}

If eviction fails mid-run, an error event is emitted:

event: error
data: {"error": "Database connection failed"}

The eviction runs in batches internally to minimize database lock contention.

Admin Attachment Management

Admins can list, inspect, download, and delete attachments across all users:

# List all attachments (optionally filter by user or status)
curl "http://localhost:8080/v1/admin/attachments?userId=alice&status=unlinked" \
  -H "Authorization: Bearer <admin-token>"

# Get a specific attachment's metadata (including archived)
curl "http://localhost:8080/v1/admin/attachments/{id}" \
  -H "Authorization: Bearer <admin-token>"

# Download attachment content; add disposition=inline or attachment when needed
curl "http://localhost:8080/v1/admin/attachments/{id}/content?disposition=attachment" \
  -H "Authorization: Bearer <admin-token>" \
  -o file.bin

# Get a signed download URL with the same disposition behavior
curl "http://localhost:8080/v1/admin/attachments/{id}/download-url?disposition=attachment" \
  -H "Authorization: Bearer <admin-token>"

# Delete any attachment (admin only)
curl -X DELETE "http://localhost:8080/v1/admin/attachments/{id}" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{"justification": "GDPR erasure request #999"}'

Attachment deletion uses reference-counting: the stored file blob is only removed when the last attachment record sharing it is deleted.

Admin Events Endpoint

The admin events endpoint streams all real-time events from all users, regardless of conversation membership:

curl -N -H "Authorization: Bearer $(get-admin-token)" \
  "http://localhost:8080/v1/admin/events?justification=Investigating+issue+%231234"

The justification value is optional by default and is logged for audit purposes when provided. If the server enables MEMORY_SERVICE_ADMIN_REQUIRE_JUSTIFICATION=true, admin event subscriptions without a justification are rejected with 400 Bad Request. See Real-Time Events for event format, kinds, and connection lifecycle details.

Admin Stats Summary Endpoint

GET /v1/admin/stats/summary returns a point-in-time inventory snapshot from the configured datastores. It is separate from the Prometheus-backed chart endpoints and works even when Prometheus is not configured.

The response includes nested totals for:

  • conversationGroups
  • conversations
  • entries
  • memories
  • outboxEvents

conversationGroups.oldestArchivedAt is the oldest updatedAt timestamp among archived conversations. memories.oldestArchivedAt is the oldest memory archive timestamp. outboxEvents is null when the outbox feature is disabled or unsupported for the active datastore.

Next Steps