memory · git:20260827.54105dd · 2026-08-27 · sha256 c7b95a7d3c8c94c6
memory git:20260827.54105ddA
Immutable. This exact content is served forever at /api/v1/blob/c7b95a7d3c8c94c6.
--- name: memory 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 (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. | 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 | 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. 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, 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 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, 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.