/ types / logs.ts
logs.ts
  1  import type { UUID } from 'crypto'
  2  import type { FileHistorySnapshot } from 'src/utils/fileHistory.js'
  3  import type { ContentReplacementRecord } from 'src/utils/toolResultStorage.js'
  4  import type { AgentId } from './ids.js'
  5  import type { Message } from './message.js'
  6  import type { QueueOperationMessage } from './messageQueueTypes.js'
  7  
  8  export type SerializedMessage = Message & {
  9    cwd: string
 10    userType: string
 11    entrypoint?: string // CLAUDE_CODE_ENTRYPOINT — distinguishes cli/sdk-ts/sdk-py/etc.
 12    sessionId: string
 13    timestamp: string
 14    version: string
 15    gitBranch?: string
 16    slug?: string // Session slug for files like plans (used for resume)
 17  }
 18  
 19  export type LogOption = {
 20    date: string
 21    messages: SerializedMessage[]
 22    fullPath?: string
 23    value: number
 24    created: Date
 25    modified: Date
 26    firstPrompt: string
 27    messageCount: number
 28    fileSize?: number // File size in bytes (for display)
 29    isSidechain: boolean
 30    isLite?: boolean // True for lite logs (messages not loaded)
 31    sessionId?: string // Session ID for lite logs
 32    teamName?: string // Team name if this is a spawned agent session
 33    agentName?: string // Agent's custom name (from /rename or swarm)
 34    agentColor?: string // Agent's color (from /rename or swarm)
 35    agentSetting?: string // Agent definition used (from --agent flag or settings.agent)
 36    isTeammate?: boolean // Whether this session was created by a swarm teammate
 37    leafUuid?: UUID // If given, this uuid must appear in the DB
 38    summary?: string // Optional conversation summary
 39    customTitle?: string // Optional user-set custom title
 40    tag?: string // Optional tag for the session (searchable in /resume)
 41    fileHistorySnapshots?: FileHistorySnapshot[] // Optional file history snapshots
 42    attributionSnapshots?: AttributionSnapshotMessage[] // Optional attribution snapshots
 43    contextCollapseCommits?: ContextCollapseCommitEntry[] // Ordered — commit B may reference commit A's summary
 44    contextCollapseSnapshot?: ContextCollapseSnapshotEntry // Last-wins — staged queue + spawn state
 45    gitBranch?: string // Git branch at the end of the session
 46    projectPath?: string // Original project directory path
 47    prNumber?: number // GitHub PR number linked to this session
 48    prUrl?: string // Full URL to the linked PR
 49    prRepository?: string // Repository in "owner/repo" format
 50    mode?: 'coordinator' | 'normal' // Session mode for coordinator/normal detection
 51    worktreeSession?: PersistedWorktreeSession | null // Worktree state at session end (null = exited, undefined = never entered)
 52    contentReplacements?: ContentReplacementRecord[] // Replacement decisions for resume reconstruction
 53  }
 54  
 55  export type SummaryMessage = {
 56    type: 'summary'
 57    leafUuid: UUID
 58    summary: string
 59  }
 60  
 61  export type CustomTitleMessage = {
 62    type: 'custom-title'
 63    sessionId: UUID
 64    customTitle: string
 65  }
 66  
 67  /**
 68   * AI-generated session title. Distinct from CustomTitleMessage so that:
 69   * - User renames (custom-title) always win over AI titles in read preference
 70   * - reAppendSessionMetadata never re-appends AI titles (they're ephemeral/
 71   *   regeneratable; re-appending would clobber user renames on resume)
 72   * - VS Code's onlyIfNoCustomTitle CAS check only matches user titles,
 73   *   allowing AI to overwrite its own previous AI title but not user titles
 74   */
 75  export type AiTitleMessage = {
 76    type: 'ai-title'
 77    sessionId: UUID
 78    aiTitle: string
 79  }
 80  
 81  export type LastPromptMessage = {
 82    type: 'last-prompt'
 83    sessionId: UUID
 84    lastPrompt: string
 85  }
 86  
 87  /**
 88   * Periodic fork-generated summary of what the agent is currently doing.
 89   * Written every min(5 steps, 2min) by forking the main thread mid-turn so
 90   * `claude ps` can show something more useful than the last user prompt
 91   * (which is often "ok go" or "fix it").
 92   */
 93  export type TaskSummaryMessage = {
 94    type: 'task-summary'
 95    sessionId: UUID
 96    summary: string
 97    timestamp: string
 98  }
 99  
100  export type TagMessage = {
101    type: 'tag'
102    sessionId: UUID
103    tag: string
104  }
105  
106  export type AgentNameMessage = {
107    type: 'agent-name'
108    sessionId: UUID
109    agentName: string
110  }
111  
112  export type AgentColorMessage = {
113    type: 'agent-color'
114    sessionId: UUID
115    agentColor: string
116  }
117  
118  export type AgentSettingMessage = {
119    type: 'agent-setting'
120    sessionId: UUID
121    agentSetting: string
122  }
123  
124  /**
125   * PR link message stored in session transcript.
126   * Links a session to a GitHub pull request for tracking and navigation.
127   */
128  export type PRLinkMessage = {
129    type: 'pr-link'
130    sessionId: UUID
131    prNumber: number
132    prUrl: string
133    prRepository: string // e.g., "owner/repo"
134    timestamp: string // ISO timestamp when linked
135  }
136  
137  export type ModeEntry = {
138    type: 'mode'
139    sessionId: UUID
140    mode: 'coordinator' | 'normal'
141  }
142  
143  /**
144   * Worktree session state persisted to the transcript for resume.
145   * Subset of WorktreeSession from utils/worktree.ts — excludes ephemeral
146   * fields (creationDurationMs, usedSparsePaths) that are only used for
147   * first-run analytics.
148   */
149  export type PersistedWorktreeSession = {
150    originalCwd: string
151    worktreePath: string
152    worktreeName: string
153    worktreeBranch?: string
154    originalBranch?: string
155    originalHeadCommit?: string
156    sessionId: string
157    tmuxSessionName?: string
158    hookBased?: boolean
159  }
160  
161  /**
162   * Records whether the session is currently inside a worktree created by
163   * EnterWorktree or --worktree. Last-wins: an enter writes the session,
164   * an exit writes null. On --resume, restored only if the worktreePath
165   * still exists on disk (the /exit dialog may have removed it).
166   */
167  export type WorktreeStateEntry = {
168    type: 'worktree-state'
169    sessionId: UUID
170    worktreeSession: PersistedWorktreeSession | null
171  }
172  
173  /**
174   * Records content blocks whose in-context representation was replaced with a
175   * smaller stub (the full content was persisted elsewhere). Replayed on resume
176   * for prompt cache stability. Written once per enforcement pass that replaces
177   * at least one block. When agentId is set, the record belongs to a subagent
178   * sidechain (AgentTool resume reads these); when absent, it's main-thread
179   * (/resume reads these).
180   */
181  export type ContentReplacementEntry = {
182    type: 'content-replacement'
183    sessionId: UUID
184    agentId?: AgentId
185    replacements: ContentReplacementRecord[]
186  }
187  
188  export type FileHistorySnapshotMessage = {
189    type: 'file-history-snapshot'
190    messageId: UUID
191    snapshot: FileHistorySnapshot
192    isSnapshotUpdate: boolean
193  }
194  
195  /**
196   * Per-file attribution state tracking Claude's character contributions.
197   */
198  export type FileAttributionState = {
199    contentHash: string // SHA-256 hash of file content
200    claudeContribution: number // Characters written by Claude
201    mtime: number // File modification time
202  }
203  
204  /**
205   * Attribution snapshot message stored in session transcript.
206   * Tracks character-level contributions by Claude for commit attribution.
207   */
208  export type AttributionSnapshotMessage = {
209    type: 'attribution-snapshot'
210    messageId: UUID
211    surface: string // Client surface (cli, ide, web, api)
212    fileStates: Record<string, FileAttributionState>
213    promptCount?: number // Total prompts in session
214    promptCountAtLastCommit?: number // Prompts at last commit
215    permissionPromptCount?: number // Total permission prompts shown
216    permissionPromptCountAtLastCommit?: number // Permission prompts at last commit
217    escapeCount?: number // Total ESC presses (cancelled permission prompts)
218    escapeCountAtLastCommit?: number // ESC presses at last commit
219  }
220  
221  export type TranscriptMessage = SerializedMessage & {
222    parentUuid: UUID | null
223    logicalParentUuid?: UUID | null // Preserves logical parent when parentUuid is nullified for session breaks
224    isSidechain: boolean
225    gitBranch?: string
226    agentId?: string // Agent ID for sidechain transcripts to enable resuming agents
227    teamName?: string // Team name if this is a spawned agent session
228    agentName?: string // Agent's custom name (from /rename or swarm)
229    agentColor?: string // Agent's color (from /rename or swarm)
230    promptId?: string // Correlates with OTel prompt.id for user prompt messages
231  }
232  
233  export type SpeculationAcceptMessage = {
234    type: 'speculation-accept'
235    timestamp: string
236    timeSavedMs: number
237  }
238  
239  /**
240   * Persisted context-collapse commit. The archived messages themselves are
241   * NOT persisted — they're already in the transcript as ordinary user/
242   * assistant messages. We only persist enough to reconstruct the splice
243   * instruction (boundary uuids) and the summary placeholder (which is NOT
244   * in the transcript because it's never yielded to the REPL).
245   *
246   * On restore, the store reconstructs CommittedCollapse with archived=[];
247   * projectView lazily fills the archive the first time it finds the span.
248   *
249   * Discriminator is obfuscated to match the gate name. sessionStorage.ts
250   * isn't feature-gated (it's the generic transcript plumbing used by every
251   * entry type), so a descriptive string here would leak into external builds
252   * via the appendEntry dispatch / loadTranscriptFile parser even though
253   * nothing in an external build ever writes or reads this entry.
254   */
255  export type ContextCollapseCommitEntry = {
256    type: 'marble-origami-commit'
257    sessionId: UUID
258    /** 16-digit collapse ID. Max across entries reseeds the ID counter. */
259    collapseId: string
260    /** The summary placeholder's uuid — registerSummary() needs it. */
261    summaryUuid: string
262    /** Full <collapsed id="...">text</collapsed> string for the placeholder. */
263    summaryContent: string
264    /** Plain summary text for ctx_inspect. */
265    summary: string
266    /** Span boundaries — projectView finds these in the resumed Message[]. */
267    firstArchivedUuid: string
268    lastArchivedUuid: string
269  }
270  
271  /**
272   * Snapshot of the staged queue and spawn trigger state. Unlike commits
273   * (append-only, replay-all), snapshots are last-wins — only the most
274   * recent snapshot entry is applied on restore. Written after every
275   * ctx-agent spawn resolves (when staged contents may have changed).
276   *
277   * Staged boundaries are UUIDs (session-stable), not collapse IDs (which
278   * reset with the uuidToId bimap). Restoring a staged span issues fresh
279   * collapse IDs for those messages on the next decorate/display, but the
280   * span itself resolves correctly.
281   */
282  export type ContextCollapseSnapshotEntry = {
283    type: 'marble-origami-snapshot'
284    sessionId: UUID
285    staged: Array<{
286      startUuid: string
287      endUuid: string
288      summary: string
289      risk: number
290      stagedAt: number
291    }>
292    /** Spawn trigger state — so the +interval clock picks up where it left off. */
293    armed: boolean
294    lastSpawnTokens: number
295  }
296  
297  export type Entry =
298    | TranscriptMessage
299    | SummaryMessage
300    | CustomTitleMessage
301    | AiTitleMessage
302    | LastPromptMessage
303    | TaskSummaryMessage
304    | TagMessage
305    | AgentNameMessage
306    | AgentColorMessage
307    | AgentSettingMessage
308    | PRLinkMessage
309    | FileHistorySnapshotMessage
310    | AttributionSnapshotMessage
311    | QueueOperationMessage
312    | SpeculationAcceptMessage
313    | ModeEntry
314    | WorktreeStateEntry
315    | ContentReplacementEntry
316    | ContextCollapseCommitEntry
317    | ContextCollapseSnapshotEntry
318  
319  export function sortLogs(logs: LogOption[]): LogOption[] {
320    return logs.sort((a, b) => {
321      // Sort by modified date (newest first)
322      const modifiedDiff = b.modified.getTime() - a.modified.getTime()
323      if (modifiedDiff !== 0) {
324        return modifiedDiff
325      }
326  
327      // If modified dates are equal, sort by created date (newest first)
328      return b.created.getTime() - a.created.getTime()
329    })
330  }