# Memory

> A persistent, cross-session memory for the agent — markdown files under ~/.kin/memory/ that survive past any one session, plus a ranked recall layer that surfaces the relevant ones automatically. The model reads and c…

A persistent, cross-session memory for the agent — markdown files under
`~/.kin/memory/` that survive past any one session, plus a ranked recall layer
that surfaces the relevant ones automatically. The model reads and curates the
store with the `memory` tool; a reflection pass at session end extracts
durable knowledge for it.

<!-- SOURCE: src/kin/harness/tools/memory.py, src/kin/harness/tools/rig.py, src/kin/harness/tools/fork_agent.py, src/kin/harness/memory/store.py, src/kin/harness/memory/db.py, src/kin/harness/memory/reflect.py, src/kin/harness/env.py, src/kin/harness/persistence/replay.py, src/kin/harness/persistence/prompt_history.py, src/kin/harness/session/, src/kin/harness/compaction.py, src/kin/harness/headless.py, src/kin/harness/settings/, src/kin/tui/replay.py, container/dashboard/memory_api.py, container/dashboard/rig_api.py, container/dashboard/transcript.py -->

## What it looks like

Memory is a directory of small markdown files, addressed under a virtual
`/memories` prefix (the same shape as Anthropic's memory tool, so models
trained against it drive it natively):

```
~/.kin/memory/
├── MEMORY.md                  # optional curated index — inlined into the system prompt
├── prefs/
│   └── reviews.md             # category: preference
└── facts/
    └── deploy-topology.md     # category: project-fact
```

Each file carries fenced `category:` frontmatter that drives indexing and
expiry (the fences are required — a bare `category:` first line is treated
as content):

```markdown
---
category: preference
---

The operator prefers a short risk summary before changes.
```

| category | meaning | expiry |
|---|---|---|
| `project-fact` | durable facts about a project/system | never — flagged **stale** after ~6 months untouched so it gets reviewed |
| `preference` | how you like things done | never — recency-weighted in recall so it isn't outranked |
| `episodic` | what happened (outcomes, incidents) | swept after ~6 weeks |
| `session` | scratch for the current session | never indexed; swept after ~24 h |

A file without frontmatter counts as `project-fact` — the never-auto-deleted
category, so an uncategorized memory can never be silently swept. **At write
time** `create` rewrites a missing or unrecognized `category` to `episodic`,
so machine-written content with no declared category *expires* by default
(~6 weeks) rather than accreting as immortal `project-fact`; the read-time
`project-fact` fallback above only matters for legacy files already on disk.

## How recall reaches the model

Two seams, chosen so recall never busts the prompt cache:

1. **A stable index, once per session** — the memory file listing (plus a
   curated `MEMORY.md`, if you keep one) is composed into the cached system
   prompt at session start (capped ~200 lines / 25 KB). `/reload` re-composes
   it.
2. **Per-turn top-5 recall** — each user turn is matched against a dedicated
   FTS5 index over the memory corpus and the best 5 snippets ride the volatile
   `<system-reminder>` channel (the same one the date/git block uses), so the
   cached prefix stays byte-identical. Anything past a snippet, the model
   `memory view`s on demand.

The recall index is deliberately **separate** from the
[workspace search](/docs/kin/guide/workspace-search/) index (its own DB inside the memory
root) — a casual preference must never outrank authoritative repo docs, and
vice versa.

## The `memory` tool

One tool, six commands (`view` / `create` / `str_replace` / `insert` /
`delete` / `rename`):

- `view` lists a directory (2 levels) or shows a file with line numbers
  (`view_range: [start, end]` paginates).
- `create` writes a file — and **overwrites** an existing one.
- `delete` / `rename` refuse to touch the memory root itself.
- Every path is canonicalized and confined to the memory root — traversal
  (`../`, encoded, absolute, symlink escapes) is rejected outright.

Permissions: mutations are writes (`edit` kind — auto-applied in `auto`, ASK
in `strict`, denied while [planning](/docs/kin/guide/modes-and-permissions/#planning)),
while `view` is auto-allowed in both [modes](/docs/kin/guide/modes-and-permissions/). The
mode gate is per-command, so `strict` only prompts when the model actually
changes a memory.

## Workspace scoping

Memory is workspace-scoped by default — a session in repo A does not see
repo B's facts, and the reflection pass cannot overwrite or delete
another workspace's memories. The mechanism is one stamped frontmatter
key, one read-side filter.

- **Auto-stamp on `create`.** `project-fact` and `episodic` files gain a
  `workspace: <realpath-of-workdir>` frontmatter line at write time.
  `preference` stays global by design (a "how you like things" fact should
  surface everywhere).
- **Hard filter on read.** The cached `# Memory` block lists only the
  current workspace's files (plus global/legacy) — foreign-workspace
  files collapse to one summary line. Per-turn recall and dashboard
  search apply the same filter. `memory view` is the deliberate
  cross-workspace surface (it goes through the same containment guard
  as every other read).
- **Opt out per file.** Add an explicit `workspace: global` (or any
  custom label) frontmatter line and the stamp is honored verbatim —
  the file surfaces in every workspace whose filter matches.
- **Legacy files (no `workspace:` key) = global.** They decay naturally;
  no migration script runs. To make a legacy file workspace-specific,
  add an explicit `workspace:` value via `str_replace`.
- **Moved repos go quiet.** The stamp is `os.path.realpath(workdir)`,
  so a repo moved to a new path keeps its memories but they stop
  surfacing — use `str_replace` to re-stamp if that's what you want.
  One carve-out: a linked `git worktree` checkout stamps its **main**
  working tree's path, so a memory written by a worktree-isolated
  subagent stays reachable after the worktree is deleted.
- **Dashboard search is unfiltered.** `memory_api._search` passes
  `workdir=None`, so the operator's view sees everything (falsy workdir
  is the deliberate operator-unfiltered surface).
- **The dashboard's per-file list** (`GET /api/memory`) now includes a
  `workspace` field per file, so operators can see at a glance which
  workspace a given memory is scoped to.

See [DR 0088](https://github.com/kinra-ai/kin/blob/dev/docs/decisions/0088-workspace-scoped-memory.md) for the
full design and the accepted edge cases (worktree-isolated subagent
sessions, hostile `workspace:` values, the frontmatter whitespace-
collapsing mismatch, `str_replace` opt-out, dashboard unfiltered).

## Memory capture at session end

When a session ends, kin runs one bounded **memory reflection** pass — a short
extra agent turn over the live `memory` tool with the full transcript in
context, so durable facts (stable preferences, project facts, notable
outcomes) persist across sessions. It prefers updating an existing file over
creating a near-duplicate, and writes nothing when nothing durable emerged.
Every write goes through the same guarded store as an in-conversation edit.
When the completed turn used `rig`, its `browser` alias, `rig_shell`, or
`rig_memory`, the same reflection pass may also use `rig_memory`. Both stores
share the pass's ten-mutation cap. Turns that never crossed the Rig boundary
remain local-memory-only, so ordinary project facts cannot bleed into the
machine's separate durable memory.

The automatic pass remains in the journal and model history for continuity
and audit, but it is an internal control-plane turn rather than part of the
human conversation. Resume, transcript inspection, markdown export, and the
Outpost run page's live transcript hide the complete exchange — reflection
prompt, memory tool activity, and closing response. The same projection keeps
it out of first-run prompt-history import, rewind/retry choices, forked-context
human-turn counts, and compaction summary input. Journals created before this
distinction was recorded explicitly receive the same treatment through a
narrowly matched historical prompt signature. Incremental views seed that
state from earlier records, so the exchange stays hidden even when a live view
or a marker-delimited replay starts after reflection began.

Reflection fires automatically on **every** client, not just the TUI:

- **TUI** — at quit, via a **two-phase quit** (see
  [DR 0089](https://github.com/kinra-ai/kin/blob/dev/docs/decisions/0089-two-phase-quit.md)). The first ctrl+c (or
  ctrl+q, the command palette, or `/exit` / `/quit`) flips the status bar
  to `curating memory… · ctrl+c to skip` and runs the bounded ~20 s
  reflection pass while the UI is still alive. A second ctrl+c (or esc)
  skips the wait and exits immediately. Exit paths that bypass
  `action_quit` (signal-killed runs, headless `app.exit()` calls) keep a
  guarded fallback reflection at unmount — same pre-Workstream-B UX on
  those paths, just no visible curation window.
- **`kin -p` / scheduled jobs** — after a run that ends cleanly (`done_reason:
  stop`) with no pending human gate, bounded to ~90 s. The pass is best-effort
  and can never change the run's outcome: `done_reason` / `final_text` /
  `needs_human` are snapshotted *before* it runs, so a job that succeeded is
  never recorded as `error` because of reflection. Runs that end any other way
  (`error`, `token_budget`, `turn_cap`, `loop_detected`, or `needs_human`) skip
  it. A halted run keeps its append-only journal focused on the blocked task
  until `--resume`; a reflection turn inserted before the operator's answer
  would make the continuation resume memory curation instead.
- **On demand** — `/memory` runs the same reflection pass mid-conversation,
  and `/memory init` bootstraps an initial memory set from the repo's
  `AGENTS.md` / `STATUS.md` / `README` / `docs/`.

Disable the automatic pass with `memory_reflect = false` /
`KIN_MEMORY_REFLECT=0` (the `/memory` command still works either way).

## Settings

```toml
# ~/.kin/settings.toml
memory_enabled = true          # default; false unregisters the tool + recall (project-ok)
memory_reflect = true          # default; false skips the end-of-run reflection pass (project-ok)
memory_dir = "~/.kin/memory"   # default; GLOBAL-ONLY (see below)
```

`memory_dir` is **global-only**: memory writes auto-apply in `auto` mode
*because* they're contained to the root, so a cloned repo's project file must
never be able to repoint that contained write surface at an arbitrary
directory. For a workspace-local store, set `KIN_MEMORY_DIR` (or the global
key) yourself.

## On Outpost

The [dashboard](/docs/kin/guide/outpost/) has a **Memory** card: the on/off toggle, the
root path, and per-category stats with a file list (names and titles only —
content stays in the store; read it in a session with `memory view`). The
store lives in the `outpost-kin` named volume, so memories survive redeploys.

The run page (a separate document — `GET /runs/<job>/<run>`) also has a
**Memory** pane (WP8 — [`docs/decisions/0031-run-inspection.md`](https://github.com/kinra-ai/kin/blob/dev/docs/decisions/0031-run-inspection.md)).
This is the FIRST body-crossing exception to the memory stats-only posture,
intentionally narrow:

- **Search** (`GET /api/memory/search?q=`) — wraps `MemoryIndex.recall(q, k=10)`.
  Body NEVER crosses the wire; only `(path, category, title, snippet)` tuples
  surface.
- **Body reads** (`GET /api/memory/body?path=`) — through the SAME
  `store.resolve_path` containment guard the `memory` tool uses (backslash,
  URL-encoded, `..`, dot-leading, symlink escape all refused at the seam).
  64 KB cap on body size; bytes NEVER cross for oversized content.
  Sanitized via `sanitize_model_id` (REFERENCE.md § Display sinks) before
  render.

**Every body read writes one `audit_log` row** (action `body_read`, target
the memory rel-path). The audit module is best-effort — an audit failure
MUST NOT break the read. See `container/dashboard/audit.py`.

The pane carries a persistent "Agent-written content — treat as data" banner
to frame the body content as untrusted input (a defense-in-depth reminder
even after server-side sanitization). Both endpoints are **human-door only**
(under `/api/`, behind the SSO gate via the dashboard's `/api/*` path
prefix) — the v1 machine door `/api/v1/` gains nothing and loses nothing,
so the PROTOCOL wire contract stays unchanged.

### Why this exception exists

Memory bodies are agent-written text — exactly the kind of content that's
useful to inspect from the browser but is also exactly the kind that can
smuggle prompt-injection vectors. The compromise: a single browser-only
surface, audited, escaped, capped, with a framed-untrusted banner so the
operator never forgets the threat model. If you need raw memory reads for
debugging, the session's `memory view` tool is still the canonical path.
