class RubyLLM::AgUI::Rails::ActiveRecordStore
The durable run store: run identity in a table instead of a process.
The in-process store is correct for one process only. A host that runs more than one Puma worker, or that restarts, needs the reservation, the interrupts and the terminal status to outlive the worker that produced them, which is what this store is for.
Ordering is by row id, so “the latest run on the thread” means the run reserved last, not the run whose clock reading is largest. Two requests that arrive in the same millisecond still have an order.
RubyLLM::AgUI.configure { |config| config.run_store = ActiveRecordStore.new }
Public Instance Methods
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 37 def find(scope:, thread_id:, run_id:) row_for(scope, thread_id, run_id)&.as_run_record end
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 59 def find_interrupt(scope:, thread_id:, interrupt_id:) raise InvalidInputError, 'interrupt_id is required.' if blank?(interrupt_id) row_for_interrupt(scope, thread_id, interrupt_id)&.as_run_record&.interrupt(interrupt_id) end
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 45 def latest_interrupted(scope:, thread_id:) interrupted = RunRecord::INTERRUPTED.to_s thread_rows(scope, thread_id).reverse.find { |row| row.status == interrupted }&.as_run_record end
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 41 def latest_run(scope:, thread_id:) thread_rows(scope, thread_id).last&.as_run_record end
Records decision on the interrupt under a row lock, so two resumes that arrive together cannot both record a decision and run the tool.
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 67 def redeem_interrupt(scope:, thread_id:, interrupt_id:, decision:, now:) unless Interrupt::DECISIONS.include?(decision) raise ConfigurationError, "decision must be one of #{Interrupt::DECISIONS.join(', ')}" end row = row_for_interrupt(scope, thread_id, interrupt_id) raise ResumeRejectedError, "no interrupt #{interrupt_id}" if row.nil? row.with_lock { redeem(row.reload.as_run_record, interrupt_id, decision, now) } end
Claims (scope, thread_id, run_id) for digest.
The unique index on the identity is the real guard: two workers that insert at the same time cannot both win, and the loser attaches to the row the winner wrote instead of executing the turn a second time.
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 26 def reserve(scope:, thread_id:, run_id:, digest:) scope_key(scope) record = RunRecord.new( scope: scope, thread_id: thread_id, run_id: run_id, digest: digest, created_at: now ) Record.from_run_record(record).save! Reservation.new(record: record, status: Reservation::CREATED) rescue ::ActiveRecord::RecordNotUnique attach(row_for(scope, thread_id, run_id), digest) end
# File lib/ruby_llm/ag_ui/rails/active_record_store.rb, line 50 def save(record) scope_key(record.scope) row = row_for(record.scope, record.thread_id, record.run_id) raise InvalidRunIdentityError, "run #{record.run_id} was never reserved" if row.nil? raise RunConflictError, conflict_message(record) if row.digest != record.digest row.write_run_record(record) end