Skip to content

Long-running agents

An agent that runs for weeks has different problems from one that runs for a minute. This page is about those.

The failure curve

flowchart LR
  A["week 1<br/>30 memories<br/>retrieval sharp"] --> B["week 4<br/>400 memories<br/>near-duplicates appear"]
  B --> C["month 3<br/>3000 memories<br/>recency ≈ 0 for most<br/>importance decides everything"]
  C --> D["month 6<br/>candidate ceiling in sight<br/>oldest memories at risk"]

Nothing breaks. It degrades, quietly, and the symptom is "retrieval got worse" with no error anywhere.

What changes as the store grows

Recency stops helping

recency = 0.5 ** (age_days / 14). After two months it contributes under 0.005 out of a possible 0.20. In a mature project, ranking is effectively:

score ≈ 0.40 · similarity + 0.25 · importance

Consequence: importance becomes your primary ranking control, and it was set months ago by an agent that had no idea how crowded the project would get.

Set it against a fixed rubric from day one, not relative to what is already stored:

Value Meaning — decide once, apply forever
0.9–1.0 Violating this breaks the system or the contract
0.6–0.8 A decision someone would need to know before changing the design
0.4–0.5 Useful background
0.1–0.3 Ephemeral

Duplicates accumulate

Venkai does not deduplicate. An agent that records "we use Postgres" at the end of every run has 60 near-identical memories after two months, and they can fill the top-5 between them.

Deduplicate before writing:

def record_once(content: str, threshold: float = 0.9, **kw) -> bool:
    """Skip the write if something very close is already stored.
    Returns True if written."""
    existing = requests.get(f"{BASE}/api/context/{PROJECT}/relevant",
                            headers=_H, params={"query": content, "limit": 3},
                            timeout=10).json()["memories"]
    if existing and existing[0]["similarity"] >= threshold:
        return False
    record(content, **kw)
    return True

Calibrate threshold against your provider: hashing similarities sit far lower than semantic ones, so a threshold tuned on one is wrong on the other.

The candidate ceiling approaches

VENKAI_RETRIEVAL_MAX_CANDIDATES defaults to 10 000 and truncates oldest first — so the memories you lose are your foundational constraints, the ones with the highest importance.

The server logs a warning naming the project when this happens. On the hosted API you cannot see that log, so track project size yourself:

curl -s https://api.venkai.fr/api/projects -H "Authorization: Bearer $VENKAI_API_KEY" \
  | python -c "import json,sys; [print(p['contexts'], p['project_id']) for p in json.load(sys.stdin)['projects']]"

Alert at ~7000 and act.

Latency grows linearly

Scoring is O(candidates). It is fine for a long time and then it is not, particularly with the 384-d semantic provider. Measure on your own data.

Patterns that hold up

Split by bounded context, not by time

billing-service          ← one deployable, one set of constraints
billing-service-2026q1   ← ✗ splits by time; the constraints live in the wrong quarter

Project boundaries should follow what an agent needs to know at once. A time-based split scatters the durable rules across projects, and the agent has to query several to get a complete picture — Venkai has no cross-project query.

Correct rather than append

When a decision is reversed, PATCH the original:

requests.patch(f"{BASE}/api/context/{ctx_id}", headers=_H, timeout=10, json={
    "content": "We use Postgres. REVERSED 2026-08: moving to MongoDB, "
               "the join requirement was removed with the new ranking model."
})

Appending a contradiction leaves both retrievable, side by side, with nothing indicating which is current. PATCH recomputes the embedding, records a version, and leaves one truth in the store. The history stays available via GET /api/context/{id}/versions.

Periodic consolidation

Once a month, review the low-value tail and rewrite it:

# what is actually in there, by type
curl -s https://api.venkai.fr/api/impact -H "Authorization: Bearer $VENKAI_API_KEY" \
  | python -c "import json,sys; print(json.load(sys.stdin)['types_distribution'])"

A project that is 90 % fact and 2 % decision is one where the agent is narrating instead of concluding. Fix that at the write site.

Budget by characters, not by count

At scale, limit=5 can mean 200 characters or 40 000. Enforce a character budget client-side — Context optimization.

Multi-agent over time

Several agents on one project share a store with no locking and no conflict detection. Two can write contradictory memories and both will be retrieved with equal standing.

If contradiction matters:

  1. Before writing a decision or constraint, recall against its content.
  2. If something contradicts it, PATCH that memory rather than appending.
  3. Keep agent_id accurate so ?agent= filtering can attribute anything odd.

Monitoring checklist

Watch Where Act when
Memory count per project GET /api/projectscontexts > 7000
Type distribution GET /api/impacttypes_distribution fact dominates
Retrieval latency Your own client-side timing Trending up
Duplicate rate Sample /relevant outputs by eye Repeats in the top-5
Score distribution score on returned memories Top result consistently < 0.35

That last one is the useful early warning: when the best available memory scores poorly, retrieval is returning the least-bad rather than the right thing — and there is no relevance floor to stop it.