memory · diff

git:20260814.21b7e3b to git:20260827.54105dd

99 added, 42 removed. Audit A to A.

---
name: memory
- description: Use when reading, searching, saving, or curating Gaia memory, deciding whether a session finding should persist, or triaging injected memory at session start
+ description: Use before the first memory verb — read or write — whenever the orchestrator is about to touch memory as this turn's subject: deciding what persists, searching, curating, or triaging what a session injected at start.
---
# Memory
Memory is the curation technique that turns selected experience into continuity:
durable knowledge kept small, live work kept visible, and raw operational
history left on the automatic event floors where it belongs.
## The three floors
| Floor | Purpose | Lifecycle |
|---|---|---|
| Events | Commands, dispatches, session events, and other operational facts | Automatic and short-lived |
| Episodes | Searchable agent-turn outcomes and anomalies | Automatic, retained for diagnosis |
| Curated memory | User-governed knowledge and work that must affect future decisions | Deliberate and long-lived |
Events and episodes are evidence, not durable truth.
## Think in roles, not storage vocabulary
Every curated item serves one human-facing role:
- **Durable knowledge**: stable facts, accepted decisions, useful dead ends,
user preferences, and meaningful milestones.
- **Live thread**: one actionable concern that must reappear in a later session.
- **Historical log**: append-only context useful for audit but not reinjection.
The internal `class`, `status`, `type`, slug, and link enums implement these
roles. Consult `reference.md` only when materializing or debugging them.
## Other home first
Before saving, ask: **does this already have a canonical home?** Work in flight
belongs in a brief, plan, or task; domain state in project context or the owning
system; raw execution detail in events, episodes, or the transcript.
Do not copy a fact into memory merely because it matters. A second source of
truth becomes stale; a durable reference to the canonical object is enough.
+ ## The one-line test: which initiative
+
+ A fact about Gaia itself splits by how it was produced, and that split decides
+ where it can physically land — not as a style choice, but because the writer
+ enforces it:
+
+ - **Observed the system fail or rub in use** — a symptom, with evidence,
+ noticed while working on something else — is `initiative=gaia_system`.
+ `gaia_system` is host-scoped (`gaia/store/writer.py::HOST_SCOPED_INITIATIVES`):
+ `apply_host_scope` (`gaia/store/writer.py::apply_host_scope`) forces the row
+ into the sentinel workspace `_gaia_host` regardless of whatever
+ `--workspace`/env/cwd resolved, and refuses a project anchor outright —
+ passing `project_ref` raises `MemoryHostScopeError`
+ (`gaia/store/writer.py::MemoryHostScopeError`, `code=host_scope_no_project`).
+ - **Decided to build or change Gaia** — a design choice, a completed change, a
+ plan for how a component should work — is `initiative=gaia` (or whatever the
+ repo's own basename normalizes to), project-scoped exactly like any other
+ project's memory: `--workspace`/`--project` apply normally and the row lives
+ wherever that project's memory already lives.
+
+ This is the only judgment the writer still leaves to you. Once the initiative
+ is named, the physical destination is not a choice: mis-picking the initiative
+ is the only way to mis-file the row, because nothing downstream corrects a
+ scope chosen wrong at the source.
+
## Process
1. **Read the injected memory, then sweep what you are about to touch.** The
digest and anchors in context are a worklist under a SessionStart budget, not
the corpus. Before writing into an initiative — or whenever the question is
what it still owes — read its whole live-pending set with
`gaia memory get-relevant --initiative=<key>`, uncapped and with bodies. A
reader who only sees the digest can add rows and never retire one.
2. **Search before writing; do not read silence as absence.** Find the topic's
existing owner and its lineage — a duplicate divides relevance instead of
strengthening knowledge. An empty result answers "no row matches this
phrasing", never "this initiative owes nothing": a pending is worded as the
problem looked when it opened, not as what just resolved it, so step 1's
sweep is what finds it.
3. **Choose the home.** Run *Other home first*, and continue only for genuinely
curated value.
- 4. **Adjudicate the change.** The orchestrator chooses scope, create/append/
- correct/transition/link, role, lifecycle, initiative, and verification.
- Preserve lineage when knowledge replaces or graduates from earlier work.
- 5. **Show exact values and obtain consent.** The user may correct the proposed
- values before durable persistence. A request such as “reflexionemos y
- guardemos” authorizes writing only after the proposal has been shown.
- `checkpoint` is one atomic operation and remains all-or-nothing.
- 6. **Verify the durable result.** Read back the affected rows, lifecycle, scope,
- and links. Report partial batch failures per operation.
-
- ## Reading a row leaves a trace
+ 4. **Adjudicate the change.** The orchestrator chooses scope (see *The one-line
+ test* for a host-scoped initiative), create/append/correct/transition/link,
+ role, lifecycle, and verification. Preserve lineage when knowledge replaces
+ or graduates from earlier work.
+ 5. **Curate against the exception boundary, then report.** Curation is
+ delegated: the orchestrator adjudicates and executes directly, inside the
+ boundary below, and reports what changed afterward — it does not show a
+ proposal and wait for it to be confirmed.
- Three counters bump as a side effect of being read, and two questions classify
- any surface, including one not built yet. **Did the caller identify these rows,
- or describe a window and take whatever fell in?** A slug identifies them; a
- named initiative identifies them; a filter, a search term, a date range or a
- dump of the table identifies nothing, however much of each row it prints.
- Identified, so reaching them was the point — **deliberate** (`show`, `story`,
- `get-relevant --initiative`). **When they were not: did the rows answer the
- question their caller asked, or were they assembled into somebody's context?**
- Answered — **neither** (`search`, `list`, `gaia query`): what fell in reflects
- the phrasing, not the row. Assembled — automatic, and split once more by what
- the block reaches: one fixed corpus every time it fires, the same rows whatever
- the occasion is about, is **kernel** (today, the dispatch block); a window
- picked for this occasion is **injection** (`get-relevant`, however launched).
- Rendering is what counts: a row trimmed out of a block was never reached.
+ | Operation | Handling |
+ |---|---|
+ | `add`/`append`/`reclassify`/`link` on `project`/`feedback`/`atom`/`negative` rows | autonomous, brief report |
+ | `type=user` rows (about the user) | autonomous, flagged above the report for veto (convention — no mechanical backstop) |
+ | contradicting or superseding a user `decision_*` row | ask first |
+ | `edit`/`delete` | T3 approval flow, and also categorically denied on the orchestrator's own `gaia` CLI lane (`hooks/modules/security/gaia_cli_only_guard.py::check`) — a dispatched specialist runs it under approval, the orchestrator's bare CLI never does |
+ | `checkpoint` | autonomous when it passes the milestone test (`session-reflection/reference.md`) |
+ | closing an objectively verifiable brief/plan | autonomous, report; run `gaia brief verify` by hand before `set-status` — `close` (which runs verification for free, `bin/cli/brief.py::_cmd_close`) is not on the orchestrator's `gaia` CLI lane, only `set-status` is |
+ | promoting a TASK | never direct — dispatch `gaia-verifier` |
+ | approvals | read/report only |
- Never blend two into one number. A count is worth reading only if a higher one
- means the row was worth more, and each axis fires at a rate set by something
- other than the row: the kernel's corpus rides on every dispatch, so folded into
- injection it heads any ranking by construction and measures dispatch volume, not
- usefulness; injection folded into deliberate lets a row pushed at people read as
- demand. A surface built tomorrow earns its own axis by that test. None of this
- changes what gets injected — selection still orders by `updated_at` and reads no
- counter.
+ 6. **Verify the durable result.** Read back the affected rows, lifecycle, scope,
+ and links. Report partial batch failures per operation.
## When curated memory earns attention
- **Decision:** accepted, with no canonical home, and it binds a later choice.
- **Project milestone:** it closes a meaningful arc. A routine session close is
not one; minting a checkpoint for it turns memory into session summaries.
- **Live handoff:** one unresolved concern has no structured work object and
must resurface. One concern per thread — a single status cannot honestly
represent several.
- **Learning or dead end:** it prevents repeated investigation or error.
- **Gaia improvement:** a concrete symptom, component, evidence and reproduction
- deserves visible follow-up. After consent, persist it as a `feedback` live
- thread in initiative `gaia_system`, carried forward until closed or graduated.
+ deserves visible follow-up. Persist it as a `feedback` live thread in
+ initiative `gaia_system`, carried forward until closed or graduated.
## When curated memory loses it
Attention is finite and a live thread spends it every session. Two exits, and
neither is deletion:
- **Closed** — the concern was resolved or stopped being relevant.
`reclassify --status=closed` retires it from the worklist and keeps the row.
- **Graduated** — the work finished and left knowledge worth holding.
`reclassify --status=graduated`, or `--class=anchor` when future dispatches
should carry the knowledge; `link --kind=graduated_to` preserves the lineage
from the thread to the anchor it became.
Both verbs are non-mutative, so cost is never the reason a thread stays open.
Whoever observes the resolution owns the exit: a session that resolves a thread
and does not close it has moved that thread's cost onto every session after it.
## Ownership
- - The **user** is the authority for durable personal and project knowledge.
- - The **orchestrator** resolves ownership, searches, adjudicates, presents the
- proposal, and verifies it.
+ - The **user** is the authority for durable personal and project knowledge, and
+ for the exceptions the process table marks "ask first" or "veto".
+ - The **orchestrator** resolves ownership, searches, adjudicates, executes
+ within the exception boundary above, and reports.
+ - `gaia-operator` executes what the orchestrator has already adjudicated,
+ without loading this skill itself (`agents/gaia-operator.md` carries no
+ `memory` entry in its `skills:` list) — a convention of responsibility split
+ between the two roles, not a technical inability for either to run the
+ other's step.
- Other specialists only propose (`memory_delta`, `memorialize_suggestions`);
they never write curated memory directly.
- The runtime enforces the writer boundary and core data invariants. The skill
supplies curation judgment; it does not duplicate enforcement details.
## Handoffs
- `session-reflection` recovers decisions, live work, learnings, Gaia
- improvements and closures, then produces the curation proposal consumed here.
+ improvements and closures, then hands off the curation this skill governs.
- `gaia-compact` runs after durable persistence and carries only transient
continuity plus references to what was saved.
- `reference.md` contains exact CLI forms, enums, scope rules, retrieval,
- checkpoint payloads, the access-telemetry columns and call sites, history
- coverage, and graph mechanics.
+ checkpoint payloads, access telemetry, history coverage, and graph mechanics.
- `examples.md` contains worked create/update/batch/checkpoint cases.
## Anti-patterns
- **Claiming perfect history:** ordinary updates are audited, but hard deletion
and workspace removal can destroy records. Exact tracked fields are defined by
the current schema and migration, not by prose.
+ - **Digest as corpus:** treating the SessionStart digest (`bin/cli/memory.py::_render_digest`)
+ as the whole state of an initiative. It is a char-budgeted worklist that trims
+ whole initiatives from the tail — an initiative absent from it may still carry
+ live-pending rows; only `get-relevant --initiative=<key>` returns the corpus.
+ - **Empty read as absence:** a search or digest miss answers only "not under
+ this phrasing", never "nothing is owed" (see Process step 2). This is now
+ mechanically ruled out for a host-scoped initiative specifically: every read
+ of `gaia_system` unions the sentinel workspace into the query
+ (`bin/cli/memory.py::_reader_workspaces`), so an empty result there really is
+ empty, from any vantage — it can no longer be explained away as "wrong
+ workspace".
+ - **Writing where you're standing:** before host-scope existed, a `gaia_system`
+ row landed in whatever workspace the session happened to be in, scattering
+ "how Gaia itself is doing" across every workspace ever used until no single
+ read could see the whole corpus. For a host-scoped initiative this is now
+ mechanically impossible — `apply_host_scope` forces the sentinel regardless
+ of cwd. The lesson still generalizes to any non-host-scoped write: the
+ workspace you happen to be standing in is not evidence of the right scope
+ for a fact that is really about something broader — run *The one-line test*.
+ - **A resolved thread nobody closes:** fixing what a thread describes without
+ reclassifying it moves that thread's cost onto every later session that has
+ to re-read and re-triage it, even though closing it costs nothing (both
+ lifecycle verbs are non-mutative).
+ - **Append leaves a stale description:** `append` (`bin/cli/memory.py::_cmd_append`)
+ concatenates onto `body` only — it never touches `description`. A listing or
+ digest renders `description`, not `body`, so a note that grows entirely
+ through appends can carry a description that no longer matches what the
+ body now says.
+ - **`add` without `--description` erases it:** `upsert_memory`
+ (`gaia/store/writer.py::upsert_memory`) writes `description` straight from
+ the call's argument on every UPSERT (`description = excluded.description`,
+ not coalesced against the existing value) — unlike `project_ref`/`initiative`/
+ `audience`, which are deliberately coalesce-or-omit. Re-running `add` on an
+ existing row without repeating `--description` silently NULLs a previously
+ set one.
+ - **Curating in the cold without a snapshot:** the database keeps moving while
+ a curation or audit pass runs — other sessions and hooks are still writing.
+ A conclusion drawn from an early read and asserted later, without re-reading
+ the rows it depends on, can state something as current that changed
+ underneath it while the pass was still running.