bitfab 0.57.6 → 0.57.7

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: 34dfb7ca50f4c2dceaa4aa4ed46ee6e9426a5d8051a3602084fee0640a77c9e9
4
- data.tar.gz: ad1e112ccdfc9a2cee2860b602e3edf3629686ef6228e81ac4b80f5132ae1fc1
3
+ metadata.gz: b92a57be2a0c05885f0f84f757ede302ab14aed53ad422c85ee2781591a39786
4
+ data.tar.gz: ddb9b050e2f66e9591c17be161c2ebd25d395b625704baee80e396801f5a9e25
5
5
  SHA512:
6
- metadata.gz: 764ad6880ee3656d90d28ecb5571298cc54ab2bd1bc6a6b2b07399fd2bee8fd151ae3adcfdbf9f2bdc8f7c0d0fccbb1da29e33c93dc68353e11058be22dc9cc8
7
- data.tar.gz: afadb371e2d697bab8aaac2a4f02a57491ab37745b6b756ca3302931915f6fec7eabcc80d4789e79f06a5f5b9dae9ab22627a1fc4f35a7f2f610d175da2c12ef
6
+ metadata.gz: 0542a3b4dcb64f183ce35f7c9f16ead26e858fa1fefb7889115842e552b4855842ba175bea19893912ec37547708403d115259a201b2e02a1e6cb5ffa84f79f6
7
+ data.tar.gz: a530cd08bc1cdbc42467ea640c8f64e3035569c6723032626284bd6397744a21f5149ef0bc2d32af50aab1b1bbb856e8ba7510d1eb3640824de4f6f4e4472c43
data/lib/bitfab/baml.rb CHANGED
@@ -170,7 +170,6 @@ module Bitfab
170
170
  end
171
171
 
172
172
  class Client
173
- # Execute a stored BAML prompt locally and return its parsed output.
174
173
  def call(method_name, **inputs)
175
174
  version = @http_client.request("/api/sdk/functions/lookup", {"name" => method_name})
176
175
  raise ArgumentError, "Function #{method_name.inspect} not found. Create it at #{@service_url}/functions" unless version["id"]
@@ -182,7 +181,9 @@ module Bitfab
182
181
  rescue
183
182
  result.to_s
184
183
  end
185
- @http_client.send_internal_trace(version["id"], {"result" => output, "inputs" => inputs, "rawCollector" => collector, "source" => "ruby-sdk"})
184
+ if should_record? && !active_span_content_withheld?
185
+ @http_client.send_internal_trace(version["id"], {"result" => output, "inputs" => inputs, "rawCollector" => collector, "source" => "ruby-sdk"})
186
+ end
186
187
  result
187
188
  end
188
189
 
data/lib/bitfab/client.rb CHANGED
@@ -49,14 +49,10 @@ module Bitfab
49
49
  @enabled_in_code = capture_enabled.nil? ? enabled : capture_enabled
50
50
  @capture_configured_from_env = nil
51
51
  @strict = strict
52
- # Cached only once a non-empty key is found, so an early resolve (before
53
- # env loaded) can't poison a later one.
54
52
  @resolved_api_key = nil
55
53
  @api_key_warned = false
56
- # The key is NOT read here. HttpClient gets a proc so the key is resolved
57
- # at send time, after any env loading has run.
58
54
  @http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url, timeout:)
59
- @http_client.configure_simulation_plan(enabled: simulation_plan)
55
+ @http_client.configure_simulation_plan(enabled: simulation_plan, start: !api_key_resolved_lazily?)
60
56
  @datasets = Datasets.new(@http_client)
61
57
  @assertion_categories = AssertionCategories.new(@http_client)
62
58
  @traces = Traces.new(@http_client)
@@ -381,11 +377,10 @@ module Bitfab
381
377
  explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil,
382
378
  surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
383
379
  result_recorded_elsewhere: false)
384
- # Decide at CALL time, not construction. The key may be set after the
385
- # client is built (env loaded later), so re-checking per call lets a
386
- # late-resolved key take effect.
387
380
  return yield unless should_record?
388
381
 
382
+ @http_client.refresh_simulation_plan
383
+
389
384
  resolved_capture_when = capture_when.to_s
390
385
  unless ["always", "nested"].include?(resolved_capture_when)
391
386
  Bitfab.warn_once(
@@ -513,6 +508,12 @@ module Bitfab
513
508
  completed = false
514
509
  @http_client.trace_completion.start(trace_id, span_id)
515
510
  recording_override = managed_root ? {} : nil
511
+ content_withheld = lambda do
512
+ instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
513
+ content_off_by_simulation_plan?(
514
+ trace_id:, trace_function_key:, span_name: recording_override&.dig(:name) || span_name, instrumentation:
515
+ ) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation:))
516
+ end
516
517
 
517
518
  complete = lambda do |final_result, final_error|
518
519
  # Never crash the host app due to span building/sending. Idempotent:
@@ -528,9 +529,7 @@ module Bitfab
528
529
  recorded_error = final_error
529
530
  recorded_name = recording_override&.dig(:name) || span_name
530
531
  recorded_instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
531
- content_off = content_off_by_simulation_plan?(
532
- trace_id:, trace_function_key:, span_name: recorded_name, instrumentation: recorded_instrumentation
533
- ) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation: recorded_instrumentation))
532
+ content_off = content_withheld.call
534
533
  if finalize && final_error.nil? && !(content_off && !result_recorded_elsewhere)
535
534
  begin
536
535
  recorded_result = finalize.call(final_result)
@@ -613,7 +612,7 @@ module Bitfab
613
612
  span_id:,
614
613
  explicit_span_receiver:,
615
614
  explicit_span_method_name: explicit_span_method_name&.to_sym,
616
- surface:, recording_override:
615
+ surface:, recording_override:, content_withheld:
617
616
  ) do
618
617
  invoke = -> { around_body ? around_body.call { yield } : yield }
619
618
  result = if explicit_span_receiver
@@ -704,6 +703,18 @@ module Bitfab
704
703
  nil
705
704
  end
706
705
 
706
+ def active_span_content_withheld?
707
+ return true if Subtree.current_session&.current_span_content_withheld?
708
+
709
+ SpanContext.current&.dig(:content_withheld)&.call == true
710
+ rescue
711
+ true
712
+ end
713
+
714
+ def api_key_resolved_lazily?
715
+ @api_key_config.respond_to?(:call) || @api_key_config.to_s.strip.empty?
716
+ end
717
+
707
718
  # Resolve the API key lazily, the first time a span actually needs it.
708
719
  #
709
720
  # The key is intentionally NOT read at construction: a client built when a
@@ -955,7 +966,7 @@ module Bitfab
955
966
  span_data["error_source"] = "code"
956
967
  end
957
968
  span_data["contexts"] = contexts if contexts&.any?
958
- span_data["prompt"] = prompt if prompt
969
+ span_data["prompt"] = prompt if prompt && !content_off
959
970
 
960
971
  raw_span = {
961
972
  "id" => span_id,
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module Bitfab
6
+ class ConnectionPool
7
+ KEEP_ALIVE_TIMEOUT_SECONDS = 30
8
+ MAX_IDLE_CONNECTIONS = 64
9
+
10
+ IdleConnection = Struct.new(:http, :released_at)
11
+ private_constant :IdleConnection
12
+
13
+ @registry = ObjectSpace::WeakMap.new
14
+ @registry_mutex = Mutex.new
15
+
16
+ class << self
17
+ def register(pool)
18
+ @registry_mutex.synchronize { @registry[pool] = true }
19
+ end
20
+
21
+ def close_all
22
+ pools = @registry_mutex.synchronize { @registry.keys }
23
+ pools.each(&:close)
24
+ end
25
+ end
26
+
27
+ def initialize
28
+ @pid = Process.pid
29
+ @mutex = Mutex.new
30
+ @idle = {}
31
+ self.class.register(self)
32
+ end
33
+
34
+ def with_connection(uri, timeout)
35
+ key = [uri.scheme, uri.host, uri.port]
36
+ owner_pid = Process.pid
37
+ reusable = false
38
+ http = checkout(key, uri, timeout)
39
+ response = yield http
40
+ reusable = true
41
+ response
42
+ ensure
43
+ release(key, http, owner_pid, reusable) if http
44
+ end
45
+
46
+ def close
47
+ drain.each { |http| finish(http) }
48
+ end
49
+
50
+ def idle_count
51
+ reset_after_fork
52
+ @mutex.synchronize { @idle.sum { |_key, connections| connections.size } }
53
+ end
54
+
55
+ private
56
+
57
+ def checkout(key, uri, timeout)
58
+ http = take_idle(key) || connect(uri, timeout)
59
+ http.read_timeout = timeout
60
+ http.write_timeout = timeout
61
+ http
62
+ end
63
+
64
+ def connect(uri, timeout)
65
+ http = Net::HTTP.new(uri.host, uri.port)
66
+ http.use_ssl = uri.scheme == "https"
67
+ http.open_timeout = timeout
68
+ http.read_timeout = timeout
69
+ http.write_timeout = timeout
70
+ http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT_SECONDS
71
+ http.start
72
+ end
73
+
74
+ def take_idle(key)
75
+ reset_after_fork
76
+ now = monotonic_now
77
+ expired = []
78
+ http = @mutex.synchronize do
79
+ connections = @idle[key] || []
80
+ stale, fresh = connections.partition { |idle| now - idle.released_at >= KEEP_ALIVE_TIMEOUT_SECONDS }
81
+ expired.concat(stale.map(&:http))
82
+ @idle[key] = fresh
83
+ fresh.pop&.http
84
+ end
85
+ expired.each { |stale_http| finish(stale_http) }
86
+ http
87
+ end
88
+
89
+ def release(key, http, owner_pid, reusable)
90
+ return unless owner_pid == Process.pid
91
+
92
+ unless reusable && http.started?
93
+ finish(http)
94
+ return
95
+ end
96
+
97
+ reset_after_fork
98
+ pooled = @mutex.synchronize do
99
+ next false if @idle.sum { |_key, connections| connections.size } >= MAX_IDLE_CONNECTIONS
100
+
101
+ (@idle[key] ||= []) << IdleConnection.new(http, monotonic_now)
102
+ true
103
+ end
104
+ finish(http) unless pooled
105
+ end
106
+
107
+ def drain
108
+ return [] if reset_after_fork
109
+
110
+ @mutex.synchronize do
111
+ connections = @idle.values.flatten.map(&:http)
112
+ @idle = {}
113
+ connections
114
+ end
115
+ end
116
+
117
+ def reset_after_fork
118
+ return false if @pid == Process.pid
119
+
120
+ @pid = Process.pid
121
+ @mutex = Mutex.new
122
+ @idle = {}
123
+ true
124
+ end
125
+
126
+ def finish(http)
127
+ http.finish if http.started?
128
+ rescue
129
+ nil
130
+ end
131
+
132
+ def monotonic_now
133
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
+ end
135
+ end
136
+ end
@@ -6,6 +6,7 @@ require "uri"
6
6
 
7
7
  require_relative "git_state"
8
8
  require_relative "compress"
9
+ require_relative "connection_pool"
9
10
  require_relative "constants"
10
11
  require_relative "serialize"
11
12
  require_relative "transport"
@@ -37,6 +38,7 @@ module Bitfab
37
38
  @api_key = api_key
38
39
  @service_url = (service_url || DEFAULT_SERVICE_URL).chomp("/")
39
40
  @timeout = timeout
41
+ @connections = ConnectionPool.new
40
42
  @transport_mutex = Mutex.new
41
43
  # Eager for the same reason the transport's mutex is: `||=` racing between
42
44
  # the export threads recording acks would hand each a different mutex.
@@ -53,9 +55,9 @@ module Bitfab
53
55
  @close_condition = ConditionVariable.new
54
56
  end
55
57
 
56
- def configure_simulation_plan(enabled: true)
58
+ def configure_simulation_plan(enabled: true, start: true)
57
59
  @simulation_plan = SimulationPlan.new(self, enabled:)
58
- @simulation_plan.refresh
60
+ @simulation_plan.refresh if start
59
61
  @simulation_plan
60
62
  end
61
63
 
@@ -80,7 +82,7 @@ module Bitfab
80
82
  end
81
83
 
82
84
  def get_simulation_plan
83
- get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT, connection_close: true)
85
+ get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT)
84
86
  end
85
87
 
86
88
  # Flush and permanently close this client's tracing transport.
@@ -112,6 +114,7 @@ module Bitfab
112
114
  shutdown = transport.nil? || transport.shutdown(remaining)
113
115
  result = released && shutdown
114
116
  ensure
117
+ @connections.close
115
118
  @transport_mutex.synchronize do
116
119
  @close_result = result
117
120
  @close_condition.broadcast
@@ -157,16 +160,11 @@ module Bitfab
157
160
  last_error = nil
158
161
 
159
162
  max_retries.times do |attempt|
160
- http = Net::HTTP.new(uri.host, uri.port)
161
- http.use_ssl = uri.scheme == "https"
162
- http.open_timeout = request_timeout
163
- http.read_timeout = request_timeout
164
-
165
163
  req = Net::HTTPGenericRequest.new(method, true, true, uri.request_uri, headers)
166
164
  req["Content-Encoding"] = prepared.content_encoding if prepared.content_encoding
167
165
  req.body = prepared.body
168
166
 
169
- response = http.request(req)
167
+ response = @connections.with_connection(uri, request_timeout) { |http| http.request(req) }
170
168
 
171
169
  unless response.is_a?(Net::HTTPSuccess)
172
170
  raise Net::HTTPError.new("HTTP #{response.code}: #{response.body}", response)
@@ -216,19 +214,12 @@ module Bitfab
216
214
 
217
215
  # Make a GET request to the Bitfab API.
218
216
  # Returns parsed JSON response hash.
219
- def get(endpoint, timeout: nil, connection_close: false)
217
+ def get(endpoint, timeout: nil)
220
218
  uri = URI("#{@service_url}#{endpoint}")
221
219
  request_timeout = timeout || @timeout
222
220
 
223
- http = Net::HTTP.new(uri.host, uri.port)
224
- http.use_ssl = uri.scheme == "https"
225
- http.open_timeout = request_timeout
226
- http.read_timeout = request_timeout
227
-
228
- # request_uri (not path) so any query string on the endpoint survives.
229
221
  req = Net::HTTP::Get.new(uri.request_uri, headers)
230
- req["Connection"] = "close" if connection_close
231
- response = http.request(req)
222
+ response = @connections.with_connection(uri, request_timeout) { |http| http.request(req) }
232
223
 
233
224
  unless response.is_a?(Net::HTTPSuccess)
234
225
  raise Net::HTTPError.new("HTTP #{response.code}: #{response.body}", response)
@@ -742,5 +733,6 @@ module Bitfab
742
733
  at_exit do
743
734
  SimulationPlan.release_all(2, stop: true)
744
735
  Transport.shutdown_trace_transports(2)
736
+ ConnectionPool.close_all
745
737
  end
746
738
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Bitfab
4
4
  class SimulationPlan
5
- CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized].freeze
5
+ CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized prompt].freeze
6
6
  FRAMEWORKS = %w[openai-agents langgraph claude-agent-sdk vercel-ai].freeze
7
7
  READ_TIMEOUT = 5.0
8
8
  REFRESH_SECONDS = 60.0
@@ -54,6 +54,7 @@ module Bitfab
54
54
  @draining = Set.new
55
55
  @stopped = false
56
56
  @first_read_finished = false
57
+ @first_read_deadline = nil
57
58
  self.class.register(self)
58
59
  end
59
60
 
@@ -144,14 +145,13 @@ module Bitfab
144
145
  @mutex.synchronize { @content_off.nil? && !reading? }
145
146
  end
146
147
 
147
- def wait_for_first_read(timeout = READ_TIMEOUT)
148
- reset_after_fork
148
+ def wait_for_first_read
149
+ refresh
149
150
  return if disabled?
150
151
 
151
- deadline = now + [timeout, 0].max
152
152
  @mutex.synchronize do
153
- while reading? && @thread
154
- remaining = deadline - now
153
+ while reading? && @thread && @first_read_deadline
154
+ remaining = @first_read_deadline - now
155
155
  break unless remaining.positive?
156
156
  @condition.wait(@mutex, remaining)
157
157
  end
@@ -229,6 +229,7 @@ module Bitfab
229
229
  end
230
230
 
231
231
  def start_reader(delay)
232
+ @first_read_deadline ||= now + delay + READ_TIMEOUT
232
233
  @thread = Thread.new { read_until_loaded(delay) }
233
234
  @thread.name = "bitfab-sim-plan"
234
235
  rescue ThreadError
@@ -302,6 +303,7 @@ module Bitfab
302
303
  @thread = nil
303
304
  @held = []
304
305
  @draining = Set.new
306
+ @first_read_deadline = nil unless @first_read_finished
305
307
  end
306
308
  end
307
309
  end
@@ -150,10 +150,11 @@ module Bitfab
150
150
 
151
151
  # Execute a block with a new span pushed onto the stack.
152
152
  # The span is automatically popped when the block completes.
153
- def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil, surface: nil, recording_override: nil)
153
+ def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil, surface: nil, recording_override: nil, content_withheld: nil)
154
154
  entry = {trace_id:, span_id:}
155
155
  entry[:surface] = surface if surface
156
156
  entry[:recording_override] = recording_override if recording_override
157
+ entry[:content_withheld] = content_withheld if content_withheld
157
158
  if explicit_span_receiver
158
159
  entry[:explicit_span_receiver] = explicit_span_receiver
159
160
  entry[:explicit_span_method_name] = explicit_span_method_name
@@ -12,6 +12,7 @@ module Bitfab
12
12
  DEFAULT_MAX_DEPTH = 30
13
13
  DEFAULT_MAX_SPANS = 2048
14
14
  DEFAULT_MAX_CAPTURED_SUBTREE_SPANS = 512
15
+ LIMIT_ORDER = %i[max_depth max_spans max_captured_subtree_spans].freeze
15
16
  SESSION_KEY = :bitfab_subtree_session
16
17
  DISPATCHER_KEY = :bitfab_subtree_dispatcher
17
18
  FIBER_CONTEXT_KEY = :bitfab_subtree_fiber_context
@@ -444,7 +445,8 @@ module Bitfab
444
445
  @method_metadata = Hash.new { |owners, owner| owners[owner] = {} }
445
446
  @spans_used = 1
446
447
  @captured_subtree_count = 0
447
- @truncated = false
448
+ @limits_fired = Set.new
449
+ @dropped_spans = 0
448
450
  @exhausted = false
449
451
  @drained = false
450
452
  @deferred_frames = []
@@ -478,7 +480,7 @@ module Bitfab
478
480
  unless @deferred
479
481
  @fiber_parents.clear
480
482
  finish_open_spans
481
- warn_if_truncated
483
+ record_truncation
482
484
  end
483
485
  end
484
486
 
@@ -491,7 +493,7 @@ module Bitfab
491
493
  end
492
494
 
493
495
  def handle(event)
494
- return if @drained
496
+ return count_drained_call(event) if @drained
495
497
 
496
498
  if event.event == :raise
497
499
  record_error(event)
@@ -523,16 +525,21 @@ module Bitfab
523
525
  def reserve_node
524
526
  stack = current_stack(Fiber.current.object_id)
525
527
  if captured_depth(stack) >= @max_depth
526
- @truncated = true
528
+ drop(:max_depth)
527
529
  return false
528
530
  end
529
531
  return true if take_slot(discovered: false)
530
532
 
531
- @truncated = true
533
+ drop(:max_spans)
532
534
  @exhausted = true
533
535
  false
534
536
  end
535
537
 
538
+ def current_span_content_withheld?
539
+ entry = @stacks[Fiber.current.object_id]&.reverse_each&.find { |candidate| candidate.is_a?(OpenSpan) }
540
+ entry&.span_data&.dig("content_off_by_simulation_plan") == true
541
+ end
542
+
536
543
  def configured_entry(name:, type:, args:, kwargs:, test_run_id:)
537
544
  return unless reserve_node
538
545
 
@@ -624,11 +631,13 @@ module Bitfab
624
631
  fiber_id = Fiber.current.object_id
625
632
  if @suppressed_depths[fiber_id].to_i.positive?
626
633
  @suppressed_depths[fiber_id] += 1
634
+ count_dropped_call(event, resolved_path)
627
635
  return
628
636
  end
629
637
 
630
638
  if @exhausted
631
639
  suppress(fiber_id)
640
+ count_dropped_call(event, resolved_path)
632
641
  return
633
642
  end
634
643
 
@@ -638,13 +647,13 @@ module Bitfab
638
647
 
639
648
  stack = current_stack(fiber_id)
640
649
  if captured_depth(stack) >= @max_depth
641
- @truncated = true
650
+ drop(:max_depth)
642
651
  suppress(fiber_id)
643
652
  return
644
653
  end
645
654
  content_off = content_off?(metadata.name)
646
655
  if (limit = limit_reached(discovered: true, content_off:))
647
- @truncated = true
656
+ drop(limit)
648
657
  stop_discovery(fiber_id) unless limit == :max_captured_subtree_spans && content_off_names?
649
658
  return
650
659
  end
@@ -927,9 +936,35 @@ module Bitfab
927
936
  false
928
937
  end
929
938
 
930
- def warn_if_truncated
931
- return unless @truncated
939
+ def drop(limit)
940
+ @limits_fired << limit
941
+ @dropped_spans += 1
942
+ end
943
+
944
+ def count_dropped_call(event, resolved_path)
945
+ metadata = metadata_for(event, resolved_path)
946
+ return if metadata.excluded || (!@include_wrappers && metadata.passthrough)
947
+
948
+ @dropped_spans += 1
949
+ end
950
+
951
+ def count_drained_call(event)
952
+ return unless event.event == :call
953
+ return if skip_boundary_call?(event)
932
954
 
955
+ resolved_path = owned_path(event.path)
956
+ count_dropped_call(event, resolved_path) if resolved_path
957
+ rescue Exception # rubocop:disable Lint/RescueException
958
+ nil
959
+ end
960
+
961
+ def record_truncation
962
+ return if @limits_fired.empty?
963
+
964
+ CurrentTrace.new(@trace_id).set_metadata(
965
+ "bitfab.truncated_by" => LIMIT_ORDER.select { |limit| @limits_fired.include?(limit) }.join(","),
966
+ "bitfab.dropped_spans" => @dropped_spans
967
+ )
933
968
  Bitfab.warn_once(
934
969
  "subtree-truncated:#{@trace_function_key}",
935
970
  "'#{@trace_function_key}' hit a subtree capture limit (max_depth=#{@max_depth}, " \
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.57.6"
4
+ VERSION = "0.57.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.57.6
4
+ version: 0.57.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -161,6 +161,7 @@ files:
161
161
  - lib/bitfab/client.rb
162
162
  - lib/bitfab/commit_ref.rb
163
163
  - lib/bitfab/compress.rb
164
+ - lib/bitfab/connection_pool.rb
164
165
  - lib/bitfab/constants.rb
165
166
  - lib/bitfab/datasets.rb
166
167
  - lib/bitfab/db_snapshot.rb