Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ API_PORT=5000
DASHBOARD_API_PORT=5001
WEB_ADMIN_PORT=5002

# -----------------------------------------------------------------------------
# Memory Retention Intervals (days per layer before archival eligibility)
# -----------------------------------------------------------------------------
# Memories below confidence threshold with low access are archived after
# exceeding their layer's retention period. L4_HEURISTIC is never archived.
RETENTION_L0_RAW_DAYS=30
RETENTION_L1_CONTEXT_DAYS=90
RETENTION_L2_SUMMARY_DAYS=180
RETENTION_L3_KNOWLEDGE_DAYS=365

# -----------------------------------------------------------------------------
# Feature Flags (Production Defaults)
# -----------------------------------------------------------------------------
Expand Down
34 changes: 31 additions & 3 deletions SerialMemory.EventSourcing/Maintenance/MaintenanceWorkers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,38 @@ private async Task<int> ArchiveColdMemoriesAsync(NpgsqlConnection conn, Cancella
// ArchiveMemoryCommand per memory. This bypasses event sourcing for performance --
// archiving cold memories is a bulk housekeeping operation. The aggregate cycle log
// records how many rows were archived.
//
// Layer-specific retention intervals:
// L0_RAW = 30 days (raw input, ephemeral)
// L1_CONTEXT = 90 days (processed notes)
// L2_SUMMARY = 180 days (synthesized concepts)
// L3_KNOWLEDGE = 365 days (validated learnings)
// L4_HEURISTIC = indefinite (never archived)
var archivedCount = await conn.ExecuteAsync(new CommandDefinition(@"
UPDATE memory_projections
SET is_archived = TRUE
WHERE is_active = TRUE
AND is_archived = FALSE
AND confidence_score < @ArchiveThreshold
AND access_count < @MinAccessCount
AND last_accessed_at < NOW() - @ColdPeriod::INTERVAL
AND layer NOT IN ('L3_KNOWLEDGE', 'L4_HEURISTIC')",
AND layer != 'L4_HEURISTIC'
AND last_accessed_at < NOW() - make_interval(days =>
CASE layer
WHEN 'L0_RAW' THEN @L0RetentionDays
WHEN 'L1_CONTEXT' THEN @L1RetentionDays
WHEN 'L2_SUMMARY' THEN @L2RetentionDays
WHEN 'L3_KNOWLEDGE' THEN @L3RetentionDays
ELSE @L0RetentionDays
END
)",
new
{
ArchiveThreshold = _config.ArchiveConfidenceThreshold,
MinAccessCount = _config.MinAccessCountForRetention,
ColdPeriod = $"{_config.ColdPeriodDays} days"
L0RetentionDays = _config.L0RawRetentionDays,
L1RetentionDays = _config.L1ContextRetentionDays,
L2RetentionDays = _config.L2SummaryRetentionDays,
L3RetentionDays = _config.L3KnowledgeRetentionDays
},
cancellationToken: cancellationToken));

Expand Down Expand Up @@ -327,6 +345,16 @@ public sealed class MaintenanceConfig
public int ReinforceIntervalDays { get; set; } = 7;
public float DuplicateSimilarityThreshold { get; set; } = 0.95f;

/// <summary>
/// Per-layer retention intervals in days. Memories are eligible for archival
/// only after they exceed their layer's retention period.
/// L4_HEURISTIC is always excluded (indefinite retention).
/// </summary>
public int L0RawRetentionDays { get; set; } = 30;
public int L1ContextRetentionDays { get; set; } = 90;
public int L2SummaryRetentionDays { get; set; } = 180;
public int L3KnowledgeRetentionDays { get; set; } = 365;

public static MaintenanceConfig Default => new();
}

Expand Down
20 changes: 19 additions & 1 deletion SerialMemory.Worker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,29 @@

// 1. Memory Maintenance Worker - handles decay, archiving, reinforcement
// Uses batch SQL operations directly (bypasses per-item event sourcing for performance).
var maintenanceConfig = new MaintenanceConfig
{
L0RawRetentionDays = int.TryParse(
builder.Configuration["RETENTION_L0_RAW_DAYS"] ?? Environment.GetEnvironmentVariable("RETENTION_L0_RAW_DAYS"),
out var l0) ? l0 : 30,
L1ContextRetentionDays = int.TryParse(
builder.Configuration["RETENTION_L1_CONTEXT_DAYS"] ?? Environment.GetEnvironmentVariable("RETENTION_L1_CONTEXT_DAYS"),
out var l1) ? l1 : 90,
L2SummaryRetentionDays = int.TryParse(
builder.Configuration["RETENTION_L2_SUMMARY_DAYS"] ?? Environment.GetEnvironmentVariable("RETENTION_L2_SUMMARY_DAYS"),
out var l2) ? l2 : 180,
L3KnowledgeRetentionDays = int.TryParse(
builder.Configuration["RETENTION_L3_KNOWLEDGE_DAYS"] ?? Environment.GetEnvironmentVariable("RETENTION_L3_KNOWLEDGE_DAYS"),
out var l3) ? l3 : 365,
};

builder.Services.AddSingleton(maintenanceConfig);
builder.Services.AddHostedService<MemoryMaintenanceWorker>(sp =>
new MemoryMaintenanceWorker(
pgConnectionString,
sp.GetRequiredService<IRetrievalEngine>(),
sp.GetRequiredService<ILoggerFactory>().CreateLogger<MemoryMaintenanceWorker>()));
sp.GetRequiredService<ILoggerFactory>().CreateLogger<MemoryMaintenanceWorker>(),
sp.GetRequiredService<MaintenanceConfig>()));

// 2. Projection Host - processes events and updates read models
// Wrapped in a BackgroundService since ProjectionHost doesn't implement IHostedService
Expand Down
Loading