rubik 0.1.0 → 0.1.1

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: 4d73f60e02ac1d3902de40f910cb600612f5685c7e7a7ef2b7736b91567c14d8
4
- data.tar.gz: ecf9501583c3e5c480efe0ad5093ba4dc8a5d91be08b33292503ee0b6f414e2f
3
+ metadata.gz: 13f2d00bb2c72a92d48741df2cb3c3e84fc970a9ce3e4e844722afa58ef59599
4
+ data.tar.gz: f3314db467118bbd56aa79625f954f5c9e76611c56b5eb5e467d442fa440aa63
5
5
  SHA512:
6
- metadata.gz: ccdb0699c44357cfe03c15ff0755293b38fca1e8b25daa1a7c47458f770242ce91c87ec723df61463931306a096a1d811dee35dd7404bb1033701276e2a81b8b
7
- data.tar.gz: 21ec55890b5ba261f9cbb97b78fe7a87b07a9bc998e01ff4af52e7353935407a12003efcfae499af7c44004103b792aa42c85ee46fd5f647dc09b97842585015
6
+ metadata.gz: a5c29f5af7119d6b9189e37d39390c6dd2463caa1fd870e49022b92a0ed72481b227616000a84a2e201f26c7a65009f7fcb05ac6591db6ca1b9ecae3f81bc6d4
7
+ data.tar.gz: 3ab1188c4607f7dbcbe4011111723f530555052497154eaea6c8d47b09dc927bfcfa43b50b88869f2dc8bc1e402c7589a569d59ad584d29b991bfff765292d08
data/CHANGELOG.md CHANGED
@@ -8,6 +8,44 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ Nothing yet.
12
+
13
+ ## [0.1.1] - 2026-10-11
14
+
15
+ The Linux surface release: CI now proves Linux on Ruby 3.4 and 4.0
16
+ (`linux-test` is a required check), and the host seams that surface
17
+ exposed are fixed. Ships the structured goal ledger, the default-on
18
+ context pruner, and the vakanüvis recall/write hardening below.
19
+
20
+ ### Fixed
21
+
22
+ - **Fiber Scheduler: Net timeouts no longer fire watchdogs into
23
+ unrelated frames** (#102): Net::HTTP implemented its open/read
24
+ deadlines with the Timeout module — an async `Thread#raise` into the
25
+ originating thread. Under the scheduler that thread hops between
26
+ parked fibers, so an expired deadline landed in whichever frame was
27
+ running (observed on Linux killing the test reporter's stdout write
28
+ and `safe_select`'s `IO.select` itself). Deadlines are now
29
+ fiber-native: EventLoop timers drive them, and the timeout never
30
+ leaves the owning fiber.
31
+ - **LSP diagnostics keys normalize by path shape, not host OS** (#103):
32
+ a windows-drive root plus `file:///C:/...` uris and backslash lookups
33
+ resolved differently on a linux host; drive semantics now follow the
34
+ path's own shape.
35
+ - **Allowlist `.exe` alias matches lexically** (#103): the windows
36
+ executable-extension rule engaged through a second host seam the
37
+ platform stub did not cover; the alias is decided inside the matcher.
38
+ - **Task manager fixtures are shell-safe and poll for readiness**
39
+ (#103): fixed sleeps raced linux process startup, so a kill arrived
40
+ after the task had already FAILED.
41
+ - **REPL wake-loop**: subagent notices stranded at turn end re-armed the
42
+ reactive wake (synthetic Enter), so a steady subagent stream chained
43
+ autonomous maestro turns forever — the prompt never returned and the cycle
44
+ survived Ctrl+C, because the interrupted turn's own ensure block re-armed
45
+ the wake. Settlement invariant: an autonomous notice turn never spawns
46
+ another one, and an interrupted turn settles the wake debt; the
47
+ model-channel no-loss delivery is unchanged.
48
+
11
49
  ### Added
12
50
 
13
51
  - **Vakanüvis recall + write hardening** (issues #99/#100): `storage put`
@@ -91,16 +129,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
91
129
  subagent reported red?"); failed instances still get a body (task + state
92
130
  line) and the upsert/dedupe/canonical-link contract is unchanged.
93
131
 
94
- ### Fixed
95
-
96
- - **REPL wake-loop**: subagent notices stranded at turn end re-armed the
97
- reactive wake (synthetic Enter), so a steady subagent stream chained
98
- autonomous maestro turns forever — the prompt never returned and the cycle
99
- survived Ctrl+C, because the interrupted turn's own ensure block re-armed
100
- the wake. Settlement invariant: an autonomous notice turn never spawns
101
- another one, and an interrupted turn settles the wake debt; the
102
- model-channel no-loss delivery is unchanged.
103
-
104
132
  ## [0.1.0] - 2026-10-09
105
133
 
106
134
  First rubygems.org release of the `rubik` gem. Linux and macOS install out of
@@ -28,8 +28,21 @@ module Rubik
28
28
  # Thread#join/block), io-waiting, or timer-pending. Finished fibers leave
29
29
  # no registration, so "is anything waiting?" answers "are we done?".
30
30
  class EventLoop
31
- READABLE = 1
32
- WRITABLE = 2
31
+ # The VM's io_wait hook passes and expects the scheduler-protocol wait
32
+ # mask — the same domain as ::IO::READABLE / ::IO::WRITABLE (1 and 4;
33
+ # 2 is IO::PRIORITY). A local 1/2 pair silently drops WRITABLE
34
+ # interests (5.anybits?(2) is false): connects park forever until a
35
+ # deadline fires — the mis-aimed-watchdog crash of #102 never even
36
+ # needed the watchdog. Aliased to the real constants so a protocol
37
+ # drift surfaces as a NameError, not a hang.
38
+ READABLE = ::IO::READABLE
39
+ WRITABLE = ::IO::WRITABLE
40
+
41
+ # Timer generation slot for deadline registrations that must survive
42
+ # re-parks (Timeout.timeout spans many io waits within ONE timed
43
+ # operation). Sticky entries never go stale — they are cancelled
44
+ # explicitly by the owning fiber's ensure.
45
+ STICKY = :sticky
33
46
 
34
47
  attr_reader :spawn_errors
35
48
 
@@ -41,7 +54,11 @@ module Rubik
41
54
  # Stale timers (fiber re-parked or resumed since registration) must
42
55
  # not gate draining — @gen is bumped on every resume, so any of this
43
56
  # fiber's outstanding registrations becomes inert at that moment.
44
- @timers = Timers.new { |fiber, gen| @gen[fiber] != gen }
57
+ @timers = Timers.new do |fiber, gen|
58
+ next false if gen == STICKY
59
+
60
+ @gen[fiber] != gen
61
+ end
45
62
  @io_waiters = {} # io => { read: {fiber=>gen}, write: {fiber=>gen} }
46
63
  @gen = ::Hash.new(0) # fiber => park generation (loop thread only)
47
64
  @live = {} # fiber => true while a spawned fiber runs
@@ -212,6 +229,34 @@ module Rubik
212
229
  ::Addrinfo.getaddrinfo(hostname, nil).map(&:ip_address).uniq
213
230
  end
214
231
 
232
+ # True while this thread is inside run/run_until_drained (a boundary
233
+ # is draining). Timeout deadlines are fiber-native only in this
234
+ # window — outside it no loop consumes the timers, so the patch falls
235
+ # back to the stock Timeout watchdog.
236
+ def draining?
237
+ !@closed && !@loop_thread.nil?
238
+ end
239
+
240
+ # Fiber-native Timeout.timeout body (see concurrency/fiber_timeout.rb):
241
+ # runs the block inline and delivers the deadline exception through
242
+ # the interrupt path at the fiber's current park. The :sticky
243
+ # generation means re-parks during the timed operation never stale
244
+ # the deadline (a Net open spans many io waits); the ensure cancels
245
+ # the timer and retracts an already-fired, still-queued delivery, so
246
+ # a deadline that loses the race against block completion can never
247
+ # leak into a later operation.
248
+ def fiber_timeout(sec, exception_class, message)
249
+ fiber = ::Fiber.current
250
+ error = (exception_class || ::Timeout::Error).new(message || "execution expired")
251
+ timer = @timers.add(monotonic + sec, fiber, error, STICKY)
252
+ begin
253
+ yield
254
+ ensure
255
+ @timers.cancel(timer)
256
+ discard_queued(fiber, error)
257
+ end
258
+ end
259
+
215
260
  # ---- Reactor -------------------------------------------------------------
216
261
 
217
262
  # Runs the boundary body as a fiber and drains everything it spawned.
@@ -359,7 +404,7 @@ module Rubik
359
404
 
360
405
  def fire_due_timers
361
406
  @timers.fire_due!(monotonic) do |fiber, value, gen|
362
- next if @gen[fiber] != gen # stale: fiber moved on after registering
407
+ next if gen != STICKY && @gen[fiber] != gen # stale: fiber moved on after registering
363
408
 
364
409
  drop_io_interest(fiber)
365
410
  push(fiber, value: value)
@@ -427,6 +472,20 @@ module Rubik
427
472
  nil
428
473
  end
429
474
 
475
+ # Retract a queued resume: a deadline that fired while its fiber was
476
+ # running (mid-operation) sits in @runnable until the next pop —
477
+ # without retraction, the block's completion would leave the
478
+ # exception aimed at the fiber's NEXT park (the watchdog-leak shape,
479
+ # in miniature). Same-thread ordering makes this airtight: the entry
480
+ # can only be consumed by a resume that delivers it into the timed
481
+ # block, and then the ensure's cancel finds nothing to retract.
482
+ def discard_queued(fiber, value)
483
+ @mutex.synchronize do
484
+ @runnable.reject! { |(queued_fiber, queued_value)| queued_fiber.equal?(fiber) && queued_value.equal?(value) }
485
+ end
486
+ nil
487
+ end
488
+
430
489
  def kick
431
490
  @wake_write.write_nonblock(".", exception: false)
432
491
  rescue StandardError
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Rubik
6
+ module Concurrency
7
+ # #102: Net::HTTP (and any stdlib code) enforces deadlines with the
8
+ # Timeout module — a watchdog Thread that Thread#raises the calling
9
+ # thread when the deadline expires. Under the scheduler that thread is
10
+ # the loop thread, shared by every parked fiber: the raise lands in
11
+ # whichever frame is executing (another fiber's IO.select, the minitest
12
+ # reporter), and after an abandoned drain it fires into unrelated
13
+ # runner code.
14
+ #
15
+ # While a boundary is draining on this thread, Timeout.timeout is
16
+ # fiber-native instead: the block runs inline (no watchdog thread is
17
+ # ever created) and the deadline exception is delivered through the
18
+ # loop's interrupt path at the calling fiber's current park. Outside a
19
+ # drain there is no loop to consume timers, so the stock watchdog
20
+ # applies — a thread-raise into the thread that is genuinely blocked is
21
+ # correctly targeted there.
22
+ module TimeoutPatch
23
+ def timeout(sec = nil, exception_class = nil, message = nil, &)
24
+ scheduler = ::Fiber.scheduler
25
+ if sec.is_a?(::Numeric) && sec.positive? &&
26
+ scheduler.is_a?(::Rubik::Concurrency::Scheduler) && scheduler.native_deadlines?
27
+ return scheduler.fiber_timeout(sec, exception_class, message, &)
28
+ end
29
+
30
+ super
31
+ end
32
+ end
33
+
34
+ ::Timeout.singleton_class.prepend(TimeoutPatch)
35
+ end
36
+ end
@@ -75,6 +75,21 @@ module Rubik
75
75
  @loop.resolve_address(hostname)
76
76
  end
77
77
 
78
+ # True while the loop is actively draining on this thread — the only
79
+ # window in which Timeout deadlines can be driven by loop timers
80
+ # (outside it the stock Timeout watchdog applies).
81
+ def native_deadlines?
82
+ @loop.draining?
83
+ end
84
+
85
+ # Runs block with a fiber-native deadline: the exception arrives at
86
+ # the fiber's current park instead of a watchdog Thread#raise into
87
+ # the loop thread (#102). Loop-thread only — the Timeout patch routes
88
+ # here exclusively from scheduled fibers.
89
+ def fiber_timeout(sec, exception_class, message, &)
90
+ @loop.fiber_timeout(sec, exception_class, message, &)
91
+ end
92
+
78
93
  def scheduler_close
79
94
  @loop.shutdown
80
95
  nil
@@ -68,6 +68,24 @@ module Rubik
68
68
  fired
69
69
  end
70
70
 
71
+ # Removes a pending entry (identity comparison — entries are arrays
72
+ # that may compare structurally equal) and re-heapifies around the
73
+ # hole. Returns true when the entry was still pending. Only the loop
74
+ # thread mutates the heap and cancellation runs on the loop thread
75
+ # too, so a cancelled entry can never fire afterwards.
76
+ def cancel(entry)
77
+ index = @heap.index { |candidate| candidate.equal?(entry) }
78
+ return false unless index
79
+
80
+ last = @heap.pop
81
+ return true if index >= @heap.size
82
+
83
+ @heap[index] = last
84
+ sift_down(index)
85
+ sift_up(index)
86
+ true
87
+ end
88
+
71
89
  private
72
90
 
73
91
  def prune!
@@ -6,6 +6,7 @@ require_relative "concurrency/event_loop"
6
6
  require_relative "concurrency/scheduler"
7
7
  require_relative "concurrency/boundary"
8
8
  require_relative "concurrency/join"
9
+ require_relative "concurrency/fiber_timeout"
9
10
 
10
11
  module Rubik
11
12
  # Silent concurrency over Ruby's Fiber Scheduler: plain blocking I/O —
@@ -17,10 +18,12 @@ module Rubik
17
18
  # Envelope (documented limits, plan §9): this is I/O concurrency, not CPU
18
19
  # parallelism (the GVL still serializes computation); regular-file reads
19
20
  # never park (read_nonblock on files returns immediately — they simply run
20
- # serially, which is harmless); Timeout.timeout-backed code paths are not
21
- # adopted inside boundaries in v1 (Thread.raise cannot be aimed at a
22
- # specific fiber). Escape hatch: RUBIK_FIBER_SCHEDULER=off degrades every
23
- # boundary to plain sequential execution.
21
+ # serially, which is harmless). Timeout.timeout-backed deadlines (Net::HTTP
22
+ # open/read) are fiber-native inside boundaries: the watchdog Thread#raise
23
+ # is replaced by a loop-timer delivery into the calling fiber, so it can
24
+ # never land in another fiber's frame or in post-shutdown runner code
25
+ # (concurrency/fiber_timeout.rb). Escape hatch: RUBIK_FIBER_SCHEDULER=off
26
+ # degrades every boundary to plain sequential execution.
24
27
  module Concurrency
25
28
  module_function
26
29
 
@@ -28,7 +28,7 @@ module Rubik
28
28
 
29
29
  def initialize(language, root_dir: Dir.pwd, server_cmd: nil)
30
30
  @language = language.to_s.downcase
31
- @root_dir = File.expand_path(root_dir)
31
+ @root_dir = Rubik::Platform.expand_path(root_dir)
32
32
  cmd = server_cmd || SERVERS[@language]
33
33
  if (cmd.to_s.start_with?("typeprof") || cmd.to_s.start_with?("typescript-language-server")) && !cmd.to_s.include?("--stdio")
34
34
  cmd = "#{cmd} --stdio"
@@ -232,7 +232,7 @@ module Rubik
232
232
  def did_open(file_path, content, language_id = @language)
233
233
  return false unless alive?
234
234
 
235
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(file_path, @root_dir))
235
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
236
236
  @mutex.synchronize do
237
237
  @version_counter += 1
238
238
  @open_documents[abs_path] = @version_counter
@@ -254,7 +254,7 @@ module Rubik
254
254
  def did_change(file_path, content)
255
255
  return false unless alive?
256
256
 
257
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(file_path, @root_dir))
257
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
258
258
  version = @mutex.synchronize do
259
259
  @version_counter += 1
260
260
  @open_documents[abs_path] = @version_counter
@@ -281,7 +281,7 @@ module Rubik
281
281
  def did_close(file_path)
282
282
  return false unless @running
283
283
 
284
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(file_path, @root_dir))
284
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
285
285
  @mutex.synchronize { @open_documents.delete(abs_path) }
286
286
 
287
287
  send_notification("textDocument/didClose", {
@@ -295,14 +295,14 @@ module Rubik
295
295
  def document_open?(file_path)
296
296
  return false if file_path.nil? || file_path.to_s.strip.empty?
297
297
 
298
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(file_path, @root_dir))
298
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
299
299
  @mutex.synchronize { @open_documents.key?(abs_path) }
300
300
  rescue ArgumentError
301
301
  false
302
302
  end
303
303
 
304
304
  def diagnostics_for(file_path)
305
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(file_path, @root_dir))
305
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
306
306
  root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
307
307
  rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : file_path.to_s.tr("\\", "/")
308
308
  @mutex.synchronize do
@@ -381,7 +381,7 @@ module Rubik
381
381
  params = msg["params"] || {}
382
382
  uri = params["uri"].to_s
383
383
  raw_path = Rubik::Platform.from_file_uri(uri)
384
- abs_path = Rubik::Platform.normalize_path_raw(File.expand_path(raw_path, @root_dir))
384
+ abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(raw_path, @root_dir))
385
385
  root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
386
386
  rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : abs_path
387
387
  diags = params["diagnostics"] || []
@@ -65,7 +65,7 @@ module Rubik
65
65
  # RFC 8089 Compliant File URI (e.g. file:///C:/path/file vs file:///path/file)
66
66
  def to_file_uri(path)
67
67
  normalized = normalize_path_raw(path)
68
- if windows? && normalized =~ %r{^([a-zA-Z]):/(.*)$}
68
+ if (windows? || windows_shaped?(normalized)) && normalized =~ %r{^([a-zA-Z]):/(.*)$}
69
69
  "file:///#{::Regexp.last_match(1)}:/#{::Regexp.last_match(2)}"
70
70
  else
71
71
  normalized.start_with?("/") ? "file://#{normalized}" : "file:///#{normalized}"
@@ -73,23 +73,49 @@ module Rubik
73
73
  end
74
74
 
75
75
  def from_file_uri(uri)
76
- str = uri.to_s.sub(%r{^file://}, "")
76
+ str = uri.to_s.sub(%r{\Afile://}, "")
77
77
  str = CGI.unescape(str)
78
- if windows?
79
- # Handles file:///C:/... and file://C:/...
80
- if str =~ %r{^/?([a-zA-Z]:/.*)$}
78
+ if windows? || str.match?(%r{\A/?[a-zA-Z]:/})
79
+ # Handles file:///C:/... and file://C:/... — keyed by SHAPE, so
80
+ # the same drive URIs resolve identically on a posix host (#103).
81
+ if str =~ %r{\A/?([a-zA-Z]:/.*)\z}
81
82
  ::Regexp.last_match(1)
82
83
  else
83
84
  str
84
85
  end
86
+ elsif str.start_with?("/")
87
+ str
85
88
  else
86
- str.start_with?("/") ? str : "/#{str}"
89
+ "/#{str}"
87
90
  end
88
91
  end
89
92
 
93
+ # Windows path SHAPE: a drive-letter root (C:/ or C:\). Host OS and
94
+ # path shape are independent — a windows-shaped path can reach a
95
+ # posix host (LSP payloads, fixtures, mounted drives) and its drive
96
+ # semantics must not depend on where the process happens to run
97
+ # (#103). Only drive-rooted paths count: a backslash is a legal
98
+ # character in a posix filename, so it never triggers windows
99
+ # semantics on its own.
100
+ def windows_shaped?(path)
101
+ path.to_s.match?(%r{\A[a-zA-Z]:[/\\]})
102
+ end
103
+
104
+ # File.expand_path is host-OS-driven: on a posix host a drive-rooted
105
+ # windows path is treated as a RELATIVE name and mangled under cwd.
106
+ # A windows-shaped path carries its own root, so expansion is a no-op
107
+ # and only the separators normalize. Posix-shaped inputs and windows
108
+ # hosts take the stock path, byte-for-byte — including its TYPE
109
+ # strictness: callers' error paths depend on File.expand_path raising
110
+ # on non-strings, so never coerce here.
111
+ def expand_path(path, base = nil)
112
+ return path.tr("\\", "/") if !windows? && windows_shaped?(path)
113
+
114
+ ::File.expand_path(path, base)
115
+ end
116
+
90
117
  def normalize_path_raw(path)
91
- expanded = File.expand_path(path.to_s)
92
- expanded.gsub("\\", "/")
118
+ expand_path(path.to_s).gsub("\\", "/")
93
119
  end
94
120
 
95
121
  # Normalizes separators and casing for comparison
@@ -257,6 +257,14 @@ module Rubik
257
257
 
258
258
  def binary_allowlist_match?(arg, token)
259
259
  return true if arg == token
260
+ # Windows exe-alias fast path (#103): on a windows host "node.exe"
261
+ # IS the allowlisted "node" — a lexical alias, no filesystem probe.
262
+ # The canonical-path probe below resolves through Platform.which +
263
+ # File.file?, a second host seam the windows? stub cannot cover (a
264
+ # linux host has no node.exe on disk, so the probe fails and the
265
+ # allowlist would dead-close). On a posix host the alias is FALSE:
266
+ # node.exe is a different binary than node there.
267
+ return true if windows_executable_alias?(arg, token)
260
268
 
261
269
  c_arg = canonical_binary_path(arg)
262
270
  c_token = canonical_binary_path(token)
@@ -266,6 +274,19 @@ module Rubik
266
274
  false
267
275
  end
268
276
 
277
+ # True when arg carries an executable extension from PATHEXT and its
278
+ # stem equals the token (case-insensitive — windows path semantics).
279
+ # Bare stems only: a path-form arg ("C:/x/node.exe") keeps going
280
+ # through the canonical-path probe, which resolves both sides.
281
+ def windows_executable_alias?(arg, token)
282
+ return false unless Rubik::Platform.windows?
283
+
284
+ ext = ::File.extname(arg)
285
+ return false if ext.empty? || !Rubik::Platform.pathext.include?(ext.downcase)
286
+
287
+ arg.delete_suffix(ext).casecmp?(token)
288
+ end
289
+
269
290
  def command_matches_allowlist?(result)
270
291
  # Missing argv (e.g. empty invocation) can never be allowlisted.
271
292
  argv = result.respond_to?(:argv) ? result.argv : nil
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.0"
4
+ VERSION = "0.1.1"
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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır
@@ -276,6 +276,7 @@ files:
276
276
  - lib/rubik/concurrency/dns_pool.rb
277
277
  - lib/rubik/concurrency/errors.rb
278
278
  - lib/rubik/concurrency/event_loop.rb
279
+ - lib/rubik/concurrency/fiber_timeout.rb
279
280
  - lib/rubik/concurrency/join.rb
280
281
  - lib/rubik/concurrency/process_waiter.rb
281
282
  - lib/rubik/concurrency/scheduler.rb