Veil帳
Every coding agent session starts blank: the codebase gets rediscovered, the fix that failed yesterday gets retried, and compaction throws out whatever the summarizer guessed was unimportant. Veil is an installable memory layer that keeps an LLM out of the memory path entirely, running on FSRS decay, AIMD eviction, and a record of every approach that already failed.
TypeScript · Go · Pixi · npm · Docker
A session with no memory relearns the codebase every time it starts. It rediscovers a constraint you explained last week. It retries a fix that already failed.
Compaction does not fix this. The summariser keeps whatever looks important by a fixed heuristic and drops the rest, and the dropped part matters more often than the tool assumes.
The auth middleware has to run before the tenant resolver, because the resolver reads the claim the middleware sets, and the fix you tried on Tuesday failed for exactly that reason.
The summariser kept what scored well on its heuristic. The agent retries Tuesday.
Veil replaces that summariser with an eviction system. Every piece of context carries its own decay schedule, so no single pass wipes a session.
The published package is @engrammic/veil. It installs globally and replaces the agent binary inside a project directory.
npm install -g @engrammic/veil cd your-project veil
Embeddings run through sqlite-vec, with the embedder in-process and Ollama as a fallback, so the store works with the network off.
Veil is a fork of pi-mono, Mario Zechner’s agent harness, not a plugin on top of one. The memory layer runs inside the loop: a manifest of what is currently loaded gets appended to the system prompt, and every tool call passes through capture, scoring and eviction on the way in and out.
- before_agent_start
- the manifest of what is loaded gets appended to the system prompt
- beforeToolCall / afterToolCall
- capture, score, evict around every tool call
- turn_end
- status bar, and the tool calls whose context got evicted go dim
const harness = new VeilHarness({ dbPath: '.veil/context.db' })
const config: AgentLoopConfig = {
...baseConfig,
...harness.getHooks(),
}The model can also call the memory layer directly. Twelve tools are registered for it.
| veil_recall | search memory by semantic query or tag, and get IDs back to act on |
| veil_promote | bring an item into active context so it is visible every turn |
| veil_demote | take an item out of active context; it stays in memory for later recall |
| veil_remember | store an insight, a decision, or a fact for later |
| veil_pin | lock an item in context; pinned items survive eviction under pressure |
| veil_unpin | unlock a pinned item and let it be evicted again |
| veil_forget | delete something from every tier; this one cannot be undone |
| veil_hydrate | expand a stub into its full content when the summary is not enough |
| veil_history | search past sessions, not just this one |
| veil_turn_meta | classify this turn: decision, exploration, action, correction, status, intent |
| veil_conflicts | list beliefs that contradict each other on the same subject |
| veil_resolve_conflict | pick which belief wins; the loser gets retracted |
Most cache eviction runs a plain age check: old enough, gone. Veil scores memory with FSRS instead, the scheduler behind spaced-repetition flashcard apps. Each item holds a stability in days, and retrievability falls from it on a power curve, calibrated so an item is still at 0.9 when it reaches its own stability.
- episodic
- S 0.02d
- R 0.28
- what happened this turn
- fact
- S 0.083d
- R 0.51
- a thing the codebase is
- procedural
- S 0.25d
- R 0.72
- how a job gets done here
- decision
- S 0.5d
- R 0.83
- a call that was made
- intent
- ∞
- R 1.00
- what you asked for; never decays
Recall raises stability, and the increment grows as retrievability falls, so an item recalled late gains more than one recalled constantly. The same curve runs twice at different horizons: stability caps at seven days in the live context window and at a year in the durable store.
Deciding what a piece of context is worth never calls the LLM. Five metadata signals combine at fixed weights, and the whole thing is arithmetic.
Recency is FSRS retrievability. Frequency is a log-scaled access count. Relevance is Jaccard overlap between the item’s tags and the current task. Structure asks whether the item points into the code graph. Cognitive weight tracks whether the item was in context when things went well or badly. Procedural items then get a 1.2 multiplier, anything you loaded by hand gets 1.5, and pinned items take a flat boost.
The same inputs produce the same score on every turn.
Eviction runs in three stages, and each one has a real predicate rather than a budget someone guessed.
tier
loaded map, in the prompt
What the model can see this turn. A manifest of it is appended to the system prompt at the start of every agent run, so the model knows what it is holding.
manager.ts
pinned items, and anything typed intent, skip all three stages
cold → hot raises the threshold 0.05, so a run that evicted too eagerly tightens itself
The threshold that decides what counts as low enough moves on its own, borrowing the AIMD shape from TCP congestion control. Nobody sets an eviction budget by hand; the threshold finds its own level from how the session is going.
A higher threshold keeps more in the window. Thrashing lowers it, a quiet stretch raises it, and asking for something already evicted raises it at once. Push the buttons and the controller does what it does in a session: it settles between 0.60 and 0.85 without anyone configuring it.
Turns get the same treatment as context items. The last twelve are protected outright. Older ones score by what kind of turn they were, and a turn the current one still refers to gets rescued: cosine similarity above 0.7 against a recent turn cuts its eviction score.
- correction
- 0.00
- you told the agent it was wrong; never evictable
- intent
- 0.00
- what the session is for
- decision
- 0.10
- a call that was made
- action
- 0.60
- a thing that got done
- status
- 0.70
- a report on the doing
- exploration
- 0.80
- looking around; first out
Corrections and intent carry a weight of zero, so they never become eviction candidates.
Veil keeps a record of every attempt against a goal: what the agent did, the target, the outcome, and a normalised fingerprint of the error. That fingerprint lets the same failure get recognised as the same failure even when the message text drifts.
| 01 | reorder mounts in app.ts | FAIL | e:4a91c0 |
| 02 | init resolver in bootstrap | FAIL | e:7bd233 |
| 03 | reorder mounts in app.ts | FAIL | e:4a91c0 |
| 04 | resolver re-reads the header | PARTIAL | e:0c11ab |
| 05 | reorder mounts in app.ts | FAIL | e:4a91c0 |
A convergence monitor watches the record and escalates in levels. Progress counts as a pass, a partial, a different error pattern, or a different file touched. Anything else is the agent going in circles.
- level 1
- 3× repeat
- the same error pattern three times; a warning goes into the context
- level 2
- 5 failures
- five consecutive failures on one goal; the harness gets a callback
- level 3
- 10 turns
- no measurable progress, or fifteen attempts; halt
Evicted context is demoted, not dropped. It moves out of the prompt into the warm cache at .veil/context.db, and out of there into cold storage, which hands back a pointer the agent can follow. The durable store underneath is an event log: the tape is the truth and everything else is derived from it.
- memory_events
- append only
- assert, retract, reinforce; nothing is ever updated in place
- current_beliefs
- projection
- the readable present, rebuildable from the log
- memory_vectors
- vec0 float[768]
- sqlite-vec index for semantic recall
- memory_fts
- fts5 mirror
- kept in sync by insert and delete triggers
Contradictions are not resolved quietly. Two beliefs that disagree are stored with their full provenance, down to the tool call and the session that produced them, and handed to the model to settle.
Eviction is not silent in the interface. When context leaves the window, the tool calls it came from are dimmed in the transcript, so the record of what the agent no longer knows stays on screen.
> /context
hot18 items · 12,480 tok
warm143 items · .veil/context.db
coldbehind pointers
last evict2 turns ago · 3 items
✓read src/server.ts
✓grep "tenantResolver"
⌁read src/auth/middleware.ts
⌁read migrations/0004_tenants.sql
✓edit src/server.ts
- package
- @engrammic/veil
- v0.2.0, MIT, node ≥ 22.19, nine workspaces
- context engine
- 30,476 loc
- typescript; the scorer, the tiers, the monitors
- tests
- 358 files
- 55 of them cover the context engine alone
- upstream
- pi-mono
- four workspaces keep their upstream names for merges