openclacky 1.5.20 → 1.5.21

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +18 -0
  4. data/lib/clacky/agent/chunk_index.rb +8 -1
  5. data/lib/clacky/agent/feedback.rb +30 -0
  6. data/lib/clacky/agent/history_navigation.rb +17 -3
  7. data/lib/clacky/agent/hook_manager.rb +6 -1
  8. data/lib/clacky/agent/llm_caller.rb +1 -0
  9. data/lib/clacky/agent/message_compressor_helper.rb +28 -2
  10. data/lib/clacky/agent/session_serializer.rb +20 -2
  11. data/lib/clacky/agent.rb +56 -43
  12. data/lib/clacky/agent_config.rb +82 -4
  13. data/lib/clacky/billing/platform_billing.rb +5 -1
  14. data/lib/clacky/brand_config.rb +1 -1
  15. data/lib/clacky/cli.rb +3 -2
  16. data/lib/clacky/client.rb +5 -5
  17. data/lib/clacky/deepseek_web_tools.rb +156 -0
  18. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  19. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
  20. data/lib/clacky/extension/loader.rb +5 -3
  21. data/lib/clacky/locales/en.rb +5 -5
  22. data/lib/clacky/locales/zh.rb +5 -5
  23. data/lib/clacky/message_format/anthropic.rb +20 -0
  24. data/lib/clacky/message_format/open_ai.rb +31 -14
  25. data/lib/clacky/message_history.rb +21 -3
  26. data/lib/clacky/platform_http_client.rb +6 -2
  27. data/lib/clacky/providers.rb +23 -14
  28. data/lib/clacky/search_config.rb +47 -13
  29. data/lib/clacky/server/browser_manager.rb +35 -5
  30. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  31. data/lib/clacky/server/discover.rb +5 -4
  32. data/lib/clacky/server/http_server.rb +194 -47
  33. data/lib/clacky/server/model_prices.rb +5 -3
  34. data/lib/clacky/server/pid_files.rb +25 -0
  35. data/lib/clacky/server/server_master.rb +9 -3
  36. data/lib/clacky/server/session_registry.rb +19 -7
  37. data/lib/clacky/server/web_ui_controller.rb +61 -38
  38. data/lib/clacky/session_manager.rb +7 -1
  39. data/lib/clacky/skill.rb +1 -1
  40. data/lib/clacky/tools/ask_user.rb +4 -1
  41. data/lib/clacky/tools/base.rb +3 -2
  42. data/lib/clacky/tools/browser.rb +18 -2
  43. data/lib/clacky/tools/file_reader.rb +1 -1
  44. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  45. data/lib/clacky/tools/terminal.rb +65 -23
  46. data/lib/clacky/tools/trash_manager.rb +5 -3
  47. data/lib/clacky/tools/web_fetch.rb +81 -22
  48. data/lib/clacky/tools/web_search.rb +45 -4
  49. data/lib/clacky/ui2/ui_controller.rb +1 -1
  50. data/lib/clacky/ui_interface.rb +4 -3
  51. data/lib/clacky/utils/file_processor.rb +51 -0
  52. data/lib/clacky/utils/model_pricing.rb +132 -13
  53. data/lib/clacky/version.rb +1 -1
  54. data/lib/clacky/web/app.css +366 -105
  55. data/lib/clacky/web/components/chat-navigator.js +8 -1
  56. data/lib/clacky/web/components/mentions.js +2 -13
  57. data/lib/clacky/web/components/model-picker.js +3 -1
  58. data/lib/clacky/web/components/onboard.js +13 -10
  59. data/lib/clacky/web/features/archive/store.js +30 -16
  60. data/lib/clacky/web/features/archive/view.js +20 -10
  61. data/lib/clacky/web/features/backup/view.js +5 -5
  62. data/lib/clacky/web/features/new-session/store.js +3 -3
  63. data/lib/clacky/web/features/trash/store.js +10 -7
  64. data/lib/clacky/web/features/trash/view.js +9 -4
  65. data/lib/clacky/web/i18n.js +106 -30
  66. data/lib/clacky/web/index.html +90 -60
  67. data/lib/clacky/web/projects.js +56 -1
  68. data/lib/clacky/web/sessions.js +244 -131
  69. data/lib/clacky/web/settings.js +143 -8
  70. data/lib/clacky/web/ws-dispatcher.js +9 -13
  71. data/lib/clacky.rb +1 -0
  72. metadata +4 -1
@@ -15,7 +15,7 @@ module Clacky
15
15
  subagent_instructions subagent_result subagent_transcript token_usage
16
16
  compressed_summary chunk_path truncated transient
17
17
  chunk_index chunk_count ext_events skill_command skill_command_display
18
- display_files display_references
18
+ display_files display_references feedback_requests
19
19
  ].freeze
20
20
 
21
21
  INTERNAL_CONTENT_BLOCK_FIELDS = %i[image_path image_name].freeze
@@ -37,7 +37,8 @@ module Clacky
37
37
  # Write operations
38
38
  # ─────────────────────────────────────────────
39
39
 
40
- # Append a single message hash to the history.
40
+ # Append a single message hash to the history. An optional block receives
41
+ # the stored, sanitized message so callers can retain its identity.
41
42
  #
42
43
  # When appending a user message, automatically drop any trailing assistant
43
44
  # message that has unanswered tool_calls (no tool_result follows it).
@@ -48,7 +49,9 @@ module Clacky
48
49
  if message[:role] == "user"
49
50
  drop_dangling_tool_calls!
50
51
  end
51
- @messages << deep_sanitize_utf8(message)
52
+ stored_message = deep_sanitize_utf8(message)
53
+ @messages << stored_message
54
+ yield stored_message if block_given?
52
55
  self
53
56
  end
54
57
 
@@ -111,6 +114,16 @@ module Clacky
111
114
  self
112
115
  end
113
116
 
117
+ # Save the effective request before waiting, including while no tool result
118
+ # exists yet. The original model-authored call remains unchanged.
119
+ def attach_feedback_request(message:, tool_call_id:, request:)
120
+ return false unless message
121
+
122
+ message[:feedback_requests] ||= {}
123
+ message[:feedback_requests][tool_call_id.to_s] = deep_sanitize_utf8(request)
124
+ true
125
+ end
126
+
114
127
  # Settle an interrupted fan-out whose assistant.tool_calls turn is dangling
115
128
  # because the batch was cancelled before any tool result was written. Mirrors
116
129
  # repair_tool_call_pairing (same helpers, same scan) but PERSISTS the result:
@@ -315,6 +328,11 @@ module Clacky
315
328
  @messages
316
329
  end
317
330
  msgs = source.map { |m| strip_for_api(m) }
331
+ # A tool message with a blank id answers no announced call, and one of
332
+ # them makes every later request fail (422 "invalid type: null"). Drop
333
+ # these before pairing: a blank id is not recognized as a tool result,
334
+ # so pairing would otherwise insert a placeholder and orphan the real one.
335
+ msgs.reject! { |m| m[:role] == "tool" && m[:tool_call_id].to_s.strip.empty? }
318
336
  msgs = repair_tool_call_pairing(msgs)
319
337
  ensure_reasoning_content_consistency(msgs, force: force_reasoning_content_pad)
320
338
  end
@@ -50,9 +50,13 @@ module Clacky
50
50
  # Auto-detects the platform endpoint:
51
51
  # - When CLACKY_LICENSE_SERVER is set → that URL (dev override)
52
52
  # - Otherwise → PRIMARY_HOST
53
+ def self.base_url
54
+ override = ENV["CLACKY_LICENSE_SERVER"]
55
+ override && !override.empty? ? override.chomp("/") : PRIMARY_HOST
56
+ end
57
+
53
58
  def initialize
54
- override = ENV["CLACKY_LICENSE_SERVER"]
55
- @base_url = override && !override.empty? ? override : PRIMARY_HOST
59
+ @base_url = self.class.base_url
56
60
  end
57
61
 
58
62
  # Send a POST request with a JSON body and return a normalised result hash.
@@ -60,8 +60,7 @@ module Clacky
60
60
  "abs-gpt-5.6-luna",
61
61
  "or-gemini-4-argon",
62
62
  "or-gemini-3-1-pro",
63
- "or-gemini-3-8-flash",
64
- "oc-grok-4.7"
63
+ "or-gemini-3-8-flash"
65
64
  ],
66
65
  # Image generation models served by the openclacky platform
67
66
  # gateway. The gateway exposes a standard OpenAI-compatible
@@ -146,7 +145,8 @@ module Clacky
146
145
  # oc-glm-5.3 is likewise text-only: its upstream rejects any content
147
146
  # block other than `type: "text"`, so images must never be sent
148
147
  # inline. oc-kimi-k3 and oc-grok-4.7 are natively multimodal and keep
149
- # vision=true.
148
+ # vision=true; oc-gpt-6.1-sol, oc-gpt-6-astra, oc-gpt-6-sol and
149
+ # oc-gpt-6-luna do too.
150
150
  "model_capabilities" => {
151
151
  "dsk-deepseek-flash" => { "vision" => true }.freeze,
152
152
  "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
@@ -199,6 +199,7 @@ module Clacky
199
199
  "endpoint_variants" => [
200
200
  { "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze
201
201
  ].freeze,
202
+ "deepseek_web_tools" => true,
202
203
  "website_url" => "https://www.openclacky.com/llm_keys"
203
204
  }.freeze,
204
205
 
@@ -229,6 +230,7 @@ module Clacky
229
230
  "model_capabilities" => {
230
231
  "deepseek-flash" => { "vision" => true }.freeze
231
232
  }.freeze,
233
+ "deepseek_web_tools" => true,
232
234
  "website_url" => "https://platform.deepseek.com/api_keys"
233
235
  }.freeze,
234
236
 
@@ -334,8 +336,8 @@ module Clacky
334
336
  "name" => "Minimax",
335
337
  "base_url" => "https://api.minimaxi.com/v1",
336
338
  "api" => "openai-completions",
337
- "default_model" => "MiniMax-M3",
338
- "models" => ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
339
+ "default_model" => "MiniMax-M3.1-Flash",
340
+ "models" => ["MiniMax-M3.1-Flash", "MiniMax-M3"],
339
341
  # MiniMax operates two regional endpoints with identical APIs & model
340
342
  # lineup — mainland China (.com) and international (.io). Listing both
341
343
  # lets find_by_base_url identify either one as provider "minimax",
@@ -345,16 +347,15 @@ module Clacky
345
347
  { "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
346
348
  { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
347
349
  ].freeze,
348
- # MiniMax M2.5/M2.7 are text-only on this endpoint. M3 (released 2026-06-01)
349
- # is natively multimodal — it accepts image input via OpenAI-style
350
- # image_url content parts — so it overrides the provider-level
351
- # vision=false below. M3 exposes a 1,000,000-token context window
352
- # (M2.7: 204,800; M2.5: 40,960).
350
+ # M3.1-Flash and M3 are natively multimodal (image_url content parts,
351
+ # 1M context) and override the provider-level vision=false, which
352
+ # stays as the safe default for retired text-only ids (M2.x).
353
353
  "capabilities" => { "vision" => false }.freeze,
354
354
  "model_capabilities" => {
355
- "MiniMax-M3" => { "vision" => true }.freeze
355
+ "MiniMax-M3.1-Flash" => { "vision" => true }.freeze,
356
+ "MiniMax-M3" => { "vision" => true }.freeze
356
357
  }.freeze,
357
- "default_ocr_model" => "MiniMax-M3",
358
+ "default_ocr_model" => "MiniMax-M3.1-Flash",
358
359
  "website_url" => "https://platform.minimax.io/"
359
360
  }.freeze,
360
361
 
@@ -642,7 +643,6 @@ module Clacky
642
643
  "glm-5.2",
643
644
  "kimi-k3",
644
645
  "glm-5.1",
645
- "minimax-m2.7",
646
646
  "minimax-m3",
647
647
  "kimi-k2.7-code",
648
648
  "kimi-k2.6",
@@ -664,7 +664,6 @@ module Clacky
664
664
  "lite_models" => {
665
665
  "glm-5.2" => "glm-5.1",
666
666
  "kimi-k3" => "kimi-k2.6",
667
- "minimax-m3" => "minimax-m2.7",
668
667
  "kimi-k2.7-code" => "kimi-k2.6",
669
668
  "deepseek-v4-pro" => "deepseek-v4-flash",
670
669
  "gemini-3-flash-preview" => "deepseek-v4-flash"
@@ -1173,6 +1172,16 @@ module Clacky
1173
1172
  return true unless caps.key?(key)
1174
1173
  caps[key] != false
1175
1174
  end
1175
+
1176
+ # True when the preset declares native DeepSeek web tools (search + fetch).
1177
+ # Currently DeepSeek direct ("deepseekv4") and the OpenClacky platform
1178
+ # gateway ("openclacky").
1179
+ def deepseek_web_tools?(provider_id)
1180
+ preset = PRESETS[provider_id]
1181
+ return false unless preset
1182
+
1183
+ preset["deepseek_web_tools"] == true
1184
+ end
1176
1185
  end
1177
1186
  end
1178
1187
  end
@@ -2,19 +2,25 @@
2
2
 
3
3
  require "yaml"
4
4
  require "fileutils"
5
+ require_relative "deepseek_web_tools"
5
6
 
6
7
  module Clacky
7
8
  # Search provider selection, backed by ~/.clacky/search.yml.
8
9
  #
9
- # The built-in Parallel/DuckDuckGo/Bing providers need no configuration and
10
- # stay the default. Pointing `provider` at a script in ~/.clacky/searchers/ routes
11
- # web_search through an API-backed engine instead, which is what users who
12
- # want a provider-specific API or policy controls reach for.
10
+ # The built-in Parallel/DuckDuckGo/Bing chain is the default: it needs no key
11
+ # and no configuration, so a fresh install searches the web out of the box.
12
+ # DeepSeek's hosted web tools are opt-in — they render JS-heavy pages to
13
+ # Markdown but bill per call, so they only run once the user picks them.
13
14
  #
14
15
  # search.yml schema:
15
- # provider: tavily — basename of a script in ~/.clacky/searchers/;
16
- # blank or missing means built-in engines only
17
- # key: tvly-xxx — credential handed to that script via env
16
+ # provider: tavily — "deepseek_web" for DeepSeek's hosted
17
+ # search/fetch, blank for built-in engines only,
18
+ # otherwise the basename of a script in ~/.clacky/searchers/
19
+ # key: tvly-xxx — credential for the selected provider (optional for
20
+ # deepseek_web, which can reuse a configured model key)
21
+ #
22
+ # A missing file, or one without a `provider` key, means "never chosen" and
23
+ # resolves to the free chain.
18
24
  #
19
25
  # The directory is the registry: dropping my_engine.rb into
20
26
  # ~/.clacky/searchers/ and setting `provider: my_engine` is all it takes,
@@ -22,21 +28,49 @@ module Clacky
22
28
  module SearchConfig
23
29
  CONFIG_PATH = File.expand_path("~/.clacky/search.yml").freeze
24
30
 
31
+ # Hosted DeepSeek search + page fetch, billed per successful call upstream.
32
+ # It behaves like a provider but ships with the gem, so it has no script.
33
+ DEEPSEEK_WEB = "deepseek_web"
34
+
25
35
  class << self
26
36
  # @return [Hash] { "provider" => String, "key" => String }
27
37
  def load
28
38
  raw = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH) : nil
29
- return { "provider" => "", "key" => "" } unless raw.is_a?(Hash)
39
+ return default_config unless raw.is_a?(Hash)
30
40
 
31
- { "provider" => raw["provider"].to_s.strip, "key" => raw["key"].to_s.strip }
41
+ {
42
+ "provider" => raw["provider"].to_s.strip,
43
+ "key" => raw["key"].to_s.strip
44
+ }
32
45
  rescue StandardError
33
- { "provider" => "", "key" => "" }
46
+ default_config
34
47
  end
35
48
 
36
- def save(provider:, key:)
49
+ def save(provider: nil, key: nil)
50
+ current = load
51
+ new_provider = provider.nil? ? current["provider"] : provider.to_s.strip
52
+ new_key = key.nil? ? current["key"] : key.to_s.strip
53
+
54
+ payload = {
55
+ "provider" => new_provider,
56
+ "key" => new_key
57
+ }
58
+
37
59
  FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
38
- File.write(CONFIG_PATH, { "provider" => provider.to_s.strip, "key" => key.to_s.strip }.to_yaml)
60
+ File.write(CONFIG_PATH, payload.to_yaml)
39
61
  File.chmod(0o600, CONFIG_PATH)
62
+
63
+ # A new provider or key invalidates an earlier failure, so the next
64
+ # search retries DeepSeek instead of sitting out the cooldown.
65
+ Clacky::DeepSeekWebTools.reset_cooldown!
66
+ end
67
+
68
+ def deepseek_web?
69
+ load["provider"] == DEEPSEEK_WEB
70
+ end
71
+
72
+ private def default_config
73
+ { "provider" => "", "key" => "" }
40
74
  end
41
75
 
42
76
  # Absolute path of the configured searcher, or nil when unset/missing.
@@ -44,7 +78,7 @@ module Clacky
44
78
  # web_search silently falls back instead of erroring on every search.
45
79
  def script_path
46
80
  name = load["provider"]
47
- return nil if name.empty?
81
+ return nil if name.empty? || name == DEEPSEEK_WEB
48
82
 
49
83
  Clacky::Utils::SearcherManager.path_for(name)
50
84
  end
@@ -19,6 +19,7 @@ module Clacky
19
19
  # enabled: true/false — whether the browser tool is active
20
20
  # chrome_version: "146" — detected Chrome version (set by browser-setup skill)
21
21
  # configured_at: date — when setup was last run
22
+ # screenshot_max_base64_bytes: Integer — screenshot size ceiling (settings UI)
22
23
  #
23
24
  # Liveness check strategy:
24
25
  # process_alive? sends an MCP `ping` (standard in MCP spec 2024-11-05) and
@@ -116,27 +117,49 @@ module Clacky
116
117
  enabled = cfg["enabled"] == true
117
118
  running = @process && @process[:wait_thr]&.alive?
118
119
  {
119
- enabled: enabled,
120
- daemon_running: !!running,
121
- chrome_version: cfg["chrome_version"]
120
+ enabled: enabled,
121
+ daemon_running: !!running,
122
+ chrome_version: cfg["chrome_version"],
123
+ screenshot_max_base64_bytes: effective_screenshot_limit(cfg)
122
124
  }
123
125
  end
124
126
 
125
127
  # Write browser.yml with the given config and reload the daemon.
126
128
  # Called by HttpServer POST /api/browser/configure.
129
+ # Existing keys are preserved so re-running browser-setup does not wipe
130
+ # user preferences such as the screenshot limits.
127
131
  # @param chrome_version [String] detected Chrome major version
128
132
  def configure(chrome_version:)
129
- cfg = {
133
+ cfg = load_config.merge(
130
134
  "enabled" => true,
131
135
  "browser" => "chrome",
132
136
  "chrome_version" => chrome_version.to_s,
133
137
  "configured_at" => Date.today.to_s
134
- }
138
+ )
135
139
  FileUtils.mkdir_p(File.dirname(BROWSER_CONFIG_PATH))
136
140
  File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
137
141
  reload
138
142
  end
139
143
 
144
+ # Persist the screenshot preferences written by the settings UI.
145
+ # No daemon reload: the browser tool re-reads browser.yml on every screenshot.
146
+ # @param max_base64_bytes [Integer, nil] screenshot size ceiling in bytes
147
+ # @return [Hash] the effective settings after the update
148
+ def update_screenshot_settings(max_base64_bytes: nil)
149
+ raise "Browser not configured. Run /browser-setup first." unless File.exist?(BROWSER_CONFIG_PATH)
150
+
151
+ if !max_base64_bytes.nil? && !(max_base64_bytes.is_a?(Integer) && max_base64_bytes.positive?)
152
+ raise ArgumentError, "screenshot_max_base64_bytes must be a positive integer"
153
+ end
154
+
155
+ cfg = load_config
156
+ cfg["screenshot_max_base64_bytes"] = max_base64_bytes unless max_base64_bytes.nil?
157
+
158
+ File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
159
+ @config = cfg
160
+ { screenshot_max_base64_bytes: effective_screenshot_limit(cfg) }
161
+ end
162
+
140
163
  # Toggle the browser tool on/off by flipping `enabled` in browser.yml.
141
164
  # Raises if browser.yml doesn't exist (not yet set up).
142
165
  # @return [Boolean] new enabled state
@@ -224,6 +247,13 @@ module Clacky
224
247
  {}
225
248
  end
226
249
 
250
+ private def effective_screenshot_limit(cfg)
251
+ limit = cfg["screenshot_max_base64_bytes"]
252
+ return limit if limit.is_a?(Integer) && limit.positive?
253
+
254
+ Clacky::Tools::Browser::SCREENSHOT_MAX_BASE64_BYTES
255
+ end
256
+
227
257
  # Must be called inside @mutex
228
258
  def ensure_process!
229
259
  return if process_alive?
@@ -152,7 +152,7 @@ module Clacky
152
152
  # receive_id for outbound messages), so we extract the chat portion.
153
153
  prefix = "#{platform}:"
154
154
  ids = []
155
- @registry.list.each do |summary|
155
+ @registry.list(archived: nil).each do |summary|
156
156
  @registry.with_session(summary[:id]) do |s|
157
157
  (s[:channel_keys] || []).each do |key|
158
158
  next unless key.start_with?(prefix)
@@ -463,7 +463,7 @@ module Clacky
463
463
  when "/unbind"
464
464
  unbound = false
465
465
  cleared_agents = []
466
- @registry.list.each do |summary|
466
+ @registry.list(archived: nil).each do |summary|
467
467
  @registry.with_session(summary[:id]) do |s|
468
468
  # Set#delete always returns self — use delete? so an unknown key
469
469
  # reports "No binding found." instead of a false "Unbound.".
@@ -726,7 +726,7 @@ module Clacky
726
726
  # /bind and /unbind keep agent.channel_info strictly in sync with channel_keys
727
727
  # (see handle_bind / handle_unbind), so the two sources never disagree on the
728
728
  # same key for two different sessions — a single pass is sufficient.
729
- @registry.list.each do |summary|
729
+ @registry.list(archived: nil).each do |summary|
730
730
  found = nil
731
731
  @registry.with_session(summary[:id]) { |s| found = s[:channel_keys]&.include?(key) }
732
732
  return summary[:id] if found
@@ -814,7 +814,7 @@ module Clacky
814
814
  # The on-disk summary is authoritative here, so sessions evicted from
815
815
  # memory are cleaned too — which is exactly the case /bind used to miss.
816
816
  def clear_channel_info_for_key(key, keep_session_id: nil)
817
- @registry.list(limit: nil).each do |summary|
817
+ @registry.list(limit: nil, archived: nil).each do |summary|
818
818
  next if summary[:id] == keep_session_id
819
819
 
820
820
  info = summary[:channel_info]
@@ -839,7 +839,7 @@ module Clacky
839
839
  end
840
840
 
841
841
  def bind_key_to_session(key, session_id)
842
- @registry.list.each do |summary|
842
+ @registry.list(archived: nil).each do |summary|
843
843
  @registry.with_session(summary[:id]) { |s| s[:channel_keys]&.delete(key) }
844
844
  end
845
845
  @registry.with_session(session_id) do |s|
@@ -932,7 +932,7 @@ module Clacky
932
932
  def restore_channel_bindings
933
933
  bound_keys = Set.new
934
934
  restored_count = 0
935
- @registry.list(limit: nil).each do |summary|
935
+ @registry.list(limit: nil, archived: nil).each do |summary|
936
936
  info = summary[:channel_info]
937
937
  next unless info.is_a?(Hash) && info[:platform] && info[:user_id] && info[:chat_id]
938
938
 
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tmpdir"
3
+ require_relative "pid_files"
4
4
  require "socket"
5
5
 
6
6
  module Clacky
7
7
  module Server
8
8
  # Discover locally-running Clacky server(s) by scanning PID files
9
- # written by Master at /tmp/clacky-master-<port>.pid.
9
+ # written by Master in the configured PID directory.
10
10
  #
11
11
  # Used by the CLI (bare `clacky agent` mode) to auto-detect a sibling
12
12
  # server process, so skills that call back into the server (channels,
@@ -17,7 +17,6 @@ module Clacky
17
17
  # Does NOT probe the TCP port (avoids false positives from stale files
18
18
  # but also avoids noisy connection attempts).
19
19
  module Discover
20
- PID_FILE_GLOB = File.join(Dir.tmpdir, "clacky-master-*.pid").freeze
21
20
  PID_FILE_REGEX = /clacky-master-(\d+)\.pid\z/.freeze
22
21
 
23
22
  module_function
@@ -41,7 +40,9 @@ module Clacky
41
40
  #
42
41
  # @return [Array<Hash>] sorted by port ascending
43
42
  def find_all_local
44
- Dir.glob(PID_FILE_GLOB).filter_map do |path|
43
+ directory = PidFiles.directory
44
+ Dir.glob("clacky-master-*.pid", base: directory).filter_map do |filename|
45
+ path = File.join(directory, filename)
45
46
  m = path.match(PID_FILE_REGEX)
46
47
  next nil unless m
47
48