Conversation Search
Overview
Conversation search lets you (or an agent) find earlier chat turns by keyword. Every message in a chat session is persisted twice: as the durable JSONL session log on disk, and as a searchable row in a full-text index. The search runs over that index and returns the matching messages ranked by relevance, each with its source session id and timestamp. It answers the question “what did we discuss — or decide — before?” without you having to remember which session, or even which agent, it happened in. This is distinct from the Crew Journalrecall
surface, which searches the workspace-wide event memory (runs, peer
conversations, escalations). Conversation search reads the chat transcripts
themselves.
Scope and isolation
A search runs in one of two scopes, and both are decided by the server:- Agent scope — you name an agent. The query is filtered to that agent’s
agent_id, and the API verifies the agent belongs to your workspace first. This is the only scope an agent-initiated search can ever have: the agent identity comes from the runtime, so one agent can never read another’s chats through this surface. - Workspace scope — you name none. The API reads the agents in your workspace — from your session, never from the request body — and runs one ranked query across them. This is what ⌘K in the dashboard sends, and what the CLI does by default. It spans at most 400 agents (the most recently created ones).
- Search-from-now-on. Only conversations recorded after this feature shipped are indexed. There is no backfill of older history — the index fills as new turns land.
- Keyword (BM25) only. This first release ranks by BM25 full-text relevance. There is no semantic / embedding re-rank yet.
CLI
Usecrewship conversation search (alias: crewship conv search):
--format yaml and --format ndjson are honoured too, like every other read command —
see Output Formats.
Arguments and flags:
Each hit shows its timestamp, the message role (
user / assistant), the
agent that said it, the source session_id, and a snippet of the matched
content.
The agent-first positional form this command shipped with
(crewship conversation search backend-bot "deploy pipeline") still works:
with no --agent and several arguments, the first is read as an agent.
Dashboard (⌘K)
The command palette carries a Conversations group. Unlike the entity groups, which are fetched once when the palette opens and filtered in the browser, this one asks the server as you type (debounced, and each keystroke cancels the request before it), because messages are the largest thing in a workspace and a match is a phrase rather than a name. A row shows the matched snippet, the agent that said it and how long ago; selecting it opens that thread at/chat/<agent-slug>?session=<chat-id> — the
same deep link a chat notification uses.
Two keystrokes are the minimum before anything is sent. If the endpoint is
not configured on the deployment, or the search fails, the group simply does
not appear: nobody explicitly ran this search, so it never raises an error.
API
POST /api/v1/conversations/search
Request body:
Response:
Agent tool
The tool’s schema and handler ship, ready for a transport that can reach the mirror from a sandbox. It is agent-scoped by construction — the agent identity comes from the runtime, never from the model, so an agent could only ever search its own history — and where the mirror is not reachable it returns a recoverable “not available” result rather than failing the run.How it works
When a chat turn is persisted, the conversation store writes the JSONL line (the durable source of truth) and, when a database mirror is configured, dual-writes a row into aconversation_messages table backed by an
SQLite FTS5 external-content index. Search joins the index, filters by the
agent set the API resolved, and orders by BM25. The JSONL files remain authoritative; the
index is a rebuildable mirror, so a transient write failure costs at most the
searchability of a single turn, never the turn itself.