rubik 0.1.2 → 0.1.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: a0ec56cafd854c2e2999c68cdfdf787322ca4d7d2a2866fac4b4e8ce9f1efac7
4
- data.tar.gz: ec584d8ac5ee2039423d71968cce852afb65fcb072cbdbb87325ef94693e6a5d
3
+ metadata.gz: a3548e6b569bb11a7c6c6349bca95300d93cd375c65860095ff18d362cd289ed
4
+ data.tar.gz: 207e5ed22d5180f7b219b546d0322ac2f597703dec1157ec44da48feb899ab1e
5
5
  SHA512:
6
- metadata.gz: 3a535d5c0f45c74363387e7df47da8c87b1296be897fdd09adc17e4adfcc576b48555066cb0b3618d27f1eeeba052c53fe1ccb7a670598a43cad57bb8a4cc942
7
- data.tar.gz: d75bb114c57fb2221f17f9d89a1a0fe1811b9e36ba52289337030b822210ef5373118e7d8438c4000da9dfca65fb4dadf3b5f85bdb8ad117ee50d56469c1a782
6
+ metadata.gz: a6b485c11a72537a00fbe813889f74b92e18edaa1445ec59bc3834bf6e56c377cc60ac628f059878e6d3fc0a1e27d4f63fa1f6161bac84513af4b3640a37c4b6
7
+ data.tar.gz: 228ce20578fc055bde12ffe0883343f21b74a9b499e718c1e953fc40c9e5054d3c21a27f5e3b986890efba08d3ae23ec77a37498fd197e37a3449de5524d0943
data/CHANGELOG.md CHANGED
@@ -10,8 +10,37 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
 
11
11
  Nothing yet.
12
12
 
13
+ ## [0.1.3] - 2026-10-12
14
+
15
+ A robustness release straight from a field incident: the `!goal` loop
16
+ no longer strands state when the provider fails mid-goal, and the
17
+ redactor gets measurably cheaper on secret-free input.
18
+
19
+ ### Fixed
20
+
21
+ - **`!goal` survives provider errors (#105):** an api read-timeout
22
+ `ClientError` escaped the goal handler (only `Interrupt` was
23
+ rescued), leaving `GoalState` stuck in `:running`, the ledger
24
+ unfinalized and the session silent while the batch spinner kept
25
+ cycling with the cursor hidden. One `fail_goal` channel now serves
26
+ the `:error` return plus `ClientError`/`StandardError` rescues
27
+ (`Interrupt` still means `:stopped`); both command-dispatch branches
28
+ kill the leaked spinner instead of masking the real failure; and
29
+ goal steps record their tool calls to the session like free-text
30
+ turns.
31
+
32
+ ### Changed
33
+
34
+ - **Redactor short-circuits secret-free lines (#104):** pre-compiled
35
+ quick-check constants plus a trigger check before scrubbing cut
36
+ allocations by ~68% on a mixed redact workload (172,940 -> 54,864
37
+ objects per 20x2000-line pass) with byte-for-byte output parity;
38
+ invalid-UTF-8 input stays total (masked/scrubbed) instead of raising
39
+ ArgumentError.
40
+
13
41
  ## [0.1.2] - 2026-10-11
14
42
 
43
+
15
44
  The Anthropic correctness release: the provider now speaks the modern
16
45
  extended-thinking contract, so pinning any current Claude model works
17
46
  instead of failing with HTTP 400.
@@ -127,6 +127,16 @@ module Rubik
127
127
  # Adım sayacı rubik://goal/state'e aksın: olaylar bir adım
128
128
  # içinde tekrarlanabilir, GoalState monotoniktir (zararsız).
129
129
  goal_state.update_steps(step_info[:step], step_info[:max_steps]) if step_info[:step].is_a?(Integer)
130
+ # Session teşhis edilebilirliği (field incident 2026-10-12):
131
+ # saatler süren !goal koşuları session dosyasına tek satır
132
+ # düşürüyordu — free-text turn simetrisinde tool adımları
133
+ # kayıtçıya akar; render ve LLM geçmişi değişmez.
134
+ case step_info[:type]&.to_sym
135
+ when :tool_call
136
+ ctx.recorder&.record_tool_call(step_info[:tool], step_info[:args], call_id: step_info[:call_id])
137
+ when :tool_result
138
+ ctx.recorder&.record_tool_result(step_info[:tool], step_info[:output], status: step_info[:status] || :ok, call_id: step_info[:call_id])
139
+ end
130
140
  ctx.render_step_event(step_info)
131
141
  }
132
142
  )
@@ -143,8 +153,28 @@ module Rubik
143
153
  end
144
154
 
145
155
  # Pruner apply seam: hedef döngüsü de aynı sınırda tohumlanır —
146
- # aşamalı plan varsa goal promptundan ÖNCE uygulanır.
156
+ # aşamalı budama planı varsa goal promptundan ÖNCE uygulanır.
147
157
  ctx.apply_staged_prune_plan!
158
+
159
+ # Tek hata kanalı (field incident 2026-10-12): eskiden yalnızca
160
+ # normal :error dönüşü başarısızlık eşlemesini yapıyordu; loop'tan
161
+ # kaçan bir ClientError (sağlayıcı read timeout) goal'u kayıtsız
162
+ # 'running' bırakıyor, ledger terminal görmiyor ve session
163
+ # dosyası sessiz kalıyordu. Rescue dalları da aynı kapanış
164
+ # sırasını buradan çalıştırır. Kayıtçı '!goal ...' user_turn'unu
165
+ # komut girişinde çoktan yazdı — assistant_response olmadan
166
+ # session restore EŞLEŞMEMİŞ bir 'Goal: ...' turu hidrate ederdi.
167
+ # Adım sayısı: loop'un truncation hatası :steps'siz döner —
168
+ # GoalState monotonik sayacı otoriter yedek.
169
+ fail_goal = lambda do |reason, tokens: nil, steps: nil|
170
+ sanitized = Rubik::Platform.safe_utf8(reason.to_s)
171
+ steps_used = steps || goal_state.step_count
172
+ goal_state.fail!(sanitized, tokens: tokens)
173
+ finalize_ledger.call(:failed)
174
+ ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
175
+ record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
176
+ end
177
+
148
178
  ctx.active_loop = orchestrator
149
179
  begin
150
180
  res = orchestrator.run(goal_text, goal_mode: true, conversation_history: ctx.conversation_history)
@@ -170,6 +200,16 @@ module Rubik
170
200
  finalize_ledger.call(:stopped)
171
201
  record_goal_pair.call("[Goal attempt interrupted after #{goal_state.step_count} steps]")
172
202
  raise
203
+ rescue Rubik::LLM::ClientError => e
204
+ # execute_free_text simetrisi: sağlayıcı hatası (read timeout,
205
+ # HTTP 4xx) kaçarsa da goal terminal görür. Yeniden yükseltmez —
206
+ # spinner temizliği dispatch katmanına (execute_command) aittir.
207
+ # next: rescue sonrası akış res'siz `if res[:ok]`'a düşmesin.
208
+ fail_goal.call(e.message)
209
+ next
210
+ rescue StandardError => e
211
+ fail_goal.call("#{e.class}: #{e.message}")
212
+ next
173
213
  ensure
174
214
  ctx.active_loop = nil
175
215
  end
@@ -197,17 +237,10 @@ module Rubik
197
237
  ctx.out.puts "\nTip: Use `!diff` to review changes and `!apply` to commit to disk." if ctx.buffer_manager.dirty?
198
238
  end
199
239
  else
200
- # Başarısızlık eşlemesi: kayıtçı '!goal ...' user_turn'unu komut
201
- # girişinde çoktan yazdı — assistant_response olmadan session
202
- # restore EŞLEŞMEMİŞ bir 'Goal: ...' turu hidrate ederdi.
203
- # Adım sayısı: loop'un truncation hatası :steps'siz döner —
204
- # GoalState monotonik sayacı otoriter yedek.
205
- sanitized = Rubik::Platform.safe_utf8(res[:error].to_s)
206
- steps_used = res[:steps] || goal_state.step_count
207
- goal_state.fail!(sanitized, tokens: res[:tokens])
208
- finalize_ledger.call(:failed)
209
- ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
210
- record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: res[:tokens])
240
+ # Başarısızlık eşlemesi yukarıdaki tek hata kanalından akar:
241
+ # GoalState → ledger finalize → kayıtçı çifti, istisna yoluyla
242
+ # aynı sırayla.
243
+ fail_goal.call(res[:error], tokens: res[:tokens], steps: res[:steps])
211
244
  end
212
245
  end
213
246
  end
@@ -56,6 +56,11 @@ module Rubik
56
56
  rescue Interrupt
57
57
  Rubik::REPL::InterruptHandler.restore_console_after_interrupt
58
58
  @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
59
+ ensure
60
+ # !goal dahil lifecycle komutları tracked dal ile aynı spinner
61
+ # garantisini taşır (field incident 2026-10-12: !goal'ten kaçan
62
+ # ClientError'da öksüz spinner terminali kilitli gösterdi).
63
+ kill_command_spinner!
59
64
  end
60
65
  else
61
66
  captured = StringIO.new
@@ -70,7 +75,7 @@ module Rubik
70
75
  @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
71
76
  ensure
72
77
  # A command (!plan/!goal) may leave a live spinner if it died mid-wait.
73
- @ui_renderer&.spinner&.kill
78
+ kill_command_spinner!
74
79
  end
75
80
  output_text = captured.string.strip
76
81
  if !cmd_interrupted && !output_text.empty?
@@ -297,11 +302,7 @@ module Rubik
297
302
  rescue StandardError => e
298
303
  warn "[rubik] markdown tail flush failed: #{e.class}: #{e.message}"
299
304
  end
300
- begin
301
- renderer&.spinner&.kill
302
- rescue StandardError => e
303
- warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
304
- end
305
+ kill_spinner!(renderer&.spinner)
305
306
  flush_subagent_notices! if respond_to?(:flush_subagent_notices!)
306
307
  # Session review B3: notices stranded in the ended loop's inbox never
307
308
  # reached the model channel and owed no wake — an idle maestro learned
@@ -318,6 +319,21 @@ module Rubik
318
319
  end
319
320
  reap_dead_kernel_units! if respond_to?(:reap_dead_kernel_units!)
320
321
  end
322
+
323
+ # Shared spinner teardown. Cleanup must never mask the in-flight
324
+ # exception (AGENTS.md §5): a spinner whose tick thread died holding
325
+ # the mutex raises ThreadError from synchronize — report it to stderr
326
+ # and continue so the real failure surfaces.
327
+ def kill_spinner!(spinner)
328
+ spinner&.kill
329
+ rescue StandardError => e
330
+ warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
331
+ end
332
+
333
+ # Command-dispatch spinner cleanup (both branches).
334
+ def kill_command_spinner!
335
+ kill_spinner!(@ui_renderer&.spinner)
336
+ end
321
337
  end
322
338
  end
323
339
  end
@@ -20,6 +20,11 @@ module Rubik
20
20
  ASSIGNMENT_KEY_REGEX = /\b(password|passwd|api_key|secret)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
21
21
  ASSIGNMENT_TOKEN_REGEX = /\b(token)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
22
22
 
23
+ # PERF: Pre-compiled constants to avoid re-allocation inside hot loops
24
+ URL_PATTERN = %r{(https?://[^:]+:)([^@]+)@}
25
+ URL_MASK = "\\1#{MASK_STRING}@".freeze
26
+ QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[^:]+:|password|passwd|api_key|secret|token}i
27
+
23
28
  # Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
24
29
  # aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
25
30
  # maskelenir; sayaç anahtarları (tokens, max_tokens) istisnadır.
@@ -33,10 +38,17 @@ module Rubik
33
38
  # scrub: redact ham geçmişe (tool çıktısı, prompt) dokunur — geçersiz
34
39
  # UTF-8 gsub!'ı ArgumentError ile düşürür; total olmak maskenin
35
40
  # kendisi bir arıza kaynağı olmasın demektir.
36
- result = text.dup.scrub
41
+ result = text.valid_encoding? ? text.dup : text.scrub
42
+
43
+ # PERF: kısa devre — sır tetikleyicisi yoksa 11 gsub turu atlanır.
44
+ # Tetik testi scrub'lanmış sonuç üstünde yapılır: match? ham geçersiz
45
+ # UTF-8'de ArgumentError düşürür; scrub sonrası metin hep geçerlidir
46
+ # ve desenler zaten bu metne uygulanıyordu, dolayısıyla eşdeğerdir.
47
+ return result unless result.match?(QUICK_CHECK_REGEX)
48
+
37
49
  PATTERNS.each do |pattern|
38
- if pattern == %r{(https?://[^:]+:)([^@]+)@}
39
- result.gsub!(pattern, "\\1#{MASK_STRING}@")
50
+ if pattern == URL_PATTERN
51
+ result.gsub!(pattern, URL_MASK)
40
52
  else
41
53
  result.gsub!(pattern, MASK_STRING)
42
54
  end
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.1.2"
4
+ VERSION = "0.1.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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır