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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +17 -1
  3. data/CHANGELOG.md +44 -0
  4. data/README.md +66 -67
  5. data/lib/clacky/agent/hook_manager.rb +1 -1
  6. data/lib/clacky/agent/memory_updater.rb +146 -0
  7. data/lib/clacky/agent/message_compressor.rb +12 -4
  8. data/lib/clacky/agent/message_compressor_helper.rb +139 -2
  9. data/lib/clacky/agent/skill_manager.rb +121 -10
  10. data/lib/clacky/agent/system_prompt_builder.rb +57 -89
  11. data/lib/clacky/agent/tool_executor.rb +3 -0
  12. data/lib/clacky/agent.rb +48 -18
  13. data/lib/clacky/agent_profile.rb +112 -0
  14. data/lib/clacky/brand_config.rb +138 -0
  15. data/lib/clacky/cli.rb +16 -12
  16. data/lib/clacky/default_agents/SOUL.md +3 -0
  17. data/lib/clacky/default_agents/USER.md +1 -0
  18. data/lib/clacky/default_agents/base_prompt.md +33 -0
  19. data/lib/clacky/default_agents/coding/profile.yml +2 -0
  20. data/lib/clacky/default_agents/coding/system_prompt.md +17 -0
  21. data/lib/clacky/default_agents/general/profile.yml +2 -0
  22. data/lib/clacky/default_agents/general/system_prompt.md +16 -0
  23. data/lib/clacky/default_skills/code-explorer/SKILL.md +1 -0
  24. data/lib/clacky/default_skills/deploy/SKILL.md +1 -0
  25. data/lib/clacky/default_skills/new/SKILL.md +1 -0
  26. data/lib/clacky/default_skills/onboard/SKILL.md +0 -5
  27. data/lib/clacky/default_skills/recall-memory/SKILL.md +66 -0
  28. data/lib/clacky/server/http_server.rb +113 -20
  29. data/lib/clacky/server/scheduler.rb +7 -7
  30. data/lib/clacky/server/session_registry.rb +3 -3
  31. data/lib/clacky/server/web_ui_controller.rb +17 -1
  32. data/lib/clacky/session_manager.rb +23 -8
  33. data/lib/clacky/skill.rb +144 -11
  34. data/lib/clacky/skill_loader.rb +98 -8
  35. data/lib/clacky/tools/browser.rb +1 -1
  36. data/lib/clacky/tools/edit.rb +1 -1
  37. data/lib/clacky/tools/file_reader.rb +1 -1
  38. data/lib/clacky/tools/glob.rb +8 -0
  39. data/lib/clacky/tools/invoke_skill.rb +8 -8
  40. data/lib/clacky/tools/request_user_feedback.rb +1 -1
  41. data/lib/clacky/tools/todo_manager.rb +1 -1
  42. data/lib/clacky/tools/web_fetch.rb +1 -1
  43. data/lib/clacky/tools/web_search.rb +1 -1
  44. data/lib/clacky/tools/write.rb +1 -1
  45. data/lib/clacky/ui2/components/command_suggestions.rb +7 -3
  46. data/lib/clacky/ui2/components/input_area.rb +3 -2
  47. data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
  48. data/lib/clacky/ui2/ui_controller.rb +3 -2
  49. data/lib/clacky/utils/logger.rb +97 -0
  50. data/lib/clacky/version.rb +1 -1
  51. data/lib/clacky/web/app.css +97 -0
  52. data/lib/clacky/web/app.js +202 -1
  53. data/lib/clacky/web/index.html +7 -0
  54. data/lib/clacky/web/onboard.js +5 -1
  55. data/lib/clacky/web/sessions.js +51 -15
  56. data/lib/clacky/web/skills.js +13 -5
  57. data/lib/clacky/web/tasks.js +15 -8
  58. data/lib/clacky.rb +2 -0
  59. data/scripts/install.sh +158 -19
  60. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 847578e565b36373d9941ed108df7b68a78a3b485681f22f606637f90f3f6a49
4
- data.tar.gz: 638855b903c61169486422f5d791a3eb7a7b4751cdee5a3b757abaf4fdd32ff2
3
+ metadata.gz: 534d725368218baaf47a39be9b5ce86005b53ebec0f3a2c81d01d754232b56d6
4
+ data.tar.gz: 0d4cfa47f16a72ddc2cc35e4c7a714ef3bee1d961be5d135aa110804b692e9f1
5
5
  SHA512:
6
- metadata.gz: 981a682c39c603d101c7755e3bdeecf053519e2eb041120311a4877c2aac9370bbc24cf57ead31595636592eb75513c6844faf9418cf378bb1c767e506b560e0
7
- data.tar.gz: f7cbdcadf728ddc1825d9f11827f3dc8843981005a59cb5851391580d3f91e25b7f2f32730b928702ea3ad638c54e7c2f2134d0d16d47173dd37816d51e290e5
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 and its changes
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
  [![Downloads](https://img.shields.io/gem/dt/openclacky?label=downloads&style=flat-square&color=brightgreen)](https://rubygems.org/gems/openclacky)
7
7
  [![License](https://img.shields.io/badge/license-MIT-lightgrey?style=flat-square)](LICENSE.txt)
8
8
 
9
- OpenClacky = Lovable + Supabase
9
+ **From expertise to business — turn your professional knowledge into a monetizable OpenClaw Skill.**
10
10
 
11
- **OpenClacky** is a CLI tool for building full-stack web applications — no technical background required. We spent months crafting a **Rails for AI** full-stack architecture that is fully production-ready, with one-click deployment, isolated dev/production environments, and automatic backups.
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
- OpenClacky's goal is to deliver the best balance of **AI quality, AI cost, and AI speed**.
13
+ ## Why OpenClacky?
14
14
 
15
- ## Quick start
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
- ```bash
18
- $ openclacky
19
- ```
17
+ OpenClacky is built for the people who have that expertise.
20
18
 
21
- - `/config` Set your API key, model, and base URL
22
- - `/new <project-name>` — Create a new project
23
- - Type your requirements and start building
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
- ## Why OpenClacky?
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
- | | **Claude Code** | **Lovable + Supabase** | **OpenClacky** |
28
- |---|---|---|---|
29
- | **Target Users** | Professional developers | Non-technical users | Non-technical users |
30
- | **Tech Stack** | Any | React + Supabase | Rails (full-stack) |
31
- | **Full-Stack Integration** | DIY | ⚠️ Frontend/backend split | ✅ Unified full-stack |
32
- | **Production-Ready** | Manual setup | ⚠️ Relies on third-party | ✅ Built-in |
33
- | **One-Click Deploy** | | ⚠️ Platform lock-in | ✅ Deploy anywhere |
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] `/new <project-name>`Scaffold a full-stack Rails web app in seconds
43
- - [x] **Skills system** — Specialized AI workflows for deploy, frontend design, PDF, PPTX, and more
44
- - [x] **Cost monitoring & compression** — Real-time cost tracking, automatic message compression (up to 90% savings)
45
- - [x] **One-click deployment** — Ship to production with a single command (with Clacky CDE)
46
- - [x] **Autonomous AI agent** — Multi-step task execution with undo/redo
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
- - [ ] **Time Machine** — Visual history to rewind and branch any point in your project *(coming soon)*
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
- ## Configuration
87
+ ## Quick Start
67
88
 
68
- Before using Clacky, you need to configure your settings:
89
+ ### Terminal (CLI)
69
90
 
70
91
  ```bash
71
- $ openclacky
72
-
73
- - /config
92
+ openclacky # start interactive agent in current directory
74
93
  ```
75
94
 
76
- You'll be prompted to enter:
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
- $ openclacky
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
- ### Scenario 2: Build a feature in an existing project
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
- $ cd ~/my-project && openclacky
98
- # > Add user authentication with email and password
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
- ### Scenario 3: Ask questions about your codebase
110
+ ## Configuration
104
111
 
105
112
  ```bash
106
113
  $ openclacky
107
- # > How does the payment module work?
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. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/clacky-ai/open-clacky/blob/main/CODE_OF_CONDUCT.md).
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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).
@@ -31,7 +31,7 @@ module Clacky
31
31
  result.merge!(hook_result) if hook_result.is_a?(Hash)
32
32
  rescue StandardError => e
33
33
  # Log error but don't fail
34
- warn "Hook error in #{event}: #{e.message}"
34
+ Clacky::Logger.error("Hook error", event: event, error: e)
35
35
  end
36
36
  end
37
37
 
@@ -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
- [{ role: "assistant", content: content }]
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