Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/memory/jme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,74 @@ describe("JME — consolidateAll (nightly batch)", () => {
expect(result.factsExtracted).toBe(0);
expect(jmeStats().turnsTotal).toBe(1); // NOT deleted — unconsumed data
});

// ── Phase 4: auto-prune when ceiling is active ───────────────────────────

it("Phase 4: auto-prunes low-confidence facts when embedding count >= VECTOR_CEILING_WARN", async () => {
const { consolidateAll, VECTOR_CEILING_WARN, PHASE4_CONFIDENCE_FLOOR } =
await getJme();

// Insert VECTOR_CEILING_WARN low-confidence facts (confidence below Phase 4
// floor, ts > 30 days ago so they qualify for deletion)
const oldTs = Date.now() - 31 * 24 * 60 * 60 * 1000;
const stmt = mockDb.prepare(
`INSERT INTO jme_facts (source_task, ts, fact_text, category, confidence, embedding)
VALUES (?, ?, ?, ?, ?, ?)`,
);
// Use a tiny 4-byte blob so embedding IS NOT NULL (counted by the ceiling query)
const tinyEmbedding = Buffer.alloc(4);
for (let i = 0; i < VECTOR_CEILING_WARN; i++) {
stmt.run("consolidator-nightly", oldTs, `low-conf fact ${i}`, "event", 0.3, tinyEmbedding);
mockDb
.prepare(`INSERT INTO jme_facts_fts(rowid, fact_text) VALUES (last_insert_rowid(), ?)`)
.run(`low-conf fact ${i}`);
}

// Also insert one high-confidence fact that should survive
mockDb
.prepare(
`INSERT INTO jme_facts (source_task, ts, fact_text, category, confidence)
VALUES ('t-keep', ?, 'important fact', 'preference', 0.9)`,
)
.run(oldTs);

// No turns to process — consolidateAll still performs the ceiling check
inferMock.mockResolvedValueOnce({ content: "[]" });

const result = await consolidateAll();

// Phase 4 pruned the low-confidence facts
expect(result.factsPruned).toBeGreaterThanOrEqual(VECTOR_CEILING_WARN);
// High-confidence fact is untouched
const remaining = mockDb
.prepare(`SELECT fact_text FROM jme_facts WHERE fact_text = 'important fact'`)
.get();
expect(remaining).toBeTruthy();
});

it("Phase 4: factsPruned is 0 when embedding count is below VECTOR_CEILING_WARN", async () => {
const { consolidateAll, VECTOR_CEILING_WARN } = await getJme();

// Insert fewer facts than the ceiling threshold
const oldTs = Date.now() - 31 * 24 * 60 * 60 * 1000;
const tinyEmbedding = Buffer.alloc(4);
for (let i = 0; i < VECTOR_CEILING_WARN - 1; i++) {
mockDb
.prepare(
`INSERT INTO jme_facts (source_task, ts, fact_text, category, confidence, embedding)
VALUES (?, ?, ?, ?, ?, ?)`,
)
.run("t1", oldTs, `fact ${i}`, "event", 0.3, tinyEmbedding);
}

insertSettledTurn("task-x", "user", "hola");
inferMock.mockResolvedValueOnce({ content: "[]" });

const result = await consolidateAll();

// Below ceiling — no Phase 4 prune
expect(result.factsPruned).toBe(0);
});
});

// ---------------------------------------------------------------------------
Expand Down
37 changes: 33 additions & 4 deletions src/memory/jme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export const TEMPORAL_DEDUP_THRESHOLD = 0.85;
*/
export const VECTOR_CEILING_WARN = 400;

/**
* Phase 4: When the vector ceiling is active, facts below this confidence
* threshold are pruned during consolidation — more aggressive than the
* baseline 0.4 floor used in pruneExpiredFacts(). Clears marginal facts
* before they degrade recall quality by filling the LIMIT 500 query window.
*/
export const PHASE4_CONFIDENCE_FLOOR = 0.5;

// ── Types ────────────────────────────────────────────────────────────────────

export type JmeTurnRole = "user" | "jarvis";
Expand Down Expand Up @@ -723,6 +731,8 @@ export interface ConsolidateResult {
factsInserted: number;
factsSkipped: number;
factsSuperseded: number;
/** Phase 4: facts pruned during auto-prune when ceiling was active. */
factsPruned: number;
}

/**
Expand All @@ -746,6 +756,7 @@ export async function consolidateAll(): Promise<ConsolidateResult> {
factsInserted: 0,
factsSkipped: 0,
factsSuperseded: 0,
factsPruned: 0,
};

try {
Expand All @@ -767,10 +778,12 @@ export async function consolidateAll(): Promise<ConsolidateResult> {
role: JmeTurnRole;
content: string;
}>;
// Phase 3 Pieza 2: vector ceiling surveillance.
// Phase 3 Pieza 2 + Phase 4: vector ceiling surveillance + auto-prune.
// When jme_facts with embeddings approaches the LIMIT 500 in queryMemory(),
// recall quality degrades (oldest facts get cut off). Warn early at 400 so
// there's runway before it becomes a problem. Phase 4 will add auto-pruning.
// recall quality degrades (oldest facts get cut off). Phase 4: when the
// ceiling is active, auto-prune low-confidence facts (threshold raised to
// PHASE4_CONFIDENCE_FLOOR=0.5) before extraction, so the query window has
// room for the facts that matter.
const embeddingCount = (
db
.prepare(
Expand All @@ -780,8 +793,24 @@ export async function consolidateAll(): Promise<ConsolidateResult> {
).n;
if (embeddingCount >= VECTOR_CEILING_WARN) {
console.warn(
`[jme] consolidateAll: vector ceiling warning — ${embeddingCount} facts with embeddings (warn threshold=${VECTOR_CEILING_WARN}, query LIMIT=500). Consider pruning low-confidence facts.`,
`[jme] consolidateAll: vector ceiling active — ${embeddingCount} facts (warn=${VECTOR_CEILING_WARN}). Auto-pruning low-confidence facts (threshold=${PHASE4_CONFIDENCE_FLOOR}).`,
);
// Phase 4 auto-prune: delete expired AND low-confidence facts using the
// elevated Phase 4 floor. This is a superset of pruneExpiredFacts() —
// it catches facts that the baseline 0.4 threshold would have kept.
const pruneResult = db
.prepare(
`DELETE FROM jme_facts
WHERE (expires_at IS NOT NULL AND expires_at < ?)
OR (confidence < ? AND ts < ?)`,
)
.run(Date.now(), PHASE4_CONFIDENCE_FLOOR, Date.now() - 30 * 24 * 60 * 60 * 1000);
result.factsPruned = pruneResult.changes;
if (result.factsPruned > 0) {
console.log(
`[jme] consolidateAll: phase4 auto-prune removed ${result.factsPruned} low-confidence facts`,
);
}
}

if (turns.length === 0) return result;
Expand Down
3 changes: 2 additions & 1 deletion src/rituals/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,8 +910,9 @@ function scheduleJmeConsolidation(): void {
await import("../memory/jme.js");
const result = await consolidateAll();
const pruned = pruneStaleTurns();
const p4Tag = result.factsPruned > 0 ? `, ${result.factsPruned} p4-pruned` : "";
console.log(
`[rituals] jme-consolidate: ${result.turnsProcessed} turns → ${result.factsExtracted} facts (${result.factsInserted} ins, ${result.factsSkipped} skip, ${result.factsSuperseded} sup), ${pruned} stale pruned`,
`[rituals] jme-consolidate: ${result.turnsProcessed} turns → ${result.factsExtracted} facts (${result.factsInserted} ins, ${result.factsSkipped} skip, ${result.factsSuperseded} sup), ${pruned} stale pruned${p4Tag}`,
);
} catch (err) {
console.error("[rituals] jme-consolidate failed:", err);
Expand Down
Loading