Memory
ws keeps one memory that every agent and model can share: facts about a repo, decisions and why they were made, your preferences, where a piece of work stands, and handoffs between sessions. It lives in a SQLite file on your machine, not inside any one agent. Moving from Claude to Codex to another CLI and back doesn’t lose it.
This page covers:
- the store and the
ws memcommands - session history from agents’ hooks, and project files indexed into the store
- context given to agents when they start
ws mcp, which lets agents search and write memory themselves- the memory panel (
prefix y) - continuing a pane’s work in another agent (
prefix A,ws handoff) - an opt-in summarizer that turns finished sessions into summaries and proposed facts
What’s in it
Section titled “What’s in it”Each item is a short piece of Markdown: a title and an optional body of up to 4 KB. For anything longer, put it in a file and link to it.
| Kind | For | Example |
|---|---|---|
fact |
Something true about the code or setup | “Run bin/setup_db before specs” |
decision |
What was decided and why | “Button variants use cva; no color props” |
preference |
How you like things done | “Small PRs; ask before adding gems” |
task |
Where a piece of work stands | “Focus ring done; dropdown next” |
handoff |
Done / state / next / open questions, for the next session | |
summary |
A summary of a session (from the summarizer, later) | |
note |
Anything else (the default) |
Scope says where an item applies:
| Scope | Key | Written as |
|---|---|---|
global |
none | global |
repo |
the repo’s origin, e.g. github.com/acme/storefront, so every clone and worktree shares it |
repo (this repo) or repo:github.com/org/name |
workspace |
the ws workspace’s name | workspace (this pane’s) or workspace:Design System |
workstream |
the workstream’s name | workstream or workstream:pr-101 |
project |
the project’s slug | project (this tab’s) or project:button-a11y |
session |
an agent session | session:<agent>:<id> |
A repo without an origin is keyed by its main checkout’s path.
Status: active items are what agents are told. proposed items wait
for you; agents’ writes to shared scopes (repo, workspace, global) will land
there. superseded items are older versions. archived items are kept
but no longer given to agents, and rejected items were turned down.
Provenance: every item records who wrote it (you, ws, or which agent,
model and session), how (cli, mcp, hook, …), and where (pane,
workspace, workstream, directory, git commit).
Ids are 26 characters. The CLI shows the last 6 (h8pyg4), and any
unique start or end of 4 or more characters works in commands.
Using it from the shell
Section titled “Using it from the shell”ws mem add "Run bin/setup_db before specs" --kind fact --link file:bin/setup_dbws mem add --kind preference --scope global "Small PRs, one feature each"ws mem search setup dbws mem list --status proposedws mem show h8pyg4ws mem edit h8pyg4-
add: the first line of the text is the title and the rest is the body. With no text, it reads stdin. The default scope isrepoinside a git repo, elseglobal.--proposestores it as proposed;--pinmeans it’s always included when context is given to an agent. -
search: full-text search over titles, bodies and tags, best first.- Words match their stems (
runningfindsrun), and the last word matches as a prefix. - Every word must match. Punctuation is just text, not search syntax.
- Words match their stems (
-
list: newest first, pinned on top, active only. Use--statusor--all(any status, expired items too) to see more. -
show: the text, provenance, tags, links, use count, expiry and version history. -
edit: opens the item in your editor (the same one ws uses for project files:$WS_EDITOR, else Neovim, else$VISUAL/$EDITOR), with a small header for kind, scope, tags and links.- Saving makes a new version; the old one becomes
supersededand stays in the history. --in-placejust fixes the text (typos).
- Saving makes a new version; the old one becomes
-
approve,reject,archive,pin,unpin. -
forget: deletes for good, by id, or by--scope,--since YYYY-MM-DDor--session <id>. It asks first (--yesskips the question). Deleted text is overwritten on disk (secure_delete). -
export: JSON lines with provenance.importbrings them back asproposed, so an import never changes what agents are told until you approve it. -
lint: reports three things:- items linking to files that no longer exist
- tasks past their expiry
- items nobody has used in 90 days (
--days)
It only reports; you decide what to archive.
-
path: where the database is.
Every command is in the command reference.
Session history
Section titled “Session history”ws records every agent session it sees, from any CLI, using the hook events it already receives for agent status. So there’s one history across Claude, Codex and any other agent with hooks. For each session it keeps:
- the agent and model, its own session id, and its transcript path
- the repo (by
origin), workspace, workstream and project, and the directory it ran in - when it started and ended
- its first and last prompt (cut to 500 characters, and redacted)
- the commits made during it (
git logfrom the commit it started on) and the uncommitted change (git diff --shortstat)
A new session in the same pane (after /clear, or a different agent) closes
the previous one; so do the agent exiting and the pane closing. Sessions a
crashed server left open are closed the next time it starts.
ws mem sessions # this repo's sessions, newest firstws mem sessions --all # every repows mem sessions --project button-a11y --jsonws’s own handoff: when a session in a workstream tab ends after doing
something (a prompt or a commit), ws writes a handoff item in that
workstream’s scope. It holds the same facts as the project log entry ws
writes: agent, model, session, first and last prompt, commits, diff. The
next agent in that workstream starts with it. Project tabs don’t need this:
ws already writes its entry into the project’s log.md, and that file is
indexed (below).
Projects in memory
Section titled “Projects in memory”Project files stay the source of truth, and ws indexes them into the store
in the project scope, one way:
| File | Becomes |
|---|---|
decisions.md |
a decision per ## section (title: the heading) |
plan.md |
a task per - [ ] item; ticked tasks are archived |
log.md |
a handoff per entry (entries in HTML comments, like the template, are skipped) |
The server checks the files every 10 seconds (the same check that keeps the PROJECTS sidebar current) and reads only files that changed.
- Editing a section updates its item in place: the file is the truth, so there’s no new version.
- Removing a section archives its item.
- Long sections are cut to 4 KB with a pointer to the file.
Search finds project decisions next to everything else:
ws mem search cva --scope project:button-a11y.
Don’t edit synced items with ws mem edit: the next sync puts back what the
file says. Edit the file.
Context for agents
Section titled “Context for agents”When ws starts an agent, it gives the agent a context pack: a short
Markdown summary of the memory that applies where the agent runs. It’s
packed to fit a budget: ws puts in the most important memory first and
stops at memory.budget tokens. “When ws
starts an agent” means an agent pane from a workstream template: one with
agent = "claude" or agent = "codex", or one whose command starts
claude or codex (as a review tab’s agent does, once {review_command}
is filled in). The pack is built in the pane at
launch, by ws context pack, so it always reflects the pane’s directory,
workstream and project, and what’s in memory right then.
| Agent | How it gets the pack |
|---|---|
| Claude Code | --append-system-prompt "$(ws context pack --for claude)" is added to its command |
| Codex | -c "developer_instructions=$(ws context pack --for codex --toml)" is added. Any developer_instructions you set in ~/.codex/config.toml are kept, and the pack goes after them |
A command that already passes its own instructions is left alone, since two
sets of instructions may not both count. The default review command is one:
it tells Claude to read the PR’s discussion first with
--append-system-prompt, so a Claude reviewer gets the pack from its
SessionStart hook instead (below). A Codex reviewer gets it on its command
line like any Codex pane. An agent ws doesn’t know how to give context to is
left alone too.
Sessions you start yourself (typing claude or codex in a pane) get
the pack from their SessionStart hook: ws report prints it as the hook’s
additionalContext, which Claude Code and Codex add to the session. When
it’s given:
| Session starts because | Pack |
|---|---|
You started claude or codex |
Yes |
| ws started it from a template (it has the pack on its command line) | No; it has it already |
/clear |
Yes, again |
Compaction (source: compact) |
Yes, again, since compaction drops detail |
claude --resume / --continue, codex resume, a Codex fork |
No; the conversation has it |
This needs ws’s hooks (ws integrate claude, ws integrate codex).
With Codex’s shared background daemon running, Codex runs every window’s
hooks with the environment of whichever pane started the daemon, so a pack
built there would describe the wrong place. ws only adds context when the
hook runs in the session’s own directory, which leaves those sessions
without one. ws integrate codex turns the daemon off, and ws doctor
warns while it runs.
What’s in a pack, in this order, from the pane’s scopes (workstream, project, workspace, repo, global; more specific first), active items only:
- Pinned items
- Preferences
- Decisions and facts (notes too)
- Current tasks (up to 5)
- Latest handoffs (the last 3, from any agent)
- Recent sessions here (the last 3 in this repo: when, agent and model, first prompt, commits)
It’s capped at memory.budget tokens (1,500 by default, about 6,000
characters). Items that don’t fit are counted at the end (“12 more items
didn’t fit; ws mem search finds them”). Items end in [m:<id>], so an
agent can run ws mem show <id> for the detail. Each item given to an agent
has its use count raised.
See the pack for where you are:
ws context packws context pack --budget 500ws context pack --jsonTurning it off
Section titled “Turning it off”In Settings › Agents & memory (prefix S), or in
~/.config/ws/config.toml:
[memory]enabled = true # false: record nothing, give nothingdeliver_to = ["claude", "codex"] # agent kinds that get packsbudget = 1500 # tokens
[memory.repos."github.com/acme/secret-client"]capture = false # record nothing from this repo, not even session rowsdeliver = false # never put this repo's items or sessions in a packRepos are keyed by origin, as in scopes (github.com/org/name,
lowercase).
Agents reading and writing memory (MCP)
Section titled “Agents reading and writing memory (MCP)”ws mcp serves memory to agents over MCP (the Model Context Protocol), so
any agent that speaks it can search memory and save to it while it works.
That includes Claude Code, Codex and others. Register it once per agent:
ws integrate claude --mcpws integrate codex --mcpThese run claude mcp add --scope user ws -- <ws> mcp and codex mcp add ws -- <ws> mcp, so the setting lives in each agent’s own config.
ws integrate <agent> --mcp --remove undoes it, and ws doctor says whether
it’s registered. New agent sessions get the tools:
| Tool | Does |
|---|---|
memory_context |
The context pack for where the agent is (the same as ws context pack) |
memory_search |
Full-text search, optionally by scope and kind |
memory_get |
One item in full: who wrote it, when, and its versions |
memory_propose |
Save something for the next agent |
memory_handoff |
Record done / state / next / open questions before stopping |
session_history |
Recent sessions from any agent in this repo, project or workstream |
What agents may write:
- Working notes in their own workstream, project or session are saved straight away.
- Anything shared (the repo, a workspace, global) is proposed. It waits
in
ws mem list --status proposeduntil youws mem approveit, and until then other sessions don’t get it. - An agent can replace its own working notes. For a shared item it can only propose a replacement, linked to the old one.
- There’s no delete tool.
Handoffs:
- In a project tab,
memory_handoffappends to the project’slog.md(the source of truth, which memory indexes). - Elsewhere it’s saved in the workstream’s scope.
Who’s calling: ws mcp runs in the agent’s process tree, so it inherits
the pane’s WS_* variables and directory. That gives it the repo,
workspace, workstream and project, and the right store (a named server’s
own). Items record the agent (from ws mcp’s parent process) and, when the
agent passes it, the model.
The memory panel
Section titled “The memory panel”prefix y opens memory inside ws: a list on the left, the selected item’s
full text and provenance on the right, and tabs across the top.
| Tab | Shows |
|---|---|
| Here | What an agent in the focused pane would be told: its workstream, project, workspace, repo and global items, pinned first, most specific scope first |
| Proposed | Everything waiting for your approval, from any scope (the tab shows the count) |
| Repo | The focused pane’s repo |
| Project | The focused tab’s project |
| Global | Global items |
| All | Everything active |
| Sessions | This repo’s session history: agent, model, prompts, commits, transcript path |
Keys:
aapproves a proposed item.xrejects a proposed item, or archives an active one.ppins or unpins.eedits the item in Neovim, in its own tab (ws mem edit).Dtwice deletes it for good./searches within the tab.
Every key is in Keys and mouse.
When anything is waiting for approval, the top bar shows ◇ N to approve;
clicking it opens the Proposed tab.
In the finder
Section titled “In the finder”The finder (prefix f) searches memory too. Once the query has three
letters, up to 8 matching items follow the workspaces, tabs, agents and
commands, each with its kind and scope (fact · global). It’s the same
full-text search as ws mem search, so a word in an item’s body finds it,
not just its title. Start with mem to search only memory (up to 50
items; mem alone lists the newest). Enter opens the item in the memory
panel’s All tab.
Continuing in another agent
Section titled “Continuing in another agent”To move work from one agent to another (Claude to Codex, Codex to Grok, and
back), use prefix A on the pane, or ws handoff --to <agent> in it.
- ws finds the session that ran in the pane (from session history).
- If the old agent is idle or done, ws asks it to record a handoff. It waits
for the agent’s turn to finish, up to one minute. If the agent is already
working or blocked, ws continues without interrupting it. Use
--no-askto skip the request. When the old agent cannot answer, ws still uses the session details and any handoff already in memory. - It opens a new pane beside it running the new agent. The agent starts with
a handoff pack: the usual context pack, headed by a “Continuing from”
section. That section holds the previous agent’s first and last prompt,
its commits, the uncommitted change, its handoff (one it wrote with
memory_handoff, ws’s own, or the newest project log entry), and its transcript path in case more detail is needed. - The new agent’s first prompt tells it to check the current state and carry on.
prefix A lists every agent twice: “Continue in codex” keeps the old pane,
and “codex, closing this pane” closes it once the new one is up. From the
shell:
ws handoff --to codexws handoff --to claude --model opus --replacews handoff --to grok --prompt "Review what the last agent did, then finish the tests"Going back to an agent
Section titled “Going back to an agent”ws remembers which session took over from which. When you switch to an agent you already used in this line of work (Claude, then Codex, then back to Claude), ws goes back to that agent’s earlier session instead of starting a new one, and tells it only what happened since its turn:
- If it’s still running in its pane, ws switches to that pane and types
the update into it (when the agent is idle or done; a busy agent isn’t
interrupted). With
--replace, the pane you left closes. - If its pane is gone, ws resumes the session in a new pane with the
agent’s own resume (
claude --resume <id>,codex resume <id>), with the update as its first message.
The update, headed “Since your last turn”, lists each agent that worked on it since (its prompts, handoff and transcript path), the commits made since you handed over, and what was added to memory since. It’s short because the resumed agent still has its own history.
ws only goes back to a session its CLI can resume: one with a session id
whose transcript is still on disk. Otherwise, and for agents ws doesn’t know
how to resume, it starts a new session with the handoff pack as above.
--fresh always starts a new session.
Adding other agents
Section titled “Adding other agents”Claude Code and Codex are built in. Add any other CLI, or change how a built-in
one starts, in ~/.config/ws/config.toml (Settings › Agents &
memory lists them and adds, edits and removes them):
[agents.grok]command = "grok"context = "first_message" # the pack is the start of its first messagemodel_flag = "--model {model}"
[agents.gemini]context = "flag:--system-prompt"context |
The agent gets the pack through |
|---|---|
append_system_prompt |
--append-system-prompt (Claude Code’s default) |
developer_instructions |
-c developer_instructions=… (Codex’s default) |
first_message |
the start of its first message (the default for other agents) |
flag:<flag> |
<flag> <pack>, for CLIs with a system-prompt option |
none |
nothing, just the prompt |
command is the program to run (default: the agent’s name), and
model_flag is how to pass --model (with {model}). The flags shown for
Grok and Gemini are examples: check your CLI’s own options.
Summaries (opt-in)
Section titled “Summaries (opt-in)”ws can summarize an agent’s session when it ends, using a command you choose.
That can be a cloud model (claude -p --model haiku) or a local one
(ollama run llama3.2). It keeps the summary, and proposes up to five facts,
decisions, preferences or tasks from it for your approval. It’s off until
you turn it on, and ws never picks a model itself. Settings ›
Agents & memory has its switch and the fields below.
[memory.summarize]enabled = true # automatic, at session endcommand = ["claude", "-p", "--model", "haiku"] # or ["ollama", "run", "llama3.2"]repos = ["github.com/acme/*"] # only these; empty = every repomax_input_tokens = 30000 # how much transcript to sendtimeout_secs = 180What ws sends to the command:
- Only the conversation: the user’s and the agent’s text from the session’s transcript. It reads Claude Code’s and Codex’s formats; tool calls, tool output and thinking are left out.
- Redacted, like everything else memory stores.
- Cut to
max_input_tokensfrom the end, where the state of the work is.
Long sessions are summarized in parts. When Claude Code is about to
compact its context (its PreCompact hook), ws summarizes what the session
did since the last summary, as “Session summary, part 1”, and so on. At the
end, the last part covers only what came after. Each part gets up to
max_input_tokens of its own stretch, so a long session’s early work is
summarized too, not cut off by the limit, and nothing is summarized twice.
A session that never compacts gets one summary, as before. (If the ws server
restarts mid-session, the end-of-session summary starts from the beginning
again, cut to the limit.)
The command gets a prompt on stdin asking for JSON (a summary and up to five items) and answers on stdout.
What ws keeps:
- The summary, as a
summaryitem. It goes in the session’s workstream or project, where the next agent’s pack includes it. Otherwise it’s proposed for the repo. - The items, proposed in the repo, for you to approve in the memory
panel’s Proposed tab or with
ws mem approve.
To summarize a session now, even with automatic summaries off:
ws mem summarize # the latest session in this panews mem summarize <session> # an id from ws mem sessions --jsonIt prints what it stored and how much transcript it sent. Automatic runs happen on a thread of their own and log to ws’s log.
Keeping it current
Section titled “Keeping it current”- New versions, not overwrites. Editing makes a new version, so what an agent was told last week can still be looked up.
- Tasks expire 14 days after they’re written unless updated, since
“where the work stands” goes stale. Expired tasks drop out of lists and
searches (
--allshows them) and show inws mem lint. Facts, decisions and preferences don’t expire. - Use counts: each time an item is given to an agent, its use count goes up, so lint and the panel can tell useful items from dead weight.
Privacy
Section titled “Privacy”-
Local only. The store is a file on your machine, readable only by you (mode
0600). ws never sends it anywhere; agents read it only when ws gives it to them, for the agent kinds you allow. -
Secrets are redacted before anything is stored:
- GitHub, OpenAI and Anthropic, AWS and Slack tokens
- JWTs and private-key blocks
NAME=valuelines whose name containsSECRET,TOKEN,PASSWORDorAPI_KEY- long random-looking strings
They’re stored as
[redacted:<kind>]. Git SHAs and paths are left alone. An item that is mostly secret is refused; store where to find the secret instead. -
Test servers get their own store. A named server (
ws -L dev) usesmemory-dev.db, andws memrun inside one of its panes, or with-L dev, uses that file too. So testing ws never touches your real memory.
Where it lives
Section titled “Where it lives”| Path | What |
|---|---|
~/.local/share/ws/memory.db |
The store (under $XDG_DATA_HOME when set) |
~/.local/share/ws/memory-<name>.db |
A named server’s store |
$WS_MEMORY_DB |
Overrides both |
It’s one SQLite file in WAL mode, so the server, the CLI and ws mcp
can all use it at once. Back it up like any other file; ws mem export gives
a portable copy.
See also
Section titled “See also”- Projects: briefs, plans and handoff logs, which are indexed into memory
- Configuration: paths and environment variables