openclacky 0.8.0 → 0.8.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 +17 -1
- data/CHANGELOG.md +44 -0
- data/README.md +66 -67
- data/lib/clacky/agent/hook_manager.rb +1 -1
- data/lib/clacky/agent/memory_updater.rb +146 -0
- data/lib/clacky/agent/message_compressor.rb +12 -4
- data/lib/clacky/agent/message_compressor_helper.rb +139 -2
- data/lib/clacky/agent/skill_manager.rb +121 -10
- data/lib/clacky/agent/system_prompt_builder.rb +57 -89
- data/lib/clacky/agent/tool_executor.rb +3 -0
- data/lib/clacky/agent.rb +48 -18
- data/lib/clacky/agent_profile.rb +112 -0
- data/lib/clacky/brand_config.rb +138 -0
- data/lib/clacky/cli.rb +16 -12
- data/lib/clacky/default_agents/SOUL.md +3 -0
- data/lib/clacky/default_agents/USER.md +1 -0
- data/lib/clacky/default_agents/base_prompt.md +33 -0
- data/lib/clacky/default_agents/coding/profile.yml +2 -0
- data/lib/clacky/default_agents/coding/system_prompt.md +17 -0
- data/lib/clacky/default_agents/general/profile.yml +2 -0
- data/lib/clacky/default_agents/general/system_prompt.md +16 -0
- data/lib/clacky/default_skills/code-explorer/SKILL.md +1 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +1 -0
- data/lib/clacky/default_skills/new/SKILL.md +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +0 -5
- data/lib/clacky/default_skills/recall-memory/SKILL.md +66 -0
- data/lib/clacky/server/http_server.rb +113 -20
- data/lib/clacky/server/scheduler.rb +7 -7
- data/lib/clacky/server/session_registry.rb +3 -3
- data/lib/clacky/server/web_ui_controller.rb +17 -1
- data/lib/clacky/session_manager.rb +23 -8
- data/lib/clacky/skill.rb +144 -11
- data/lib/clacky/skill_loader.rb +98 -8
- data/lib/clacky/tools/browser.rb +1 -1
- data/lib/clacky/tools/edit.rb +1 -1
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/glob.rb +8 -0
- data/lib/clacky/tools/invoke_skill.rb +8 -8
- data/lib/clacky/tools/request_user_feedback.rb +1 -1
- data/lib/clacky/tools/todo_manager.rb +1 -1
- data/lib/clacky/tools/web_fetch.rb +1 -1
- data/lib/clacky/tools/web_search.rb +1 -1
- data/lib/clacky/tools/write.rb +1 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +7 -3
- data/lib/clacky/ui2/components/input_area.rb +3 -2
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +3 -2
- data/lib/clacky/utils/logger.rb +97 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +97 -0
- data/lib/clacky/web/app.js +202 -1
- data/lib/clacky/web/index.html +7 -0
- data/lib/clacky/web/onboard.js +5 -1
- data/lib/clacky/web/sessions.js +51 -15
- data/lib/clacky/web/skills.js +13 -5
- data/lib/clacky/web/tasks.js +15 -8
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +158 -19
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 534d725368218baaf47a39be9b5ce86005b53ebec0f3a2c81d01d754232b56d6
|
|
4
|
+
data.tar.gz: 0d4cfa47f16a72ddc2cc35e4c7a714ef3bee1d961be5d135aa110804b692e9f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d4250858cbb68fd49f0e25cadde03352ffd40566f05957d7de23b386e0e4a13668e02ae00858a0039ce3346eeff69a6bf2cba40da9e8074f3f954c22cc564c8
|
|
7
|
+
data.tar.gz: 4a794d9b85a3a0a6f64e70e18e047aadc4d69b64780965f97a5c827403fff75536068937e7b7f824064f4bb0efbd6e793abcf4eb0975a1523d3e21fe27633624
|
|
@@ -126,7 +126,7 @@ To use this skill, simply say:
|
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
3. **Analyze and Categorize Commits**
|
|
129
|
-
- Review each commit message
|
|
129
|
+
- Review each commit message AND its diff (`git show <hash> --stat`) to understand the actual change
|
|
130
130
|
- Categorize into:
|
|
131
131
|
- **Major Features**: User-visible functionality additions
|
|
132
132
|
- **Improvements**: Performance, UX, architecture enhancements
|
|
@@ -134,6 +134,22 @@ To use this skill, simply say:
|
|
|
134
134
|
- **Changes**: Breaking changes or significant refactoring
|
|
135
135
|
- **Minor Details**: Small fixes, style changes, trivial updates
|
|
136
136
|
|
|
137
|
+
**⚠️ Critical: Do NOT over-merge commits on the same topic**
|
|
138
|
+
|
|
139
|
+
It is tempting to group multiple commits under one bullet because they share a theme (e.g., "all about memory"). Resist this — each commit with **independent user-facing value** deserves its own bullet.
|
|
140
|
+
|
|
141
|
+
Ask for every commit: *"Does this enable something the user couldn't do before, separate from other commits on this topic?"*
|
|
142
|
+
- YES → write a separate CHANGELOG bullet
|
|
143
|
+
- NO (pure refactor, stability fix, threshold tweak) → merge into a related bullet or put in "More"
|
|
144
|
+
|
|
145
|
+
**Example of the mistake to avoid:**
|
|
146
|
+
- `feat: add long-term memory update system` and `feat: skill template context and recall-memory meta injection` are both "about memory", but they describe distinct capabilities:
|
|
147
|
+
- First: agent writes memories after sessions
|
|
148
|
+
- Second: skills receive a pre-built index so agent can selectively load only relevant memories
|
|
149
|
+
- These must be two separate bullets, not one.
|
|
150
|
+
|
|
151
|
+
**Sanity check after writing:** Count your `### Added` bullets vs the number of `feat:` commits. If `feat` commits > bullets, you likely merged too aggressively — revisit.
|
|
152
|
+
|
|
137
153
|
4. **Write CHANGELOG Entries**
|
|
138
154
|
|
|
139
155
|
**Format for Significant Items:**
|
data/CHANGELOG.md
CHANGED
|
@@ -7,9 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.2] - 2026-03-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Skill count limits**: two-layer guard to keep context tokens bounded — at most 50 skills loaded from disk (`MAX_SKILLS`) and at most 30 injected into the system prompt (`MAX_CONTEXT_SKILLS`); excess skills are skipped and a warning is written to the file logger
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- Skill `agent` field is now self-declared in each `SKILL.md` instead of being listed in `profile.yml` — makes skill-to-profile assignment portable and removes the need to edit profile config when adding skills
|
|
17
|
+
- Slash command autocomplete in the web UI now filters by the active session's agent profile, so only relevant skills appear
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- CLI startup crash: `ui: nil` keyword argument now correctly passed to `Agent.new`
|
|
21
|
+
|
|
22
|
+
## [0.8.1] - 2026-03-09
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Agent profile system**: define named agent profiles (`--agent coding|general`) with custom system prompts and skill whitelists via `profile.yml`; built-in `coding` and `general` profiles included
|
|
26
|
+
- **Skill autocomplete dropdown** in the web UI: type `/` in the chat input to see a filtered list of available skills
|
|
27
|
+
- **File-based logger** (`Clacky::Logger`): thread-safe structured logging to `~/.clacky/logs/` for debugging agent sessions
|
|
28
|
+
- **Session persistence on startup**: server now restores the most recent session for the working directory automatically on boot
|
|
29
|
+
- **Long-term memory update system**: agent automatically updates `~/.clacky/memories/` after sessions using a whitelist-driven approach; memories persist across restarts and are injected into agent context on startup
|
|
30
|
+
- **recall-memory skill with smart meta injection**: the `recall-memory` skill now receives a pre-built index of all memory files (topic, description, last updated) so the agent can selectively load only relevant memories without reading every file
|
|
31
|
+
- **Compressed message archiving**: older messages are compressed and archived to chunk Markdown files to keep context window manageable
|
|
32
|
+
- **Network pre-flight check**: connection is verified before agent starts; helpful VPN/proxy suggestions shown on failure
|
|
33
|
+
- **Encrypted brand skills**: white-label brand skills can now be shipped as encrypted `.enc` files for privacy
|
|
34
|
+
|
|
35
|
+
### Improved
|
|
36
|
+
- Memory update logic tightened: whitelist-driven approach, raised trigger threshold, and dynamic prompt — reduces false writes and improves reliability
|
|
37
|
+
- Slash commands in onboarding (`/create-task`, `/skill-add`) now use the pending-message pattern so they work correctly before WS connects
|
|
38
|
+
- Sidebar shows "No sessions yet" placeholder during onboarding
|
|
39
|
+
- Session delete is now optimistic — UI updates immediately without waiting for WS broadcast, and 404 ghost sessions are cleaned up automatically
|
|
40
|
+
- Tool call summaries from `format_call` are now rendered in the web UI for cleaner tool output display
|
|
41
|
+
- Agent error handling and memory update flow stabilized
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Create Task / Create Skill buttons during onboarding now correctly send the command after WS connects (previously messages were silently dropped)
|
|
45
|
+
- Pending slash commands are now queued until the session WS subscription is confirmed
|
|
46
|
+
- `working_dir: nil` added to all tool `execute` signatures to fix unknown keyword errors
|
|
47
|
+
|
|
48
|
+
### More
|
|
49
|
+
- `clacky` install script robustness and UX improvements
|
|
50
|
+
- Disabled rdoc/ri generation on gem install for faster installs
|
|
51
|
+
- Strip `.git/.svn/.hg` directories from glob results
|
|
52
|
+
|
|
10
53
|
## [0.8.0] - 2026-03-06
|
|
11
54
|
|
|
12
55
|
### Added
|
|
56
|
+
- **Browser tool**: AI agent can now control the user's Chrome browser via Chrome DevTools Protocol (CDP) — click, fill forms, take screenshots, scroll, and interact with pages using the user's real login session
|
|
13
57
|
- White-label brand licensing system: customize the web UI with your own name, logo, colors, and skills via `brand_config.yml`
|
|
14
58
|
- Brand skills tab in the web UI with private badge, shown only when brand skills are configured
|
|
15
59
|
- Slash command prompt rule: skill invocations (e.g. `/skill-name`) are now expanded inside the agent at run time, enabling mid-session skill triggering
|
data/README.md
CHANGED
|
@@ -6,46 +6,67 @@
|
|
|
6
6
|
[](https://rubygems.org/gems/openclacky)
|
|
7
7
|
[](LICENSE.txt)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**From expertise to business — turn your professional knowledge into a monetizable OpenClaw Skill.**
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
OpenClacky is the creator-side platform for the OpenClaw ecosystem. Package your methods and workflows into encrypted, white-labeled Skills that your clients install and use — under your name, your brand, your price.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Why OpenClacky?
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The OpenClaw ecosystem has 5,700+ Skills and growing. But almost all of them are open-sourced, free, and easily copied. The real scarcity isn't more Skills — it's **expertise-backed, production-grade Skills worth paying for**.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
$ openclacky
|
|
19
|
-
```
|
|
17
|
+
OpenClacky is built for the people who have that expertise.
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
| | **Openclaw** | **OpenClacky** |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| **Core model** | Open sharing | Encrypted & protected |
|
|
22
|
+
| **Primary users** | Users who install Skills | Creators who sell Skills |
|
|
23
|
+
| **Revenue** | None | Creator-defined pricing |
|
|
24
|
+
| **Brand** | Platform brand | Your own brand |
|
|
25
|
+
| **Driven by** | Technical contributors | Domain expertise |
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
**Four steps from capability to business:**
|
|
30
|
+
|
|
31
|
+
1. **Craft your Skill** — Turn your domain methodology into a repeatable AI workflow
|
|
32
|
+
2. **Encrypt & protect** — Your logic stays yours; clients can't inspect or copy it
|
|
33
|
+
3. **Package your brand** — Ship under your name, your logo, your onboarding experience
|
|
34
|
+
4. **Launch & acquire** — One-click sales page, built-in SEO, start converting traffic
|
|
35
|
+
|
|
36
|
+
## Who It's For
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| **Dev/Prod Isolation** | ❌ | ❌ | ✅ Automatic |
|
|
35
|
-
| **Automatic Backups** | ❌ | ⚠️ Paid feature | ✅ Built-in |
|
|
36
|
-
| **AI Cost Control** | ❌ Pay per token | ❌ Subscription | ✅ Optimally balanced |
|
|
37
|
-
| **Data Ownership** | ✅ | ❌ Platform-owned | ✅ Fully yours |
|
|
38
|
-
| **Interface** | Terminal | Web UI | Terminal |
|
|
38
|
+
OpenClacky is built for domain experts whose knowledge can be expressed as *information processing + executable actions*:
|
|
39
|
+
|
|
40
|
+
- **SEO specialists** — keyword research, content scoring, rank monitoring
|
|
41
|
+
- **Lawyers** — contract review, case retrieval, risk flagging
|
|
42
|
+
- **Traders** — signal detection, strategy backtesting, automated execution
|
|
43
|
+
- **Data analysts** — cleaning, modeling, report generation
|
|
44
|
+
- **Content strategists** — topic selection, outlines, drafts at scale
|
|
39
45
|
|
|
40
46
|
## Features
|
|
41
47
|
|
|
42
|
-
- [x]
|
|
43
|
-
- [x] **
|
|
44
|
-
- [x] **
|
|
45
|
-
- [x] **
|
|
46
|
-
- [x] **
|
|
48
|
+
- [x] **Skill builder** — Create AI workflows via conversation or UI, iterate and ship fast
|
|
49
|
+
- [x] **Encryption** — Protect your knowledge assets; end users cannot read your Skill source
|
|
50
|
+
- [x] **White-label packaging** — Your brand, your product line, your client experience
|
|
51
|
+
- [x] **Auto-update delivery** — Push updates to all users seamlessly, with version control
|
|
52
|
+
- [x] **Cross-platform distribution** — Windows, macOS, Linux — one Skill, every platform
|
|
53
|
+
- [x] **Sales page generator** — Launch your storefront fast, with built-in SEO foundations
|
|
54
|
+
- [x] **Cost monitoring** — Real-time token tracking, automatic compression (up to 90% savings)
|
|
47
55
|
- [x] **Multi-provider support** — OpenAI, Anthropic, DeepSeek, and any OpenAI-compatible API
|
|
48
|
-
- [ ] **
|
|
56
|
+
- [ ] **Skill marketplace** — Discover and distribute premium Skills *(coming soon)*
|
|
57
|
+
|
|
58
|
+
## Coding Support
|
|
59
|
+
|
|
60
|
+
OpenClacky also works as a general AI coding assistant — scaffold full-stack Rails apps, add features, or explore an unfamiliar codebase:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ openclacky
|
|
64
|
+
> /new my-app # scaffold a full-stack Rails app
|
|
65
|
+
> Add user auth with email and password
|
|
66
|
+
> How does the payment module work?
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Built on a production-ready Rails architecture with one-click deployment, dev/prod isolation, and automatic backups.
|
|
49
70
|
|
|
50
71
|
## Installation
|
|
51
72
|
|
|
@@ -63,52 +84,38 @@ $ openclacky
|
|
|
63
84
|
gem install openclacky
|
|
64
85
|
```
|
|
65
86
|
|
|
66
|
-
##
|
|
87
|
+
## Quick Start
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
### Terminal (CLI)
|
|
69
90
|
|
|
70
91
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- /config
|
|
92
|
+
openclacky # start interactive agent in current directory
|
|
74
93
|
```
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
- **API Key**: Your API key from any OpenAI-compatible provider
|
|
78
|
-
- **Model**: Model name
|
|
79
|
-
- **Base URL**: OpenAI-compatible API endpoint
|
|
80
|
-
|
|
81
|
-
## Usage
|
|
82
|
-
|
|
83
|
-
### Scenario 1: Create a new web app
|
|
95
|
+
### Web UI
|
|
84
96
|
|
|
85
97
|
```bash
|
|
86
|
-
|
|
87
|
-
> /new my-blog
|
|
88
|
-
# OpenClacky scaffolds a full-stack Rails app in seconds
|
|
89
|
-
# > Add a posts page with title, content, and author fields
|
|
90
|
-
# > Deploy to production
|
|
91
|
-
# > exit
|
|
98
|
+
openclacky server # start the web server (default: http://localhost:7070)
|
|
92
99
|
```
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
Then open **http://localhost:7070** in your browser. You'll get a full-featured chat interface with multi-session support — run separate sessions for coding, copywriting, research, and more, all in parallel.
|
|
102
|
+
|
|
103
|
+
Options:
|
|
95
104
|
|
|
96
105
|
```bash
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# > Write tests for the auth flow
|
|
100
|
-
# > exit
|
|
106
|
+
openclacky server --port 8080 # custom port
|
|
107
|
+
openclacky server --host 0.0.0.0 # listen on all interfaces (e.g. remote access)
|
|
101
108
|
```
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
## Configuration
|
|
104
111
|
|
|
105
112
|
```bash
|
|
106
113
|
$ openclacky
|
|
107
|
-
|
|
108
|
-
# > Where is the user session managed?
|
|
109
|
-
# > exit
|
|
114
|
+
> /config
|
|
110
115
|
```
|
|
111
116
|
|
|
117
|
+
You'll be prompted to set your **API Key**, **Model**, and **Base URL** (any OpenAI-compatible provider).
|
|
118
|
+
|
|
112
119
|
## Install from Source
|
|
113
120
|
|
|
114
121
|
```bash
|
|
@@ -118,18 +125,10 @@ bundle install
|
|
|
118
125
|
bin/clacky
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
## Development
|
|
122
|
-
|
|
123
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/clacky` for an interactive prompt that will allow you to experiment.
|
|
124
|
-
|
|
125
128
|
## Contributing
|
|
126
129
|
|
|
127
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/clacky-ai/open-clacky.
|
|
130
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/clacky-ai/open-clacky. Contributors are expected to adhere to the [code of conduct](https://github.com/clacky-ai/open-clacky/blob/main/CODE_OF_CONDUCT.md).
|
|
128
131
|
|
|
129
132
|
## License
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
## Code of Conduct
|
|
134
|
-
|
|
135
|
-
Everyone interacting in the OpenClacky project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/clacky-ai/open-clacky/blob/main/CODE_OF_CONDUCT.md).
|
|
134
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
class Agent
|
|
5
|
+
# Long-term memory update functionality
|
|
6
|
+
# Triggered at the end of a session to persist important knowledge.
|
|
7
|
+
#
|
|
8
|
+
# The LLM decides:
|
|
9
|
+
# - Which topics were discussed
|
|
10
|
+
# - Which memory files to update or create
|
|
11
|
+
# - How to merge new info with existing content
|
|
12
|
+
# - What to drop to stay within the per-file token limit
|
|
13
|
+
#
|
|
14
|
+
# Trigger condition:
|
|
15
|
+
# - Iteration count >= MEMORY_UPDATE_MIN_ITERATIONS (avoids trivial tasks like commits)
|
|
16
|
+
module MemoryUpdater
|
|
17
|
+
# Minimum LLM iterations for this task before triggering memory update.
|
|
18
|
+
# Set high enough to skip short utility tasks (commit, deploy, etc.)
|
|
19
|
+
MEMORY_UPDATE_MIN_ITERATIONS = 10
|
|
20
|
+
|
|
21
|
+
MEMORIES_DIR = File.expand_path("~/.clacky/memories")
|
|
22
|
+
|
|
23
|
+
# Check if memory update should be triggered for this task.
|
|
24
|
+
# Only triggers when the task had enough LLM iterations,
|
|
25
|
+
# skipping short utility tasks (e.g. commit, deploy).
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def should_update_memory?
|
|
28
|
+
return false unless memory_update_enabled?
|
|
29
|
+
|
|
30
|
+
task_iterations = @iterations - (@task_start_iterations || 0)
|
|
31
|
+
task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Inject memory update prompt into @messages so the main agent loop handles it.
|
|
35
|
+
# Builds the prompt dynamically, injecting the current memory file list so the
|
|
36
|
+
# LLM doesn't need to scan the directory itself.
|
|
37
|
+
# Returns true if prompt was injected, false otherwise.
|
|
38
|
+
def inject_memory_prompt!
|
|
39
|
+
return false unless should_update_memory?
|
|
40
|
+
return false if @memory_prompt_injected
|
|
41
|
+
|
|
42
|
+
@memory_prompt_injected = true
|
|
43
|
+
@memory_updating = true
|
|
44
|
+
@ui&.show_info("Updating long-term memory...")
|
|
45
|
+
|
|
46
|
+
@messages << {
|
|
47
|
+
role: "user",
|
|
48
|
+
content: build_memory_update_prompt,
|
|
49
|
+
system_injected: true,
|
|
50
|
+
memory_update: true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Clean up memory update messages from conversation history after loop ends.
|
|
57
|
+
# Call this once after the main loop finishes.
|
|
58
|
+
def cleanup_memory_messages
|
|
59
|
+
return unless @memory_prompt_injected
|
|
60
|
+
|
|
61
|
+
@messages.reject! { |m| m[:memory_update] }
|
|
62
|
+
@memory_prompt_injected = false
|
|
63
|
+
@memory_updating = false
|
|
64
|
+
@ui&.show_info("Memory updated.")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private def memory_update_enabled?
|
|
68
|
+
# Check config flag; default to true if not set
|
|
69
|
+
return true unless @config.respond_to?(:memory_update_enabled)
|
|
70
|
+
|
|
71
|
+
@config.memory_update_enabled != false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Build the memory update prompt with the current memory file list injected.
|
|
75
|
+
# Uses a whitelist approach: default is NO write, only write if explicit criteria are met.
|
|
76
|
+
# @return [String]
|
|
77
|
+
private def build_memory_update_prompt
|
|
78
|
+
today = Time.now.strftime("%Y-%m-%d")
|
|
79
|
+
meta = load_memories_meta
|
|
80
|
+
|
|
81
|
+
<<~PROMPT
|
|
82
|
+
═══════════════════════════════════════════════════════════════
|
|
83
|
+
MEMORY UPDATE MODE
|
|
84
|
+
═══════════════════════════════════════════════════════════════
|
|
85
|
+
The conversation above has ended. You are now in MEMORY UPDATE MODE.
|
|
86
|
+
|
|
87
|
+
## Default: Do NOT write anything.
|
|
88
|
+
|
|
89
|
+
Memory writes are expensive. Only write if the session contains at least one of the
|
|
90
|
+
following high-value signals. If NONE apply, respond immediately with:
|
|
91
|
+
"No memory updates needed." and STOP — do not use any tools.
|
|
92
|
+
|
|
93
|
+
## Whitelist: Write ONLY if at least one condition is met
|
|
94
|
+
|
|
95
|
+
1. **Explicit decision** — The user made a clear technical, product, or process decision
|
|
96
|
+
that will affect future work (e.g. "we'll use X instead of Y going forward").
|
|
97
|
+
2. **New persistent context** — The user introduced project background, constraints, or
|
|
98
|
+
goals that are not already obvious from the code (e.g. a new feature direction,
|
|
99
|
+
a deployment target, a team convention).
|
|
100
|
+
3. **Correction of prior knowledge** — The user corrected a previous misunderstanding
|
|
101
|
+
or the agent discovered that an existing memory is wrong or outdated.
|
|
102
|
+
4. **Stated preference** — The user expressed a clear personal or team preference about
|
|
103
|
+
how they want the agent to behave, communicate, or write code.
|
|
104
|
+
|
|
105
|
+
## What does NOT qualify (skip these entirely)
|
|
106
|
+
|
|
107
|
+
- Running tests, fixing lint, formatting code
|
|
108
|
+
- Committing, deploying, or releasing
|
|
109
|
+
- Answering a one-off question or explaining a concept
|
|
110
|
+
- Any task that produced no lasting decisions or preferences
|
|
111
|
+
- Repeating or slightly rephrasing what is already in memory
|
|
112
|
+
|
|
113
|
+
## Existing Memory Files (pre-loaded — do NOT re-scan the directory)
|
|
114
|
+
|
|
115
|
+
#{meta}
|
|
116
|
+
|
|
117
|
+
Each file has YAML frontmatter:
|
|
118
|
+
```
|
|
119
|
+
---
|
|
120
|
+
topic: <topic name>
|
|
121
|
+
description: <one-line description>
|
|
122
|
+
updated_at: <YYYY-MM-DD>
|
|
123
|
+
---
|
|
124
|
+
<content in concise Markdown>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Steps (only if a whitelist condition is met)
|
|
128
|
+
|
|
129
|
+
For each qualifying topic:
|
|
130
|
+
a. If a matching file exists → read it with `file_reader(path: "~/.clacky/memories/<filename>")`, then write an updated version (merge new + old, drop stale)
|
|
131
|
+
b. If no matching file → create a new one at `~/.clacky/memories/<new-filename>.md`
|
|
132
|
+
Use the `write` tool to save each file. Do NOT use `safe_shell` or `file_reader` to list the directory.
|
|
133
|
+
|
|
134
|
+
## Hard constraints (CRITICAL)
|
|
135
|
+
- Each file MUST stay under 4000 characters of content (after the frontmatter)
|
|
136
|
+
- If merging would exceed this limit, remove the least important information
|
|
137
|
+
- Write concise, factual Markdown — no fluff
|
|
138
|
+
- Update `updated_at` to today's date: #{today}
|
|
139
|
+
- Only write files for topics that genuinely appeared in this conversation
|
|
140
|
+
|
|
141
|
+
Begin by checking the whitelist. If no condition is met, stop immediately.
|
|
142
|
+
PROMPT
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -85,13 +85,14 @@ module Clacky
|
|
|
85
85
|
# @param compressed_content [String] The compressed summary from LLM
|
|
86
86
|
# @param original_messages [Array<Hash>] Original messages before compression
|
|
87
87
|
# @param recent_messages [Array<Hash>] Recent messages to preserve
|
|
88
|
+
# @param chunk_path [String, nil] Path to the archived chunk MD file (if saved)
|
|
88
89
|
# @return [Array<Hash>] Rebuilt message list: system + compressed + recent
|
|
89
|
-
def rebuild_with_compression(compressed_content, original_messages:, recent_messages:)
|
|
90
|
+
def rebuild_with_compression(compressed_content, original_messages:, recent_messages:, chunk_path: nil)
|
|
90
91
|
# Find and preserve system message
|
|
91
92
|
system_msg = original_messages.find { |m| m[:role] == "system" }
|
|
92
93
|
|
|
93
94
|
# Parse the compressed result
|
|
94
|
-
parsed_messages = parse_compressed_result(compressed_content)
|
|
95
|
+
parsed_messages = parse_compressed_result(compressed_content, chunk_path: chunk_path)
|
|
95
96
|
|
|
96
97
|
# If parsing fails or returns empty, raise error
|
|
97
98
|
if parsed_messages.nil? || parsed_messages.empty?
|
|
@@ -104,7 +105,7 @@ module Clacky
|
|
|
104
105
|
|
|
105
106
|
private
|
|
106
107
|
|
|
107
|
-
def parse_compressed_result(result)
|
|
108
|
+
def parse_compressed_result(result, chunk_path: nil)
|
|
108
109
|
# Return the compressed result as a single assistant message
|
|
109
110
|
# Keep the <analysis> or <summary> tags as they provide semantic context
|
|
110
111
|
content = result.strip
|
|
@@ -112,7 +113,14 @@ module Clacky
|
|
|
112
113
|
if content.empty?
|
|
113
114
|
[]
|
|
114
115
|
else
|
|
115
|
-
|
|
116
|
+
# Inject chunk anchor so AI knows where to find original conversation
|
|
117
|
+
if chunk_path
|
|
118
|
+
anchor = "\n\n---\n📁 **Original conversation archived at:** `#{chunk_path}`\n" \
|
|
119
|
+
"_Use `file_reader` tool to recall details from this chunk._"
|
|
120
|
+
content = content + anchor
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
[{ role: "assistant", content: content, compressed_summary: true, chunk_path: chunk_path }]
|
|
116
124
|
end
|
|
117
125
|
end
|
|
118
126
|
end
|
|
@@ -119,10 +119,20 @@ module Clacky
|
|
|
119
119
|
# Note: we need to remove the compression instruction message we just added
|
|
120
120
|
original_messages = @messages[0..-2] # All except the last (compression instruction)
|
|
121
121
|
|
|
122
|
+
# Archive compressed messages to a chunk MD file before discarding them
|
|
123
|
+
chunk_index = @compressed_summaries.size + 1
|
|
124
|
+
chunk_path = save_compressed_chunk(
|
|
125
|
+
original_messages,
|
|
126
|
+
compression_context[:recent_messages],
|
|
127
|
+
chunk_index: chunk_index,
|
|
128
|
+
compression_level: compression_context[:compression_level]
|
|
129
|
+
)
|
|
130
|
+
|
|
122
131
|
@messages = @message_compressor.rebuild_with_compression(
|
|
123
132
|
compressed_content,
|
|
124
133
|
original_messages: original_messages,
|
|
125
|
-
recent_messages: compression_context[:recent_messages]
|
|
134
|
+
recent_messages: compression_context[:recent_messages],
|
|
135
|
+
chunk_path: chunk_path
|
|
126
136
|
)
|
|
127
137
|
|
|
128
138
|
# Track this compression
|
|
@@ -130,7 +140,8 @@ module Clacky
|
|
|
130
140
|
level: compression_context[:compression_level],
|
|
131
141
|
message_count: compression_context[:original_message_count],
|
|
132
142
|
timestamp: Time.now.iso8601,
|
|
133
|
-
strategy: :insert_then_compress
|
|
143
|
+
strategy: :insert_then_compress,
|
|
144
|
+
chunk_path: chunk_path
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
final_tokens = total_message_tokens[:total]
|
|
@@ -249,6 +260,132 @@ module Clacky
|
|
|
249
260
|
|
|
250
261
|
private
|
|
251
262
|
|
|
263
|
+
# Save the messages being compressed to a chunk MD file for future recall
|
|
264
|
+
# File path: ~/.clacky/sessions/{datetime}-{short_id}-chunk-{n}.md
|
|
265
|
+
# @param original_messages [Array<Hash>] All messages before compression (excluding compression instruction)
|
|
266
|
+
# @param recent_messages [Array<Hash>] Recent messages being kept (to exclude from chunk)
|
|
267
|
+
# @param chunk_index [Integer] Sequential chunk number
|
|
268
|
+
# @param compression_level [Integer] Compression level
|
|
269
|
+
# @return [String, nil] Path to saved chunk file, or nil if save failed
|
|
270
|
+
def save_compressed_chunk(original_messages, recent_messages, chunk_index:, compression_level:)
|
|
271
|
+
return nil unless @session_id && @created_at
|
|
272
|
+
|
|
273
|
+
# Messages being compressed = original minus system message minus recent messages
|
|
274
|
+
recent_set = recent_messages.to_a
|
|
275
|
+
messages_to_archive = original_messages.reject do |m|
|
|
276
|
+
m[:role] == "system" || recent_set.include?(m)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
return nil if messages_to_archive.empty?
|
|
280
|
+
|
|
281
|
+
sessions_dir = Clacky::SessionManager::SESSIONS_DIR
|
|
282
|
+
datetime = Time.parse(@created_at).strftime("%Y-%m-%d-%H-%M-%S")
|
|
283
|
+
short_id = @session_id[0..7]
|
|
284
|
+
base_name = "#{datetime}-#{short_id}"
|
|
285
|
+
chunk_filename = "#{base_name}-chunk-#{chunk_index}.md"
|
|
286
|
+
chunk_path = File.join(sessions_dir, chunk_filename)
|
|
287
|
+
|
|
288
|
+
md_content = build_chunk_md(messages_to_archive, chunk_index: chunk_index, compression_level: compression_level)
|
|
289
|
+
|
|
290
|
+
File.write(chunk_path, md_content)
|
|
291
|
+
FileUtils.chmod(0o600, chunk_path)
|
|
292
|
+
|
|
293
|
+
chunk_path
|
|
294
|
+
rescue => e
|
|
295
|
+
@ui&.log("Failed to save chunk MD: #{e.message}", level: :warn)
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Build markdown content from a list of messages
|
|
300
|
+
# @param messages [Array<Hash>] Messages to render
|
|
301
|
+
# @param chunk_index [Integer] Chunk number for metadata
|
|
302
|
+
# @param compression_level [Integer] Compression level
|
|
303
|
+
# @return [String] Markdown content
|
|
304
|
+
def build_chunk_md(messages, chunk_index:, compression_level:)
|
|
305
|
+
lines = []
|
|
306
|
+
|
|
307
|
+
# Front matter
|
|
308
|
+
lines << "---"
|
|
309
|
+
lines << "session_id: #{@session_id}"
|
|
310
|
+
lines << "chunk: #{chunk_index}"
|
|
311
|
+
lines << "compression_level: #{compression_level}"
|
|
312
|
+
lines << "archived_at: #{Time.now.iso8601}"
|
|
313
|
+
lines << "message_count: #{messages.size}"
|
|
314
|
+
lines << "---"
|
|
315
|
+
lines << ""
|
|
316
|
+
lines << "# Session Chunk #{chunk_index}"
|
|
317
|
+
lines << ""
|
|
318
|
+
lines << "> This file contains the original conversation archived during compression."
|
|
319
|
+
lines << "> Use `file_reader` to recall specific details from this conversation."
|
|
320
|
+
lines << ""
|
|
321
|
+
|
|
322
|
+
messages.each do |msg|
|
|
323
|
+
role = msg[:role]
|
|
324
|
+
content = msg[:content]
|
|
325
|
+
|
|
326
|
+
case role
|
|
327
|
+
when "user"
|
|
328
|
+
lines << "## User"
|
|
329
|
+
lines << ""
|
|
330
|
+
lines << format_message_content(content)
|
|
331
|
+
lines << ""
|
|
332
|
+
when "assistant"
|
|
333
|
+
# If this message is itself a compressed summary, annotate the header
|
|
334
|
+
# so the reader knows the original conversation is in the referenced chunk
|
|
335
|
+
if msg[:compressed_summary] && msg[:chunk_path]
|
|
336
|
+
prev_chunk = File.basename(msg[:chunk_path])
|
|
337
|
+
lines << "## Assistant [Compressed Summary — original conversation at: #{prev_chunk}]"
|
|
338
|
+
else
|
|
339
|
+
lines << "## Assistant"
|
|
340
|
+
end
|
|
341
|
+
lines << ""
|
|
342
|
+
# Include tool calls summary if present
|
|
343
|
+
if msg[:tool_calls]&.any?
|
|
344
|
+
tool_names = msg[:tool_calls].map { |tc| tc.dig(:function, :name) }.compact.join(", ")
|
|
345
|
+
lines << "_Tool calls: #{tool_names}_"
|
|
346
|
+
lines << ""
|
|
347
|
+
end
|
|
348
|
+
lines << format_message_content(content) if content
|
|
349
|
+
lines << ""
|
|
350
|
+
when "tool"
|
|
351
|
+
tool_name = msg[:name] || "tool"
|
|
352
|
+
lines << "### Tool Result: #{tool_name}"
|
|
353
|
+
lines << ""
|
|
354
|
+
lines << "```"
|
|
355
|
+
lines << truncate_content(content.to_s, max_length: 500)
|
|
356
|
+
lines << "```"
|
|
357
|
+
lines << ""
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
lines.join("\n")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Format message content (handles string or array of content blocks)
|
|
365
|
+
def format_message_content(content)
|
|
366
|
+
return "" if content.nil?
|
|
367
|
+
return content.to_s if content.is_a?(String)
|
|
368
|
+
|
|
369
|
+
# Handle array of content blocks (e.g., text + images)
|
|
370
|
+
if content.is_a?(Array)
|
|
371
|
+
content.map do |block|
|
|
372
|
+
if block.is_a?(Hash) && block[:type] == "text"
|
|
373
|
+
block[:text].to_s
|
|
374
|
+
else
|
|
375
|
+
"[#{block[:type] || 'content'}]"
|
|
376
|
+
end
|
|
377
|
+
end.join("\n")
|
|
378
|
+
else
|
|
379
|
+
content.to_s
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Truncate long content with a note
|
|
384
|
+
def truncate_content(text, max_length: 500)
|
|
385
|
+
return text if text.length <= max_length
|
|
386
|
+
"#{text[0...max_length]}\n... [truncated, #{text.length} chars total]"
|
|
387
|
+
end
|
|
388
|
+
|
|
252
389
|
# Calculate how many recent messages to keep based on how much we need to compress
|
|
253
390
|
def calculate_target_recent_count(reduction_needed)
|
|
254
391
|
# We want recent messages to be around 20-30% of the total target
|