# CTM Session Management Architecture

This document describes the current identity model for CTM sessions and provider
sessions. The goal is consistency: every code path should keep CTM tab identity,
provider-native session identity, and transcript file paths separate.

## Identity Rules

1. `ctm_sessions.id` is the CTM tab/session ID.
   - It is the frontend route ID, WebSocket session ID, active tab ID, and primary UI identity.
   - CTM owns title, cwd, provider, starred state, and user rename state here.

2. `agent_sessions.agent_session_id` is the provider-native session ID.
   - Claude, Codex, Gemini, and future agents own these IDs.
   - CTM uses this ID for provider resume commands and provider transcript lookup.
   - One CTM tab can have many agent session IDs.

3. `agent_sessions.ctm_session_id` is the authoritative CTM-to-agent mapping.
   - This is the only normal source of truth for mapping CTM IDs to agent IDs.
   - Queries should resolve a CTM ID through `agent_sessions`, then use the returned agent IDs.
   - Code should store these in distinct variables such as `ctmSessionId` and `agentSessionId`.

4. Transcript file path is not an agent session ID.
   - Claude usually stores the agent ID in the JSONL filename, but that is provider behavior, not CTM identity.
   - Codex rollout filenames can differ from the true thread ID; prefer `session_meta.payload.id` or provider state.
   - Store file locations in `agent_sessions.jsonl_path`.

5. `startup_tasks` is lifecycle state, not mapping state.
   - It tells CTM which live tabs to restore after a CTM restart: command, args, cwd, label, model, type, chat ID, branch/worktree.
   - New schemas must not create provider identity columns here.
   - Legacy `agent_session_id` / `claude_session_id` columns may exist for old databases and diagnostics only.
   - Normal restore, review, prompt navigation, search, and transcript reads must not treat those columns as ownership.

## Tables

| Table | Role |
| --- | --- |
| `ctm_sessions` | CTM tab metadata: ID, provider, title, cwd, starred, rename state |
| `agent_sessions` | Provider-native sessions linked to a CTM tab; 1 CTM ID to many agent IDs |
| `startup_tasks` | Live tab restore/lifecycle state |
| `session_conversations` | Imported transcript cache. Rows are keyed by transcript source ID, usually an agent ID |
| `session_messages` | Per-message cache/FTS rows keyed by transcript source ID |
| `scrollback_log` | Terminal scrollback chunks keyed by CTM tab ID |

## Restore Flow

On CTM restart:

1. Load rows from `startup_tasks` to know which tabs were active.
2. For each startup row, keep `ctmSessionId = startup_tasks.ctm_session_id`.
3. Resolve the latest resumable agent row from `agent_sessions` for that CTM ID.
4. Resume provider CLIs with the selected `agentSessionId`.
5. If no resumable agent row exists, keep the original startup args instead of inventing a mapping from `startup_tasks`.

`startup_tasks.args` is a materialized restore cache. It may be rewritten for
crash recovery, but only by first resolving the provider identity through
`agent_sessions`. Background jobs must not fill restore IDs directly from live
PTY/session fields such as `_claudeSessionId`; those fields are runtime evidence,
not durable ownership.

The compatibility helper named `updateStartupTaskAgentSession` is intentionally
misleading after the identity split. It may refresh the materialized resume args
cache, and it may keep old diagnostic project-dir columns tidy when they already
exist, but it must not write provider session IDs into `startup_tasks`.

Provider notes:

- Claude can have multiple agent IDs for one CTM tab because continuations, compaction, or resumed sessions can create additional provider sessions. Restore picks the latest valid `agent_sessions` row by `modified_at`, then `updated_at`, then `created_at`.
- Codex restore uses the mapped Codex thread ID. If a rollout filename disagrees with the metadata ID, metadata wins.
- Codex `state_5.sqlite` is a provider-owned resume locator. Before CTM spawns `codex resume <id>`, CTM verifies that Codex state points at the same JSONL path stored in `agent_sessions`. See [Codex Resume State Guard](./codex-resume-state-guard-design.md).
- Gemini resume is keyed by its provider session ID. CTM does not require a JSONL transcript size for Gemini.
- Wall-E chat sessions are first-class CTM sessions but are not resumed through a provider CLI resume command.

## Transcript Reads

Conversation/review code should follow this sequence:

1. Start from `ctmSessionId`.
2. Resolve linked `agentSessionIds` from `agent_sessions`.
3. Read `session_conversations` and `session_messages` by those agent IDs.
4. Only use the CTM ID as a legacy transcript-cache fallback, never as a replacement mapping source.
5. If cache is stale or absent, locate files through `agent_sessions.jsonl_path` or provider-specific lookup by agent ID.

This avoids the old bug where callers asked `session_conversations` directly by
CTM tab ID and missed rows imported under the provider-native agent ID.

## Code Contracts

- `db.getSessionIdentity(id)` resolves either a CTM ID or an agent ID through `ctm_sessions` and `agent_sessions`.
- `db.resolveSession(id)` returns only a real CTM session and its mapped agent rows. It intentionally does not synthesize sessions from `startup_tasks`.
- `db.getLatestAgentSessionForCtm(ctmSessionId, opts)` selects the latest mapped agent row for restore-like decisions.
- `db.getSessionConversationSourceIds(ctmSessionId, { includeCtmFallback })` returns the transcript source IDs to query.
- `lib/session-restore.js` owns restart resume selection and reads `agent_sessions`, not startup-task agent fields.

## Anti-Patterns

Do not:

- Use `startup_tasks.agent_session_id` or `startup_tasks.claude_session_id` as the normal CTM-to-agent mapping.
- Name variables `sessionId` when the value might be either a CTM ID or an agent ID. Split it into `ctmSessionId`, `agentSessionId`, or `conversationSourceId`.
- Assume JSONL filename equals provider session ID.
- Query `session_conversations` only by CTM ID when agent rows exist.
- Pick "nearby" provider sessions by cwd/time when a mapped agent ID exists.

## Recovery

Recovery tools may inspect `startup_tasks` to repair legacy/orphan state, but any
successful repair must materialize the result into `ctm_sessions` and
`agent_sessions`. After that, normal code should use the canonical tables only.

Safety-net jobs follow the same rule. If a startup row has missing or stale
resume args, the job must select the canonical row from `agent_sessions`, derive
provider-specific resume args from that row, and then update the startup cache.
If no canonical row exists, the job should report/skip rather than inventing a
provider mapping from an in-memory session object.

## Legacy Startup Identity Deprecation

Old databases can still contain `startup_tasks.agent_session_id`,
`startup_tasks.claude_session_id`, `startup_tasks.agent_project_dir`, and
`startup_tasks.claude_project_dir` from earlier CTM releases. The deprecation
rules are:

- Fresh databases do not create those provider identity columns.
- Migration may rename old columns so older rows remain readable for explicit
  diagnostics and repair.
- Runtime message cache, transcript file resolution, restore, and prompt-menu
  code must resolve provider IDs from `agent_sessions`, never from
  `startup_tasks`.
- Background safety-net jobs may canonicalize `startup_tasks.args` from
  `agent_sessions`; if the canonical row is absent, they skip rather than
  copying `_claudeSessionId`, Codex live state, or a nearby filename.
- Legacy identity columns should be considered stale hints. Do not add new
  feature behavior that depends on them.
