Sessions
C3 keeps every Claude Code conversation as a searchable row: what it was, whether it's live, whether it's worth resuming. c3_session is how an agent marks a session stale, leaves a note, or lists past ones; the Hub's Sessions tab is where a person browses, resumes and marks them. A separate layer, durable c3_task tasks and milestones, links to the sessions that created or touched them.
Overview
Every Claude Code conversation is kept as a transcript (~/.claude/projects/<slug>/<uuid>.jsonl). C3 keeps its own session records, snapshots and tasks beside the project. Before 2.143.0 nothing joined the two: "which session was I doing X in, is it worth going back to, how do I get back" meant opening JSONL files by hand, and nothing could record that a session was a dead end.
Two different things are both called "session" in C3 — this page is about the first:
- The session catalog (this page): a historical row per Claude Code conversation — title, transcript excerpts, stale flag, resume command.
services/session_catalog.py. - Session liveness: whether the MCP server behind this conversation is running right now, and whether its per-project web UI is up. Heartbeat files, UI autostart and reaping — a different mechanism, covered by
docs/session-liveness.md, feeding theliveflag the catalog shows.
There's a third, unrelated thing with "sessions" in its name: c3_status(view='sessions') reads .c3/session_stats.jsonl and prints token/cost totals per host session — a budget report, not the catalog or the liveness heartbeat.
One row per conversation
The row key is the Claude Code session id — the transcript's file name and the value claude --resume takes. session_catalog.py joins everything that belongs to it:
| Field | From |
|---|---|
title | The transcript's custom title, else its newest AI-generated title, else the first prompt, else the C3 session description |
first_prompt, last_prompt | Transcript head and its last-prompt row. System reminders, meta rows and tool results are stripped; a slash command reads as /plan <args> |
started, last_active, branch | Transcript timestamps and gitBranch |
live | A fresh .c3/live/ heartbeat — see Liveness |
note | An agent note (c3_session action='note'), else the newest snapshot a person or agent wrote. Machine labels like "auto-snapshot on stop" are skipped |
links.c3_sessions | Every C3 session this conversation ran — an MCP restart inside one conversation starts a new C3 session, so there can be several |
links.tasks | Tasks created during the session (origin_session) or linked to it (c3_task link_type='session') |
links.decisions, links.snapshots | Counted across all linked C3 sessions |
links.predecessor / successor | A /clear chain (session_end reason=clear then session_open start_source=clear within 2 minutes), or the successor named when a session was marked stale |
resume | command, cwd, remote_url, can_launch, why_not |
A transcript is read from its head (64KB, widened to 512KB if a pasted prompt pushes the first one out) and its tail (256KB, then 1MB, then 4MB until a title turns up) — the whole file is never parsed. Results are cached per file in .c3/cache/session_catalog.json, keyed on size and mtime. The newest 200 transcripts per project are listed.
The transcript folder must be the exact Claude Code slug of the project path (or an exact alphanumeric match) — it never falls back to "a folder whose name contains the project name", because a wrong folder would resume another project's session. A transcript whose recorded cwd is a different directory is skipped. A session run from a worktree has its own slug and is still found through its C3 record, resuming in the worktree. CLAUDE_CONFIG_DIR is honored, as Claude Code itself honors it.
c3_session actions
c3_session(action=..., data=..., reasoning=..., description=..., summary=..., event_type=..., target=...). Two families: session-lifecycle actions (unchanged since before 2.143.0) and the catalog actions list / stale / unstale / note, added in 2.143.0 and taking the new target parameter.
Lifecycle
| Action | Plan-mode safe? | Does |
|---|---|---|
start | — | Starts a new C3 session (called automatically on MCP server start) |
save | — | Flushes auto-memory, persists the budget, saves the session (summary optional) |
plan | — | Records an ephemeral plan/approach (data + reasoning). Durable TODOs belong in c3_task, not here |
log | yes | Records a decision or file-change note. event_type='auto' (default) infers file_change from a leading path-like token, else decision |
snapshot | yes | Full session capture before /clear — data is the task, reasoning the next steps, summary an optional comma-separated file list to embed structural maps for |
restore | — | Restores the snapshot named in data (or latest), resets the token budget to what it reports |
compact | — | One-step snapshot + budget reset — a checkpoint-and-restart shortcut |
convo_log | — | Appends one turn to the conversation store |
Session catalog (2.143.0)
| Action | Parameters | Does |
|---|---|---|
list |
target = stale | likely | all (default: unmarked only); data = optional search text |
Titles and flags only — never another session's prompts |
stale |
target = a session id, an 8+ char prefix, or current (required); reasoning required; data = optional successor id |
Marks a session superseded, finished or abandoned — don't resume this |
unstale |
target required |
Clears the stale flag |
note |
target (defaults to current); data = what was done; reasoning = next steps |
Leaves a note on the session card — do this before stopping, so the next session (or person) sees where things stand |
# Mark this branch's earlier session as superseded
c3_session(action='stale', target='a1b2c3d4',
reasoning='superseded by the 2.143 branch', data='<successor id>')
# Undo it
c3_session(action='unstale', target='a1b2c3d4')
# Leave a note before stopping
c3_session(action='note', data='Hub tab done', reasoning='Desk segment next')
# List sessions likely worth marking
c3_session(action='list', target='likely')
Stale, marks & hints
Stale means superseded, finished, or abandoned: don't resume this. Only an agent or a person sets it, and always with a reason. A successor session can be named as well. Marks append to .c3/session_marks.jsonl ({ts, provider, id, op, reason, successor, summary, next_steps, by: agent|user, by_session}) and fold last-wins — several MCP processes and the Hub can write at once, and an append never loses a concurrent row the way rewriting one JSON file would. The file is also the audit trail; every mark writes a session_mark activity row too.
The managed instructions ask the agent to leave a note before it stops, and to mark a session stale when its own work supersedes or abandons that session.
Hints — heuristics, never a hide
Hints are grey chips that suggest a look. They never hide a row, and a live session gets none.
idle Nd ended by /clear branch gone short
| Hint | When |
|---|---|
idle Nd | No activity for sessions.idle_days (default 14, .c3/config.json) |
ended by /clear | The session ended with /clear |
branch gone | Its git branch no longer exists locally |
short | The transcript is under 16KB |
The Hub's Likely stale filter shows unmarked rows that have at least one hint.
Liveness v2.128.0
Before 2.128.0 the hub's "live" light was inferred from activity-log rows, and it lied three different ways: a closed IDE read live for up to 20 minutes (a real session_end row was written but nothing read it), a quiet-but-open session read dead after 20 minutes of tool-call silence even with its MCP process right there, and a busy session sometimes read dead too — the lookup scanned only the log's last 100 lines, and this repo's own running session's session_start once sat 319 lines from the end of a 20,825-line log.
The fix is a fact, not an inference: the MCP server writes one heartbeat file per session, .c3/live/<c3_session_id>.json = {session_id, host_session_id, ide, pid, ts}, refreshed every 60s (HEARTBEAT_INTERVAL_S). A reader treats a file whose ts is within 180s (HEARTBEAT_TTL_S, three missed beats) as live. Clean shutdown removes the file before slower teardown steps run; a crash or a taskkill leaves a stale file, which the next reader ignores and prunes. pid is recorded for diagnostics only — liveness is decided by the timestamp, since pids are reused across reboots and there's no cheap portable "is this pid alive" check.
One heartbeat file per session, not per project, so two IDEs on one project, or a repo and its worktree, each report themselves; the hub card shows MCP×N. Where there's no heartbeat (an older C3 in another checkout, or a process killed without cleanup), the old activity-log inference is still the fallback, now also honoring a terminal session_end row.
Checking it by hand
ls .c3/live # one file per live session in this project
python -c "from services import session_live as s; print(s.live_sessions('.'))"
The hooks and the MCP server load their code at process start, so a change here is not live in the session that made it — a new IDE session is required, and the Hub must be restarted.
Hub Sessions tab
The top bar's Sessions view lists past sessions across every registered project (or one, via the left rail), one row each, with a resume button. It never returns another session's live prompts unless you expand that specific row.
Filters
| Filter | Shows |
|---|---|
| Active (default) | Everything not marked stale |
| Likely stale | Not marked, but idle for weeks, ended by /clear, branch deleted, or very short |
| Stale | Marked stale by an agent or by you |
| All | Everything |
Each row shows a live dot, the title, a LIVE pill when applicable, the branch, the project (in the all-projects view), the hint chips or the stale reason, and counts of linked tasks/decisions/a snapshot. Expanding a row loads its detail: first/last prompt, the agent note or last snapshot, linked tasks and recent decisions, the predecessor/successor chain, and a preview of the last 8 turns.
Row actions
| Action | What happens |
|---|---|
| Resume | Opens a terminal in the session's directory running claude --resume <id> (Windows Terminal, falling back to a console). Disabled with a reason when the session can't be launched — most commonly because it's already live. |
| Copy | Puts the exact claude --resume command on the clipboard, for running it yourself. |
| Remote | Opens the claude.ai/code page for a bridged session — only shown when a remote_url exists. |
| Mark stale / Unmark | Mark opens a dialog for an optional reason; Unmark clears the flag directly. |
Resume safety. The route resolves path against the project registry first: an unregistered folder is 404, an uninitialized one 409. The id must be a UUID whose transcript exists for that project, and the argv is fixed (["claude", "--resume", id]); nothing from the request body reaches the command line. A live session is refused with 409, since two processes appending to one transcript would corrupt it — continue it where it's already open, or use the remote link. Codex and Gemini sessions are listed but not resumable yet.
Routes (Hub, loopback + CSRF guard)
| Route | What |
|---|---|
GET /api/hub/sessions/overview | Per project: counts {total, live, stale, idle}, last_active, newest, and features for capability probing. Stats files only — fast. |
GET /api/hub/sessions?path=&stale=hide|likely|only|all&q=&limit=&before= | Rows for one project, or all projects when path is empty. Built in parallel, newest first. |
GET /api/hub/sessions/detail?path=&id= | The row plus decisions, snapshot, tasks, and a preview of the last 8 turns (each run of tool calls collapses to one ⚙ line). |
POST /api/hub/sessions/mark {path, id, op, reason?, successor?, summary?, next_steps?} | Recorded with by: user. |
POST /api/hub/sessions/resume {path, id} | 200 {launched, command, cwd}, or 404 / 409 / 500 with the command to run by hand. |
These routes are deliberately not on the Oracle gateway. Oracle can bind to the tailnet, and transcript previews (someone else's prompts) should stay on this machine. The first all-projects listing on a machine reads every transcript head and tail once; around 1,000 transcripts took about 20 seconds on the box this was measured on. Later loads are served from the per-file cache.
c3_task & sessions
Sessions are conversation history; c3_task is durable project-management state that survives every conversation — tasks (backlog / in_progress / blocked / done, priority p0–p3, due dates, tags), milestones, and a decision-note log, stored in .c3/pm/pm.json. A session links to the tasks it created or touched, which is what links.tasks in the catalog and the Tasks section of a session's Hub detail view come from.
| Action group | Actions |
|---|---|
| Tasks | add (title + description/priority/due_date/tags/milestone), update, done, list, get, board, archive |
| Links | link / unlink — task_id + link_type (file | commit | edit | session) + ref; ref='current' links this conversation |
| Dependencies | block / unblock — task_id + ref (the blocking task); cycle-safe; completing the last open blocker auto-releases dependents to backlog |
| Subtasks | One level via parent on add/update (parent='none' clears it) |
| Milestones | milestone_add / _update / _list / _archive, plus milestone_complete v2.86.0 / milestone_reopen |
| Time | time_add (minutes 1–1440 + note/due_date/task_id) / time_update / time_delete (by entry id) / time_list / time_summary (today/7d/30d, auto vs. manual) |
| Notes | note_add (kind='decision' for the decision log) / note_list |
| Reports | report: overdue, blocked chains + aging, ready-to-unblock, milestone health/at-risk, throughput. history — append-only event log, newest first |
Milestones can be completed, not just archived v2.86.0
Archiving a milestone was the only way to close one, and archiving detaches every task from it — the record of what shipped under which milestone survived only in the event log. milestone_complete flips the lifecycle to completed (stamping completed_at): the milestone leaves default listings and the board, but its tasks keep their link. It refuses while open tasks remain. milestone_reopen undoes it.
# Link a task to this conversation
c3_task(action='link', task_id='a1b2c3d4', link_type='session', ref='current')
# Close out a shipped milestone, keeping the history
c3_task(action='milestone_complete', milestone='v2.145')
# What's blocking, and what's ready to unblock
c3_task(action='report')
Task ids accept any unique prefix (≥4 chars); milestones resolve by id or unique name. Read actions (list, get, board, report, history, the time_* reads) are plan-mode safe. Ephemeral session plans stay in c3_session(action='plan') — they don't belong in the durable task store.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| A session I know is running shows as not live | Check .c3/live/ for a fresh file. A heartbeat is written right after session_start and every 60s after; if the file is missing entirely, the running C3 predates 2.128.0 or crashed without cleanup — restart the IDE session. |
| Resume returns 409 | The session is already live — two processes cannot safely append to one transcript. Continue it where it's open, or use the Remote link if it was bridged. |
| Resume returns 404 | Either the project isn't registered with the Hub, or the session id has no matching transcript for that project. |
| A session doesn't show up at all | Its transcript's recorded cwd doesn't match the project path exactly, or it's outside the newest 200 transcripts for that project. Worktree sessions resolve through their own Claude Code slug but still show under the project via their C3 record. |
| Marking stale does nothing / errors | stale requires reasoning and a resolvable target (an id, an 8+ char prefix, or current if this conversation's C3 session id is already known). |
| The Hub's first Sessions load is slow | Expected on a machine with many transcripts — the first all-projects read parses every transcript's head and tail once (~20s per ~1,000 transcripts). Subsequent loads use the per-file cache in .c3/cache/session_catalog.json. |
| A change to session/liveness code doesn't take effect | Hooks and the MCP server load code at process start. Start a new IDE session and restart the Hub. |
Full design docs: docs/sessions.md, docs/session-liveness.md.