Watch an agent work on a real codebase and the same failure plays out over and over: it hits a Rust compile error, tries a fix, hits it again in another file, tries another fix, hits it a third time. Each attempt consumes tokens, wall time, and user patience. The agent's context window scrolls the earlier failures out of view, and the memory systems available today store facts and preferences, not "this approach is broken."
Vlk² is a single Rust binary that runs over MCP, backed by SQLite. It gives an agent a sense of time: what's happening now (PRESENT), what's already been tried (PAST), and what should guide future decisions (FUTURE). When the same error repeats, it acts.
The temporal state machine
Every memory entry lives in one of four states:
- PRESENT: active context the agent works with
- PAST: archived, out of context, but auditable
- FUTURE: preventive constraints that shape what the agent attempts
- PURGED: forgotten, for the entries that deserve it
A background consolidation agent (30s cycle) scans entries, evaluates importance, archives stale ones, and forgets disconnected ones. Every 30 seconds it also checks the token budget: if PRESENT + FUTURE exceed 8K tokens, the lowest-importance entries get archived until the context fits. The agent's context stays bounded no matter how long the session runs.
Loop detection: fingerprint by type, not by string
String matching fails here. The same error appears with different line numbers, timestamps, and file paths: error[E0277] in lib.rs:10 and error[E0277] in main.rs:220 are the same failure, twice. Vlk² fingerprints errors by language and type:
// fingerprint by type — near-duplicates count as one pattern
fn fingerprint(line: &str) -> Fingerprint {
if line.contains("error[E") { return Rust } // error[E0277]
if line.contains("TS") { return TypeScript } // TS2345
if line.contains("TypeError") { return Python }
if line.contains("panic:") { return Go }
if line.contains("503 Service") { return Http }
if line.contains("expected:") { return Test }
TimestampStripped(line[..80]) // fallback
}
When the same fingerprint appears three times, Vlk² auto-archives the whole loop to PAST and injects a FUTURE constraint into the context. The next time the agent fetches context, it sees something like:
[SYSTEM ANCHOR] error[E0277] has repeated 3 times. This approach is failing; do not retry it. Consider a different strategy.
The agent stops retrying. The system makes the broken approach unavailable.
Why constraints, not answers
A loop-breaking system has to be conservative. It shouldn't tell the agent what to do; it should tell it what not to do and let the model's own reasoning pick the path. That's why injected lessons are revocable: vlk_revoke_future removes a constraint when it was learned from a misdiagnosis, and vlk_pin_memory protects patterns that should never be forgotten.
Every recall also reconsolidates: if a new observation contradicts what the agent believes, the memory rewrites itself rather than accumulating noise.
The verification loop
The core path is E2E-tested with a smoke test that speaks raw JSON-RPC to the binary:
# send the same rust error 3 times, then fetch context
for i in range(3):
rpc({"method": "tools/call", "params": {
"name": "vlk_record_state",
"arguments": {"raw_log": "error[E0277]: cannot add `str` to `str`",
"file_context": f"lib.rs:{i*10}"}}})
r = rpc({"method": "tools/call", "params": {
"name": "vlk_fetch_context", "arguments": {}}})
# assertion: loop archived + constraint injected
assert "E0277" in r.text
assert "SYSTEM ANCHOR" in r.text # → PASS
The binary speaks MCP over stdin/stdout — one JSON-RPC line per message — so it drops into any MCP client: Hermes, Claude Code, Codex, OpenCode. No server to deploy, no port to open. The whole thing is a 6.4MB executable.
Architecture
SQLite does the remembering; Rust does the deciding.
memory_contents (immutable, team-agnostic)
└── FK ──► agent_timeline (PRESENT / PAST / FUTURE / PURGED)
├── memory_metadata (temporal vectors, importance, connectivity)
└── semantic_facts (distilled patterns from errors)
└── fact_contradictions (resolution tracking)
consolidation agent (background tokio, 30s cycle)
SCAN → EVALUATE importance → BUDGET_CHECK → ARCHIVE low-value → FORGET stale
Lessons
- Forgetting is a feature. Without PURGED and the token budget, memory systems grow until they become the context problem they were meant to solve.
- Fingerprint by type, group near-duplicates. String matching flags the same failure as "new" every time.
- Constraints must be revocable. The system will misdiagnose; when it does, the agent has to be able to undo the lesson.
- A specialized fault-detection layer beats a general memory system at its own job. Vlk² runs alongside memory systems and does one thing they don't: break the loop.
Three identical errors, one constraint, zero retries. The mechanism is general: any past→present→future transition an agent cares about can be tracked the same way.