Module: Clacky::Agent::MemoryUpdater
- Included in:
- Clacky::Agent
- Defined in:
- lib/clacky/agent/memory_updater.rb
Overview
Long-term memory update functionality.
Runs at the end of a qualifying task to persist important knowledge into ~/.clacky/memories/. The LLM decides:
- Which topics were discussed
- Which memory files to update or create
- How to merge new info with existing content
- What to drop to stay within the per-file token limit
Architecture:
Memory update runs as a **forked subagent**, NOT inline in the
main agent's loop. The subagent inherits the main agent's history
(so it can see what happened) via +fork_subagent+'s standard
deep-clone, and inherits the same model/tools so prompt-cache is
reused maximally. The subagent runs synchronously; when it returns,
the main agent prints +show_complete+.
This gives us, structurally:
- Clean main-agent history (no memory_update messages to clean up)
- Correct visual ordering ([OK] Task Complete is the LAST thing
printed — the memory-update progress finishes before it)
- Independent cost accounting (task cost vs. memory update cost)
- Natural recursion guard (+@is_subagent+ blocks re-entry)
Trigger condition:
- Iteration count >= MEMORY_UPDATE_MIN_ITERATIONS (skip trivial tasks)
- Not already a subagent (no recursion)
- Memory update is enabled in config
Constant Summary collapse
- MEMORY_UPDATE_MIN_ITERATIONS =
Minimum LLM iterations for this task before triggering memory update. Set high enough to skip short utility tasks (commit, deploy, etc.)
10- MEMORIES_DIR =
File.("~/.clacky/memories")
Instance Method Summary collapse
-
#run_memory_update_subagent ⇒ Object
Run memory update as a forked subagent.
-
#should_update_memory? ⇒ Boolean
Check if memory update should be triggered for this task.
Instance Method Details
#run_memory_update_subagent ⇒ Object
Run memory update as a forked subagent.
This is called by Agent#run on the success path, AFTER the main loop exits and BEFORE show_complete is printed. It blocks until the subagent finishes, so the visual order is structurally correct:
... task output ...
[progress] Updating long-term memory… (spinner)
[progress finishes]
[OK] Task Complete
Safe to call unconditionally; returns early if preconditions fail. Never raises for “no update needed” — only propagates genuine errors (Clacky::AgentInterrupted for Ctrl+C, other exceptions are caught and logged so memory-update failures never mask the parent task’s result).
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/clacky/agent/memory_updater.rb', line 68 def run_memory_update_subagent return unless should_update_memory? handle = @ui&.start_progress(message: "Updating long-term memory…", style: :primary) # Fork subagent inheriting main agent's model, tools, and history. # Maximizes prompt-cache reuse: same model, same tool set, same # cloned history — only the +system_prompt_suffix+ (the memory # update instructions) and the final "Please proceed." user turn # are new, landing on top of a warm cache. subagent = fork_subagent(system_prompt_suffix: build_memory_update_prompt) # Memory update is a background consolidation task — never prompt # the user for confirmation on memory file writes. The subagent # has its own config copy (fork_subagent does deep_copy), so this # doesn't affect the parent. sub_config = subagent.instance_variable_get(:@config) sub_config. = :auto_approve if sub_config.respond_to?(:permission_mode=) begin result = subagent.run("Please proceed.") rescue Clacky::AgentInterrupted # User pressed Ctrl+C during memory update. Propagate so the # parent agent's interrupt handler runs. raise rescue StandardError => e # Memory update failures are NEVER fatal to the parent task. # Log and move on — the user's actual work is already done. @debug_logs << { timestamp: Time.now.iso8601, event: "memory_update_error", error_class: e.class.name, error_message: e., backtrace: e.backtrace&.first(10) } Clacky::Logger.error("memory_update_error", error: e) return ensure handle&.finish end return unless result # Merge subagent cost into parent's cumulative session spend so the # sessionbar shows the real total. The parent's task-complete cost # (result[:total_cost_usd] in Agent#run) stays unaffected — it # still reflects ONLY the user's task, not the memory update. subagent_cost = result[:total_cost_usd] || 0.0 @total_cost += subagent_cost @ui&.(cost: @total_cost, cost_source: @cost_source) # Only surface a completion info line if the subagent actually # wrote something to memory. The common "No memory updates needed." # path stays silent to avoid visual noise. if subagent_wrote_memory?(subagent) @ui&.show_info("Memory updated: #{result[:iterations]} iterations, $#{subagent_cost.round(4)}") end end |
#should_update_memory? ⇒ Boolean
Check if memory update should be triggered for this task. Only triggers when the task had enough LLM iterations, skipping short utility tasks (e.g. commit, deploy).
44 45 46 47 48 49 50 |
# File 'lib/clacky/agent/memory_updater.rb', line 44 def should_update_memory? return false unless memory_update_enabled? return false if @is_subagent # Subagents never update memory task_iterations = @iterations - (@task_start_iterations || 0) task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS end |