Data Retention
Octopus implements a two-stage retention lifecycle for episodic memory — soft delete followed by hard purge — with configurable TTL per agent. Semantic and procedural memory are persistent until explicitly deleted. This page covers retention policies, background jobs, and the user erasure endpoint.
Episodic Memory Retention
Episodic memory is the only memory type with automatic time-based expiry. The retention lifecycle has three stages:
IsDeleted = 0. Recalled by MemoryOrchestrator on each turn.
EpisodicRetentionDays. IsDeleted = 1, DeletedAt = now(). Hidden from all queries by global filter.
Retention Background Job
public class EpisodicRetentionJob : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await RunRetentionPassAsync(ct);
await Task.Delay(TimeSpan.FromHours(1), ct); // Run hourly
}
}
private async Task RunRetentionPassAsync(CancellationToken ct)
{
using var scope = _services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<OctopusDbContext>();
// Stage 1: Soft-delete expired episodes
// (uses IgnoreQueryFilters to operate across all tenants)
var expiredEpisodes = await db.Episodes
.IgnoreQueryFilters()
.Where(e => !e.IsDeleted
&& e.EndedAt < DateTime.UtcNow.AddDays(-e.RetentionDays))
.ToListAsync(ct);
foreach (var ep in expiredEpisodes)
{
ep.IsDeleted = true;
ep.DeletedAt = DateTime.UtcNow;
}
// Stage 2: Hard-purge grace-period-elapsed soft-deleted episodes
var toPurge = await db.Episodes
.IgnoreQueryFilters()
.Where(e => e.IsDeleted
&& e.DeletedAt < DateTime.UtcNow.AddDays(-7)) // 7-day grace
.ToListAsync(ct);
db.Episodes.RemoveRange(toPurge);
await db.SaveChangesAsync(ct);
}
}
User Data Erasure (GDPR Right to Erasure)
The erasure endpoint immediately hard-deletes all episodic data for a specific user — bypassing TTL and the soft-delete grace period:
// DELETE /api/octopus/users/{userId}/memory?agentId={agentId}
public async Task EraseUserDataAsync(string userId, Guid agentId, Guid tenantId, CancellationToken ct)
{
using var tx = await _db.Database.BeginTransactionAsync(ct);
// Hard delete episode messages
var messageIds = await _db.EpisodeMessages
.IgnoreQueryFilters()
.Where(m => m.Episode.UserId == userId
&& m.Episode.AgentId == agentId
&& m.TenantId == tenantId)
.Select(m => m.MessageId)
.ToListAsync(ct);
await _db.Database.ExecuteSqlRawAsync(
"DELETE FROM Octopus_EpisodeMessages WHERE MessageId IN ({0})",
string.Join(",", messageIds.Select(id => $"'{id}'")));
// Hard delete episodes
await _db.Database.ExecuteSqlRawAsync(
"DELETE FROM Octopus_Episodes WHERE UserId = {0} AND AgentId = {1} AND TenantId = {2}",
userId, agentId, tenantId);
await tx.CommitAsync(ct);
}
Retention Policy Reference
| Memory Type | Default Retention | Configurable | Auto-Expiry |
|---|---|---|---|
| Working | Request duration only | No | N/A |
| Episodic | 90 days (soft delete) + 7 days grace (hard purge) | Yes — per agent | Yes — background job |
| Semantic | Permanent | No (manual delete only) | No |
| Procedural | Permanent (soft deactivate) | No | No |
The 7-day grace period between soft delete and hard purge allows administrators to recover accidentally expired episodes before they are permanently lost. Episodes in the grace period can be restored by setting IsDeleted = 0 and DeletedAt = NULL directly in SQL — a manual operation for extraordinary cases only.