File size: 11,786 Bytes
064bfd6 2c4e38d 064bfd6 2c4e38d 064bfd6 2c4e38d 064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | 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
goal?: GoalEntry // /goal state for session resume
}
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
}
/** /goal state persisted to the transcript for resume. Last-wins. */
export type GoalEntry = {
type: 'goal'
sessionId: UUID
id: string
objective: string
status:
| 'pursuing'
| 'paused'
| 'achieved'
| 'blocked'
| 'usage-limited'
| 'budget-limited'
startedAt: number
startCostUSD: number
startTokensUsed: number
continuationCount: number
lastReason?: string
lastUpdatedAt: number
}
/**
* 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
| GoalEntry
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()
})
}
|