session · git:20260806.9aa5028 · 2026-08-06 · sha256 6251bde8d8cfdf90

session git:20260806.9aa5028A

Immutable. This exact content never changes and is served at /api/v1/blob/6251bde8d8cfdf90.

---
name: session
description: Always-active cross-session working memory. Auto-activates every conversation to offer resuming or starting a session. Also triggers on "start a session", "resume", "park this", "save this", "wrapping up", or multi-session/multi-repo work. Routes work to the correct Obsidian vault, groups sessions under a parent Project (epic), and distills durable insights up into the Project note and MOCs at stopping points.
allowed-tools: Bash(vaultmem *), Bash(git *), Bash(stat *), Bash(find *), Read, Write, Edit, Glob, Grep
---

<!-- CANONICAL SOURCE: this repo (jayantak/vaultmem). The dotfiles copy under agents-source/skills/ is synced FROM here — edit this file, not that one. -->

# session — cross-session working memory

The hot layer above the durable Obsidian vaults. Sessions are per-thread
worklogs that survive `/clear`. The conversation is disposable; the session
file is the state. Pairs with `vaultmem` (search/registry/router),
`vault-recall` (finding what's already written), and `vault-capture` (durable
capture).

The vault layout, frontmatter fields, status vocabulary, and glyph convention
this skill writes are the normative contract in `SCHEMA.md` (run `vaultmem
vaults` for the live registry). The templates below are the operational shapes;
`SCHEMA.md` is the reference when they disagree.

## Always-on activation

The `SessionStart` hook prints a picker line (`vaultmem sessions`) ending
in an AGENT DIRECTIVE — the trailing prompt that tells the agent what to do
with the picker. Its wording is configurable (the registry's `directive_file`);
the default asks the agent to route session handling through this skill. That
directive is binding: invoke this skill FIRST, every
conversation, before any other skill, tool call, or reply. Memory lookups
(`vault-recall`, `vaultmem`) do NOT substitute — even when they read the
same session file, route through this skill first, then continue. Be concise;
never block the user's actual task. If the user skips, do NOT re-ask this
conversation.

**Trivial one-shot exception:** if the first user message is a quick question
or a single read-only lookup answerable in one reply, skip this skill silently
and just answer. The moment the thread turns into real work (edits, a task, a
second substantive turn), invoke the skill then. Never announce the skip.

## Vault routing (never hardcode a vault)

Always resolve the vault through the router:

- Guess: `vaultmem which` (prints the routed vault id).
- Confirm with one line: "New session in the **<vault>** vault — ok? (or say the other)".
- Registry facts (paths, sessions root, default MOC): `vaultmem vaults`.

Resolve `<vault_path>` with `vaultmem path <vault>` (bare path on
stdout — use it directly in scripts as `V="$(vaultmem path <vault>)"`;
`vaultmem vaults` still prints the full registry).

## A session is one task, not one project

A session = **one focused unit of work** (a feature, a bug, an investigation).
The tier *above* a session is a **Project** (`Projects/<name>.md`) — that is
where cross-session state accumulates. When the unit of work closes, **park the
session and open a fresh one under the same Project**; do not keep one session
open as a stand-in for the whole project (that is what makes `_index.md`
balloon). Tiers: **MOC** (domain map) → **Project** (epic, owns repos +
cross-session state) → **Session** (one thread).

A **Task** (`Tasks/<slug>.md`) is the state *before* a session: work that is
planned but not started. A session is created when work **starts**, so intent
that predates it has nowhere to live — that is what a task is for. See § The
backlog below.

List projects with `vaultmem projects`; see one with
`vaultmem project <name>` (its repos, linear pointer, MOC, and sessions
by status).

## The backlog (Tasks/)

A **Task** is a spec, not a worklog: enough context that an agent (or you, in
three weeks) can start cold without asking a follow-up question. It is the
answer to "I want to plan this now and have something pick it up later."

- `vaultmem next` — the ready queue, most-ready first. **This is the "what
  should I pick up" surface.** Blocked tasks are listed separately, never mixed
  into the ready set.
- `vaultmem task <slug>` — one task in full: its facts plus the brief.
- `vaultmem task <slug> --promote` — prints the exact edits that convert the
  task into an active session. Read-only; you apply them.

Task status is its own vocabulary — `backlog | next | active | done`. There is
no `parked`: an unstarted thing pausing is just `backlog`. `next` means
explicitly queued; `active` means it was promoted and **its session now owns the
state**.

### Write a task

Create `<vault_path>/Tasks/<slug>.md` (flat folder, no per-task directory):

```markdown
---
type: task
status: backlog       # backlog | next | active | done
project: <plain project name>
repos: [<repo>]       # so an agent knows where to work
linear:               # OUTBOUND pointer only — never synced back
blocked_by:           # slug of a task that must land first (omit when unblocked)
updated: <YYYY-MM-DD HH:MM>
---
# <task title>

<Why this needs doing, and the context needed to start cold.>

## Acceptance criteria
- [ ] <verifiable outcome, not an activity>
```

`linear:` is a **one-way pointer**. Linear/Jira stays the canonical team
surface; the vault never syncs status back, so the two can't fight over truth.
Use a task for work you don't want in the tracker, or to hold the agent-ready
brief for a ticket that is already there.

### Promote a task into a session

Task → session is a **conversion, not a copy**: one unit of work keeps one
identity from planned → active → done. Run `vaultmem task <slug> --promote`
and apply what it prints — it creates `Sessions/<slug>/` with a `task: <slug>`
backlink, flips the task to `status: active` with a `session: <slug>` pointer,
and registers the session on the Project.

**Do not restate the brief in the session.** The task note remains the spec;
link it (`[[<slug>]]`) and let `## Bookmark` carry live state. Duplicating it
is how the two drift.

`vaultmem task <slug> --promote --apply` performs the same recipe itself
(session note, then Project index, then the task's own fields — in that
order, so a mid-failure never leaves a task pointing at a session that
doesn't exist) for a caller that needs it done non-interactively rather than
applied by hand.

### Keep the backlog honest

An unstarted task has no forcing function — nothing fails when it rots — and a
stale backlog makes `next` untrustworthy, which defeats the point. `groom` and
the SessionStart nudge flag tasks untouched past `VAULTMEM_TASK_STALE_DAYS`
(default 14). When one surfaces: promote it, re-scope it, or delete it. Leaving
it is the one option that costs you the tool.

## Create a session

1. Route + confirm vault (above).
2. **Attach to a Project.** Resolve the parent epic:
   - Run `vaultmem which` for the vault, then `vaultmem projects`
     to see candidates. If the cwd is a repo, prefer the Project whose `repos:`
     lists it.
   - If exactly one fits, use it. If ambiguous or none, ask one line: "Which
     project does this belong to — `<a>` / `<b>` / new / none?".
   - If a new Project is needed, create `Projects/🟢 <name>.md` from the Project
     shape (below) with `type: project`, `status: active`, `repos:`, and
     `aliases: ["<name>"]` (glyph on filename + H1; see Status glyphs below).
3. Pick a short kebab `<thread>` from the task.
4. Write `<vault_path>/Sessions/<thread>/_index.md` from the template, stamping
   `project:`, `aliases: [<thread>]` (so the Project's back-link resolves), +
   inheriting `repos:` from the Project, and auto-linking MOCs/repos/people
   inferred from the opening context.
5. **Register the session on the Project.** Append a line to the Project note's
   `## Sessions` index: `- [[<thread>]] — <one-line> (status: active)` (+ the
   tracker issue id if the project has one, e.g. `(PROJ-1234, active)`). This `[[<thread>]]` only
   resolves because step 4 gave the session an `aliases: [<thread>]` entry.

### `_index.md` template

```markdown
---
thread: <thread>
project: <project-or-blank>
vault: <vault-id>
status: active
aliases: [<thread>]   # REQUIRED — so [[<thread>]] back-links resolve (the file is _index.md, basename ≠ thread)
updated: <YYYY-MM-DD HH:MM>
---
# 🟢 <thread>
**Goal:** <one line>   **Project:** [[<project>]]   **MOCs:** [[<moc>]]   **People:** [[<person>]]
**Repos:** [[<repo>]] (branch <branch>)   **Related:** [[<prior-session>]]

## Bookmark
Last: <what just happened> · Next: <next action> · Open: <unresolved>

## Pinned

## Work log

## Decisions

## Git state
| Repo | Branch / worktree | PR | State |
|---|---|---|---|
```

**Parent Project.** `project:` is the session's parent epic — a `Projects/<name>.md`
note. The picker groups sessions by it, and distill promotes durable bits up to
it. Leave it blank only for genuinely one-off work with no home. `repos:` default-
inherit from the Project note; restate them in the header (override only if this
session touches a different repo).

**The spine is fixed; topical sections are free-form.** `Bookmark`, `Pinned`,
`Work log`, `Decisions`, `Git state` are the required skeleton. Add your own
`## <topic>` working sections as the task needs them (an analysis in progress, a
decomposed plan, a verdict) — real sessions grow these, and distill collapses
them. Don't force everything into the work log.

**`## Pinned` = load-bearing constants that must survive distill.** The handful
of facts you'd be annoyed to re-derive: the stage/env gotcha, the one binding
constraint, the non-obvious command, the canonical file to edit, the worktree
layout. Keep it short (≤6 lines); it is re-read first on resume. Distill never
strips it — correct it in place when a constant changes. (Leave the heading empty
until you have a constant worth pinning.)

**Link targets must resolve.** The `[[<moc>]]` / `[[<repo>]]` / `[[<person>]]`
placeholders are not free text — a wikilink works only if a note's exact
basename or alias matches. Link MOCs by filename `[[MOC - <Topic>]]` (or a
declared alias like `[[Payments]]`); for a repo/person, link only if a real note
exists (`vaultmem mocs` / `index` to check), else use plain text. Never
wikilink repo artifacts (ADR IDs, file paths, PR numbers). Full rules:
`vault-capture` § Linking Rules.

### The Project note (`Projects/<name>.md`)

The long-lived epic. Frontmatter:

```yaml
---
type: project
status: active            # active | parked | done
vault: <vault-id>
repos: [<repo>]           # sessions inherit this
linear: <url>             # optional, if the project tracks tickets (read for context; never a worklog)
moc: "[[MOC - <Topic>]]"  # optional — the domain map above this project
updated: <YYYY-MM-DD>
---
```

Body spine: `## Sessions` (the index, grouped by status) · `## Pinned`
(cross-session load-bearing constants) · `## Decisions` (durable cross-session
decisions) · free-form topical sections. Keep it lean the same way `_index.md`
is — distill (below) is what keeps it that way.

### Status glyphs (sidebar self-sorts by state)

Every Project and Session carries a status glyph so the Obsidian file-tree
sidebar shows state at a glance. The glyph maps 1:1 to the `status:` field:

| status | glyph |
|---|---|
| active | 🟢 |
| parked | 💤 |
| done / shipped | ✅ |

- **Session** (`_index.md`): glyph goes on the **H1 only** (`# 🟢 <thread>`).
  NEVER glyph the session *folder* name — `vaultmem` keys sessions by
  folder basename == the `thread:` field, so a glyphed folder desyncs them and
  breaks the picker/groom. Sessions churn fast and their status shows in the
  picker anyway, so the H1 glyph is enough.
- **Project** (`Projects/<name>.md`): glyph goes on **both the filename and the
  H1** (`🟢 <name>.md` / `# 🟢 <name>`) — the filename is what the sidebar
  sorts. Renaming a flat project file is safe as long as you first add the
  plain name as a frontmatter `aliases:` entry (`aliases: ["<name>"]`) so every
  existing `[[<name>]]` link keeps resolving. `vaultmem` strips the
  leading glyph when matching a project to its sessions, so sessions keep the
  **plain** `project:` name — never write the glyph into a session's `project:`.
- **Keep it in sync on every transition.** When a status flips (active→parked at
  park, →done at retire), update the glyph in the same write: the H1 for a
  session; the H1 **and the filename** for a project (re-`mv` + keep the alias).
  A stale glyph is a lie the sidebar tells — treat it like a stale `## Bookmark`.
- Non-status notes (references, reviews, `2026 Review`) take no status glyph.

## Indexed mode (auto-persist, no prompts)

Once a session is active, after every few tool calls (or whenever you produce
durable content — decisions, findings, plans):

**First edit of the conversation: Read `_index.md` before editing it** (a
section read via offset/limit satisfies this). The Edit tool rejects writes
to never-read files — this is the top recorded tool error in past sessions.
The same applies to the Project note and `MEMORY.md`.

`vaultmem cat`/`bookmark` do NOT satisfy that precondition. They are separate
processes; the Edit tool only counts a file as read when *you* read it with the
Read tool. Use them for cheap inspection (resume, re-anchoring, checking a
section before deciding), and Read the file itself before the first edit.

1. Append a timestamped bullet to `## Work log`. Lead it with a status marker
   when it helps scanning — **DONE** / **BLOCKED** / **DECISION** / **NEXT**.
2. Rewrite `## Bookmark` (Last / Next / Open) — always current, never stale.
3. If git state moved (branch, PR, worktree, merge/push), refresh `## Git state`
   **in the same write**. The work log is the source of truth for what happened;
   the table is just the at-a-glance index — never let it contradict the log.
   `vaultmem worktrees [thread] [--format json]` reads this table back out
   (one thread, or every active session) for a consumer that needs "which
   worktree is thread X on" without parsing markdown itself.
4. Add or correct a `## Pinned` constant if this turn surfaced one (a gotcha,
   a binding decision, the canonical command/path).
5. Keep header wikilinks current (`[[MOC]]`, `[[repo]]`, `[[person]]`,
   `[[prior-session]]`); bump `updated:`.

Never keep durable results only in conversation memory. The write is atomic:
content + Bookmark (+ Git state if it moved), same turn. Corrections edit the
existing section in place rather than appending.

**Re-anchor periodically.** On a long thread, every ~5–6 substantive turns
re-read your own `## Bookmark` + `## Pinned` (`vaultmem bookmark <thread>`) before deciding the next move — it
keeps you on the task instead of drifting with the conversation, and it surfaces
when the file has bloated enough to checkpoint (below).

## Resume

Run `vaultmem bookmark <thread>` — it prints exactly the `## Bookmark` +
`## Pinned` blocks, not the whole file. Those two are the resumable state (what
just happened / what's next / the load-bearing constants). Read further sections
only if the next action needs them, with `vaultmem cat <note> --section '## <Heading>'`.
The picker's number/name selects the thread; resolve its `_index.md` path.

**If the bookmark already declares completion** (Next: none / done / shipped, or
the only Open item is a tracker link), don't resume it as if there's work left —
flip `status: done` + ✅ right then and tell the user, rather than reopening a
finished unit.

## Checkpoint (distill in place, keep going)

Checkpoint is the pressure-release valve that keeps `_index.md` lean **without
needing a stopping point**. Trigger it on the FIRST of these — don't wait for the
user to ask:

- the user says "checkpoint" / "distill but keep going"; OR
- `_index.md` drifts past **~150 lines** (the work log is dense — it bloats fast); OR
- the conversation has run long / context is filling AND you've produced durable
  content (a decision, a finished analysis, a shipped change) since the last
  distill — checkpoint at the next turn boundary rather than riding context to the
  edge. Catching it early beats an emergency compaction that loses the trail.

Then load [references/distill.md](references/distill.md) and run the
**checkpoint** flow: promote durable bits to the vault, collapse the promoted
work-log entries to `→ promoted to [[note]]` pointers, leave `## Pinned` intact,
and keep `status: active`. No `_meta.md` event, no clear — the conversation
continues. It is the lighter alternative to park when there's no stopping point
yet. A one-line confirm is enough; don't interrupt the user's flow to ask
permission for an in-place checkpoint.

## Park / end + distill

When the user says "park", "wrapping up", "let's clear", OR you detect a natural
stopping point (task list drained AND a coherent unit closed AND context
growing), load [references/distill.md](references/distill.md) and run the **park**
flow (checkpoint + `_meta.md` event + clear-safe). Offer: "Natural stopping point
— distill durable bits into the vault and clear?"

**At park time, decide done vs parked — don't default to parked.** If the unit
of work is complete (the bookmark says shipped/done/nothing next, or the only
remainder is tracked on GitHub/Linear), set `status: done` + ✅ immediately in
the same write, not "parked" as a placeholder for later cleanup. `parked`/💤 is
ONLY for work that will genuinely resume — a real next action you expect to
pick up. Sessions are not ticket trackers: an open PR review, a follow-up
issue, or a "waiting on review" state tracked on GitHub/Linear does not keep a
session alive on its own — leave a pointer to the tracker item and retire the
session as done.

## Lifecycle & grooming

A session moves `active → parked → done`, then is **archived** — `archived` is a
*location*, not a status: archived sessions live under `Sessions/_archive/<thread>/`
and drop out of every listing surface (picker, `projects`, `project`). Wikilinks
resolve by basename, so `[[<thread>]]` keeps working after the move. The picker
shows **active + parked only** (resumable work); `done` is hidden, awaiting groom.
On each transition, update the session H1 glyph (🟢→💤→✅) in the same write —
and when a **Project's own** status changes, re-`mv` its file to the new glyph
(💤 when parked, ✅ when shipped) and fix its H1, keeping the plain-name alias.

The SessionStart picker appends a `⚠ … run vaultmem groom` nudge when a
vault has `done` sessions (ready to archive), `parked` sessions untouched past
`OBSIDIAN_SESSION_COLD_DAYS` (default 21), `active` sessions untouched past
`VAULTMEM_STALE_ACTIVE_DAYS` (default 7), or `backlog`/`next` **tasks**
untouched past `VAULTMEM_TASK_STALE_DAYS` (default 14). `vaultmem status`
surfaces the same nudge in short form. When you see it:

- **`vaultmem groom`** mechanically moves every `done` session into
  `_archive/` and flips its parent Project's `## Sessions` line to `archived`.
  Safe to run anytime (done was already distilled at park). Scope with `-v <vault>`.
  It also archives every `done` **Project** into `Projects/_archive/` — unless a
  session outside `_archive/` still points at it, in which case `groom` prints a
  warning naming the blocking session(s) instead of moving it. Clear those first
  (archive or reassign the session) and re-run `groom`.
- It also **lists cold-parked sessions for triage** — it never auto-retires them.
  For each, drive the decision with the user: **resume** (open it, status back to
  `active`), or **retire**. To retire: confirm it was distilled at its last park
  (it has a `_meta` park event / `→ promoted` pointers — cold means untouched
  since park, so normally yes); if not, run the distill loop
  ([references/distill.md](references/distill.md)) first; then set `status: done`
  so the next `groom` archives it.
- It also **lists stale-active sessions for triage** — an `active` session that
  has gone untouched this long likely stalled. Triage it the same way as
  cold-parked, just from the other side of the fence: either the work actually
  finished (a bookmark that was never flipped — set `status: done` per the
  park-time rule above, so the next `groom` archives it), or it is genuinely
  still live (**park** it if paused but not abandoned, or touch `updated:` and
  keep it `active`). A stale-active entry is usually the symptom this whole rule
  set exists to catch — the park-time done/parked decision above is what
  prevents it from recurring.
- It also **archives `done` tasks** into `Tasks/_archive/` (a done task has no
  dependents to strand, so nothing blocks the move) and **lists stale backlog**
  for triage. Drive that decision too: promote, re-scope, or delete. Do not
  leave it — see § Keep the backlog honest.

### Project retirement

Projects get the same `active → done → archived` lifecycle as sessions, with one
added precondition: a Project can only retire once its **goal is met and every
session under it is `done` or already `archived`** — a Project with a live
session is not done, no matter how old it looks (`groom` enforces this too: it
refuses to archive a `done` Project a live session still points at, and names
the blockers). When that holds:

1. Distill any remaining durable content (open `## Pinned` constants, unresolved
   `## Decisions`) up to the vault's MOC tree, same as a session distill —
   the Project note disappears from active listings after this, so nothing
   should be left only there.
2. Set `status: done` in frontmatter.
3. Glyph both the filename and the H1 (✅), keeping the plain-name `aliases:`
   entry so existing `[[<name>]]` links keep resolving — same rule as any other
   Project status transition (see Status glyphs above).
4. `vaultmem groom` moves it into `Projects/_archive/` on its next run.
   Archived is a location, not a status, exactly like sessions — wikilinks
   keep resolving via basename/alias after the move.

## Frugality rule

Never read a full file when a section will do — `vaultmem cat <note> --section
'## <Heading>'` extracts one heading block without guessing offsets, and
`vaultmem bookmark <thread>` is the pre-cut Bookmark+Pinned pair (measured on a
real vault: 2568 bytes vs 11950 for the whole `_index.md`, 4.7x cheaper). Both
are read-only inspection — they do not satisfy the Edit tool's read-before-write
precondition (see Indexed mode). The `_index.md` IS the state;
keep it ~100–150 lines — when it grows past that, **checkpoint** (see above) to
promote and collapse rather than letting it bloat. Search across everything with
`vaultmem <query>` — run 2–3 pattern variants (exact term, synonym, adjacent
concept), and treat match lines as leads: read the note (or its section) before
answering, never answer from search output alone. Follow `[[links]]` between notes with
`vaultmem links` / `backlinks` (see `vault-recall` § Researching by
following wikilinks) instead of reading whole folders.