rails_console_ai 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '083adc4ffd8b69f950fbc4ab18f2ac056e8642c6946c8ba46e67e19e4299e0f2'
4
- data.tar.gz: a01fef0fe45dfa157d638a7d4edb1ee5c8d9753be3a79956b3ebde220d3ee939
3
+ metadata.gz: 8af25f720aae1fe703b947bdc860dcace9a13e2e1db61d9d698634fa46838e59
4
+ data.tar.gz: efc3b4c00ea66d941e5fb03c61543398fed5ffdde700c44c080ee5e8c8e09ad6
5
5
  SHA512:
6
- metadata.gz: 56b8fdcd0cf445d9480d4e56e252271241af017c8ca51e2a11af36d2158ce4a3a1fdf9670685f931b8cbd0994e6828e6bac08d10247e8bec1af8c66ad83c47f4
7
- data.tar.gz: 6ccb72b8b8e9b157b28722d1e532d2abf5df8037f4ba4f1f3635bb37ad8a3441e48d832d903f6f92b6dce80106082215368dde15626eb0fc41d93da60100f261
6
+ metadata.gz: 0b8de788b6391a303815cd160fb57ce4300c33dfd3f66645ce31f9bb256de0922b7ce8d26fd05c23dc8802c80da6201ffb9935c89cfd82b94ebde609b1631733
7
+ data.tar.gz: 5c73d678909c5e950cff5191b506a72570c2eb50bd2d6f01602bfe63ec2fade56c94b88d91f6d487803b073abd7b3565c9b92c355f90f7e32be82e465b72b7f9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.33.0]
6
+
7
+ - Support Claude Opus 5 and make it the default model for the Anthropic and Bedrock providers
8
+ - Add runaway-loop circuit breakers to the tool loop — break when the same error signature repeats (warn at 3 occurrences, stop at 5) or when a token budget is exceeded (nudge at 500K input tokens, force a final answer at 1M), with a tailored wrap-up prompt for each break reason
9
+ - Fail fast on known environment errors: a built-in hint recognizes decryption key misconfiguration (`bad decrypt` / `ActiveRecord::Encryption`) and tells the model not to retry, and apps can register their own hints via `config.error_hints`
10
+
11
+ ## [0.32.0]
12
+
13
+ - Switch default models to Claude Sonnet 5 and Opus 4.8, and rework pricing to match models by family so dated snapshots and Bedrock IDs are covered
14
+ - Add an abort mechanism for background sub-agents
15
+ - Require human approval before AI-proposed memory changes take effect
16
+ - Keep `explore_output` focused on exploring a previous command's output instead of spinning off into database reads
17
+ - Fix loading `ApplicationController` under the classic autoloader
18
+
5
19
  ## [0.31.0]
6
20
 
7
21
  - Simplify DB-backed skills, sub-agents, and memories to a single content field per record, replacing the separate frontmatter/body columns and streamlining the models, controllers, and forms
@@ -22,32 +36,6 @@ All notable changes to this project will be documented in this file.
22
36
  - Harden `Skill` / `Agent` model accessors to return safe defaults when newly added columns are not yet present
23
37
  - Fix built-in agent `.md` files with UTF-8 characters failing to load under US-ASCII locales
24
38
 
25
- ## [Unreleased]
26
-
27
- - New skill / memory / agent pages now have a "Paste a .md file" textarea at the top. Paste the contents of a Markdown file with YAML frontmatter (same format used by `.rails_console_ai/{skills,memories,agents}/*.md` and the gem's built-in agents), click "Parse pasted content ↓", and the form fields below are prefilled from the parse. You still click Create to actually save, so the normal proposed-status + version-row flow applies — and you can tweak the parsed content before saving. Useful for moving an existing on-disk record into the versioned DB store
28
- - `RailsConsoleAi::SkillLoader.parse`, `RailsConsoleAi::AgentLoader.parse`, and `RailsConsoleAi::Tools::MemoryTools.parse` are now public class methods. They return a parsed frontmatter+body hash, or `nil` on malformed input
29
-
30
- - Usage tracking for DB-backed skills, memories, and agents. New `use_count` and `last_used_at` columns are bumped atomically (one SQL `UPDATE … SET use_count = use_count + 1, last_used_at = NOW()`, no callbacks, no `updated_at` change) when:
31
- - `activate_skill` resolves a DB skill
32
- - `recall_memory` resolves a DB memory, or `recall_memories` returns a DB memory in its result set
33
- - `delegate_task` resolves a DB sub-agent
34
- File / built-in records have no DB row so they're not tracked (the web UI shows `—` for them). System-prompt summary inclusion does **not** count — counters only move when the AI actively invokes/loads the record. Surfaced in the index tables (with a "Sort: most used" toggle) and on each show page (Times activated / Times recalled / Times invoked, plus Last used)
35
- - Bugfix: `RailsConsoleAi.migrate!` now always re-runs the `setup_*_tables!` methods so column-add probes execute on upgrades. Previously, when the base table already existed, the outer `unless table_exists?` guard skipped column probes entirely, leaving new columns un-added and causing `NameError: undefined local variable or method 'status'` from `Skill#proposed?` on host apps that hadn't fully migrated
36
- - Bugfix: `Skill` / `Agent` model accessors for `status` / `approved_by` / `approved_at` / `use_count` / `last_used_at` are now defensive — they return safe defaults via `has_attribute?` rather than raising NameError when the column hasn't been added yet. The `status` inclusion validation is also gated on the column being present
37
-
38
- - Sub-agents can now be stored in the database, versioned, and approved through the web UI — same workflow as skills. DB-backed agents start as `proposed` and are invisible to `delegate_task` until a human clicks Approve at `/rails_console_ai/agents`; editing an approved agent reverts it to proposed. Built-in (gem-shipped) and file-based agents are pre-approved and continue working unchanged
39
- - New AI tools `save_agent` and `delete_agent` — the AI can now draft a sub-agent definition (lands as proposed, awaits human approval) and request deletion. `delegate_task` now emits a specific "awaiting human approval" error when the AI references a proposed DB agent
40
- - New web UI section at `/rails_console_ai/agents` with three-source listing (DB / FILE / BUILTIN badges) plus the full skills-style CRUD + version history + diff + restore + approve. Built-in agents are read-only with a "Create DB override" link that prefills the new-agent form
41
- - `ai_db_setup` / `ai_db_migrate` now also create `rails_console_ai_agents` and `rails_console_ai_agent_versions` tables idempotently
42
- - Fix: built-in agent .md files containing UTF-8 (em-dashes, smart quotes) now load correctly. `safe_load_builtin_agents` was using `File.read`'s locale-default encoding, which silently swallowed `Encoding::CompatibilityError` on US-ASCII locales
43
-
44
- - Skills and memories can now be stored in the database (in addition to the existing on-disk `.rails_console_ai/skills` and `.rails_console_ai/memories` files). DB-backed records are versioned — every save creates a `SkillVersion` / `MemoryVersion` row with `edited_by` and an optional change note
45
- - DB-backed skills start in a **proposed** state and cannot be activated by the AI until a human approves them in the web UI. Editing an approved skill reverts it to proposed. File-backed skills are unaffected (already git-tracked, considered pre-approved). Memories are not gated
46
- - `SkillLoader#load_all_skills` and `MemoryTools#load_all_memories` now return the union of DB and file records (DB wins on name collision). `SkillLoader#find_skill` / `skill_summaries` filter out proposed skills so the AI never sees them
47
- - `save_skill` / `save_memory` AI tools accept an optional `target` parameter (`"db"` default, `"file"` to write to disk) plus `change_note`. When the AI saves a DB skill, the tool response tells it the skill is awaiting human approval
48
- - New web UI sections at `/rails_console_ai/skills` and `/rails_console_ai/memories` provide list, view, create, edit, delete, version history, side-by-side diff, and restore. Skills also have an Approve button and PROPOSED / APPROVED badges. File-sourced records are surfaced but read-only in the UI
49
- - `ai_db_setup` / `ai_db_migrate` now create the new `rails_console_ai_skills`, `rails_console_ai_skill_versions`, `rails_console_ai_memories`, and `rails_console_ai_memory_versions` tables idempotently, plus the `status` / `approved_by` / `approved_at` columns on skills
50
-
51
39
  ## [0.29.0]
52
40
 
53
41
  - Allow steering Slack conversations mid-run by sending follow-up messages that are folded in as user guidance at the next tool-loop boundary
data/README.md CHANGED
@@ -291,7 +291,7 @@ RailsConsoleAi.configure do |config|
291
291
  end
292
292
  ```
293
293
 
294
- Default model: `claude-sonnet-4-6`. Thinking model: `claude-opus-4-6`. Prompt caching is enabled automatically.
294
+ Default model: `claude-sonnet-5`. Thinking model: `claude-opus-5`. Prompt caching is enabled automatically.
295
295
 
296
296
  ### OpenAI
297
297
 
@@ -317,8 +317,8 @@ gem 'aws-sdk-bedrockruntime'
317
317
  RailsConsoleAi.configure do |config|
318
318
  config.provider = :bedrock
319
319
  config.bedrock_region = 'us-east-1'
320
- # config.model = 'us.anthropic.claude-sonnet-4-6' # default
321
- # config.thinking_model = 'us.anthropic.claude-opus-4-6-v1' # default
320
+ # config.model = 'us.anthropic.claude-sonnet-5' # default
321
+ # config.thinking_model = 'us.anthropic.claude-opus-5' # default
322
322
  end
323
323
  ```
324
324
 
@@ -365,25 +365,25 @@ Before adopting a new Claude model, smoke-test it against the Anthropic or Bedro
365
365
 
366
366
  ```bash
367
367
  # Anthropic — provider inferred from the `claude-` prefix
368
- ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-7
368
+ ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-8
369
369
 
370
370
  # Bedrock — provider inferred from the regional `us.anthropic.` prefix.
371
371
  # Requires the aws-sdk-bedrockruntime gem and AWS credentials in the environment.
372
- bin/smoke_model.rb --model us.anthropic.claude-opus-4-7
372
+ bin/smoke_model.rb --model us.anthropic.claude-opus-4-8
373
373
 
374
374
  # Bedrock in another region
375
- bin/smoke_model.rb --model eu.anthropic.claude-opus-4-7 --region eu-west-1
375
+ bin/smoke_model.rb --model eu.anthropic.claude-opus-4-8 --region eu-west-1
376
376
 
377
377
  # Subset of checks, e.g. when iterating on cache behavior
378
- bin/smoke_model.rb --model claude-sonnet-4-6 --checks cache
378
+ bin/smoke_model.rb --model claude-sonnet-5 --checks cache
379
379
 
380
380
  # Force a provider when the model ID is ambiguous
381
- bin/smoke_model.rb --provider anthropic --model claude-opus-4-7
381
+ bin/smoke_model.rb --provider anthropic --model claude-opus-4-8
382
382
  ```
383
383
 
384
384
  `DEBUG=1` enables the providers' raw request/response logging.
385
385
 
386
- If the model rejects a parameter the gem sends by default (e.g. opus-4-7 deprecated `temperature`), add the model ID to `Configuration::MODELS_WITHOUT_TEMPERATURE` in `lib/rails_console_ai/configuration.rb` so the providers omit the field.
386
+ Pricing, default max tokens, and parameter support (e.g. which families reject `temperature`) are keyed by model family in `Configuration::MODEL_FAMILIES` (`lib/rails_console_ai/configuration.rb`). Family matching is by substring, so one entry covers bare IDs, dated snapshots, and Bedrock inference profiles (`us.` / `global.` prefixes). When adopting a new model family, add an entry there and smoke-test it.
387
387
 
388
388
  ## Configuration
389
389
 
@@ -396,9 +396,48 @@ RailsConsoleAi.configure do |config|
396
396
  config.timeout = 30 # HTTP timeout in seconds
397
397
  config.max_tool_rounds = 200 # safety cap on tool-use loops
398
398
  config.code_search_paths = %w[app] # directories for list_files / search_code
399
+
400
+ # Runaway-loop circuit breakers (see "Runaway sessions" below)
401
+ config.token_nudge_threshold = 500_000 # input tokens in one tool loop → nudge model to wrap up (nil disables)
402
+ config.token_stop_threshold = 1_000_000 # input tokens in one tool loop → force a final answer (nil disables)
399
403
  end
400
404
  ```
401
405
 
406
+ ### Runaway sessions & known-issue hints
407
+
408
+ Two mechanisms stop the LLM from burning tokens on a dead end:
409
+
410
+ **Known-issue hints** — when executed code fails (or prints a rescued error) matching a
411
+ known environment-level problem, the tool result includes explicit guidance telling the
412
+ model not to retry. The built-in hint recognizes decryption failures
413
+ (`OpenSSL::Cipher::CipherError` / "bad decrypt" / `ActiveRecord::Encryption` errors),
414
+ which indicate a missing or placeholder encryption key in the console process — an
415
+ environment issue no amount of retrying can fix. Repeat occurrences escalate the message.
416
+ Apps can add their own:
417
+
418
+ ```ruby
419
+ RailsConsoleAi.configure do |config|
420
+ config.error_hints << {
421
+ name: :vpn_required,
422
+ pattern: /Errno::ECONNREFUSED.*10\.8\./,
423
+ hint: "This host is only reachable over the VPN, which this console does not have. " \
424
+ "Do not retry; report the limitation to the user."
425
+ }
426
+ end
427
+ ```
428
+
429
+ **Circuit breakers** — inside a single tool loop, the engine tracks:
430
+
431
+ - *Identical tool calls* (same tool + same args): warns at 3, breaks at 5.
432
+ - *Repeated error signatures* (same normalized error from **different** code): warns at 3,
433
+ breaks at 5. This catches "new code, same dead end" loops that identical-call detection
434
+ misses. Digits are normalized so varying record IDs don't disguise the same failure.
435
+ - *Token budget*: at `token_nudge_threshold` cumulative input tokens the model is told to
436
+ wrap up; at `token_stop_threshold` the loop is stopped and a final answer is forced.
437
+
438
+ When any breaker trips, the model is asked to summarize what it established, what it
439
+ could not determine and why, and what a human should do next — instead of iterating.
440
+
402
441
  ### Code Search Paths
403
442
 
404
443
  By default, `list_files` and `search_code` only look in `app/`. If your project has code in other directories (e.g. a frontend in `public/portal`, or shared code in `lib`), add them:
@@ -513,12 +552,17 @@ id = RailsConsoleAi.run_agent("How many users signed up yesterday?", name: 'dail
513
552
  # => 4821 (Integer session id, returned immediately)
514
553
 
515
554
  RailsConsoleAi.check_agent(id)
516
- # => 'queued' | 'running' | 'ready' | 'failed' | nil
555
+ # => 'queued' | 'running' | 'ready' | 'failed' | 'aborted' | nil
517
556
 
518
557
  RailsConsoleAi.get_agent_response(id)
519
558
  # => { status: 'ready', result: "1,432 users signed up yesterday.\n", error: nil }
559
+
560
+ RailsConsoleAi.abort_agent(id)
561
+ # => true (aborted) | false (already finished, or unknown id)
520
562
  ```
521
563
 
564
+ `abort_agent` cancels a run: a queued run is never picked up, and a run already executing keeps going but its result is discarded when it completes -- the session stays `status='aborted'`.
565
+
522
566
  `run_agent` enqueues a row in the sessions table with `mode='agent_api'` and `status='queued'`. A separate long-running rake task picks them up and runs each in its own thread using the same engine that powers `ai "..."` in the console.
523
567
 
524
568
  ### Per-run options
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class AgentVersionsController < ApplicationController
2
+ class AgentVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_agent
4
4
 
5
5
  def index
@@ -1,7 +1,7 @@
1
1
  require 'rails_console_ai/agent_loader'
2
2
 
3
3
  module RailsConsoleAi
4
- class AgentsController < ApplicationController
4
+ class AgentsController < RailsConsoleAi::ApplicationController
5
5
  before_action :load_agent, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
@@ -1,8 +1,8 @@
1
1
  require 'rails_console_ai/tools/memory_tools'
2
2
 
3
3
  module RailsConsoleAi
4
- class MemoriesController < ApplicationController
5
- before_action :load_memory, only: [:show, :edit, :update, :destroy]
4
+ class MemoriesController < RailsConsoleAi::ApplicationController
5
+ before_action :load_memory, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
8
8
  @memories = Tools::MemoryTools.new.load_all_memories
@@ -73,6 +73,26 @@ module RailsConsoleAi
73
73
  end
74
74
  end
75
75
 
76
+ def approve
77
+ redirect_to memories_path, alert: file_memory_message and return unless @memory.is_a?(RailsConsoleAi::Memory)
78
+
79
+ approver = params[:approved_by].presence ||
80
+ (request.respond_to?(:remote_user) && request.remote_user.presence) ||
81
+ 'web'
82
+
83
+ if @memory.approved?
84
+ redirect_to memory_path(@memory), notice: 'Memory is already approved.'
85
+ return
86
+ end
87
+
88
+ begin
89
+ @memory.approve!(approved_by: approver)
90
+ redirect_to memory_path(@memory), notice: "Approved by #{approver}. The AI can now recall this memory."
91
+ rescue ArgumentError, ActiveRecord::RecordInvalid => e
92
+ redirect_to memory_path(@memory), alert: "Could not approve: #{e.message}"
93
+ end
94
+ end
95
+
76
96
  def diff
77
97
  @memory = Memory.find(params[:memory_id])
78
98
  @from = @memory.versions.find(params[:from])
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class MemoryVersionsController < ApplicationController
2
+ class MemoryVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_memory
4
4
 
5
5
  def index
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class SessionsController < ApplicationController
2
+ class SessionsController < RailsConsoleAi::ApplicationController
3
3
  PER_PAGE = 50
4
4
 
5
5
  def index
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class SkillVersionsController < ApplicationController
2
+ class SkillVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_skill
4
4
 
5
5
  def index
@@ -1,7 +1,7 @@
1
1
  require 'rails_console_ai/skill_loader'
2
2
 
3
3
  module RailsConsoleAi
4
- class SkillsController < ApplicationController
4
+ class SkillsController < RailsConsoleAi::ApplicationController
5
5
  before_action :load_skill, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
@@ -4,6 +4,10 @@ module RailsConsoleAi
4
4
  class Memory < ActiveRecord::Base
5
5
  self.table_name = 'rails_console_ai_memories'
6
6
 
7
+ STATUS_PROPOSED = 'proposed'.freeze
8
+ STATUS_APPROVED = 'approved'.freeze
9
+ STATUSES = [STATUS_PROPOSED, STATUS_APPROVED].freeze
10
+
7
11
  has_many :versions,
8
12
  -> { order(created_at: :desc) },
9
13
  class_name: 'RailsConsoleAi::MemoryVersion',
@@ -12,11 +16,14 @@ module RailsConsoleAi
12
16
 
13
17
  validates :content, presence: true
14
18
  validates :name, presence: true, uniqueness: { case_sensitive: false }
19
+ validates :status, inclusion: { in: STATUSES }
15
20
  validate :content_parses
16
21
 
17
22
  before_validation :sync_name_from_content
18
23
 
19
24
  scope :alphabetical, -> { order(Arel.sql('LOWER(name)')) }
25
+ scope :approved, -> { where(status: STATUS_APPROVED) }
26
+ scope :proposed, -> { where(status: STATUS_PROPOSED) }
20
27
 
21
28
  def self.connection
22
29
  klass = RailsConsoleAi.configuration.connection_class
@@ -42,6 +49,9 @@ module RailsConsoleAi
42
49
  def description; parsed['description']; end
43
50
  def tags; Array(parsed['tags']); end
44
51
 
52
+ def proposed?; status.to_s == STATUS_PROPOSED; end
53
+ def approved?; status.to_s == STATUS_APPROVED; end
54
+
45
55
  def self.record_use!(id)
46
56
  where(id: id).update_all([
47
57
  'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
@@ -60,6 +70,9 @@ module RailsConsoleAi
60
70
  'description' => description,
61
71
  'tags' => tags,
62
72
  'content' => content,
73
+ 'status' => status,
74
+ 'approved_by' => approved_by,
75
+ 'approved_at' => approved_at,
63
76
  'use_count' => use_count,
64
77
  'last_used_at' => last_used_at,
65
78
  'source' => :db,
@@ -67,14 +80,25 @@ module RailsConsoleAi
67
80
  }
68
81
  end
69
82
 
70
- def update_with_version!(attrs, edited_by: nil, change_note: nil)
83
+ # Assigns attrs, saves, and records one MemoryVersion snapshot.
84
+ # Any change to `content` reverts approval back to "proposed" unless
85
+ # `preserve_approval: true` is passed (approve! does this).
86
+ def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
71
87
  transaction do
72
88
  assign_attributes(attrs)
89
+
90
+ if !preserve_approval && approved? && changes.key?('content')
91
+ self.status = STATUS_PROPOSED
92
+ self.approved_by = nil
93
+ self.approved_at = nil
94
+ end
95
+
73
96
  save!
74
97
  RailsConsoleAi::MemoryVersion.create!(
75
98
  memory_id: id,
76
99
  name: name,
77
100
  content: content,
101
+ status: status,
78
102
  edited_by: edited_by,
79
103
  change_note: change_note
80
104
  )
@@ -82,6 +106,21 @@ module RailsConsoleAi
82
106
  self
83
107
  end
84
108
 
109
+ def approve!(approved_by:)
110
+ raise ArgumentError, 'approved_by is required' if approved_by.to_s.strip.empty?
111
+
112
+ update_with_version!(
113
+ {
114
+ status: STATUS_APPROVED,
115
+ approved_by: approved_by,
116
+ approved_at: Time.now.utc
117
+ },
118
+ edited_by: approved_by,
119
+ change_note: "Approved by #{approved_by}",
120
+ preserve_approval: true
121
+ )
122
+ end
123
+
85
124
  private
86
125
 
87
126
  def sync_name_from_content
@@ -11,6 +11,13 @@
11
11
  </form>
12
12
  </div>
13
13
 
14
+ <% proposed_count = @memories.count { |m| m['source'] == :db && m['status'] != 'approved' } %>
15
+ <% if proposed_count > 0 %>
16
+ <div class="flash flash-alert" style="margin-bottom:16px;">
17
+ <strong><%= proposed_count %></strong> memor<%= proposed_count == 1 ? 'y' : 'ies' %> awaiting approval. The AI cannot recall them until a human approves.
18
+ </div>
19
+ <% end %>
20
+
14
21
  <% if @memories.empty? %>
15
22
  <div class="meta-card">
16
23
  <p class="text-muted">No memories yet. <%= link_to 'Create one', new_memory_path %> or drop a Markdown file in <code>.rails_console_ai/memories/</code>.</p>
@@ -22,7 +29,7 @@
22
29
  <th>Name</th>
23
30
  <th>Description</th>
24
31
  <th>Tags</th>
25
- <th>Source</th>
32
+ <th>Source / Status</th>
26
33
  <th title="Number of times the AI recalled this memory (DB only)">Uses</th>
27
34
  <th>Last used</th>
28
35
  <th></th>
@@ -40,6 +47,11 @@
40
47
  <td>
41
48
  <% if m['source'] == :db %>
42
49
  <span class="badge source-db">DB</span>
50
+ <% if m['status'] == 'approved' %>
51
+ <span class="badge status-approved">APPROVED</span>
52
+ <% else %>
53
+ <span class="badge status-proposed">PROPOSED</span>
54
+ <% end %>
43
55
  <% else %>
44
56
  <span class="badge source-file">FILE</span>
45
57
  <% end %>
@@ -8,11 +8,23 @@
8
8
 
9
9
  <%= link_to '← All memories', memories_path, class: 'back-link' %>
10
10
 
11
+ <% if is_db && !@memory.approved? %>
12
+ <div class="flash flash-alert" style="margin-bottom:16px;">
13
+ <strong>Awaiting approval.</strong> The AI cannot recall this memory until a human approves it.
14
+ Review the content below, then click <strong>Approve</strong>.
15
+ </div>
16
+ <% end %>
17
+
11
18
  <div class="meta-card">
12
19
  <h2 style="margin-bottom:8px;">
13
20
  <%= name %>
14
21
  <% if is_db %>
15
22
  <span class="badge source-db">DB</span>
23
+ <% if @memory.approved? %>
24
+ <span class="badge status-approved">APPROVED</span>
25
+ <% else %>
26
+ <span class="badge status-proposed">PROPOSED</span>
27
+ <% end %>
16
28
  <% else %>
17
29
  <span class="badge source-file">FILE</span>
18
30
  <% end %>
@@ -24,6 +36,10 @@
24
36
  <div class="meta-item"><label>Versions</label><span><%= @memory.versions.count %></span></div>
25
37
  <div class="meta-item"><label>Times recalled</label><span><%= @memory.use_count %></span></div>
26
38
  <div class="meta-item"><label>Last recalled</label><span><%= @memory.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
39
+ <% if @memory.approved? %>
40
+ <div class="meta-item"><label>Approved by</label><span><%= @memory.approved_by || '—' %></span></div>
41
+ <div class="meta-item"><label>Approved at</label><span><%= @memory.approved_at&.strftime('%Y-%m-%d %H:%M') || '—' %></span></div>
42
+ <% end %>
27
43
  <% else %>
28
44
  <div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
29
45
  <% end %>
@@ -32,6 +48,12 @@
32
48
 
33
49
  <div class="btn-bar">
34
50
  <% if is_db %>
51
+ <% unless @memory.approved? %>
52
+ <%= form_with url: approve_memory_path(@memory), method: :post, local: true, style: 'display:inline' do %>
53
+ <input type="text" name="approved_by" placeholder="Your name (required)" style="padding:7px 10px; border:1px solid #ccc; border-radius:4px; margin-right:6px;">
54
+ <button type="submit" class="btn btn-danger" data-confirm="Approve this memory? The AI will then be able to recall it.">Approve</button>
55
+ <% end %>
56
+ <% end %>
35
57
  <%= link_to 'Edit', edit_memory_path(@memory), class: 'btn' %>
36
58
  <%= link_to 'Versions', memory_versions_path(@memory), class: 'btn btn-secondary' %>
37
59
  <%= button_to 'Delete', memory_path(@memory), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' },data: { confirm: 'Delete this memory? Past versions remain in history.' } %>
@@ -49,6 +71,7 @@
49
71
  <div>
50
72
  <strong>v<%= v.id %></strong>
51
73
  <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
74
+ <% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span><% end %>
52
75
  <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
53
76
  </div>
54
77
  <div>
data/config/routes.rb CHANGED
@@ -17,6 +17,9 @@ RailsConsoleAi::Engine.routes.draw do
17
17
  end
18
18
 
19
19
  resources :memories do
20
+ member do
21
+ post :approve
22
+ end
20
23
  collection do
21
24
  get :diff
22
25
  end
@@ -5,11 +5,11 @@ RailsConsoleAi.configure do |config|
5
5
  # API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
6
6
  # config.api_key = 'sk-...'
7
7
 
8
- # Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
9
- # config.model = 'claude-opus-4-6'
8
+ # Model override (defaults: claude-sonnet-5 for Anthropic, gpt-5.3-codex for OpenAI)
9
+ # config.model = 'claude-opus-4-8'
10
10
 
11
- # Max tokens for LLM response
12
- config.max_tokens = 4096
11
+ # Max tokens for LLM response (default resolves per model family; 16000 for Claude models)
12
+ # config.max_tokens = 4096
13
13
 
14
14
  # Temperature (0.0 - 1.0)
15
15
  config.temperature = 0.2
@@ -35,7 +35,7 @@ RailsConsoleAi.configure do |config|
35
35
  # AWS Bedrock provider (uses AWS credential chain — no API key needed):
36
36
  # config.provider = :bedrock
37
37
  # config.bedrock_region = 'us-east-1'
38
- # config.model = 'us.anthropic.claude-sonnet-4-6'
38
+ # config.model = 'us.anthropic.claude-sonnet-5'
39
39
 
40
40
  # Debug mode: prints full API requests/responses and tool calls to stderr
41
41
  # config.debug = true
@@ -134,6 +134,10 @@ module RailsConsoleAi
134
134
  end
135
135
  result_text = compose_result(channel.captured_output, exec_result)
136
136
 
137
+ if aborted?(session.id)
138
+ puts ">> aborted (result discarded)"
139
+ return
140
+ end
137
141
  SessionLogger.update(session.id, status: 'ready', result: result_text)
138
142
 
139
143
  elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
@@ -142,16 +146,23 @@ module RailsConsoleAi
142
146
  puts ">> ready (#{elapsed}ms) #{preview}"
143
147
  rescue RunnerTimeoutError => e
144
148
  warn ">> TIMEOUT #{e.message}"
145
- SessionLogger.update(session.id, status: 'failed', error_message: e.message)
149
+ SessionLogger.update(session.id, status: 'failed', error_message: e.message) unless aborted?(session.id)
146
150
  rescue => e
147
151
  warn ">> FAILED #{e.class}: #{e.message}"
148
152
  e.backtrace&.first(5)&.each { |line| warn " #{line}" }
153
+ return if aborted?(session.id)
149
154
  SessionLogger.update(session.id,
150
155
  status: 'failed',
151
156
  error_message: "#{e.class}: #{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
152
157
  )
153
158
  end
154
159
 
160
+ # An abort (RailsConsoleAi.abort_agent) flips the row to 'aborted' while we
161
+ # run; in that case the run's outcome must not overwrite the abort.
162
+ def aborted?(session_id)
163
+ Session.where(id: session_id).pluck(:status).first == 'aborted'
164
+ end
165
+
155
166
  # When cap is nil or non-positive, run inline. Otherwise spawn a nested
156
167
  # worker thread, join with the deadline, and kill + raise on overshoot.
157
168
  # Kept localized (vs Timeout.timeout) so a runaway provider call can't
@@ -368,7 +368,7 @@ module RailsConsoleAi
368
368
  config = RailsConsoleAi.configuration
369
369
  model = @engine.effective_model
370
370
  thinking = config.resolved_thinking_model
371
- pricing = Configuration::PRICING[model]
371
+ pricing = Configuration.pricing_for(model)
372
372
 
373
373
  @real_stdout.puts "\e[36m Model info:\e[0m"
374
374
  @real_stdout.puts "\e[2m Provider: #{config.provider}\e[0m"