# File lib/graceful_quit.rb, line 59 def primary_channel(bot) Array(bot.config.channels).map(&:to_s).reject(&:empty?).first end
module GracefulQuit
Constants
- AI_TIMEOUT_SEC
- CONTEXT_LINES
- GRACE_DELAY_SEC
- MAX_MESSAGE_LENGTH
- QUIT_INSTRUCTION
Public Instance Methods
Source
# File lib/graceful_quit.rb, line 124 def ai_timeout_sec value = ENV.fetch("RABBOT_QUIT_AI_TIMEOUT_SEC", AI_TIMEOUT_SEC).to_f value.positive? ? value : AI_TIMEOUT_SEC end
Source
# File lib/graceful_quit.rb, line 63 def build_quit_prompt(bot:, channel:, context_lines:) parts = [] profile = bot.config.shared[:character_profile] profile_prompt = AiAgent.render_profile_prompt(profile) parts << profile_prompt unless profile_prompt.to_s.strip.empty? parts << QUIT_INSTRUCTION parts << "Bot nick: #{bot.nick}" parts << "Channel: #{channel}" if context_lines.empty? parts << "Recent channel chat: (none recorded)" else parts << "Recent channel chat (oldest to newest):" parts.concat(context_lines) end AiGuard.guard_prompt(parts.join("\n")) end
Source
# File lib/graceful_quit.rb, line 41 def clear! @mutex.synchronize do @requested = false @performed = false end end
Source
# File lib/graceful_quit.rb, line 107 def default_runner(bot) channel = primary_channel(bot) lambda do |prompt| model = AiModel.resolve( network: bot.config.server, channel: channel, yaml_default: bot.config.shared[:ai_model], fallback: AiModel::DEFAULT ) AiAgent.run_agent(prompt, model: model) end end
Source
# File lib/graceful_quit.rb, line 53 def fallback_message(bot) nick = bot.respond_to?(:nick) ? bot.nick.to_s.strip : "Bot" nick = "Bot" if nick.empty? "#{nick} signing off — back soon" end
Source
# File lib/graceful_quit.rb, line 80 def generate_quit_message(bot, runner: default_runner(bot)) return nil if skip_ai? channel = primary_channel(bot) return nil if channel.nil? || channel.empty? network = bot.config.server context_lines = RoomReader.channel_context_lines( network: network, channel: channel, limit: CONTEXT_LINES ) prompt = build_quit_prompt(bot: bot, channel: channel, context_lines: context_lines) result = Timeout.timeout(ai_timeout_sec) { runner.call(prompt) } return nil unless result.respond_to?(:success?) && result.success? text = sanitize_message(result.text) return nil if text.empty? return nil if AiGuard.system_request?(text) text rescue Timeout::Error nil rescue StandardError nil end
Source
# File lib/graceful_quit.rb, line 129 def grace_delay_sec value = ENV.fetch("RABBOT_QUIT_GRACE_SEC", GRACE_DELAY_SEC).to_f value.positive? ? value : GRACE_DELAY_SEC end
Source
# File lib/graceful_quit.rb, line 134 def perform!(bot, message: nil, runner: nil) should_run = @mutex.synchronize do next false if @performed @performed = true true end return false unless should_run quit_message = message.to_s.strip.empty? ? nil : sanitize_message(message) quit_message = generate_quit_message(bot, runner: runner || default_runner(bot)) if quit_message.nil? quit_message = fallback_message(bot) if quit_message.to_s.strip.empty? if bot.respond_to?(:quit) bot.quit(quit_message) sleep grace_delay_sec end true rescue StandardError => e warn "[rabbot] graceful quit failed: #{e.class}: #{e.message}" begin bot.quit(fallback_message(bot)) if bot.respond_to?(:quit) rescue StandardError nil end false end
Source
# File lib/graceful_quit.rb, line 37 def performed? @mutex.synchronize { @performed } end
Source
Source
# File lib/graceful_quit.rb, line 28 def request! # Signal traps cannot use Mutex#synchronize (ThreadError in trap context). @requested = true end
Source
# File lib/graceful_quit.rb, line 33 def requested? @mutex.synchronize { @requested } end
Source
# File lib/graceful_quit.rb, line 48 def sanitize_message(message) text = message.to_s.gsub(/[\x00-\x1f]/, "").squeeze(" ").strip text[0, MAX_MESSAGE_LENGTH] end
Source
# File lib/graceful_quit.rb, line 120 def skip_ai? ENV["RABBOT_SKIP_AI_QUIT"].to_s == "1" end