Skip to main content
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 SkillDistilledProcedure 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:
  1. when_to_use and markdown_body are NOT NULL (memory_store.py:286-287) and put_skill raises 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 a NOT NULL without rebuilding the table — so the “additive migration” story breaks immediately.
  2. The reconcile match scan would eat deltas. _nearest_enabled_procedure scans 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.
  3. The recall FAISS index would surface deltas as procedures. _rebuild_proc_faiss_index indexes every enabled non-superseded row (procedural_memory.py:80-82), and recall_skill injects whatever it finds. A fragment would be presented as a complete proven procedure — the exact failure #2676 describes.
Fixing 2 and 3 in place means adding a 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’s tools_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.
The existing tier-3 recall path satisfies neither. Filed as #2686 against milestone 60 — it is a defect in shipped code, worth fixing independently of v2, and adjacent to #2676 in the same recall path.

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 on success_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:
  1. 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.
  2. 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.
This is the most consequential gap found in either review pass, because it invalidates an assumption both the umbrella issue and this document treat as already satisfied.

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:
The silent no-op is worse than the token cost. A tier-0 fact written to a store no tool reads looks exactly like successful learning — the write succeeds, the confirmation says “saved”, and behavior never changes. A tier-3 prose delta at least reaches the model. Tier 0 trades a visible cost for an invisible failure mode, and nothing in the current design would surface it.
Two mitigations, both cheap, both required:
  1. 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_facts table (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 move KNOWN_TOOLS already applies to tool mixins.
  2. 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). Then gaia skill deltas can render “12 senders marked important — honored by triage_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.
Revised claim, for the record: tier 0 has zero resident context cost and zero embedder dependency, at the price of one small, explicit integration per consuming tool, and it must report which tools consume it. “Zero cost” without that qualifier should not appear in any issue or doc.

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)

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.
Across five sessions the goal “clean up my inbox” succeeded via 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:
Precedence is 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.
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.
The authored triage-inbox skill has a section:
The user corrects the agent twice: 72 hours is too slow for the CFO. Instead of appending a paragraph, one typed row:
Rendered into the prompt as a single line under the anchored 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.
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 across MIN_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:
  1. No section parser exists. The in-flight loader keeps the body as one opaque string (Skill.body: str, PR #2669 skills/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.
  2. version is 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_version is 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 is CREATE 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.
What this borrows from 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

Precedence, most specific last: base section text → tier-2 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.
  1. 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.
  2. 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.
  3. Stage. The delta is written status = 'staged' — stored, inert, invisible to resolution. Nothing a staged delta contains reaches the model.
  4. Review. gaia skill deltas <name> --pending shows 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.
  5. 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.
  6. Promote on evidence, not opinion. A delta earns active on a measured track record — the shape the email agent already ships (min_samples=5, threshold=0.85, trust.py:345) and the dominance rule reconcile_and_store already 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.
  7. Archive, never delete. Retirement sets superseded_by or status = 'archived'. History stays inspectable, matching supersede_skill (memory_store.py:2735-2754).
The escalation this creates. Today a prompt injection lasts one turn. A persisted delta lasts forever. Local execution makes the payoff bigger — GAIA can learn from full message content that a cloud agent must not persist — which raises the stakes proportionally. The staged-diff consent gate is what makes exercising that capability safe, and it is not optional scaffolding to be added later: an inference-driven write path shipped without it converts a one-shot attack into permanent compromise.

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:git plus “if the branch has diverged, use git push --force.”
Both are inside the declared permission. Both are destructive. Neither trips a permission check, a tier check, or a tool-addition check. So three further controls:
  1. 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 base TOOLS_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.
  2. 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.
  3. v1 does not allow deltas on capability-bearing skills at all. Deltas are restricted to skills whose metadata.gaia.permissions is 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.
Sequencing consequence: adaptive skills must not ship deltas on capability-bearing skills before #2672’s enforcement exists. That is a hard dependency in one direction only — v2 does not block #2672, but the reverse combination (sandboxed capabilities plus unreviewed learned instructions steering them) is the compounding risk the challenge identifies.

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 property superseded_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 goes v1.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:
  1. Every delta attached to that skill moves to status = 'orphaned'.
  2. No delta is deleted, ever — same archive-never-delete property as supersede_skill (memory_store.py:2735).
  3. The user is told, once, with a list and a bulk re-approve action alongside per-delta approve/discard.
  4. 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.
Bases with no headings anchor whole-body, so under v2’s selective scheme any edit orphans everything on them — i.e. v2’s worst case equals v1’s normal case. Another reason v1 loses less than it appears to.

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 the gaia skill subcommand from PR #2669.
The Agent UI panel mirrors this on the skills panel Skill Format proposes: a per-skill delta count, the diff view, approve/revert, and a visible badge when a skill is running with an overlay. A user must be able to see, at a glance, that an agent is not running the shipped skill.

Eval strategy

Two runs, one baseline.
  1. Baselines run deltas-off. Non-negotiable: per-user divergence makes a scorecard unreproducible and a bug report uninterpretable. gaia eval agent must force the off-state. Today it does not isolate memory at all — there is no GAIA_MEMORY_DISABLED handling anywhere in src/gaia/eval/, so a machine’s synthesized procedures corpus can already contaminate a baseline. That is a pre-existing gap this milestone must close, not a new requirement it introduces.
  2. 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.
Fixture deltas live in 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:
  1. 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).
  2. 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.
  3. 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.
Accepted instead: cut the blast radius inside email rather than changing agents. In v1, deltas may affect only read-path and classification tools — 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.
Named anti-KPI: the number of things learned. Hermes’ “20+ skills → 40% faster” is vendor-adjacent and directional only — treat it as evidence the category is real, not as a target. A system optimized for delta count will manufacture deltas, and delta count is precisely the quantity the learning budget exists to bound. Any dashboard that shows a growing count must show cost and non-recurrence beside it.

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 agent forces 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. 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 shared learned_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 first last_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:
  1. 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). The scope column makes the choice explicit; which default is open. Sharing is more useful and leaks more.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Trusted auto-approve default. Ships off (every delta staged) or on for user_instruction provenance? 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 + the DistilledProcedure 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 / #1451procedural_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.