Skip to content

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 curates the store with the memory tool; a reflection pass at session end extracts durable knowledge for it.

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):

---
category: preference
---

Blake prefers squash merges on this repo.
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 views on demand.

The recall index is deliberately separate from the 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), while view is auto-allowed in both modes. 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 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.

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

  • TUI — at quit, via a two-phase quit (see DR 0089). 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), 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) skip it.
  • 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

# ~/.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 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). 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.