openclacky 1.5.10 → 1.5.11
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +4 -4
- data/README_CN.md +4 -4
- data/README_JA.md +4 -4
- data/lib/clacky/agent/llm_caller.rb +7 -3
- data/lib/clacky/agent/session_serializer.rb +24 -14
- data/lib/clacky/agent/skill_manager.rb +1 -2
- data/lib/clacky/agent/tool_executor.rb +1 -1
- data/lib/clacky/agent/tool_registry.rb +6 -4
- data/lib/clacky/agent.rb +105 -32
- data/lib/clacky/brand_config.rb +15 -17
- data/lib/clacky/cli.rb +2 -2
- data/lib/clacky/client.rb +5 -2
- data/lib/clacky/default_extensions/advisor/advisors/general.md +48 -0
- data/lib/clacky/default_extensions/advisor/api/handler.rb +27 -0
- data/lib/clacky/default_extensions/advisor/ext.yml +35 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +291 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +11 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor_tool_use.rb +12 -0
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +462 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +6 -19
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +91 -56
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +14 -7
- data/lib/clacky/default_extensions/meeting/api/handler.rb +1 -1
- data/lib/clacky/default_skills/channel-manager/SKILL.md +4 -4
- data/lib/clacky/default_skills/new/SKILL.md +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/extension/hook_loader.rb +2 -0
- data/lib/clacky/extension/packager.rb +23 -10
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +10 -22
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +3 -3
- data/lib/clacky/fanout.rb +1 -1
- data/lib/clacky/idle_compression_timer.rb +3 -5
- data/lib/clacky/json_ui_controller.rb +1 -1
- data/lib/clacky/mcp/http_transport.rb +1 -1
- data/lib/clacky/mcp/registry.rb +2 -3
- data/lib/clacky/mcp/stdio_transport.rb +21 -17
- data/lib/clacky/plain_ui_controller.rb +19 -16
- data/lib/clacky/platform_http_client.rb +14 -14
- data/lib/clacky/providers.rb +15 -7
- data/lib/clacky/rich_ui/rich_ui_controller.rb +5 -5
- data/lib/clacky/server/browser_manager.rb +4 -7
- data/lib/clacky/server/channel/adapters/dingtalk/stream_client.rb +2 -1
- data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +7 -5
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +4 -3
- data/lib/clacky/server/channel/adapters/telegram/adapter.rb +3 -2
- data/lib/clacky/server/channel/adapters/telegram/api_client.rb +5 -3
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +6 -5
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +13 -11
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +7 -2
- data/lib/clacky/server/channel/channel_config.rb +25 -9
- data/lib/clacky/server/channel/channel_manager.rb +16 -9
- data/lib/clacky/server/channel/channel_ui_controller.rb +25 -6
- data/lib/clacky/server/http_server.rb +119 -47
- data/lib/clacky/server/scheduler.rb +4 -5
- data/lib/clacky/server/server_master.rb +15 -3
- data/lib/clacky/server/session_registry.rb +7 -2
- data/lib/clacky/server/web_ui_controller.rb +13 -14
- data/lib/clacky/session_manager.rb +11 -4
- data/lib/clacky/shell_hook_loader.rb +3 -3
- data/lib/clacky/shutdown.rb +71 -0
- data/lib/clacky/telemetry.rb +1 -1
- data/lib/clacky/thread_registry.rb +102 -0
- data/lib/clacky/tools/ask_user.rb +212 -0
- data/lib/clacky/tools/base.rb +5 -0
- data/lib/clacky/tools/terminal.rb +1 -1
- data/lib/clacky/tools/trash_manager.rb +16 -0
- data/lib/clacky/tools/web_search.rb +1 -1
- data/lib/clacky/ui2/components/input_area.rb +2 -2
- data/lib/clacky/ui2/progress_handle.rb +1 -2
- data/lib/clacky/ui2/progress_indicator.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +22 -18
- data/lib/clacky/ui_interface.rb +3 -3
- data/lib/clacky/utils/arguments_parser.rb +31 -4
- data/lib/clacky/utils/model_pricing.rb +37 -0
- data/lib/clacky/utils/parser_manager.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +202 -36
- data/lib/clacky/web/components/notify.js +9 -7
- data/lib/clacky/web/core/ext.js +4 -6
- data/lib/clacky/web/features/channels/store.js +16 -1
- data/lib/clacky/web/features/channels/view.js +45 -2
- data/lib/clacky/web/i18n.js +46 -4
- data/lib/clacky/web/index.html +5 -7
- data/lib/clacky/web/sessions.js +384 -95
- data/lib/clacky/web/settings.js +31 -15
- data/lib/clacky/web/ws-dispatcher.js +41 -5
- data/lib/clacky.rb +3 -1
- metadata +11 -2
- data/lib/clacky/tools/request_user_feedback.rb +0 -84
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cef597eb5d6c066192ba6c2c587e0dcd62fdf2dd9ed422161f71718acbb7f25
|
|
4
|
+
data.tar.gz: 03bcd29f23eacd7b49a37db0494f7964757391ac52e8f3ef9787c49a6b2f8efb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7543c4f518bf5a2e20778472e60909b5eda8f727015b66aad37c69b2d256b53ba3cd5126d2b385a06456a45a6531fe4fdaad7c6d7f8b247aeb2a76bf1446a9f1
|
|
7
|
+
data.tar.gz: d6ceb974a12d29096ae65bcbed3d6c6467880005101664919357572201846792faf4f89956ff5ca568519d74960674e214a39c61333e527741a9eb38083fcbf1
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.11] - 2026-08-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Creator Studio: separate Marketplace and Brand sections for published extensions
|
|
11
|
+
- Advisor extension for next-job suggestions
|
|
12
|
+
- ask_user tool with multi-question support (replaces feedback_user)
|
|
13
|
+
- Sidebar states for awaiting-feedback and just-finished sessions
|
|
14
|
+
- Auto-purge file-trash entries older than 8 days
|
|
15
|
+
- DeepSeek V4 flash vision exp model preset
|
|
16
|
+
- IM channel toggle to mute tool-call narration and previews
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- Unified internal thread management with graceful shutdown
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Skill decrypt retry now picks up a renewed license key
|
|
23
|
+
- DeepSeek weekends billed entirely at off-peak rates
|
|
24
|
+
- Skill and extension zip downloads fall back to a secondary CDN host
|
|
25
|
+
- Session status dot aligned with name and channel badge
|
|
26
|
+
- Project session tag top margin
|
|
27
|
+
- Unified modal close button and ext-studio modal semantics
|
|
28
|
+
- Unified checkbox accent and distinct verified badge
|
|
29
|
+
- Edit-model modal overflow on small screens
|
|
30
|
+
- Subagent UI fixes
|
|
31
|
+
- Star History charts migrated to star-history.dera.page (#491 - @CrustyMozarella)
|
|
32
|
+
|
|
7
33
|
## [1.5.10] - 2026-08-19
|
|
8
34
|
|
|
9
35
|
### Added
|
data/README.md
CHANGED
|
@@ -209,11 +209,11 @@ $ openclacky
|
|
|
209
209
|
|
|
210
210
|
## Star History
|
|
211
211
|
|
|
212
|
-
<a href="https://
|
|
212
|
+
<a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
|
|
213
213
|
<picture>
|
|
214
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
215
|
-
<source media="(prefers-color-scheme: light)" srcset="https://
|
|
216
|
-
<img alt="Star History Chart" src="https://
|
|
214
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
|
|
215
|
+
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
216
|
+
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
217
217
|
</picture>
|
|
218
218
|
</a>
|
|
219
219
|
|
data/README_CN.md
CHANGED
|
@@ -204,11 +204,11 @@ $ openclacky
|
|
|
204
204
|
|
|
205
205
|
## Star 历史
|
|
206
206
|
|
|
207
|
-
<a href="https://
|
|
207
|
+
<a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
|
|
208
208
|
<picture>
|
|
209
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
210
|
-
<source media="(prefers-color-scheme: light)" srcset="https://
|
|
211
|
-
<img alt="Star History Chart" src="https://
|
|
209
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
|
|
210
|
+
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
211
|
+
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
212
212
|
</picture>
|
|
213
213
|
</a>
|
|
214
214
|
|
data/README_JA.md
CHANGED
|
@@ -209,11 +209,11 @@ $ openclacky
|
|
|
209
209
|
|
|
210
210
|
## Star History
|
|
211
211
|
|
|
212
|
-
<a href="https://
|
|
212
|
+
<a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
|
|
213
213
|
<picture>
|
|
214
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://
|
|
215
|
-
<source media="(prefers-color-scheme: light)" srcset="https://
|
|
216
|
-
<img alt="Star History Chart" src="https://
|
|
214
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
|
|
215
|
+
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
216
|
+
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
|
|
217
217
|
</picture>
|
|
218
218
|
</a>
|
|
219
219
|
|
|
@@ -95,6 +95,7 @@ module Clacky
|
|
|
95
95
|
|
|
96
96
|
begin
|
|
97
97
|
begin
|
|
98
|
+
Clacky::Shutdown.checkpoint!
|
|
98
99
|
# Use active_messages (Time Machine) when undone, otherwise send full history.
|
|
99
100
|
# to_api strips internal fields and handles orphaned tool_calls.
|
|
100
101
|
messages_to_send = if respond_to?(:active_messages)
|
|
@@ -167,6 +168,7 @@ module Clacky
|
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
rescue Faraday::TimeoutError => e
|
|
171
|
+
Clacky::Shutdown.checkpoint!
|
|
170
172
|
# Faraday::TimeoutError on our non-streaming POST almost always means
|
|
171
173
|
# the *response* took longer than the 300s read-timeout to come back —
|
|
172
174
|
# i.e. the model is trying to produce a huge output in one shot
|
|
@@ -198,7 +200,7 @@ module Clacky
|
|
|
198
200
|
metadata: { attempt: retries, total: max_retries }
|
|
199
201
|
)
|
|
200
202
|
retrying_progress_opened = true
|
|
201
|
-
sleep
|
|
203
|
+
Clacky::Shutdown.sleep(retry_delay)
|
|
202
204
|
retry
|
|
203
205
|
else
|
|
204
206
|
# All retries exhausted for a timeout. Try the secondary gateway URL if available.
|
|
@@ -219,6 +221,7 @@ module Clacky
|
|
|
219
221
|
end
|
|
220
222
|
|
|
221
223
|
rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
|
|
224
|
+
Clacky::Shutdown.checkpoint!
|
|
222
225
|
retries += 1
|
|
223
226
|
|
|
224
227
|
# Errno::EPIPE means the underlying TCP socket is dead (server closed the
|
|
@@ -245,7 +248,7 @@ module Clacky
|
|
|
245
248
|
metadata: { attempt: retries, total: max_retries }
|
|
246
249
|
)
|
|
247
250
|
retrying_progress_opened = true
|
|
248
|
-
sleep
|
|
251
|
+
Clacky::Shutdown.sleep(retry_delay)
|
|
249
252
|
retry
|
|
250
253
|
else
|
|
251
254
|
# All retries on the primary endpoint exhausted for a network-level
|
|
@@ -269,6 +272,7 @@ module Clacky
|
|
|
269
272
|
end
|
|
270
273
|
|
|
271
274
|
rescue RetryableError => e
|
|
275
|
+
Clacky::Shutdown.checkpoint!
|
|
272
276
|
retries += 1
|
|
273
277
|
|
|
274
278
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -298,7 +302,7 @@ module Clacky
|
|
|
298
302
|
metadata: { attempt: retries, total: current_max }
|
|
299
303
|
)
|
|
300
304
|
retrying_progress_opened = true
|
|
301
|
-
sleep
|
|
305
|
+
Clacky::Shutdown.sleep(retry_delay)
|
|
302
306
|
retry
|
|
303
307
|
else
|
|
304
308
|
# All retries on the current endpoint exhausted.
|
|
@@ -742,16 +742,17 @@ module Clacky
|
|
|
742
742
|
args_raw = tc[:arguments] || tc.dig(:function, :arguments) || {}
|
|
743
743
|
args = args_raw.is_a?(String) ? (JSON.parse(args_raw) rescue args_raw) : args_raw
|
|
744
744
|
|
|
745
|
-
# Special handling:
|
|
746
|
-
#
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
745
|
+
# Special handling: the ask_user question is shown as an assistant
|
|
746
|
+
# message (matching real-time behavior), not as a tool call.
|
|
747
|
+
if Clacky::Tools::AskUser.feedback_tool?(name)
|
|
748
|
+
questions = Clacky::Tools::AskUser.normalize_questions(args)
|
|
749
|
+
context = Clacky::Tools::AskUser.fetch_key(args, :context).to_s
|
|
750
|
+
first = questions.first
|
|
751
|
+
|
|
752
|
+
unless first.nil?
|
|
753
|
+
ui.show_feedback_request(first[:question], context, first[:options],
|
|
754
|
+
questions: questions)
|
|
755
|
+
end
|
|
755
756
|
else
|
|
756
757
|
ui.show_tool_call(name, args)
|
|
757
758
|
end
|
|
@@ -796,14 +797,23 @@ module Clacky
|
|
|
796
797
|
end
|
|
797
798
|
end
|
|
798
799
|
|
|
799
|
-
# Replay
|
|
800
|
+
# Replay subagent transcripts stored on a tool result message. Emits a
|
|
800
801
|
# bracketed sequence of UI events the frontend can render as a collapsible
|
|
801
802
|
# sub-process block: subagent_start → (assistant_message / tool_call /
|
|
802
|
-
# tool_result)* → subagent_end.
|
|
803
|
+
# tool_result)* → subagent_end. A fan-out batch renders one block per job,
|
|
804
|
+
# in the caller's original job order.
|
|
805
|
+
#
|
|
806
|
+
# Sessions saved before fan-out support stored a bare Hash here.
|
|
803
807
|
def replay_subagent_transcript(msg, ui)
|
|
804
|
-
|
|
805
|
-
|
|
808
|
+
stored = msg[:subagent_transcript]
|
|
809
|
+
Array(stored.is_a?(Hash) ? [stored] : stored).each do |transcript|
|
|
810
|
+
next unless transcript.is_a?(Hash)
|
|
811
|
+
|
|
812
|
+
replay_one_subagent_transcript(transcript, ui)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
806
815
|
|
|
816
|
+
def replay_one_subagent_transcript(transcript, ui)
|
|
807
817
|
events = transcript[:events] || transcript["events"] || []
|
|
808
818
|
return if events.empty?
|
|
809
819
|
|
|
@@ -651,8 +651,7 @@ module Clacky
|
|
|
651
651
|
# observe() attaches each trail to its own tool result message.
|
|
652
652
|
# Kept out of the LLM payload via INTERNAL_FIELDS.
|
|
653
653
|
if tool_call_id
|
|
654
|
-
|
|
655
|
-
extract_subagent_transcript(subagent, skill.identifier)
|
|
654
|
+
record_subagent_transcript(tool_call_id, subagent, skill.identifier)
|
|
656
655
|
end
|
|
657
656
|
|
|
658
657
|
# Mutate the subagent_instructions message in-place to become the result summary
|
|
@@ -13,7 +13,7 @@ module Clacky
|
|
|
13
13
|
case @config.permission_mode
|
|
14
14
|
when :auto_approve, :confirm_all
|
|
15
15
|
# Both modes auto-execute all file/shell tools without confirmation.
|
|
16
|
-
# The difference is only in
|
|
16
|
+
# The difference is only in ask_user handling:
|
|
17
17
|
# auto_approve → no human present, inject auto_reply
|
|
18
18
|
# confirm_all → human present, truly wait for user input
|
|
19
19
|
true
|
|
@@ -55,10 +55,12 @@ module Clacky
|
|
|
55
55
|
# todo_manager aliases
|
|
56
56
|
"todo" => "todo_manager",
|
|
57
57
|
"task_manager" => "todo_manager",
|
|
58
|
-
#
|
|
59
|
-
"
|
|
60
|
-
"user_feedback" => "
|
|
61
|
-
"ask" => "
|
|
58
|
+
# ask_user aliases
|
|
59
|
+
"request_user_feedback" => "ask_user",
|
|
60
|
+
"user_feedback" => "ask_user",
|
|
61
|
+
"ask" => "ask_user",
|
|
62
|
+
"ask_question" => "ask_user",
|
|
63
|
+
"clarify" => "ask_user",
|
|
62
64
|
# trash_manager aliases
|
|
63
65
|
"trash" => "trash_manager",
|
|
64
66
|
"delete" => "trash_manager",
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -108,7 +108,8 @@ module Clacky
|
|
|
108
108
|
@ui = ui # UIController for direct UI interaction
|
|
109
109
|
@debug_logs = [] # Debug logs for troubleshooting
|
|
110
110
|
@pending_injections = [] # Pending inline skill injections to flush after observe()
|
|
111
|
-
@pending_subagent_transcripts = {} # tool_call_id => subagent
|
|
111
|
+
@pending_subagent_transcripts = {} # tool_call_id => [subagent trails], attached by observe()
|
|
112
|
+
@subagent_transcripts_mutex = Mutex.new # fan-out collects from worker threads
|
|
112
113
|
@pending_script_tmpdirs = [] # Decrypted-script tmpdirs that live for the agent's lifetime
|
|
113
114
|
@pending_error_rollback = false # Deferred rollback flag set by restore_session on error
|
|
114
115
|
@last_run_interrupted = false # Set when run() exits via AgentInterrupted; tells the next run() to keep the task-start snapshot (continuation of the same task across a relay, not a brand-new task)
|
|
@@ -688,12 +689,13 @@ module Clacky
|
|
|
688
689
|
|
|
689
690
|
result = nil
|
|
690
691
|
begin
|
|
691
|
-
# Track if
|
|
692
|
+
# Track if ask_user was called
|
|
692
693
|
awaiting_user_feedback = false
|
|
693
694
|
# Track if task was interrupted by user (denied tool execution)
|
|
694
695
|
task_interrupted = false
|
|
695
696
|
|
|
696
697
|
loop do
|
|
698
|
+
Clacky::Shutdown.checkpoint!
|
|
697
699
|
@iterations += 1
|
|
698
700
|
@hooks.trigger(:on_iteration, @iterations)
|
|
699
701
|
|
|
@@ -819,7 +821,7 @@ module Clacky
|
|
|
819
821
|
|
|
820
822
|
# Show assistant message if there's content before tool calls
|
|
821
823
|
if response[:content] && !response[:content].empty?
|
|
822
|
-
emit_assistant_message(response[:content], reasoning_content: response[:reasoning_content])
|
|
824
|
+
emit_assistant_message(response[:content], reasoning_content: response[:reasoning_content], interim: true)
|
|
823
825
|
end
|
|
824
826
|
|
|
825
827
|
# Show token usage after assistant message (or immediately if no message).
|
|
@@ -829,7 +831,7 @@ module Clacky
|
|
|
829
831
|
# Act: Execute tool calls
|
|
830
832
|
action_result = act(response[:tool_calls])
|
|
831
833
|
|
|
832
|
-
# Check if
|
|
834
|
+
# Check if ask_user was called
|
|
833
835
|
if action_result[:awaiting_feedback]
|
|
834
836
|
awaiting_user_feedback = true
|
|
835
837
|
observe(response, action_result[:tool_results])
|
|
@@ -866,7 +868,7 @@ module Clacky
|
|
|
866
868
|
end
|
|
867
869
|
end
|
|
868
870
|
|
|
869
|
-
result = build_result
|
|
871
|
+
result = build_result(awaiting_user_feedback: awaiting_user_feedback)
|
|
870
872
|
|
|
871
873
|
# Run skill evolution hooks after main loop completes
|
|
872
874
|
# Skip if task was interrupted by user (denied tool) or awaiting user feedback
|
|
@@ -1226,10 +1228,10 @@ module Clacky
|
|
|
1226
1228
|
end
|
|
1227
1229
|
end
|
|
1228
1230
|
|
|
1229
|
-
# Special handling for
|
|
1231
|
+
# Special handling for the ask_user family
|
|
1230
1232
|
# The interactive countdown (auto_approve) is handled after the tool
|
|
1231
1233
|
# executes, once the question itself has been rendered to the user.
|
|
1232
|
-
unless call[:name]
|
|
1234
|
+
unless Tools::AskUser.feedback_tool?(call[:name])
|
|
1233
1235
|
@ui&.show_tool_call(call[:name], redact_tool_args(call[:arguments]))
|
|
1234
1236
|
end
|
|
1235
1237
|
|
|
@@ -1254,6 +1256,11 @@ module Clacky
|
|
|
1254
1256
|
args[:tool_call_id] = call[:id]
|
|
1255
1257
|
end
|
|
1256
1258
|
|
|
1259
|
+
# Same anchor for extension tools that fan out to their own subagents.
|
|
1260
|
+
if tool && tool.class.respond_to?(:receives_tool_call_id) && tool.class.receives_tool_call_id
|
|
1261
|
+
args[:tool_call_id] = call[:id]
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1257
1264
|
# Inject working_dir so tools don't rely on Dir.chdir global state
|
|
1258
1265
|
args[:working_dir] = @working_dir if @working_dir
|
|
1259
1266
|
|
|
@@ -1319,10 +1326,13 @@ module Clacky
|
|
|
1319
1326
|
@ui&.update_todos(@todos.dup) unless action == "list"
|
|
1320
1327
|
end
|
|
1321
1328
|
|
|
1322
|
-
# Special handling for
|
|
1323
|
-
|
|
1329
|
+
# Special handling for ask_user: emit as interactive feedback card.
|
|
1330
|
+
# A rejected call (no usable question) falls through to the normal
|
|
1331
|
+
# result path so the model sees the error and can retry.
|
|
1332
|
+
if Tools::AskUser.feedback_tool?(call[:name]) &&
|
|
1333
|
+
result.is_a?(Hash) && result[:awaiting_feedback]
|
|
1324
1334
|
# Pass the raw call arguments to show_tool_call so the WebUI controller
|
|
1325
|
-
# can extract
|
|
1335
|
+
# can extract the questions and emit a "request_feedback" event
|
|
1326
1336
|
# (renders as a clickable card in the browser).
|
|
1327
1337
|
# Fallback UIs (terminal, IM channels) receive the formatted text message.
|
|
1328
1338
|
@ui&.show_tool_call(call[:name], call[:arguments])
|
|
@@ -1456,23 +1466,25 @@ module Clacky
|
|
|
1456
1466
|
end
|
|
1457
1467
|
end
|
|
1458
1468
|
|
|
1459
|
-
# Attach captured subagent transcripts (set by execute_skill_with_subagent
|
|
1460
|
-
#
|
|
1461
|
-
# Each transcript rides on its tool message as
|
|
1462
|
-
#
|
|
1463
|
-
#
|
|
1464
|
-
#
|
|
1465
|
-
#
|
|
1469
|
+
# Attach captured subagent transcripts (set by execute_skill_with_subagent
|
|
1470
|
+
# and fan_out_labeled) to the tool result messages just appended by
|
|
1471
|
+
# observe(). Each transcript rides on its tool message as an internal field
|
|
1472
|
+
# stripped before the LLM call but persisted to session.json and replayed to
|
|
1473
|
+
# the WebUI. Keyed by tool_call_id so a turn with several such calls attaches
|
|
1474
|
+
# each trail to the call that spawned it. Consumed entries are deleted so
|
|
1475
|
+
# each fires exactly once.
|
|
1476
|
+
# A single subagent is just a batch of one, so both invoke_skill and
|
|
1477
|
+
# fan-out land in the same buffer and the same message field.
|
|
1466
1478
|
private def attach_pending_subagent_transcripts(response)
|
|
1467
1479
|
return if @pending_subagent_transcripts.empty?
|
|
1468
1480
|
|
|
1481
|
+
# Not filtered by tool name: any tool may spawn subagents, and the
|
|
1482
|
+
# tool_call_id it recorded under is already the anchor.
|
|
1469
1483
|
Array(response[:tool_calls]).each do |tc|
|
|
1470
|
-
|
|
1484
|
+
batch = @subagent_transcripts_mutex.synchronize { @pending_subagent_transcripts.delete(tc[:id]) }
|
|
1485
|
+
next if batch.nil? || batch.empty?
|
|
1471
1486
|
|
|
1472
|
-
|
|
1473
|
-
next unless transcript
|
|
1474
|
-
|
|
1475
|
-
@history.attach_to_tool_result(tc[:id], :subagent_transcript, transcript)
|
|
1487
|
+
@history.attach_to_tool_result(tc[:id], :subagent_transcript, batch.sort_by { |t| t[:index] || 0 })
|
|
1476
1488
|
end
|
|
1477
1489
|
end
|
|
1478
1490
|
|
|
@@ -1491,6 +1503,12 @@ module Clacky
|
|
|
1491
1503
|
TERMINAL_HEAD_CHARS = 40_000
|
|
1492
1504
|
TERMINAL_TAIL_CHARS = 40_000
|
|
1493
1505
|
|
|
1506
|
+
# Per-subagent transcript budget. Mirrors the intent of
|
|
1507
|
+
# MessageHistory::MAX_EXT_EVENTS_PER_MESSAGE: milestones are worth keeping,
|
|
1508
|
+
# runaway trails are not. A fan-out stores one of these per job.
|
|
1509
|
+
MAX_TRANSCRIPT_EVENTS = 200
|
|
1510
|
+
MAX_TRANSCRIPT_BYTES = 64 * 1024
|
|
1511
|
+
|
|
1494
1512
|
private def truncate_oversized_tool_content(msg, tool_name: nil)
|
|
1495
1513
|
content = msg[:content]
|
|
1496
1514
|
return msg unless content.is_a?(String) && content.length > MAX_TOOL_RESULT_CHARS
|
|
@@ -1586,7 +1604,7 @@ module Clacky
|
|
|
1586
1604
|
!@start_time.nil?
|
|
1587
1605
|
end
|
|
1588
1606
|
|
|
1589
|
-
private def build_result(status = :success, error: nil)
|
|
1607
|
+
private def build_result(status = :success, error: nil, awaiting_user_feedback: false)
|
|
1590
1608
|
task_iterations = @iterations - (@task_start_iterations || 0)
|
|
1591
1609
|
task_cost = @total_cost - (@task_start_cost || 0)
|
|
1592
1610
|
|
|
@@ -1601,7 +1619,8 @@ module Clacky
|
|
|
1601
1619
|
cost_source: @task_cost_source,
|
|
1602
1620
|
cache_stats: @task_cache_stats || @cache_stats,
|
|
1603
1621
|
history: @history,
|
|
1604
|
-
error: error
|
|
1622
|
+
error: error,
|
|
1623
|
+
awaiting_user_feedback: awaiting_user_feedback
|
|
1605
1624
|
}
|
|
1606
1625
|
end
|
|
1607
1626
|
|
|
@@ -1640,7 +1659,7 @@ module Clacky
|
|
|
1640
1659
|
@tool_registry.register(Tools::WebSearch.new)
|
|
1641
1660
|
@tool_registry.register(Tools::WebFetch.new)
|
|
1642
1661
|
@tool_registry.register(Tools::TodoManager.new)
|
|
1643
|
-
@tool_registry.register(Tools::
|
|
1662
|
+
@tool_registry.register(Tools::AskUser.new)
|
|
1644
1663
|
@tool_registry.register(Tools::InvokeSkill.new)
|
|
1645
1664
|
@tool_registry.register(Tools::Browser.new)
|
|
1646
1665
|
end
|
|
@@ -1719,11 +1738,16 @@ module Clacky
|
|
|
1719
1738
|
# the jobs over — forking deep-copies parent config + history, which must
|
|
1720
1739
|
# not race. Only the blocking run belongs in the lambda.
|
|
1721
1740
|
#
|
|
1722
|
-
#
|
|
1741
|
+
# Pass :subagent alongside :run (and a tool_call_id) to have each job's
|
|
1742
|
+
# message trail persisted onto the tool result, so the WebUI can replay the
|
|
1743
|
+
# whole batch after a reload instead of just the collapsed return values.
|
|
1744
|
+
#
|
|
1745
|
+
# @param jobs [Array<Hash>] each { label: String, run: #call, subagent: Agent (optional) }
|
|
1723
1746
|
# @param max_concurrency [Integer] jobs allowed to run at once
|
|
1724
1747
|
# @param timeout [Numeric, nil] wall-clock budget for the whole batch
|
|
1748
|
+
# @param tool_call_id [String, nil] anchors persisted transcripts to this tool call
|
|
1725
1749
|
# @return [Array<Fanout::Result>] aligned to the input order
|
|
1726
|
-
def fan_out_labeled(jobs, max_concurrency: Fanout::DEFAULT_MAX_CONCURRENCY, timeout: nil)
|
|
1750
|
+
def fan_out_labeled(jobs, max_concurrency: Fanout::DEFAULT_MAX_CONCURRENCY, timeout: nil, tool_call_id: nil)
|
|
1727
1751
|
return [] if jobs.empty?
|
|
1728
1752
|
|
|
1729
1753
|
# Fanout workers are fresh threads, so the epoch that lets the web
|
|
@@ -1734,11 +1758,18 @@ module Clacky
|
|
|
1734
1758
|
wrapped = jobs.each_with_index.map do |job, index|
|
|
1735
1759
|
label = job[:label] || job["label"] || "Subagent #{index + 1}/#{jobs.size}"
|
|
1736
1760
|
run = job[:run] || job["run"]
|
|
1761
|
+
subagent = job[:subagent] || job["subagent"]
|
|
1737
1762
|
raise ArgumentError, "job #{index} must provide a callable :run" unless run.respond_to?(:call)
|
|
1738
1763
|
|
|
1739
1764
|
lambda do
|
|
1740
1765
|
Thread.current[:task_epoch] = epoch
|
|
1741
|
-
|
|
1766
|
+
begin
|
|
1767
|
+
within_phase(label, kind: "fanout_subagent", concurrent: true) { run.call }
|
|
1768
|
+
ensure
|
|
1769
|
+
# Runs in ensure so a job that raised still leaves a trail — a failed
|
|
1770
|
+
# subagent is exactly the one worth inspecting afterwards.
|
|
1771
|
+
record_subagent_transcript(tool_call_id, subagent, label, index: index) if subagent
|
|
1772
|
+
end
|
|
1742
1773
|
end
|
|
1743
1774
|
end
|
|
1744
1775
|
|
|
@@ -1751,6 +1782,21 @@ module Clacky
|
|
|
1751
1782
|
@ui.with_phase(kind: kind, label: label, concurrent: concurrent, &block)
|
|
1752
1783
|
end
|
|
1753
1784
|
|
|
1785
|
+
# Called from fan-out worker threads, hence the mutex. Slots are keyed by
|
|
1786
|
+
# job index so the persisted order matches the caller's job order rather
|
|
1787
|
+
# than completion order.
|
|
1788
|
+
def record_subagent_transcript(tool_call_id, subagent, label, index: 0)
|
|
1789
|
+
return unless tool_call_id
|
|
1790
|
+
|
|
1791
|
+
transcript = extract_subagent_transcript(subagent, label)
|
|
1792
|
+
transcript[:index] = index
|
|
1793
|
+
@subagent_transcripts_mutex.synchronize do
|
|
1794
|
+
(@pending_subagent_transcripts[tool_call_id] ||= []) << transcript
|
|
1795
|
+
end
|
|
1796
|
+
rescue StandardError => e
|
|
1797
|
+
Clacky::Logger.warn("agent.subagent_transcript_failed", error: e.message, label: label)
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1754
1800
|
# The subagent's last non-empty assistant message — its actual answer.
|
|
1755
1801
|
#
|
|
1756
1802
|
# A subagent's `run` result carries cost and iteration counts but no reply
|
|
@@ -1986,10 +2032,34 @@ module Clacky
|
|
|
1986
2032
|
skill: skill_identifier,
|
|
1987
2033
|
iterations: subagent.iterations,
|
|
1988
2034
|
cost_usd: subagent.total_cost.round(4),
|
|
1989
|
-
events: events
|
|
2035
|
+
events: cap_transcript_events(events)
|
|
1990
2036
|
}
|
|
1991
2037
|
end
|
|
1992
2038
|
|
|
2039
|
+
# session.json is rewritten in full on every save, so a transcript has to
|
|
2040
|
+
# stay bounded — a fan-out of chatty subagents would otherwise multiply an
|
|
2041
|
+
# unbounded trail by the batch size. Oldest events are dropped first: the
|
|
2042
|
+
# tail is what explains how the subagent ended up where it did.
|
|
2043
|
+
private def cap_transcript_events(events)
|
|
2044
|
+
kept = events.last(MAX_TRANSCRIPT_EVENTS)
|
|
2045
|
+
dropped = events.size - kept.size
|
|
2046
|
+
|
|
2047
|
+
budget = MAX_TRANSCRIPT_BYTES
|
|
2048
|
+
kept = kept.reverse.take_while do |entry|
|
|
2049
|
+
budget -= transcript_entry_bytes(entry)
|
|
2050
|
+
budget.positive?
|
|
2051
|
+
end.reverse
|
|
2052
|
+
dropped = events.size - kept.size
|
|
2053
|
+
|
|
2054
|
+
return kept if dropped.zero?
|
|
2055
|
+
|
|
2056
|
+
[{ role: "system", content: "[#{dropped} earlier event(s) omitted]" }] + kept
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
private def transcript_entry_bytes(entry)
|
|
2060
|
+
entry[:content].to_s.bytesize + Array(entry[:tool_calls]).sum { |tc| tc[:arguments].to_s.bytesize }
|
|
2061
|
+
end
|
|
2062
|
+
|
|
1993
2063
|
# Deep clone helper for messages using Marshal
|
|
1994
2064
|
# @param obj [Object] Object to clone
|
|
1995
2065
|
# @return [Object] Deep cloned object
|
|
@@ -2119,7 +2189,10 @@ module Clacky
|
|
|
2119
2189
|
begin
|
|
2120
2190
|
Clacky::Vision::Resolver.new(ocr_entry).describe(image)
|
|
2121
2191
|
ensure
|
|
2122
|
-
|
|
2192
|
+
# Must pass progress_type: "vision" — the UI's legacy shim pairs
|
|
2193
|
+
# active/done by type, so a bare done would leave the OCR spinner
|
|
2194
|
+
# frozen forever (same trap as the old retrying-slot bug).
|
|
2195
|
+
@ui&.show_progress(progress_type: "vision", phase: "done")
|
|
2123
2196
|
end
|
|
2124
2197
|
end
|
|
2125
2198
|
|
|
@@ -2327,7 +2400,7 @@ module Clacky
|
|
|
2327
2400
|
# and cannot load file:// directly) and must stay scoped to the Web UI
|
|
2328
2401
|
# controller. IM channel subscribers need the original file:// markdown so
|
|
2329
2402
|
# parse_file_links can extract paths and deliver images as native attachments.
|
|
2330
|
-
private def emit_assistant_message(content, reasoning_content: nil)
|
|
2403
|
+
private def emit_assistant_message(content, reasoning_content: nil, interim: false)
|
|
2331
2404
|
# Prepend reasoning/thinking content (from thinking-mode providers like
|
|
2332
2405
|
# DeepSeek V4, Kimi K2) wrapped in <think> tags so the Web UI renders it
|
|
2333
2406
|
# as a collapsible thinking block (see sessions.js _renderMarkdown).
|
|
@@ -2340,7 +2413,7 @@ module Clacky
|
|
|
2340
2413
|
return if full_content.nil? || full_content.to_s.strip.empty?
|
|
2341
2414
|
|
|
2342
2415
|
parsed = parse_file_links(content)
|
|
2343
|
-
@ui&.show_assistant_message(full_content, files: parsed[:files])
|
|
2416
|
+
@ui&.show_assistant_message(full_content, files: parsed[:files], interim: interim)
|
|
2344
2417
|
end
|
|
2345
2418
|
|
|
2346
2419
|
# Record BEFORE-change snapshots for any file a tool is about to mutate,
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -519,7 +519,7 @@ module Clacky
|
|
|
519
519
|
return nil if activated?
|
|
520
520
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
Clacky::ThreadRegistry.spawn(name: "brand-fetch-free-skills") do
|
|
523
523
|
Thread.current.abort_on_exception = false
|
|
524
524
|
|
|
525
525
|
begin
|
|
@@ -922,7 +922,7 @@ module Clacky
|
|
|
922
922
|
return nil unless activated?
|
|
923
923
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
924
924
|
|
|
925
|
-
|
|
925
|
+
Clacky::ThreadRegistry.spawn(name: "brand-fetch-extensions") do
|
|
926
926
|
Thread.current.abort_on_exception = false
|
|
927
927
|
|
|
928
928
|
begin
|
|
@@ -1305,7 +1305,7 @@ module Clacky
|
|
|
1305
1305
|
return nil unless activated?
|
|
1306
1306
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
1307
1307
|
|
|
1308
|
-
|
|
1308
|
+
Clacky::ThreadRegistry.spawn(name: "brand-fetch-skills") do
|
|
1309
1309
|
Thread.current.abort_on_exception = false
|
|
1310
1310
|
|
|
1311
1311
|
begin
|
|
@@ -1890,26 +1890,24 @@ module Clacky
|
|
|
1890
1890
|
return cached[:key] if key_valid && within_grace
|
|
1891
1891
|
end
|
|
1892
1892
|
|
|
1893
|
-
#
|
|
1894
|
-
#
|
|
1895
|
-
#
|
|
1896
|
-
#
|
|
1897
|
-
#
|
|
1898
|
-
|
|
1899
|
-
if @device_id.nil? || @device_id.strip.empty?
|
|
1900
|
-
reloaded = BrandConfig.load
|
|
1901
|
-
@device_id = reloaded.device_id if reloaded.device_id && !reloaded.device_id.strip.empty?
|
|
1902
|
-
end
|
|
1893
|
+
# Single disk read so device_id and license_key share one snapshot.
|
|
1894
|
+
# Reading license_key live (not the instance snapshot) is what lets a
|
|
1895
|
+
# license renewed mid-session be picked up on retry — a live agent still
|
|
1896
|
+
# holds the expired key and the server would otherwise reject the
|
|
1897
|
+
# signature as "license expired".
|
|
1898
|
+
reloaded = BrandConfig.load
|
|
1899
|
+
@device_id = reloaded.device_id if @device_id.nil? || @device_id.strip.empty?
|
|
1903
1900
|
raise "Device ID is missing. Please re-activate your license with `clacky license activate`." \
|
|
1904
1901
|
if @device_id.nil? || @device_id.strip.empty?
|
|
1905
1902
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1903
|
+
license_key = reloaded.license_key
|
|
1904
|
+
license_key = @license_key if license_key.nil? || license_key.strip.empty?
|
|
1905
|
+
user_id = parse_user_id_from_key(license_key)
|
|
1906
|
+
key_hash = Digest::SHA256.hexdigest(license_key)
|
|
1909
1907
|
ts = Time.now.utc.to_i.to_s
|
|
1910
1908
|
nonce = SecureRandom.hex(16)
|
|
1911
1909
|
message = "#{user_id}:#{@device_id}:#{ts}:#{nonce}"
|
|
1912
|
-
signature = OpenSSL::HMAC.hexdigest("SHA256",
|
|
1910
|
+
signature = OpenSSL::HMAC.hexdigest("SHA256", license_key, message)
|
|
1913
1911
|
|
|
1914
1912
|
payload = {
|
|
1915
1913
|
key_hash: key_hash,
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -419,7 +419,7 @@ module Clacky
|
|
|
419
419
|
|
|
420
420
|
if brand.heartbeat_due?
|
|
421
421
|
Clacky::Logger.info("[Brand] check_brand_license_cli: heartbeat due, dispatching async...")
|
|
422
|
-
|
|
422
|
+
Clacky::ThreadRegistry.spawn(name: "cli-brand-heartbeat") do
|
|
423
423
|
begin
|
|
424
424
|
result = brand.heartbeat!
|
|
425
425
|
if result[:success]
|
|
@@ -1053,7 +1053,7 @@ module Clacky
|
|
|
1053
1053
|
auto_name_session(agent, input)
|
|
1054
1054
|
|
|
1055
1055
|
# Run agent in background thread
|
|
1056
|
-
current_task_thread =
|
|
1056
|
+
current_task_thread = Clacky::ThreadRegistry.spawn(name: "cli-agent-task") do
|
|
1057
1057
|
begin
|
|
1058
1058
|
# Set status to working when agent starts
|
|
1059
1059
|
ui_controller.set_working_status
|