Grounds on (exists today, verified on
main): src/gaia/agents/base/memory_store.py (procedures table :283, put_skill :2532, search_skills :2674, supersede_skill :2735, touch_skills :2756) · skill_synthesis.py (reconcile_and_store :653, _nearest_enabled_procedure :580, SIMILARITY_TAU :58) · procedural_memory.py (recall_skill :181, the procedures FAISS index :50) · tool_loader.py (CORE ∪ SKILL ∪ SEMANTIC :224, DEFAULT_MAX_TOOLS = 14 :72) · agent.py (_compose_system_prompt :596) · the email agent’s shipped preference store (preference_tools.py:63) and trust ledger (trust.py:336).Proposed (not written yet): skill_deltas (schema v4), the delta grammar and its section anchorer, EffectiveSkill resolution, the staged-write consent gate, the rebase/re-anchor pass, the learning budget, and gaia skill deltas. Every such symbol is marked PROPOSED where it appears.In flight (do not treat as shipped): the Phase 1 gaia.skills loader — SkillManager, Agent.load_skill / unload_skill / get_skills_system_prompt — lives on PR #2669, open and unmerged. src/gaia/skills/ does not exist on main. Also in flight: the Skill → DistilledProcedure rename (part of #2671). This document uses the new name DistilledProcedure throughout for the synthesis intermediate; on main that class is still skill_synthesis.Skill (:159).Component: the learned overlay on an authored skill — the runtime half of Agent Skills v2 (issue #2674, milestone Agent Skills v2: Adaptive Skills).Module:
gaia.agents.base (extends the shipped memory layer) + gaia.skills (extends the in-flight loader). No overlay code exists. The design is grounded on the live procedures corpus, the live tool loader, and the live email-agent preference/trust stores.Status: Architecture = decided (this revision, after a code review of the shipped machinery plus an adversarial review of the milestone). Runtime = proposed. Four claims the umbrella issue treats as settled were corrected by the code review — see What the code review corrected — and nine challenges from the adversarial pass moved four of the five riskiest items out of v1, see Adversarial review. Skill Format owns the on-disk field grammar; Skill Synthesis owns the synthesized-procedure loop; this document owns the overlay on an authored skill.Sub-issues this document specifies: #2677 (tiered store), #2678 (resolution + off-states), #2679 (write path), #2680 (rebase), #2681 (legibility), #2682 (budget + consolidation + decay), #2683 (email reference agent). Three scopes are not yet covered by any issue: the learning trigger, privacy/retention/deletion, and outcome attribution.Target consumers: #888 (loader/format — blocking), #2671 (graduation bridge), #2468 (injection analyzer), #2466 (email reference agent), #1451 (tool-loader SKILL term), #2672 (skill sandbox — see local-capability skills), #2676 + #2686 (the two shipped-path defects v2 inherits).Why this exists
A skill shipped in milestone 60 behaves identically on day 1 and day 300. That is the point of an authored skill — it is a portable, shareable, reviewable artifact. But it is also generic by necessity:triage-inbox cannot know that
[email protected] outranks everything else in your mailbox, and it must not,
because the file is publishable.
So the capability has to grow somewhere that is not the file. This document
specifies where: a per-user, provenance-tagged, typed overlay whose effective
value is base ⊕ deltas, where discarding the overlay returns exactly the
shipped behavior.
The hard constraint is that GAIA runs locally. On the default profile the model
is Gemma-4-E4B (~4B, lemonade_client.py:124)
inside a 32,768-token NPU window (NPU_CTX_SIZE,
:174; 65,536 on
GPU/CPU, :173).
A learning design that writes prose into that window is a slow context leak, and
re-prefilling an NPU window costs far more than the learning saves. The
constraint is what makes this design better rather than merely different: most
of what an agent learns is not prose, and the cheap representations are already
sitting in the codebase unused for this purpose.
Prior art (credit before contribution)
GAIA does not invent adaptive skills. This design was stress-tested against shipped implementations rather than derived in isolation, and three of its mechanisms are adopted wholesale. Crediting the prior art before the contribution is a repo convention, not a courtesy — see Skill Format → Prior art and #893.
GAIA’s contribution is the part none of the above solves: applying a learned
overlay to a third-party authored skill that can be updated upstream, which
requires typed, section-anchored deltas and a rebase story (Hermes never faces
this — it has no authored base to update against); tiered representation, so
a lesson is recorded at the cheapest layer that can hold it instead of always as
prose; and empirical promotion gated on a measured success rate rather than
the model’s own judgment that a skill is wrong — which matters more here than
anywhere, because the judge is a 4B local model.
What the code review corrected
Before designing anything, the shipped machinery was read end to end. Three working assumptions from the umbrella issue’s discussion did not survive, and one open question turned out to be already answered — inconsistently.Correction 1: procedures cannot hold delta rows
The procedures table cannot hold a delta row without lying about it. Every
column and every read path assumes the row is a standalone, recallable
procedure:
when_to_useandmarkdown_bodyareNOT NULL(memory_store.py:286-287) andput_skillraises on either being blank (:2580-2583). A tier-2 delta (“followup_hours = 48”) has neither a trigger sentence nor a Markdown body. Storing one means inventing both. SQLite cannot drop aNOT NULLwithout rebuilding the table — so the “additive migration” story breaks immediately.- The reconcile match scan would eat deltas.
_nearest_enabled_procedurescans every enabled, non-superseded row with an embedding (skill_synthesis.py:620-625) and supersedes the nearest match above τ. A delta row carrying an embedding becomes a supersede target for an unrelated synthesized candidate. - The recall FAISS index would surface deltas as procedures.
_rebuild_proc_faiss_indexindexes every enabled non-superseded row (procedural_memory.py:80-82), andrecall_skillinjects whatever it finds. A fragment would be presented as a complete proven procedure — the exact failure #2676 describes.
WHERE kind = 'procedure' guard to every
existing read path — three call sites today, and every future one, each a silent
corruption if forgotten. The correct reading of “one store” is: one SQLite
file, one migration chain, one lineage idiom (superseded_by + enabled, never
DELETE), one embedder — and a sibling skill_deltas table. That keeps the
procedures corpus meaning exactly what it means today.
Correction 2: tier 1 is bounded, not free
Admitting a recalled procedure’stools_required renders that tool’s schema into
the AVAILABLE TOOLS block. Below max_tools that is real added text (~40–80
tokens per tool); at the cap the loader LRU-evicts a non-CORE tool and the cost
is genuinely net-zero (tool_loader.py:326-337).
The honest claim: tier 1 adds no prose and is hard-bounded by DEFAULT_MAX_TOOLS = 14
regardless of how much is learned. That is still categorically cheaper than
prose — an unbounded corpus of tool-selection priors costs at most 14 schemas —
but “zero” overstates it.
There is also a security property here worth naming, because it falls out of the
existing code rather than needing enforcement: the SKILL term drops any name
absent from the live registry (tool_loader.py:316-323),
and the loader never touches _TOOL_REGISTRY at all
(:5-8).
So a tier-1 delta is structurally incapable of adding a tool the agent does
not already have. It can only reorder what is already there.
Correction 3: the KV-cache premise is already broken
_refresh_recalled_skills runs every user turn (memory.py:2053)
and calls rebuild_system_prompt() whenever the recalled set changes
(procedural_memory.py:418-425).
Mixin fragments — including the recalled-procedure block and (on PR #2669) the
authored skills block — are composed first
(agent.py:617-618),
while the deliberately volatile tools block was moved last precisely to
protect the prefix (:625-651).
The docstring three lines above the refresh call even states that “the system
prompt is left frozen so the LLM inference engine can reuse its KV cache across
turns” (memory.py:2038-2039) —
and then unfreezes it from the front.
So the requirement (“deltas apply at session boundaries only”) is right, but it
is not sufficient and not yet true of the shipped recall path. Two rules,
not one:
- Timing: learned content is resolved once per session, at session start. Never mid-turn. (Cold cache at session start makes position irrelevant there.)
- Position: if any mid-session application is ever permitted, the learned fragment must render in the volatile tail alongside the tools block, not in the mixin-prompt prefix. Tier 1 already satisfies this by construction — it lands in the tools block.
Correction 4: the empirical promotion gate has no counter
The design’s central safety claim — promote on measured track record, never on model judgment — rests onsuccess_count / attempt_count. Those columns are
written once, at insert, from the cluster that produced the row, and never
updated again.
reconcile_and_store always inserts with skill_id=None
(skill_synthesis.py:693-704),
so put_skill’s update branch — the only UPDATE … success_count in the store
(memory_store.py:2613) —
is unreachable from the synthesis path. There is no increment, no
record_procedure_outcome, nothing. A recalled procedure that is used and fails
twenty times carries the same counters it was born with.
Worse, what those counters measure is not what the design assumes. iter_sessions
increments success_count per tool call that did not error
(memory_store.py:2855-2858),
so MIN_SUCCESS_RATE = 0.80 (skill_synthesis.py:55)
reads “80% of tool calls returned without raising” — not “the user got what they
wanted.” A procedure can be born with a 100% rate having produced a completely
wrong answer, provided every tool returned cleanly.
So the empirical gate as it stands is a proxy measured on the raw sessions
before distillation, not a track record of the artifact after adoption. Two
consequences for v2:
- Outcome attribution is a prerequisite, not a detail. Something must record
“delta D (or procedure P) was in context on this turn, and the turn’s outcome
was positive/negative,” and increment the right row. The shape already exists
in the email agent —
TrustLedger.record_outcome(db, action_type, scope, positive)keyed per scope (trust.py:358) — but nothing links a delta to a scope. This needs its own issue; without it #2679’s “promotion requires a track record, not a model assertion” is unimplementable for deltas. - The outcome signal must be user-observable, not tool-return-code. A correction, an undo, an accepted suggestion — the things the trust ledger already counts. Tool-call non-error is available and nearly worthless as a quality signal, and it is what would get used by default if nobody says otherwise.
The open question that was already answered inconsistently
“Per-agent or per-user?” has a de-facto answer today, and it differs by agent.procedures has no agent or context column
(memory_store.py:283-299),
so scope is decided by which DB file the host opens: the email agent passes its
own db_path (agent.py:637-639),
while ChatAgent takes the default ~/.gaia/memory.db
(memory_store.py:354-357).
Meanwhile tier-0 preferences live in the email agent’s own state.db
(preference_tools.py:63) —
per-agent by construction. Deltas must carry an explicit scope column rather
than inheriting whatever DB the host happened to open.
Adversarial review: the nine challenges
A second review pass challenged nine points. All nine are addressed; two are accepted only in part, and one is accepted further than proposed.
The net effect is a materially smaller v1: no tier classifier, no tier-2
grammar, no selective re-anchoring, no deltas on capability-bearing skills. What
remains is the spine — a delta store, an appended learned block, tier-0/1
wiring, the consent gate, legibility, a budget, and orphan-on-change. Four of the
five things most likely to be wrong on the first attempt are deferred behind
evidence rather than built speculatively.
Decided design
The three layers, one direction of trust
The effective skill is
base ⊕ deltas. Nothing ever writes to the authored
file. Reset = delete the deltas, and it always returns exactly the shipped
behavior, byte-identically.
Mutability follows provenance
A blanket “never mutate” rule would be a regression: it would make the shipped synthesized corpus worse, since supersede lineage is how that corpus improves.
The two are not alternatives. A synthesized procedure that proves itself can be
graduated into an authored skill (#2671),
at which point it stops being mutable and starts accruing deltas instead.
Tiered learning — the cheapest representation wins
Most of what an agent learns is not prose. “This sender matters,” “for this goal use these four tools,” “escalate above 48 hours” are structured facts wearing a sentence as a costume. Encode them structurally and they cost almost nothing.
The governing rule: a lesson is recorded at the lowest tier that can represent
it. Prose is the escape hatch, never the default. Hermes only has tier 3 —
everything it learns becomes a file destined for the context window. GAIA can do
better because it has a tool layer between the model and the work.
Note what tiers 0 and 1 buy beyond token cost: they need no embedder. When
the embedding model is unreachable, or after
clear_all_embeddings() nulls every
procedure vector on an embedder change
(memory_store.py:1487-1512),
tier-3 recall goes dark while tiers 0–2 keep working. Pushing learning down the
stack buys robustness, not just tokens.
The honest cost of tier 0: integration, not inference
Tier 0’s “zero cost” claim is true about inference and misleading about engineering, and the correction is the same shape as Correction 2: a cost was moved, not removed. Learning stored as data costs nothing to carry — but it only helps a tool that was written to read it. The shipped numbers, counted on the email agent:
So a user can mark a sender important and 57 of 59 tools will not care. The
per-tool integration is small (read a dict, branch), but it is paid once per tool,
by hand, and it is invisible when skipped. That last part is the real hazard:
Two mitigations, both cheap, both required:
- A shared opt-in seam, not a bespoke store per agent. Today the email
agent’s preferences are a hand-rolled table plus an instance dict. Generalize
it once on the memory layer: a
learned_factstable (namespace, key, value, scope, provenance) plus one accessor a tool calls in a single line —self.learned("priority_senders", default=frozenset()). An agent author opts a tool in by calling it; nothing else changes. This is the same extract-shared-logic moveKNOWN_TOOLSalready applies to tool mixins. - Declared consumers, so the gap is legible. A tool that honors a tier-0
namespace declares it (a
@tool(learns_from=…)argument or a mixin-level registry). Thengaia skill deltascan render “12 senders marked important — honored bytriage_inbox,pre_scan_inbox”, and a fact with zero declared consumers is reported as inert rather than presented as learned. Without this, the legibility work in #2681 shows a count that does not mean what a user will read it to mean.
Why v1 ships three tiers, not four
The challenge — that a four-tier taxonomy built before we know what users teach agents is speculative — is right, and the answer is not “keep four for elegance” nor “cut to 0 and 3”. It is that two of the four tiers are not new machinery at all, and the genuinely speculative part is the classifier, not the tiers.
Dropping tier 1 would not save building anything — it would decline to use
something that already exists and costs at most 14 tool schemas. Dropping tier 2
saves real work. And there is a design reason to suspect tier 2 was never a tier:
The tier-2 collapse hypothesis. If a learned value can be read by a tool, it is tier 0. If it cannot, prose is the only thing that can carry it. Tier 2 — “a typed value the model reads as one rendered line” — is tier 0 for values whose tool has not been wired yet, plus a rendering convention. If that holds, tier 2 should never be built; the right fix is to wire the tool.Defer it and find out. A threshold expressed as one short tier-3 sentence costs roughly twenty tokens, which is not worth a new grammar, a new payload validator, and a new anchoring dependency before a single user has taught an agent anything. The tier classifier is deleted from v1 entirely. In its place: the tier is declared at the write site by whatever captured the lesson — a preference tool call is tier 0 by construction, a recipe outcome is tier 1 by construction, everything else is tier 3. No general classifier, no misclassification risk, and #2677’s requirement that a misclassified delta be promotable between tiers becomes cheap because the only move is 3 → 0 (write the tool that reads it).
Worked examples — the email agent (#2466)
Tier 0 — '[email protected] is always urgent' (zero context, shipped today)
Tier 0 — '[email protected] is always urgent' (zero context, shipped today)
The user says “anything from Alice is urgent.” The lesson becomes a row, not
a sentence.This is already shipped: the table DDL is at
preference_tools.py:63,
the tool at :355,
and the consuming triage path is documented at
:21-27.
The agent gets measurably better at your mail with zero prompt growth and
no embedder dependency. Note also that the tool reports a persisted flag and
refuses to claim durability it does not have — the honesty pattern every tier
should copy.v2’s job here is not to build this. It is to (a) generalize the pattern so any
agent can declare a tier-0 store without hand-rolling one, and (b) make the write
site route sender-shaped lessons here instead of writing “Alice is important” into
a prompt.Tier 1 — 'for inbox cleanup, this recipe works' (bounded, shipped today)
Tier 1 — 'for inbox cleanup, this recipe works' (bounded, shipped today)
Across five sessions the goal “clean up my inbox” succeeded via
Precedence is
pre_scan_inbox → archive_message → summarize_thread. Synthesis distils that
into a procedure whose tools_required records the recipe
(skill_synthesis.py:171),
and on the next matching goal:CORE > SKILL > SEMANTIC
(tool_loader.py:22-27),
the admission loop is cap-bound with no bundle pull-in
(:310-337),
and the recall is free because it rides the single per-turn pass
_refresh_recalled_skills already makes
(procedural_memory.py:369-387).What learning changes is which schemas render — not how much text is added.v2’s job: feed the same signal from an authored skill’s overlay. A
tool-hint delta on triage-inbox contributes names to the identical
skill_tools list. Because the loader drops registry-absent names
(:316-323),
a delta cannot smuggle in a tool the agent lacks — the invariant is enforced by
construction, not by a check that could be forgotten.Tier 2 — 'escalate CFO threads after 48h, not 72h' (DEFERRED past v1)
Tier 2 — 'escalate CFO threads after 48h, not 72h' (DEFERRED past v1)
Deferred past v1 — kept here because it is the clearest illustration of what a
typed delta would buy, and because the collapse hypothesis
is stated against this exact example: if
followup_hours is a value the escalation
tool could read, the right fix is to wire the tool (tier 0), not to invent a tier.triage-inbox skill has a section:Follow-up window for [email protected]: 48h (learned)) — or, better, consumed
directly by the escalation tool and rendered as nothing at all. A typed
parameter is individually inspectable, diffable, and revertible; a prose
paragraph saying the same thing is none of those.Tier 3 — 'how to decline a recruiter' (hundreds of tokens, retrieval-gated, PROPOSED)
Tier 3 — 'how to decline a recruiter' (hundreds of tokens, retrieval-gated, PROPOSED)
Some lessons genuinely are procedural prose:
When declining a recruiter, keep it to two sentences, thank them by name, and never mention a competing offer or a salary figure.No parameter holds that. It becomes a
preference delta with a body, anchored to
## Drafting replies, with its own trigger embedding — so it is
retrievable, not resident. It enters the prompt only when the turn’s goal
matches its trigger above τ (SIMILARITY_TAU = 0.82,
skill_synthesis.py:58),
reusing the FAISS mechanism reconcile_and_store already maintains.Tier 3 must remain a genuine escape hatch. A lesson forced into a tier too
weak to hold it is worse than a lesson that cost 200 tokens — burying knowledge in
a representation that cannot carry it is the failure mode that would have made a
tier classifier the second-riskiest item in the milestone. Deleting the classifier
and declaring the tier at the write site
retires that risk rather than mitigating it: the only lessons that reach tier 3 are
the ones no shipped mechanism claimed.The learning trigger
#2679 owns how a delta is written and approved. Nothing owned when one is proposed — a real gap, because “explicit user correction only” is a policy, and a policy with no detector is a policy that never fires. The answer is not a correction detector. It is three signals the codebase already produces, in descending order of confidence, none of which requires the model to judge anything:
Explicitly out of scope for v2.0: inferring a preference from repeated
behavior without the user saying anything. That is where both the injection risk
and the compounding-error risk live.
GAIA does not need Hermes’ trigger heuristic
Hermes creates a skill autonomously after a complex task — 5+ tool calls. It is cheap and concrete, and GAIA’s equivalent already exists and is stricter: synthesis requires a goal to recur acrossMIN_OCCURRENCES = 3 sessions at
MIN_SUCCESS_RATE = 0.80 (skill_synthesis.py:52-55) —
repetition and measured success, not merely complexity. A 5-tool-call heuristic
fires on one hard task that may have gone badly.
So the trigger gap is narrower than it looks: it exists only for deltas on
authored skills, not for the synthesized corpus, which has had a trigger since
#887. But note the qualifier from
Correction 4: that
0.80 is measured on tool calls that did not raise, not on outcomes the user
endorsed. The trigger is stricter than Hermes’; the quality of the signal it
gates on is weaker than the number suggests.
This needs its own issue — it is a peer of #2679, not a sub-clause of it.
Sequencing matters: signals 1 and 2 exist today and can land with the store,
while signal 3 is the only piece with design risk.
The delta grammar
A bounded vocabulary, not free prose. The agent may add a preference or an exception; it may not rewrite the procedure.fact is deliberately not storable as a delta. If a lesson is a fact the
tools can query, the write site routes it to tier 0 and the delta write is
refused. Allowing prose facts is how a 32K window silently fills with things
that could have been rows.
Section anchoring, not line offsets
A delta anchors to a named section of the base, identified by three things:
Two honest caveats:
- No section parser exists. The in-flight loader keeps the body as one
opaque string (
Skill.body: str, PR #2669skills/format.py:341); nothing splits it into headings. The anchorer is new machinery, and it must be deterministic — the same body must always yield the same slugs, or every delta re-anchors on a whitespace change. versionis optional in the format (Skill Format → field reference), so rebase cannot depend on a bumped version to know the base changed. The digest is the primary drift signal;base_versionis corroborating detail. A skill body with no headings at all (a legal bare-standard skill) anchors only at the whole-body level, and whole-body-anchored deltas are the ones most likely to orphan on any edit — surface that at write time so the user knows the learning is fragile before it is stored.
v1 records anchors; it does not act on them. Writing an anchor is one small
function (slug + digest at write time). Matching an anchor across a base update
is the research problem, and v1 declines it — see
Rebase. Every v1 delta still
carries a full anchor so it stays rebaseable later, and rendering prefers the
anchored section when it resolves cleanly, falling back to a single appended
Learned adjustments block. That fallback is also the only rendering path
needed for a base with no headings, which removes anchoring from v1’s critical
path without losing forward compatibility.
Provenance and trust class
Every delta carries where it came from. This is the injection control, and it is the highest-stakes part of the design: today an injected instruction lasts one turn; a persisted one lasts forever.
Deltas may never widen permissions, add tools, or raise a security tier. They
move in the restrictive direction only. Concretely: a delta may not edit
metadata.gaia.permissions, may not change security_tier, and may not
introduce a tool name — the last being unforgeable by construction
(tool_loader.py:316-323).
A delta may narrow: tighten a threshold, add an exception that declines an
action, remove a tool from a recipe.
Storage: one database, two tables
One SQLite file, one migration chain, one lineage idiom, one embedder — and a sibling table, for the three reasons in Correction 1.Minimal additive migration: v3 → v4 (PROPOSED)
The existing migration pattern isCREATE TABLE IF NOT EXISTS in _SCHEMA_SQL
plus a version-marker bump — exactly how v2→v3 added procedures
(memory_store.py:457-470).
v4 follows it identically: no ALTER TABLE, no row rewrite, no change to any
existing read path.
procedures unchanged — deliberately, so there is one
idiom to learn: provenance as JSON, success_count/attempt_count as the
empirical gate, superseded_by lineage with no DELETE path, embedding as a
raw float32 BLOB via the existing _embedding_to_blob layout
(memory.py:331),
and last_used_at for decay.
What it adds because procedures has no equivalent: base_name +
base_root + base_version (a delta is attached; a procedure is standalone),
anchor (where inside the base), kind/tier/payload (typed rather than
prose), scope (the leak boundary the review found missing),
and status + approved_at (the staged-write consent gate has no analogue in the
synthesized loop, which writes directly).
Store-layer methods to add (mirroring the procedures accessors so the
review surface is familiar): put_delta, search_deltas(base_name, scope, status, …), supersede_delta, approve_delta, touch_deltas. Every one
additive; none changes a procedures signature.
Reuse, extend, build — the review’s verdict per module
Effective-skill resolution
parameter →
tier-2 exception → tier-3 preference / example. Within a tier, later
created_at wins; a superseded delta never applies. Tier-1 hints do not
participate in text composition at all — they go to the loader.
Because tier 2 is deferred, v1’s ladder is
just base → tier-3 prose, rendered in the appended Learned adjustments block.
The full ladder is specified now so that adding tier 2 later is an insertion into a
defined order rather than a redesign.
Resolution happens once, at session start. The resolved fragment is cached
for the session’s lifetime. A delta approved mid-session takes effect on the
next session — stated in the approval UI, not discovered by the user.
Rendering is deterministic and stable: deltas render in (tier, kind, created_at, id) order so two sessions with the same delta set produce
byte-identical text. A learned fragment that reorders between sessions would
defeat any prefix reuse and make bug reports irreproducible.
Off-states (safe floors)
Every degraded condition lands on a conservative floor. No condition anywhere in this table produces a more-permissive result than the authored base.The write path
Adopted from Hermes’skills.write_approval model, made typed.
- Trigger. v2.0 default is explicit user correction only — the user says the agent got it wrong, or the outcome ledger records a rejection. Inference from observed behavior stays behind a setting until the safety rails have run in the field. This is the conservative fork and it is deliberate.
- Route, don’t classify. The tier is declared by whatever captured the
lesson: a preference-tool call is tier 0 by construction, a recipe outcome is
tier 1 by construction, everything else is tier 3. A
fact-shaped lesson writes a tier-0 row and no delta. There is no general classifier to misfile anything — see why v1 ships three tiers. - Stage. The delta is written
status = 'staged'— stored, inert, invisible to resolution. Nothing a staged delta contains reaches the model. - Review.
gaia skill deltas <name> --pendingshows a reviewable diff of the effective skill with and without the delta. Hermes’ reviewers consistently single out that its learning is legible — a file you can open and diff — as what makes the feature feel real rather than claimed. Legibility is product surface, not tooling polish. - Gate. Trusted provenance: one-click approve, optionally auto-approve by
setting. Untrusted provenance:
quarantined, scanned by #2468’s body-injection analyzer before it is shown, and never auto-approvable at any setting. - Promote on evidence, not opinion. A delta earns
activeon a measured track record — the shape the email agent already ships (min_samples=5, threshold=0.85,trust.py:345) and the dominance rulereconcile_and_storealready uses (skill_synthesis.py:713). Never on the model’s judgment that the base is wrong. Hermes patches on model judgment; with a ~4B local model that is not a safe import — a confidently wrong model would degrade its own instructions with nothing to stop it. - Archive, never delete. Retirement sets
superseded_byorstatus = 'archived'. History stays inspectable, matchingsupersede_skill(memory_store.py:2735-2754).
Interaction with local-capability skills (#2672)
“Deltas may never widen permissions” is necessary and not sufficient, and the challenge that says so is correct. Permissions gate whether a capability exists; a delta steers how it is used inside a grant that was already given. Two examples that widen nothing and are still bad:- A skill granted
filesystem:write:./**plus a learned delta “when the user asks to clean up, delete the oldest files in the working directory.” - A skill granted
shell:execute:gitplus “if the branch has diverged, usegit push --force.”
- Deltas inherit the tool-confirmation policy; they never bypass it. A delta
whose instruction leads to a tool in the agent’s confirmation set still hits
that gate at execution time — the email agent’s
CONFIRMATION_REQUIRED_TOOLS(agent.py:453) unioned with the baseTOOLS_REQUIRING_CONFIRMATION(agent.py:152). A delta can change what the agent proposes; it can never change what executes unattended. This is the load-bearing control, and it already exists. - Trust class is the minimum of provenance trust and skill-capability trust.
A delta on a skill carrying any local-capability permission
(
filesystem/shell/database/desktop/env— the domains Skill Format routes to the sandbox) is untrusted regardless of who authored it, because being wrong there is unrecoverable rather than annoying. - v1 does not allow deltas on capability-bearing skills at all. Deltas are
restricted to skills whose
metadata.gaia.permissionsis empty or purely connector-backed (network:*,mcp:connect). Note this is already the only loadable class of skill: PR #2669’s loader refuses a skill declaring a local-capability permission outright rather than loading it unenforced (refuse_unbridged_permissions). So the restriction costs nothing today and becomes a real gate the moment #2672’s sandbox lands and those skills become loadable.
Privacy, retention, and deletion
Deltas derive from real mail, real documents, real pages. #2671 covers graduation (private → shareable) and nothing covered the rest. Filed gap; needs its own issue.Where learned data lives, and its posture today
Deltas live in the same SQLite file as the rest of memory —~/.gaia/memory.db
by default, or the agent’s own path
(memory_store.py:354-357;
the email agent passes its own,
agent.py:637-639).
It is not encrypted at rest, and neither is today’s memory.db. Deltas
therefore add no new exposure — but they make an existing gap more acute,
because a prose delta can quote message text where a knowledge row usually
paraphrases. Encryption at rest is a memory-layer decision, out of scope here,
and it should be named as an open gap rather than implied to be handled.
The controls that are in scope
The conflict worth naming: archive-never-delete vs. right-to-erasure
Archive-never-delete is the correct default for learning operations: it is what makes a bad delta recoverable and lineage auditable, and it is the propertysuperseded_by gives the whole store today. It is the wrong answer for a user
who says “delete everything you learned about me.”
Resolution: these are two different operations and the spec should stop conflating
them.
- Learning retirement (supersede, revert, consolidate, decay) → archive. Never deletes.
- User-initiated erasure → genuinely deletes rows, embeddings, and index entries. “The user asked to forget” outranks lineage, and a system that cannot honor it has a compliance problem, not a design preference.
gaia skill reset <name> is the learning operation (archive). Erasure is a
separate, explicitly-named command, so nobody discovers that “reset” left the
content on disk.
Rebase on base update: v1 is deliberately crude
The base goesv1.0 → v1.1 with deltas attached. There is no prior art
anywhere. Hermes never faces this — it has no authored base to update against;
git’s three-way merge gives only the failure vocabulary, because a delta is
typed structured data, not a text hunk.
The original plan (and #2680) was to
build selective re-anchoring in v1. That is now judged premature, and the
challenge that raised it is accepted plainly — this is the single largest
de-risking available in the milestone.
The reasoning is a timing argument, not a difficulty argument. At launch the hub
carries approximately zero authored skills with real version history, so a
re-anchoring path would be built, shipped, and never exercised — untested code
guarding a rare event, which is how a subtle silent-reapplication bug reaches
production a year later when skill updates finally become common. The scenario is
rare early and common late; the code should arrive on the same schedule.
v1 behavior: orphan everything, retain everything, surface everything
On any change to a base skill’s content digest:- Every delta attached to that skill moves to
status = 'orphaned'. - No delta is deleted, ever — same archive-never-delete property as
supersede_skill(memory_store.py:2735). - The user is told, once, with a list and a bulk re-approve action alongside per-delta approve/discard.
- Nothing re-applies until the user says so.
Both safety properties are preserved. What v1 gives up is convenience. The
honest cost: a skill that updates frequently produces review fatigue, and a fatigued
user bulk-approves without reading — which is the same failure the consent gate
exists to prevent. That is the argument for building v2’s selective path
eventually, and it is an argument from observed update frequency, which is data
v1 will produce.
What v2 still has to solve (research, not review)
- “Materially changed.” Heading identity, content hash, or embedding distance — each with a different false-positive/false-negative profile. Hash is the conservative default (any edit orphans); embeddings introduce a judgment call the local model is poorly suited to make.
- Re-anchoring to a renamed section. Almost certainly unsafe by similarity alone — that is precisely the silent-reapplication failure. If it ships at all it ships as a user-confirmed suggestion.
- Do orphans expire? They accumulate. Proposed: they persist but stop being offered for re-approval after N base updates, and are then archived — visible in history, out of the active surface.
Consolidation, learning budget, and decay
Three related bounds. Without them a long-lived agent accumulates near-duplicate learning that each individually clears every threshold. Consolidation (umbrella merge).reconcile_and_store matches a candidate
against a single nearest neighbour and supersedes it
(skill_synthesis.py:691-726) —
it never merges several related items into one. Hermes does: it extracts the core
steps of similar micro-skills into a master umbrella. Adopt that for both
corpora: when N deltas on the same base and anchor say near-identical things, or
N procedures cluster tightly, propose one consolidated replacement through the
same staged-diff gate. Consolidation is a proposal, not an automatic rewrite.
One more finding the budget has to cover. The in-flight loader renders every
loaded skill’s full body into the system prompt with no cap (PR #2669,
Agent.get_skills_system_prompt) — correct for atomicity (#888’s “no partial
load”), but it means authored skills have no runtime ceiling at all, bounded
only by what an agent author chooses to load. That is the mirror image of the
truncated recall path (#2676): one
half cuts mid-step, the other half is unbounded. The budget below must cover the
authored + learned total, not the learned part alone, or a well-behaved overlay
can still be the thing that overflows a 32K window.
Learning budget, enforced. Tier-2 and tier-3 deltas get a hard resident-token
ceiling per agent, sized against the 32K NPU window and the existing
MAX_RECALL_BODY_CHARS = 1500 recall cap
(skill_synthesis.py:65).
At the ceiling: consolidate first, then demote the lowest-value tier-3 deltas to
retrieval-only, then archive. Tiers 0 and 1 are exempt — tier 0 never enters the
prompt and tier 1 is already capped at DEFAULT_MAX_TOOLS = 14
(tool_loader.py:72).
Unbounded accumulation is untidy on a 64K window and fatal on 32K.
Decay. last_used_at exists and is stamped on every recall
(memory_store.py:2756,
called from procedural_memory.py:281-289)
but has exactly one consumer today: a MAX(last_used_at) readout for
gaia memory status (memory_store.py:2017).
Nothing prunes or ranks on it. So decay is a scoping decision, not a schema
change — the data has been accruing all along. Proposed policy: last_used_at
is a ranking input for budget eviction and a surfacing input for
consolidation, and it never silently disables a delta the user explicitly
approved. Age demotes; it does not delete.
Inspection and control surface
Legibility is the feature, not the tooling. All PROPOSED; extends thegaia skill subcommand from PR #2669.
Eval strategy
Two runs, one baseline.- Baselines run deltas-off. Non-negotiable: per-user divergence makes a
scorecard unreproducible and a bug report uninterpretable.
gaia eval agentmust force the off-state. Today it does not isolate memory at all — there is noGAIA_MEMORY_DISABLEDhandling anywhere insrc/gaia/eval/, so a machine’s synthesizedprocedurescorpus can already contaminate a baseline. That is a pre-existing gap this milestone must close, not a new requirement it introduces. - A second eval measures what deltas add. Same scenarios, deltas on, seeded with a fixture delta set; the delta is the difference between the two scorecards. A learning feature whose value cannot be measured against its own off-state is not shippable — and per CLAUDE.md, any change touching prompt assembly requires an eval run against the committed baseline before merge.
tests/fixtures/skill_deltas/ alongside the existing
tests/fixtures/skills/ set PR #2669 adds, so the on/off comparison is
reproducible on any machine.
Blast radius: why email stays, but only half of it
The challenge that email is the wrong first reference agent — highest value, highest stakes, and a wrong delta means mis-triaged important mail — is half right, and the half that is right does not imply switching agents. Rejected: proving the mechanism on a lower-stakes agent first (analyst, browser). Three reasons, and they compound:- Neither ships a tier-0 store or an outcome ledger, so “proving the mechanism”
there means building both from scratch — the opposite of de-risking. Email
has both already (
preference_tools.py:63,trust.py:336). - Low-stakes agents have low-value learning. Nobody teaches the browser agent anything worth remembering, so the eval delta would be unmeasurable — and an unmeasurable reference example cannot satisfy the measured-value KPI.
- What is learned on a low-stakes agent does not transfer to the agent that actually needs it, so the risk is deferred rather than retired.
triage_inbox, pre_scan_inbox, summarize_thread — and never the write path:
send_draft, send_now, schedule_send, forward_message, accept_invite,
decline_invite, create_event_from_email, trash_message, archive_message.
That set is not invented here; it is the agent’s existing
CONFIRMATION_REQUIRED_TOOLS
(agent.py:453)
plus the destructive/organizing tools. So the v1 rule reduces to: a delta may
change what the agent proposes and how it ranks, never what it sends, deletes, or
files. The worst outcome of a wrong delta is that an important message is
ranked low and the user sees it later — bad, visible, and recoverable — rather
than a message sent or archived on a learned instruction nobody reviewed.
The write path opens up in v2, gated on the correction-non-recurrence KPI having
held on the read path for a release.
KPIs
Milestone-level: how we would know this was worth building
Nothing previously answered that question, and it is the one a reviewer should ask first. Three metrics, each measurable with data the system already records or records as part of this milestone.Implementation-level
Entry gate: what “milestone 60 validated” means
#2674 says do not start until milestone 60 ships “and is validated,” which was undefined and therefore unenforceable. Concrete exit criteria — all seven, not a majority:
Criteria 3 and 7 are the ones most likely to be waved through and the ones most
likely to cost the milestone if they are.
Phased build
Each phase holds the prior floor until it lands. Phase 1 does not start until every entry-gate criterion is met — the whole design rests on the authored layer being stable, and building an overlay against a moving base wastes the work. The phases below reflect the adversarial review: the classifier, tier 2, and selective re-anchoring have moved out of v1.Phase 0 — Architecture (this revision)
Success criteria: the layer/tier model, the delta grammar, the anchoring scheme, and the off-state floors are decided; the four corrections and the nine review verdicts are recorded; every citation resolves.Phase 0b — Prerequisite fixes on the shipped path (PROPOSED; unblocks everything)
Not optional and not part of v2 proper: three invariants v2 depends on are currently violated or absent in shipped code. Success criteria:- Recalled-procedure bodies cut at a structural boundary or drop whole (#2676).
- The recalled-procedure fragment no longer rebuilds the prompt from the mixin prefix mid-session, or moves to the volatile tail (#2686).
gaia eval agentforces a deterministic memory/learning off-state.- Outcome attribution exists — something increments a procedure’s or delta’s counters after adoption, on a user-observable signal (Correction 4). Without it, “promotion gates on track record” cannot be implemented.
Phase 1 — Delta store + bounded grammar (PROPOSED)
skill_deltas at schema v4, the five store accessors, the tool-hint /
preference / example kinds, and the write-time anchor recorder. No
consumer — writes and reads only via tests. No classifier; tier is declared at
the write site.
Success criteria: a v1/v2/v3 database migrates to v4 with no row rewritten; a
delta round-trips; the anchor recorder is deterministic across whitespace changes;
every procedures read path is byte-identical with deltas present.
Phase 2 — Resolution + off-states (PROPOSED)
EffectiveSkill = base ⊕ deltas, precedence, session-start-only resolution,
--no-learned-skills, eval isolation, tier-1 wiring into the existing
skill_tools list, and the appended Learned adjustments render block.
Success criteria: the off-state hash test passes; a tier-1 delta changes tool
selection without changing prompt bytes elsewhere; every off-state row resolves to
the authored base.
Phase 3 — Trigger + write path + consent gate (PROPOSED; needs #2468)
The three trigger signals, staged proposals with reviewable diffs, the trusted/untrusted split, injection scanning, and the evidence-based promotion gate (which requires Phase 0b’s attribution). Success criteria: an untrusted-provenance delta cannot reach the model without explicit approval at any setting; a delta that would widen a permission is refused at write time; a fact-shaped lesson writes a tier-0 row and no delta; v1’s read-path-only restriction is enforced, not documented.Phase 4 — Tier-0 generalization + legibility (PROPOSED)
The sharedlearned_facts seam and its declared-consumer registry, then
gaia skill deltas + the UI panel — including the inert fact report that makes
a learned-but-unconsulted fact visible.
Success criteria: a second agent adopts tier 0 without writing its own store;
a fact with zero declared consumers is reported as inert; every active delta is
listable, diffable, and revertible.
Phase 5 — Budget, consolidation, decay (PROPOSED)
The enforced ceiling over authored and learned resident content, umbrella merge through the staged gate, and the firstlast_used_at consumer.
Success criteria: the ceiling holds under a synthetic 200-delta corpus with
nothing cut mid-step; consolidation supersedes rather than deletes.
Phase 6 — Deferred by evidence, not by schedule (PROPOSED)
Each item here ships only when data from earlier phases says it is needed:Open questions
The architectural forks (provenance-driven mutability, one database / two tables, tiered representation, empirical promotion, session-boundary resolution) are settled above. What remains:- Delta scope: per-agent or per-user? Already answered inconsistently in
shipped code — tier-0 preferences are per-agent (
state.db), procedures are per-DB-file with no scope column, and the email agent passes its own path while ChatAgent does not (evidence). Thescopecolumn makes the choice explicit; which default is open. Sharing is more useful and leaks more. - What is the outcome signal for attribution? A correction, an undo, an accepted suggestion, or an explicit thumbs-down — Correction 4 shows tool-call non-error is available and nearly worthless. Whatever is chosen becomes the definition of “empirical” for the whole milestone, so it deserves a decision rather than a default.
- Does the tier-2 collapse hypothesis hold? If every learned value a tool could read should just be wired into the tool, tier 2 never ships. v1 is designed to answer this from data rather than argument.
- Deltas on a shadowed skill. Precedence roots mean a same-named skill can
appear in two roots (PR #2669
SkillManager.shadowed). Does a delta follow the name or the (name, root) pair? Following the name is more forgiving and is how an overlay silently attaches to a different author’s skill. - Decay strength. Does an unused delta ever deactivate on age alone, or only ever demote in ranking? Proposed: demote only, never deactivate what the user approved — but a 300-session mailbox may argue otherwise.
- Trusted auto-approve default. Ships off (every delta staged) or on for
user_instructionprovenance? Off is safer; on is what makes the feature feel alive.
Current state of the code
Dependencies
Blocked by milestone 60 in full — especially #888 (loader/format, in flight as PR #2669), #2671 (graduation bridge + theDistilledProcedure rename this document’s naming depends on), and
#2468 (injection analyzer, required
before any untrusted-provenance write path).
Blocked in practice by three shipped-code gaps —
#2676 (procedure atomicity),
#2686 (the mid-session prompt rebuild,
Correction 3), and the
missing outcome attribution
(Correction 4, no
issue yet). Building an overlay on a recall path that truncates mid-step, rebuilds
the prefix every turn, and has no counter to gate promotion on would inherit all
three.
Entry gate. See what “milestone 60 validated” means —
seven criteria, of which “the format has gone a full release cycle without a
breaking change” is the real one.
Builds on (shipped): #887 /
#1451 — procedural_memory.py,
skill_synthesis.py, memory_store.py, tool_loader.py.
Reference agent: the email agent (#2466),
which already ships the tier-0 store and the empirical-evidence ledger this
design generalizes.
Design inputs: Hermes Agent (staged-write consent, archive-never-delete,
umbrella consolidation), agentskills.io (base format), Mem0 (ADD/UPDATE/NOOP),
Zep (supersede lineage); in-repo Skill Format,
Skill Synthesis, Tool Loader,
Agent Skills.
Non-goals. No mutation of authored SKILL.md files, ever. Not a second
synthesis engine — #887 owns procedure
synthesis. No sharing or publishing of learned deltas; graduation
(#2671) is the only private→shareable
path. No semantic re-anchoring of orphans. Not started before milestone 60 ships
and is validated.