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:
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:
- Before writing a
decisionorconstraint, recall against its content. - If something contradicts it,
PATCHthat memory rather than appending. - Keep
agent_idaccurate so?agent=filtering can attribute anything odd.
Monitoring checklist¶
| Watch | Where | Act when |
|---|---|---|
| Memory count per project | GET /api/projects → contexts |
> 7000 |
| Type distribution | GET /api/impact → types_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.