mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
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:
+12
-14
@@ -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;
|
||||
|
||||
+1
-1
@@ -36,8 +36,8 @@ import {
|
||||
updateWorkspaceMemory,
|
||||
updateWorkspaceMemoryWithAccounting,
|
||||
renderWorkspaceMemory,
|
||||
reinforceMemory,
|
||||
} from "./workspace-memory.ts";
|
||||
import { reinforceMemory } from "./retention.ts";
|
||||
import {
|
||||
appendPendingMemories,
|
||||
clearPendingMemories,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "./types.ts";
|
||||
|
||||
// Retention decay model constants (v1.5)
|
||||
export const BASE_HALF_LIFE_DAYS = 45;
|
||||
export const REINFORCEMENT_HALFLIFE_FACTOR = 0.85;
|
||||
export const REINFORCEMENT_MAX_COUNT = 6;
|
||||
export const REINFORCEMENT_MIN_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
export const WORKSPACE_DORMANT_AFTER_DAYS = 14;
|
||||
export const DORMANT_DECAY_MULTIPLIER = 0.25;
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const TYPE_FACTOR = {
|
||||
reference: 1.0,
|
||||
project: 1.25,
|
||||
feedback: 2.25,
|
||||
decision: 2.5,
|
||||
} as const;
|
||||
|
||||
export const SOURCE_FACTOR = {
|
||||
compaction: 1.0,
|
||||
manual: 1.4,
|
||||
explicit: 2.0,
|
||||
} as const;
|
||||
|
||||
export const USER_IMPORTANCE_FACTOR = {
|
||||
low: 0.7,
|
||||
normal: 1.0,
|
||||
high: 1.5,
|
||||
} as const;
|
||||
|
||||
export const RETENTION_TYPE_MAX = {
|
||||
feedback: 10,
|
||||
decision: 10,
|
||||
project: 8,
|
||||
reference: 6,
|
||||
} as const;
|
||||
|
||||
export function calculateInitialStrength(memory: LongTermMemoryEntry): number {
|
||||
const typeFactor = TYPE_FACTOR[memory.type] ?? 1.0;
|
||||
const sourceFactor = SOURCE_FACTOR[memory.source] ?? 1.0;
|
||||
const importanceFactor = USER_IMPORTANCE_FACTOR[memory.userImportance ?? "normal"] ?? 1.0;
|
||||
|
||||
return typeFactor * sourceFactor * importanceFactor;
|
||||
}
|
||||
|
||||
export function calculateEffectiveHalfLife(memory: LongTermMemoryEntry): number {
|
||||
const reinforcementCount = Math.min(
|
||||
memory.reinforcementCount ?? 0,
|
||||
REINFORCEMENT_MAX_COUNT,
|
||||
);
|
||||
const factor = Math.pow(REINFORCEMENT_HALFLIFE_FACTOR, reinforcementCount);
|
||||
return BASE_HALF_LIFE_DAYS / factor;
|
||||
}
|
||||
|
||||
function timestampMs(value: unknown, fallback: number): number {
|
||||
const ms = typeof value === "number" ? value : new Date(String(value)).getTime();
|
||||
return Number.isFinite(ms) ? ms : fallback;
|
||||
}
|
||||
|
||||
export function calculateRetentionStrength(
|
||||
memory: LongTermMemoryEntry,
|
||||
now: number,
|
||||
lastActivityAt?: string,
|
||||
): number {
|
||||
const initialStrength = calculateInitialStrength(memory);
|
||||
const effectiveHalfLife = calculateEffectiveHalfLife(memory);
|
||||
|
||||
// Use retentionClock if available, fallback to updatedAt.
|
||||
const retentionStart = Number.isFinite(memory.retentionClock)
|
||||
? memory.retentionClock
|
||||
: memory.updatedAt ?? memory.createdAt;
|
||||
const createdAtMs = timestampMs(retentionStart, now);
|
||||
const effectiveAgeDays = calculateEffectiveAgeDays(createdAtMs, now, lastActivityAt);
|
||||
|
||||
// Calculate strength using exponential decay.
|
||||
const strength = initialStrength * Math.pow(2, -effectiveAgeDays / effectiveHalfLife);
|
||||
|
||||
return Number.isFinite(strength) ? Math.max(0, strength) : 0;
|
||||
}
|
||||
|
||||
export function calculateDormantDays(store: WorkspaceMemoryStore, now: number): number {
|
||||
const lastActivity = store.lastActivityAt
|
||||
? new Date(store.lastActivityAt).getTime()
|
||||
: now;
|
||||
if (!Number.isFinite(lastActivity)) return 0;
|
||||
|
||||
const daysSinceActivity = (now - lastActivity) / DAY_MS;
|
||||
return Math.max(0, daysSinceActivity);
|
||||
}
|
||||
|
||||
export function calculateEffectiveAgeDays(
|
||||
entryStartMs: number,
|
||||
now: number,
|
||||
lastActivityAt?: string,
|
||||
): number {
|
||||
const wallAgeDays = Math.max(0, (now - entryStartMs) / DAY_MS);
|
||||
|
||||
if (!lastActivityAt) return wallAgeDays;
|
||||
|
||||
const lastActivityMs = new Date(lastActivityAt).getTime();
|
||||
if (!Number.isFinite(lastActivityMs)) return wallAgeDays;
|
||||
|
||||
const dormantStartMs = lastActivityMs + WORKSPACE_DORMANT_AFTER_DAYS * DAY_MS;
|
||||
const overlapStartMs = Math.max(entryStartMs, dormantStartMs);
|
||||
const dormantOverlapDays = Math.max(0, (now - overlapStartMs) / DAY_MS);
|
||||
const activeDays = wallAgeDays - dormantOverlapDays;
|
||||
|
||||
return activeDays + dormantOverlapDays * DORMANT_DECAY_MULTIPLIER;
|
||||
}
|
||||
|
||||
export function reinforceMemory(
|
||||
memory: LongTermMemoryEntry,
|
||||
sessionId: string,
|
||||
now: number,
|
||||
): LongTermMemoryEntry {
|
||||
if (memory.lastReinforcedSessionID === sessionId) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
if (memory.lastReinforcedAt && now - memory.lastReinforcedAt < REINFORCEMENT_MIN_INTERVAL_MS) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
if ((memory.reinforcementCount ?? 0) >= REINFORCEMENT_MAX_COUNT) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
return {
|
||||
...memory,
|
||||
reinforcementCount: (memory.reinforcementCount ?? 0) + 1,
|
||||
lastReinforcedAt: now,
|
||||
lastReinforcedSessionID: sessionId,
|
||||
retentionClock: now,
|
||||
};
|
||||
}
|
||||
+6
-135
@@ -6,146 +6,17 @@ import { migrationLogPath, workspaceKey, workspaceMemoryPath } from "./paths.ts"
|
||||
import { atomicWriteJSON, readJSON, updateJSON } from "./storage.ts";
|
||||
import { assessMemoryQuality, isHardQualityReason, isProgressSnapshotViolation } from "./memory-quality.ts";
|
||||
import { redactCredentials } from "./redaction.ts";
|
||||
import {
|
||||
RETENTION_TYPE_MAX,
|
||||
calculateRetentionStrength,
|
||||
reinforceMemory,
|
||||
} from "./retention.ts";
|
||||
|
||||
// Minimum length for workspace_memory envelope: <workspace_memory>\n...\n</workspace_memory>
|
||||
const MIN_ENVELOPE_LENGTH = 80;
|
||||
const MIGRATION_ID = "2026-04-26-p0-cleanup";
|
||||
const QUALITY_CLEANUP_MIGRATION_ID = "2026-04-28-quality-cleanup";
|
||||
|
||||
// Retention decay model constants (v1.5)
|
||||
const BASE_HALF_LIFE_DAYS = 45;
|
||||
const REINFORCEMENT_HALFLIFE_FACTOR = 0.85;
|
||||
const REINFORCEMENT_MAX_COUNT = 6;
|
||||
const REINFORCEMENT_MIN_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
const WORKSPACE_DORMANT_AFTER_DAYS = 14;
|
||||
const DORMANT_DECAY_MULTIPLIER = 0.25;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const TYPE_FACTOR = {
|
||||
reference: 1.0,
|
||||
project: 1.25,
|
||||
feedback: 2.25,
|
||||
decision: 2.5,
|
||||
} as const;
|
||||
|
||||
const SOURCE_FACTOR = {
|
||||
compaction: 1.0,
|
||||
manual: 1.4,
|
||||
explicit: 2.0,
|
||||
} as const;
|
||||
|
||||
const USER_IMPORTANCE_FACTOR = {
|
||||
low: 0.7,
|
||||
normal: 1.0,
|
||||
high: 1.5,
|
||||
} as const;
|
||||
|
||||
const TYPE_MAX = {
|
||||
feedback: 10,
|
||||
decision: 10,
|
||||
project: 8,
|
||||
reference: 6,
|
||||
} as const;
|
||||
|
||||
export function calculateInitialStrength(memory: LongTermMemoryEntry): number {
|
||||
const typeFactor = TYPE_FACTOR[memory.type] ?? 1.0;
|
||||
const sourceFactor = SOURCE_FACTOR[memory.source] ?? 1.0;
|
||||
const importanceFactor = USER_IMPORTANCE_FACTOR[memory.userImportance ?? "normal"] ?? 1.0;
|
||||
|
||||
return typeFactor * sourceFactor * importanceFactor;
|
||||
}
|
||||
|
||||
export function calculateEffectiveHalfLife(memory: LongTermMemoryEntry): number {
|
||||
const reinforcementCount = Math.min(
|
||||
memory.reinforcementCount ?? 0,
|
||||
REINFORCEMENT_MAX_COUNT,
|
||||
);
|
||||
const factor = Math.pow(REINFORCEMENT_HALFLIFE_FACTOR, reinforcementCount);
|
||||
return BASE_HALF_LIFE_DAYS / factor;
|
||||
}
|
||||
|
||||
function timestampMs(value: unknown, fallback: number): number {
|
||||
const ms = typeof value === "number" ? value : new Date(String(value)).getTime();
|
||||
return Number.isFinite(ms) ? ms : fallback;
|
||||
}
|
||||
|
||||
export function calculateRetentionStrength(
|
||||
memory: LongTermMemoryEntry,
|
||||
now: number,
|
||||
lastActivityAt?: string,
|
||||
): number {
|
||||
const initialStrength = calculateInitialStrength(memory);
|
||||
const effectiveHalfLife = calculateEffectiveHalfLife(memory);
|
||||
|
||||
// Use retentionClock if available, fallback to updatedAt.
|
||||
const retentionStart = Number.isFinite(memory.retentionClock)
|
||||
? memory.retentionClock
|
||||
: memory.updatedAt ?? memory.createdAt;
|
||||
const createdAtMs = timestampMs(retentionStart, now);
|
||||
const effectiveAgeDays = calculateEffectiveAgeDays(createdAtMs, now, lastActivityAt);
|
||||
|
||||
// Calculate strength using exponential decay.
|
||||
const strength = initialStrength * Math.pow(2, -effectiveAgeDays / effectiveHalfLife);
|
||||
|
||||
return Number.isFinite(strength) ? Math.max(0, strength) : 0;
|
||||
}
|
||||
|
||||
export function calculateDormantDays(store: WorkspaceMemoryStore, now: number): number {
|
||||
const lastActivity = store.lastActivityAt
|
||||
? new Date(store.lastActivityAt).getTime()
|
||||
: now;
|
||||
if (!Number.isFinite(lastActivity)) return 0;
|
||||
|
||||
const daysSinceActivity = (now - lastActivity) / DAY_MS;
|
||||
return Math.max(0, daysSinceActivity);
|
||||
}
|
||||
|
||||
export function calculateEffectiveAgeDays(
|
||||
entryStartMs: number,
|
||||
now: number,
|
||||
lastActivityAt?: string,
|
||||
): number {
|
||||
const wallAgeDays = Math.max(0, (now - entryStartMs) / DAY_MS);
|
||||
|
||||
if (!lastActivityAt) return wallAgeDays;
|
||||
|
||||
const lastActivityMs = new Date(lastActivityAt).getTime();
|
||||
if (!Number.isFinite(lastActivityMs)) return wallAgeDays;
|
||||
|
||||
const dormantStartMs = lastActivityMs + WORKSPACE_DORMANT_AFTER_DAYS * DAY_MS;
|
||||
const overlapStartMs = Math.max(entryStartMs, dormantStartMs);
|
||||
const dormantOverlapDays = Math.max(0, (now - overlapStartMs) / DAY_MS);
|
||||
const activeDays = wallAgeDays - dormantOverlapDays;
|
||||
|
||||
return activeDays + dormantOverlapDays * DORMANT_DECAY_MULTIPLIER;
|
||||
}
|
||||
|
||||
export function reinforceMemory(
|
||||
memory: LongTermMemoryEntry,
|
||||
sessionId: string,
|
||||
now: number,
|
||||
): LongTermMemoryEntry {
|
||||
if (memory.lastReinforcedSessionID === sessionId) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
if (memory.lastReinforcedAt && now - memory.lastReinforcedAt < REINFORCEMENT_MIN_INTERVAL_MS) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
if ((memory.reinforcementCount ?? 0) >= REINFORCEMENT_MAX_COUNT) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
return {
|
||||
...memory,
|
||||
reinforcementCount: (memory.reinforcementCount ?? 0) + 1,
|
||||
lastReinforcedAt: now,
|
||||
lastReinforcedSessionID: sessionId,
|
||||
retentionClock: now,
|
||||
};
|
||||
}
|
||||
|
||||
export type MemoryConsolidationReason =
|
||||
| "promoted"
|
||||
| "absorbed_exact"
|
||||
@@ -658,7 +529,7 @@ function applyTypeMaxCaps(entries: LongTermMemoryEntry[]): LongTermMemoryEntry[]
|
||||
|
||||
for (const entry of entries) {
|
||||
const count = typeCounts[entry.type] ?? 0;
|
||||
const max = TYPE_MAX[entry.type] ?? Infinity;
|
||||
const max = RETENTION_TYPE_MAX[entry.type] ?? Infinity;
|
||||
if (count >= max) continue;
|
||||
|
||||
capped.push(entry);
|
||||
|
||||
@@ -19,13 +19,16 @@ import {
|
||||
loadWorkspaceMemory,
|
||||
saveWorkspaceMemory,
|
||||
updateWorkspaceMemoryWithAccounting,
|
||||
} from "../src/workspace-memory.ts";
|
||||
import {
|
||||
RETENTION_TYPE_MAX,
|
||||
calculateInitialStrength,
|
||||
calculateEffectiveHalfLife,
|
||||
calculateRetentionStrength,
|
||||
calculateDormantDays,
|
||||
calculateEffectiveAgeDays,
|
||||
reinforceMemory,
|
||||
} from "../src/workspace-memory.ts";
|
||||
} from "../src/retention.ts";
|
||||
import { redactCredentials } from "../src/redaction.ts";
|
||||
import { assessMemoryQuality, isHardQualityReason, isProgressSnapshotViolation } from "../src/memory-quality.ts";
|
||||
import { reviewerCurrent28Fixture } from "./fixtures/memory-quality-current-28.ts";
|
||||
@@ -580,7 +583,7 @@ test("enforceLongTermLimits applies per-type caps after strength sorting", () =>
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
});
|
||||
|
||||
test("safetyCritical entries compete under TYPE_MAX caps like other entries", () => {
|
||||
test("safetyCritical entries compete under RETENTION_TYPE_MAX caps like other entries", () => {
|
||||
const safetyEntries: LongTermMemoryEntry[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
...entry(`safety-${i}`, `Safety memory ${i}`, "feedback"),
|
||||
source: "explicit",
|
||||
@@ -596,7 +599,7 @@ test("safetyCritical entries compete under TYPE_MAX caps like other entries", ()
|
||||
const kept = enforceLongTermLimits(all);
|
||||
|
||||
const feedbackCount = kept.filter(e => e.type === "feedback").length;
|
||||
assert.equal(feedbackCount, 10);
|
||||
assert.equal(feedbackCount, RETENTION_TYPE_MAX.feedback);
|
||||
// safetyCritical entries are no longer exempt from type caps
|
||||
assert.ok(kept.filter(e => e.safetyCritical).length < 6);
|
||||
});
|
||||
@@ -639,7 +642,7 @@ test("workspace memory JSON with deprecated safetyCritical loads and competes no
|
||||
);
|
||||
|
||||
const kept = enforceLongTermLimits(loaded.entries);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, RETENTION_TYPE_MAX.feedback);
|
||||
assert.ok(kept.filter(memory => memory.safetyCritical).length < safetyEntries.length);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
@@ -657,8 +660,8 @@ test("enforceLongTermLimits applies type caps to deprecated safetyCritical entri
|
||||
|
||||
const kept = enforceLongTermLimits([safetyCriticalFeedback, ...ordinaryFeedback]);
|
||||
|
||||
assert.equal(kept.length, 10);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
assert.equal(kept.length, RETENTION_TYPE_MAX.feedback);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, RETENTION_TYPE_MAX.feedback);
|
||||
});
|
||||
|
||||
test("mixed retention scenario applies caps and reinforcement ordering", () => {
|
||||
@@ -709,9 +712,9 @@ test("mixed retention scenario applies caps and reinforcement ordering", () => {
|
||||
const result = enforceLongTermLimitsWithAccounting(entries, store);
|
||||
|
||||
assert.ok(result.kept.length <= 28);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "feedback").length <= 10);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "decision").length <= 10);
|
||||
assert.equal(result.kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "feedback").length <= RETENTION_TYPE_MAX.feedback);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "decision").length <= RETENTION_TYPE_MAX.decision);
|
||||
assert.equal(result.kept.filter(memory => memory.type === "feedback").length, RETENTION_TYPE_MAX.feedback);
|
||||
const reinforcedIndex = result.kept.findIndex(memory => memory.id === "old-reinforced");
|
||||
const unreinforcedIndex = result.kept.findIndex(memory => memory.id === "old-unreinforced");
|
||||
assert.ok(reinforcedIndex >= 0, "old reinforced reference should be kept");
|
||||
|
||||
Reference in New Issue
Block a user