openclacky 1.5.18 → 1.5.20
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 +87 -0
- data/README.md +1 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/docs/session-observation-hooks.md +86 -0
- data/lib/clacky/agent/chunk_index.rb +4 -1
- data/lib/clacky/agent/history_navigation.rb +6 -1
- data/lib/clacky/agent/hook_manager.rb +11 -4
- data/lib/clacky/agent/llm_caller.rb +16 -3
- data/lib/clacky/agent/message_compressor_helper.rb +8 -0
- data/lib/clacky/agent/session_serializer.rb +25 -2
- data/lib/clacky/agent.rb +139 -36
- data/lib/clacky/billing/platform_billing.rb +10 -0
- data/lib/clacky/cli.rb +2 -2
- data/lib/clacky/client.rb +7 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +2 -1
- data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +3 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +77 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/channel-manager/SKILL.md +70 -9
- data/lib/clacky/default_skills/channel-manager/qq_setup.rb +79 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +45 -14
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/locales/en.rb +2 -2
- data/lib/clacky/locales/zh.rb +2 -2
- data/lib/clacky/media/openai_compat.rb +89 -2
- data/lib/clacky/message_format/bedrock.rb +5 -2
- data/lib/clacky/message_history.rb +24 -1
- data/lib/clacky/providers.rb +54 -144
- data/lib/clacky/server/channel/adapters/qq/adapter.rb +662 -0
- data/lib/clacky/server/channel/adapters/qq/api_client.rb +245 -0
- data/lib/clacky/server/channel/adapters/qq/gateway_client.rb +340 -0
- data/lib/clacky/server/channel/adapters/qq/qr_binder.rb +140 -0
- data/lib/clacky/server/channel/channel_config.rb +9 -0
- data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +235 -45
- data/lib/clacky/server/model_prices.rb +14 -3
- data/lib/clacky/server/session_registry.rb +21 -3
- data/lib/clacky/server/web_ui_controller.rb +49 -0
- data/lib/clacky/session_manager.rb +21 -5
- data/lib/clacky/shell_hook_loader.rb +3 -1
- data/lib/clacky/utils/model_pricing.rb +254 -30
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1133 -308
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +154 -171
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/composer.js +11 -2
- data/lib/clacky/web/components/model-picker.js +167 -64
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/core/aside.js +72 -0
- data/lib/clacky/web/features/archive/store.js +182 -0
- data/lib/clacky/web/features/archive/view.js +399 -0
- data/lib/clacky/web/features/backup/view.js +12 -2
- data/lib/clacky/web/features/billing/store.js +1 -1
- data/lib/clacky/web/features/billing/view.js +207 -41
- data/lib/clacky/web/features/channels/store.js +25 -0
- data/lib/clacky/web/features/channels/view.js +168 -5
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/skills/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +4 -4
- data/lib/clacky/web/features/trash/view.js +57 -9
- data/lib/clacky/web/features/workspace/view.js +509 -28
- data/lib/clacky/web/i18n.js +186 -48
- data/lib/clacky/web/index.html +224 -87
- data/lib/clacky/web/projects.js +131 -3
- data/lib/clacky/web/sessions.js +416 -131
- data/lib/clacky/web/settings.js +62 -24
- data/lib/clacky/web/ws-dispatcher.js +24 -8
- data/lib/clacky.rb +3 -2
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a87f3bff7a9f13d77ae6ae11ccc67a67a6429d596d922309e3f3847469b240f4
|
|
4
|
+
data.tar.gz: 6f7043a63e27dfb922bac4e586ec19c6d0659efb75ba9ef7e1b522b1a16b9410
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23cc0c1d6902579a09f2cb3c9b9f692c61dcb485daca62ff226ea6fa1c165afa141fbee8ee51de24e97ec4d7060bc872988c6f8b785575180f821a52e68acc26
|
|
7
|
+
data.tar.gz: d198a4ed2d6887094fe8f90fc1d13a429cd06008f47859acb7b334804d5d8dfc727f31dba6486ae02d11613bfd0dbe2f0004c83090a8e7f457d31891b4e29fae
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,93 @@ 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.20] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add a data management page with session archiving and a recycle bin
|
|
11
|
+
- Add a tab context menu and reopen tabs after a reload
|
|
12
|
+
- Preview audio and video files in the workspace (#634)
|
|
13
|
+
- Preview pending image attachments before sending (#629)
|
|
14
|
+
- Zoom and pan in the viewer's image preview
|
|
15
|
+
- Move the todo list into a session bar popover (#619)
|
|
16
|
+
- Add opt-in per-task feedback countdowns (#633 - @no7dw)
|
|
17
|
+
- Name a session automatically from its first message
|
|
18
|
+
- Show the active session title in the top header
|
|
19
|
+
- Show the QQ bind QR inline with expiry refresh and a sticky success state (#621)
|
|
20
|
+
- Add the oc-grok-4.7, abs-claude-haiku-5-5 and or-gemini-4-argon aliases
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
- Lead model picker rows with their provider
|
|
24
|
+
- Retune the model series discounts and say where they come from
|
|
25
|
+
- Point the openclacky lite tier at claude-haiku-5-5
|
|
26
|
+
- Soften the segmented control hover
|
|
27
|
+
- Tidy the top-up label and the model row spacing
|
|
28
|
+
- Put the model remark under the API key and align it left
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Pair replayed tool results with their own calls so a refreshed session no longer strands them in the wrong tool item (#640)
|
|
32
|
+
- Keep the assistant timestamp when a session is archived and replayed from chunk history (#639 - @BoyYangzai)
|
|
33
|
+
- Stop dropping attachment messages while a task is being processed (#628)
|
|
34
|
+
- Handle a missing workspace path in the directory picker (#623)
|
|
35
|
+
- Restore the settings tab after switching sessions (#626)
|
|
36
|
+
- Send model picker actions to the right settings tab (#624)
|
|
37
|
+
- Stop session action buttons from squeezing the session title (#625)
|
|
38
|
+
- Improve the mobile billing chart usability (#627)
|
|
39
|
+
- Stop truncating the navigator preview too early (#632)
|
|
40
|
+
- Refresh the Files tree after restoring a task from the timeline (#638)
|
|
41
|
+
- Checkpoint the latest task after a session reload (#637)
|
|
42
|
+
- Localize screenshot upload errors and retry after binding a device (#636)
|
|
43
|
+
- Persist run errors in the session history
|
|
44
|
+
- Stop pinning an auto-routed task to a dead lane when a failure carries no tier
|
|
45
|
+
- Reword network failures and link them to the key console
|
|
46
|
+
- Align the sidebar toggle buttons and unify their icons at 16px (#622)
|
|
47
|
+
- Fade the collapsed aside opener icon
|
|
48
|
+
|
|
49
|
+
### More
|
|
50
|
+
- Retire gemini-3.7-flash and drop the oc-minimax-m2.7 alias from the model picker
|
|
51
|
+
- Remove the OrcaRouter and Requesty provider presets
|
|
52
|
+
|
|
53
|
+
## [1.5.19] - 2026-10-10
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Add the built-in QQ official-bot channel (#615 - @chengoak)
|
|
57
|
+
- Drag a session onto a project to move it (#610)
|
|
58
|
+
- Add bookmarks to the chat navigator and tighten its layout (#617)
|
|
59
|
+
- Scroll an overflowing session title on hover so it can be read in full (#618)
|
|
60
|
+
- Add session lifecycle hooks (#607)
|
|
61
|
+
- Rework the billing charts with a year heatmap and a 365-day range
|
|
62
|
+
- Add an "Add Model" button to the model picker
|
|
63
|
+
- Show an API Key hint in the model dialog and restyle the key link
|
|
64
|
+
- Add oc-glm-5.3 and oc-kimi-k3 aliases with pricing and capability caps
|
|
65
|
+
- Add abs-claude-sonnet-5-5 and abs-gpt-6.1-sol with corrected pricing
|
|
66
|
+
- Add Xiaomi MiMo V2.6 models (#614)
|
|
67
|
+
- Add the oc-minimax-m2.7 alias with its launch discount
|
|
68
|
+
- Show a GLM/Kimi series promotion with a badge in the model picker
|
|
69
|
+
- Forward Seedance reference images over the OpenAI-compatible video gateway (#616 - @codevoyager1984)
|
|
70
|
+
- Document the input image limits and the content_filter retry in the media skill (#593 - @codevoyager1984)
|
|
71
|
+
|
|
72
|
+
### Improved
|
|
73
|
+
- Make the model picker visually distinct from the variant switcher
|
|
74
|
+
- Explain model price discounts with a tooltip
|
|
75
|
+
- Soften the chat navigator tick color
|
|
76
|
+
- Hide the skills tab strip when only one tab is available
|
|
77
|
+
- Tune the billing chart axis labels
|
|
78
|
+
- Clarify ambient audio generation in the media skill (#606 - @YeEmrick)
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- Stop the workspace file tree from flickering on reload
|
|
82
|
+
- Deliver web-originated replies to IM after a finished progress card (#603, #605 - @chengoak)
|
|
83
|
+
- Fall back to gem update when the OSS CDN fails (#583, #608 - @theluckystrike)
|
|
84
|
+
- Keep the chat position when toggling preview fullscreen (#613)
|
|
85
|
+
- Stop opening an empty variant panel for custom models
|
|
86
|
+
- Use a white background for the context bar popovers
|
|
87
|
+
- Point a rate limit error at switching models
|
|
88
|
+
- Skip reasoning fields for non-Anthropic Converse models
|
|
89
|
+
|
|
90
|
+
### More
|
|
91
|
+
- Rename the billing panel title to Usage
|
|
92
|
+
- Use 服务商 instead of 供应商 in model settings
|
|
93
|
+
|
|
7
94
|
## [1.5.18] - 2026-10-02
|
|
8
95
|
|
|
9
96
|
### Fixed
|
data/README.md
CHANGED
|
@@ -205,7 +205,7 @@ $ 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
209
|
|
|
210
210
|
## Coding use case
|
|
211
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
|
|
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
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Agent task lifecycle hooks
|
|
2
|
+
|
|
3
|
+
## Task lifecycle: on_start / on_complete
|
|
4
|
+
|
|
5
|
+
> Availability: the additional context and abnormal completion paths require
|
|
6
|
+
> the runtime change on branch `session-lifecycle-hooks`. They are not
|
|
7
|
+
> available in 1.5.16. Upgrade the runtime before relying on these fields.
|
|
8
|
+
|
|
9
|
+
Use the existing events without extra configuration. `on_start` runs after task
|
|
10
|
+
and history initialization, before execution, and retains `deny` / `handled`.
|
|
11
|
+
`on_complete` observes the outcome of each Agent turn, including normal completion,
|
|
12
|
+
waiting, handled starts, denial, exceptions and interruption. Its return value is
|
|
13
|
+
ignored, including `deny` and `handled`; all completion callbacks can run.
|
|
14
|
+
|
|
15
|
+
Ruby callbacks receive `(input, agent, context)` or `(result, agent, context)`.
|
|
16
|
+
The first two positions are unchanged; ordinary two-argument Ruby blocks ignore
|
|
17
|
+
the extra argument. Existing completion callbacks must check `result[:status] ==
|
|
18
|
+
:success` before doing success-only work. A handled result has `context[:handled]`
|
|
19
|
+
set to true; skip follow-up work already performed by the handler.
|
|
20
|
+
|
|
21
|
+
These are Agent-turn events for server, CLI and channel execution, not server
|
|
22
|
+
persistence notifications. Queued input emits start only when consumed; steering
|
|
23
|
+
inside an active turn does not create an extra start or completion. Automatic goal
|
|
24
|
+
continuations have their own turn events. Normal completion fires before goal
|
|
25
|
+
continuation, and exceptions still propagate after notification. A superseded
|
|
26
|
+
turn's exception cleanup cannot settle its successor.
|
|
27
|
+
|
|
28
|
+
Context contains `session_id`, `run_id` (process-local task ID, not durable),
|
|
29
|
+
`source` (execution source, not authenticated identity), `input_created_at` (the
|
|
30
|
+
optional timestamp passed to `run`), `observed_at` (UTC ISO8601), and `status`.
|
|
31
|
+
Start status is `running`. No input text is added to context; input and result
|
|
32
|
+
arguments can contain sensitive data.
|
|
33
|
+
|
|
34
|
+
| Completion context status | Meaning |
|
|
35
|
+
|---------------------------|---------|
|
|
36
|
+
| `completed` | Result explicitly has `status: :success`, without a feedback request. This does not certify App generation or validation. |
|
|
37
|
+
| `awaiting_user` | Successful result requests `awaiting_user_feedback`. |
|
|
38
|
+
| `failed` | Exception or non-success result. Structured exceptions can supply `error_code`. |
|
|
39
|
+
| `cancelled` | Interruption or denial. `reason` may be `user`, `replacement`, `hook_denied` or `tool_denied`. |
|
|
40
|
+
|
|
41
|
+
Cancelled completion notifications carry `result[:status] == :cancelled`, even
|
|
42
|
+
when the original denial return value remains unchanged for queue compatibility.
|
|
43
|
+
An execution start alone does not establish recovery from a previous failure.
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
id: session-observer
|
|
47
|
+
name: Session Observer
|
|
48
|
+
version: "0.1.0"
|
|
49
|
+
contributes:
|
|
50
|
+
hooks:
|
|
51
|
+
- event: on_start
|
|
52
|
+
file: hooks/start.rb
|
|
53
|
+
- event: on_complete
|
|
54
|
+
file: hooks/complete.rb
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# hooks/start.rb
|
|
59
|
+
Clacky::ExtensionHookRegistry.add do |_input, _agent, context|
|
|
60
|
+
next unless context
|
|
61
|
+
Clacky::Logger.debug("[session-observer] started", session_id: context[:session_id],
|
|
62
|
+
run_id: context[:run_id])
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# hooks/complete.rb
|
|
69
|
+
Clacky::ExtensionHookRegistry.add do |_result, _agent, context|
|
|
70
|
+
next unless context
|
|
71
|
+
Clacky::Logger.debug("[session-observer] settled", session_id: context[:session_id],
|
|
72
|
+
status: context[:status], run_id: context[:run_id])
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use separate files for separate registrations: each Ruby file is required only
|
|
78
|
+
once. Programmatic registration uses `agent.add_hook(:on_complete) { |result,
|
|
79
|
+
agent, context| ... }`. Shell hooks keep their JSON payload shape; neither the
|
|
80
|
+
Agent nor context is serialized, while abnormal completion results are now reported.
|
|
81
|
+
|
|
82
|
+
Callbacks execute synchronously; keep them short and dispatch network work
|
|
83
|
+
asynchronously. `StandardError` is logged without stopping later callbacks.
|
|
84
|
+
The host does not persist, retry, replay or acknowledge hook delivery. Consumers
|
|
85
|
+
must tolerate missing events and cannot infer inactivity solely from missing data.
|
|
86
|
+
`agent.emit_event(persist: true)` is a separate frontend-history feature.
|
|
@@ -41,6 +41,7 @@ module Clacky
|
|
|
41
41
|
nested = []
|
|
42
42
|
section_count = 0
|
|
43
43
|
user_start = nil
|
|
44
|
+
user_task_id = nil
|
|
44
45
|
archived_at = nil
|
|
45
46
|
front_matter = false
|
|
46
47
|
directory = File.dirname(path)
|
|
@@ -61,6 +62,7 @@ module Clacky
|
|
|
61
62
|
section_count += 1
|
|
62
63
|
nested << header[:nested_chunk] if header[:nested_chunk]
|
|
63
64
|
user_start = header[:role] == "user" ? start : nil
|
|
65
|
+
user_task_id = header[:role] == "user" ? header[:task_id] : nil
|
|
64
66
|
elsif user_start && !text.strip.empty?
|
|
65
67
|
# Match replay's empty/metadata-only user handling without
|
|
66
68
|
# retaining the rest of the user message or any assistant text.
|
|
@@ -69,8 +71,9 @@ module Clacky
|
|
|
69
71
|
next if visible.empty? && events.empty? && files.empty?
|
|
70
72
|
|
|
71
73
|
rounds.last[:length] = user_start - rounds.last[:start] if rounds.any?
|
|
72
|
-
rounds << { start: user_start }
|
|
74
|
+
rounds << { start: user_start, task_id: user_task_id }
|
|
73
75
|
user_start = nil
|
|
76
|
+
user_task_id = nil
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
rounds.last[:length] = position - rounds.last[:start] if rounds.any?
|
|
@@ -12,7 +12,12 @@ module Clacky
|
|
|
12
12
|
manifest = sources.map do |source|
|
|
13
13
|
entry = { key: source[:key], version: source[:version], count: navigation_source_count(source),
|
|
14
14
|
volatile: !!source[:rounds] || source[:continuation].to_a.any? }
|
|
15
|
-
|
|
15
|
+
if source[:rounds]
|
|
16
|
+
entry[:identities] = source[:rounds].map { |round| round[:user_msg].values_at(:created_at, :task_id) }
|
|
17
|
+
else
|
|
18
|
+
task_ids = source[:index][:rounds].map { |round| round[:task_id] }
|
|
19
|
+
entry[:task_ids] = task_ids if task_ids.any?
|
|
20
|
+
end
|
|
16
21
|
entry
|
|
17
22
|
end
|
|
18
23
|
{ sources: manifest, total: manifest.sum { |source| source[:count] } }
|
|
@@ -26,20 +26,27 @@ module Clacky
|
|
|
26
26
|
|
|
27
27
|
# @return [Hash] `{action: :allow}`, `{action: :deny, reason:}`, or
|
|
28
28
|
# `{action: :handled, result:}` when a hook fulfilled the event itself.
|
|
29
|
-
# on_start receives (user_input, agent) after task/history initialization.
|
|
29
|
+
# on_start receives (user_input, agent, context) after task/history initialization.
|
|
30
30
|
# Its handled result is returned unchanged from Agent#run; the extension
|
|
31
31
|
# supplies the run-result status and any queue/feedback flags it needs.
|
|
32
32
|
# on_start is not fired for steering input inside an already running turn.
|
|
33
|
-
#
|
|
33
|
+
# The agent follows the event payload, so ext hooks can
|
|
34
34
|
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
35
|
-
# params (`|call|`, `|call, result|`) silently ignore
|
|
35
|
+
# params (`|call|`, `|call, result|`) silently ignore extra arguments.
|
|
36
|
+
# Start/complete append context after the agent. Completion is observation-only.
|
|
36
37
|
def trigger(event, *args)
|
|
37
38
|
validate_event!(event)
|
|
39
|
+
# Keep payload hashes positional: Ruby 2.6 otherwise promotes their keys
|
|
40
|
+
# into keyword arguments, breaking existing tool hooks.
|
|
41
|
+
context = args.pop if [:on_start, :on_complete].include?(event) && args.length == 2
|
|
38
42
|
result = { action: :allow }
|
|
39
43
|
|
|
40
44
|
@hooks[event].each do |hook|
|
|
41
45
|
begin
|
|
42
|
-
|
|
46
|
+
callback_args = [*args, @agent]
|
|
47
|
+
callback_args << context.dup if context
|
|
48
|
+
hook_result = hook.call(*callback_args)
|
|
49
|
+
next if event == :on_complete
|
|
43
50
|
next unless hook_result.is_a?(Hash)
|
|
44
51
|
# First deny wins and stops the chain: a weaker later verdict must
|
|
45
52
|
# never clobber a stronger earlier one, and the first deny's reason
|
|
@@ -56,8 +56,15 @@ module Clacky
|
|
|
56
56
|
retries = 0
|
|
57
57
|
# Per-call tier attribution for auto routing: each retryable failure is
|
|
58
58
|
# charged to the tier the gateway actually routed that call to (echoed
|
|
59
|
-
# in X-Clacky-Routed-Tier).
|
|
60
|
-
#
|
|
59
|
+
# in X-Clacky-Routed-Tier). A tier-less failure is charged to the floor
|
|
60
|
+
# when the call went out as "auto": the gateway only stamps the tier
|
|
61
|
+
# header after it has routed the call, so a missing tier means the
|
|
62
|
+
# failure happened before routing (HTML error page, truncated body,
|
|
63
|
+
# rejection at the auth layer) — and the gateway defaults to the floor
|
|
64
|
+
# lane in exactly that case. Leaving it uncharged would keep
|
|
65
|
+
# agent_upstream_fails at 0 and pin the task to a dead floor lane until
|
|
66
|
+
# its retry budget ran out. Non-auto calls and unknown tier values are
|
|
67
|
+
# attributed to neither lane.
|
|
61
68
|
floor_fails = 0
|
|
62
69
|
upgrade_fails = 0
|
|
63
70
|
|
|
@@ -247,7 +254,7 @@ module Clacky
|
|
|
247
254
|
else
|
|
248
255
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
249
256
|
# 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)
|
|
257
|
+
raise AgentError.new("#{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message, action_url: @client.account_url)
|
|
251
258
|
end
|
|
252
259
|
|
|
253
260
|
rescue RetryableError => e
|
|
@@ -257,6 +264,12 @@ module Clacky
|
|
|
257
264
|
case e.routed_tier
|
|
258
265
|
when "floor" then floor_fails += 1
|
|
259
266
|
when "upgrade" then upgrade_fails += 1
|
|
267
|
+
when nil then floor_fails += 1 if api_call_model == "auto"
|
|
268
|
+
else
|
|
269
|
+
if defined?(Clacky::Logger)
|
|
270
|
+
Clacky::Logger.warn("llm_caller.unknown_routed_tier",
|
|
271
|
+
tier: e.routed_tier, model: api_call_model)
|
|
272
|
+
end
|
|
260
273
|
end
|
|
261
274
|
|
|
262
275
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -641,6 +641,7 @@ module Clacky
|
|
|
641
641
|
lines << "## Assistant"
|
|
642
642
|
end
|
|
643
643
|
lines << ""
|
|
644
|
+
append_created_at_line(lines, msg)
|
|
644
645
|
# Include tool calls summary if present
|
|
645
646
|
# Format: "_Tool calls: name | {args_json}_" so replay can restore args for WebUI display.
|
|
646
647
|
if msg[:tool_calls]&.any?
|
|
@@ -733,6 +734,13 @@ module Clacky
|
|
|
733
734
|
lines << ""
|
|
734
735
|
end
|
|
735
736
|
|
|
737
|
+
def append_created_at_line(lines, msg)
|
|
738
|
+
return unless msg[:created_at].is_a?(Numeric)
|
|
739
|
+
|
|
740
|
+
lines << "_Created at: #{msg[:created_at]}_"
|
|
741
|
+
lines << ""
|
|
742
|
+
end
|
|
743
|
+
|
|
736
744
|
# Serialize persisted extension events into the chunk MD so they survive
|
|
737
745
|
# compression and can be replayed in place. Mirrors the "_Tool calls:"
|
|
738
746
|
# 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")
|
|
@@ -75,6 +76,9 @@ module Clacky
|
|
|
75
76
|
@task_parents = raw_parents.each_with_object({}) { |(k, v), h| h[k.to_s.to_i] = v.to_i }
|
|
76
77
|
@current_task_id = (session_data.dig(:time_machine, :current_task_id) || 0).to_i
|
|
77
78
|
@active_task_id = (session_data.dig(:time_machine, :active_task_id) || 0).to_i
|
|
79
|
+
# Not persisted: assume the latest task's AFTER may be stale so the next
|
|
80
|
+
# task start re-checkpoints it instead of skipping it.
|
|
81
|
+
@latest_after_dirty = true
|
|
78
82
|
|
|
79
83
|
raw_meta = session_data.dig(:time_machine, :task_meta) || {}
|
|
80
84
|
@task_meta = raw_meta.each_with_object({}) do |(k, v), h|
|
|
@@ -99,7 +103,20 @@ module Clacky
|
|
|
99
103
|
last_error = session_data.dig(:stats, :last_error)
|
|
100
104
|
|
|
101
105
|
if last_status == "error" && last_error
|
|
102
|
-
|
|
106
|
+
real_users = @history.real_user_messages
|
|
107
|
+
task_tagged_history = real_users.any? { |message| !message[:task_id].nil? }
|
|
108
|
+
failed_turn_persisted = real_users.any? do |message|
|
|
109
|
+
message[:task_id].to_s == @current_task_id.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Modern sessions identify each user turn by task_id. Only schedule
|
|
113
|
+
# the legacy deferred rollback when the failed task's user message
|
|
114
|
+
# actually survived in history. Attachment setup can now roll its
|
|
115
|
+
# staged turn back before the error snapshot is saved; blindly
|
|
116
|
+
# removing the last user message after restore would then delete the
|
|
117
|
+
# preceding successful turn. Sessions without task ids keep the old
|
|
118
|
+
# fallback for backward compatibility.
|
|
119
|
+
@pending_error_rollback = true if !task_tagged_history || failed_turn_persisted
|
|
103
120
|
end
|
|
104
121
|
|
|
105
122
|
saved_reasoning = session_data.dig(:config, :reasoning_effort)
|
|
@@ -244,6 +261,9 @@ module Clacky
|
|
|
244
261
|
session_id: @session_id,
|
|
245
262
|
name: @name,
|
|
246
263
|
pinned: @pinned,
|
|
264
|
+
# Kept next to the other scalars so SessionManager's header-only
|
|
265
|
+
# reader can pick it up without parsing the message payload.
|
|
266
|
+
archived_at: @archived_at,
|
|
247
267
|
created_at: @created_at,
|
|
248
268
|
updated_at: stamp,
|
|
249
269
|
working_dir: @working_dir,
|
|
@@ -543,12 +563,15 @@ module Clacky
|
|
|
543
563
|
# Old: "_Tool calls: name1, name2_" (backward compat)
|
|
544
564
|
remaining_lines = []
|
|
545
565
|
pending_tool_entries = [] # [{name:, args:}]
|
|
566
|
+
created_at = nil
|
|
546
567
|
|
|
547
568
|
text.each_line do |line|
|
|
548
569
|
stripped = line.strip
|
|
549
570
|
if (m = stripped.match(/\A_Tool calls?:\s*(.+?)_?\z/i))
|
|
550
571
|
raw = m[1]
|
|
551
572
|
pending_tool_entries.concat(parse_tool_calls_line(raw))
|
|
573
|
+
elsif (m = stripped.match(/\A_Created at: (\d+(?:\.\d+)?)_\z/))
|
|
574
|
+
created_at = m[1].to_f
|
|
552
575
|
else
|
|
553
576
|
remaining_lines << line
|
|
554
577
|
end
|
|
@@ -557,7 +580,7 @@ module Clacky
|
|
|
557
580
|
# Flush any plain text
|
|
558
581
|
plain_text = remaining_lines.join.strip
|
|
559
582
|
unless plain_text.empty?
|
|
560
|
-
current_round[:events] << { role: "assistant", content: plain_text, interim: !pending_tool_entries.empty
|
|
583
|
+
current_round[:events] << { role: "assistant", content: plain_text, interim: !pending_tool_entries.empty?, created_at: created_at }
|
|
561
584
|
end
|
|
562
585
|
|
|
563
586
|
# Emit one synthetic tool_calls message per detected tool
|