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.expand_path("~/.clacky/memories")

Instance Method Summary collapse

Instance Method Details

#run_memory_update_subagentObject

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.permission_mode = :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.message,
      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&.update_sessionbar(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).

Returns:

  • (Boolean)


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