Module: ClaudeAgentSDK::SessionMutations
- Defined in:
- lib/claude_agent_sdk/session_mutations.rb
Overview
Session mutation functions: rename, tag, delete, and fork sessions.
Ported from Python SDK’s _internal/session_mutations.py. Appends typed metadata entries to the session’s JSONL file, matching the CLI pattern. Safe to call from any SDK host process.
Constant Summary collapse
- UNICODE_STRIP_RE =
Unicode sanitization — ported from Python SDK / TS sanitization.ts
Iteratively applies NFKC normalization and strips format/private-use/ unassigned characters until stable (max 10 iterations).
/[\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff\ue000-\uf8ff]/- FORMAT_CATEGORIES =
%w[Cf Co Cn].freeze
Class Method Summary collapse
-
.delete_session(session_id:, directory: nil) ⇒ Object
Delete a session by removing its JSONL file.
-
.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session into a new branch with fresh UUIDs.
-
.rename_session(session_id:, title:, directory: nil) ⇒ Object
Rename a session by appending a custom-title entry.
-
.tag_session(session_id:, tag:, directory: nil) ⇒ Object
Tag a session.
Class Method Details
.delete_session(session_id:, directory: nil) ⇒ Object
Delete a session by removing its JSONL file.
This is a hard delete — the file is removed permanently. For soft-delete semantics, use tag_session(id, ‘__hidden’) and filter on listing instead.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/claude_agent_sdk/session_mutations.rb', line 73 def delete_session(session_id:, directory: nil) raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE) result = find_session_file_with_dir(session_id, directory) raise Errno::ENOENT, "Session #{session_id} not found#{" in project directory for #{directory}" if directory}" unless result path = result[0] begin File.delete(path) rescue Errno::ENOENT raise Errno::ENOENT, "Session #{session_id} not found" end end |
.fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) ⇒ ForkSessionResult
Fork a session into a new branch with fresh UUIDs.
Creates a copy of the session transcript (or a prefix up to up_to_message_id) with remapped UUIDs and a new session ID. Sidechains are filtered out, progress entries are excluded from the written output but used for parentUuid chain walking.
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 |
# File 'lib/claude_agent_sdk/session_mutations.rb', line 102 def fork_session(session_id:, directory: nil, up_to_message_id: nil, title: nil) # rubocop:disable Metrics/MethodLength raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE) raise ArgumentError, "Invalid up_to_message_id: #{}" if && !.match?(Sessions::UUID_RE) result = find_session_file_with_dir(session_id, directory) raise Errno::ENOENT, "Session #{session_id} not found#{" in project directory for #{directory}" if directory}" unless result file_path, project_dir = result content = File.read(file_path) raise ArgumentError, "Session #{session_id} has no messages to fork" if content.empty? transcript, content_replacements = parse_fork_transcript(content, session_id) transcript.reject! { |e| e['isSidechain'] } raise ArgumentError, "Session #{session_id} has no messages to fork" if transcript.empty? if cutoff = transcript.index { |e| e['uuid'] == } raise ArgumentError, "Message #{} not found in session #{session_id}" unless cutoff transcript = transcript[0..cutoff] end # Build UUID mapping (including progress entries for parentUuid chain walk) uuid_mapping = {} transcript.each { |e| uuid_mapping[e['uuid']] = SecureRandom.uuid } by_uuid = transcript.to_h { |e| [e['uuid'], e] } # Filter out progress messages from written output writable = transcript.reject { |e| e['type'] == 'progress' } raise ArgumentError, "Session #{session_id} has no messages to fork" if writable.empty? forked_session_id = SecureRandom.uuid now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%3NZ') lines = writable.each_with_index.map do |original, i| build_forked_entry(original, i, writable.size, uuid_mapping, by_uuid, forked_session_id, session_id, now) end # Append content-replacement entry if any if content_replacements && !content_replacements.empty? lines << JSON.generate({ 'type' => 'content-replacement', 'sessionId' => forked_session_id, 'replacements' => content_replacements }) end # Derive title fork_title = title&.strip if fork_title.nil? || fork_title.empty? head = content[0, Sessions::LITE_READ_BUF_SIZE] || '' tail = content.length > Sessions::LITE_READ_BUF_SIZE ? content[-Sessions::LITE_READ_BUF_SIZE..] : head base = Sessions.extract_json_string_field(tail, 'customTitle', last: true) || Sessions.extract_json_string_field(head, 'customTitle', last: true) || Sessions.extract_json_string_field(tail, 'aiTitle', last: true) || Sessions.extract_json_string_field(head, 'aiTitle', last: true) || Sessions.extract_first_prompt_from_head(head) || 'Forked session' fork_title = "#{base} (fork)" end lines << JSON.generate({ 'type' => 'custom-title', 'sessionId' => forked_session_id, 'customTitle' => fork_title }) fork_path = File.join(project_dir, "#{forked_session_id}.jsonl") io = nil fd = IO.sysopen(fork_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) begin io = IO.new(fd) io.write("#{lines.join("\n")}\n") ensure if io io.close else IO.for_fd(fd).close rescue nil # rubocop:disable Style/RescueModifier end end ForkSessionResult.new(session_id: forked_session_id) end |
.rename_session(session_id:, title:, directory: nil) ⇒ Object
Rename a session by appending a custom-title entry.
list_sessions reads the LAST custom-title from the file tail, so repeated calls are safe — the most recent wins.
26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/claude_agent_sdk/session_mutations.rb', line 26 def rename_session(session_id:, title:, directory: nil) raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE) stripped = title.strip raise ArgumentError, 'title must be non-empty' if stripped.empty? data = "#{JSON.generate({ type: 'custom-title', customTitle: stripped, sessionId: session_id }, space_size: 0)}\n" append_to_session(session_id, data, directory) end |
.tag_session(session_id:, tag:, directory: nil) ⇒ Object
Tag a session. Pass nil to clear the tag.
Appends a type:‘tag’,tag:<tag>,sessionId:<id> JSONL entry. Tags are Unicode-sanitized before storing.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/claude_agent_sdk/session_mutations.rb', line 48 def tag_session(session_id:, tag:, directory: nil) raise ArgumentError, "Invalid session_id: #{session_id}" unless session_id.match?(Sessions::UUID_RE) if tag sanitized = sanitize_unicode(tag).strip raise ArgumentError, 'tag must be non-empty (use nil to clear)' if sanitized.empty? tag = sanitized end data = "#{JSON.generate({ type: 'tag', tag: tag || '', sessionId: session_id }, space_size: 0)}\n" append_to_session(session_id, data, directory) end |