refactor(retention): extract retention module from workspace-memory

Move retention constants and math to a focused src/retention.ts module:
- All half-life, reinforcement, dormancy constants
- TYPE_FACTOR, SOURCE_FACTOR, USER_IMPORTANCE_FACTOR
- RETENTION_TYPE_MAX (renamed from TYPE_MAX)
- calculateInitialStrength, calculateEffectiveHalfLife,
  calculateRetentionStrength, calculateDormantDays,
  calculateEffectiveAgeDays, reinforceMemory

No behavior changes. retention.ts imports only types from types.ts.
Workspace-memory.ts still owns storage, consolidation, and rendering.
This commit is contained in:
Ralph Chang
2026-04-30 17:28:31 +08:00
parent 36f00147ca
commit aa7cc6c60e
5 changed files with 166 additions and 159 deletions
+12 -14
View File
@@ -12,7 +12,13 @@ import { dataHome, extractionRejectionLogPath, migrationLogPath, workspaceKey, w
import { assessMemoryQuality, HARD_QUALITY_REASONS } from "../src/memory-quality.ts";
import { redactCredentials } from "../src/redaction.ts";
import { scanWorkspaceResidues } from "../src/workspace-cleanup.ts";
import { calculateRetentionStrength, renderWorkspaceMemory } from "../src/workspace-memory.ts";
import { renderWorkspaceMemory } from "../src/workspace-memory.ts";
import {
DORMANT_DECAY_MULTIPLIER,
RETENTION_TYPE_MAX,
WORKSPACE_DORMANT_AFTER_DAYS,
calculateRetentionStrength,
} from "../src/retention.ts";
import type { LongTermMemoryEntry, LongTermSource, LongTermType, PendingMemoryJournalStore, WorkspaceMemoryStore } from "../src/types.ts";
import { LONG_TERM_LIMITS, PROMOTION_RETRY_LIMITS } from "../src/types.ts";
@@ -65,14 +71,6 @@ type MigrationLogRecord = {
};
const TYPES: LongTermType[] = ["feedback", "decision", "project", "reference"];
const TYPE_MAX_FOR_DIAG: Record<LongTermType, number> = {
feedback: 10,
decision: 10,
project: 8,
reference: 6,
};
const WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG = 14;
const DORMANT_DECAY_MULTIPLIER_FOR_DIAG = 0.25;
const SUSPICIOUS_REASONS = [
"progress_snapshot",
"active_file_snapshot",
@@ -280,7 +278,7 @@ function retentionCandidatesForDiag(store: WorkspaceMemoryStore): {
for (const item of sorted) {
const count = typeCounts[item.entry.type] ?? 0;
const max = TYPE_MAX_FOR_DIAG[item.entry.type] ?? Infinity;
const max = RETENTION_TYPE_MAX[item.entry.type] ?? Infinity;
if (count >= max) {
typeCapped.push(item);
continue;
@@ -430,7 +428,7 @@ async function printWorkspaceHealth(input: {
const storedCount = active.filter(entry => entry.type === type).length;
const renderedCount = renderedEntries.filter(entry => entry.type === type).length;
const supersededCount = superseded.filter(entry => entry.type === type).length;
console.log(` ${type.padEnd(9)} stored=${String(storedCount).padEnd(3)} rendered=${String(renderedCount).padEnd(3)} typeCap=${TYPE_MAX_FOR_DIAG[type]} superseded=${supersededCount}`);
console.log(` ${type.padEnd(9)} stored=${String(storedCount).padEnd(3)} rendered=${String(renderedCount).padEnd(3)} typeCap=${RETENTION_TYPE_MAX[type]} superseded=${supersededCount}`);
}
console.log("");
@@ -452,16 +450,16 @@ async function printWorkspaceHealth(input: {
console.log("");
const wallDaysSinceActivity = daysSinceIso(store.lastActivityAt);
const dormantDiscountActive = wallDaysSinceActivity !== null && wallDaysSinceActivity > WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG;
const dormantDiscountActive = wallDaysSinceActivity !== null && wallDaysSinceActivity > WORKSPACE_DORMANT_AFTER_DAYS;
const dormantDaysPastGrace = wallDaysSinceActivity === null
? 0
: Math.max(0, wallDaysSinceActivity - WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG);
: Math.max(0, wallDaysSinceActivity - WORKSPACE_DORMANT_AFTER_DAYS);
console.log("Dormancy:");
console.log(` lastActivityAt: ${store.lastActivityAt ?? "(missing)"}`);
console.log(` wall days since activity: ${wallDaysSinceActivity === null ? "unknown" : wallDaysSinceActivity.toFixed(1)}`);
console.log(` dormant discount active: ${dormantDiscountActive ? "yes" : "no"}`);
console.log(` dormant days past grace: ${dormantDaysPastGrace.toFixed(1)}`);
console.log(` dormant multiplier: ${DORMANT_DECAY_MULTIPLIER_FOR_DIAG}`);
console.log(` dormant multiplier: ${DORMANT_DECAY_MULTIPLIER}`);
console.log("");
const highImportanceCount = active.filter(entry => entry.userImportance === "high").length;