mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-01 22:11:08 +02:00
feat(memory): implement retention decay model with strength-based ordering
- Add retention model constants (45-day half-life, 6.0 safety factor) - Add TYPE_MAX caps (feedback:10, decision:10, project:8, reference:6) - Add strength calculation: initialStrength × 2^(-age/halfLife) - Integrate strength-based sorting into enforceLongTermLimits - Safety-critical entries bypass type caps - Add fields: retentionClock, reinforcementCount, userImportance, safetyCritical
This commit is contained in:
@@ -20,6 +20,11 @@ export type LongTermMemoryEntry = {
|
||||
promotionAttempts?: number;
|
||||
lastPromotionAttemptAt?: string;
|
||||
lastPromotionFailureReason?: string;
|
||||
retentionClock?: number; // Unix timestamp when retention started
|
||||
reinforcementCount?: number; // Number of times this memory was reinforced
|
||||
lastReinforcedAt?: number; // Unix timestamp of last reinforcement
|
||||
userImportance?: "low" | "normal" | "high";
|
||||
safetyCritical?: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceMemoryStore = {
|
||||
|
||||
+118
-18
@@ -12,6 +12,87 @@ 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 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 SAFETY_CRITICAL_FACTOR = 6.0;
|
||||
|
||||
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;
|
||||
const safetyFactor = memory.safetyCritical ? SAFETY_CRITICAL_FACTOR : 1.0;
|
||||
|
||||
return typeFactor * sourceFactor * importanceFactor * safetyFactor;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function calculateRetentionStrength(
|
||||
memory: LongTermMemoryEntry,
|
||||
now: number,
|
||||
dormantDays: number,
|
||||
): number {
|
||||
const initialStrength = calculateInitialStrength(memory);
|
||||
const effectiveHalfLife = calculateEffectiveHalfLife(memory);
|
||||
|
||||
// Use retentionClock if available, fallback to updatedAt.
|
||||
const retentionStart = memory.retentionClock ?? memory.updatedAt;
|
||||
const createdAtMs = new Date(retentionStart).getTime();
|
||||
const ageMs = now - createdAtMs;
|
||||
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
||||
|
||||
// Apply dormant boost to effective age.
|
||||
let effectiveAgeDays = ageDays;
|
||||
if (dormantDays > 0) {
|
||||
// Dormant workspace ages faster.
|
||||
effectiveAgeDays += dormantDays * DORMANT_DECAY_MULTIPLIER;
|
||||
}
|
||||
|
||||
// Calculate strength using exponential decay.
|
||||
const strength = initialStrength * Math.pow(2, -effectiveAgeDays / effectiveHalfLife);
|
||||
|
||||
return Math.max(0, strength);
|
||||
}
|
||||
|
||||
export type MemoryConsolidationReason =
|
||||
| "promoted"
|
||||
| "absorbed_exact"
|
||||
@@ -497,6 +578,9 @@ export function enforceLongTermLimits(entries: LongTermMemoryEntry[]): LongTermM
|
||||
|
||||
export function enforceLongTermLimitsWithAccounting(entries: LongTermMemoryEntry[]): LongTermLimitResult {
|
||||
const now = Date.now();
|
||||
// Store-level last activity tracking is not available yet; dormant handling
|
||||
// will be wired in a later wave.
|
||||
const dormantDays = 0;
|
||||
const staleDropped: MemoryConsolidationEvent[] = [];
|
||||
|
||||
// Phase 1: filter active, prune by age
|
||||
@@ -511,8 +595,9 @@ export function enforceLongTermLimitsWithAccounting(entries: LongTermMemoryEntry
|
||||
}
|
||||
|
||||
const dedupeResult = dedupeLongTermEntriesWithAccounting(phase1);
|
||||
const sorted = [...dedupeResult.kept].sort(compareLongTermMemoryForRetention);
|
||||
const kept = sorted.slice(0, LONG_TERM_LIMITS.maxEntries);
|
||||
const sorted = [...dedupeResult.kept].sort((a, b) => compareLongTermMemoryForRetention(a, b, now, dormantDays));
|
||||
const capped = applyTypeMaxCaps(sorted);
|
||||
const kept = capped.slice(0, LONG_TERM_LIMITS.maxEntries);
|
||||
const keptIds = new Set(kept.map(entry => entry.id));
|
||||
const capacityDropped = sorted
|
||||
.filter(entry => !keptIds.has(entry.id))
|
||||
@@ -526,6 +611,27 @@ export function enforceLongTermLimitsWithAccounting(entries: LongTermMemoryEntry
|
||||
};
|
||||
}
|
||||
|
||||
function applyTypeMaxCaps(entries: LongTermMemoryEntry[]): LongTermMemoryEntry[] {
|
||||
const capped: LongTermMemoryEntry[] = [];
|
||||
const typeCounts: Partial<Record<LongTermMemoryEntry["type"], number>> = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.safetyCritical) {
|
||||
capped.push(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
const count = typeCounts[entry.type] ?? 0;
|
||||
const max = TYPE_MAX[entry.type] ?? Infinity;
|
||||
if (count >= max) continue;
|
||||
|
||||
capped.push(entry);
|
||||
typeCounts[entry.type] = count + 1;
|
||||
}
|
||||
|
||||
return capped;
|
||||
}
|
||||
|
||||
export function dedupeLongTermEntriesWithAccounting(entries: LongTermMemoryEntry[]): LongTermLimitResult {
|
||||
const absorbed: MemoryConsolidationEvent[] = [];
|
||||
const superseded: MemoryConsolidationEvent[] = [];
|
||||
@@ -598,10 +704,16 @@ export function dedupeLongTermEntriesWithAccounting(entries: LongTermMemoryEntry
|
||||
};
|
||||
}
|
||||
|
||||
function compareLongTermMemoryForRetention(a: LongTermMemoryEntry, b: LongTermMemoryEntry): number {
|
||||
const pA = priority(a);
|
||||
const pB = priority(b);
|
||||
if (pB !== pA) return pB - pA;
|
||||
function compareLongTermMemoryForRetention(
|
||||
a: LongTermMemoryEntry,
|
||||
b: LongTermMemoryEntry,
|
||||
now: number,
|
||||
dormantDays: number,
|
||||
): number {
|
||||
const strengthA = calculateRetentionStrength(a, now, dormantDays);
|
||||
const strengthB = calculateRetentionStrength(b, now, dormantDays);
|
||||
if (strengthB !== strengthA) return strengthB - strengthA;
|
||||
|
||||
const sourceDiff = sourcePriority(b.source) - sourcePriority(a.source);
|
||||
if (sourceDiff !== 0) return sourceDiff;
|
||||
const createdDiff = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
@@ -609,18 +721,6 @@ function compareLongTermMemoryForRetention(a: LongTermMemoryEntry, b: LongTermMe
|
||||
return a.id.localeCompare(b.id);
|
||||
}
|
||||
|
||||
function priority(entry: LongTermMemoryEntry): number {
|
||||
const typeWeight = {
|
||||
feedback: 400,
|
||||
decision: 300,
|
||||
project: 200,
|
||||
reference: 100,
|
||||
}[entry.type];
|
||||
|
||||
const sourceWeight = entry.source === "explicit" ? 1000 : 0;
|
||||
return sourceWeight + typeWeight + entry.confidence * 10;
|
||||
}
|
||||
|
||||
function wouldFit(
|
||||
lines: string[],
|
||||
nextLine: string,
|
||||
|
||||
+104
-8
@@ -1422,9 +1422,9 @@ test("session.compacted clears compaction pending memory rejected by workspace e
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
await updateWorkspaceMemory(tmpDir, store => {
|
||||
for (let i = 0; i < 28; i += 1) {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_${i}`,
|
||||
id: `mem_high_feedback_${i}`,
|
||||
type: "feedback",
|
||||
text: `High priority user feedback memory ${i} that should outrank low priority references.`,
|
||||
source: "explicit",
|
||||
@@ -1434,6 +1434,30 @@ test("session.compacted clears compaction pending memory rejected by workspace e
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_decision_${i}`,
|
||||
type: "decision",
|
||||
text: `High priority decision memory ${i} that should outrank low priority references.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_project_${i}`,
|
||||
type: "project",
|
||||
text: `High priority project memory ${i} that should outrank low priority references.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return store;
|
||||
});
|
||||
|
||||
@@ -1476,9 +1500,9 @@ test("session.compacted keeps explicit pending memory rejected by workspace entr
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
await updateWorkspaceMemory(tmpDir, store => {
|
||||
for (let i = 0; i < 28; i += 1) {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_explicit_reject_${i}`,
|
||||
id: `mem_high_explicit_reject_feedback_${i}`,
|
||||
type: "feedback",
|
||||
text: `Pinned high priority feedback for explicit reject ${i}.`,
|
||||
source: "explicit",
|
||||
@@ -1488,6 +1512,30 @@ test("session.compacted keeps explicit pending memory rejected by workspace entr
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_explicit_reject_decision_${i}`,
|
||||
type: "decision",
|
||||
text: `Pinned high priority decision for explicit reject ${i}.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_explicit_reject_project_${i}`,
|
||||
type: "project",
|
||||
text: `Pinned high priority project for explicit reject ${i}.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return store;
|
||||
});
|
||||
|
||||
@@ -1531,9 +1579,9 @@ test("explicit capacity rejection records bounded retry metadata", async () => {
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
await updateWorkspaceMemory(tmpDir, store => {
|
||||
for (let i = 0; i < 28; i += 1) {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_bounded_reject_${i}`,
|
||||
id: `mem_high_bounded_reject_feedback_${i}`,
|
||||
type: "feedback",
|
||||
text: `Pinned high priority feedback for bounded rejection ${i}.`,
|
||||
source: "explicit",
|
||||
@@ -1543,6 +1591,30 @@ test("explicit capacity rejection records bounded retry metadata", async () => {
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_bounded_reject_decision_${i}`,
|
||||
type: "decision",
|
||||
text: `Pinned high priority decision for bounded rejection ${i}.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_bounded_reject_project_${i}`,
|
||||
type: "project",
|
||||
text: `Pinned high priority project for bounded rejection ${i}.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return store;
|
||||
});
|
||||
|
||||
@@ -1614,9 +1686,9 @@ test("session.compacted clears compaction pending memories when all rejected by
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
await updateWorkspaceMemory(tmpDir, store => {
|
||||
for (let i = 0; i < 28; i += 1) {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_all_rejected_${i}`,
|
||||
id: `mem_high_all_rejected_feedback_${i}`,
|
||||
type: "feedback",
|
||||
text: `Pinned high priority feedback ${i} that keeps the workspace entry cap full.`,
|
||||
source: "explicit",
|
||||
@@ -1626,6 +1698,30 @@ test("session.compacted clears compaction pending memories when all rejected by
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_all_rejected_decision_${i}`,
|
||||
type: "decision",
|
||||
text: `Pinned high priority decision ${i} that keeps the workspace entry cap full.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
store.entries.push({
|
||||
id: `mem_high_all_rejected_project_${i}`,
|
||||
type: "project",
|
||||
text: `Pinned high priority project ${i} that keeps the workspace entry cap full.`,
|
||||
source: "explicit",
|
||||
confidence: 1,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return store;
|
||||
});
|
||||
|
||||
|
||||
+121
-30
@@ -19,6 +19,9 @@ import {
|
||||
loadWorkspaceMemory,
|
||||
saveWorkspaceMemory,
|
||||
updateWorkspaceMemoryWithAccounting,
|
||||
calculateInitialStrength,
|
||||
calculateEffectiveHalfLife,
|
||||
calculateRetentionStrength,
|
||||
} from "../src/workspace-memory.ts";
|
||||
import { redactCredentials } from "../src/redaction.ts";
|
||||
import { assessMemoryQuality, isHardQualityReason, isProgressSnapshotViolation } from "../src/memory-quality.ts";
|
||||
@@ -256,6 +259,104 @@ test("enforceLongTermLimits respects maxEntries limit", () => {
|
||||
assert.ok(kept.length <= 28, `Should respect maxEntries. Got: ${kept.length}`);
|
||||
});
|
||||
|
||||
test("calculateInitialStrength multiplies type, source, importance, and safety factors", () => {
|
||||
const memory: LongTermMemoryEntry = {
|
||||
...entry("strength", "Never store raw credentials", "reference"),
|
||||
source: "explicit",
|
||||
userImportance: "high",
|
||||
safetyCritical: true,
|
||||
};
|
||||
|
||||
assert.equal(calculateInitialStrength(memory), 18);
|
||||
});
|
||||
|
||||
test("calculateEffectiveHalfLife clamps reinforcement count at configured maximum", () => {
|
||||
const unreinforced = entry("unreinforced", "Use pnpm for this project");
|
||||
const overReinforced: LongTermMemoryEntry = {
|
||||
...entry("over-reinforced", "Use npm scripts for verification"),
|
||||
reinforcementCount: 99,
|
||||
};
|
||||
|
||||
assert.equal(calculateEffectiveHalfLife(unreinforced), 45);
|
||||
assert.equal(
|
||||
calculateEffectiveHalfLife(overReinforced),
|
||||
45 / Math.pow(0.85, 6),
|
||||
);
|
||||
});
|
||||
|
||||
test("calculateRetentionStrength decays from retentionClock and applies dormant age", () => {
|
||||
const now = Date.UTC(2026, 3, 29);
|
||||
const fiveDaysAgo = now - 5 * 24 * 60 * 60 * 1000;
|
||||
const memory: LongTermMemoryEntry = {
|
||||
...entry("retention-clock", "Use TypeScript for plugin code", "reference"),
|
||||
source: "compaction",
|
||||
retentionClock: fiveDaysAgo,
|
||||
updatedAt: new Date(now).toISOString(),
|
||||
};
|
||||
|
||||
const expectedEffectiveAgeDays = 5 + 4 * 0.25;
|
||||
const expected = Math.pow(2, -expectedEffectiveAgeDays / 45);
|
||||
|
||||
assert.equal(calculateRetentionStrength(memory, now, 4), expected);
|
||||
});
|
||||
|
||||
test("calculateRetentionStrength falls back to updatedAt when retentionClock is absent", () => {
|
||||
const now = Date.UTC(2026, 3, 29);
|
||||
const memory: LongTermMemoryEntry = {
|
||||
...entry("updated-at", "Prefer concise verification summaries", "feedback"),
|
||||
source: "manual",
|
||||
updatedAt: new Date(now - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
};
|
||||
|
||||
const initialStrength = 2.25 * 1.4;
|
||||
assert.equal(calculateRetentionStrength(memory, now, 0), initialStrength / 2);
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits orders entries by retention strength", () => {
|
||||
const now = Date.now();
|
||||
const freshFeedback: LongTermMemoryEntry = {
|
||||
...entry("fresh-feedback", "User prefers direct concise answers", "feedback"),
|
||||
source: "compaction",
|
||||
updatedAt: new Date(now).toISOString(),
|
||||
};
|
||||
const oldExplicitReference: LongTermMemoryEntry = {
|
||||
...entry("old-explicit-reference", "Legacy docs are at https://example.com/old", "reference"),
|
||||
source: "explicit",
|
||||
updatedAt: new Date(now - 365 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
};
|
||||
|
||||
const kept = enforceLongTermLimits([oldExplicitReference, freshFeedback]);
|
||||
|
||||
assert.deepEqual(kept.map(memory => memory.id), ["fresh-feedback", "old-explicit-reference"]);
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits applies per-type caps after strength sorting", () => {
|
||||
const entries = Array.from({ length: 12 }, (_, i) =>
|
||||
entry(`feedback_${i}`, `Unique user feedback preference ${i}`, "feedback")
|
||||
);
|
||||
|
||||
const kept = enforceLongTermLimits(entries);
|
||||
|
||||
assert.equal(kept.length, 10);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits exempts safety-critical entries from type caps", () => {
|
||||
const ordinaryFeedback = Array.from({ length: 12 }, (_, i) =>
|
||||
entry(`feedback_${i}`, `Unique safe ordinary feedback preference ${i}`, "feedback")
|
||||
);
|
||||
const safetyCriticalFeedback: LongTermMemoryEntry = {
|
||||
...entry("safety-feedback", "Never persist raw credentials in memory", "feedback"),
|
||||
safetyCritical: true,
|
||||
};
|
||||
|
||||
const kept = enforceLongTermLimits([safetyCriticalFeedback, ...ordinaryFeedback]);
|
||||
|
||||
assert.equal(kept.length, 11);
|
||||
assert.ok(kept.some(memory => memory.id === "safety-feedback"));
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback" && !memory.safetyCritical).length, 10);
|
||||
});
|
||||
|
||||
test("normalization ordering is deterministic for retention ties", () => {
|
||||
const createdAt = "2026-04-28T00:00:00.000Z";
|
||||
const a = {
|
||||
@@ -378,16 +479,12 @@ test("dedupeLongTermEntriesWithAccounting does not report heuristic topic supers
|
||||
|
||||
test("enforceLongTermLimitsWithAccounting reports capacity drops", () => {
|
||||
const now = new Date().toISOString();
|
||||
const entries = Array.from({ length: LONG_TERM_LIMITS.maxEntries + 2 }, (_, i) => ({
|
||||
id: `mem_${i}`,
|
||||
type: "reference" as const,
|
||||
text: `Unique low priority reference ${i}`,
|
||||
source: "compaction" as const,
|
||||
confidence: i < LONG_TERM_LIMITS.maxEntries ? 0.8 : 0.1,
|
||||
status: "active" as const,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
const entries = [
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`feedback_${i}`, `Capacity feedback ${i}`, "feedback")),
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`decision_${i}`, `Capacity decision ${i}`, "decision")),
|
||||
...Array.from({ length: 8 }, (_, i) => entry(`project_${i}`, `Capacity project ${i}`, "project")),
|
||||
...Array.from({ length: 2 }, (_, i) => entry(`reference_${i}`, `Capacity overflow reference ${i}`, "reference")),
|
||||
].map(memory => ({ ...memory, createdAt: now, updatedAt: now }));
|
||||
|
||||
const result = enforceLongTermLimitsWithAccounting(entries);
|
||||
|
||||
@@ -445,21 +542,18 @@ test("normalizeWorkspaceMemoryWithAccounting redacts credentials before accounti
|
||||
test("normalizeWorkspaceMemoryWithAccounting reports overflow capacity drops", async () => {
|
||||
const root = "/repo";
|
||||
const now = new Date().toISOString();
|
||||
const entries = [
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`overflow_feedback_${i}`, `Overflow feedback ${i}`, "feedback")),
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`overflow_decision_${i}`, `Overflow decision ${i}`, "decision")),
|
||||
...Array.from({ length: 8 }, (_, i) => entry(`overflow_project_${i}`, `Overflow project ${i}`, "project")),
|
||||
entry("overflow_reference", "Overflow low strength reference", "reference"),
|
||||
].map(memory => ({ ...memory, createdAt: now, updatedAt: now }));
|
||||
const store: WorkspaceMemoryStore = {
|
||||
version: 1,
|
||||
workspace: { root, key: "abc" },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
migrations: ["2026-04-26-p0-cleanup"],
|
||||
entries: Array.from({ length: LONG_TERM_LIMITS.maxEntries + 1 }, (_, i) => ({
|
||||
id: `overflow_${i}`,
|
||||
type: "reference" as const,
|
||||
text: `Overflow reference ${i}`,
|
||||
source: "compaction" as const,
|
||||
confidence: i < LONG_TERM_LIMITS.maxEntries ? 0.8 : 0.1,
|
||||
status: "active" as const,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
entries,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
@@ -506,16 +600,13 @@ test("updateWorkspaceMemoryWithAccounting emits accounting events for persisted
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const result = await updateWorkspaceMemoryWithAccounting(root, store => {
|
||||
store.entries.push(...Array.from({ length: LONG_TERM_LIMITS.maxEntries + 1 }, (_, i) => ({
|
||||
id: `persisted_${i}`,
|
||||
type: "reference" as const,
|
||||
text: `Persisted accounting reference ${i}`,
|
||||
source: "compaction" as const,
|
||||
confidence: i < LONG_TERM_LIMITS.maxEntries ? 0.8 : 0.1,
|
||||
status: "active" as const,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})));
|
||||
store.entries.push(
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`persisted_feedback_${i}`, `Persisted feedback ${i}`, "feedback")),
|
||||
...Array.from({ length: 10 }, (_, i) => entry(`persisted_decision_${i}`, `Persisted decision ${i}`, "decision")),
|
||||
...Array.from({ length: 8 }, (_, i) => entry(`persisted_project_${i}`, `Persisted project ${i}`, "project")),
|
||||
entry("persisted_reference", "Persisted low strength reference", "reference"),
|
||||
);
|
||||
store.entries = store.entries.map(memory => ({ ...memory, createdAt: now, updatedAt: now }));
|
||||
return store;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user