Class: Clacky::SessionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/session_manager.rb

Constant Summary collapse

SESSIONS_DIR =
File.join(Dir.home, ".clacky", "sessions")

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sessions_dir: nil) ⇒ SessionManager

Returns a new instance of SessionManager.



19
20
21
22
# File 'lib/clacky/session_manager.rb', line 19

def initialize(sessions_dir: nil)
  @sessions_dir = sessions_dir || SESSIONS_DIR
  ensure_sessions_dir
end

Class Method Details

.generate_idObject

Generate a new unique session ID (16-char hex string). This is the single authoritative source for session IDs — all components (Agent, SessionRegistry) should receive an ID generated here rather than creating their own.



15
16
17
# File 'lib/clacky/session_manager.rb', line 15

def self.generate_id
  SecureRandom.hex(8)
end

Instance Method Details

#all_sessions(current_dir: nil, limit: nil) ⇒ Object

All sessions from disk, newest-first (sorted by created_at). Optional filters:

current_dir: (String) if given, sessions matching working_dir come first
limit:       (Integer) max number of sessions to return


69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/clacky/session_manager.rb', line 69

def all_sessions(current_dir: nil, limit: nil)
  sessions = Dir.glob(File.join(@sessions_dir, "*.json")).filter_map do |filepath|
    load_session_file(filepath)
  end.sort_by { |s| s[:created_at] || "" }.reverse

  if current_dir
    current_sessions = sessions.select { |s| s[:working_dir] == current_dir }
    other_sessions   = sessions.reject { |s| s[:working_dir] == current_dir }
    sessions = current_sessions + other_sessions
  end

  limit ? sessions.first(limit) : sessions
end

#cleanup(days: 90) ⇒ Object

Delete sessions not accessed within the given number of days (default: 90). Returns count of deleted sessions.



90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/clacky/session_manager.rb', line 90

def cleanup(days: 90)
  cutoff = Time.now - (days * 24 * 60 * 60)
  deleted = 0
  Dir.glob(File.join(@sessions_dir, "*.json")).each do |filepath|
    session = load_session_file(filepath)
    next unless session
    if Time.parse(session[:updated_at]) < cutoff
      delete_session_with_chunks(filepath)
      deleted += 1
    end
  end
  deleted
end

#cleanup_by_count(keep:) ⇒ Object

Keep only the most recent N sessions by created_at; delete the rest. Returns count of deleted sessions.



106
107
108
109
110
111
112
113
114
# File 'lib/clacky/session_manager.rb', line 106

def cleanup_by_count(keep:)
  sessions = all_sessions # already sorted newest-first
  return 0 if sessions.size <= keep

  sessions[keep..].each do |session|
    filepath = File.join(@sessions_dir, generate_filename(session[:session_id], session[:created_at]))
    delete_session_with_chunks(filepath) if File.exist?(filepath)
  end.size
end

#delete(session_id) ⇒ Object

Physical delete — removes disk file + associated chunk files. Returns true if found and deleted, false if not found.



56
57
58
59
60
61
62
63
# File 'lib/clacky/session_manager.rb', line 56

def delete(session_id)
  session = all_sessions.find { |s| s[:session_id].to_s.start_with?(session_id.to_s) }
  return false unless session

  filepath = File.join(@sessions_dir, generate_filename(session[:session_id], session[:created_at]))
  delete_session_with_chunks(filepath)
  true
end

#delete_session_with_chunks(json_filepath) ⇒ Object

Delete a session JSON file and all its associated chunk MD files.



128
129
130
131
132
# File 'lib/clacky/session_manager.rb', line 128

def delete_session_with_chunks(json_filepath)
  File.delete(json_filepath) if File.exist?(json_filepath)
  base = File.basename(json_filepath, ".json")
  Dir.glob(File.join(@sessions_dir, "#{base}-chunk-*.md")).each { |f| File.delete(f) }
end

#ensure_sessions_dirObject



117
118
119
# File 'lib/clacky/session_manager.rb', line 117

def ensure_sessions_dir
  FileUtils.mkdir_p(@sessions_dir) unless Dir.exist?(@sessions_dir)
end

#generate_filename(session_id, created_at) ⇒ Object



121
122
123
124
125
# File 'lib/clacky/session_manager.rb', line 121

def generate_filename(session_id, created_at)
  datetime = Time.parse(created_at).strftime("%Y-%m-%d-%H-%M-%S")
  short_id = session_id[0..7]
  "#{datetime}-#{short_id}.json"
end

#last_saved_pathObject

Path of the last saved session file.



45
46
47
# File 'lib/clacky/session_manager.rb', line 45

def last_saved_path
  @last_saved_path
end

#latest_for_directory(working_dir) ⇒ Object

Return the most recent session for a given working directory, or nil.



84
85
86
# File 'lib/clacky/session_manager.rb', line 84

def latest_for_directory(working_dir)
  all_sessions(current_dir: working_dir).first
end

#load(session_id) ⇒ Object

Load a specific session by ID. Returns nil if not found.



50
51
52
# File 'lib/clacky/session_manager.rb', line 50

def load(session_id)
  all_sessions.find { |s| s[:session_id].to_s.start_with?(session_id.to_s) }
end

#load_session_file(filepath) ⇒ Object



134
135
136
137
138
# File 'lib/clacky/session_manager.rb', line 134

def load_session_file(filepath)
  JSON.parse(File.read(filepath), symbolize_names: true)
rescue JSON::ParserError, Errno::ENOENT
  nil
end

#save(session_data) ⇒ Object

Save a session. Returns the file path.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/clacky/session_manager.rb', line 25

def save(session_data)
  filename = generate_filename(session_data[:session_id], session_data[:created_at])
  filepath = File.join(@sessions_dir, filename)

  File.write(filepath, JSON.pretty_generate(session_data))
  FileUtils.chmod(0o600, filepath)

  @last_saved_path = filepath

  # Keep only the most recent 200 sessions (best-effort, never block save)
  begin
    cleanup_by_count(keep: 200)
  rescue Exception # rubocop:disable Lint/RescueException
    # Cleanup is non-critical; swallow all errors (including AgentInterrupted)
  end

  filepath
end