Module: ClaudeAgentSDK::SessionSummary

Defined in:
lib/claude_agent_sdk/session_summary.rb

Overview

Incremental session-summary derivation for SessionStore adapters.

fold_session_summary lets a store maintain a per-session summary sidecar incrementally inside #append so list_sessions_from_store can fetch all metadata in a single #list_session_summaries call instead of N per-session #load calls. Every derived field is append-incremental (set-once or last-wins) so adapters never need to re-read previously appended entries.

All structures use STRING keys throughout: entries are raw JSONL objects (string keys from JSON), and the summary’s opaque data dict is persisted verbatim by adapters — string keys survive a JSON round-trip (Postgres JSONB, Redis) losslessly, whereas symbol keys would not.

Constant Summary collapse

LAST_WINS_FIELDS =

JSONL entry keys -> summary data keys for last-wins string fields. Each appended entry overwrites the previous value when present.

{
  'customTitle' => 'custom_title',
  'aiTitle' => 'ai_title',
  'lastPrompt' => 'last_prompt',
  'summary' => 'summary_hint',
  'gitBranch' => 'git_branch'
}.freeze

Class Method Summary collapse

Class Method Details

.fold_session_summary(prev, key, entries) ⇒ Hash

Fold a batch of appended entries into the running summary for key.

Stores call this from inside #append to keep a summary sidecar up to date without re-reading the transcript. prev is the previous summary for the same key (or nil for the first append).

Do NOT call this for keys with a subpath — subagent transcripts must not contribute to the main session’s summary. Guard with ‘if key.nil?` before calling.

mtime is NOT touched by the fold — it is the sidecar’s storage write time and must be stamped by the adapter after persisting (sharing a clock with the mtime returned by SessionStore#list_sessions). For a new session (prev nil) the fold returns mtime 0 as a placeholder for the adapter to overwrite.

Parameters:

  • prev (Hash, nil)

    previous summary entry for this key

  • key (Hash)

    the SessionKey (string keys)

  • entries (Array<Hash>)

    newly appended transcript entries

Returns:

  • (Hash)

    the updated summary entry ({ ‘session_id’, ‘mtime’, ‘data’ })



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
# File 'lib/claude_agent_sdk/session_summary.rb', line 52

def fold_session_summary(prev, key, entries)
  summary = if prev
              { 'session_id' => prev['session_id'], 'mtime' => prev['mtime'], 'data' => prev['data'].dup }
            else
              { 'session_id' => key['session_id'], 'mtime' => 0, 'data' => {} }
            end
  data = summary['data']

  entries.each do |entry|
    next unless entry.is_a?(Hash)

    data['is_sidechain'] = (entry['isSidechain'] == true) unless data.key?('is_sidechain')

    # created_at is set-once, so skip the (regex + Time.iso8601) parse for
    # every entry after the first timestamped one — folds run over whole
    # transcripts on the store read paths.
    ms = data.key?('created_at') ? nil : Sessions.parse_iso_timestamp_ms(entry['timestamp'])
    data['created_at'] = ms if ms

    unless data.key?('cwd')
      cwd = entry['cwd']
      data['cwd'] = cwd if cwd.is_a?(String) && !cwd.empty?
    end

    fold_first_prompt(data, entry)

    LAST_WINS_FIELDS.each do |src, dst|
      val = entry[src]
      data[dst] = val if val.is_a?(String)
    end

    next unless entry['type'] == 'tag'

    tag_val = entry['tag']
    if tag_val.is_a?(String) && !tag_val.empty?
      data['tag'] = tag_val
    else
      # Empty string or absent tag clears the tag.
      data.delete('tag')
    end
  end

  summary
end

.summary_entry_to_sdk_info(entry, project_path) ⇒ SDKSessionInfo?

Convert a summary entry to SDKSessionInfo. Returns nil for sidechain sessions or sessions with no extractable summary, matching the disk lite-parse’s filtering in Sessions#build_session_info.

Parameters:

  • entry (Hash)

    a summary entry from SessionStore#list_session_summaries

  • project_path (String, nil)

    fallback cwd

Returns:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/claude_agent_sdk/session_summary.rb', line 104

def summary_entry_to_sdk_info(entry, project_path)
  data = entry['data'] || {}
  return nil if data['is_sidechain']

  first_prompt = presence(data['first_prompt_locked'] ? data['first_prompt'] : data['command_fallback'])
  custom_title = presence(data['custom_title']) || presence(data['ai_title'])
  summary = custom_title || presence(data['last_prompt']) || presence(data['summary_hint']) || first_prompt
  return nil unless summary

  SDKSessionInfo.new(
    session_id: entry['session_id'],
    summary: summary,
    last_modified: entry['mtime'],
    # file_size is a JSONL byte count — meaningful only for the local-disk
    # path. Stores have no equivalent.
    file_size: nil,
    custom_title: custom_title,
    first_prompt: first_prompt,
    git_branch: presence(data['git_branch']),
    cwd: presence(data['cwd']) || presence(project_path),
    tag: presence(data['tag']),
    created_at: data['created_at']
  )
end