Module: OllamaChat::SessionManagement

Included in:
Chat
Defined in:
lib/ollama_chat/session_management.rb

Overview

The OllamaChat::SessionHandling module provides methods for managing chat sessions, including creating, listing, switching, renaming, and deleting sessions.

It integrates closely with the database-backed Session model and ensures that session data is persisted correctly, especially the conversation history.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#session ⇒ OllamaChat::Database::Models::Session (readonly)

The session reader returns the current session object.

Returns:



65
66
67
# File 'lib/ollama_chat/session_management.rb', line 65

def session
  @session
end

Instance Method Details

#change_session(name) ⇒ Object (private)

Changes to a different session, saving the current one and loading the new one.

Parameters:

  • name (String) —

    the name or ID of the session to switch to



645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/ollama_chat/session_management.rb', line 645

def change_session(name)
  name.full? or name = ??
  previous_session = nil
  loop do
    if chosen_session = choose_session(name, except_id: session.id, allow_new: true)
      if chosen_session.nil? || chosen_session == session
        confirm?(
          prompt: "\n⏎  Same session chosen, Press any key to continue (%s). ",
          timeout: 3
        )
        break
      end
      session_close
      previous_session = session
      @session = chosen_session
      if session.lock?
        messages.read_conversation_jsonl(session.messages.to_s)
        if current_collection = session.current_collection.full? and
            database_collection?(current_collection)
        then
          set_documents_collection(current_collection)
        else
          set_documents_collection(:default)
        end
        session.current_model.full? { use_model(_1) }
        set_default_persona_name(session.default_persona_name.full? || :none)
        set_current_system_prompt(session.current_system_prompt.full? || 'default')
        session_apply
        log(:info, "Session changed", data: { session_id: session.id, name: session.name, previous_session_id: previous_session&.id })
        info_session
        break
      else
        feedback(
          "session #{session.id} locked by running process #{session.locked?}",
          type: :warn
        )
        @session = previous_session
        @session.lock
        name = ??
        confirm?(
          prompt: "\n⏎  Session locked: could not switch, Press any key to continue (%s). ",
          timeout: 3
        )
        redo
      end
    else
      feedback("Cancelled.", type: :cancel)
      break
    end
  end
ensure
  set_previous_session_on_change(previous_session&.id)
  session_sync
end

#choose_session(session_name, except_id: nil, allow_new: false, exit_app: false) ⇒ OllamaChat::Database::Models::Session? (private)

Finds or selects a session based on a name, ID, or pattern.

Parameters:

  • session_name (String) —

    the name, ID, or pattern to search for

  • except_id (String, Integer, nil) (defaults to: nil) —

    an ID to exclude from the search results

  • allow_new (Boolean) (defaults to: false) —

    whether to offer creating a new session

Returns:



720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
# File 'lib/ollama_chat/session_management.rb', line 720

def choose_session(session_name, except_id: nil, allow_new: false, exit_app: false)
  session_name = session_name.to_s
  session_query = models::Session
  if except_id
    session_query = session_query.where(Sequel[:id] !~ except_id)
  end
  if session_name =~ /\A\d+\z/ and
    session = session_query.first(id: session_name)
  then
    return session
  end
  selector = if session_name =~ /\A\?+(.*)\z/
               session_name = nil
               Regexp.new($1)
             end
  if session_name and session = session_query.first(name: session_name)
    session
  elsif selector
    now = Time.now
    sessions = session_query.order(Sequel.desc(:updated_at)).map { |session|
      duration = session.age(now:)
      es       = session.estimate_tokens
      count    = session.count_messages
      locked   = if pid = session.locked?
                   if pid == $$
                     " πŸ”“#{pid} "
                   else
                     " πŸ”#{pid} "
                   end
                 else
                   ' '
                 end
      suffix               = " πŸ†”#{session.id}#{locked}πŸ“¨#{count} 🧩#{es.tokens_formatted} ⏳#{duration}"
      display_session_name = truncate(session.name, length: (0.8 * (width - suffix.length)).floor.clamp(1..))
      display              = "#{display_session_name}#{suffix}"
      SearchUI::Wrapper.new([ session.id, session.name ] * ?-, display:)
    }
    selector and sessions = sessions.select { _1 =~ selector }
    session_selected =
      if sessions.size == 1
        sessions.first.value
      else
        allow_new and sessions.unshift(SearchUI::Wrapper.new('[new]', display: '[NEW]'))
        if exit_app
          sessions.unshift(SearchUI::Wrapper.new('[quit-app]', display: '[QUIT-APP]'))
        end
        sessions.unshift(SearchUI::Wrapper.new('[exit]', display: '[EXIT]'))
        value = choose_entry(sessions, prompt: 'Select a chat session: %s')&.value
        if value == '[new]'
          name = determine_valid_new_name_for_session('to create') or return
          return new_session(name:)
        elsif value == '[quit-app]'
          return :quit_app
        elsif value == '[exit]'
          return nil
        end
        value
      end
    if session_selected
      session_query.first(id: session_selected.split(?-, 2).first)
    end
  end
end

#delete_session ⇒ Object (private)

Deletes the current session and prompts the user to pick a new one to switch to.



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
# File 'lib/ollama_chat/session_management.rb', line 305

def delete_session
  current_session_name, current_session_id = session.name, session.id
  feedback(<<~EOT, type: :info)
    The current session
      #{current_session_name.inspect} (#{current_session_id})
    will be deleted, pick a new session to switch to.
  EOT
  confirm?(prompt: "\n⏎  Press any key to continue (%s). ", timeout: 3)
  chosen = choose_session(??, except_id: current_session_id, allow_new: true, exit_app: true)
  if chosen == :quit_app
    feedback("Exiting application.")
    exit 0
  end
  if chosen
    chosen.save
    confirm?(
      prompt: "πŸ”” Delete session #{current_session_name.inspect} (#{current_session_id})? (y/n) ",
      yes: /\Ay/i
    ) or return
    change_session(chosen.id)
    models::Session.where(id: current_session_id).destroy
    log(:info, "Session deleted", data: { session_id: current_session_id, name: current_session_name })
    feedback("Just deleted session #{current_session_name.inspect}!", type: :info)
  end
end

#derive_session_name(length: 128) ⇒ String? (private)

Derives a title for the session based on its content.

Parameters:

  • length (Integer) (defaults to: 128) —

    the maximum length of the title (default: 128)

Returns:

  • (String, nil) —

    the derived session name, or nil



596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'lib/ollama_chat/session_management.rb', line 596

def derive_session_name(length: 128)
  content = messages.each_message.inject('') do |c, message|
    message.content.present? or next c
    sender_name = sender_name_displayed(message)
    c << "%s: %s\n\n" % [ sender_name, message.content ]
  end
  prompt = prompt(:session_title).to_s.named_placeholders_interpolate({ length:, content: })
  Infobar.busy(
    label: 'Naming session…',
    frames: :braille7,
    output: STDOUT,
  ) do
    generate(prompt:).full? do |name|
      name = name.
        gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
        gsub(/\s+/, ' ')
      truncate(name, length:)
    end
  end
end

#determine_valid_new_name_for_session(action, default_name: nil) ⇒ String? (private)

Interactively prompts the user for a unique session name.

This method will keep prompting the user until a name is provided that does not already exist in the database, or until the user cancels.

Parameters:

  • action (String) —

    a description of the action being performed

  • default_name (String, nil) (defaults to: nil) —

    an optional prefill value for the prompt

Returns:

  • (String, nil) —

    the unique session name, or nil if cancelled



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/ollama_chat/session_management.rb', line 164

def determine_valid_new_name_for_session(action, default_name: nil)
  session_name = nil
  loop do
    session_name = ask?(
      prompt: "❓ Enter new session name #{action}, C-u β‡’ random, C-c β‡’ cancel: ",
      prefill: default_name
    )
    if session_name.nil?
      feedback("Cancelled.", type: :cancel)
      return
    elsif session_name.blank?
      session_name = new_random_session_name
    end
    feedback("New session name is #{session_name.inspect}.", type: :info)
    if models::Session.where(name: session_name).present?
      feedback("Session named #{bold{session_name}} already exists.", type: :warn)
    else
      break
    end
  end
  session_name
end

#duplicate_session ⇒ Object (private)

Duplicates the current session into a new one.

This method creates a copy of the current session's attributes and prompts the user for a new name and whether to clear the duplicated session's message history.



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
# File 'lib/ollama_chat/session_management.rb', line 228

def duplicate_session
  name = switch_history(:session_name) do
    determine_valid_new_name_for_session(
      'to create', default_name: session.name
    )
  end or return
  old_session = session
  old_session.unlock
  @session = session.duplicate
  set_previous_session_on_change(old_session.id)
  session.update(name:)
  session.lock
  confirm?(
    prompt: "πŸ”” Clear messages of duplicated session? (y/n) ",
    yes: /\Ay/i
  ) and messages.clear
  session.current_model.full? {
    use_model(_1)
    copy_model_options_to_session
  }
  log(:info, "Session duplicated", data: {
    session_id: session.id, name: session.name, old_session_id: old_session.id
  })
  nil
end

#edit_session_trigger ⇒ Object (private)

Interactively edits the trigger configuration for a collection.

Presents a chooser for available rerank prompt names, then prompts for the collection, text_count, and enabled state. Existing values are prefilled when re-editing a known trigger so the user can simply accept defaults or toggle enabled off.



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/ollama_chat/session_management.rb', line 523

def edit_session_trigger
  prompts = models::Prompt.where(context: 'rerank').map(&:name).sort
  return feedback('No rerank prompts available.', type: :warn) \
    unless prompts.any?

  collections = all_collections.map(&:name).sort
  return feedback('No collections available.', type: :warn) \
    unless collections.any?

  existing = session.trigger.to_h

  collection = nil

  choose_with_state do
    # Prefill with the first configured collection as a starting
    # point. Fine for the single-trigger case; with multiple
    # configured, it's just an arbitrary default the user can
    # override in the chooser.
    if selected = existing.first&.first&.dup
      self.current_search_state = SearchUI::Search::State.new(selected, 0)
    end
    collection = choose_entry(collections, prompt: 'Collection? %s')
    return feedback('Exiting chooser.', type: :info) unless collection
  end

  prompt_name = nil
  choose_with_state do
    if selected = existing.dig(collection, 'prompt_name')
      self.current_search_state = SearchUI::Search::State.new(selected, 0)
    end
    prompt_name = choose_entry(prompts, prompt: 'Rerank prompt? %s')
    return feedback('Exiting chooser.', type: :info) unless prompt_name
  end

  prefill_tc      = ''
  prefill_enabled = 'y'
  if cfg = existing[collection]
    prefill_tc      = cfg['text_count'].to_s
    prefill_enabled = cfg['enabled'] ? 'y' : 'n'
  end

  tc_raw = ask?(
    prompt:  'text_count (empty for no limit): ',
    prefill: prefill_tc
  )
  return feedback('Cancelled.', type: :cancel) if tc_raw.nil?
  text_count = tc_raw.strip.empty? ? nil : tc_raw.strip.to_i

  enabled_raw = ask?(
    prompt: 'Enabled? (y/n): ',
    prefill: prefill_enabled
  )
  return feedback('Cancelled.', type: :cancel) if enabled_raw.nil?
  enabled = enabled_raw.strip.match?(/\Ay/i)

  cfg = {
    'enabled'     => enabled,
    'prompt_name' => prompt_name,
  }
  cfg['text_count'] = text_count if text_count

  session.update(trigger: { collection => cfg })

  feedback(
    "Trigger for #{collection.inspect} β†’ #{cfg.inspect}.",
    type: :info
  )
end

#generate_conversation_report(name: nil) ⇒ String? (private)

Generates a report document for the current session's conversation.

Uses a user-selected prompt template from the 'report' context (e.g. coding_brief, roleplay_story) to produce a single cohesive document describing the session. Uses compacted_messages (summary + recent tail) rather than each_message so the interpolated content stays bounded even after multiple compaction rounds.

Parameters:

  • name (String, nil) (defaults to: nil) —

    specific template name (skips chooser)

Returns:

  • (String, nil) —

    the report or nil if empty



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/ollama_chat/session_management.rb', line 461

def generate_conversation_report(name: nil)
  content = messages.compacted_messages.inject('') do |c, message|
    message.content.present? or next c
    sender = sender_name_displayed(message)
    c << "%s: %s\n\n" % [ sender, message.content ]
  end
  content.empty? and return

  template = if name
               prompt(name, context: 'report')
             else
               choose_prompt(
                 context: 'report',
                 prompt: 'Which report template? %s'
               )
             end
  template or return

  system = prompt(:report, context: 'system').to_s

  Infobar.busy(
    label: 'Generating conversation report…',
    frames: :braille7,
    output: STDOUT,
  ) do
    generate(
      system:,
      prompt: template.to_s.named_placeholders_interpolate({ content: }),
      think:  true
    )
  end
end

#list_sessions ⇒ Object (private)

Lists all sessions in a formatted table.



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
# File 'lib/ollama_chat/session_management.rb', line 103

def list_sessions
  use_pager do |output|
    table = Terminal::Table.new
    table.style = {
      all_separators: true,
      border:         :unicode_round,
    }
    headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |h| bold { h } }
    now      = Time.now
    rows     = []
    models::Session.order(Sequel.desc(:updated_at)).each do |s|
      name   = s.name
      name   = session.id == s.id ? bold { name } : name
      name   = if pid = s.locked?
                 if pid == $$
                   "#{name} πŸ”“"
                 else
                   "#{name} πŸ”"
                 end
               else
                 name
               end
      es     = s.estimate_tokens
      rows << [
        s.id.to_s,
        name,
        es.bytes_formatted,
        es.tokens_formatted,
        s.count_messages,
        s.age(now:),
      ]
    end
    wrap_table_rows(rows, 6, headings:).each { |row| table << row }
    table.align_column 0, :right
    table.align_column 2, :left
    table.align_column 3, :right
    table.align_column 4, :right
    table.align_column 5, :right
    output.puts table
  end
end

Loads the collection of links from the current session.

This method reads the links attribute from the session and deserializes it from JSONL format.

Returns:

  • (Array<String>) —

    The list of links associated with the session.



48
49
50
51
# File 'lib/ollama_chat/session_management.rb', line 48

def load_links_from_session
  input = StringIO.new(session.links)
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').read_io(input:)
end

#new_random_session_name ⇒ String

Generates a unique random session name in the format New Session <token>.

Returns:

  • (String) —

    a unique session name



57
58
59
# File 'lib/ollama_chat/session_management.rb', line 57

def new_random_session_name
  "New Session #{Tins::Token.new}"
end

#new_session(name: nil) ⇒ OllamaChat::Database::Models::Session (private)

Creates a new, default session instance.

Parameters:

  • name (String, nil) (defaults to: nil) —

    an optional session name (defaults to a random name)

Returns:



74
75
76
# File 'lib/ollama_chat/session_management.rb', line 74

def new_session(name: nil)
  models::Session.with_defaults(self, name:)
end

#preferred_session ⇒ OllamaChat::Database::Models::Session (private)

Retrieves the preferred session from the database, or creates a new one if none exist.

Returns:



83
84
85
86
87
88
# File 'lib/ollama_chat/session_management.rb', line 83

def preferred_session
  models::Session.
    where(working_directory: Dir.pwd).
    order(:updated_at).last ||
    new_session
end

#previous_session ⇒ OllamaChat::Database::Models::Session? (private)

Returns the session associated with the stored @previous_session_id, provided the session exists in the database and is not currently locked.

Returns:



95
96
97
98
99
100
# File 'lib/ollama_chat/session_management.rb', line 95

def previous_session
  @previous_session_id  or return
  prev = models::Session.where(id: @previous_session_id).first or return
  prev.locked? and return
  prev
end

#rename_session ⇒ Object (private)

Note:

The use of 1.times do and redo ensures a single-retry capability for automatic name derivation.

Prompts the user to rename the current session interactively.

This method manages a sophisticated renaming workflow:

  1. It presents an interactive prompt using ask?.
  2. If the user provides an empty string, it attempts to automatically derive a new name using derive_session_name.
  3. After derivation, it uses redo to re-prompt the user, now pre-filling the prompt with the newly suggested name.
  4. If the user provides an arbitrary string, the session is renamed.
  5. If the user interrupts (e.g., via C-c), the process is cancelled.


344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/ollama_chat/session_management.rb', line 344

def rename_session
  switch_history(:session_name) do
    name = nil
    1.times do
      derived = false
      prefill ||= session.name
      name = ask?(
        prompt: "❓ Enter the new name for the session (C-u β‡’ auto, C-c β‡’ cancel): ",
        prefill:
      )
      if name.nil?
        feedback("Interrupt: Session renaming was cancelled.", type: :warn)
        return
      end
      if name.empty?
        if derived
          break
        else
          derived = true
          if prefill = derive_session_name.full?
            redo
          end
        end
      end
    end
    if name == session.name
      feedback("Keeping the old name #{name.inspect}.", type: :info)
    elsif name.present?
      if exists = models::Session.where(name:).present?
        feedback("Session with name #{name.inspect} already exists.", type: :warn)
      else
        session.update(name:)
        log(:info, "Session renamed", data: { session_id: session.id, new_name: name })
        feedback("Renamed current session to #{name.inspect}.", type: :info)
      end
    else
      feedback("Could not rename current session!", type: :warn)
    end
  rescue Sequel::UniqueConstraintViolation
    feedback("Could not rename session to #{name.inspect}, already exists!", type: :warn)
  end
end

#repair_group_uuids ⇒ Object (private)

Repairs group_uuid assignments in the current message list.

Walks the message array backwards looking for user messages (that are not tool calls) lacking a group_uuid. For each such anchor it:

  1. Swaps a preceding runtime_information tool message so the user message comes first (structural invariant).
  2. Propagates the anchor's UUID forward to all subsequent messages until the next user message or the end of the list.

After the backward pass, any remaining orphaned messages (e.g. system messages not part of a user-led exchange) are assigned fresh UUIDs via initialize_group_uuid.

Persists the repaired message list via store_messages_in_session.



799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
# File 'lib/ollama_chat/session_management.rb', line 799

def repair_group_uuids
  msgs = messages.messages # Direct reference to the internal array
  msgs.empty? and return

  # Work backwards to find and repair user-anchored groups
  i = msgs.size - 1
  while i >= 0
    # Find the first 'user' message from the back that lacks a group_uuid
    if msgs[i].role == 'user' && !msgs[i].tool? && msgs[i].group_uuid.nil?
      anchor_index = i
      group_uuid = msgs[anchor_index].initialize_group_uuid.group_uuid

      # 1. Structural Swap: [RuntimeInfo, User] -> [User, RuntimeInfo]
      if anchor_index > 0 && msgs[anchor_index - 1].tool_name == 'runtime_information'
        msgs[anchor_index - 1], msgs[anchor_index] = msgs[anchor_index], msgs[anchor_index - 1]
        i            -= 1
        anchor_index -= 1
      end

      # 2. Forward Propagation: Assign the same UUID to all following messages
      # until we hit another user message or the end of the list.
      ((anchor_index + 1)...msgs.size).each do |k|
        msgs[k].group_uuid and break
        msgs[k].group_uuid = group_uuid
      end

      # We just fixed a whole block; the next possible anchor is before this one.
    end
    i -= 1
  end

  # Handle any orphaned messages that aren't part of a user-led exchange,
  # e.g. system messages
  msgs.each(&:initialize_group_uuid)

  store_messages_in_session
end

#report_conversation(name: nil) ⇒ Object (private)

Generates a report and displays / saves the result.

Parameters:

  • name (String, nil) (defaults to: nil) —

    specific report template name (skips chooser)



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/ollama_chat/session_management.rb', line 497

def report_conversation(name: nil)
  result = generate_conversation_report(name:)
  if result.full?
    use_pager do |output|
      output.puts kramdown_ansi_parse(result)
    end
    if filename = ask_for_filename?(action: 'for report') and
        should_overwrite?(filename)
    then
      filename.write(result)
      feedback("File successfully written.", type: :success)
    else
      feedback("Cancelled.", type: :cancel)
    end
  else
    feedback("Nothing to report!", type: :info)
    return
  end
end

#session_apply ⇒ OllamaChat::Database::Models::Session (private)

Applies the loaded session to the chat context.

Performs the post-lock setup sequence:

1. Updates the session's working directory to `Dir.pwd`.
2. Initializes the in-memory message history from the session
 payload.
3. Repairs any orphaned group UUIDs in the message list.
4. Ensures the persistent memory collection for the session's
 persona exists in the database (no-op if already present).

Called from #setup_session after the session has been locked.

Returns:



295
296
297
298
299
300
301
# File 'lib/ollama_chat/session_management.rb', line 295

def session_apply
  sync_trigger
  session.update(working_directory: Dir.pwd)
  init_history
  repair_group_uuids
  session
end

#session_close ⇒ Object (private)

Closes the current session synchronizing it first and releasing the process lock. This should be called during application shutdown or when switching sessions to ensure the session is available for future instances.



636
637
638
639
# File 'lib/ollama_chat/session_management.rb', line 636

def session_close
  session_sync
  session.unlock
end

#session_sync ⇒ OllamaChat::Database::Models::Session (private)

Synchronizes the current session state to the database.

Persists messages, links, and history, updates the working directory, and re-locks the session. Called before closing or switching sessions to ensure no unsaved state is lost.

Returns:



624
625
626
627
628
629
630
631
# File 'lib/ollama_chat/session_management.rb', line 624

def session_sync
  store_messages_in_session
  links.sync
  save_history
  session.working_directory = Dir.pwd
  session.lock
  session
end

#set_new_session ⇒ nil (private)

Creates and activates a new session with a unique name.

This method prompts for a name, initializes a new session record, locks it, and sets up the associated model and options.

Returns:

  • (nil)


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
# File 'lib/ollama_chat/session_management.rb', line 193

def set_new_session
  name = switch_history(:session_name) do
    determine_valid_new_name_for_session('to create')
  end
  name.nil? and return
  session_close
  previous_session_id = @session.id
  @session = new_session(name:)
  set_previous_session_on_change(previous_session_id)
  unless session.lock?
    feedback(
      "session #{session.id} locked by running process #{session.locked?}",
      type: :warn
    )
    raise OllamaChat::OllamaChatError,
      "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
  end
  @default_persona_name = session.default_persona_name
  session_apply
  messages.clear
  session.current_model.full? {
    use_model(_1)
    copy_model_options_to_session
  }
  log(:info, "New session created", data: { session_id: session.id, name: session.name })
  nil
ensure
  session.lock
end

#set_previous_session_on_change(previous_session_id) ⇒ Object (private)

Records the previous session ID after a session transition.

Stores the ID in @previous_session_id so that #previous_session can later retrieve it, unless the ID refers to the currently active session (i.e., the user re-selected the same session).

Parameters:

  • previous_session_id (Integer, nil) —

    the ID of the session that was active before the transition; nil if no transition occurred



708
709
710
711
712
# File 'lib/ollama_chat/session_management.rb', line 708

def set_previous_session_on_change(previous_session_id)
  if previous_session_id && previous_session_id != session.id
    @previous_session_id = previous_session_id
  end
end

#setup_session ⇒ OllamaChat::Database::Models::Session (private)

Sets up the current session based on command-line options or the last used session.

Returns:



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/ollama_chat/session_management.rb', line 258

def setup_session
  @session = if session_name = @opts[?l]
               choose_session(session_name,  allow_new: true)
             elsif @opts[?n]
               new_session
             else
               preferred_session
             end
  session or abort "No session named #{bold{session_name.inspect}} found."
  if session.lock?
    messages.read_conversation_jsonl(session.messages.to_s)
    @default_persona_name = session.default_persona_name || nil
    session_apply
  else
    feedback(
      "session #{session.id} locked by running process #{session.locked?}",
      type: :warn
    )
    raise OllamaChat::OllamaChatError,
      "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
  end
end

#show_session(output: STDOUT) ⇒ Object (private)

Displays information about the current session.

Parameters:

  • output (IO) (defaults to: STDOUT) —

    the output stream to write the information to (default: STDOUT)



148
149
150
151
152
153
154
# File 'lib/ollama_chat/session_management.rb', line 148

def show_session(output: STDOUT)
  es             = messages.full_estimate_tokens
  messages_count = session.count_messages
  output.puts "#{bold{session.name}} (#{italic{session.id}}), "\
    "#{es.tokens_formatted} (#{es.bytes_formatted}), "\
    "#{messages_count} messages"
end

Persists a collection of links to the session in the database.

This method serializes the links into JSONL format and updates the links attribute of the current session.

Parameters:

  • links (Enumerable) —

    The collection of links to save.



32
33
34
35
36
37
38
39
40
# File 'lib/ollama_chat/session_management.rb', line 32

def store_links_in_session(links)
  output = StringIO.new
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').write_io(
    output:, collection: links
  )
  session.update(links: output.string)
  log(:info, "Links stored in session", data: { session_id: session.id, count: links.count })
  self
end

#store_messages_in_session ⇒ Object

Persists the current conversation messages to the database.

This method serializes the current message list into JSONL format and updates the messages attribute of the current session.



12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/ollama_chat/session_management.rb', line 12

def store_messages_in_session
  output = StringIO.new
  messages.write_conversation_jsonl(output)
  session.update(messages: output.string)
  es = session.estimate_tokens # We just use the formatted bytecount
  log(:info, "Messages stored in session", data: {
    session_id:    session.id,
    size:          es.bytes_formatted,
    context_usage: context_usage_plain,
    messages:      session.count_messages
  })
  self
end

#summarize_conversation(sentence: false) ⇒ Object (private)

Summarizes the conversation and displays / saves the result.

Parameters:

  • sentence (Boolean) (defaults to: false) —

    summarize each message in one sentence (default: false)



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/ollama_chat/session_management.rb', line 430

def summarize_conversation(sentence: false)
  summary = summarize_session(sentence:) do |content|
    infobar.puts kramdown_ansi_parse(content) << ?\n
  end
  if summary.full?
    use_pager do |output|
      output.puts kramdown_ansi_parse(summary)
    end
    if filename = ask_for_filename?(action: 'for summarization') and
        should_overwrite?(filename)
    then
      filename.write(summary)
      feedback("File successfully written.", type: :success)
    else
      feedback("Cancelled.", type: :cancel)
    end
  else
    feedback("Nothing to summarize!", type: :info)
  end
end

#summarize_session(sentence: false, &block) ⇒ String? (private)

Generates a summary of the current session's conversation.

Parameters:

  • sentence (Boolean) (defaults to: false) —

    whether to summarize each message in one sentence (default: false)

  • block (Proc) —

    a block to handle each summary fragment

Returns:

  • (String, nil) —

    the session summary or nil if empty



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/ollama_chat/session_management.rb', line 392

def summarize_session(sentence: false, &block)
  unit                  = sentence ? 'sentence' : 'paragraph'
  contents              = []
  messages_to_summarize = messages.each_message
  messages_to_summarize = messages_to_summarize.with_infobar(
    label:   'Summarizing message',
    total:   messages_to_summarize.count,
    message: infobar_message,
  )
  messages_to_summarize.each do |message|
    message_content = message.content.full?
    unless message_content
      -infobar
      next
    end
    sender_name_output = sender_name_displayed(message)
    sender_name        = sender_name_displayed(message, template: false)
    context            = contents * "\n\n"
    summary            = generate(
      prompt:  prompt(:session_summarize).to_s.
                 named_placeholders_interpolate(
                   { sender_name:, unit:, message_content:, context: }
                 )
    )
    content = '**%s**: %s' % [ sender_name_output, summary ]
    block&.(content)
    contents << content
    +infobar
  end
  contents.empty? and return

  contents.unshift(%{# Summary of session "#{session.name}"})
  contents * "\n\n"
end