feat(memory): TTL expiry — supersede stale memories after 28/90 days
- Add expireStaleMemories(unstartedTtlDays=28, maxTtlDays=90) to sf-db.js - Never-accessed (hit_count=0) memories expire after 28 days - All memories expire after 90 days regardless of hit_count - Marks superseded_by='ttl-expired' (non-destructive, same as CAP_EXCEEDED pattern) - Returns count of expired memories (non-fatal on failure) - Call from auto-start.js after DB opens at autonomous session start - Logs warning with count if any memories expired - Catches errors silently — TTL failure never blocks autonomous start Mirrors Copilot Memory's 28-day TTL model learned from research. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
d2eda0cc12
commit
692328ad45
2 changed files with 42 additions and 1 deletions
|
|
@ -87,7 +87,7 @@ import {
|
|||
updateSessionLock,
|
||||
} from "./session-lock.js";
|
||||
import { getSessionModelOverride } from "./session-model-override.js";
|
||||
import { getMilestone, isDbAvailable, openDatabase } from "./sf-db.js";
|
||||
import { expireStaleMemories, getMilestone, isDbAvailable, openDatabase } from "./sf-db.js";
|
||||
import { snapshotSkills } from "./skill-discovery.js";
|
||||
import { deriveState, isGhostMilestone } from "./state.js";
|
||||
import { isClosedStatus } from "./status-guards.js";
|
||||
|
|
@ -1036,6 +1036,18 @@ export async function bootstrapAutoSession(
|
|||
}
|
||||
// Initialize routing history
|
||||
initRoutingHistory(s.basePath);
|
||||
// Expire stale memories to prevent poisoning future sessions.
|
||||
// Never-accessed memories expire after 28 days; all memories after 90 days.
|
||||
if (isDbAvailable()) {
|
||||
try {
|
||||
const expired = expireStaleMemories();
|
||||
if (expired > 0) {
|
||||
logWarning("engine", `Expired ${expired} stale ${expired === 1 ? "memory" : "memories"} (TTL exceeded)`);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal — TTL expiry failure must not block autonomous start
|
||||
}
|
||||
}
|
||||
// Restore the model that was active when auto bootstrap began (#650, #2829).
|
||||
if (startModelSnapshot) {
|
||||
s.autoModeStartModel = {
|
||||
|
|
|
|||
|
|
@ -7720,6 +7720,35 @@ export function decayMemoriesBefore(cutoffTs, now) {
|
|||
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`)
|
||||
.run({ ":now": now, ":cutoff": cutoffTs });
|
||||
}
|
||||
/**
|
||||
* Supersede memories that have exceeded their TTL.
|
||||
*
|
||||
* Purpose: prevent stale memories from silently poisoning future sessions.
|
||||
* Mirrors Copilot Memory's 28-day TTL model — memories that were never
|
||||
* accessed expire sooner; memories actively used get a longer lease.
|
||||
*
|
||||
* Rules:
|
||||
* - Never accessed (hit_count = 0) + older than unstartedTtlDays → expire
|
||||
* - Any memory older than maxTtlDays → expire regardless of hit_count
|
||||
*
|
||||
* Consumer: called at autonomous mode startup from auto-start.js.
|
||||
* Returns the number of memories superseded.
|
||||
*/
|
||||
export function expireStaleMemories(unstartedTtlDays = 28, maxTtlDays = 90) {
|
||||
if (!currentDb) return 0;
|
||||
const now = new Date().toISOString();
|
||||
const cutoffUnstarted = new Date(Date.now() - unstartedTtlDays * 86_400_000).toISOString();
|
||||
const cutoffMax = new Date(Date.now() - maxTtlDays * 86_400_000).toISOString();
|
||||
const result = currentDb
|
||||
.prepare(`UPDATE memories SET superseded_by = 'ttl-expired', updated_at = :now
|
||||
WHERE superseded_by IS NULL
|
||||
AND (
|
||||
(hit_count = 0 AND updated_at < :cutoff_unstarted)
|
||||
OR updated_at < :cutoff_max
|
||||
)`)
|
||||
.run({ ":now": now, ":cutoff_unstarted": cutoffUnstarted, ":cutoff_max": cutoffMax });
|
||||
return result.changes ?? 0;
|
||||
}
|
||||
export function supersedeLowestRankedMemories(limit, now) {
|
||||
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
||||
currentDb
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue