rubik 0.3.2 → 0.3.3

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: fc6b8b3ff7cb67e302bbb3a74237615515318acde17b8c41193a293dadef7576
4
- data.tar.gz: 3c0616b8f9b6544619903234c4595242b95c1998b2602aca584334eaf19fcc5d
3
+ metadata.gz: 29ee7f0d84218a3bf3d2fe0699c2c37d7f260f6406231a46338f169410ae5c60
4
+ data.tar.gz: a600c600071a3267b8ab4cdfd0b2f755a2068f2cd8289949ae57e41383355d1b
5
5
  SHA512:
6
- metadata.gz: e714d3f5fb612958caba5ed92829af4262b55ac50390d5ed9262cdb8c81143886d613424c13f1fe585c4debead0758469b4aeadea855edcc02d14ef5b9eb5d9b
7
- data.tar.gz: cef0f0645353fe8c6f4413c39f6adff50c810c51720022774db779bce54de99a2555396f211a62af336be0be5c4a534f1c14c616a180c3c3344790ca56235772
6
+ metadata.gz: fabc3ee65ba68ea6244727ba265232c1b05ac2bd06eee192a376dbfc6b6da746070bac6f129b750011803c2945f092096b5cd122065dd79bde224fcf9018892f
7
+ data.tar.gz: 30313695e55c4ae537d9d3acb27b1dbc7365d6db817e93b487e7612ce7c2921235deb7e92db6b2232e33b71569b65e1190f9c8c948bef8090fce093412fcc14c
data/CHANGELOG.md CHANGED
@@ -10,6 +10,35 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
 
11
11
  Nothing yet.
12
12
 
13
+ ## [0.3.3] - 2026-10-21
14
+
15
+ A field-incident patch: subagent runs get a lifetime step ceiling,
16
+ budget grants stop lying about what they applied, and execution
17
+ journals keep their terminal outcomes after truncation.
18
+
19
+ ### Fixed
20
+
21
+ - **Subagent runs have a lifetime step ceiling (#219):** a live
22
+ incident showed one subagent at 950 model steps, 1,004 tool calls
23
+ and 10 budget grants — repeated grants could extend a run forever.
24
+ The new `subagents.max_total_steps` ceiling (default 256) bounds
25
+ the initial allocation plus every grant; grants that would exceed
26
+ it are truncated to the remaining room and partial or rejected
27
+ grants are reported as such, so the REPL and `manage_subagents`
28
+ echo the number that actually applied.
29
+ - **Execution journals keep their terminal outcomes (#219):** when a
30
+ journal hits its byte cap, truncation now preserves the terminal
31
+ `run_finished` and `cancelled` records instead of cutting exactly
32
+ the facts a post-mortem needs; the default per-execution journal
33
+ cap rises from 2 MiB to 8 MiB (`subagents.journal.max_bytes`).
34
+
35
+ ### Changed
36
+
37
+ - **Bare `rescue` clauses are explicit `rescue StandardError`
38
+ (#218):** standardization across platform, process, sanitize,
39
+ config store and the interrupt handler — same semantics, harder to
40
+ misread.
41
+
13
42
  ## [0.3.2] - 2026-10-21
14
43
 
15
44
  A reliability patch: LSP requests tell timeouts and crashes apart and
@@ -142,6 +142,7 @@ module Rubik
142
142
  "max_concurrency" => 10,
143
143
  "max_depth" => 2,
144
144
  "default_max_steps" => 88,
145
+ "max_total_steps" => 256, # Hard per-run ceiling across the initial allocation and all grants.
145
146
  "provider_concurrency" => 2,
146
147
  "budget_wait_timeout" => 180,
147
148
  "auto_apply_buffer" => true,
@@ -156,7 +157,7 @@ module Rubik
156
157
  # Sonuç arşivi (issue #122): tamamlanan koşular oturum-içi saklanır; redeploy öncekini EZMEZ.
157
158
  "result_archive" => { "ttl_seconds" => 3600, "max_runs_per_instance" => 8, "page_chars" => 4000 },
158
159
  # Append-only execution journal (issue #124): per-execution JSONL, capped per file.
159
- "journal" => { "max_bytes" => 2_097_152 },
160
+ "journal" => { "max_bytes" => 8_388_608 },
160
161
  # Maestro kapısı: maestro write_file/replace_file_content/run_command/run_tests'ı ÇALIŞTIRAMAZ:
161
162
  "maestro_gate" => true,
162
163
  "auto_pause_on_network_error" => true,
@@ -223,11 +224,7 @@ module Rubik
223
224
  validate_prompt_grace_value!(current_key, val)
224
225
  if schema.is_a?(Hash) && !schema.key?(key.to_s)
225
226
  candidate_keys = schema.keys
226
- candidates = if defined?(Rubik::Tools::FuzzyMatcher)
227
- Rubik::Tools::FuzzyMatcher.best_matches(key.to_s, candidate_keys)
228
- else
229
- []
230
- end
227
+ candidates = defined?(Rubik::Tools::FuzzyMatcher) ? Rubik::Tools::FuzzyMatcher.best_matches(key.to_s, candidate_keys) : []
231
228
 
232
229
  suggestion_msg = if candidates && !candidates.empty?
233
230
  suggested_full = prefix.empty? ? candidates.first : "#{prefix}.#{candidates.first}"
@@ -113,7 +113,7 @@ module Rubik
113
113
  if File.exist?(target_path)
114
114
  begin
115
115
  File.chmod(0o600, target_path)
116
- rescue
116
+ rescue StandardError
117
117
  nil
118
118
  end
119
119
  end
@@ -28,9 +28,11 @@ module Rubik
28
28
  class Budget
29
29
  DEFAULT_MAX_STEPS = 88
30
30
  DEFAULT_GRANT_STEPS = 20
31
+ DEFAULT_MAX_TOTAL_STEPS = 256
31
32
 
32
- def initialize(max_steps: nil)
33
- @max_steps = (max_steps || DEFAULT_MAX_STEPS).to_i
33
+ def initialize(max_steps: nil, max_total_steps: DEFAULT_MAX_TOTAL_STEPS)
34
+ @max_total_steps = [(max_total_steps || DEFAULT_MAX_TOTAL_STEPS).to_i, 1].max
35
+ @max_steps = [(max_steps || DEFAULT_MAX_STEPS).to_i, @max_total_steps].min
34
36
  @current_step = 0
35
37
  @monitor = Monitor.new
36
38
  end
@@ -39,6 +41,10 @@ module Rubik
39
41
  @monitor.synchronize { @max_steps }
40
42
  end
41
43
 
44
+ def max_total_steps
45
+ @monitor.synchronize { @max_total_steps }
46
+ end
47
+
42
48
  def current_step
43
49
  @monitor.synchronize { @current_step }
44
50
  end
@@ -62,14 +68,16 @@ module Rubik
62
68
  @monitor.synchronize { "[Step #{@current_step}/#{@max_steps}]" }
63
69
  end
64
70
 
65
- # Normalize -> floor -> +=, atomically. Returns the
66
- # [extra, total] pair Instance's note lambda needs. Terminal-state
71
+ # Normalize -> floor -> cap, atomically. Returns the [extra, total]
72
+ # pair Instance's note lambda needs. Repeated grants can never push a
73
+ # run past its configured lifetime ceiling. Terminal-state
67
74
  # refusal, the :running transition, the history note, and the condvar
68
75
  # broadcast stay on Instance — lifecycle facts, not budget facts.
69
76
  # Called with the Instance monitor held (lock order: instance -> budget).
70
77
  def grant!(steps)
71
78
  @monitor.synchronize do
72
- extra = Budget.normalize_grant(steps)
79
+ available = [@max_total_steps - @max_steps, 0].max
80
+ extra = [Budget.normalize_grant(steps), available].min
73
81
  @max_steps += extra
74
82
  [extra, @max_steps]
75
83
  end
@@ -80,11 +88,10 @@ module Rubik
80
88
  @monitor.synchronize { { current_step: @current_step, max_steps: @max_steps } }
81
89
  end
82
90
 
83
- # Pure half of the grant invariant, shared by grant! (mutation edge)
84
- # and the reporting edges (manage_subagents steps arg, REPL
85
- # !subagents extend arg): nil/non-positive -> DEFAULT_GRANT_STEPS, so
86
- # the number a caller echoes can never disagree with the number
87
- # grant! applies. Idempotent.
91
+ # Pure normalization half shared by grant! and the reporting edges
92
+ # (manage_subagents steps arg, REPL !subagents extend arg). A grant
93
+ # may be smaller than this normalized request when the lifetime
94
+ # ceiling has less room remaining. Idempotent.
88
95
  def self.normalize_grant(steps)
89
96
  extra = (steps || DEFAULT_GRANT_STEPS).to_i
90
97
  extra = DEFAULT_GRANT_STEPS if extra <= 0
@@ -52,6 +52,7 @@ module Rubik
52
52
  # Budget delegation: step counters live in Subagent::Budget (reader contract preserved).
53
53
  def current_step = @budget.current_step
54
54
  def max_steps = @budget.max_steps
55
+ def max_total_steps = @budget.max_total_steps
55
56
 
56
57
  def initialize(id:, parent_id:, role:, type_name:, prompt:, context:, tool_registry:,
57
58
  system_prompt: nil, workspace: "inherit", model: "inherit",
@@ -72,7 +73,15 @@ module Rubik
72
73
  @tool_registry = tool_registry
73
74
  @provider = provider
74
75
  @buffer_manager = buffer_manager
75
- @budget = Budget.new(max_steps: max_steps)
76
+ max_total_steps = if config.is_a?(Hash)
77
+ config["subagents.max_total_steps"] || config.dig("subagents", "max_total_steps") ||
78
+ Budget::DEFAULT_MAX_TOTAL_STEPS
79
+ elsif config.respond_to?(:fetch)
80
+ config.fetch("subagents.max_total_steps", Budget::DEFAULT_MAX_TOTAL_STEPS)
81
+ else
82
+ Budget::DEFAULT_MAX_TOTAL_STEPS
83
+ end
84
+ @budget = Budget.new(max_steps: max_steps, max_total_steps: max_total_steps)
76
85
  @root_dir = root_dir
77
86
  @config = config
78
87
  @event_bus = event_bus
@@ -189,7 +198,8 @@ module Rubik
189
198
  def to_h
190
199
  @monitor.synchronize do
191
200
  { id: @id, parent_id: @parent_id, role: @role, type: @type_name, state: @state.to_s,
192
- **@budget.to_h, grants_received: @grants_received, run: @run_seq,
201
+ **@budget.to_h, max_total_steps: @budget.max_total_steps,
202
+ grants_received: @grants_received, run: @run_seq,
193
203
  result_ref: result_ref, owner: @owner_task&.id, detached: @owner_task.nil?, uptime_seconds: uptime.round(2),
194
204
  operation: operations.current, timings: operations.timings,
195
205
  last_progress_seconds_ago: operations.last_progress_seconds_ago,
@@ -230,6 +240,8 @@ module Rubik
230
240
  return false if terminal?
231
241
 
232
242
  extra, total = @budget.grant!(steps)
243
+ return false if extra <= 0
244
+
233
245
  transition!(:running)
234
246
  @grants_received += 1
235
247
  @pause_facts = nil # the wake retires the wait fact with the pause (B3's clearer, now fact-only)
@@ -22,9 +22,9 @@ module Rubik
22
22
  #
23
23
  # Retention is bounded independently of the model context: at most
24
24
  # `subagents.journal.max_bytes` per execution file. Once the cap is
25
- # reached a final `journal_truncated` record marks the boundary and
26
- # further events are not persisted (a cancellation outcome is recorded
27
- # when the handler can still write one). Tool payloads are previewed
25
+ # reached a `journal_truncated` record marks the boundary; detail events
26
+ # stop, while reserved space preserves the terminal run/cancellation
27
+ # outcome. Tool payloads are previewed
28
28
  # (PREVIEW_CHARS) with explicit `output_chars` / `output_truncated`
29
29
  # metadata plus an `artifact_ref` to the conversation snapshot, so replay
30
30
  # can distinguish originals from summaries from clipped output.
@@ -37,10 +37,12 @@ module Rubik
37
37
  # in Instance#to_h), never raised into the run.
38
38
  class Journal
39
39
  FILENAME = "journal.jsonl"
40
- DEFAULT_MAX_BYTES = 2_097_152
40
+ DEFAULT_MAX_BYTES = 8_388_608
41
41
  # Headroom kept free for the final truncation marker: accepted records
42
42
  # never consume the last bytes, so the marker always fits inside the cap.
43
43
  MARKER_RESERVE = 512
44
+ TERMINAL_RESERVE = 16_384
45
+ TERMINAL_EVENTS = ["run_finished", "cancelled"].freeze
44
46
  MIN_MAX_BYTES = 1024
45
47
  PREVIEW_CHARS = 4000
46
48
  ARGS_CHARS = 2000
@@ -54,9 +56,12 @@ module Rubik
54
56
  @path = path.to_s
55
57
  @monitor = Monitor.new
56
58
  @max_bytes = [config_get(config, "subagents.journal.max_bytes", DEFAULT_MAX_BYTES).to_i, MIN_MAX_BYTES].max
57
- @seq = self.class.read(@path).records.length
59
+ existing_records = self.class.read(@path).records
60
+ @seq = existing_records.map { |record| record["seq"].to_i }.max || 0
58
61
  @bytes = File.exist?(@path) ? File.size(@path) : 0
59
- @closed = false
62
+ @marker_reserve = [MARKER_RESERVE, @max_bytes / 4].min
63
+ @terminal_reserve = [TERMINAL_RESERVE, @max_bytes / 3].min
64
+ @closed = existing_records.any? { |record| record["event"] == "journal_truncated" }
60
65
  @last_error = nil
61
66
  @run_metrics = {}
62
67
  end
@@ -65,16 +70,24 @@ module Rubik
65
70
  # record envelope; the payload is masked HERE (single redaction gate).
66
71
  def append(event, run: 1, data: {}, call_id: nil)
67
72
  @monitor.synchronize do
68
- return nil if @closed
73
+ event_name = event.to_s
74
+ terminal = TERMINAL_EVENTS.include?(event_name)
75
+ return nil if @closed && !terminal
69
76
 
70
77
  @last_run = run.to_i
71
78
  row = { "seq" => @seq + 1, "run" => @last_run, "ts" => Time.now.utc.iso8601(3),
72
- "event" => event.to_s, "data" => Rubik::Security::Redactor.mask_value(data) }
79
+ "event" => event_name, "data" => Rubik::Security::Redactor.mask_value(data) }
73
80
  row["call_id"] = call_id.to_s if call_id
74
- line = "#{Rubik::Platform.safe_json_generate(row)}\n"
75
- if @bytes + line.bytesize + MARKER_RESERVE > @max_bytes
81
+
82
+ return append_terminal!(row) if @closed
83
+
84
+ line = serialize(row)
85
+ if @bytes + line.bytesize + @marker_reserve + @terminal_reserve > @max_bytes
76
86
  mark_truncated
77
- return nil
87
+ return nil unless terminal
88
+
89
+ row["seq"] = @seq + 1
90
+ return append_terminal!(row)
78
91
  end
79
92
  return nil unless write!(line)
80
93
 
@@ -199,6 +212,35 @@ module Rubik
199
212
 
200
213
  private
201
214
 
215
+ def serialize(row) = "#{Rubik::Platform.safe_json_generate(row)}\n"
216
+
217
+ def append_terminal!(row)
218
+ line = serialize(row)
219
+ if @bytes + line.bytesize > @max_bytes
220
+ row["data"] = compact_terminal_data(row["data"])
221
+ line = serialize(row)
222
+ end
223
+ return nil if @bytes + line.bytesize > @max_bytes || !write!(line)
224
+
225
+ @seq += 1
226
+ @bytes += line.bytesize
227
+ row
228
+ end
229
+
230
+ def compact_terminal_data(data)
231
+ return {} unless data.is_a?(Hash)
232
+
233
+ summary = data.slice("state", "outcome", "duration_ms", "step_count")
234
+ ["state", "outcome"].each do |key|
235
+ summary[key] = summary[key].to_s.slice(0, 64) if summary.key?(key)
236
+ end
237
+ summary["reason"] = data["reason"].to_s.slice(0, 256) if data["reason"]
238
+ result_ref = data["result_ref"]
239
+ summary["result_ref"] = result_ref.to_s.slice(0, 128) if result_ref
240
+ summary["terminal_summary_truncated"] = true
241
+ summary
242
+ end
243
+
202
244
  def empty_run_metrics
203
245
  { tool_usage: {}, model_responses: 0, input_tokens: 0, output_tokens: 0,
204
246
  input_token_responses: 0, output_token_responses: 0 }
@@ -240,8 +282,9 @@ module Rubik
240
282
  @closed = true
241
283
  marker = { "seq" => @seq + 1, "run" => (@last_run || 1).to_i, "ts" => Time.now.utc.iso8601(3),
242
284
  "event" => "journal_truncated",
243
- "data" => { "max_bytes" => @max_bytes, "reason" => "retention cap reached; further events are not journaled" } }
244
- line = "#{Rubik::Platform.safe_json_generate(marker)}\n"
285
+ "data" => { "max_bytes" => @max_bytes,
286
+ "reason" => "retention cap reached; detail events stop, terminal outcomes use reserved space" } }
287
+ line = serialize(marker)
245
288
  return nil if @bytes + line.bytesize > @max_bytes || !write!(line)
246
289
 
247
290
  @seq += 1
@@ -59,7 +59,7 @@ module Rubik
59
59
  drain_inbox_into_history
60
60
  @history << { role: "user", content: "[New task from maestro]: #{prompt}" }
61
61
  @prompt = prompt.to_s
62
- @budget = Budget.new(max_steps: max_steps.to_i)
62
+ @budget = Budget.new(max_steps: max_steps.to_i, max_total_steps: @budget.max_total_steps)
63
63
  # @result reset is the ONE sanctioned clearer (terminal-only
64
64
  # contract): a new lineage begins. @pause_facts rides along —
65
65
  # the old run's wait fact must not narrate the new one.
@@ -69,11 +69,14 @@ module Rubik
69
69
  def build_effective_system_prompt
70
70
  base = "You are a specialized subagent operating under autonomous supervision.\n\n" \
71
71
  "Subagent ID: #{@id} | Role: #{@role} | Type: #{@type_name}\n" \
72
- "Allocated Step Budget: #{@budget.max_steps} steps.\n\n" \
73
- "A [Step X/Y] status line precedes each of your steps (X = current step, Y = total budget). Pace yourself: " \
74
- "when remaining steps (Y - X) run low, conclude the task or prepare a progress summary for the maestro " \
75
- "supervisor — it may extend your budget or steer you. If a requirement is genuinely ambiguous, ask the maestro directly with the " \
76
- "ask_question tool — it blocks until the maestro answers or times out; ask only when the answer truly changes your work.\n\n" \
72
+ "Allocated Step Budget: #{@budget.max_steps} steps; lifetime ceiling: " \
73
+ "#{@budget.max_total_steps} steps across all grants.\n\n" \
74
+ "A [Step X/Y] status line precedes each of your steps (X = current step, Y = total budget). " \
75
+ "Pace yourself: when remaining steps (Y - X) run low, conclude the task or prepare a progress " \
76
+ "summary for the maestro supervisor — it may extend your budget up to the lifetime ceiling or " \
77
+ "steer you. If a requirement is genuinely ambiguous, ask the maestro directly with the " \
78
+ "ask_question tool — it blocks until the maestro answers or times out; ask only when the answer " \
79
+ "truly changes your work.\n\n" \
77
80
  "Perform your assigned task with high precision and provide a structured synthesis report."
78
81
  @system_prompt && !@system_prompt.strip.empty? ? "#{base}\n\nCustom Instructions:\n#{@system_prompt}" : base
79
82
  end
@@ -38,6 +38,7 @@ module Rubik
38
38
  class Supervisor
39
39
  attr_accessor :tool_registry, :router, :init
40
40
  attr_reader :root_context, :type_registry, :instances, :max_depth, :max_concurrency, :default_max_steps,
41
+ :max_total_steps,
41
42
  :provider_gate, :result_archive
42
43
 
43
44
  include RedeploySupervisor
@@ -70,7 +71,12 @@ module Rubik
70
71
  @counter = 0
71
72
  @max_depth = (config&.fetch("subagents.max_depth", 2) || 2).to_i
72
73
  @max_concurrency = (config&.fetch("subagents.max_concurrency", 10) || 10).to_i
73
- @default_max_steps = (config&.fetch("subagents.default_max_steps", Budget::DEFAULT_MAX_STEPS) || Budget::DEFAULT_MAX_STEPS).to_i
74
+ configured_max_total_steps = config&.fetch("subagents.max_total_steps", Budget::DEFAULT_MAX_TOTAL_STEPS) ||
75
+ Budget::DEFAULT_MAX_TOTAL_STEPS
76
+ @max_total_steps = [configured_max_total_steps.to_i, 1].max
77
+ configured_default_steps = config&.fetch("subagents.default_max_steps", Budget::DEFAULT_MAX_STEPS) ||
78
+ Budget::DEFAULT_MAX_STEPS
79
+ @default_max_steps = [configured_default_steps.to_i, @max_total_steps].min
74
80
  @provider_concurrency = (config&.fetch("subagents.provider_concurrency", 2) || 2).to_i
75
81
  @provider_gate = ConcurrencyGate.new(@provider_concurrency)
76
82
  # Retained completed-run results (issue #122): session-scoped,
@@ -92,7 +92,7 @@ module Rubik
92
92
  # Plan 55 D-1 ile tutarlı dizi-form çağrı: cmd.exe ara katmanı ve kabuk
93
93
  # yönlendirmesi ("> NUL 2>&1") yerine out:/err: seçenekleri kullanılır.
94
94
  system("taskkill", "/F", "/T", "/PID", pid.to_s, out: File::NULL, err: File::NULL)
95
- rescue
95
+ rescue StandardError
96
96
  nil
97
97
  end
98
98
  else
@@ -39,7 +39,7 @@ module Rubik
39
39
  debug_warn("clean_utf8 scrub fallback: #{e.class}: #{e.message}")
40
40
  begin
41
41
  str.to_s.scrub("")
42
- rescue
42
+ rescue StandardError
43
43
  ""
44
44
  end
45
45
  end
@@ -108,7 +108,7 @@ module Rubik
108
108
  fallback = sanitize_for_json_fallback(obj)
109
109
  begin
110
110
  JSON.generate(fallback, *)
111
- rescue
111
+ rescue StandardError
112
112
  "{}"
113
113
  end
114
114
  end
@@ -122,7 +122,7 @@ module Rubik
122
122
  fallback = sanitize_for_json_fallback(obj)
123
123
  begin
124
124
  JSON.dump(fallback, *)
125
- rescue
125
+ rescue StandardError
126
126
  "{}"
127
127
  end
128
128
  end
@@ -148,7 +148,7 @@ module Rubik
148
148
  debug_warn("sanitize_for_json_fallback rescue: #{e.class}: #{e.message}")
149
149
  begin
150
150
  obj.to_s.scrub("")
151
- rescue
151
+ rescue StandardError
152
152
  ""
153
153
  end
154
154
  end
@@ -87,7 +87,7 @@ module Rubik
87
87
 
88
88
  def home_dir
89
89
  Dir.home
90
- rescue
90
+ rescue StandardError
91
91
  (ENV["USERPROFILE"] || (ENV.fetch("HOMEDRIVE",
92
92
  nil) && ENV.fetch("HOMEPATH",
93
93
  nil) && "#{ENV.fetch('HOMEDRIVE',
@@ -96,7 +96,7 @@ module Rubik
96
96
 
97
97
  def tmp_dir
98
98
  Dir.tmpdir
99
- rescue
99
+ rescue StandardError
100
100
  (ENV["TEMP"] || ENV["TMP"] || "/tmp")
101
101
  end
102
102
  end
@@ -113,10 +113,7 @@ module Rubik
113
113
  end
114
114
 
115
115
  def self.handle_kill(ctx, sup, id)
116
- if id.to_s.strip.empty?
117
- ctx.out.puts "Usage: !subagents kill <subagent_id>"
118
- return
119
- end
116
+ return ctx.out.puts "Usage: !subagents kill <subagent_id>" if id.to_s.strip.empty?
120
117
 
121
118
  if sup.kill(id)
122
119
  ctx.out.puts "Subagent '#{id}' and child tree killed."
@@ -126,17 +123,21 @@ module Rubik
126
123
  end
127
124
 
128
125
  def self.handle_extend(ctx, sup, id, steps_arg)
129
- if id.to_s.strip.empty?
130
- ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]"
131
- return
132
- end
126
+ return ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]" if id.to_s.strip.empty?
133
127
 
134
128
  steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(steps_arg)
129
+ inst = sup.get(id)
135
130
  if sup.extend_budget(id, steps)
136
- inst = sup.get(id)
137
- ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}, state: #{inst.state.to_s.upcase})."
131
+ ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
132
+ ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}#{ceiling}, " \
133
+ "state: #{inst.state.to_s.upcase})."
138
134
  else
139
- ctx.out.puts "Could not extend budget for #{id} (not found or inactive)."
135
+ msg = if inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
136
+ "Could not extend budget for #{id}: lifetime step limit of #{inst.max_total_steps} reached."
137
+ else
138
+ "Could not extend budget for #{id} (not found or inactive)."
139
+ end
140
+ ctx.out.puts msg
140
141
  end
141
142
  end
142
143
 
@@ -89,14 +89,14 @@ module Rubik
89
89
  if get_std_handle
90
90
  new_in = begin
91
91
  get_std_handle.call(Reline::Windows::STD_INPUT_HANDLE)
92
- rescue
92
+ rescue StandardError
93
93
  nil
94
94
  end
95
95
  io.instance_variable_set(:@hConsoleInputHandle, new_in) if new_in && new_in != 0
96
96
 
97
97
  new_out = begin
98
98
  get_std_handle.call(Reline::Windows::STD_OUTPUT_HANDLE)
99
- rescue
99
+ rescue StandardError
100
100
  nil
101
101
  end
102
102
  io.instance_variable_set(:@hConsoleHandle, new_out) if new_out && new_out != 0
@@ -105,14 +105,14 @@ module Rubik
105
105
  flush_func = io.instance_variable_get(:@FlushConsoleInputBuffer) ||
106
106
  begin
107
107
  Reline::Windows::Win32API.new("kernel32", "FlushConsoleInputBuffer", ["L"], "I")
108
- rescue
108
+ rescue StandardError
109
109
  nil
110
110
  end
111
111
  if flush_func
112
112
  io.instance_variable_set(:@FlushConsoleInputBuffer, flush_func)
113
113
  begin
114
114
  flush_func.call(new_in)
115
- rescue
115
+ rescue StandardError
116
116
  nil
117
117
  end
118
118
  end
@@ -143,7 +143,7 @@ if defined?(Reline::Windows)
143
143
  if @hConsoleInputHandle.nil? || @hConsoleInputHandle.zero? || @hConsoleInputHandle == -1
144
144
  @hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
145
145
  @GetStdHandle.call(STD_INPUT_HANDLE)
146
- rescue
146
+ rescue StandardError
147
147
  nil
148
148
  end
149
149
  end
@@ -156,18 +156,18 @@ if defined?(Reline::Windows)
156
156
  if [0xFFFFFFFF, 4_294_967_295, -1].include?(wait_res)
157
157
  @hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
158
158
  @GetStdHandle.call(STD_INPUT_HANDLE)
159
- rescue
159
+ rescue StandardError
160
160
  nil
161
161
  end
162
162
  @hConsoleHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
163
163
  @GetStdHandle.call(STD_OUTPUT_HANDLE)
164
- rescue
164
+ rescue StandardError
165
165
  nil
166
166
  end
167
167
  end
168
168
  @legacy_console = begin
169
169
  getconsolemode.nobits?(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
170
- rescue
170
+ rescue StandardError
171
171
  false
172
172
  end
173
173
  next
@@ -175,7 +175,7 @@ if defined?(Reline::Windows)
175
175
  if @GetNumberOfConsoleInputEvents.call(@hConsoleInputHandle, num_of_events).zero? || num_of_events.unpack1("L").zero?
176
176
  @hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
177
177
  @GetStdHandle.call(STD_INPUT_HANDLE)
178
- rescue
178
+ rescue StandardError
179
179
  nil
180
180
  end
181
181
  next
@@ -185,7 +185,7 @@ if defined?(Reline::Windows)
185
185
  if @ReadConsoleInputW.call(@hConsoleInputHandle, input_records, 80, read_event).zero?
186
186
  @hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
187
187
  @GetStdHandle.call(STD_INPUT_HANDLE)
188
- rescue
188
+ rescue StandardError
189
189
  nil
190
190
  end
191
191
  else
@@ -251,10 +251,15 @@ module Rubik
251
251
  return ToolOutput.error("Missing required parameter 'conversation_id' for action 'extend_budget'.") if id.empty?
252
252
 
253
253
  steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(args["steps"] || args[:steps])
254
+ inst = sup.get(id)
254
255
  if sup.extend_budget(id, steps)
255
- "Step budget for subagent '#{id}' successfully extended by #{steps} steps (total budget: #{sup.get(id).max_steps}, state: #{sup.get(id).state.to_s.upcase})."
256
+ ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
257
+ "Step budget for subagent '#{id}' successfully extended by #{steps} steps (total budget: #{inst.max_steps}#{ceiling}, " \
258
+ "state: #{inst.state.to_s.upcase})."
259
+ elsif inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
260
+ ToolOutput.error("Subagent '#{id}' reached its lifetime step limit (#{inst.max_total_steps}); no more steps can be granted.")
256
261
  else
257
- ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{sup.get(id)&.state || 'not found'}).")
262
+ ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{inst&.state || 'not found'}).")
258
263
  end
259
264
  when "resume"
260
265
  id = resolve_id(args)
data/lib/rubik/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubik
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubik
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır