release: v2.2.1 — Windows embeddings fix + backfill safety + first-run guide (#105)

* patch(backfill-safety): VESTIGE_BACKFILL_AUTOFIRE gate (default OFF) + bounded promote_memory_backfill

Off-by-default env gate around step-8.5 auto-fire in run_consolidation (decouples
backfill from consolidation cadence). New promote_memory_backfill caps stability at
MIN(stability*1.5, stability+365.0) (the bound retroactive_backfill.rs:300 already
computes but discarded); both backfill entry points use it. Fixes the false 'capped'
comment. Cloud-sync excluded at build (--no-default-features). Pending upstream PR to
samvallad33/vestige. omega-backfill-safety-v2.2.0 off tag v2.2.0 (3bcd4667).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* release: v2.2.1 — Windows embeddings fix + backfill safety + first-run guide

Board-clearing patch release.

Fixes:
- #101 Windows embeddings: release.yml already restores vector-search on the
  x86_64-pc-windows-msvc target (merged in #102); this release rebuilds the
  Windows binary so users actually get working embeddings.
- #103 Retroactive Salience Backfill safety (from community PR #104, adjusted):
  * promote_memory_backfill bounds the stability multiply to
    MIN(stability*1.5, stability+365.0) on both auto-fire and manual paths.
  * VESTIGE_BACKFILL_AUTOFIRE gate — default ON (preserves the shipped/documented
    v2.2.0 behavior), disable with 0/false/off/no. Env value is trimmed.
  * Corrected the false "capped" comment and the promote_memory_backfill doc.
  * Added 3 tests: +365 cap binds, *1.5 multiply below crossover, gate parsing.

Docs:
- #83 First-Run: new docs/GETTING-STARTED.md + README pointer.
- Consolidated roadmap issues #82,#84-#92 into docs/ROADMAP.md (Tracked Issues).
- Documented VESTIGE_BACKFILL_AUTOFIRE in docs/CONFIGURATION.md.
- CHANGELOG v2.2.1 entry.

Version bumped to 2.2.1 across all manifests + Cargo.lock + dashboard build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Peter Lauzon <inbijiburu@protonmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-07-02 12:02:42 -05:00 committed by GitHub
parent 9d15cce7ab
commit b380e36f1f
391 changed files with 5130 additions and 4359 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-core"
version = "2.2.0"
version = "2.2.1"
edition = "2024"
rust-version = "1.91"
authors = ["Vestige Team"]

View file

@ -1656,6 +1656,42 @@ impl SqliteMemoryStore {
.ok_or_else(|| StorageError::NotFound(id.to_string()))
}
/// Backfill-specific promote: identical retrieval/retention boost to
/// `promote_memory`, but the stability multiply is CAPPED at an additive
/// +365-day ceiling: `MIN(stability * 1.5, stability + 365.0)`. The `1.5`
/// factor preserves the multiplier `promote_memory` already applied; the
/// `+365` ceiling is the same additive bound `retroactive_backfill.rs`
/// uses for its reason string (that module pairs +365 with a 2.5 factor
/// for display only — this DB write intentionally keeps 1.5 so backfill
/// promotion strength is unchanged, just bounded). Repeated per-(cause,
/// failure) backfill promotions therefore cannot inflate stability without
/// bound. Used by the step-8.5 auto-fire path and the manual `backfill` tool.
pub fn promote_memory_backfill(&self, id: &str) -> Result<KnowledgeNode> {
let now = Utc::now();
{
let writer = self
.writer
.lock()
.map_err(|_| StorageError::Init("Writer lock poisoned".into()))?;
writer.execute(
"UPDATE knowledge_nodes SET
last_accessed = ?1,
retrieval_strength = MIN(1.0, retrieval_strength + 0.20),
retention_strength = MIN(1.0, retention_strength + 0.10),
stability = MIN(stability * 1.5, stability + 365.0)
WHERE id = ?2",
params![now.to_rfc3339(), id],
)?;
}
let _ = self.log_access(id, "promote");
let _ = self.set_waking_tag(id);
self.get_node(id)?
.ok_or_else(|| StorageError::NotFound(id.to_string()))
}
/// Demote a memory (thumbs down) - used when a memory led to a bad outcome
/// Significantly reduces retrieval strength so better alternatives surface
/// Does NOT delete - the memory stays for reference but ranks lower
@ -3659,10 +3695,30 @@ impl SqliteMemoryStore {
// consolidation pass IS the offline window. Bounded on every axis so a
// noisy day cannot trigger a promotion storm, and idempotent across cycles
// via a durable causal edge (so the same cause is promoted once per
// failure, not every cycle — promote_memory's stability boost is capped
// but would still inflate without this guard).
// failure, not every cycle).
//
// OPT-OUT (backfill-safety, v2.2.1): auto-fire is ON by default — it shipped
// and was documented in v2.2.0, so we keep the behavior — but is now bounded
// and disableable. It mutates FSRS scores on the canonical store and can lift
// a memory across a downstream consolidation floor, so a consumer that reads
// `stability` as a durability gate can turn it off with
// VESTIGE_BACKFILL_AUTOFIRE=0 (or false/off/no). The `backfill` MCP tool + CLI
// remain available for on-demand, operator-driven backfill regardless of the
// gate. The promote is bounded: both the auto-fire and manual paths call
// promote_memory_backfill (stability = MIN(stability*1.5, stability+365)) so
// repeated per-(cause, failure) promotions cannot inflate without bound (the
// prior comment claimed promote_memory was capped — it was not).
let backfill_autofire = std::env::var("VESTIGE_BACKFILL_AUTOFIRE")
.map(|v| {
let v = v.trim();
!(v.eq_ignore_ascii_case("false")
|| v.eq_ignore_ascii_case("off")
|| v.eq_ignore_ascii_case("no")
|| v == "0")
})
.unwrap_or(true);
let mut backfilled_causes = 0i64;
{
if backfill_autofire {
use crate::advanced::retroactive_backfill::{
self as rb, BackfillCandidate, FailureEvent, RetroactiveBackfill,
};
@ -3755,7 +3811,7 @@ impl SqliteMemoryStore {
if self.save_connection(&conn).is_err() {
continue;
}
if self.promote_memory(&cause.memory_id).is_ok() {
if self.promote_memory_backfill(&cause.memory_id).is_ok() {
backfilled_causes += 1;
}
}
@ -13230,4 +13286,97 @@ mod tests {
);
});
}
// Seed a node's stability directly via the scheduling seam so the +365 cap
// in promote_memory_backfill is actually exercised (a freshly ingested node
// has low stability where the *1.5 multiply, not the additive ceiling, wins).
fn seed_stability(s: &Storage, id: &str, stability: f64) {
use crate::storage::memory_store::{MemoryStoreSend, SchedulingState};
rt().block_on(async {
let state = SchedulingState {
memory_id: uuid::Uuid::parse_str(id).unwrap(),
stability,
difficulty: 0.4,
retrievability: 0.8,
last_review: Some(chrono::Utc::now()),
next_review: Some(chrono::Utc::now() + chrono::Duration::days(7)),
reps: 3,
lapses: 0,
};
MemoryStoreSend::update_scheduling(s, &state)
.await
.unwrap();
});
}
#[test]
fn promote_memory_backfill_caps_stability_at_plus_365() {
// Above the crossover (stability=730) the additive +365 ceiling must win
// over the *1.5 multiply, so repeated backfill promotions cannot inflate
// stability without bound. This is the bound issue #103 asked us to apply.
let s = create_test_storage();
let node = s
.ingest(IngestInput {
content: "high-stability cause memory".to_string(),
node_type: "fact".to_string(),
..Default::default()
})
.unwrap();
seed_stability(&s, &node.id, 1000.0);
let promoted = s.promote_memory_backfill(&node.id).unwrap();
// 1000 * 1.5 = 1500 (uncapped) vs 1000 + 365 = 1365 (capped). Cap wins.
assert!(
(promoted.stability - 1365.0).abs() < 1e-6,
"expected additive +365 cap (1365.0), got {} (uncapped would be 1500.0)",
promoted.stability
);
}
#[test]
fn promote_memory_backfill_uses_multiply_below_crossover() {
// Below the crossover the *1.5 multiply wins (the cap never binds), so
// backfill promotion strength is unchanged from the old promote_memory.
let s = create_test_storage();
let node = s
.ingest(IngestInput {
content: "low-stability cause memory".to_string(),
node_type: "fact".to_string(),
..Default::default()
})
.unwrap();
seed_stability(&s, &node.id, 10.0);
let promoted = s.promote_memory_backfill(&node.id).unwrap();
// 10 * 1.5 = 15 (multiply) vs 10 + 365 = 375 (cap). Multiply wins.
assert!(
(promoted.stability - 15.0).abs() < 1e-6,
"expected *1.5 multiply (15.0) below crossover, got {}",
promoted.stability
);
}
#[test]
fn backfill_autofire_gate_defaults_on_and_reads_opt_out() {
// v2.2.1 opt-out semantics: unset => ON (preserves shipped v2.2.0
// behavior); explicit 0/false/off/no => OFF; anything else => ON.
fn parse(v: Option<&str>) -> bool {
v.map(|v| {
let v = v.trim();
!(v.eq_ignore_ascii_case("false")
|| v.eq_ignore_ascii_case("off")
|| v.eq_ignore_ascii_case("no")
|| v == "0")
})
.unwrap_or(true)
}
assert!(parse(None), "unset must default ON");
assert!(parse(Some("1")), "1 is ON");
assert!(parse(Some("true")), "true is ON");
assert!(parse(Some("anything")), "unrecognized is ON");
assert!(!parse(Some("0")), "0 is OFF");
assert!(!parse(Some("false")), "false is OFF");
assert!(!parse(Some("OFF")), "OFF (case-insensitive) is OFF");
assert!(!parse(Some(" no ")), "whitespace-padded no is OFF (trim)");
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-mcp"
version = "2.2.0"
version = "2.2.1"
edition = "2024"
description = "Cognitive memory MCP server for AI agents - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research"
authors = ["samvallad33"]
@ -60,7 +60,7 @@ path = "src/bin/cli.rs"
# Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are
# toggled via vestige-mcp's own feature flags below so `--no-default-features`
# actually works (previously hardcoded here, which silently defeated the flag).
vestige-core = { version = "2.2.0", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
vestige-core = { version = "2.2.1", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
# ============================================================================
# MCP Server Dependencies

View file

@ -188,8 +188,10 @@ pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Valu
.unwrap_or_default();
let mut did_promote = false;
if promote {
// promote_memory boosts retrieval strength + reps (the FSRS promote knob)
did_promote = storage.promote_memory(&cause.memory_id).is_ok();
// promote_memory_backfill boosts retrieval strength + reps (the FSRS
// promote knob) with a bounded stability multiply — shared with the
// step-8.5 auto-fire path (omega-backfill-safety patch, pending upstream).
did_promote = storage.promote_memory_backfill(&cause.memory_id).is_ok();
}
promoted.push(json!({
"memory_id": cause.memory_id,