openclacky 0.9.37 → 1.0.0.beta.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 951665db04cf6c2a4f8ef9b5c0555f4df91b84af08f63d21097d97cbd64d44b2
4
- data.tar.gz: 82457a522007f54ecd5fcc36fee8e79cf16a65996dd9b95d7a00fd0dcfbc2cac
3
+ metadata.gz: b0938f51d788566d1c708a0054a3b45565f0f3b2ea7cd68f9e18897247c745cd
4
+ data.tar.gz: 96bef1e4b6333fcee57e26fb330f12d7d5b9f79f470a34f35483ab5f51571e64
5
5
  SHA512:
6
- metadata.gz: 031b1031a702aca3a7cee36ea8ba23bc4982e95f8381e59d07ecb652432f6bc8a40e9a386e4a254d640f18f0088d9e18fa1f6434d92c038844f4821cef40a703
7
- data.tar.gz: 5c5c36517efebb37a7a44d0531e0baadedb8600319682c39696a971771852019f2649386e15a3505d9ae32e86cf8c5818c64cf36943247220a7db6e0df24e994
6
+ metadata.gz: 59ba5927fef6187d4d862f210282190b589838e4950b0a5bf0e37731e6cd772bba0d4bfd483d6ff14f979eabf7a4dc09abacb2c60f98df7762af6b4191dc67cf
7
+ data.tar.gz: 52063a91b64281d4dcf3e6bd8e3f4d87cc63cfed70169c87cfacf55e4f6c1cb16f2d938a9f554f564829e879755573d3eb8fb3988260285d11087dfa0058473d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ 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.1] - 2026-04-26
11
+
12
+ ### Added
13
+ - **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.
14
+ - **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.
15
+ - **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.
16
+ - **Usage telemetry.** Anonymous usage data collection helps us understand how the product is used and prioritize improvements. No personal or conversation data is collected.
17
+ - **Brand configuration auto-refresh.** White-label brand settings now refresh automatically when the WebUI starts up, no manual restart needed.
18
+
19
+ ### Improved
20
+ - **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.
21
+ - **Todo manager tool upgrades.** Batch add/remove multiple todos at once, and completed todos auto-clear when you add new ones.
22
+ - **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.
23
+
24
+ ### Fixed
25
+ - **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.
26
+ - **MiniMax → DeepSeek switch error.** Switching models from MiniMax to DeepSeek no longer fails due to mismatched message format handling.
27
+ - **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.
28
+ - **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.
29
+ - **Double-render regression.** An output buffer lifecycle bug that occasionally caused duplicate content in the terminal UI has been fixed.
30
+ - **DeepSeek V4 message content extraction.** Compression no longer mishandles DeepSeek V4's user message content format.
31
+
32
+ ## [0.9.38] - 2026-04-24
33
+
34
+ ### Fixed
35
+ - **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.
36
+
37
+ ### More
38
+ - Auth prompt input field now uses `type="password"` while the user is typing (reverts to text after), preventing shoulder-surfing
39
+
10
40
  ## [0.9.37] - 2026-04-24
11
41
 
12
42
  ### 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
- # The LLM decides:
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 (avoids trivial tasks like commits)
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
- # Inject memory update prompt into @messages so the main agent loop handles it.
36
- # Builds the prompt dynamically, injecting the current memory file list so the
37
- # LLM doesn't need to scan the directory itself.
38
- # Returns true if prompt was injected, false otherwise.
39
- def inject_memory_prompt!
40
- return false unless should_update_memory?
41
- return false if @memory_prompt_injected
42
-
43
- @memory_prompt_injected = true
44
- @memory_updating = true
45
- @ui&.show_progress("Updating long-term memory…")
46
-
47
- @history.append({
48
- role: "user",
49
- content: build_memory_update_prompt,
50
- system_injected: true,
51
- memory_update: true
52
- })
53
-
54
- true
55
- end
56
-
57
- # Clean up memory update messages from conversation history after loop ends.
58
- # Call this once after the main loop finishes.
59
- def cleanup_memory_messages
60
- return unless @memory_prompt_injected
61
-
62
- @history.delete_where { |m| m[:memory_update] }
63
- @memory_prompt_injected = false
64
- @memory_updating = false
65
- @ui&.show_progress(phase: "done")
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 assistant message
129
- # Keep the <summary> tags as they provide semantic context
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
- [{ role: "assistant", content: content_without_topics, compressed_summary: true, chunk_path: chunk_path }]
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
- # Insert compression message
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
- begin
39
- # Execute compression using shared LLM call logic
40
- response = call_llm
41
- handle_compression_response(response, compression_context)
42
- true
43
- rescue Clacky::AgentInterrupted => e
44
- @ui&.log("Idle compression canceled: #{e.message}", level: :info)
45
- @history.rollback_before(compression_message)
46
- false
47
- rescue => e
48
- @ui&.log("Idle compression failed: #{e.message}", level: :error)
49
- @history.rollback_before(compression_message)
50
- false
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
- def handle_compression_response(response, compression_context)
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
- @ui&.show_progress(compression_summary, progress_type: "idle_compress", phase: "done")
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.
@@ -93,13 +93,22 @@ module Clacky
93
93
  # Filter messages to only show tasks up to active_task_id.
94
94
  # This hides "future" messages when user has undone.
95
95
  # Returns API-ready array (strips internal fields + handles orphaned tool_calls).
96
+ # @param force_reasoning_content_pad [Boolean] forwarded to MessageHistory,
97
+ # enables one-shot pad-and-retry for thinking-mode providers that
98
+ # require reasoning_content on every assistant message.
96
99
  # Made public for testing
97
- def active_messages
98
- return @history.to_api if @active_task_id == @current_task_id
100
+ def active_messages(force_reasoning_content_pad: false)
101
+ if @active_task_id == @current_task_id
102
+ return @history.to_api(force_reasoning_content_pad: force_reasoning_content_pad)
103
+ end
99
104
 
100
- @history.for_task(@active_task_id).map do |msg|
105
+ stripped = @history.for_task(@active_task_id).map do |msg|
101
106
  msg.reject { |k, _| MessageHistory::INTERNAL_FIELDS.include?(k) }
102
107
  end
108
+ # Apply the same reasoning_content padding rule used by to_api so
109
+ # Time Machine replays satisfy thinking-mode providers after a
110
+ # 400 retry.
111
+ MessageHistory.pad_reasoning_content_if_needed(stripped, force: force_reasoning_content_pad)
103
112
  end
104
113
 
105
114
  # Undo to parent task
@@ -10,9 +10,6 @@ module Clacky
10
10
  # @param tool_params [Hash, String] Tool parameters
11
11
  # @return [Boolean] true if should auto-execute
12
12
  def should_auto_execute?(tool_name, tool_params = {})
13
- # During memory update phase, always auto-execute (no user confirmation needed)
14
- return true if @memory_updating
15
-
16
13
  case @config.permission_mode
17
14
  when :auto_approve, :confirm_all
18
15
  # Both modes auto-execute all file/shell tools without confirmation.