331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
import type { UUID } from 'crypto'
|
|
import type { FileHistorySnapshot } from 'src/utils/fileHistory.js'
|
|
import type { ContentReplacementRecord } from 'src/utils/toolResultStorage.js'
|
|
import type { AgentId } from './ids.js'
|
|
import type { Message } from './message.js'
|
|
import type { QueueOperationMessage } from './messageQueueTypes.js'
|
|
|
|
export type SerializedMessage = Message & {
|
|
cwd: string
|
|
userType: string
|
|
entrypoint?: string // CLAUDE_CODE_ENTRYPOINT — distinguishes cli/sdk-ts/sdk-py/etc.
|
|
sessionId: string
|
|
timestamp: string
|
|
version: string
|
|
gitBranch?: string
|
|
slug?: string // Session slug for files like plans (used for resume)
|
|
}
|
|
|
|
export type LogOption = {
|
|
date: string
|
|
messages: SerializedMessage[]
|
|
fullPath?: string
|
|
value: number
|
|
created: Date
|
|
modified: Date
|
|
firstPrompt: string
|
|
messageCount: number
|
|
fileSize?: number // File size in bytes (for display)
|
|
isSidechain: boolean
|
|
isLite?: boolean // True for lite logs (messages not loaded)
|
|
sessionId?: string // Session ID for lite logs
|
|
teamName?: string // Team name if this is a spawned agent session
|
|
agentName?: string // Agent's custom name (from /rename or swarm)
|
|
agentColor?: string // Agent's color (from /rename or swarm)
|
|
agentSetting?: string // Agent definition used (from --agent flag or settings.agent)
|
|
isTeammate?: boolean // Whether this session was created by a swarm teammate
|
|
leafUuid?: UUID // If given, this uuid must appear in the DB
|
|
summary?: string // Optional conversation summary
|
|
customTitle?: string // Optional user-set custom title
|
|
tag?: string // Optional tag for the session (searchable in /resume)
|
|
fileHistorySnapshots?: FileHistorySnapshot[] // Optional file history snapshots
|
|
attributionSnapshots?: AttributionSnapshotMessage[] // Optional attribution snapshots
|
|
contextCollapseCommits?: ContextCollapseCommitEntry[] // Ordered — commit B may reference commit A's summary
|
|
contextCollapseSnapshot?: ContextCollapseSnapshotEntry // Last-wins — staged queue + spawn state
|
|
gitBranch?: string // Git branch at the end of the session
|
|
projectPath?: string // Original project directory path
|
|
prNumber?: number // GitHub PR number linked to this session
|
|
prUrl?: string // Full URL to the linked PR
|
|
prRepository?: string // Repository in "owner/repo" format
|
|
mode?: 'coordinator' | 'normal' // Session mode for coordinator/normal detection
|
|
worktreeSession?: PersistedWorktreeSession | null // Worktree state at session end (null = exited, undefined = never entered)
|
|
contentReplacements?: ContentReplacementRecord[] // Replacement decisions for resume reconstruction
|
|
}
|
|
|
|
export type SummaryMessage = {
|
|
type: 'summary'
|
|
leafUuid: UUID
|
|
summary: string
|
|
}
|
|
|
|
export type CustomTitleMessage = {
|
|
type: 'custom-title'
|
|
sessionId: UUID
|
|
customTitle: string
|
|
}
|
|
|
|
/**
|
|
* AI-generated session title. Distinct from CustomTitleMessage so that:
|
|
* - User renames (custom-title) always win over AI titles in read preference
|
|
* - reAppendSessionMetadata never re-appends AI titles (they're ephemeral/
|
|
* regeneratable; re-appending would clobber user renames on resume)
|
|
* - VS Code's onlyIfNoCustomTitle CAS check only matches user titles,
|
|
* allowing AI to overwrite its own previous AI title but not user titles
|
|
*/
|
|
export type AiTitleMessage = {
|
|
type: 'ai-title'
|
|
sessionId: UUID
|
|
aiTitle: string
|
|
}
|
|
|
|
export type LastPromptMessage = {
|
|
type: 'last-prompt'
|
|
sessionId: UUID
|
|
lastPrompt: string
|
|
}
|
|
|
|
/**
|
|
* Periodic fork-generated summary of what the agent is currently doing.
|
|
* Written every min(5 steps, 2min) by forking the main thread mid-turn so
|
|
* `claude ps` can show something more useful than the last user prompt
|
|
* (which is often "ok go" or "fix it").
|
|
*/
|
|
export type TaskSummaryMessage = {
|
|
type: 'task-summary'
|
|
sessionId: UUID
|
|
summary: string
|
|
timestamp: string
|
|
}
|
|
|
|
export type TagMessage = {
|
|
type: 'tag'
|
|
sessionId: UUID
|
|
tag: string
|
|
}
|
|
|
|
export type AgentNameMessage = {
|
|
type: 'agent-name'
|
|
sessionId: UUID
|
|
agentName: string
|
|
}
|
|
|
|
export type AgentColorMessage = {
|
|
type: 'agent-color'
|
|
sessionId: UUID
|
|
agentColor: string
|
|
}
|
|
|
|
export type AgentSettingMessage = {
|
|
type: 'agent-setting'
|
|
sessionId: UUID
|
|
agentSetting: string
|
|
}
|
|
|
|
/**
|
|
* PR link message stored in session transcript.
|
|
* Links a session to a GitHub pull request for tracking and navigation.
|
|
*/
|
|
export type PRLinkMessage = {
|
|
type: 'pr-link'
|
|
sessionId: UUID
|
|
prNumber: number
|
|
prUrl: string
|
|
prRepository: string // e.g., "owner/repo"
|
|
timestamp: string // ISO timestamp when linked
|
|
}
|
|
|
|
export type ModeEntry = {
|
|
type: 'mode'
|
|
sessionId: UUID
|
|
mode: 'coordinator' | 'normal'
|
|
}
|
|
|
|
/**
|
|
* Worktree session state persisted to the transcript for resume.
|
|
* Subset of WorktreeSession from utils/worktree.ts — excludes ephemeral
|
|
* fields (creationDurationMs, usedSparsePaths) that are only used for
|
|
* first-run analytics.
|
|
*/
|
|
export type PersistedWorktreeSession = {
|
|
originalCwd: string
|
|
worktreePath: string
|
|
worktreeName: string
|
|
worktreeBranch?: string
|
|
originalBranch?: string
|
|
originalHeadCommit?: string
|
|
sessionId: string
|
|
tmuxSessionName?: string
|
|
hookBased?: boolean
|
|
}
|
|
|
|
/**
|
|
* Records whether the session is currently inside a worktree created by
|
|
* EnterWorktree or --worktree. Last-wins: an enter writes the session,
|
|
* an exit writes null. On --resume, restored only if the worktreePath
|
|
* still exists on disk (the /exit dialog may have removed it).
|
|
*/
|
|
export type WorktreeStateEntry = {
|
|
type: 'worktree-state'
|
|
sessionId: UUID
|
|
worktreeSession: PersistedWorktreeSession | null
|
|
}
|
|
|
|
/**
|
|
* Records content blocks whose in-context representation was replaced with a
|
|
* smaller stub (the full content was persisted elsewhere). Replayed on resume
|
|
* for prompt cache stability. Written once per enforcement pass that replaces
|
|
* at least one block. When agentId is set, the record belongs to a subagent
|
|
* sidechain (AgentTool resume reads these); when absent, it's main-thread
|
|
* (/resume reads these).
|
|
*/
|
|
export type ContentReplacementEntry = {
|
|
type: 'content-replacement'
|
|
sessionId: UUID
|
|
agentId?: AgentId
|
|
replacements: ContentReplacementRecord[]
|
|
}
|
|
|
|
export type FileHistorySnapshotMessage = {
|
|
type: 'file-history-snapshot'
|
|
messageId: UUID
|
|
snapshot: FileHistorySnapshot
|
|
isSnapshotUpdate: boolean
|
|
}
|
|
|
|
/**
|
|
* Per-file attribution state tracking Claude's character contributions.
|
|
*/
|
|
export type FileAttributionState = {
|
|
contentHash: string // SHA-256 hash of file content
|
|
claudeContribution: number // Characters written by Claude
|
|
mtime: number // File modification time
|
|
}
|
|
|
|
/**
|
|
* Attribution snapshot message stored in session transcript.
|
|
* Tracks character-level contributions by Claude for commit attribution.
|
|
*/
|
|
export type AttributionSnapshotMessage = {
|
|
type: 'attribution-snapshot'
|
|
messageId: UUID
|
|
surface: string // Client surface (cli, ide, web, api)
|
|
fileStates: Record<string, FileAttributionState>
|
|
promptCount?: number // Total prompts in session
|
|
promptCountAtLastCommit?: number // Prompts at last commit
|
|
permissionPromptCount?: number // Total permission prompts shown
|
|
permissionPromptCountAtLastCommit?: number // Permission prompts at last commit
|
|
escapeCount?: number // Total ESC presses (cancelled permission prompts)
|
|
escapeCountAtLastCommit?: number // ESC presses at last commit
|
|
}
|
|
|
|
export type TranscriptMessage = SerializedMessage & {
|
|
parentUuid: UUID | null
|
|
logicalParentUuid?: UUID | null // Preserves logical parent when parentUuid is nullified for session breaks
|
|
isSidechain: boolean
|
|
gitBranch?: string
|
|
agentId?: string // Agent ID for sidechain transcripts to enable resuming agents
|
|
teamName?: string // Team name if this is a spawned agent session
|
|
agentName?: string // Agent's custom name (from /rename or swarm)
|
|
agentColor?: string // Agent's color (from /rename or swarm)
|
|
promptId?: string // Correlates with OTel prompt.id for user prompt messages
|
|
}
|
|
|
|
export type SpeculationAcceptMessage = {
|
|
type: 'speculation-accept'
|
|
timestamp: string
|
|
timeSavedMs: number
|
|
}
|
|
|
|
/**
|
|
* Persisted context-collapse commit. The archived messages themselves are
|
|
* NOT persisted — they're already in the transcript as ordinary user/
|
|
* assistant messages. We only persist enough to reconstruct the splice
|
|
* instruction (boundary uuids) and the summary placeholder (which is NOT
|
|
* in the transcript because it's never yielded to the REPL).
|
|
*
|
|
* On restore, the store reconstructs CommittedCollapse with archived=[];
|
|
* projectView lazily fills the archive the first time it finds the span.
|
|
*
|
|
* Discriminator is obfuscated to match the gate name. sessionStorage.ts
|
|
* isn't feature-gated (it's the generic transcript plumbing used by every
|
|
* entry type), so a descriptive string here would leak into external builds
|
|
* via the appendEntry dispatch / loadTranscriptFile parser even though
|
|
* nothing in an external build ever writes or reads this entry.
|
|
*/
|
|
export type ContextCollapseCommitEntry = {
|
|
type: 'marble-origami-commit'
|
|
sessionId: UUID
|
|
/** 16-digit collapse ID. Max across entries reseeds the ID counter. */
|
|
collapseId: string
|
|
/** The summary placeholder's uuid — registerSummary() needs it. */
|
|
summaryUuid: string
|
|
/** Full <collapsed id="...">text</collapsed> string for the placeholder. */
|
|
summaryContent: string
|
|
/** Plain summary text for ctx_inspect. */
|
|
summary: string
|
|
/** Span boundaries — projectView finds these in the resumed Message[]. */
|
|
firstArchivedUuid: string
|
|
lastArchivedUuid: string
|
|
}
|
|
|
|
/**
|
|
* Snapshot of the staged queue and spawn trigger state. Unlike commits
|
|
* (append-only, replay-all), snapshots are last-wins — only the most
|
|
* recent snapshot entry is applied on restore. Written after every
|
|
* ctx-agent spawn resolves (when staged contents may have changed).
|
|
*
|
|
* Staged boundaries are UUIDs (session-stable), not collapse IDs (which
|
|
* reset with the uuidToId bimap). Restoring a staged span issues fresh
|
|
* collapse IDs for those messages on the next decorate/display, but the
|
|
* span itself resolves correctly.
|
|
*/
|
|
export type ContextCollapseSnapshotEntry = {
|
|
type: 'marble-origami-snapshot'
|
|
sessionId: UUID
|
|
staged: Array<{
|
|
startUuid: string
|
|
endUuid: string
|
|
summary: string
|
|
risk: number
|
|
stagedAt: number
|
|
}>
|
|
/** Spawn trigger state — so the +interval clock picks up where it left off. */
|
|
armed: boolean
|
|
lastSpawnTokens: number
|
|
}
|
|
|
|
export type Entry =
|
|
| TranscriptMessage
|
|
| SummaryMessage
|
|
| CustomTitleMessage
|
|
| AiTitleMessage
|
|
| LastPromptMessage
|
|
| TaskSummaryMessage
|
|
| TagMessage
|
|
| AgentNameMessage
|
|
| AgentColorMessage
|
|
| AgentSettingMessage
|
|
| PRLinkMessage
|
|
| FileHistorySnapshotMessage
|
|
| AttributionSnapshotMessage
|
|
| QueueOperationMessage
|
|
| SpeculationAcceptMessage
|
|
| ModeEntry
|
|
| WorktreeStateEntry
|
|
| ContentReplacementEntry
|
|
| ContextCollapseCommitEntry
|
|
| ContextCollapseSnapshotEntry
|
|
|
|
export function sortLogs(logs: LogOption[]): LogOption[] {
|
|
return logs.sort((a, b) => {
|
|
// Sort by modified date (newest first)
|
|
const modifiedDiff = b.modified.getTime() - a.modified.getTime()
|
|
if (modifiedDiff !== 0) {
|
|
return modifiedDiff
|
|
}
|
|
|
|
// If modified dates are equal, sort by created date (newest first)
|
|
return b.created.getTime() - a.created.getTime()
|
|
})
|
|
}
|