openclacky 1.5.19 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a120f8d1189892876508f1c49309b7e4a21652c9293499211444b6e42770f649
|
|
4
|
+
data.tar.gz: 2c02a199d26ae835426fa5ce8fb65665ac3c342aa7b717337c3e72470fd868f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aed2dcb3fb47de8a9fc9b20cccbae1f45e70338b37a44e5edbea6ec54c8c3065a27976a2ea5cb2b34ac209cbc3d53c8b6866598deb1a78ff5194ecbef6c0d66e
|
|
7
|
+
data.tar.gz: 948e67f2dc8f003e55daea5f8e4d790e5368ef58eec63489d317e8be4336e2e04a9db498bdb1e362005a66b502f930a1dd5c88e9da6607863d9dfc4b06986feb
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,101 @@ 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.21] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Switch the current session between Manual and Auto approval from the composer, even while a task is running
|
|
11
|
+
- Wait 20 seconds on `ask_user` in auto mode before the agent decides, with the delay configurable through `ask_user_timeout_seconds`
|
|
12
|
+
- Add DeepSeek hosted web search and page fetch
|
|
13
|
+
- Archive every session in a project at once
|
|
14
|
+
- Add MiniMax M3.1 Flash
|
|
15
|
+
- Support multiline terminal commands (#641 - @YeEmrick)
|
|
16
|
+
- Fall back to a dependency-free PNG256 encoder when no image tool is available (#668 - @YeEmrick)
|
|
17
|
+
- Configure where the server writes its PID file (#663)
|
|
18
|
+
- Explain an unreachable license server where the license is entered
|
|
19
|
+
- Point extension publishers at the contributor page
|
|
20
|
+
- Prepare and persist shared feedback requests (#649)
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
- Downscale oversized browser screenshots so they cost fewer tokens
|
|
24
|
+
- Raise the file reader per-line limit to 8000 characters (#645 - @YeEmrick)
|
|
25
|
+
- Spell out the billing basis in price tooltips
|
|
26
|
+
- Tidy the session search palette and its content-match snippets
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Refresh data management lists after archiving or deleting from the sidebar (#673)
|
|
30
|
+
- Make bulk delete in the recycle bin and archive respect the active filters (#672)
|
|
31
|
+
- Keep routing channel messages to a session after it is archived (#671)
|
|
32
|
+
- Stop long terminal commands from echoing a duplicated wrapped tail (#670)
|
|
33
|
+
- Page session lists by a server-issued cursor so long lists no longer skip sessions (#669)
|
|
34
|
+
- Refresh the header title when the active session is renamed (#666)
|
|
35
|
+
- Match chat navigator rounds by task id so compression no longer misaligns them (#664)
|
|
36
|
+
- Stop dropping replayed chunks that continue an earlier turn (#662)
|
|
37
|
+
- Apply model card edits to a running session immediately (#661)
|
|
38
|
+
- Keep the attachment strip inside the new-session composer card (#659)
|
|
39
|
+
- Keep the /#new draft name out of sessions created by a quick action (#658)
|
|
40
|
+
- Keep the Default reasoning choice intact after reopening settings
|
|
41
|
+
- Suggest switching models when retries are exhausted
|
|
42
|
+
- Correct MiniMax-M3 rates with its 512K tier
|
|
43
|
+
- Complete MiniMax Flash effort handling and retire the legacy Ollama choices (#655 - @octo-patch)
|
|
44
|
+
- Drop tool results with a blank call id before sending
|
|
45
|
+
- Preserve response headers when handling Anthropic stream errors (#642)
|
|
46
|
+
- Stop compressing oversized recent messages over and over (#643)
|
|
47
|
+
- Localize device-login errors and stop showing the user code
|
|
48
|
+
- Localize the QR lightbox caption and stop clipping its close button
|
|
49
|
+
- Keep web search on the free engine chain unless DeepSeek is chosen
|
|
50
|
+
- Stop extension reloads from serving a stale cached bundle when the manifest path changes
|
|
51
|
+
|
|
52
|
+
### More
|
|
53
|
+
- Retire the slow GPT-6 and Grok 4.7 aliases from the model picker; their lanes stay routable for existing sessions
|
|
54
|
+
- Bill the Bedrock GPT lane at a 20% markup
|
|
55
|
+
|
|
56
|
+
## [1.5.20] - 2026-10-15
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Add a data management page with session archiving and a recycle bin
|
|
60
|
+
- Add a tab context menu and reopen tabs after a reload
|
|
61
|
+
- Preview audio and video files in the workspace (#634)
|
|
62
|
+
- Preview pending image attachments before sending (#629)
|
|
63
|
+
- Zoom and pan in the viewer's image preview
|
|
64
|
+
- Move the todo list into a session bar popover (#619)
|
|
65
|
+
- Add opt-in per-task feedback countdowns (#633 - @no7dw)
|
|
66
|
+
- Name a session automatically from its first message
|
|
67
|
+
- Show the active session title in the top header
|
|
68
|
+
- Show the QQ bind QR inline with expiry refresh and a sticky success state (#621)
|
|
69
|
+
- Add the oc-grok-4.7, abs-claude-haiku-5-5 and or-gemini-4-argon aliases
|
|
70
|
+
|
|
71
|
+
### Improved
|
|
72
|
+
- Lead model picker rows with their provider
|
|
73
|
+
- Retune the model series discounts and say where they come from
|
|
74
|
+
- Point the openclacky lite tier at claude-haiku-5-5
|
|
75
|
+
- Soften the segmented control hover
|
|
76
|
+
- Tidy the top-up label and the model row spacing
|
|
77
|
+
- Put the model remark under the API key and align it left
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- Pair replayed tool results with their own calls so a refreshed session no longer strands them in the wrong tool item (#640)
|
|
81
|
+
- Keep the assistant timestamp when a session is archived and replayed from chunk history (#639 - @BoyYangzai)
|
|
82
|
+
- Stop dropping attachment messages while a task is being processed (#628)
|
|
83
|
+
- Handle a missing workspace path in the directory picker (#623)
|
|
84
|
+
- Restore the settings tab after switching sessions (#626)
|
|
85
|
+
- Send model picker actions to the right settings tab (#624)
|
|
86
|
+
- Stop session action buttons from squeezing the session title (#625)
|
|
87
|
+
- Improve the mobile billing chart usability (#627)
|
|
88
|
+
- Stop truncating the navigator preview too early (#632)
|
|
89
|
+
- Refresh the Files tree after restoring a task from the timeline (#638)
|
|
90
|
+
- Checkpoint the latest task after a session reload (#637)
|
|
91
|
+
- Localize screenshot upload errors and retry after binding a device (#636)
|
|
92
|
+
- Persist run errors in the session history
|
|
93
|
+
- Stop pinning an auto-routed task to a dead lane when a failure carries no tier
|
|
94
|
+
- Reword network failures and link them to the key console
|
|
95
|
+
- Align the sidebar toggle buttons and unify their icons at 16px (#622)
|
|
96
|
+
- Fade the collapsed aside opener icon
|
|
97
|
+
|
|
98
|
+
### More
|
|
99
|
+
- Retire gemini-3.7-flash and drop the oc-minimax-m2.7 alias from the model picker
|
|
100
|
+
- Remove the OrcaRouter and Requesty provider presets
|
|
101
|
+
|
|
7
102
|
## [1.5.19] - 2026-10-10
|
|
8
103
|
|
|
9
104
|
### Added
|
data/README.md
CHANGED
|
@@ -205,7 +205,25 @@ $ openclacky
|
|
|
205
205
|
|
|
206
206
|
Set your **API Key**, **Model**, and **Base URL** (any OpenAI-compatible provider).
|
|
207
207
|
|
|
208
|
-
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter
|
|
208
|
+
Supported out of the box: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter** — or any custom endpoint.
|
|
209
|
+
|
|
210
|
+
In Web, use the permission selector above the message box to switch the current
|
|
211
|
+
session between **Manual** and **Auto**, including while running. The choice
|
|
212
|
+
is saved for that session. Running tools finish normally; subsequent actions use
|
|
213
|
+
the new mode. Switching to Manual stops an active question countdown. Auto
|
|
214
|
+
approve runs tools without approval.
|
|
215
|
+
|
|
216
|
+
In auto-approve mode, CLI and Web wait 20 seconds after `ask_user` before the
|
|
217
|
+
agent decides how to continue. Typing or selecting an answer cancels the timer
|
|
218
|
+
and waits for submission. Configure the duration in `~/.clacky/config.yml`:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
settings:
|
|
222
|
+
ask_user_timeout_seconds: 20
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use a non-negative integer (seconds); `0` continues immediately. Missing or
|
|
226
|
+
invalid values use 20 seconds. Other permission modes wait for an answer.
|
|
209
227
|
|
|
210
228
|
## Coding use case
|
|
211
229
|
|
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
|
|
192
|
+
开箱即支持:**Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter**,或任意自定义端点。
|
|
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
|
|
197
|
+
標準でサポート: **Claude (Anthropic) · GPT (OpenAI) · DeepSeek · Kimi (Moonshot) · MiniMax · OpenRouter** — または任意のカスタムエンドポイント。
|
|
198
198
|
|
|
199
199
|
## コーディングのユースケース
|
|
200
200
|
|
|
@@ -40,6 +40,7 @@ module Clacky
|
|
|
40
40
|
rounds = []
|
|
41
41
|
nested = []
|
|
42
42
|
section_count = 0
|
|
43
|
+
continuation_start = nil
|
|
43
44
|
user_start = nil
|
|
44
45
|
user_task_id = nil
|
|
45
46
|
archived_at = nil
|
|
@@ -60,6 +61,7 @@ module Clacky
|
|
|
60
61
|
header = chunk_section_header(text, directory)
|
|
61
62
|
if header
|
|
62
63
|
section_count += 1
|
|
64
|
+
continuation_start ||= start
|
|
63
65
|
nested << header[:nested_chunk] if header[:nested_chunk]
|
|
64
66
|
user_start = header[:role] == "user" ? start : nil
|
|
65
67
|
user_task_id = header[:role] == "user" ? header[:task_id] : nil
|
|
@@ -78,7 +80,12 @@ module Clacky
|
|
|
78
80
|
end
|
|
79
81
|
rounds.last[:length] = position - rounds.last[:start] if rounds.any?
|
|
80
82
|
end
|
|
81
|
-
|
|
83
|
+
# Compression can cut mid-turn: sections before the first user turn continue the previous turn.
|
|
84
|
+
continuation_end = rounds.any? ? rounds.first[:start] : position
|
|
85
|
+
if continuation_start && continuation_end > continuation_start
|
|
86
|
+
continuation_range = { start: continuation_start, length: continuation_end - continuation_start }
|
|
87
|
+
end
|
|
88
|
+
{ rounds: rounds, nested: nested, section_count: section_count, continuation_range: continuation_range,
|
|
82
89
|
base_time: (archived_at || File.mtime(path)).to_f }
|
|
83
90
|
end
|
|
84
91
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
module Feedback
|
|
6
|
+
# Extensions prepare a single immutable snapshot before the UI or its
|
|
7
|
+
# countdown sees it. The same snapshot becomes the model's tool result.
|
|
8
|
+
private def prepare_feedback_request(call, result, assistant_message:)
|
|
9
|
+
args = call[:arguments].is_a?(String) ? JSON.parse(call[:arguments]) : call[:arguments]
|
|
10
|
+
request = {
|
|
11
|
+
request_id: call[:id].to_s,
|
|
12
|
+
questions: Tools::AskUser.normalize_questions(args),
|
|
13
|
+
context: Tools::AskUser.fetch_key(args, :context).to_s
|
|
14
|
+
}
|
|
15
|
+
@hooks.trigger(:prepare_feedback, request)
|
|
16
|
+
check_stale!
|
|
17
|
+
request[:request_id] = call[:id].to_s
|
|
18
|
+
request[:questions] = Tools::AskUser.normalize_questions(request)
|
|
19
|
+
raise ArgumentError, "Feedback preparation removed all questions" if request[:questions].empty?
|
|
20
|
+
|
|
21
|
+
request[:context] = request[:context].to_s
|
|
22
|
+
request[:answer_context] = request[:answer_context].to_s
|
|
23
|
+
@history.attach_feedback_request(message: assistant_message, tool_call_id: call[:id], request: request)
|
|
24
|
+
result[:message] = [Tools::AskUser.render_text(request[:questions], request[:context]),
|
|
25
|
+
request[:answer_context]].reject(&:empty?).join("\n\n")
|
|
26
|
+
request.reject { |key, _| key == :answer_context }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -11,7 +11,7 @@ module Clacky
|
|
|
11
11
|
sources = navigation_sources
|
|
12
12
|
manifest = sources.map do |source|
|
|
13
13
|
entry = { key: source[:key], version: source[:version], count: navigation_source_count(source),
|
|
14
|
-
volatile: !!source[:rounds] || source[:continuation].to_a.any? }
|
|
14
|
+
volatile: !!source[:rounds] || source[:continuation].to_a.any? || source[:continuation_ranges].to_a.any? }
|
|
15
15
|
if source[:rounds]
|
|
16
16
|
entry[:identities] = source[:rounds].map { |round| round[:user_msg].values_at(:created_at, :task_id) }
|
|
17
17
|
else
|
|
@@ -144,6 +144,13 @@ module Clacky
|
|
|
144
144
|
end
|
|
145
145
|
chunks.uniq.each { |path| append_source.call(path) }
|
|
146
146
|
@chunk_index_mutex&.synchronize { @chunk_indexes&.delete_if { |path, _| !visited.include?(path) } }
|
|
147
|
+
owner = nil
|
|
148
|
+
sources.each do |source|
|
|
149
|
+
if owner && source[:index][:continuation_range]
|
|
150
|
+
(owner[:continuation_ranges] ||= []) << { path: source[:path], index: source[:index] }
|
|
151
|
+
end
|
|
152
|
+
owner = source if navigation_source_count(source).positive?
|
|
153
|
+
end
|
|
147
154
|
last_archive = sources.reverse_each.find { |source| navigation_source_count(source).positive? }
|
|
148
155
|
last_archive[:continuation] = leading if last_archive
|
|
149
156
|
version = rounds.map { |round| round[:user_msg].values_at(:created_at, :task_id) }.hash.to_s
|
|
@@ -201,8 +208,15 @@ module Clacky
|
|
|
201
208
|
end
|
|
202
209
|
result
|
|
203
210
|
end
|
|
204
|
-
if rounds.any? && offset + length == count
|
|
205
|
-
|
|
211
|
+
if rounds.any? && offset + length == count
|
|
212
|
+
Array(source[:continuation_ranges]).each do |entry|
|
|
213
|
+
events = parse_chunk_md_events(entry[:path], excluded_paths: siblings,
|
|
214
|
+
byte_range: entry[:index][:continuation_range], index: entry[:index])
|
|
215
|
+
raise ArgumentError, "History changed while reading" unless chunk_file_version(entry[:path]) == entry[:index][:version]
|
|
216
|
+
|
|
217
|
+
rounds.last[:events].concat(events)
|
|
218
|
+
end
|
|
219
|
+
rounds.last[:events].concat(source[:continuation]) if source[:continuation]
|
|
206
220
|
end
|
|
207
221
|
rounds.each { |round| round[:events].reject! { |event| event[:compressed_summary] } }
|
|
208
222
|
rounds.each_with_index do |round, position|
|
|
@@ -5,6 +5,7 @@ module Clacky
|
|
|
5
5
|
HOOK_EVENTS = [
|
|
6
6
|
:before_tool_use,
|
|
7
7
|
:after_tool_use,
|
|
8
|
+
:prepare_feedback,
|
|
8
9
|
:on_tool_error,
|
|
9
10
|
:on_start,
|
|
10
11
|
:on_complete,
|
|
@@ -34,6 +35,10 @@ module Clacky
|
|
|
34
35
|
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
35
36
|
# params (`|call|`, `|call, result|`) silently ignore extra arguments.
|
|
36
37
|
# Start/complete append context after the agent. Completion is observation-only.
|
|
38
|
+
# prepare_feedback receives (request, agent) after tool tracking, before UI
|
|
39
|
+
# display/countdown. It may extend questions, display context and model-only
|
|
40
|
+
# answer_context in place; answer_context is never sent to UI subscribers.
|
|
41
|
+
# return values are ignored. Keep work bounded; no separate UI events.
|
|
37
42
|
def trigger(event, *args)
|
|
38
43
|
validate_event!(event)
|
|
39
44
|
# Keep payload hashes positional: Ruby 2.6 otherwise promotes their keys
|
|
@@ -46,7 +51,7 @@ module Clacky
|
|
|
46
51
|
callback_args = [*args, @agent]
|
|
47
52
|
callback_args << context.dup if context
|
|
48
53
|
hook_result = hook.call(*callback_args)
|
|
49
|
-
next if
|
|
54
|
+
next if [:on_complete, :prepare_feedback].include?(event)
|
|
50
55
|
next unless hook_result.is_a?(Hash)
|
|
51
56
|
# First deny wins and stops the chain: a weaker later verdict must
|
|
52
57
|
# never clobber a stronger earlier one, and the first deny's reason
|
|
@@ -48,6 +48,7 @@ module Clacky
|
|
|
48
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
|
+
refresh_client_if_card_changed!
|
|
51
52
|
|
|
52
53
|
tools_to_send = @tool_registry.all_definitions
|
|
53
54
|
|
|
@@ -56,8 +57,15 @@ module Clacky
|
|
|
56
57
|
retries = 0
|
|
57
58
|
# Per-call tier attribution for auto routing: each retryable failure is
|
|
58
59
|
# charged to the tier the gateway actually routed that call to (echoed
|
|
59
|
-
# in X-Clacky-Routed-Tier).
|
|
60
|
-
#
|
|
60
|
+
# in X-Clacky-Routed-Tier). A tier-less failure is charged to the floor
|
|
61
|
+
# when the call went out as "auto": the gateway only stamps the tier
|
|
62
|
+
# header after it has routed the call, so a missing tier means the
|
|
63
|
+
# failure happened before routing (HTML error page, truncated body,
|
|
64
|
+
# rejection at the auth layer) — and the gateway defaults to the floor
|
|
65
|
+
# lane in exactly that case. Leaving it uncharged would keep
|
|
66
|
+
# agent_upstream_fails at 0 and pin the task to a dead floor lane until
|
|
67
|
+
# its retry budget ran out. Non-auto calls and unknown tier values are
|
|
68
|
+
# attributed to neither lane.
|
|
61
69
|
floor_fails = 0
|
|
62
70
|
upgrade_fails = 0
|
|
63
71
|
|
|
@@ -247,7 +255,7 @@ module Clacky
|
|
|
247
255
|
else
|
|
248
256
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
249
257
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
250
|
-
raise AgentError.new("#{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message)
|
|
258
|
+
raise AgentError.new("#{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message, action_url: @client.account_url)
|
|
251
259
|
end
|
|
252
260
|
|
|
253
261
|
rescue RetryableError => e
|
|
@@ -257,6 +265,12 @@ module Clacky
|
|
|
257
265
|
case e.routed_tier
|
|
258
266
|
when "floor" then floor_fails += 1
|
|
259
267
|
when "upgrade" then upgrade_fails += 1
|
|
268
|
+
when nil then floor_fails += 1 if api_call_model == "auto"
|
|
269
|
+
else
|
|
270
|
+
if defined?(Clacky::Logger)
|
|
271
|
+
Clacky::Logger.warn("llm_caller.unknown_routed_tier",
|
|
272
|
+
tier: e.routed_tier, model: api_call_model)
|
|
273
|
+
end
|
|
260
274
|
end
|
|
261
275
|
|
|
262
276
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -246,6 +246,25 @@ module Clacky
|
|
|
246
246
|
# Note: we need to remove the compression instruction message we just added
|
|
247
247
|
original_messages = @history.to_a[0..-2] # All except the last (compression instruction)
|
|
248
248
|
|
|
249
|
+
# Keep the usual recent messages unless they would immediately trigger
|
|
250
|
+
# compression again. The summary covers all original_messages, so any
|
|
251
|
+
# dropped recent messages can safely join the archive below.
|
|
252
|
+
[nil, 10, 0].each do |count|
|
|
253
|
+
if count
|
|
254
|
+
compression_context[:recent_messages] = get_recent_messages_with_tool_pairs(compression_context[:recent_messages], count)
|
|
255
|
+
end
|
|
256
|
+
candidate = @message_compressor.rebuild_with_compression(
|
|
257
|
+
compressed_content,
|
|
258
|
+
original_messages: original_messages,
|
|
259
|
+
recent_messages: compression_context[:recent_messages],
|
|
260
|
+
pulled_back_messages: compression_context[:pulled_back_messages] || []
|
|
261
|
+
)
|
|
262
|
+
break if MessageHistory.new(candidate).estimate_tokens < @config.compression_threshold &&
|
|
263
|
+
candidate.size < @config.message_count_threshold
|
|
264
|
+
|
|
265
|
+
raise "History still exceeds compression thresholds after summarization" if count == 0
|
|
266
|
+
end
|
|
267
|
+
|
|
249
268
|
# Archive compressed messages to a chunk MD file before discarding them.
|
|
250
269
|
#
|
|
251
270
|
# IMPORTANT: chunk_index and previous_chunks MUST come from disk, not from
|
|
@@ -309,7 +328,7 @@ module Clacky
|
|
|
309
328
|
{ basename: c[:basename], path: c[:path], topics: c[:topics] }
|
|
310
329
|
end
|
|
311
330
|
|
|
312
|
-
@
|
|
331
|
+
rebuilt_messages = @message_compressor.rebuild_with_compression(
|
|
313
332
|
compressed_content,
|
|
314
333
|
original_messages: original_messages,
|
|
315
334
|
recent_messages: compression_context[:recent_messages],
|
|
@@ -317,7 +336,14 @@ module Clacky
|
|
|
317
336
|
topics: topics,
|
|
318
337
|
previous_chunks: previous_chunks,
|
|
319
338
|
pulled_back_messages: compression_context[:pulled_back_messages] || []
|
|
320
|
-
)
|
|
339
|
+
)
|
|
340
|
+
# Archive references also consume context; never re-enter compression
|
|
341
|
+
# indefinitely if even the final rebuilt history cannot fit.
|
|
342
|
+
if MessageHistory.new(rebuilt_messages).estimate_tokens >= @config.compression_threshold ||
|
|
343
|
+
rebuilt_messages.size >= @config.message_count_threshold
|
|
344
|
+
raise "History still exceeds compression thresholds after summarization"
|
|
345
|
+
end
|
|
346
|
+
@history.replace_all(rebuilt_messages)
|
|
321
347
|
|
|
322
348
|
# Reset to the estimated size of the rebuilt (small) history.
|
|
323
349
|
# The compression call_llm reported the OLD large token count, so
|
|
@@ -641,6 +667,7 @@ module Clacky
|
|
|
641
667
|
lines << "## Assistant"
|
|
642
668
|
end
|
|
643
669
|
lines << ""
|
|
670
|
+
append_created_at_line(lines, msg)
|
|
644
671
|
# Include tool calls summary if present
|
|
645
672
|
# Format: "_Tool calls: name | {args_json}_" so replay can restore args for WebUI display.
|
|
646
673
|
if msg[:tool_calls]&.any?
|
|
@@ -733,6 +760,13 @@ module Clacky
|
|
|
733
760
|
lines << ""
|
|
734
761
|
end
|
|
735
762
|
|
|
763
|
+
def append_created_at_line(lines, msg)
|
|
764
|
+
return unless msg[:created_at].is_a?(Numeric)
|
|
765
|
+
|
|
766
|
+
lines << "_Created at: #{msg[:created_at]}_"
|
|
767
|
+
lines << ""
|
|
768
|
+
end
|
|
769
|
+
|
|
736
770
|
# Serialize persisted extension events into the chunk MD so they survive
|
|
737
771
|
# compression and can be replayed in place. Mirrors the "_Tool calls:"
|
|
738
772
|
# convention; parse_chunk_md_to_rounds reads it back.
|
|
@@ -17,6 +17,7 @@ module Clacky
|
|
|
17
17
|
@session_id = session_data[:session_id]
|
|
18
18
|
@name = session_data[:name] || ""
|
|
19
19
|
@pinned = session_data[:pinned] || false
|
|
20
|
+
@archived_at = session_data[:archived_at]
|
|
20
21
|
@history = MessageHistory.new(session_data[:messages] || [])
|
|
21
22
|
@input_queue = (session_data[:pending_inputs] || []).map do |entry|
|
|
22
23
|
entry.reject { |key, _| key == :steer_target }.merge(delivery: "queue")
|
|
@@ -42,6 +43,9 @@ module Clacky
|
|
|
42
43
|
# Restore project association (nil = no project)
|
|
43
44
|
@project_id = session_data[:project_id]
|
|
44
45
|
|
|
46
|
+
mode_override = session_data.dig(:config, :permission_mode_override)
|
|
47
|
+
self.permission_mode = mode_override if %w[confirm_all auto_approve].include?(mode_override)
|
|
48
|
+
|
|
45
49
|
# Restore standing goal state (nil = no goal). The judge client/model
|
|
46
50
|
# are rebound lazily via #goal_manager when first accessed after
|
|
47
51
|
# restore — but the persisted state must be reattached here so a
|
|
@@ -75,6 +79,9 @@ module Clacky
|
|
|
75
79
|
@task_parents = raw_parents.each_with_object({}) { |(k, v), h| h[k.to_s.to_i] = v.to_i }
|
|
76
80
|
@current_task_id = (session_data.dig(:time_machine, :current_task_id) || 0).to_i
|
|
77
81
|
@active_task_id = (session_data.dig(:time_machine, :active_task_id) || 0).to_i
|
|
82
|
+
# Not persisted: assume the latest task's AFTER may be stale so the next
|
|
83
|
+
# task start re-checkpoints it instead of skipping it.
|
|
84
|
+
@latest_after_dirty = true
|
|
78
85
|
|
|
79
86
|
raw_meta = session_data.dig(:time_machine, :task_meta) || {}
|
|
80
87
|
@task_meta = raw_meta.each_with_object({}) do |(k, v), h|
|
|
@@ -99,7 +106,20 @@ module Clacky
|
|
|
99
106
|
last_error = session_data.dig(:stats, :last_error)
|
|
100
107
|
|
|
101
108
|
if last_status == "error" && last_error
|
|
102
|
-
|
|
109
|
+
real_users = @history.real_user_messages
|
|
110
|
+
task_tagged_history = real_users.any? { |message| !message[:task_id].nil? }
|
|
111
|
+
failed_turn_persisted = real_users.any? do |message|
|
|
112
|
+
message[:task_id].to_s == @current_task_id.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Modern sessions identify each user turn by task_id. Only schedule
|
|
116
|
+
# the legacy deferred rollback when the failed task's user message
|
|
117
|
+
# actually survived in history. Attachment setup can now roll its
|
|
118
|
+
# staged turn back before the error snapshot is saved; blindly
|
|
119
|
+
# removing the last user message after restore would then delete the
|
|
120
|
+
# preceding successful turn. Sessions without task ids keep the old
|
|
121
|
+
# fallback for backward compatibility.
|
|
122
|
+
@pending_error_rollback = true if !task_tagged_history || failed_turn_persisted
|
|
103
123
|
end
|
|
104
124
|
|
|
105
125
|
saved_reasoning = session_data.dig(:config, :reasoning_effort)
|
|
@@ -244,6 +264,9 @@ module Clacky
|
|
|
244
264
|
session_id: @session_id,
|
|
245
265
|
name: @name,
|
|
246
266
|
pinned: @pinned,
|
|
267
|
+
# Kept next to the other scalars so SessionManager's header-only
|
|
268
|
+
# reader can pick it up without parsing the message payload.
|
|
269
|
+
archived_at: @archived_at,
|
|
247
270
|
created_at: @created_at,
|
|
248
271
|
updated_at: stamp,
|
|
249
272
|
working_dir: @working_dir,
|
|
@@ -263,6 +286,7 @@ module Clacky
|
|
|
263
286
|
# model_name is saved so the session can restore its original model on restart
|
|
264
287
|
# (falling back to the current default if the model no longer exists).
|
|
265
288
|
permission_mode: @config.permission_mode.to_s,
|
|
289
|
+
permission_mode_override: @permission_mode_override,
|
|
266
290
|
enable_compression: @config.enable_compression,
|
|
267
291
|
enable_idle_compression: @config.enable_idle_compression,
|
|
268
292
|
enable_prompt_caching: @config.enable_prompt_caching,
|
|
@@ -410,7 +434,7 @@ module Clacky
|
|
|
410
434
|
# @param chunk_path [String] Path to the chunk md file
|
|
411
435
|
# @return [Array<Hash>] rounds array (may be empty if file missing/unreadable)
|
|
412
436
|
private def parse_chunk_md_to_rounds(chunk_path, visited: Set.new, excluded_paths: Set.new,
|
|
413
|
-
byte_range: nil, index: nil, round_offset: 0)
|
|
437
|
+
byte_range: nil, index: nil, round_offset: 0, continue_round: nil)
|
|
414
438
|
return [] unless chunk_path
|
|
415
439
|
|
|
416
440
|
# 1. Try the stored absolute path first (same machine, normal case).
|
|
@@ -486,7 +510,7 @@ module Clacky
|
|
|
486
510
|
|
|
487
511
|
# Group into rounds: each user section starts a new round
|
|
488
512
|
rounds = []
|
|
489
|
-
current_round =
|
|
513
|
+
current_round = continue_round
|
|
490
514
|
round_index = 0
|
|
491
515
|
|
|
492
516
|
sections.each do |sec|
|
|
@@ -543,12 +567,15 @@ module Clacky
|
|
|
543
567
|
# Old: "_Tool calls: name1, name2_" (backward compat)
|
|
544
568
|
remaining_lines = []
|
|
545
569
|
pending_tool_entries = [] # [{name:, args:}]
|
|
570
|
+
created_at = nil
|
|
546
571
|
|
|
547
572
|
text.each_line do |line|
|
|
548
573
|
stripped = line.strip
|
|
549
574
|
if (m = stripped.match(/\A_Tool calls?:\s*(.+?)_?\z/i))
|
|
550
575
|
raw = m[1]
|
|
551
576
|
pending_tool_entries.concat(parse_tool_calls_line(raw))
|
|
577
|
+
elsif (m = stripped.match(/\A_Created at: (\d+(?:\.\d+)?)_\z/))
|
|
578
|
+
created_at = m[1].to_f
|
|
552
579
|
else
|
|
553
580
|
remaining_lines << line
|
|
554
581
|
end
|
|
@@ -557,7 +584,7 @@ module Clacky
|
|
|
557
584
|
# Flush any plain text
|
|
558
585
|
plain_text = remaining_lines.join.strip
|
|
559
586
|
unless plain_text.empty?
|
|
560
|
-
current_round[:events] << { role: "assistant", content: plain_text, interim: !pending_tool_entries.empty
|
|
587
|
+
current_round[:events] << { role: "assistant", content: plain_text, interim: !pending_tool_entries.empty?, created_at: created_at }
|
|
561
588
|
end
|
|
562
589
|
|
|
563
590
|
# Emit one synthetic tool_calls message per detected tool
|
|
@@ -583,6 +610,14 @@ module Clacky
|
|
|
583
610
|
[]
|
|
584
611
|
end
|
|
585
612
|
|
|
613
|
+
# Parse a chunk byte range holding only the tail of a turn that began in an earlier chunk.
|
|
614
|
+
private def parse_chunk_md_events(chunk_path, byte_range:, index:, excluded_paths: Set.new)
|
|
615
|
+
round = { events: [] }
|
|
616
|
+
parse_chunk_md_to_rounds(chunk_path, excluded_paths: excluded_paths, byte_range: byte_range,
|
|
617
|
+
index: index, continue_round: round)
|
|
618
|
+
round[:events]
|
|
619
|
+
end
|
|
620
|
+
|
|
586
621
|
|
|
587
622
|
# Pull lightweight attachment badge metadata out of a chunk section.
|
|
588
623
|
# The marker is internal archive data and must not appear in replayed text.
|
|
@@ -756,6 +791,12 @@ module Clacky
|
|
|
756
791
|
# Special handling: the ask_user question is shown as an assistant
|
|
757
792
|
# message (matching real-time behavior), not as a tool call.
|
|
758
793
|
if Clacky::Tools::AskUser.feedback_tool?(name)
|
|
794
|
+
prepared = msg[:feedback_requests] || {}
|
|
795
|
+
request = prepared[tc[:id].to_s] || prepared[tc[:id].to_s.to_sym]
|
|
796
|
+
if request
|
|
797
|
+
ui.show_tool_call(name, request.reject { |key, _| key.to_s == "answer_context" })
|
|
798
|
+
next
|
|
799
|
+
end
|
|
759
800
|
questions = Clacky::Tools::AskUser.normalize_questions(args)
|
|
760
801
|
context = Clacky::Tools::AskUser.fetch_key(args, :context).to_s
|
|
761
802
|
first = questions.first
|