ホーム

Veil帳

Veil is a coding agent built on Pi. It keeps context in a local store, scores what stays in the prompt, and records failed attempts so later work can refer to them.

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.

A conversation summary can omit a detail that becomes useful later. I wanted a way to retain and retrieve individual records as the active context changes.

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 scores context items and moves them between the active prompt and local storage. Eviction frees space in the prompt while keeping records available for retrieval.

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

The memory store uses sqlite-vec, with an in-process embedder and Ollama as a fallback. Model files need to be available locally for offline embedding. The coding agent's model provider is configured separately; choosing a remote model can send context to that provider.

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

Veil uses an FSRS-based retrievability score, adapted from spaced-repetition scheduling. 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.

The scorer combines five metadata signals at fixed weights. It does not call a language model.

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 metadata, task tags, and time produce the same score.

Eviction runs in three stages, using age, scores, and context pressure to choose candidates.

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 controller adjusts the context-pressure threshold within configured limits. It responds to repeated evictions, requests for evicted items, and periods of stability.

eviction threshold · aimd, step 0.05eviction.ts
0.850.60session startlater0.75−0.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 records attempts 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. These are heuristics for deciding when to intervene.

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 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. An event log records changes in the durable store, and the current view is derived from that log.

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

Conflicting beliefs are stored with their 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