v2.145.1

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.

v2.145.0 services/session_catalog.py since 2.143.0

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:

  1. The session catalog (this page): a historical row per Claude Code conversation — title, transcript excerpts, stale flag, resume command. services/session_catalog.py.
  2. 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 the live flag 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:

FieldFrom
titleThe transcript's custom title, else its newest AI-generated title, else the first prompt, else the C3 session description
first_prompt, last_promptTranscript 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, branchTranscript timestamps and gitBranch
liveA fresh .c3/live/ heartbeat — see Liveness
noteAn 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_sessionsEvery C3 session this conversation ran — an MCP restart inside one conversation starts a new C3 session, so there can be several
links.tasksTasks created during the session (origin_session) or linked to it (c3_task link_type='session')
links.decisions, links.snapshotsCounted across all linked C3 sessions
links.predecessor / successorA /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
resumecommand, 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

ActionPlan-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
logyesRecords a decision or file-change note. event_type='auto' (default) infers file_change from a leading path-like token, else decision
snapshotyesFull 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)

ActionParametersDoes
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

HintWhen
idle NdNo activity for sessions.idle_days (default 14, .c3/config.json)
ended by /clearThe session ended with /clear
branch goneIts git branch no longer exists locally
shortThe 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

FilterShows
Active (default)Everything not marked stale
Likely staleNot marked, but idle for weeks, ended by /clear, branch deleted, or very short
StaleMarked stale by an agent or by you
AllEverything

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

ActionWhat 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)

RouteWhat
GET /api/hub/sessions/overviewPer 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 groupActions
Tasksadd (title + description/priority/due_date/tags/milestone), update, done, list, get, board, archive
Linkslink / unlink — task_id + link_type (file | commit | edit | session) + ref; ref='current' links this conversation
Dependenciesblock / unblock — task_id + ref (the blocking task); cycle-safe; completing the last open blocker auto-releases dependents to backlog
SubtasksOne level via parent on add/update (parent='none' clears it)
Milestonesmilestone_add / _update / _list / _archive, plus milestone_complete v2.86.0 / milestone_reopen
Timetime_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)
Notesnote_add (kind='decision' for the decision log) / note_list
Reportsreport: 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

SymptomCause & 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.