openclacky 0.9.38 → 1.0.0.beta.2
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 +67 -13
- data/CHANGELOG.md +40 -0
- data/lib/clacky/agent/llm_caller.rb +48 -2
- data/lib/clacky/agent/memory_updater.rb +131 -35
- data/lib/clacky/agent/message_compressor.rb +30 -3
- data/lib/clacky/agent/message_compressor_helper.rb +53 -19
- data/lib/clacky/agent/time_machine.rb +12 -3
- data/lib/clacky/agent/tool_executor.rb +0 -3
- data/lib/clacky/agent.rb +190 -61
- data/lib/clacky/agent_config.rb +201 -47
- data/lib/clacky/brand_config.rb +77 -5
- data/lib/clacky/cli.rb +101 -45
- data/lib/clacky/message_format/bedrock.rb +4 -0
- data/lib/clacky/message_history.rb +79 -4
- data/lib/clacky/platform_http_client.rb +7 -7
- data/lib/clacky/providers.rb +170 -8
- data/lib/clacky/server/http_server.rb +138 -21
- data/lib/clacky/telemetry.rb +111 -0
- data/lib/clacky/tools/terminal.rb +27 -0
- data/lib/clacky/tools/todo_manager.rb +11 -2
- data/lib/clacky/ui2/layout_manager.rb +22 -1
- data/lib/clacky/ui2/progress_handle.rb +291 -0
- data/lib/clacky/ui2/ui_controller.rb +261 -185
- data/lib/clacky/ui_interface.rb +69 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +53 -0
- data/lib/clacky/web/app.js +1 -1
- data/lib/clacky/web/brand.js +112 -1
- data/lib/clacky/web/i18n.js +24 -16
- data/lib/clacky/web/index.html +15 -2
- data/lib/clacky/web/sessions.js +23 -6
- data/lib/clacky/web/settings.js +34 -0
- data/lib/clacky/web/ws.js +3 -2
- data/lib/clacky.rb +1 -0
- data/scripts/install.ps1 +20 -5
- metadata +3 -2
- data/lib/clacky/ui2/README.md +0 -214
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6a51d00de51f04f142d6be7e3e726561384752ca02f44a6971eeaad9c2cdb28
|
|
4
|
+
data.tar.gz: 185e635750793082206332377649095313b62297a2bd2f9f0b825a523a499afb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66e14242f2d4b0e049fd45283c77c71919614d5f2ef8558b8635318f443afd41ab3ebf941a3b10a22c987805569082f699683af5eae23f5ef54e6dbd993e4931
|
|
7
|
+
data.tar.gz: 4734d321c296e168f505185e67bddf8967687fd4889871a9a148f8bc003a7bb866774eb4f8c15ab4aba8b27384ae28e1c436477b3dd65e898c90743b393dbfb9
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
name: gem-release
|
|
4
4
|
description: >-
|
|
5
5
|
Automates the complete process of releasing a new version of the openclacky Ruby
|
|
6
|
-
gem
|
|
6
|
+
gem. Supports both stable releases (auto-increment) and pre-release versions
|
|
7
|
+
(user-specified, e.g., 1.0.0.beta.1). Handles version bumping, testing, building,
|
|
8
|
+
RubyGems publishing, GitHub Releases, and OSS CDN mirroring.
|
|
7
9
|
disable-model-invocation: false
|
|
8
10
|
user-invocable: true
|
|
9
11
|
---
|
|
@@ -21,6 +23,7 @@ This skill handles the entire gem release workflow from version bumping to publi
|
|
|
21
23
|
To use this skill, simply say:
|
|
22
24
|
- "Release a new version"
|
|
23
25
|
- "Publish a new gem version"
|
|
26
|
+
- "Release version 1.0.0.beta.1" (pre-release with explicit version)
|
|
24
27
|
- Use the command: `/gem-release`
|
|
25
28
|
|
|
26
29
|
## Process Steps
|
|
@@ -31,10 +34,30 @@ To use this skill, simply say:
|
|
|
31
34
|
- Ensure the repository is in a clean state
|
|
32
35
|
|
|
33
36
|
### 2. Version Management
|
|
37
|
+
|
|
38
|
+
**Stable releases (default):**
|
|
34
39
|
- Read current version from `lib/clacky/version.rb`
|
|
35
40
|
- Increment version number (typically patch version: x.y.z → x.y.z+1)
|
|
36
41
|
- Update the VERSION constant in the version file
|
|
37
42
|
|
|
43
|
+
**Pre-release versions (when user specifies a version like `1.0.0.beta.1`):**
|
|
44
|
+
- Accept the user-provided version string directly — do NOT auto-increment
|
|
45
|
+
- The version must follow semver pre-release format: `X.Y.Z-<identifier>` or `X.Y.Z.<identifier>` (e.g., `1.0.0.beta.1`, `2.0.0-alpha`, `1.5.0-rc1`)
|
|
46
|
+
- Before proceeding, warn the user about pre-release caveats (see Pre-Release Caveats below)
|
|
47
|
+
|
|
48
|
+
### 2a. Pre-Release Caveats
|
|
49
|
+
|
|
50
|
+
When releasing a pre-release version, inform the user of these known behaviors in the Clacky ecosystem:
|
|
51
|
+
|
|
52
|
+
| Concern | Behavior | Impact |
|
|
53
|
+
|---------|----------|--------|
|
|
54
|
+
| **Version check notification** | RubyGems API returns the highest version number, including prereleases. `Gem::Version("0.9.38") < Gem::Version("1.0.0.beta.1")` → `true`. | ✅ The upgrade dot WILL appear in the Web UI for most users. |
|
|
55
|
+
| **`gem update` (official source)** | `gem update openclacky --no-document` does NOT install prereleases without `--pre`. | ❌ Users on official RubyGems source who click "Upgrade" will see the notification but the upgrade will silently do nothing. |
|
|
56
|
+
| **OSS CDN upgrade (mirror users)** | `upgrade_via_oss_cdn` downloads the exact `.gem` from `latest.txt` on OSS. | ⚠️ If you update `latest.txt` to point to the prerelease, mirror users WILL get the beta. |
|
|
57
|
+
| **OSS `latest.txt`** | Stable users fetching `latest.txt` for fresh installs would get the beta. | ⚠️ By default, do NOT update `latest.txt` for pre-releases. Only update if this is intentional (e.g., a release candidate for broad testing). |
|
|
58
|
+
|
|
59
|
+
**Action**: Ask the user whether to update `latest.txt` on OSS before proceeding. For internal testing, the answer is usually "no".
|
|
60
|
+
|
|
38
61
|
### 3. Quality Assurance
|
|
39
62
|
- Run the full test suite with `bundle exec rspec`
|
|
40
63
|
- Ensure all 167+ tests pass
|
|
@@ -93,15 +116,26 @@ To use this skill, simply say:
|
|
|
93
116
|
|
|
94
117
|
4. **Create GitHub Release and Upload gem**
|
|
95
118
|
|
|
96
|
-
Extract the release notes for this version from CHANGELOG.md, then create a GitHub Release with the .gem file attached
|
|
119
|
+
Extract the release notes for this version from CHANGELOG.md, then create a GitHub Release with the .gem file attached.
|
|
120
|
+
|
|
121
|
+
**For stable releases:**
|
|
97
122
|
```bash
|
|
98
123
|
gh release create v{version} \
|
|
99
124
|
--title "v{version}" \
|
|
100
|
-
--notes-file /tmp/
|
|
125
|
+
--notes-file /tmp/release_notes_{version}.md \
|
|
101
126
|
--latest \
|
|
102
127
|
openclacky-{version}.gem
|
|
103
128
|
```
|
|
104
129
|
|
|
130
|
+
**For pre-release versions (e.g., `1.0.0.beta.1`):** use `--prerelease` instead of `--latest`:
|
|
131
|
+
```bash
|
|
132
|
+
gh release create v{version} \
|
|
133
|
+
--title "v{version}" \
|
|
134
|
+
--notes-file /tmp/release_notes_{version}.md \
|
|
135
|
+
--prerelease \
|
|
136
|
+
openclacky-{version}.gem
|
|
137
|
+
```
|
|
138
|
+
|
|
105
139
|
Steps:
|
|
106
140
|
- Parse the CHANGELOG.md section for `[{version}]`
|
|
107
141
|
- Write it to a temp file (e.g., `/tmp/release_notes_{version}.md`) to avoid shell escaping issues
|
|
@@ -112,22 +146,28 @@ To use this skill, simply say:
|
|
|
112
146
|
|
|
113
147
|
5. **Sync to Tencent Cloud OSS (CN mirror)**
|
|
114
148
|
|
|
115
|
-
After GitHub Release is created, upload the .gem file
|
|
149
|
+
After GitHub Release is created, upload the .gem file to OSS so Chinese users can install without hitting GitHub directly.
|
|
116
150
|
|
|
117
151
|
```bash
|
|
118
|
-
# Upload .gem file
|
|
152
|
+
# Upload .gem file (always do this for any release)
|
|
119
153
|
coscli cp openclacky-{version}.gem cos://clackyai-1258723534/openclacky/openclacky-{version}.gem
|
|
154
|
+
```
|
|
120
155
|
|
|
121
|
-
|
|
156
|
+
**For stable releases only** — update `latest.txt` so fresh installs and mirror users pick up the new version:
|
|
157
|
+
```bash
|
|
122
158
|
echo "{version}" > /tmp/latest.txt
|
|
123
159
|
coscli cp /tmp/latest.txt cos://clackyai-1258723534/openclacky/latest.txt
|
|
124
160
|
|
|
125
161
|
# Verify
|
|
126
162
|
curl -fsSL https://oss.1024code.com/openclacky/latest.txt
|
|
127
163
|
```
|
|
128
|
-
|
|
129
164
|
Expected output of verify: `{version}`
|
|
130
165
|
|
|
166
|
+
**For pre-release versions** — do NOT update `latest.txt` unless the user explicitly requested it. Updating `latest.txt` to a prerelease would cause:
|
|
167
|
+
- Mirror users clicking "Upgrade" to get the beta via `upgrade_via_oss_cdn`
|
|
168
|
+
- Fresh installs via the install script to get the beta
|
|
169
|
+
- Only skip this if the user explicitly wants broad beta distribution
|
|
170
|
+
|
|
131
171
|
> **Prerequisite**: `coscli` installed at `/usr/local/bin/coscli` and configured at `~/.cos.yaml`
|
|
132
172
|
|
|
133
173
|
6. **Sync scripts/ to OSS**
|
|
@@ -325,22 +365,34 @@ git tag vX.Y.Z
|
|
|
325
365
|
git push origin main
|
|
326
366
|
git push origin --tags
|
|
327
367
|
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
#
|
|
331
|
-
# 3. Create the release and attach .gem file
|
|
368
|
+
# ── GitHub Release ──────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
# Stable release:
|
|
332
371
|
gh release create vX.Y.Z \
|
|
333
372
|
--title "vX.Y.Z" \
|
|
334
373
|
--notes-file /tmp/release_notes_X.Y.Z.md \
|
|
335
374
|
--latest \
|
|
336
375
|
openclacky-X.Y.Z.gem
|
|
337
376
|
|
|
338
|
-
#
|
|
377
|
+
# Pre-release (use --prerelease instead of --latest):
|
|
378
|
+
gh release create vX.Y.Z-beta.1 \
|
|
379
|
+
--title "vX.Y.Z-beta.1" \
|
|
380
|
+
--notes-file /tmp/release_notes_X.Y.Z-beta.1.md \
|
|
381
|
+
--prerelease \
|
|
382
|
+
openclacky-X.Y.Z.beta.1.gem
|
|
383
|
+
|
|
384
|
+
# ── OSS CDN (CN mirror) ─────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
# Always upload the .gem file:
|
|
339
387
|
coscli cp openclacky-X.Y.Z.gem cos://clackyai-1258723534/openclacky/openclacky-X.Y.Z.gem
|
|
388
|
+
|
|
389
|
+
# Stable releases ONLY — update latest.txt:
|
|
340
390
|
echo "X.Y.Z" > /tmp/latest.txt
|
|
341
391
|
coscli cp /tmp/latest.txt cos://clackyai-1258723534/openclacky/latest.txt
|
|
342
392
|
curl -fsSL https://oss.1024code.com/openclacky/latest.txt # verify
|
|
343
393
|
|
|
394
|
+
# Pre-releases — skip latest.txt update unless user explicitly requests it
|
|
395
|
+
|
|
344
396
|
# Sync scripts/ to OSS (build from templates first)
|
|
345
397
|
bash scripts/build/build.sh
|
|
346
398
|
for script in scripts/*; do
|
|
@@ -365,8 +417,10 @@ curl -fsSL https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.sh
|
|
|
365
417
|
- Git repository updated with version tag
|
|
366
418
|
- CHANGELOG.md updated with release notes
|
|
367
419
|
- GitHub Release created with .gem file attached at https://github.com/clacky-ai/openclacky/releases
|
|
420
|
+
- Use `--latest` for stable releases, `--prerelease` for pre-releases
|
|
368
421
|
- .gem file uploaded to OSS: https://oss.1024code.com/openclacky/openclacky-{version}.gem
|
|
369
|
-
- latest.txt updated on OSS: https://oss.1024code.com/openclacky/latest.txt returns the new version
|
|
422
|
+
- For stable releases: `latest.txt` updated on OSS: https://oss.1024code.com/openclacky/latest.txt returns the new version
|
|
423
|
+
- For pre-releases: `latest.txt` NOT updated (unless user explicitly opts in)
|
|
370
424
|
- No build or deployment errors
|
|
371
425
|
- User-facing release summary presented at the end
|
|
372
426
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.0.beta.2] - 2026-04-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **New session creation supports model & working-directory options.** The Web UI "new session" dialog now lets you pick the model and starting directory up front, instead of having to adjust them after the session opens.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **System prompt now refreshes when you switch models.** Previously the system prompt captured at session start stuck around even after `/model` or `/provider` switches, which could leave model-specific instructions out of sync. The agent now re-injects the correct system prompt on every model change.
|
|
17
|
+
- **Port 7070 properly released when the terminal tool exits.** A lingering listener on port 7070 could block subsequent runs; the terminal tool now cleans it up on shutdown.
|
|
18
|
+
- **Windows installer uses `[IO.Path]::GetTempPath()` for the temp directory** (#58) — more reliable than `$env:TEMP` on systems where the env var is unset or points to a non-ASCII path.
|
|
19
|
+
|
|
20
|
+
## [1.0.0.beta.1] - 2026-04-26
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Vision support — agents can now "see" images.** When you attach image files (PNG, JPG, GIF, WebP), the agent can analyze them visually with vision-capable models. Non-vision models automatically fall back to disk references instead of breaking.
|
|
24
|
+
- **DeepSeek V4 (Clacky-DS) provider.** New `deepseekv4` provider preset with native DeepSeek API endpoint, supporting `deepseek-v4-pro` and `deepseek-v4-flash` models with accurate pricing.
|
|
25
|
+
- **Memory subagent.** Long-term memory management now runs as a dedicated background subagent — writes memories when the task reaches meaningful completion, instead of on every turn.
|
|
26
|
+
- **Usage telemetry.** Anonymous usage data collection helps us understand how the product is used and prioritize improvements. No personal or conversation data is collected.
|
|
27
|
+
- **Brand configuration auto-refresh.** White-label brand settings now refresh automatically when the WebUI starts up, no manual restart needed.
|
|
28
|
+
|
|
29
|
+
### Improved
|
|
30
|
+
- **Progress handles revamped.** Nested progress handles now hide/show automatically, ticker threads keep animations smooth, and fast-completing tasks no longer flash a pointless "done" message.
|
|
31
|
+
- **Todo manager tool upgrades.** Batch add/remove multiple todos at once, and completed todos auto-clear when you add new ones.
|
|
32
|
+
- **Model switching more robust.** CLI slash commands (`/model`, `/provider`) now work seamlessly, server-side routing handles dynamic endpoints correctly, and switching between all provider types is more reliable.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **Access key now persists via cookies.** The WebUI login key was stored only in `localStorage`, causing WebSocket connections to lose authentication. Now also written to a `clacky_access_key` cookie for consistent auth across all connection types.
|
|
36
|
+
- **MiniMax → DeepSeek switch error.** Switching models from MiniMax to DeepSeek no longer fails due to mismatched message format handling.
|
|
37
|
+
- **Bedrock truncated tool call recovery.** When AWS Bedrock truncates a tool call mid-argument, the agent now detects the error, sends feedback, and successfully retries on the next turn.
|
|
38
|
+
- **Sidebar "Load More" scroll jump.** Clicking "Load More" at the bottom of the session list no longer jerks the sidebar back to the active session — scroll position is now preserved.
|
|
39
|
+
- **Double-render regression.** An output buffer lifecycle bug that occasionally caused duplicate content in the terminal UI has been fixed.
|
|
40
|
+
- **DeepSeek V4 message content extraction.** Compression no longer mishandles DeepSeek V4's user message content format.
|
|
41
|
+
|
|
42
|
+
## [0.9.38] - 2026-04-24
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- **Access key now persists correctly via cookie**. When the Web UI server was configured with `--access-key`, the key entered at login was stored only in `localStorage` — but WebSocket connections and some API requests read the key from cookies. This mismatch caused authenticated sessions to sporadically lose access (e.g. WebSocket falling back to unauthorized). The auth flow now writes the key to both `localStorage` _and_ a `clacky_access_key` cookie, and probes the server using the cookie. Incorrect keys are cleared from both stores before retry. Up to 3 attempts are allowed before giving up.
|
|
46
|
+
|
|
47
|
+
### More
|
|
48
|
+
- Auth prompt input field now uses `type="password"` while the user is typing (reverts to text after), preventing shoulder-surfing
|
|
49
|
+
|
|
10
50
|
## [0.9.37] - 2026-04-24
|
|
11
51
|
|
|
12
52
|
### Fixed
|
|
@@ -54,14 +54,25 @@ module Clacky
|
|
|
54
54
|
max_retries = 10
|
|
55
55
|
retry_delay = 5
|
|
56
56
|
retries = 0
|
|
57
|
+
# One-shot flag set by the BadRequestError rescue below when the server
|
|
58
|
+
# complained about missing reasoning_content. The subsequent retry will
|
|
59
|
+
# pad every assistant message's reasoning_content, which satisfies
|
|
60
|
+
# DeepSeek / Kimi thinking-mode providers even when the earlier turns
|
|
61
|
+
# were produced by a different provider (e.g. MiniMax keeps thinking
|
|
62
|
+
# inline in content and never emits a reasoning_content field, so the
|
|
63
|
+
# history-evidence heuristic in MessageHistory can't infer thinking
|
|
64
|
+
# mode on its own). We retry at most once — if padding doesn't fix it,
|
|
65
|
+
# the error is something else and we let it propagate.
|
|
66
|
+
force_reasoning_content_pad = false
|
|
67
|
+
thinking_retry_attempted = false
|
|
57
68
|
|
|
58
69
|
begin
|
|
59
70
|
# Use active_messages (Time Machine) when undone, otherwise send full history.
|
|
60
71
|
# to_api strips internal fields and handles orphaned tool_calls.
|
|
61
72
|
messages_to_send = if respond_to?(:active_messages)
|
|
62
|
-
active_messages
|
|
73
|
+
active_messages(force_reasoning_content_pad: force_reasoning_content_pad)
|
|
63
74
|
else
|
|
64
|
-
@history.to_api
|
|
75
|
+
@history.to_api(force_reasoning_content_pad: force_reasoning_content_pad)
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
response = @client.send_messages_with_tools(
|
|
@@ -137,6 +148,25 @@ module Clacky
|
|
|
137
148
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
138
149
|
raise AgentError, "[LLM] Service unavailable after #{current_max} retries"
|
|
139
150
|
end
|
|
151
|
+
|
|
152
|
+
rescue Clacky::BadRequestError => e
|
|
153
|
+
# One-shot recovery for thinking-mode providers (DeepSeek V4, Kimi K2)
|
|
154
|
+
# that require every assistant message in the history to carry a
|
|
155
|
+
# reasoning_content field. The history-evidence heuristic in
|
|
156
|
+
# MessageHistory#to_api can miss this when the preceding turns came
|
|
157
|
+
# from a different thinking style (e.g. MiniMax keeps <think>...</think>
|
|
158
|
+
# inline in content and never emits reasoning_content) — so we detect
|
|
159
|
+
# the error here and retry once with forced padding.
|
|
160
|
+
if !thinking_retry_attempted && reasoning_content_missing_error?(e)
|
|
161
|
+
thinking_retry_attempted = true
|
|
162
|
+
force_reasoning_content_pad = true
|
|
163
|
+
Clacky::Logger.info(
|
|
164
|
+
"[thinking-mode] retrying with forced reasoning_content padding " \
|
|
165
|
+
"(model=#{@config.model_name.inspect} base_url=#{@config.base_url.inspect})"
|
|
166
|
+
)
|
|
167
|
+
retry
|
|
168
|
+
end
|
|
169
|
+
raise
|
|
140
170
|
end
|
|
141
171
|
|
|
142
172
|
# Track cost and collect token usage data.
|
|
@@ -183,6 +213,22 @@ module Clacky
|
|
|
183
213
|
"Continuing with fallback model: #{fallback}"
|
|
184
214
|
)
|
|
185
215
|
end
|
|
216
|
+
|
|
217
|
+
# True when a 400 BadRequestError is specifically about a missing
|
|
218
|
+
# reasoning_content field in thinking mode (DeepSeek V4, Kimi K2 thinking).
|
|
219
|
+
# We require TWO distinct substrings to avoid false positives — a generic
|
|
220
|
+
# 400 that happens to mention "reasoning_content" in passing (e.g. a
|
|
221
|
+
# validation hint in some unrelated provider) must NOT trigger the pad
|
|
222
|
+
# retry, which would silently add an empty field to every assistant
|
|
223
|
+
# message in the history.
|
|
224
|
+
private def reasoning_content_missing_error?(err)
|
|
225
|
+
return false unless err.is_a?(Clacky::BadRequestError)
|
|
226
|
+
|
|
227
|
+
msg = err.message.to_s.downcase
|
|
228
|
+
msg.include?("reasoning_content") &&
|
|
229
|
+
(msg.include?("thinking") || msg.include?("must be passed back") ||
|
|
230
|
+
msg.include?("must be provided"))
|
|
231
|
+
end
|
|
186
232
|
end
|
|
187
233
|
end
|
|
188
234
|
end
|
|
@@ -2,17 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
module Clacky
|
|
4
4
|
class Agent
|
|
5
|
-
# Long-term memory update functionality
|
|
6
|
-
# Triggered at the end of a session to persist important knowledge.
|
|
5
|
+
# Long-term memory update functionality.
|
|
7
6
|
#
|
|
8
|
-
#
|
|
7
|
+
# Runs at the end of a qualifying task to persist important knowledge
|
|
8
|
+
# into ~/.clacky/memories/. The LLM decides:
|
|
9
9
|
# - Which topics were discussed
|
|
10
10
|
# - Which memory files to update or create
|
|
11
11
|
# - How to merge new info with existing content
|
|
12
12
|
# - What to drop to stay within the per-file token limit
|
|
13
13
|
#
|
|
14
|
+
# Architecture:
|
|
15
|
+
# Memory update runs as a **forked subagent**, NOT inline in the
|
|
16
|
+
# main agent's loop. The subagent inherits the main agent's history
|
|
17
|
+
# (so it can see what happened) via +fork_subagent+'s standard
|
|
18
|
+
# deep-clone, and inherits the same model/tools so prompt-cache is
|
|
19
|
+
# reused maximally. The subagent runs synchronously; when it returns,
|
|
20
|
+
# the main agent prints +show_complete+.
|
|
21
|
+
#
|
|
22
|
+
# This gives us, structurally:
|
|
23
|
+
# - Clean main-agent history (no memory_update messages to clean up)
|
|
24
|
+
# - Correct visual ordering ([OK] Task Complete is the LAST thing
|
|
25
|
+
# printed — the memory-update progress finishes before it)
|
|
26
|
+
# - Independent cost accounting (task cost vs. memory update cost)
|
|
27
|
+
# - Natural recursion guard (+@is_subagent+ blocks re-entry)
|
|
28
|
+
#
|
|
14
29
|
# Trigger condition:
|
|
15
|
-
# - Iteration count >= MEMORY_UPDATE_MIN_ITERATIONS (
|
|
30
|
+
# - Iteration count >= MEMORY_UPDATE_MIN_ITERATIONS (skip trivial tasks)
|
|
31
|
+
# - Not already a subagent (no recursion)
|
|
32
|
+
# - Memory update is enabled in config
|
|
16
33
|
module MemoryUpdater
|
|
17
34
|
# Minimum LLM iterations for this task before triggering memory update.
|
|
18
35
|
# Set high enough to skip short utility tasks (commit, deploy, etc.)
|
|
@@ -32,37 +49,79 @@ module Clacky
|
|
|
32
49
|
task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS
|
|
33
50
|
end
|
|
34
51
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
# Run memory update as a forked subagent.
|
|
53
|
+
#
|
|
54
|
+
# This is called by +Agent#run+ on the success path, AFTER the main
|
|
55
|
+
# loop exits and BEFORE +show_complete+ is printed. It blocks until
|
|
56
|
+
# the subagent finishes, so the visual order is structurally correct:
|
|
57
|
+
#
|
|
58
|
+
# ... task output ...
|
|
59
|
+
# [progress] Updating long-term memory… (spinner)
|
|
60
|
+
# [progress finishes]
|
|
61
|
+
# [OK] Task Complete
|
|
62
|
+
#
|
|
63
|
+
# Safe to call unconditionally; returns early if preconditions fail.
|
|
64
|
+
# Never raises for "no update needed" — only propagates genuine errors
|
|
65
|
+
# (+Clacky::AgentInterrupted+ for Ctrl+C, other exceptions are caught
|
|
66
|
+
# and logged so memory-update failures never mask the parent task's
|
|
67
|
+
# result).
|
|
68
|
+
def run_memory_update_subagent
|
|
69
|
+
return unless should_update_memory?
|
|
70
|
+
|
|
71
|
+
handle = @ui&.start_progress(message: "Updating long-term memory…", style: :primary)
|
|
72
|
+
|
|
73
|
+
# Fork subagent inheriting main agent's model, tools, and history.
|
|
74
|
+
# Maximizes prompt-cache reuse: same model, same tool set, same
|
|
75
|
+
# cloned history — only the +system_prompt_suffix+ (the memory
|
|
76
|
+
# update instructions) and the final "Please proceed." user turn
|
|
77
|
+
# are new, landing on top of a warm cache.
|
|
78
|
+
subagent = fork_subagent(system_prompt_suffix: build_memory_update_prompt)
|
|
79
|
+
|
|
80
|
+
# Memory update is a background consolidation task — never prompt
|
|
81
|
+
# the user for confirmation on memory file writes. The subagent
|
|
82
|
+
# has its own config copy (fork_subagent does deep_copy), so this
|
|
83
|
+
# doesn't affect the parent.
|
|
84
|
+
sub_config = subagent.instance_variable_get(:@config)
|
|
85
|
+
sub_config.permission_mode = :auto_approve if sub_config.respond_to?(:permission_mode=)
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
result = subagent.run("Please proceed.")
|
|
89
|
+
rescue Clacky::AgentInterrupted
|
|
90
|
+
# User pressed Ctrl+C during memory update. Propagate so the
|
|
91
|
+
# parent agent's interrupt handler runs.
|
|
92
|
+
raise
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
# Memory update failures are NEVER fatal to the parent task.
|
|
95
|
+
# Log and move on — the user's actual work is already done.
|
|
96
|
+
@debug_logs << {
|
|
97
|
+
timestamp: Time.now.iso8601,
|
|
98
|
+
event: "memory_update_error",
|
|
99
|
+
error_class: e.class.name,
|
|
100
|
+
error_message: e.message,
|
|
101
|
+
backtrace: e.backtrace&.first(10)
|
|
102
|
+
}
|
|
103
|
+
Clacky::Logger.error("memory_update_error", error: e)
|
|
104
|
+
return
|
|
105
|
+
ensure
|
|
106
|
+
handle&.finish
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return unless result
|
|
110
|
+
|
|
111
|
+
# Merge subagent cost into parent's cumulative session spend so the
|
|
112
|
+
# sessionbar shows the real total. The parent's task-complete cost
|
|
113
|
+
# (result[:total_cost_usd] in Agent#run) stays unaffected — it
|
|
114
|
+
# still reflects ONLY the user's task, not the memory update.
|
|
115
|
+
subagent_cost = result[:total_cost_usd] || 0.0
|
|
116
|
+
@total_cost += subagent_cost
|
|
117
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
118
|
+
|
|
119
|
+
# Only surface a completion info line if the subagent actually
|
|
120
|
+
# wrote something to memory. The common "No memory updates needed."
|
|
121
|
+
# path stays silent to avoid visual noise.
|
|
122
|
+
if subagent_wrote_memory?(subagent)
|
|
123
|
+
@ui&.show_info("Memory updated: #{result[:iterations]} iterations, $#{subagent_cost.round(4)}")
|
|
124
|
+
end
|
|
66
125
|
end
|
|
67
126
|
|
|
68
127
|
private def memory_update_enabled?
|
|
@@ -72,6 +131,43 @@ module Clacky
|
|
|
72
131
|
@config.memory_update_enabled != false
|
|
73
132
|
end
|
|
74
133
|
|
|
134
|
+
# Inspect the subagent's history for a successful write/edit tool
|
|
135
|
+
# call targeting a memory file. Used to decide whether to surface a
|
|
136
|
+
# "Memory updated" info line (option C — silent when nothing changed).
|
|
137
|
+
# @param subagent [Clacky::Agent]
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
private def subagent_wrote_memory?(subagent)
|
|
140
|
+
return false unless subagent.respond_to?(:history) && subagent.history
|
|
141
|
+
|
|
142
|
+
subagent.history.to_a.any? do |msg|
|
|
143
|
+
next false unless msg.is_a?(Hash)
|
|
144
|
+
|
|
145
|
+
# Match OpenAI-style tool_calls on assistant messages …
|
|
146
|
+
tool_calls = msg[:tool_calls] || msg["tool_calls"]
|
|
147
|
+
if tool_calls.is_a?(Array) && tool_calls.any?
|
|
148
|
+
next true if tool_calls.any? do |tc|
|
|
149
|
+
name = tc.dig(:function, :name) || tc.dig("function", "name") || tc[:name] || tc["name"]
|
|
150
|
+
%w[write edit].include?(name.to_s)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# … and Anthropic-style content blocks with type=tool_use.
|
|
155
|
+
content = msg[:content] || msg["content"]
|
|
156
|
+
if content.is_a?(Array)
|
|
157
|
+
next true if content.any? do |block|
|
|
158
|
+
block.is_a?(Hash) &&
|
|
159
|
+
(block[:type] == "tool_use" || block["type"] == "tool_use") &&
|
|
160
|
+
%w[write edit].include?((block[:name] || block["name"]).to_s)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
false
|
|
165
|
+
end
|
|
166
|
+
rescue StandardError
|
|
167
|
+
# Defensive: never let introspection errors break memory update.
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
75
171
|
# Build the memory update prompt with the current memory file list injected.
|
|
76
172
|
# Uses a whitelist approach: default is NO write, only write if explicit criteria are met.
|
|
77
173
|
# @return [String]
|
|
@@ -125,8 +125,25 @@ module Clacky
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def parse_compressed_result(result, chunk_path: nil)
|
|
128
|
-
# Return the compressed result as a single
|
|
129
|
-
#
|
|
128
|
+
# Return the compressed result as a single user message (role: "user").
|
|
129
|
+
#
|
|
130
|
+
# Why role:"user" instead of "assistant":
|
|
131
|
+
# When all original user messages get archived into the chunk during compression
|
|
132
|
+
# (e.g. a long single-turn `/slash` task), the rebuilt history can end up as
|
|
133
|
+
# `system → assistant(summary) → assistant(tool_calls) → tool → …` with NO user
|
|
134
|
+
# message anywhere. Strict providers (notably DeepSeek V4 thinking mode) reject
|
|
135
|
+
# this as a malformed turn structure with a misleading
|
|
136
|
+
# "reasoning_content must be passed back" 400 error.
|
|
137
|
+
#
|
|
138
|
+
# Marking it as a user message gives the conversation a valid turn boundary.
|
|
139
|
+
# `system_injected: true` ensures the UI's replay_history still hides it from
|
|
140
|
+
# the chat panel (the real-user filter excludes system_injected messages), while
|
|
141
|
+
# INTERNAL_FIELDS in MessageHistory strips the marker before the API payload is
|
|
142
|
+
# built — so DeepSeek/OpenAI/Anthropic only see a plain `{role:"user", content:…}`.
|
|
143
|
+
#
|
|
144
|
+
# The `compressed_summary: true` flag is preserved so that replay_history still
|
|
145
|
+
# routes this message through the chunk-expansion path (which keys off that flag,
|
|
146
|
+
# not the role).
|
|
130
147
|
content = result.to_s.strip
|
|
131
148
|
|
|
132
149
|
if content.empty?
|
|
@@ -142,7 +159,17 @@ module Clacky
|
|
|
142
159
|
content_without_topics = content_without_topics + anchor
|
|
143
160
|
end
|
|
144
161
|
|
|
145
|
-
|
|
162
|
+
# Prefix lets the model recognise this is injected context, not a user utterance.
|
|
163
|
+
framed_content = "[Compressed conversation summary — previous turns archived]\n\n" \
|
|
164
|
+
"#{content_without_topics}"
|
|
165
|
+
|
|
166
|
+
[{
|
|
167
|
+
role: "user",
|
|
168
|
+
content: framed_content,
|
|
169
|
+
compressed_summary: true,
|
|
170
|
+
chunk_path: chunk_path,
|
|
171
|
+
system_injected: true
|
|
172
|
+
}]
|
|
146
173
|
end
|
|
147
174
|
end
|
|
148
175
|
end
|
|
@@ -15,11 +15,10 @@ module Clacky
|
|
|
15
15
|
# Trigger compression during idle time (user-friendly, interruptible)
|
|
16
16
|
# Returns true if compression was performed, false otherwise
|
|
17
17
|
def trigger_idle_compression
|
|
18
|
-
# Check if we should compress (force mode)
|
|
18
|
+
# Check if we should compress (force mode) BEFORE opening any UI, so
|
|
19
|
+
# "skipped" doesn't flash a spinner on screen.
|
|
19
20
|
compression_context = compress_messages_if_needed(force: true)
|
|
20
|
-
@ui&.show_progress("Idle detected. Compressing conversation to optimize costs...", progress_type: "idle_compress", phase: "active")
|
|
21
21
|
if compression_context.nil?
|
|
22
|
-
@ui&.show_progress("Idle skipped.", progress_type: "idle_compress", phase: "done")
|
|
23
22
|
Clacky::Logger.info(
|
|
24
23
|
"Idle compression skipped",
|
|
25
24
|
enable_compression: @config.enable_compression,
|
|
@@ -31,23 +30,44 @@ module Clacky
|
|
|
31
30
|
return false
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
#
|
|
33
|
+
# Own the progress indicator through +with_progress+: the ensure
|
|
34
|
+
# block guarantees the spinner/ticker is released even when the
|
|
35
|
+
# user interrupts mid-way (AgentInterrupted from current thread)
|
|
36
|
+
# or the LLM call fails. No more orphan gray tickers.
|
|
37
|
+
#
|
|
38
|
+
# When @ui is nil (tests / headless) we still need to run the
|
|
39
|
+
# compression work — safe-navigation with a block would silently
|
|
40
|
+
# skip it, so branch explicitly.
|
|
35
41
|
compression_message = compression_context[:compression_message]
|
|
36
42
|
@history.append(compression_message)
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
run_compression = lambda do |handle|
|
|
45
|
+
begin
|
|
46
|
+
response = call_llm
|
|
47
|
+
handle_compression_response(response, compression_context, progress: handle)
|
|
48
|
+
true
|
|
49
|
+
rescue Clacky::AgentInterrupted => e
|
|
50
|
+
@ui&.log("Idle compression canceled: #{e.message}", level: :info)
|
|
51
|
+
@history.rollback_before(compression_message)
|
|
52
|
+
false
|
|
53
|
+
rescue => e
|
|
54
|
+
@ui&.log("Idle compression failed: #{e.message}", level: :error)
|
|
55
|
+
@history.rollback_before(compression_message)
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if @ui
|
|
61
|
+
result = nil
|
|
62
|
+
@ui.with_progress(
|
|
63
|
+
message: "Idle detected. Compressing conversation to optimize costs...",
|
|
64
|
+
style: :quiet
|
|
65
|
+
) do |handle|
|
|
66
|
+
result = run_compression.call(handle)
|
|
67
|
+
end
|
|
68
|
+
result
|
|
69
|
+
else
|
|
70
|
+
run_compression.call(nil)
|
|
51
71
|
end
|
|
52
72
|
end
|
|
53
73
|
|
|
@@ -117,7 +137,14 @@ module Clacky
|
|
|
117
137
|
end
|
|
118
138
|
|
|
119
139
|
# Handle compression response and rebuild message list
|
|
120
|
-
|
|
140
|
+
# @param response [Hash] LLM response
|
|
141
|
+
# @param compression_context [Hash] context returned by +compress_messages_if_needed+
|
|
142
|
+
# @param progress [#finish, nil] Owned progress handle from the caller's
|
|
143
|
+
# with_progress block. When provided, the final summary message is
|
|
144
|
+
# delivered via +progress.finish(final_message: ...)+ instead of the
|
|
145
|
+
# legacy +show_progress(phase: "done")+ — this lets +ensure+ in the
|
|
146
|
+
# caller guarantee cleanup even if this method raises mid-way.
|
|
147
|
+
def handle_compression_response(response, compression_context, progress: nil)
|
|
121
148
|
# Extract compressed content from response
|
|
122
149
|
compressed_content = response[:content]
|
|
123
150
|
|
|
@@ -168,7 +195,14 @@ module Clacky
|
|
|
168
195
|
# Show compression info (use estimated tokens from rebuilt history)
|
|
169
196
|
compression_summary = "History compressed (~#{compression_context[:original_token_count]} -> ~#{@history.estimate_tokens} tokens, " \
|
|
170
197
|
"level #{compression_context[:compression_level]})"
|
|
171
|
-
|
|
198
|
+
if progress
|
|
199
|
+
# Owned-handle path: the caller's ensure block will still call
|
|
200
|
+
# handle.finish; finishing here with a final_message means that
|
|
201
|
+
# later finish (with no final_message) is a no-op (idempotent).
|
|
202
|
+
progress.finish(final_message: compression_summary)
|
|
203
|
+
else
|
|
204
|
+
@ui&.show_progress(compression_summary, progress_type: "idle_compress", phase: "done")
|
|
205
|
+
end
|
|
172
206
|
end
|
|
173
207
|
|
|
174
208
|
# Get recent messages while preserving tool_calls/tool_results pairs.
|