-
Notifications
You must be signed in to change notification settings - Fork 1
Conversation History
Permanent persistence of every Claude console turn — introduced in v0.16.0
on top of the v0.14.0 PostgreSQL transition. View at
/projects/{id}/history (per-project) or /chat/history (General Chat).
| Role | Source | Notes |
|---|---|---|
user |
ClaudeSessionManager.sendPrompt(...) |
The text the user sent. |
assistant |
ClaudeStreamParser → AssistantMessage(isPartial=false)
|
Streaming partials are skipped — only the finalized turn text is saved. |
tool_use |
ClaudeStreamParser → ToolUse
|
content = stream-json input (JSON). toolName + toolUseId recorded for matching. |
tool_result |
ClaudeStreamParser → ToolResult (ok) |
content = stream-json output. |
tool_result_error |
ClaudeStreamParser → ToolResult(isError=true)
|
Same shape, separate role for filtering. |
system |
ClaudeEvent.SessionStarted / Done / emitSystem(...)
|
content = small JSON envelope {kind,...}. |
error |
ClaudeStreamParser → ErrorEvent
|
content = {code,message} JSON. |
unknown |
ClaudeStreamParser → Unknown
|
Raw stream-json line preserved in content + raw. |
Persistence is fire-and-forget — a DB write failure logs a warning but never breaks the live console stream.
CREATE TABLE conversation_turns (
id VARCHAR(64) PRIMARY KEY,
project_id VARCHAR(64) NOT NULL,
session_id VARCHAR(64), -- nullable (first user prompt arrives before system/init)
turn_idx INTEGER NOT NULL, -- monotonic within (project, session)
ts VARCHAR(64) NOT NULL, -- ISO-8601 UTC
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL, -- text or JSON-encoded payload
tool_name VARCHAR(64),
tool_use_id VARCHAR(128), -- match tool_use ↔ tool_result
tokens_in INTEGER,
tokens_out INTEGER,
raw TEXT,
agent_name VARCHAR(64), -- v0.49.0+ — null = main console; non-null = sub-agent
INDEX (project_id, ts),
INDEX (project_id, session_id, turn_idx),
INDEX (tool_use_id),
INDEX (project_id, agent_name, ts) -- v0.49.0+
);PG TEXT is unlimited (up to 1 GB per cell). Even verbose Read outputs of
hundreds of KB fit without trouble.
When Phase 23 (v0.44.0) introduced the sub-agent process pool,
each (projectId, agentName) got its own Claude child process — but
the turns lived only in the in-memory LogHub ring. v0.49.0 adds the
agent_name column so SubAgentSessionManager persists turns
alongside the main console:
-
agent_name IS NULL— main project console (default for every row inserted before v0.49.0). -
agent_name = '<name>'— sub-agent turn. The(projectId, agentName)pair is the natural conversation scope. The samesession_idper agent (separate from the main console's) keepsturn_idxmonotonic per agent.
History pages currently show both main + sub-agent turns mixed
together — a agentName= filter UI is on the roadmap.
/projects/{id}/history and /chat/history expose 6 filter knobs as GET
query parameters. Combine freely:
| Param | Example | Notes |
|---|---|---|
session |
f1d4e0c2-… |
dropdown populated from distinct sessionIds for the project |
role |
tool_use |
one of: user / assistant / tool_use / tool_result / tool_result_error / system / error / unknown |
tool |
Bash |
exact match on tool_name
|
from |
2026-05-24T00:00:00Z |
ISO ts ≥ |
to |
2026-05-25T00:00:00Z |
ISO ts ≤ |
q |
assembleDebug |
content LIKE %q% — see "Search" below |
Pagination: 100 rows per page, ?p=<index> (0-based), oldest-first within the page.
/projects/my-app/history?role=tool_use&tool=Bash
# what shell commands did Claude run?
/projects/my-app/history?role=assistant
# all of Claude's textual replies
/projects/my-app/history?q=NullPointerException
# turns mentioning a specific crash
/chat/history?role=user
# everything I've asked General Chat
/projects/my-app/history?from=2026-05-24T00:00:00Z&to=2026-05-24T23:59:59Z
# one calendar day
Content search uses LIKE '%query%' on the content column — fine for small
projects but linear scan as the table grows. Patterns are escaped (%, _,
\ are quoted), so users can't inject wildcards or break the query.
Planned for the next minor: tsvector GIN index on content (PostgreSQL
full-text search) for sub-second matches over millions of rows.
docker exec -it vibe-coder-postgres psql -U vibecoder -d vibecoder
vibecoder=> \d conversation_turns
vibecoder=> SELECT role, count(*) FROM conversation_turns
WHERE project_id = 'my-app' GROUP BY 1 ORDER BY 2 DESC;
vibecoder=> SELECT ts, role, left(content, 80)
FROM conversation_turns
WHERE project_id = 'my-app' AND tool_name = 'Edit'
ORDER BY ts DESC LIMIT 20;Not automatic in v0.16.0. The table grows by (turns per session) × (sessions).
Estimate: a heavy 8-hour day of one project might log ~500 turns → ~5 MB.
A whole year ~2 GB — well within the comfortable range for the bind-mounted
vibe-coder-data/postgres/ directory.
When you do want to prune:
DELETE FROM conversation_turns WHERE ts < (now() - interval '180 days')::text;
VACUUM ANALYZE conversation_turns;Or via a per-project cleanup:
DELETE FROM conversation_turns
WHERE project_id = 'old-experiment-app';ProjectService.delete cascades to this table automatically when a project
is removed via UI.
- No request bodies are recorded — only the turn payloads (which the user already sees in the console).
- Secrets (PATs, passwords) that a user pastes into a prompt will be saved as-is — be mindful when chatting about credentials. Same caveat as Slack/Discord/etc. The whole content is the user's own data.
-
No PII obfuscation — the database belongs to the operator; they can
PG
DELETEorUPDATErows directly.
SQLite is single-writer. With both the live console stream and full-text search over years of history hitting the same DB, lock contention would make the console feel laggy. PG's row-level locking + multi-connection pool makes both ops trivial.
JSONB columns and tsvector + GIN (future) also need PG — SQLite's JSON1
and FTS5 are usable but the future path is more straightforward on PG.
See the Upgrade Guide v0.13 → v0.14 section for the migration story.
The original v0.16.0 page provides per-project filtering. Three follow-ups extend this:
-
v0.30.0 — Cross-project search.
/history(no project id) walks every project'sconversation_turnsand highlights matches. See Conversation Search & Archive. -
v0.31.0 — Export / import.
GET /projects/{id}/history/export+POST .../history/importfor hand-offs between hosts. JSON envelope (schemaVersion: 1), sessionId-level idempotency. -
v0.33.0 — Auto-archive.
ConversationArchiverdumps sessions older than 30 days to<workspace>/.vibecoder/<projectId>/archive/session-<sid>.jsonand deletes the rows. Idempotent re-run safe.
The archive JSON uses the same envelope as manual export, so
restoring an old session is just 📤 가져오기 on the same page.