Skip to content

Retention interfaces

Python, CLI, REST, MCP, and the scheduler all call RetentionService. The service owns validation, namespace/agent scope, policy and rule operations, revision checks, schedule lifecycle, execution, and audit projections. The adapters supply authentication context and translate input/output formats.

initiated_by records who requested a run or schedule change. It is audit attribution, not a user-memory filter. Scheduled jobs preserve this identity when admitted. The REST adapter derives it from the authenticated user.

Python

Use client.retention(namespace_id, agent_id=...), or import RetentionService and RetentionError from altk_evolve.retention:

from altk_evolve.frontend.client.evolve_client import EvolveClient

client = EvolveClient()
retention = client.retention("service-1", agent_id="agent-1")
retention.create_policy("memory-retention", name="Memory retention")
retention.add_rule(
    "memory-retention", "old-memories",
    {"max_age_days": 90, "action": "delete"},
)

schedule = retention.create_schedule(
    "nightly",
    {
        "policy_id": "memory-retention",
        "agent_id": "agent-1",
        "dry_run": True,
        "spec": {"schedule": "0 2 * * *", "timeZone": "America/Los_Angeles"},
    },
    initiated_by="alice",
)
shown = retention.get_schedule("nightly")  # configuration + next_runs
stopped = retention.stop_schedule(
    "nightly", initiated_by="alice", expected_revision=shown["revision"],
)
retention.start_schedule(
    "nightly", initiated_by="alice", expected_revision=stopped["revision"],
)
report = retention.run("memory-retention", initiated_by="alice")  # dry run
persisted = retention.get_run(report["run_id"])

Methods accept Python dictionaries and return Python dictionaries, not JSON strings. RetentionError exposes status (400 validation, 403 forbidden scope change, 404 missing, 409 conflict, or 500 execution failure) and payload(). Individual entity failures appear in the returned run report and persisted failed status; callers should inspect errors even when the overall request returns a report.

Policies are reusable namespace-wide definitions. Optional agent_id constrains schedules, jobs, and runs. Attempts to change a scoped schedule to another agent fail. get_schedule includes the next five nominal UTC times, or an empty list while suspended. get_job includes its persisted run when available.

Operation mapping

REST paths below are relative to /manage/retention under the host's chosen mount prefix. MCP argument names match Python method parameters except for explicit namespace_id and, on rule updates, changes.

Python method REST MCP tool
create_policy POST /policies create_retention_policy
get_policy GET /policies/{id} get_retention_policy
list_policies GET /policies list_retention_policies
update_policy PATCH /policies/{id} update_retention_policy
delete_policy DELETE /policies/{id} delete_retention_policy
add_rule POST /policies/{id}/rules add_retention_rule
list_rules GET /policies/{id}/rules list_retention_rules
update_rule PATCH /policies/{id}/rules/{name} update_retention_rule
remove_rule DELETE /policies/{id}/rules/{name} remove_retention_rule
create_schedule POST /schedules create_retention_schedule
get_schedule GET /schedules/{id} get_retention_schedule
list_schedules GET /schedules list_retention_schedules
update_schedule PATCH /schedules/{id} update_retention_schedule
delete_schedule DELETE /schedules/{id} delete_retention_schedule
start_schedule POST /schedules/{id}/start start_retention_schedule
stop_schedule POST /schedules/{id}/stop stop_retention_schedule
run POST /runs run_retention
get_run GET /runs/{id} get_retention_run
list_runs GET /runs list_retention_runs
get_job GET /jobs/{id} get_retention_job
list_jobs GET /jobs list_retention_jobs
cancel_job POST /jobs/{id}/cancel cancel_retention_job
recover_job POST /jobs/{id}/acknowledge-interrupted acknowledge_interrupted_retention_job

Full-document replacement is also available as put_policy / PUT /policies/{id} / put_retention_policy and put_schedule / PUT /schedules/{id} / put_retention_schedule. Schedule PUT creates only at revision zero and otherwise requires the observed revision. These use the same service as the explicit create/update operations. validate_policy and validate_retention_policy validate a complete policy without storage access.

REST bodies

Create a policy:

{"policy_id": "memory-retention", "name": "Memory retention", "enabled": true}

Add a rule at /policies/memory-retention/rules:

{"name": "old-memories", "rule": {"max_age_days": 90, "action": "delete"}}

Update that rule at /policies/memory-retention/rules/old-memories:

{"changes": {"max_age_days": 180}}

Create a schedule:

{
  "schedule_id": "nightly",
  "definition": {
    "policy_id": "memory-retention",
    "agent_id": "agent-1",
    "dry_run": true,
    "spec": {"schedule": "0 2 * * *", "timeZone": "America/Los_Angeles"}
  }
}

PATCH schedule bodies contain changes and expected_revision. Start/stop bodies contain only expected_revision. DELETE supplies expected_revision as a query parameter. Run bodies contain policy_id, optional dry_run, and optional additional_matches. Recovery requires {"worker_stopped": true} after confirming that the owning process stopped.

Create/add requests return 201, reads and mutations return 200, malformed HTTP bodies return 422, and service failures use the service's status with {"detail": {"error": "..."}}. REST run responses and persisted audits omit memory content; programmatic/MCP immediate reports can include pre-action previews for trusted integrations.

The host injects an authenticated MemoryScope; namespace and initiator are not accepted from request bodies. Administrative routes require management permission. Python and MCP callers are trusted integrations and must authenticate and authorize their supplied namespace, initiator, and agent scope. See embedding.

MCP inputs

New tools take structured objects. For example, create_retention_schedule receives schedule_id, namespace_id, initiated_by, and definition as an object. update_retention_rule receives an object in changes. Complete-policy/schedule PUT tools and run_retention also accept structured objects; their earlier JSON-string forms remain accepted by the transport adapter. Business operations never parse transport JSON.

MCP tools return JSON text; errors contain error and optional details. The separate preview tool/REST endpoint is removed: retrieve the schedule to see upcoming times.

Runtime

Execution runs directly through the shared service, without invoking MCP from Python, REST, the CLI, or the scheduler. The evolve-mcp launcher owns background scheduling. Embedded hosts attach retention_runtime(client) to their lifespan; CLI catalog commands do not start background execution. See scheduling.

PostgreSQL collection

Applied PostgreSQL runs now mark candidates durably and sweep them in separate, small transactions. mark alone never deletes a memory or invokes a hook. A flag rule creates a review candidate; a delete rule creates a pending deletion. Marking uses a persisted keyset cursor, so later runs advance past an already processed page. Repeated marks of the same entity version and policy are idempotent.

retention = client.retention("service-instance-id")
retention.mark("memory-retention", initiated_by="admin")
retention.list_candidates()
retention.sweep("memory-retention", initiated_by="admin")
retention.list_audit()

The matching CLI commands are evolve retention mark POLICY, sweep POLICY, candidates, and audit; supply --namespace and, for mutations, --initiated-by. REST adds POST /manage/retention/policies/{id}/mark and /sweep, plus GET /manage/retention/candidates and /audit. MCP exposes mark_retention, sweep_retention, list_retention_candidates, and list_retention_audit. These use the same service-instance namespace; an administrator identity is attribution, not a filter on memory owners. Omit agent scope for the entire service instance.

A sweep locks the candidate, policy, and current entity rows. Legal holds are checked directly; a held memory remains marked. A changed entity version or policy withdraws the old candidate for reevaluation by a later marking pass. Deletion and its audit receipt commit together on the same PostgreSQL connection. No deletion hook or external notification is part of this transaction. Ordinary non-retention memory operations retain their existing hooks.

Candidate and audit APIs contain references, statuses, policy identifiers, rule identifiers, timestamps, and operator attribution, never memory contents or titles. Marks retain PostgreSQL row versions, not hashes of memory contents. Audit events include a policy-definition hash identifying the applied revision. missing means an entity was already unavailable; it is never reported as a confirmed deletion. The UI resolves existing memories separately and groups committed outcomes; technical references are expandable administrative details.

Scheduled PostgreSQL jobs have a 10-second heartbeat. A heartbeat older than 60 seconds permits the scheduler to mark the old run interrupted and admit future occurrences. It never resumes that run. Already committed candidates and action receipts survive. An old executor may overlap with a later run; idempotent marks and transactional sweep claims make that overlap safe. Cancellation is checked between candidates. A sweep already inside its transaction finishes or rolls back.

This atomic collection API requires PostgreSQL. Other backends retain the existing immediate retention implementation. PostgreSQL applied runs use current policy eligibility and namespace/agent scope: historical as_of, arbitrary metadata filters, and externally computed deletion matches are rejected. CUGA's separate-database orphan-conversation criterion is therefore not advertised by the new collection UI. Dry-run evaluation remains available through run(dry_run=True).

Explicit source-deletion receipts

PostgreSQL policies may set source_deleted: true alongside an age/disuse threshold. Only memories with an acknowledged source deletion can match such a rule. Trusted hosts call client.retention(namespace).record_source_deletion(source_id, user_id=..., agent_id=..., deleted_at=...), MCP record_source_deletion, or the authenticated management endpoint POST /manage/retention/deleted-sources with those four fields. The REST namespace is injected by the host; callers need management permission. Delivery is idempotent. The host must record the event atomically with its source deletion and retry delivery after failures; absence from a listing is not evidence.

Matching requires exact namespace, metadata.user_id, metadata.agent_id, and metadata.thread_id (or session_id). Unknown provenance is kept. Memories created in or after the receipt's deletion second are kept, protecting source-ID reuse despite integer-second creation timestamps. Existing memories are not backfilled based on missing source records. Late-generated memories created after deletion are conservatively retained for ordinary age rules. Marking still records held memories; sweeping checks holds and current row versions. Receipts retain identifiers and timestamps only, never conversation content.

An existing policy can opt in with:

evolve retention policies rules add cuga-standard --namespace service-1 \
  --name orphaned-conversations --source-deleted --max-age-days 7 --action delete

Existing policies are not silently overwritten when a host updates its defaults.