Skip to content

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 mem commands
  • 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

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.

Terminal window
ws mem add "Run bin/setup_db before specs" --kind fact --link file:bin/setup_db
ws mem add --kind preference --scope global "Small PRs, one feature each"
ws mem search setup db
ws mem list --status proposed
ws mem show h8pyg4
ws 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 is repo inside a git repo, else global. --propose stores it as proposed; --pin means 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 (running finds run), and the last word matches as a prefix.
    • Every word must match. Punctuation is just text, not search syntax.
  • list: newest first, pinned on top, active only. Use --status or --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 superseded and stays in the history.
    • --in-place just fixes the text (typos).
  • approve, reject, archive, pin, unpin.

  • forget: deletes for good, by id, or by --scope, --since YYYY-MM-DD or --session <id>. It asks first (--yes skips the question). Deleted text is overwritten on disk (secure_delete).

  • export: JSON lines with provenance. import brings them back as proposed, 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.

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 log from 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.

Terminal window
ws mem sessions # this repo's sessions, newest first
ws mem sessions --all # every repo
ws mem sessions --project button-a11y --json

ws’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).

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.

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:

  1. Pinned items
  2. Preferences
  3. Decisions and facts (notes too)
  4. Current tasks (up to 5)
  5. Latest handoffs (the last 3, from any agent)
  6. 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:

Terminal window
ws context pack
ws context pack --budget 500
ws context pack --json

In Settings › Agents & memory (prefix S), or in ~/.config/ws/config.toml:

[memory]
enabled = true # false: record nothing, give nothing
deliver_to = ["claude", "codex"] # agent kinds that get packs
budget = 1500 # tokens
[memory.repos."github.com/acme/secret-client"]
capture = false # record nothing from this repo, not even session rows
deliver = false # never put this repo's items or sessions in a pack

Repos are keyed by origin, as in scopes (github.com/org/name, lowercase).

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:

Terminal window
ws integrate claude --mcp
ws integrate codex --mcp

These 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 proposed until you ws mem approve it, 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_handoff appends to the project’s log.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.

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:

  • a approves a proposed item.
  • x rejects a proposed item, or archives an active one.
  • p pins or unpins.
  • e edits the item in Neovim, in its own tab (ws mem edit).
  • D twice 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.

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.

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.

  1. ws finds the session that ran in the pane (from session history).
  2. 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-ask to skip the request. When the old agent cannot answer, ws still uses the session details and any handoff already in memory.
  3. 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.
  4. 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:

Terminal window
ws handoff --to codex
ws handoff --to claude --model opus --replace
ws handoff --to grok --prompt "Review what the last agent did, then finish the tests"

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.

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 message
model_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.

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 end
command = ["claude", "-p", "--model", "haiku"] # or ["ollama", "run", "llama3.2"]
repos = ["github.com/acme/*"] # only these; empty = every repo
max_input_tokens = 30000 # how much transcript to send
timeout_secs = 180

What 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_tokens from 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 summary item. 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:

Terminal window
ws mem summarize # the latest session in this pane
ws mem summarize <session> # an id from ws mem sessions --json

It 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.

  • 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 (--all shows them) and show in ws 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.
  • 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=value lines whose name contains SECRET, TOKEN, PASSWORD or API_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) uses memory-dev.db, and ws mem run inside one of its panes, or with -L dev, uses that file too. So testing ws never touches your real memory.

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.

  • Projects: briefs, plans and handoff logs, which are indexed into memory
  • Configuration: paths and environment variables