rubik 0.3.4 → 0.3.5

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +1 -1
  3. data/CHANGELOG.md +54 -0
  4. data/lib/rubik/adapter/process.rb +11 -8
  5. data/lib/rubik/ast/go_extractor.rb +9 -2
  6. data/lib/rubik/ast/rust_extractor.rb +9 -2
  7. data/lib/rubik/ast/typescript_extractor.rb +13 -7
  8. data/lib/rubik/buffer/manager.rb +78 -66
  9. data/lib/rubik/config/defaults.rb +1 -0
  10. data/lib/rubik/context/git_lister.rb +3 -1
  11. data/lib/rubik/llm/anthropic.rb +5 -2
  12. data/lib/rubik/llm/openai_compatible.rb +83 -68
  13. data/lib/rubik/lsp/client.rb +320 -96
  14. data/lib/rubik/lsp/manager.rb +83 -12
  15. data/lib/rubik/lsp/server_command.rb +102 -0
  16. data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
  17. data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
  18. data/lib/rubik/repl/commands/agent_context.rb +126 -104
  19. data/lib/rubik/repl/commands/agent_execution.rb +83 -68
  20. data/lib/rubik/repl/commands/agent_models.rb +112 -87
  21. data/lib/rubik/repl/commands/buffer.rb +122 -88
  22. data/lib/rubik/repl/commands/mcp.rb +120 -92
  23. data/lib/rubik/repl/commands/session.rb +90 -59
  24. data/lib/rubik/repl/commands/workspace.rb +110 -94
  25. data/lib/rubik/repl/shell.rb +10 -1
  26. data/lib/rubik/runtime/discovery.rb +12 -6
  27. data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
  28. data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
  29. data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
  30. data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
  31. data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
  32. data/lib/rubik/tools/builtins/lsp.rb +36 -8
  33. data/lib/rubik/version.rb +1 -1
  34. metadata +2 -1
@@ -3,6 +3,7 @@
3
3
  require "open3"
4
4
  require "json"
5
5
  require "uri"
6
+ require_relative "server_command"
6
7
  require_relative "../platform"
7
8
  require_relative "../log"
8
9
 
@@ -34,18 +35,20 @@ module Rubik
34
35
  "vue" => "vue-language-server --stdio"
35
36
  }.freeze
36
37
 
37
- attr_reader :language, :server_cmd, :root_dir, :running, :diagnostics, :server_capabilities
38
+ attr_reader :language, :server_cmd, :root_dir, :running, :diagnostics, :server_capabilities,
39
+ :sync_capabilities
38
40
 
39
41
  def initialize(language, root_dir: Dir.pwd, server_cmd: nil)
40
42
  @language = language.to_s.downcase
41
43
  @root_dir = Rubik::Platform.expand_path(root_dir)
42
44
  cmd = server_cmd || SERVERS[@language]
43
- if cmd.to_s.start_with?("typeprof", "typescript-language-server") && !cmd.to_s.include?("--stdio")
44
- cmd = "#{cmd} --stdio"
45
- elsif cmd.to_s.start_with?("solargraph") && !cmd.to_s.include?("stdio")
46
- cmd = "#{cmd} stdio"
47
- end
48
45
  @server_cmd = cmd
46
+ @server_argv = ServerCommand.argv(cmd)
47
+ if ["typeprof", "typescript-language-server"].include?(File.basename(@server_argv.first.to_s)) && !@server_argv.include?("--stdio")
48
+ @server_argv << "--stdio"
49
+ elsif File.basename(@server_argv.first.to_s) == "solargraph" && !@server_argv.include?("stdio")
50
+ @server_argv << "stdio"
51
+ end
49
52
  @running = false
50
53
  @crashed = false
51
54
  @req_id = 0
@@ -56,43 +59,59 @@ module Rubik
56
59
  @reader_thr = nil
57
60
  @stderr_drain = nil
58
61
  @mutex = Mutex.new
62
+ @document_mutex = Mutex.new
63
+ @write_mutex = Mutex.new
59
64
  @cv = ConditionVariable.new
60
65
  @responses = {}
61
66
  @pending_requests = {}
62
67
  @reader_failure = nil
63
68
  @diagnostics = {}
64
69
  @open_documents = {}
70
+ @document_contents = {}
71
+ @diagnostic_states = {}
65
72
  @version_counter = 0
66
73
  @server_capabilities = {}
74
+ @sync_capabilities = {
75
+ source: :not_advertised, open_close: false, change: 0,
76
+ save: false, include_text: false
77
+ }.freeze
67
78
  end
68
79
 
69
80
  def binary_name
70
- @server_cmd.to_s.split.first
81
+ @server_argv.first
71
82
  end
72
83
 
73
84
  def available?
74
85
  bin = binary_name
75
86
  return false if bin.nil? || bin.empty?
76
87
 
77
- !Rubik::Platform.which(bin).nil?
88
+ !resolved_binary(bin).nil?
78
89
  end
79
90
 
80
91
  def start
81
92
  return false unless available?
82
93
 
83
- @mutex.synchronize do
94
+ @document_mutex.synchronize do
84
95
  @req_id = 0
96
+ @open_documents.clear
97
+ @document_contents.clear
98
+ @version_counter = 0
99
+ @mutex.synchronize do
100
+ @diagnostics.clear
101
+ @diagnostic_states.clear
102
+ end
103
+ end
104
+ @mutex.synchronize do
85
105
  @responses.clear
86
106
  @pending_requests.clear
87
107
  @reader_failure = nil
88
- @open_documents.clear
89
- @diagnostics.clear
90
- @version_counter = 0
91
108
  end
92
109
 
93
- cmd_parts = @server_cmd.to_s.split
110
+ cmd_parts = @server_argv.dup
94
111
  bin = cmd_parts.first
95
- resolved_bin = Rubik::Platform.which(bin)
112
+ resolved_bin = resolved_binary(bin)
113
+ return false unless resolved_bin
114
+
96
115
  cmd_parts[0] = resolved_bin if resolved_bin
97
116
 
98
117
  @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(*cmd_parts, chdir: @root_dir)
@@ -118,8 +137,14 @@ module Rubik
118
137
  capabilities: {
119
138
  textDocument: {
120
139
  hover: { contentFormat: ["markdown", "plaintext"] },
121
- definition: { dynamicRegistration: true },
122
- references: { dynamicRegistration: true },
140
+ synchronization: {
141
+ dynamicRegistration: false,
142
+ willSave: false,
143
+ willSaveWaitUntil: false,
144
+ didSave: true
145
+ },
146
+ definition: { dynamicRegistration: false },
147
+ references: { dynamicRegistration: false },
123
148
  publishDiagnostics: { relatedInformation: true }
124
149
  }
125
150
  }
@@ -131,7 +156,11 @@ module Rubik
131
156
  end
132
157
 
133
158
  @server_capabilities = init_res.is_a?(Hash) ? (init_res["capabilities"] || {}) : {}
134
- send_notification("initialized", {})
159
+ @sync_capabilities = normalize_sync_capabilities(@server_capabilities["textDocumentSync"])
160
+ unless send_notification("initialized", {})
161
+ stop
162
+ return false
163
+ end
135
164
  @running = true
136
165
  init_res
137
166
  rescue StandardError
@@ -246,23 +275,41 @@ module Rubik
246
275
  end
247
276
 
248
277
  def did_open(file_path, content, language_id = @language)
249
- return false unless alive?
278
+ return false unless alive? && @sync_capabilities[:open_close]
250
279
 
251
280
  abs_path = normalized_document_path(file_path)
252
- @mutex.synchronize do
253
- @version_counter += 1
254
- @open_documents[abs_path] = @version_counter
255
- end
281
+ @document_mutex.synchronize do
282
+ if @open_documents.key?(abs_path)
283
+ return true if @document_contents[abs_path] == content
256
284
 
257
- send_notification("textDocument/didOpen", {
258
- textDocument: {
259
- uri: document_uri(abs_path),
260
- languageId: language_id.to_s,
261
- version: @version_counter,
262
- text: content
263
- }
264
- })
265
- true
285
+ return send_document_change_unlocked(abs_path, content)
286
+ end
287
+
288
+ version = next_document_version
289
+ @open_documents[abs_path] = version
290
+ @document_contents[abs_path] = content
291
+ old_state = @mutex.synchronize do
292
+ previous = @diagnostic_states[abs_path]
293
+ @diagnostic_states[abs_path] = pending_diagnostics(version)
294
+ previous
295
+ end
296
+ sent = send_notification("textDocument/didOpen", {
297
+ textDocument: {
298
+ uri: document_uri(abs_path),
299
+ languageId: language_id.to_s,
300
+ version: version,
301
+ text: content
302
+ }
303
+ })
304
+ unless sent
305
+ @open_documents.delete(abs_path)
306
+ @document_contents.delete(abs_path)
307
+ @mutex.synchronize do
308
+ old_state ? @diagnostic_states[abs_path] = old_state : @diagnostic_states.delete(abs_path)
309
+ end
310
+ end
311
+ sent
312
+ end
266
313
  rescue StandardError
267
314
  false
268
315
  end
@@ -271,44 +318,50 @@ module Rubik
271
318
  return false unless alive?
272
319
 
273
320
  abs_path = normalized_document_path(file_path)
274
- version = @mutex.synchronize do
275
- @version_counter += 1
276
- @open_documents[abs_path] = @version_counter
277
- @diagnostics.delete(abs_path)
278
- @diagnostics.delete(file_path)
279
- @diagnostics.delete_if { |k, _| Rubik::Platform.path_equal?(k, abs_path) }
280
- @version_counter
281
- end
282
-
283
- send_notification("textDocument/didChange", {
284
- textDocument: {
285
- uri: document_uri(abs_path),
286
- version: version
287
- },
288
- contentChanges: [
289
- { text: content }
290
- ]
291
- })
292
- true
321
+ @document_mutex.synchronize { send_document_change_unlocked(abs_path, content) }
293
322
  rescue StandardError
294
323
  false
295
324
  end
296
325
 
297
326
  def did_close(file_path)
298
- return false unless @running
327
+ return false unless @running && @sync_capabilities[:open_close]
299
328
 
300
329
  abs_path = normalized_document_path(file_path)
301
- raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
302
- @mutex.synchronize do
330
+ @document_mutex.synchronize do
331
+ version = @open_documents[abs_path]
332
+ return false unless version
333
+
334
+ content = @document_contents[abs_path]
335
+ state, diagnostics = @mutex.synchronize do
336
+ [@diagnostic_states.delete(abs_path), @diagnostics.delete(abs_path)]
337
+ end
303
338
  @open_documents.delete(abs_path)
304
- @open_documents.delete(raw_abs)
305
- @open_documents.delete_if { |k, _| Rubik::Platform.path_equal?(k, abs_path) }
339
+ @document_contents.delete(abs_path)
340
+ sent = send_notification("textDocument/didClose", {
341
+ textDocument: { uri: document_uri(abs_path) }
342
+ })
343
+ unless sent
344
+ @open_documents[abs_path] = version
345
+ @document_contents[abs_path] = content
346
+ @mutex.synchronize do
347
+ @diagnostic_states[abs_path] = state if state
348
+ @diagnostics[abs_path] = diagnostics if diagnostics
349
+ end
350
+ end
351
+ sent
306
352
  end
353
+ rescue StandardError
354
+ false
355
+ end
307
356
 
308
- send_notification("textDocument/didClose", {
309
- textDocument: { uri: document_uri(abs_path) }
310
- })
311
- true
357
+ def did_save(file_path, content = nil)
358
+ return false unless alive? && @sync_capabilities[:save]
359
+ return false if @sync_capabilities[:include_text] && content.nil?
360
+
361
+ abs_path = normalized_document_path(file_path)
362
+ params = { textDocument: { uri: document_uri(abs_path) } }
363
+ params[:text] = content if @sync_capabilities[:include_text] && !content.nil?
364
+ send_notification("textDocument/didSave", params)
312
365
  rescue StandardError
313
366
  false
314
367
  end
@@ -317,34 +370,54 @@ module Rubik
317
370
  return false if file_path.nil? || file_path.to_s.strip.empty?
318
371
 
319
372
  abs_path = normalized_document_path(file_path)
320
- raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
321
- @mutex.synchronize do
322
- @open_documents.key?(abs_path) ||
323
- @open_documents.key?(raw_abs) ||
324
- @open_documents.keys.any? { |k| Rubik::Platform.path_equal?(k, abs_path) }
325
- end
373
+ @document_mutex.synchronize { @open_documents.key?(abs_path) }
326
374
  rescue ArgumentError
327
375
  false
328
376
  end
329
377
 
330
378
  def diagnostics_for(file_path)
379
+ diagnostics_snapshot_for(file_path)[:diagnostics]
380
+ end
381
+
382
+ def all_diagnostics
383
+ @mutex.synchronize do
384
+ @diagnostics.dup
385
+ end
386
+ end
387
+
388
+ def diagnostics_snapshot_for(file_path)
389
+ abs_path = normalized_document_path(file_path)
390
+ @mutex.synchronize { diagnostics_snapshot_unlocked(abs_path) }
391
+ rescue ArgumentError, TypeError
392
+ { state: :unavailable, diagnostics: [], document_version: nil, published_version: nil }
393
+ end
394
+
395
+ def wait_for_diagnostics(file_path, timeout: 0.2)
331
396
  abs_path = normalized_document_path(file_path)
332
- raw_abs = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
333
- root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
334
- rel_path = raw_abs.start_with?("#{root_norm}/") ? raw_abs.delete_prefix("#{root_norm}/") : file_path.to_s.tr("\\", "/")
397
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
335
398
  @mutex.synchronize do
336
- @diagnostics[abs_path] ||
337
- @diagnostics[raw_abs] ||
338
- @diagnostics[rel_path] ||
339
- @diagnostics[file_path] ||
340
- @diagnostics.find { |k, _| Rubik::Platform.path_equal?(k, abs_path) }&.last ||
341
- []
399
+ while @diagnostic_states[abs_path]&.dig(:state) == :pending
400
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
401
+ break if remaining <= 0
402
+
403
+ @cv.wait(@mutex, remaining)
404
+ end
405
+ diagnostics_snapshot_unlocked(abs_path)
342
406
  end
407
+ rescue ArgumentError, TypeError
408
+ { state: :unavailable, diagnostics: [], document_version: nil, published_version: nil }
343
409
  end
344
410
 
345
- def all_diagnostics
411
+ def all_diagnostic_snapshots
346
412
  @mutex.synchronize do
347
- @diagnostics.dup
413
+ @diagnostic_states.each_with_object({}) do |(path, state), result|
414
+ result[path] = {
415
+ state: state[:state],
416
+ diagnostics: (@diagnostics[path] || []).dup,
417
+ document_version: state[:document_version],
418
+ published_version: state[:published_version]
419
+ }
420
+ end
348
421
  end
349
422
  end
350
423
 
@@ -417,16 +490,22 @@ module Rubik
417
490
  uri = params["uri"].to_s
418
491
  raw_path = Rubik::Platform.from_file_uri(uri)
419
492
  norm_path = normalized_document_path(raw_path)
420
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(raw_path, @root_dir))
421
- root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
422
- rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : abs_path
423
- diags = params["diagnostics"] || []
424
-
425
- @mutex.synchronize do
426
- @diagnostics[norm_path] = diags
427
- @diagnostics[abs_path] = diags
428
- @diagnostics[rel_path] = diags
429
- @diagnostics[raw_path] = diags
493
+ diags = params["diagnostics"].is_a?(Array) ? params["diagnostics"] : []
494
+ published_version = params["version"]
495
+
496
+ @document_mutex.synchronize do
497
+ current_version = @open_documents[norm_path]
498
+ next if published_version.is_a?(Numeric) && current_version && published_version < current_version
499
+
500
+ @mutex.synchronize do
501
+ @diagnostics[norm_path] = diags
502
+ @diagnostic_states[norm_path] = {
503
+ state: published_version.is_a?(Numeric) ? :fresh : :unversioned,
504
+ document_version: current_version,
505
+ published_version: published_version
506
+ }
507
+ @cv.broadcast
508
+ end
430
509
  end
431
510
  end
432
511
  end
@@ -443,7 +522,13 @@ module Rubik
443
522
  method: method,
444
523
  params: params
445
524
  }
446
- write_message(msg)
525
+ unless write_message(msg)
526
+ @mutex.synchronize do
527
+ @pending_requests.delete(req_id)
528
+ @responses.delete(req_id)
529
+ end
530
+ raise RequestError.new(:transport_failure, @reader_failure || "LSP request write failed")
531
+ end
447
532
  wait_for_response(req_id, timeout: timeout)
448
533
  end
449
534
 
@@ -501,10 +586,133 @@ module Rubik
501
586
  end
502
587
 
503
588
  def normalized_document_path(file_path)
589
+ raise ArgumentError, "file_path cannot be nil or empty" if file_path.nil? || file_path.to_s.strip.empty?
590
+
591
+ return normalize_windows_document_path(file_path) if windows_document_path?(file_path) || windows_document_path?(@root_dir)
592
+
504
593
  path = Rubik::Platform.expand_path(file_path, @root_dir)
505
594
  Rubik::Platform.normalize_path(path)
506
595
  end
507
596
 
597
+ def windows_document_path?(path)
598
+ path.to_s.match?(%r{\A[A-Za-z]:[\\/]}) || path.to_s.start_with?("\\\\")
599
+ end
600
+
601
+ def normalize_windows_document_path(file_path)
602
+ raw = file_path.to_s.tr("\\", "/")
603
+ root = @root_dir.to_s.tr("\\", "/")
604
+ combined = if raw.match?(%r{\A[A-Za-z]:/}) || raw.start_with?("//")
605
+ raw
606
+ else
607
+ "#{root}/#{raw}"
608
+ end
609
+
610
+ prefix = combined.match(%r{\A([A-Za-z]:|//[^/]+/[^/]+)})&.[](1)
611
+ segments = combined.delete_prefix(prefix.to_s).split("/").each_with_object([]) do |segment, result|
612
+ next if segment.empty? || segment == "."
613
+
614
+ segment == ".." ? result.pop : result << segment
615
+ end
616
+ [prefix, *segments].compact.reject(&:empty?).join("/").downcase
617
+ end
618
+
619
+ def next_document_version
620
+ @version_counter += 1
621
+ end
622
+
623
+ def resolved_binary(bin)
624
+ if bin.include?("/") || (Rubik::Platform.windows? && bin.include?("\\"))
625
+ path = File.expand_path(bin, @root_dir)
626
+ return path if Rubik::Platform.executable_file?(path)
627
+
628
+ return nil
629
+ end
630
+
631
+ Rubik::Platform.which(bin)
632
+ end
633
+
634
+ def pending_diagnostics(version)
635
+ {
636
+ state: :pending,
637
+ document_version: version,
638
+ published_version: nil
639
+ }
640
+ end
641
+
642
+ def diagnostics_snapshot_unlocked(abs_path)
643
+ state = @diagnostic_states[abs_path]
644
+ {
645
+ state: state ? state[:state] : :unavailable,
646
+ diagnostics: (@diagnostics[abs_path] || []).dup,
647
+ document_version: state && state[:document_version],
648
+ published_version: state && state[:published_version]
649
+ }
650
+ end
651
+
652
+ def send_document_change_unlocked(abs_path, content)
653
+ return false unless @sync_capabilities[:change].to_i.positive?
654
+
655
+ previous_version = @open_documents[abs_path]
656
+ return false unless previous_version
657
+ return true if @document_contents[abs_path] == content
658
+
659
+ previous_content = @document_contents[abs_path]
660
+ previous_state = @mutex.synchronize { @diagnostic_states[abs_path] }
661
+ version = next_document_version
662
+ @open_documents[abs_path] = version
663
+ @document_contents[abs_path] = content
664
+ @mutex.synchronize { @diagnostic_states[abs_path] = pending_diagnostics(version) }
665
+
666
+ sent = send_notification("textDocument/didChange", {
667
+ textDocument: {
668
+ uri: document_uri(abs_path),
669
+ version: version
670
+ },
671
+ # A text-only event is a full-document replacement,
672
+ # which is valid for both Full and Incremental sync.
673
+ contentChanges: [{ text: content }]
674
+ })
675
+ unless sent
676
+ @open_documents[abs_path] = previous_version
677
+ @document_contents[abs_path] = previous_content
678
+ @mutex.synchronize do
679
+ previous_state ? @diagnostic_states[abs_path] = previous_state : @diagnostic_states.delete(abs_path)
680
+ end
681
+ end
682
+ sent
683
+ end
684
+
685
+ def normalize_sync_capabilities(value)
686
+ case value
687
+ when Numeric
688
+ change = value.to_i.clamp(0, 2)
689
+ {
690
+ source: :legacy_numeric,
691
+ open_close: change.positive?,
692
+ change: change,
693
+ save: false,
694
+ include_text: false
695
+ }.freeze
696
+ when Hash
697
+ save = value["save"]
698
+ {
699
+ source: :options,
700
+ open_close: value["openClose"] == true,
701
+ change: (value["change"] || 0).to_i.clamp(0, 2),
702
+ save: !!save,
703
+ include_text: save.is_a?(Hash) && save["includeText"] == true
704
+ }.freeze
705
+ else
706
+ {
707
+ source: :not_advertised,
708
+ open_close: false,
709
+ change: 0,
710
+ save: false,
711
+ include_text: false
712
+ }.freeze
713
+ end
714
+ end
715
+
508
716
  def document_uri(file_path)
509
717
  Rubik::Platform.to_file_uri(normalized_document_path(file_path))
510
718
  end
@@ -515,7 +723,12 @@ module Rubik
515
723
  params = msg["params"]
516
724
  items = params.is_a?(Hash) && params["items"].is_a?(Array) ? params["items"] : []
517
725
  write_message({ jsonrpc: "2.0", id: msg["id"], result: items.map { {} } })
518
- when "client/registerCapability", "client/unregisterCapability", "window/workDoneProgress/create"
726
+ when "client/registerCapability", "client/unregisterCapability"
727
+ write_message({
728
+ jsonrpc: "2.0", id: msg["id"],
729
+ error: { code: -32_602, message: "Dynamic capability registration is not supported" }
730
+ })
731
+ when "window/workDoneProgress/create"
519
732
  write_message({ jsonrpc: "2.0", id: msg["id"], result: nil })
520
733
  else
521
734
  write_message({
@@ -538,16 +751,27 @@ module Rubik
538
751
  def write_message(msg_hash)
539
752
  json = JSON.dump(msg_hash)
540
753
  header = "Content-Length: #{json.bytesize}\r\n\r\n"
541
- begin
542
- @stdin.write(header + json)
543
- rescue IOError, SystemCallError
544
- nil
545
- end
546
- begin
754
+ bytes = header + json
755
+ @write_mutex.synchronize do
756
+ offset = 0
757
+ while offset < bytes.bytesize
758
+ written = @stdin.write(bytes.byteslice(offset, bytes.bytesize - offset))
759
+ raise IOError, "LSP transport made no write progress" unless written&.positive?
760
+
761
+ offset += written
762
+ end
547
763
  @stdin.flush
548
- rescue IOError, SystemCallError
549
- nil
550
764
  end
765
+ true
766
+ rescue IOError, SystemCallError, NoMethodError => e
767
+ message = "LSP transport write failed: #{e.class}: #{e.message}"
768
+ @mutex.synchronize do
769
+ @reader_failure ||= message
770
+ @running = false
771
+ @crashed = true if @wait_thr
772
+ @cv.broadcast
773
+ end
774
+ false
551
775
  end
552
776
  end
553
777
  end