Sessions, memory & recall
The nine tools an agent uses most - open a session, write, edit and read memory, and search the store.
On this page
These nine tools are the agent loop. In a repo mapped to a project, most of them
need no project argument at all: session_start binds the connection, and
everything after it inherits that scope.
The shape of a session
session_start returns the project briefing and binds the session to the
connection. session_end persists findings for the next agent's briefing. Both
are optional in the sense that Claude Code's hooks already open an ambient
session per agent - calling session_start explicitly adopts it and gets you the
full briefing rather than the short injected one.
If tasks_update ever fails claiming a task is held by your own session id,
the connection binding was lost. Re-run session_start with the same name to
rebind.
Edit, update, append, supersede, or delete?
Five ways to change memory, and picking the wrong one is how a store rots:
| You want to | Use | What happens |
|---|---|---|
| Fix part of a memory without resending it | memory_edit |
Exact search/replace on the body, plus description and tag add/remove; returns a diff |
| Rewrite what a memory says, whole | memory_write with the same name |
Updated in place; the id is stable |
| Add to the end without rereading it | memory_append |
Body grows; nothing else changes |
| Replace a different, now-outdated memory | memory_write with supersedes |
The old one is marked invalid, leaves every index, and stays readable with a pointer to its replacement |
| Remove something written by mistake | memory_delete |
Gone |
The distinction that matters is supersede vs. delete. Superseding is how the store stays honest about its own history: the old memory leaves the briefing and recall, but an agent that follows an old reference still finds it, marked invalid, pointing at what replaced it. Delete is for mistakes - things that were never true - not for things that stopped being true.
A supersedes that fails is reported rather than swallowed: the new memory is
still written and kept, and the call returns an error naming it. The target is
then still active, so re-run the supersede.
Edit vs. supersede
memory_edit is cheap, which is exactly why its boundary has to be explicit.
Edit is for changes that carry no new claim: a typo, broken formatting, a stale
path or command, a stage's Status flip, a description, a tag. If the meaning
changes - the conclusion is now different, the advice reversed - that is a new
memory, and it goes through memory_write with supersedes.
The reason is provenance. A supersession retires the old memory into readable history; an in-place edit leaves no trace that the store ever believed something else. Using edit to change what a memory claims silently rewrites the record.
memory_edit is also the only way to change a memory's description or tags
without rewriting its body, and tags_remove is the only way to clear a tag at
all - an empty tags array reads as absent everywhere else.
Concurrency: content_hash and expect_hash
memory_read and notes_read return a content_hash - the SHA-256 of the whole
file. Pass it back as expect_hash on memory_edit, notes_edit, or
notes_update, and the write is refused if the stored file has moved on since
you read it. Omit it and the write is unconditional.
It answers a different question from the daemon's own serialization. Every application write already goes through a per-file lock, so two agents can no longer interleave a read and a write and lose one of them. What the lock cannot see is an agent acting on something it read minutes ago, or the owner editing the markdown in an editor - which is why the precondition is checked against the file, inside the lock, rather than against the index (the watcher re-indexes on a debounce, so the index would happily confirm the stale hash).
Scope
memory_write fails closed: with no session and no explicit project, it is
rejected as ambiguous rather than silently landing in the global scope. Pass
project: global to write a deliberately cross-project memory.
memory_edit targets a memory that already exists, so it resolves like
memory_append: the session's scope first, then a global fallback. The write is
judged against the project the memory actually sits in.
Recall is the only search tool
There is one search entry point. recall fuses FTS5 keyword matching and vector
similarity with reciprocal rank fusion, nudges the fused order by favorite and
utility (both bounded), scoped to the
current project plus global items, and packs results into a token budget. A call
that finds nothing is recorded as a miss - recurring misses become the
gardener's memory-wanted proposals.
The optional kind filter restricts hits to memories of one frontmatter kind.
It implies memories-only: combining it with scope=notes is rejected as
contradictory rather than returning a misleading empty result, and a
kind-filtered miss still counts as memory-wanted demand.
With kind set, query becomes optional: a kind alone is the browse mode
behind briefing hints like recall kind=convention - the scope's active
memories of that kind, listed newest-first under the same limit and token
budget. A browse is a listing, not a search: no fusion, no favorite or utility
boost, its hits record as passive exposure (never query-gated demand), and an
empty browse records no miss - "this project has no conventions yet" is not a
missing memory.
It degrades rather than fails: if the embedding provider is unreachable, recall falls back to keyword-only results instead of erroring. A local misconfiguration is surfaced instead of hidden - the two cases are deliberately not treated alike.
Results and failures
| Call | Success result | Failure that matters |
|---|---|---|
session_start |
session_id, name, resolved project, explanatory scope, and briefing; resumed/adopted sessions also say resumed: true |
Briefing assembly degrades to an empty string and logs; creating or binding the session itself still fails loudly |
memory_write |
Stable id, canonical name, resolved project, updated, optional similar, and optional superseded |
An occupied tombstone path is an error; if the new memory lands but supersession fails, the tool errors while naming the kept replacement and the still-active target |
memory_edit |
id, name, project, the new content_hash, a unified diff, and a stage_hint when a kind=stage body still has no parseable Status |
An old_string that matches zero or several places is an error naming the count, and nothing is written - the edits apply all-or-nothing. A stale expect_hash is refused rather than overwriting |
recall |
hits, possibly empty |
Remote embedder failures degrade to lexical-only; local request/config construction errors surface |
session_end |
Confirmation of the close - session_id, claims_released, mishaps_recorded; findings persist for the next briefing |
A missing/ambiguous session is an error rather than a fabricated successful close |
session_start
Begin or resume an agent work session and bind it to this connection. Returns the project briefing. Later memory/recall/notes calls inherit this session's project scope, so you rarely pass project again.
| Parameter | Type | Required | Description |
|---|---|---|---|
cwd |
string | no | Absolute working directory; auto-mapped to a project from the repo root on a repo's first session (no setup step -- seamlessd map-repo only overrides the derived slug) |
model |
string | no | Model id powering this agent, exactly as the provider names it (e.g. claude-fable-5, gpt-5.5). Stamped onto memories/notes this session writes; hooks keep it current for Claude Code/Codex sessions, so pass it mainly from other clients |
name |
string | no | Optional stable session name; reusing a name resumes that session |
source |
string | no | what began this session (default explicit). One of: startup, resume, clear, compact, explicit. |
session_update
Record interim progress on the current session (working findings so far). Uses the bound session unless you pass one.
| Parameter | Type | Required | Description |
|---|---|---|---|
findings |
string | yes | Working findings / progress note so far |
session |
string | no | Session name to operate on: the cc/<id> or cx/<id> on your briefing's 'Seam session' line, or a sess/* name. Defaults to the bound session; pass it whenever you have not run session_start and several agents are active -- the bare call is then ambiguous and fails rather than guesses |
session_id |
string | no | Session ULID to operate on; takes precedence over session and the bound session |
session_end
Complete the current session, persisting its findings for future briefings. Uses the bound session unless you pass one.
| Parameter | Type | Required | Description |
|---|---|---|---|
findings |
string | yes | Final findings: what was learned, decided, or left open. Prefer a tight summary (briefings show a short preview), but long findings are stored in full -- they are not rejected. |
mishaps |
array | no | Self-report mishaps this session caused: an action a warning or convention said not to take, live state touched by mistake, a command that hit the wrong target. Pass an array with one short entry per incident; omit when none happened. When a mishap violated a stored memory, name that memory by its exact slug in the entry (e.g. "violated chroma-boot-race by ...") -- the report is then linked to it. Recorded for recurrence review, not blame -- report them even when fully recovered. |
session |
string | no | Session name to operate on: the cc/<id> or cx/<id> on your briefing's 'Seam session' line, or a sess/* name. Defaults to the bound session; pass it whenever you have not run session_start and several agents are active -- the bare call is then ambiguous and fails rather than guesses |
session_id |
string | no | Session ULID to operate on; takes precedence over session and the bound session |
memory_write
Create or update a durable memory -- the compact knowledge a future session must not miss (a constraint, gotcha, decision, runbook). Long-form write-ups belong in notes_create; put the one-line lesson here. Writing an existing name updates it in place (its id is stable). On a new name, a semantically similar existing memory is reported as an advisory hint; the write still proceeds. The hint is withheld when the target project's content is fenced from you (see the withheld marker on the response) -- the write lands either way. Pass supersedes to replace a DIFFERENT, now-outdated memory: it is marked invalid and leaves every index (briefing, recall) but stays readable with a pointer here. If superseding fails, the new memory is still written and kept; the error says how to retry.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | yes | kebab-case identifier, unique within the project |
kind |
string | yes | memory kind; constraint = what any agent must or must not do regardless of task; convention = a project-local choice or layout fact (naming, branding, where things live or deploy, which files sync together); for kind=stage, open the body with Status: open|in_progress|blocked|done and optionally Gate: human|ai, and flip the status with memory_edit (or a full memory_write) -- append cannot change the header, which is parsed from the top of the body. One of: constraint, convention, runbook, protocol, gotcha, decision, refuted, reference, stage. |
description |
string | yes | one line, <=150 chars -- the only text shown in indexes |
body |
string | yes | markdown body (aliases: content, text) |
project |
string | no | project slug; defaults to the bound/ambient session's project. An unknown slug CREATES that project -- naming a new one is normal and never an error. Pass project=global ONLY for knowledge that belongs in EVERY project's briefing; it is not a neutral default. With no session and no explicit project the call is rejected as ambiguous. A session bound to a confidential or sealed project can write ONLY into that project. |
supersedes |
string | no | name of an existing memory this one replaces; that memory is marked superseded (invalid) and pointed here |
tags |
array | no | tags, replacing all (a comma-separated string is also accepted); omit to leave an existing memory's tags untouched, and note an empty list reads as absent, not as a clear |
memory_append
Append markdown to an existing memory's body. The memory keeps its id. To create a new memory, use memory_write.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | yes | memory name |
body |
string | yes | markdown to append (aliases: content, text) |
project |
string | no | project slug; defaults to the bound/ambient session's project, then global. Pass project=global to target a global memory. |
memory_edit
Edit an existing memory in place with exact search/replace, instead of resending its whole body through memory_write. Each edit's old_string must match the current body exactly and uniquely (or pass replace_all); all edits apply together or none do, so a failed match changes nothing. It is also the only way to change a memory's description or tags on their own: send description/tags_add/tags_remove with no edits and the body is left untouched (memory_write requires a body, so there was no metadata-only path before this), and tags_remove is the only way to clear a tag at all. Returns a unified diff of what landed plus the new content_hash. Use this for corrections that do not change what the item CLAIMS: typos, broken formatting, a stale path or command, a stage's Status flip, metadata. If the MEANING changes -- the conclusion is now different, the advice reversed -- that is a new memory: use memory_write with supersedes, which retires the old one into readable history instead of erasing what it used to say.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | yes | memory name (kebab-case, as memory_read takes it) |
description |
string | no | replace the one-line description (<=150 chars -- the only text shown in indexes); omit to leave it untouched |
edits |
array | no | ordered list of exact search/replace edits, applied in order to the CURRENT body. Each is {old_string, new_string, replace_all?}. old_string must match the body EXACTLY (whitespace and indentation included) and must be unique unless replace_all is true; include surrounding lines to make it unique. All-or-nothing: if any edit fails to match, nothing is written. |
expect_hash |
string | no | optional precondition: the content_hash you last read for this item (memory_read/notes_read return it). The write is refused if the stored file has changed since -- another agent or the owner edited it -- so re-read and re-apply your change instead of overwriting theirs. Omit it to write unconditionally. |
project |
string | no | project slug; defaults to the bound/ambient session's project, then global. Pass project=global to target a global memory. |
tags_add |
array | no | tags to add, leaving the rest in place (a comma-separated string is also accepted) |
tags_remove |
array | no | tags to remove, leaving the rest in place; this is how a tag gets cleared (a comma-separated string is also accepted) |
memory_read
Read a memory by name within the current project (falling back to a global memory of the same name), or directly by id.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | no | memory id (ULID), as carried by events, recall results, and gardener proposals; bypasses name/project resolution |
name |
string | no | memory name; pass exactly one of name or id |
project |
string | no | project slug; defaults to the bound session's project |
memory_delete
Delete a memory by name: the markdown file leaves the disk and its index row goes with it, with no provenance and no pointer left behind. Prefer nearly anything else. To replace knowledge that turned out to be wrong, use memory_write with supersedes -- the old memory drops out of every index (briefings, recall) but stays readable, pointing at what replaced it, which is how a later reader learns the thing was reconsidered rather than that it was never believed. To retire something merely stale, leave it for the gardener's archive proposal. Reserve deletion for memories written by mistake: a duplicate, a test, a write into the wrong project.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | yes | memory name |
project |
string | no | project slug; defaults to the bound session's project |
recall
Search durable knowledge (memories, notes) and the work record (tasks, trials, session findings) by meaning and keyword (fused), scoped to the current project plus global items. This is the single search entry point. Work-record hits carry a status (a task's status, a trial's outcome) and match on keyword only. With kind set and no query it lists that memory kind newest-first instead (browse).
| Parameter | Type | Required | Description |
|---|---|---|---|
kind |
string | no | only memories of this frontmatter kind (e.g. convention); implies memories-only, so any scope that excludes memories is rejected; with no query, lists the kind newest-first. One of: constraint, convention, runbook, protocol, gotcha, decision, refuted, reference, stage. |
limit |
integer | no | maximum results (default 10, max 100) |
project |
string | no | project slug; defaults to the bound session's project |
query |
string | no | what you are looking for; required unless kind is set (kind alone lists that kind newest-first) |
scope |
string | no | what to search (default all). One of: all, memories, notes, tasks, trials, sessions. |