mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-03 14:15:20 +02:00
feat(deprecation): remove safetyCritical retention multiplier and type-cap bypass
- Remove SAFETY_CRITICAL_FACTOR = 6.0 from workspace-memory.ts - Remove safetyFactor from calculateInitialStrength() - all memories now fade according to the same rules - Remove safetyCritical bypass from applyTypeMaxCaps() - safetyCritical entries compete normally under TYPE_MAX caps - Preserve safetyCritical?: boolean in LongTermMemoryEntry type for backward compatibility (no producer sets it to true) - Update memory-diag to show deprecation warning instead of capacity alert - Update tests: add backward-compatibility fixture test, deprecation strength test, normal cap competition test - Update docs/architecture.md, RELEASE_NOTES.md, CHANGELOG.md, docs/configuration.md Phase 1.5 complete: safetyCritical is now a deprecated field with no active behavior. Safety rules belong in user-controlled agent.md files.
This commit is contained in:
@@ -77,7 +77,7 @@ test("memory health reports stored vs rendered retention counts", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("memory health reports dormancy and retention monitoring alerts", async () => {
|
||||
test("memory health reports dormancy and retention monitoring deprecations", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-memory-diag-"));
|
||||
try {
|
||||
const lastActivityAt = new Date(Date.now() - 19 * 24 * 60 * 60 * 1000).toISOString();
|
||||
@@ -96,7 +96,7 @@ test("memory health reports dormancy and retention monitoring alerts", async ()
|
||||
assert.match(stdout, /dormant discount active: yes/);
|
||||
assert.match(stdout, /dormant days past grace: 5\.0/);
|
||||
assert.match(stdout, /high_importance_ratio: 40\.0% .* ALERT/);
|
||||
assert.match(stdout, /safety_critical_count: 6 .* ALERT/);
|
||||
assert.match(stdout, /safety_critical_count: 6 .*deprecated.* WARNING/);
|
||||
assert.match(stdout, /max_reinforced_count: 2 \(20\.0%, alert > 10%\) ALERT/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
@@ -135,7 +135,7 @@ test("memory health reports missing dormancy and non-alert monitoring defaults",
|
||||
assert.match(stdout, /wall days since activity: unknown/);
|
||||
assert.match(stdout, /dormant discount active: no/);
|
||||
assert.match(stdout, /high_importance_ratio: 0\.0% \(alert > 30%\)\n/);
|
||||
assert.match(stdout, /safety_critical_count: 0 \(alert > 5\)\n/);
|
||||
assert.match(stdout, /safety_critical_count: 0 \(deprecated field\)\n/);
|
||||
assert.match(stdout, /max_reinforced_count: 0 \(alert > 10% active\)/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
|
||||
@@ -270,7 +270,7 @@ 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", () => {
|
||||
test("calculateInitialStrength multiplies type, source, and importance factors", () => {
|
||||
const memory: LongTermMemoryEntry = {
|
||||
...entry("strength", "Never store raw credentials", "reference"),
|
||||
source: "explicit",
|
||||
@@ -278,7 +278,20 @@ test("calculateInitialStrength multiplies type, source, importance, and safety f
|
||||
safetyCritical: true,
|
||||
};
|
||||
|
||||
assert.equal(calculateInitialStrength(memory), 18);
|
||||
assert.equal(calculateInitialStrength(memory), 3);
|
||||
});
|
||||
|
||||
test("calculateInitialStrength ignores deprecated safetyCritical field", () => {
|
||||
const memory: LongTermMemoryEntry = {
|
||||
...entry("safety-deprecated", "Deprecated safety field should not affect strength", "decision"),
|
||||
source: "explicit",
|
||||
userImportance: "high",
|
||||
safetyCritical: true,
|
||||
};
|
||||
|
||||
const withoutSafety = { ...memory, safetyCritical: undefined };
|
||||
|
||||
assert.equal(calculateInitialStrength(memory), calculateInitialStrength(withoutSafety));
|
||||
});
|
||||
|
||||
test("calculateEffectiveHalfLife clamps reinforcement count at configured maximum", () => {
|
||||
@@ -567,7 +580,73 @@ test("enforceLongTermLimits applies per-type caps after strength sorting", () =>
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits exempts safety-critical entries from type caps", () => {
|
||||
test("safetyCritical entries compete under TYPE_MAX caps like other entries", () => {
|
||||
const safetyEntries: LongTermMemoryEntry[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
...entry(`safety-${i}`, `Safety memory ${i}`, "feedback"),
|
||||
source: "explicit",
|
||||
safetyCritical: true,
|
||||
}));
|
||||
|
||||
const ordinaryEntries: LongTermMemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
...entry(`ordinary-${i}`, `Ordinary memory ${i}`, "feedback"),
|
||||
source: "explicit",
|
||||
}));
|
||||
|
||||
const all = [...safetyEntries, ...ordinaryEntries];
|
||||
const kept = enforceLongTermLimits(all);
|
||||
|
||||
const feedbackCount = kept.filter(e => e.type === "feedback").length;
|
||||
assert.equal(feedbackCount, 10);
|
||||
// safetyCritical entries are no longer exempt from type caps
|
||||
assert.ok(kept.filter(e => e.safetyCritical).length < 6);
|
||||
});
|
||||
|
||||
test("workspace memory JSON with deprecated safetyCritical loads and competes normally", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-safety-compat-"));
|
||||
try {
|
||||
const key = await workspaceKey(root);
|
||||
const path = await workspaceMemoryPath(root);
|
||||
const now = new Date().toISOString();
|
||||
const safetyEntries: LongTermMemoryEntry[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
...entry(`safety-fixture-${i}`, `Safety fixture memory ${i}`, "feedback"),
|
||||
source: "explicit",
|
||||
userImportance: i === 0 ? "high" : "normal",
|
||||
safetyCritical: true,
|
||||
}));
|
||||
const ordinaryEntries: LongTermMemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
...entry(`ordinary-fixture-${i}`, `Ordinary fixture memory ${i}`, "feedback"),
|
||||
source: "explicit",
|
||||
}));
|
||||
const store: WorkspaceMemoryStore = {
|
||||
version: 1,
|
||||
workspace: { root, key },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries: [...safetyEntries, ...ordinaryEntries],
|
||||
migrations: [],
|
||||
updatedAt: now,
|
||||
lastActivityAt: now,
|
||||
};
|
||||
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, JSON.stringify(store, null, 2), "utf8");
|
||||
|
||||
const loaded = await loadWorkspaceMemory(root);
|
||||
const safetyEntry = loaded.entries.find(memory => memory.safetyCritical);
|
||||
assert.ok(safetyEntry, "fixture should include deprecated safetyCritical entries");
|
||||
assert.equal(
|
||||
calculateInitialStrength(safetyEntry),
|
||||
calculateInitialStrength({ ...safetyEntry, safetyCritical: undefined }),
|
||||
);
|
||||
|
||||
const kept = enforceLongTermLimits(loaded.entries);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
assert.ok(kept.filter(memory => memory.safetyCritical).length < safetyEntries.length);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits applies type caps to deprecated safetyCritical entries", () => {
|
||||
const ordinaryFeedback = Array.from({ length: 12 }, (_, i) =>
|
||||
entry(`feedback_${i}`, `Unique safe ordinary feedback preference ${i}`, "feedback")
|
||||
);
|
||||
@@ -578,12 +657,11 @@ test("enforceLongTermLimits exempts safety-critical entries from type caps", ()
|
||||
|
||||
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);
|
||||
assert.equal(kept.length, 10);
|
||||
assert.equal(kept.filter(memory => memory.type === "feedback").length, 10);
|
||||
});
|
||||
|
||||
test("mixed retention scenario applies caps, safety exemption, and reinforcement ordering", () => {
|
||||
test("mixed retention scenario applies caps and reinforcement ordering", () => {
|
||||
const now = Date.now();
|
||||
const oldAge = now - 120 * DAY_MS;
|
||||
const ordinaryFeedback = Array.from({ length: 17 }, (_, i) =>
|
||||
@@ -625,18 +703,15 @@ test("mixed retention scenario applies caps, safety exemption, and reinforcement
|
||||
lastActivityAt: new Date(now).toISOString(),
|
||||
};
|
||||
|
||||
assert.ok(entries.filter(memory => memory.type === "feedback" && !memory.safetyCritical).length > 10);
|
||||
assert.ok(entries.filter(memory => memory.type === "decision" && !memory.safetyCritical).length > 10);
|
||||
assert.ok(entries.filter(memory => memory.type === "feedback").length > 10);
|
||||
assert.ok(entries.filter(memory => memory.type === "decision").length > 10);
|
||||
|
||||
const result = enforceLongTermLimitsWithAccounting(entries, store);
|
||||
|
||||
assert.ok(result.kept.length <= 28);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "feedback" && !memory.safetyCritical).length <= 10);
|
||||
assert.ok(result.kept.filter(memory => memory.type === "decision" && !memory.safetyCritical).length <= 10);
|
||||
assert.ok(result.kept.some(memory => memory.safetyCritical));
|
||||
assert.equal(result.kept.filter(memory => memory.type === "feedback" && !memory.safetyCritical).length, 10);
|
||||
assert.equal(result.kept.filter(memory => memory.type === "feedback" && memory.safetyCritical).length, 1);
|
||||
assert.equal(result.kept.filter(memory => memory.type === "feedback").length, 11);
|
||||
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);
|
||||
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