首页

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.

session two, turn one

auth middleware has to because resolver the the middleware and you on Tuesday 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.

where it sits in the loopextension.ts · harness.ts
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.

tools.ts · exposed to the model
veil_recallsearch memory by semantic query or tag, and get IDs back to act on
veil_promotebring an item into active context so it is visible every turn
veil_demotetake an item out of active context; it stays in memory for later recall
veil_rememberstore an insight, a decision, or a fact for later
veil_pinlock an item in context; pinned items survive eviction under pressure
veil_unpinunlock a pinned item and let it be evicted again
veil_forgetdelete something from every tier; this one cannot be undone
veil_hydrateexpand a stub into its full content when the summary is not enough
veil_historysearch past sessions, not just this one
veil_turn_metaclassify this turn: decision, exploration, action, correction, status, intent
veil_conflictslist beliefs that contradict each other on the same subject
veil_resolve_conflictpick 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.

retrievability · R(t) = (1 + 19/81 · t/S)^-0.5fsrs.ts
0.00.51.00d1d3d5d7devict 0.10episodicepisodic: initial stability 0.02dfactfact: initial stability 0.083dproceduralprocedural: initial stability 0.25ddecisiondecision: initial stability 0.5dintentintent: initial stability never decays1.0d
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.

score weights · fixed
relevance0.30
recency0.25
frequency0.15
structure0.15
cognitive0.15

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.

eviction cascademanager.ts

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.

eviction threshold · aimd, step 0.05eviction.ts
0.850.60session startlater0.750.05 thrash again+0.05 re-request miss+0.05 5 min idle

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.

turn weights · higher goes first
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.

goal · auth-middleware-orderingattempt record
01reorder mounts in app.tsFAILe:4a91c0
02init resolver in bootstrapFAILe:7bd233
03reorder mounts in app.tsFAILe:4a91c0
04resolver re-reads the headerPARTIALe:0c11ab
05reorder mounts in app.tsFAILe:4a91c0
convergence: 3× e:4a91c0 on one goalhard stop

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.

convergence monitor
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.

durable store · schema v2
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.

veil · ~/projects/apictx 41% · threshold 0.70

> /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

the two dimmed calls lost their context · drawing, not a capture
repository
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