openclacky 1.5.15 → 1.5.17
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/.clacky/skills/gem-release/SKILL.md +13 -3
- data/CHANGELOG.md +79 -0
- data/README.md +12 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/history_navigation.rb +1 -1
- data/lib/clacky/agent/hook_manager.rb +7 -3
- data/lib/clacky/agent/llm_caller.rb +36 -58
- data/lib/clacky/agent/message_compressor_helper.rb +13 -7
- data/lib/clacky/agent/session_serializer.rb +67 -6
- data/lib/clacky/agent.rb +215 -4
- data/lib/clacky/agent_config.rb +21 -73
- data/lib/clacky/billing/platform_billing.rb +12 -2
- data/lib/clacky/cli.rb +12 -0
- data/lib/clacky/client.rb +51 -22
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +1 -1
- data/lib/clacky/default_extensions/computer-use/ext.yml +15 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/geometry.rb +134 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/grid.rb +214 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/keycodes.rb +100 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/macos.rb +557 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows.rb +299 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows_agent.ps1 +436 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows_keycodes.rb +119 -0
- data/lib/clacky/default_extensions/computer-use/skills/computer-use/SKILL.md +143 -0
- data/lib/clacky/default_extensions/computer-use/skills/computer-use/bin/computer.rb +547 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +13 -19
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +2 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +1 -0
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +2 -0
- data/lib/clacky/default_skills/channel-manager/SKILL.md +2 -0
- data/lib/clacky/default_skills/channel-manager/install_feishu_skills.rb +1 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +2 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +2 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +3 -1
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +2 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +2 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -0
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +1 -2
- data/lib/clacky/default_skills/persist-memory/SKILL.md +2 -0
- data/lib/clacky/default_skills/personal-website/SKILL.md +2 -0
- data/lib/clacky/default_skills/personal-website/publish.rb +6 -10
- data/lib/clacky/default_skills/product-help/SKILL.md +2 -0
- data/lib/clacky/default_skills/recall-memory/SKILL.md +2 -0
- data/lib/clacky/default_skills/search-skills/SKILL.md +2 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +2 -0
- data/lib/clacky/default_skills/skill-creator/SKILL.md +2 -0
- data/lib/clacky/extension/loader.rb +102 -25
- data/lib/clacky/extension/packager.rb +3 -20
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/locales/en.rb +35 -3
- data/lib/clacky/locales/i18n.rb +16 -2
- data/lib/clacky/locales/zh.rb +36 -4
- data/lib/clacky/media/base.rb +2 -2
- data/lib/clacky/media/generator.rb +2 -2
- data/lib/clacky/message_format/open_ai.rb +28 -0
- data/lib/clacky/platform_http_client.rb +52 -117
- data/lib/clacky/providers.rb +190 -148
- data/lib/clacky/rich_ui/view_renderer.rb +3 -2
- data/lib/clacky/server/browser_manager.rb +14 -0
- data/lib/clacky/server/channel/adapters/base.rb +19 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +76 -2
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +546 -0
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +48 -13
- data/lib/clacky/server/channel/channel_config.rb +21 -4
- data/lib/clacky/server/channel/channel_manager.rb +15 -30
- data/lib/clacky/server/channel/channel_ui_controller.rb +264 -17
- data/lib/clacky/server/http_server.rb +183 -103
- data/lib/clacky/server/scheduler.rb +117 -2
- data/lib/clacky/server/server_master.rb +17 -7
- data/lib/clacky/server/session_registry.rb +0 -4
- data/lib/clacky/server/web_ui_controller.rb +2 -1
- data/lib/clacky/shell_hook_loader.rb +3 -3
- data/lib/clacky/skill.rb +3 -0
- data/lib/clacky/skill_loader.rb +5 -1
- data/lib/clacky/tools/ask_user.rb +3 -12
- data/lib/clacky/tools/file_reader.rb +9 -4
- data/lib/clacky/tools/terminal/session_manager.rb +3 -1
- data/lib/clacky/tools/terminal.rb +12 -1
- data/lib/clacky/ui2/ui_controller.rb +5 -5
- data/lib/clacky/utils/channel_prompt.rb +33 -0
- data/lib/clacky/utils/file_processor.rb +28 -3
- data/lib/clacky/utils/model_pricing.rb +95 -34
- data/lib/clacky/utils/resource_group.rb +67 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1848 -1757
- data/lib/clacky/web/app.js +123 -21
- data/lib/clacky/web/components/composer.js +130 -6
- data/lib/clacky/web/components/datepicker.js +11 -4
- data/lib/clacky/web/components/model-picker.js +181 -34
- data/lib/clacky/web/components/onboard.js +14 -22
- data/lib/clacky/web/components/quote-select.js +51 -14
- data/lib/clacky/web/core/ext.js +39 -1
- data/lib/clacky/web/features/billing/view.js +63 -17
- data/lib/clacky/web/features/channels/store.js +16 -1
- data/lib/clacky/web/features/channels/view.js +158 -148
- data/lib/clacky/web/features/extensions/store.js +206 -272
- data/lib/clacky/web/features/extensions/view.js +388 -274
- data/lib/clacky/web/features/mcp/store.js +1 -1
- data/lib/clacky/web/features/mcp/view.js +112 -119
- data/lib/clacky/web/features/new-session/store.js +4 -2
- data/lib/clacky/web/features/new-session/view.js +94 -16
- data/lib/clacky/web/features/profile/view.js +9 -9
- data/lib/clacky/web/features/share/view.js +500 -571
- data/lib/clacky/web/features/skills/store.js +16 -8
- data/lib/clacky/web/features/skills/view.js +568 -215
- data/lib/clacky/web/features/tasks/store.js +28 -6
- data/lib/clacky/web/features/tasks/view.js +314 -102
- data/lib/clacky/web/features/trash/view.js +4 -6
- data/lib/clacky/web/features/workspace/view.js +32 -0
- data/lib/clacky/web/i18n.js +198 -70
- data/lib/clacky/web/index.html +180 -134
- data/lib/clacky/web/projects.js +10 -3
- data/lib/clacky/web/sessions.js +347 -127
- data/lib/clacky/web/settings.js +8 -0
- data/lib/clacky/web/skills.js +47 -16
- data/lib/clacky/web/utils.js +20 -0
- data/lib/clacky/web/ws-dispatcher.js +26 -10
- data/lib/clacky.rb +16 -1
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23761f95ba9b1fa52c2acc37020fd4e903a92d8f49c0c97668d5cfbb6e572329
|
|
4
|
+
data.tar.gz: 16838f20ba7d650e15952c738c35b93ca418a8df81baaeb6f9fd0263c5754e65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0a29e5ab8a0e6b21e1f2a2f36657bcb8043cfc9d904671acdead8157f082ac032c2d1f7f0a0cee16c0f72a7fdbb939020b3abfe52145a9eea722f4f4856bac2
|
|
7
|
+
data.tar.gz: '050389221f4f71ab6e54a6038606f0d1f48f1a6571116c5b052493362158a850fe0f1a90a4d520b35fff875315fd1292610bab104366251626d5d9fdcb5b7f54'
|
|
@@ -95,15 +95,25 @@ This is the one step the agent handles manually — the script does not write ch
|
|
|
95
95
|
```bash
|
|
96
96
|
gh api orgs/clacky-ai/members --paginate --jq '.[].login' | sort
|
|
97
97
|
```
|
|
98
|
-
- Then cross-reference PR authors in the release range
|
|
98
|
+
- Then cross-reference PR authors in the release range. Use `grep -oE`, not `-oP` —
|
|
99
|
+
BSD grep on macOS rejects `-P` with "invalid option", and the loop silently yields nothing:
|
|
99
100
|
```bash
|
|
100
|
-
git log <previous_tag>..HEAD --oneline | grep -
|
|
101
|
+
git log <previous_tag>..HEAD --oneline | grep -oE '#[0-9]+' | while read pr; do
|
|
101
102
|
num=${pr#\#}
|
|
102
103
|
gh pr view $num --json number,author --jq '"#\(.number) \(.author.login)"'
|
|
103
104
|
done
|
|
104
105
|
```
|
|
105
106
|
- Skip attribution for org members (internal contributors) - only call out external contributors
|
|
106
|
-
-
|
|
107
|
+
- Some cited numbers no longer resolve at all. A commit subject can name a superseded PR
|
|
108
|
+
next to the merged one (`feat: discover custom-endpoint models in the add-model dialog
|
|
109
|
+
(#559) (#560)`), and `gh pr view 559` answers "Could not resolve to a PullRequest".
|
|
110
|
+
When that happens, ask the commit which PR actually merged it:
|
|
111
|
+
```bash
|
|
112
|
+
gh api repos/clacky-ai/openclacky/commits/<sha>/pulls \
|
|
113
|
+
--jq '.[] | "PR #\(.number) | \(.user.login) | \(.state)"'
|
|
114
|
+
```
|
|
115
|
+
Attribute the bullet to the PR that resolves and carries a real author; a number that
|
|
116
|
+
resolves to nobody is not a contributor reference.
|
|
107
117
|
- Example: `- Todo panel with SVG icons (#437 - @shipinliang)`
|
|
108
118
|
|
|
109
119
|
6. Commit the changelog:
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ 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.17] - 2026-09-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Show a top-up action when a free-trial model rejects the request
|
|
11
|
+
- Add Requesty as a named provider preset (#587 - @Thibaultjaigu)
|
|
12
|
+
- Redesign the skills page around cards and a detail view
|
|
13
|
+
- Redesign the scheduled tasks page and keep a run history per task
|
|
14
|
+
- Redesign the channels page as a compact two-column list
|
|
15
|
+
- Rework the MCP page layout, its row menu and the remove confirmation
|
|
16
|
+
- Add "Add to chat" to the workspace file context menu
|
|
17
|
+
- Add a global switch for IM progress cards (#599)
|
|
18
|
+
- Show the vendor logo on the model chip of a new session
|
|
19
|
+
- Use Chinese names for the built-in skills
|
|
20
|
+
|
|
21
|
+
### Improved
|
|
22
|
+
- Optimize the Feishu card styling (#597)
|
|
23
|
+
- Improve drag-and-drop for session references and files (#600)
|
|
24
|
+
- Restyle the channel status badge, cron rows, message settings and search boxes
|
|
25
|
+
- Polish the assistant memory page, onboarding page, billing header and trash toolbar
|
|
26
|
+
- Improve the discoverability of overflowing aside tabs (#601)
|
|
27
|
+
- Show skill, MCP and task errors as in-app toasts instead of browser alerts
|
|
28
|
+
- Localize the default extension icon letter
|
|
29
|
+
- Ship meeting as a default-off extension
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Make an IM message interrupt the running task instead of queueing silently (#586)
|
|
33
|
+
- Hide the sidecar models from the model switchers (#598)
|
|
34
|
+
- Show the "View process" panel in Feishu only when there is process history (#596)
|
|
35
|
+
- Keep a subagent phase running when a message is queued (#594)
|
|
36
|
+
- Fix the skill install click and the extension installed state
|
|
37
|
+
- Strip channel-only prompt prefixes when replaying history (#585)
|
|
38
|
+
- Use the in-app dialog for delete confirmations instead of the browser prompt
|
|
39
|
+
- Toggle a sidebar ··· menu on a repeated click (#595)
|
|
40
|
+
- Hide the auto tag and routing tooltip once the alias is pinned to a model
|
|
41
|
+
|
|
42
|
+
### More
|
|
43
|
+
- Retire Claude 4.5 / Opus 4.6 and Gemini 3.5 / 3.6 Flash from the catalogs
|
|
44
|
+
- Remove legacy fallback domains and dead failover paths
|
|
45
|
+
|
|
46
|
+
## [1.5.16] - 2026-09-24
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- Add Claude Opus 5.5 and GPT-6 Sol/Luna to the model catalog
|
|
50
|
+
- Send task progress cards in Feishu, with collapsible process history (#582)
|
|
51
|
+
- Print a receipt-style usage scorecard from the billing page and share it as an image
|
|
52
|
+
- Understand video attachments through a sidecar model (#564)
|
|
53
|
+
- Open the app straight into a chat from an external clacky:// link, including the welcome view
|
|
54
|
+
- Route requests automatically: the auto alias picks a tier per request and reports which tier served it (#576)
|
|
55
|
+
- Drive the Windows desktop through WSL in computer-use
|
|
56
|
+
- Ship computer-use as a default-off extension
|
|
57
|
+
- Let on_start hooks veto a turn before the agent starts (#573 - @afumu)
|
|
58
|
+
- Cap the memory and CPU of a task's shells and browser daemons on Linux hosts with --task-cgroup
|
|
59
|
+
- Refuse to start on a taken port instead of silently moving to another one, with --strict-port
|
|
60
|
+
- Keep attachment paths when a compressed history chunk is replayed (#581 - @BoyYangzai)
|
|
61
|
+
- Point generated skills at their own directory through an inlined skill_dir
|
|
62
|
+
|
|
63
|
+
### Improved
|
|
64
|
+
- Restyle the web search result card (#575)
|
|
65
|
+
- Use official brand marks for model vendor badges (#566)
|
|
66
|
+
- Refresh the supported model catalogs and pricing (#571)
|
|
67
|
+
- Rename the extension group label User to Personal
|
|
68
|
+
- Adjust the session title size and unify the sidebar active highlight
|
|
69
|
+
- Soften the extension row action buttons and tighten the segmented control corners
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Keep queued guidance attached to the message it was sent from, and allow cancelling it (#584, #578)
|
|
73
|
+
- Stop treating a pasted path as a slash command (#577)
|
|
74
|
+
- Restore structured tool result cards when a session history is replayed (#574)
|
|
75
|
+
- Stop duplicating user bubbles after an edit (#568)
|
|
76
|
+
- Show attachment badges in realtime user bubbles (#567)
|
|
77
|
+
- Transcribe audio attachments through the STT sidecar (#565)
|
|
78
|
+
- Preserve a quote selection while a live update streams in (#569)
|
|
79
|
+
- Load per-key platform usage in parallel in the billing view (#580)
|
|
80
|
+
- Prevent subagents from publishing input queues (#570 - @afumu)
|
|
81
|
+
- Block model switching while a session is running instead of racing the running turn
|
|
82
|
+
- Hand binary files back to the OS when they are reopened
|
|
83
|
+
- Report the doctor host before resolving the backend
|
|
84
|
+
- Stop the DeepSeek preset from showing a version suffix
|
|
85
|
+
|
|
7
86
|
## [1.5.15] - 2026-09-17
|
|
8
87
|
|
|
9
88
|
### Added
|
data/README.md
CHANGED
|
@@ -185,6 +185,17 @@ openclacky server --port 8080 # custom port
|
|
|
185
185
|
openclacky server --host 0.0.0.0 # listen on all interfaces (remote access)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
Linux sandbox hosts can opt into task limits with
|
|
189
|
+
`openclacky server --task-cgroup /sys/fs/cgroup/tasks`. The host must first create
|
|
190
|
+
that cgroup, configure its aggregate memory/CPU limits, and enable the `memory`
|
|
191
|
+
and `cpu` subtree controllers. Terminal shells and browser MCP daemons enter
|
|
192
|
+
child groups before exec; the server remains outside. Terminal results include
|
|
193
|
+
`resource_exhausted: "memory"` and an error when their group records an OOM kill,
|
|
194
|
+
even if the shell masks the child's failure. A separately managed Chrome must
|
|
195
|
+
also be placed under the same parent by its launcher. This option is off by
|
|
196
|
+
default and does not establish a security boundary against privileged commands.
|
|
197
|
+
|
|
198
|
+
|
|
188
199
|
## Configuration
|
|
189
200
|
|
|
190
201
|
```bash
|
|
@@ -194,7 +205,7 @@ $ openclacky
|
|
|
194
205
|
|
|
195
206
|
Set your **API Key**, **Model**, and **Base URL** (any OpenAI-compatible provider).
|
|
196
207
|
|
|
197
|
-
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — or any custom endpoint.
|
|
208
|
+
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty** — or any custom endpoint.
|
|
198
209
|
|
|
199
210
|
## Coding use case
|
|
200
211
|
|
data/README_CN.md
CHANGED
|
@@ -189,7 +189,7 @@ $ openclacky
|
|
|
189
189
|
|
|
190
190
|
设置你的 **API Key**、**模型**和 **Base URL**(任意 OpenAI 兼容提供商)。
|
|
191
191
|
|
|
192
|
-
开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter**,或任意自定义端点。
|
|
192
|
+
开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty**,或任意自定义端点。
|
|
193
193
|
|
|
194
194
|
## 代码开发场景
|
|
195
195
|
|
data/README_JA.md
CHANGED
|
@@ -194,7 +194,7 @@ $ openclacky
|
|
|
194
194
|
|
|
195
195
|
**API Key**、**Model**、**Base URL**(任意の OpenAI 互換プロバイダー)を設定します。
|
|
196
196
|
|
|
197
|
-
標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter** — または任意のカスタムエンドポイント。
|
|
197
|
+
標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter · OrcaRouter · Requesty** — または任意のカスタムエンドポイント。
|
|
198
198
|
|
|
199
199
|
## コーディングのユースケース
|
|
200
200
|
|
|
@@ -208,7 +208,7 @@ module Clacky
|
|
|
208
208
|
|
|
209
209
|
private def navigation_entry(round)
|
|
210
210
|
msg = round[:user_msg]
|
|
211
|
-
user = navigation_preview(msg[:display_text] || extract_text_from_content(msg[:content]))
|
|
211
|
+
user = navigation_preview(msg[:display_text] || strip_channel_prompt_prefix(extract_text_from_content(msg[:content])))
|
|
212
212
|
if user.empty?
|
|
213
213
|
names = Array(msg[:display_files]).map { |file| file[:name] || file["name"] }
|
|
214
214
|
Array(msg[:content]).each do |block|
|
|
@@ -25,7 +25,11 @@ module Clacky
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @return [Hash] `{action: :allow}`, `{action: :deny, reason:}`, or
|
|
28
|
-
# `{action: :handled, result:}` when a hook fulfilled the
|
|
28
|
+
# `{action: :handled, result:}` when a hook fulfilled the event itself.
|
|
29
|
+
# on_start receives (user_input, agent) after task/history initialization.
|
|
30
|
+
# Its handled result is returned unchanged from Agent#run; the extension
|
|
31
|
+
# supplies the run-result status and any queue/feedback flags it needs.
|
|
32
|
+
# on_start is not fired for steering input inside an already running turn.
|
|
29
33
|
# Extra trailing arg: the agent that owns this hook chain, so ext hooks can
|
|
30
34
|
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
31
35
|
# params (`|call|`, `|call, result|`) silently ignore it.
|
|
@@ -43,8 +47,8 @@ module Clacky
|
|
|
43
47
|
# place (chained rewrite), so for non-deny results there's nothing to
|
|
44
48
|
# merge — we just keep going.
|
|
45
49
|
#
|
|
46
|
-
# :handled short-circuits the same way
|
|
47
|
-
# the
|
|
50
|
+
# :handled short-circuits the same way: the hook has already fulfilled
|
|
51
|
+
# the event, so later hooks have nothing left to act on.
|
|
48
52
|
if hook_result[:action] == :deny || hook_result[:action] == :handled
|
|
49
53
|
result = hook_result
|
|
50
54
|
break
|
|
@@ -45,7 +45,7 @@ module Clacky
|
|
|
45
45
|
# Inside call_llm we only *update in place* during retries, so the
|
|
46
46
|
# already-live progress slot shows meaningful transient status
|
|
47
47
|
# ("Network failed… attempt 2/10", etc.).
|
|
48
|
-
private def call_llm
|
|
48
|
+
private def call_llm(agent_upstream_fails: nil, agent_upgrade_fails: nil)
|
|
49
49
|
# Transition :fallback_active → :probing if cooling-off has expired.
|
|
50
50
|
@config.maybe_start_probing
|
|
51
51
|
|
|
@@ -54,6 +54,12 @@ module Clacky
|
|
|
54
54
|
max_retries = 10
|
|
55
55
|
retry_delay = 5
|
|
56
56
|
retries = 0
|
|
57
|
+
# Per-call tier attribution for auto routing: each retryable failure is
|
|
58
|
+
# charged to the tier the gateway actually routed that call to (echoed
|
|
59
|
+
# in X-Clacky-Routed-Tier). Failures without a tier (network layer,
|
|
60
|
+
# timeouts — the gateway never answered) are not attributed to either.
|
|
61
|
+
floor_fails = 0
|
|
62
|
+
upgrade_fails = 0
|
|
57
63
|
|
|
58
64
|
# Track whether any of the retry/fallback branches below opened a
|
|
59
65
|
# "retrying" progress slot via show_progress(progress_type:
|
|
@@ -111,7 +117,9 @@ module Clacky
|
|
|
111
117
|
max_tokens: @config.max_tokens,
|
|
112
118
|
enable_caching: @config.enable_prompt_caching,
|
|
113
119
|
reasoning_effort: @reasoning_effort,
|
|
114
|
-
on_chunk: build_progress_on_chunk
|
|
120
|
+
on_chunk: build_progress_on_chunk,
|
|
121
|
+
agent_upstream_fails: (agent_upstream_fails || 0) + floor_fails,
|
|
122
|
+
agent_upgrade_fails: (agent_upgrade_fails || 0) + upgrade_fails
|
|
115
123
|
)
|
|
116
124
|
|
|
117
125
|
# Successful response — if we were probing, confirm primary is healthy.
|
|
@@ -142,7 +150,7 @@ module Clacky
|
|
|
142
150
|
finish_reason: response[:finish_reason].to_s,
|
|
143
151
|
completion_tokens: response.dig(:token_usage, :completion_tokens)
|
|
144
152
|
)
|
|
145
|
-
raise RetryableError
|
|
153
|
+
raise RetryableError.new("[LLM] Model returned empty response (no content, no tool_calls), retrying...", routed_tier: response[:routed_tier])
|
|
146
154
|
end
|
|
147
155
|
|
|
148
156
|
# Thinking-mode silent response detector. DeepSeek V4 / Kimi K2 /
|
|
@@ -164,7 +172,7 @@ module Clacky
|
|
|
164
172
|
reasoning_tail: reasoning_str[-200, 200] || reasoning_str,
|
|
165
173
|
completion_tokens: response.dig(:token_usage, :completion_tokens)
|
|
166
174
|
)
|
|
167
|
-
raise RetryableError
|
|
175
|
+
raise RetryableError.new("[LLM] Thinking-mode model produced reasoning but empty content/tool_calls, retrying...", routed_tier: response[:routed_tier])
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
rescue Faraday::TimeoutError => e
|
|
@@ -203,21 +211,7 @@ module Clacky
|
|
|
203
211
|
Clacky::Shutdown.sleep(retry_delay)
|
|
204
212
|
retry
|
|
205
213
|
else
|
|
206
|
-
#
|
|
207
|
-
if !@config.url_fallback_active?
|
|
208
|
-
fallback_url = @config.fallback_base_url_for_current_provider
|
|
209
|
-
if fallback_url
|
|
210
|
-
@config.activate_url_fallback!(fallback_url)
|
|
211
|
-
rebuild_client_for_current_model!
|
|
212
|
-
retries = 0
|
|
213
|
-
@ui&.show_warning(
|
|
214
|
-
I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
|
|
215
|
-
)
|
|
216
|
-
retry
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
raise AgentError.new("[LLM] #{I18n.t("llm.error.request_timeout", retries: max_retries)}", raw_message: e.message)
|
|
214
|
+
raise AgentError.new("#{I18n.t("llm.error.request_timeout", retries: max_retries)}", raw_message: e.message)
|
|
221
215
|
end
|
|
222
216
|
|
|
223
217
|
rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
|
|
@@ -251,30 +245,20 @@ module Clacky
|
|
|
251
245
|
Clacky::Shutdown.sleep(retry_delay)
|
|
252
246
|
retry
|
|
253
247
|
else
|
|
254
|
-
# All retries on the primary endpoint exhausted for a network-level
|
|
255
|
-
# failure (DNS/TCP/TLS). Try the secondary gateway URL if available.
|
|
256
|
-
if !@config.url_fallback_active?
|
|
257
|
-
fallback_url = @config.fallback_base_url_for_current_provider
|
|
258
|
-
if fallback_url
|
|
259
|
-
@config.activate_url_fallback!(fallback_url)
|
|
260
|
-
rebuild_client_for_current_model!
|
|
261
|
-
retries = 0
|
|
262
|
-
@ui&.show_warning(
|
|
263
|
-
I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
|
|
264
|
-
)
|
|
265
|
-
retry
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
248
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
270
249
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
271
|
-
raise AgentError.new("
|
|
250
|
+
raise AgentError.new("#{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message)
|
|
272
251
|
end
|
|
273
252
|
|
|
274
253
|
rescue RetryableError => e
|
|
275
254
|
Clacky::Shutdown.checkpoint!
|
|
276
255
|
retries += 1
|
|
277
256
|
|
|
257
|
+
case e.routed_tier
|
|
258
|
+
when "floor" then floor_fails += 1
|
|
259
|
+
when "upgrade" then upgrade_fails += 1
|
|
260
|
+
end
|
|
261
|
+
|
|
278
262
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
279
263
|
if @config.probing?
|
|
280
264
|
handle_probe_failure
|
|
@@ -305,28 +289,9 @@ module Clacky
|
|
|
305
289
|
Clacky::Shutdown.sleep(retry_delay)
|
|
306
290
|
retry
|
|
307
291
|
else
|
|
308
|
-
# All retries on the current endpoint exhausted.
|
|
309
|
-
# If a secondary gateway URL is configured and not yet tried, switch
|
|
310
|
-
# to it now, rebuild the client, and reset the retry counter so the
|
|
311
|
-
# same request gets a fresh MAX_RETRIES_ON_FALLBACK budget on the
|
|
312
|
-
# new endpoint. This is a one-shot switch — url_fallback_active?
|
|
313
|
-
# prevents a second switch if the fallback URL also fails.
|
|
314
|
-
if !@config.url_fallback_active?
|
|
315
|
-
fallback_url = @config.fallback_base_url_for_current_provider
|
|
316
|
-
if fallback_url
|
|
317
|
-
@config.activate_url_fallback!(fallback_url)
|
|
318
|
-
rebuild_client_for_current_model!
|
|
319
|
-
retries = 0
|
|
320
|
-
@ui&.show_warning(
|
|
321
|
-
I18n.t("llm.warn.switching_to_fallback_url", url: fallback_url)
|
|
322
|
-
)
|
|
323
|
-
retry
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
292
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
328
293
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
329
|
-
raise AgentError.new("
|
|
294
|
+
raise AgentError.new("#{I18n.t("llm.error.service_unavailable", retries: current_max)}", raw_message: e.message)
|
|
330
295
|
end
|
|
331
296
|
|
|
332
297
|
rescue Clacky::BadRequestError => e
|
|
@@ -398,10 +363,21 @@ module Clacky
|
|
|
398
363
|
raise
|
|
399
364
|
end
|
|
400
365
|
|
|
366
|
+
# Fold this call's tier-attributed failures into the task-level tallies
|
|
367
|
+
# so later calls in the same task keep routing away from the failing
|
|
368
|
+
# lane — the per-call counters above reset to 0 once this call
|
|
369
|
+
# succeeds. A floor failure proves the floor lane is shaky for this
|
|
370
|
+
# task (route up); an upgrade failure proves the upgrade lane is shaky
|
|
371
|
+
# (route back down and stay on the floor until the task boundary).
|
|
372
|
+
@task_upstream_fails = (@task_upstream_fails || 0) + floor_fails if floor_fails > 0
|
|
373
|
+
@task_upgrade_fails = (@task_upgrade_fails || 0) + upgrade_fails if upgrade_fails > 0
|
|
374
|
+
|
|
401
375
|
# Track cost and collect token usage data.
|
|
402
376
|
# Pass the model name captured at API call time to ensure accurate billing
|
|
403
377
|
# even if the user switched models during the (potentially long) API call.
|
|
404
|
-
|
|
378
|
+
# For the "auto" alias the gateway reports the concrete model it routed
|
|
379
|
+
# to via X-Clacky-Routed-Model — bill under that so pricing resolves.
|
|
380
|
+
token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage], model: response[:routed_model] || api_call_model)
|
|
405
381
|
response[:token_usage] = token_data
|
|
406
382
|
|
|
407
383
|
# [DIAG] Log raw client response shape. Only emit when we see the
|
|
@@ -721,9 +697,11 @@ module Clacky
|
|
|
721
697
|
# and the hint just sits in history without affecting behaviour.
|
|
722
698
|
inject_upstream_truncation_hint_if_first(truncated)
|
|
723
699
|
|
|
724
|
-
raise Clacky::UpstreamTruncatedError
|
|
700
|
+
raise Clacky::UpstreamTruncatedError.new(
|
|
725
701
|
"[LLM] Upstream truncated tool_call `#{truncated[:name]}` " \
|
|
726
|
-
"(args=#{args_str[0, 40].inspect}). Retrying..."
|
|
702
|
+
"(args=#{args_str[0, 40].inspect}). Retrying...",
|
|
703
|
+
routed_tier: response[:routed_tier]
|
|
704
|
+
)
|
|
727
705
|
end
|
|
728
706
|
|
|
729
707
|
# True when a tool_call's arguments field is unusable — either empty
|
|
@@ -678,9 +678,8 @@ module Clacky
|
|
|
678
678
|
lines
|
|
679
679
|
end
|
|
680
680
|
|
|
681
|
-
# Serialize
|
|
682
|
-
#
|
|
683
|
-
# contents deliberately stay out of the chunk archive.
|
|
681
|
+
# Serialize lightweight UI metadata for file badges after compression.
|
|
682
|
+
# Includes path/preview_path when set; file contents and sizes stay out of the archive.
|
|
684
683
|
def display_files_for_archive(msg)
|
|
685
684
|
files = Array(msg[:display_files]).dup
|
|
686
685
|
Array(msg[:content]).each do |block|
|
|
@@ -688,12 +687,14 @@ module Clacky
|
|
|
688
687
|
next unless %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
689
688
|
|
|
690
689
|
name = block[:image_name] || block["image_name"]
|
|
690
|
+
path = block[:image_path] || block["image_path"]
|
|
691
691
|
if name.nil? || name.to_s.strip.empty?
|
|
692
|
-
path = block[:image_path] || block["image_path"]
|
|
693
692
|
name = File.basename(path.to_s).sub(/\A[0-9a-f]{16}_/, "") unless path.to_s.empty?
|
|
694
693
|
end
|
|
695
694
|
name = "image" if name.nil? || name.to_s.strip.empty?
|
|
696
|
-
|
|
695
|
+
entry = { name: name, type: "image" }
|
|
696
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
697
|
+
files << entry
|
|
697
698
|
end
|
|
698
699
|
|
|
699
700
|
files.filter_map do |file|
|
|
@@ -704,8 +705,13 @@ module Clacky
|
|
|
704
705
|
|
|
705
706
|
type = file[:type] || file["type"] || "file"
|
|
706
707
|
type = "file" if type.to_s.strip.empty?
|
|
707
|
-
{ name: name.to_s, type: type.to_s }
|
|
708
|
-
|
|
708
|
+
entry = { name: name.to_s, type: type.to_s }
|
|
709
|
+
path = file[:path] || file["path"]
|
|
710
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
711
|
+
preview = file[:preview_path] || file["preview_path"]
|
|
712
|
+
entry[:preview_path] = preview.to_s unless preview.nil? || preview.to_s.strip.empty?
|
|
713
|
+
entry
|
|
714
|
+
end.uniq { |file| [file[:name], file[:type], file[:path]] }
|
|
709
715
|
end
|
|
710
716
|
|
|
711
717
|
# Once an image has a badge in the archive, omit the generic [image_url]
|
|
@@ -332,10 +332,20 @@ module Clacky
|
|
|
332
332
|
{ has_more: has_more }
|
|
333
333
|
end
|
|
334
334
|
|
|
335
|
+
# ChannelManager prefixes channel prompts with agent-only context for
|
|
336
|
+
# the LLM; the clean original rides in display_text. Replay paths that
|
|
337
|
+
# fall back to raw content (pre-display_text sessions, chunk MD archives)
|
|
338
|
+
# must strip that block for UI parity with live rendering. Gated on
|
|
339
|
+
# channel_info so a normal user literally typing these lines keeps them.
|
|
340
|
+
private def strip_channel_prompt_prefix(text)
|
|
341
|
+
return text unless @channel_info
|
|
342
|
+
Clacky::Utils::ChannelPrompt.strip(text)
|
|
343
|
+
end
|
|
344
|
+
|
|
335
345
|
private def replay_rounds(ui, page)
|
|
336
346
|
page.each do |round|
|
|
337
347
|
msg = round[:user_msg]
|
|
338
|
-
raw_text = msg[:display_text] || extract_text_from_content(msg[:content])
|
|
348
|
+
raw_text = msg[:display_text] || strip_channel_prompt_prefix(extract_text_from_content(msg[:content]))
|
|
339
349
|
# Images: recovered from inline image_url blocks in content (carry data_url for <img> rendering)
|
|
340
350
|
image_files = extract_image_files_from_content(msg[:content])
|
|
341
351
|
# Disk files (PDF, doc, etc.): stored in display_files on the user message at send time
|
|
@@ -507,7 +517,7 @@ module Clacky
|
|
|
507
517
|
synthetic_ts = base_time - ((index ? index[:section_count] : sections.size) - round_offset - round_index) * 1.0
|
|
508
518
|
user_msg = {
|
|
509
519
|
role: "user",
|
|
510
|
-
content: text,
|
|
520
|
+
content: strip_channel_prompt_prefix(text),
|
|
511
521
|
created_at: synthetic_ts,
|
|
512
522
|
ext_events: sec_ext_events,
|
|
513
523
|
_from_chunk: true
|
|
@@ -576,7 +586,7 @@ module Clacky
|
|
|
576
586
|
|
|
577
587
|
# Pull lightweight attachment badge metadata out of a chunk section.
|
|
578
588
|
# The marker is internal archive data and must not appear in replayed text.
|
|
579
|
-
#
|
|
589
|
+
# Restores name, type, and path/preview_path when present in the chunk marker.
|
|
580
590
|
def extract_display_files_from_text(text)
|
|
581
591
|
return [text, []] unless text.include?("_Display files:")
|
|
582
592
|
|
|
@@ -594,7 +604,12 @@ module Clacky
|
|
|
594
604
|
|
|
595
605
|
type = file["type"] || file[:type] || "file"
|
|
596
606
|
type = "file" if type.to_s.strip.empty?
|
|
597
|
-
|
|
607
|
+
entry = { name: name.to_s, type: type.to_s }
|
|
608
|
+
path = file["path"] || file[:path]
|
|
609
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
610
|
+
preview = file["preview_path"] || file[:preview_path]
|
|
611
|
+
entry[:preview_path] = preview.to_s unless preview.nil? || preview.to_s.strip.empty?
|
|
612
|
+
files << entry
|
|
598
613
|
end
|
|
599
614
|
true
|
|
600
615
|
end
|
|
@@ -750,6 +765,7 @@ module Clacky
|
|
|
750
765
|
questions: questions)
|
|
751
766
|
end
|
|
752
767
|
else
|
|
768
|
+
remember_replayed_tool_name(tc[:id], name)
|
|
753
769
|
ui.show_tool_call(name, args)
|
|
754
770
|
end
|
|
755
771
|
end
|
|
@@ -768,20 +784,65 @@ module Clacky
|
|
|
768
784
|
msg[:content].each do |blk|
|
|
769
785
|
next unless blk.is_a?(Hash) && blk[:type] == "tool_result"
|
|
770
786
|
|
|
771
|
-
|
|
787
|
+
content = blk[:content].to_s
|
|
788
|
+
emit_replayed_tool_result(ui, content, blk[:tool_use_id])
|
|
772
789
|
end
|
|
773
790
|
replay_subagent_transcript(msg, ui)
|
|
774
791
|
end
|
|
775
792
|
|
|
776
793
|
when "tool"
|
|
777
794
|
# OpenAI-format tool result
|
|
778
|
-
ui
|
|
795
|
+
emit_replayed_tool_result(ui, msg[:content].to_s, msg[:tool_call_id])
|
|
779
796
|
replay_subagent_transcript(msg, ui)
|
|
780
797
|
end
|
|
781
798
|
|
|
782
799
|
replay_ext_events(msg, ui)
|
|
783
800
|
end
|
|
784
801
|
|
|
802
|
+
# Tool results persist as the raw JSON the tool returned, so replay can
|
|
803
|
+
# rebuild the same structured `ui:` payload the live path sent instead of
|
|
804
|
+
# degrading the card to plain text (C-5797).
|
|
805
|
+
private def emit_replayed_tool_result(ui, content, tool_call_id)
|
|
806
|
+
ui_payload = replayed_tool_ui(content, tool_call_id)
|
|
807
|
+
if ui_payload
|
|
808
|
+
ui.show_tool_result(content, ui: ui_payload)
|
|
809
|
+
else
|
|
810
|
+
ui.show_tool_result(content)
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Tool names live on the preceding assistant message, so remember them by
|
|
815
|
+
# call id while walking the round.
|
|
816
|
+
private def remember_replayed_tool_name(call_id, name)
|
|
817
|
+
return if call_id.nil? || name.to_s.empty?
|
|
818
|
+
|
|
819
|
+
@replayed_tool_names ||= {}
|
|
820
|
+
@replayed_tool_names[call_id.to_s] = name
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
private def replayed_tool_ui(content, tool_call_id)
|
|
824
|
+
return nil if tool_call_id.nil?
|
|
825
|
+
|
|
826
|
+
name = (@replayed_tool_names || {})[tool_call_id.to_s]
|
|
827
|
+
return nil if name.nil?
|
|
828
|
+
|
|
829
|
+
text = content.to_s
|
|
830
|
+
return nil unless text.start_with?("{")
|
|
831
|
+
|
|
832
|
+
resolved = @tool_registry.resolve(name)
|
|
833
|
+
return nil if resolved.nil?
|
|
834
|
+
|
|
835
|
+
tool = @tool_registry.get(resolved)
|
|
836
|
+
return nil unless tool.respond_to?(:ui_result)
|
|
837
|
+
|
|
838
|
+
parsed = JSON.parse(text, symbolize_names: true)
|
|
839
|
+
return nil unless parsed.is_a?(Hash)
|
|
840
|
+
|
|
841
|
+
tool.ui_result(parsed)
|
|
842
|
+
rescue StandardError
|
|
843
|
+
nil
|
|
844
|
+
end
|
|
845
|
+
|
|
785
846
|
# Re-emit custom extension events recorded on a message so extension
|
|
786
847
|
# panels can rebuild their state after a page reload. Mirrors what
|
|
787
848
|
# Agent#emit_event pushed live. No-op when the message carries none.
|