bone-agent 1.3.1 → 1.3.3

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 (66) hide show
  1. package/README.md +2 -2
  2. package/config.yaml.example +8 -0
  3. package/package.json +3 -2
  4. package/prompts/main/ask_questions.md +31 -0
  5. package/prompts/main/batch_independent_calls.md +5 -0
  6. package/prompts/main/casual_interactions.md +11 -0
  7. package/prompts/main/code_references.md +8 -0
  8. package/prompts/main/communication_style.md +12 -0
  9. package/prompts/main/context_reliability.md +12 -0
  10. package/prompts/main/conversational_tool_calling.md +15 -0
  11. package/prompts/main/dream.md +36 -0
  12. package/prompts/main/editing_pattern.md +13 -0
  13. package/prompts/main/error_handling.md +6 -0
  14. package/prompts/main/exploration_pattern.md +21 -0
  15. package/prompts/main/intro.md +1 -0
  16. package/prompts/main/obsidian.md +16 -0
  17. package/prompts/main/obsidian_project.md +79 -0
  18. package/prompts/main/professional_objectivity.md +3 -0
  19. package/prompts/main/targeted_searching.md +10 -0
  20. package/prompts/main/task_lists_pattern.md +8 -0
  21. package/prompts/main/temp_folder.md +9 -0
  22. package/prompts/main/think_before_acting.md +10 -0
  23. package/prompts/main/tone_and_style.md +4 -0
  24. package/prompts/main/tool_preferences.md +24 -0
  25. package/prompts/main/trust_subagent_context.md +21 -0
  26. package/prompts/main/when_to_use_sub_agent.md +7 -0
  27. package/prompts/micro/ask_questions.md +1 -0
  28. package/prompts/micro/batch_independent_calls.md +1 -0
  29. package/prompts/micro/casual_interactions.md +1 -0
  30. package/prompts/micro/code_references.md +1 -0
  31. package/prompts/micro/communication_style.md +1 -0
  32. package/prompts/micro/context_reliability.md +1 -0
  33. package/prompts/micro/conversational_tool_calling.md +1 -0
  34. package/prompts/micro/editing_pattern.md +1 -0
  35. package/prompts/micro/error_handling.md +1 -0
  36. package/prompts/micro/exploration_pattern.md +1 -0
  37. package/prompts/micro/intro.md +1 -0
  38. package/prompts/micro/obsidian.md +4 -0
  39. package/prompts/micro/obsidian_project.md +5 -0
  40. package/prompts/micro/professional_objectivity.md +1 -0
  41. package/prompts/micro/targeted_searching.md +1 -0
  42. package/prompts/micro/task_lists_pattern.md +1 -0
  43. package/prompts/micro/temp_folder.md +1 -0
  44. package/prompts/micro/think_before_acting.md +5 -0
  45. package/prompts/micro/tone_and_style.md +1 -0
  46. package/prompts/micro/tool_preferences.md +1 -0
  47. package/prompts/micro/trust_subagent_context.md +1 -0
  48. package/prompts/micro/when_to_use_sub_agent.md +1 -0
  49. package/src/core/agentic.py +9 -78
  50. package/src/core/chat_manager.py +120 -108
  51. package/src/core/config_manager.py +6 -0
  52. package/src/core/cron.py +57 -2
  53. package/src/core/memory.py +3 -90
  54. package/src/llm/config.py +28 -2
  55. package/src/llm/prompts.py +251 -497
  56. package/src/llm/providers.py +25 -6
  57. package/src/llm/token_tracker.py +17 -1
  58. package/src/tools/edit.py +8 -6
  59. package/src/tools/helpers/path_resolver.py +18 -12
  60. package/src/tools/rg_search.py +97 -30
  61. package/src/tools/select_option.py +12 -5
  62. package/src/ui/commands.py +120 -5
  63. package/src/ui/displays.py +1 -0
  64. package/src/ui/main.py +1 -0
  65. package/src/utils/settings.py +19 -2
  66. package/src/utils/user_message_logger.py +120 -0
@@ -2,7 +2,6 @@
2
2
 
3
3
  import json
4
4
  import logging
5
- import time
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
@@ -172,63 +171,6 @@ class AgenticOrchestrator:
172
171
  # Check if we're in a parallel context with suppressed console
173
172
  return self._parallel_context.get('console', self.console)
174
173
 
175
- def _is_memory_file(self, path: str) -> bool:
176
- """Check if path targets a memory file (auto-approved).
177
-
178
- Auto-approve scope (restricted to known memory paths):
179
- - {repo_root}/.bone/agents.md — project memory
180
- - ~/.bone/user_memory.md — global user memory
181
- - Any file under {repo_root}/.bone/ — project memory directory
182
-
183
- Args:
184
- path: File path from tool arguments.
185
-
186
- Returns:
187
- True if the file should be auto-approved as a memory file.
188
- """
189
- p = Path(path).resolve()
190
- repo_root = Path(self.repo_root).resolve()
191
- # Known memory paths
192
- if p == Path.home() / ".bone" / "user_memory.md":
193
- return True
194
- # Any file under {repo_root}/.bone/ (future memory files)
195
- bone_dir = repo_root / ".bone"
196
- if p.is_relative_to(bone_dir):
197
- return True
198
- return False
199
-
200
- def _execute_memory_edit(self, arguments) -> bool:
201
- """Apply a memory file edit synchronously with one retry on failure.
202
-
203
- Args:
204
- arguments: Tool arguments dict (path, search, replace, etc.)
205
- """
206
- from tools.edit import _execute_edit_file
207
-
208
- kwargs = dict(
209
- path=arguments.get("path"),
210
- search=arguments.get("search"),
211
- replace=arguments.get("replace"),
212
- repo_root=self.repo_root,
213
- console=None, # silent — no output in chat
214
- gitignore_spec=self.gitignore_spec,
215
- context_lines=arguments.get("context_lines", 3),
216
- vault_root=vault_root_str(),
217
- )
218
- try:
219
- _execute_edit_file(**kwargs)
220
- return True
221
- except Exception as e:
222
- logger.warning("Memory edit failed (retrying in 0.5s): %s", e)
223
- time.sleep(0.5)
224
- try:
225
- _execute_edit_file(**kwargs)
226
- logger.info("Memory edit retry succeeded after initial failure.")
227
- return True
228
- except Exception as e2:
229
- logger.error("Memory edit failed after retry: %s", e2)
230
- return False
231
-
232
174
  def run(self, user_input, thinking_indicator=None, allowed_tools=None):
233
175
  """Main orchestration loop.
234
176
 
@@ -380,7 +322,7 @@ class AgenticOrchestrator:
380
322
  self.chat_manager.log_message(response)
381
323
 
382
324
  # NEW: Compact tool results after final answer (per-message compaction)
383
- self.chat_manager.compact_tool_results()
325
+ self.chat_manager.compact_tool_results(skip_token_update=True)
384
326
 
385
327
  # Update context tokens with current mode's tools
386
328
  tools_for_mode = TOOLS()
@@ -597,9 +539,8 @@ class AgenticOrchestrator:
597
539
  # Log tool result
598
540
  self.chat_manager.log_message(tool_msg)
599
541
 
600
- # Mid-loop compaction: compact older completed tool blocks
601
- # after each tool result is appended (safe — only compacts completed blocks)
602
- self.chat_manager.compact_tool_results()
542
+ # Compact completed tool blocks once after all tools complete
543
+ self.chat_manager.compact_tool_results(skip_token_update=True)
603
544
 
604
545
  # Update context tokens with current mode's tools
605
546
  tools_for_mode = TOOLS()
@@ -840,7 +781,7 @@ class AgenticOrchestrator:
840
781
 
841
782
  # Mid-loop compaction: compact older completed tool blocks
842
783
  # after all parallel results are appended (safe — only compacts completed blocks)
843
- self.chat_manager.compact_tool_results()
784
+ self.chat_manager.compact_tool_results(skip_token_update=True)
844
785
 
845
786
  # Update context tokens with current mode's tools
846
787
  tools_for_mode = TOOLS()
@@ -909,26 +850,12 @@ class AgenticOrchestrator:
909
850
 
910
851
  # Check if tool requires approval
911
852
  if tool.requires_approval:
912
- # For edit_file: check memory file auto-approve first
853
+ # For edit_file: validate path then request approval
913
854
  if function_name == "edit_file":
914
855
  edit_path = arguments.get("path", "")
915
856
  if not edit_path:
916
857
  return False, "Error: path is required for edit_file."
917
858
 
918
- # Memory file: auto-approve, fire-and-forget
919
- if self._is_memory_file(edit_path):
920
- # Generate preview to validate the edit (reuses existing logic)
921
- result = tool.execute(arguments, context)
922
- preview, is_valid = resolve_edit_preview(result)
923
- if is_valid:
924
- ok = self._execute_memory_edit(arguments)
925
- if self.debug_mode:
926
- console = self._get_console()
927
- if console:
928
- console.print(f"[dim]Memory edit auto-approved: {edit_path}[/dim]")
929
- return False, "Memory saved." if ok else f"Memory edit failed: {edit_path}"
930
- return False, str(result)
931
-
932
859
  # Normal edit: generate preview and request approval
933
860
  result = tool.execute(arguments, context)
934
861
 
@@ -1023,6 +950,10 @@ class AgenticOrchestrator:
1023
950
 
1024
951
  return False, str(result)
1025
952
  except Exception as e:
953
+ # If thinking_indicator was paused (TERMINAL_YIELD) and tool
954
+ # raised, resume it so the spinner reappears for the next iteration
955
+ if policy == TERMINAL_YIELD and thinking_indicator:
956
+ thinking_indicator.resume()
1026
957
  return False, f"Error executing tool '{function_name}': {str(e)}"
1027
958
 
1028
959
  return False, f"Error: Unknown tool '{function_name}'."
@@ -15,6 +15,7 @@ from pathlib import Path
15
15
  from llm.token_tracker import TokenTracker
16
16
  from utils.settings import server_settings, context_settings
17
17
  from utils.logger import MarkdownConversationLogger
18
+ from utils.user_message_logger import UserMessageLogger
18
19
  from utils.result_parsers import extract_exit_code, extract_metadata_from_result
19
20
 
20
21
  # Token counting constants
@@ -62,6 +63,9 @@ class ChatManager:
62
63
  conversations_dir=context_settings.conversations_dir
63
64
  )
64
65
 
66
+ # User message logging (always on, for dream memory system)
67
+ self.user_message_logger = UserMessageLogger()
68
+
65
69
  # Compaction lock: prevents compaction during active tool execution
66
70
  # Set by agentic.py before executing tools, cleared after all results appended
67
71
  self._compaction_locked = False
@@ -119,20 +123,37 @@ class ChatManager:
119
123
  self._update_context_tokens()
120
124
  self.context_token_estimate = self.token_tracker.current_context_tokens
121
125
 
122
- def _build_system_prompt(self) -> str:
123
- """Build system prompt."""
124
- return build_system_prompt()
126
+ def _build_system_prompt(self, variant: str | None = None) -> str:
127
+ """Build system prompt.
128
+
129
+ Args:
130
+ variant: Prompt variant name (e.g. 'main', 'micro').
131
+ If None, reads from prompt_settings.
132
+ """
133
+ if variant is None:
134
+ from utils.settings import prompt_settings
135
+ variant = prompt_settings.variant
136
+ return build_system_prompt(variant)
137
+
138
+ def update_system_prompt(self, variant: str | None = None):
139
+ """Rebuild system prompt in-place (e.g. after hotswap or session reset).
125
140
 
126
- def update_system_prompt(self):
127
- """Rebuild system prompt (e.g. after session reset)."""
141
+ Args:
142
+ variant: Prompt variant to use. If None, keeps current variant.
143
+ Updates token_tracker.current_variant.
144
+ """
128
145
  if not self.messages:
129
146
  raise RuntimeError("Cannot update system prompt: messages array is empty")
130
147
 
131
148
  if self.messages[0]["role"] != "system":
132
149
  raise RuntimeError(f"Cannot update system prompt: messages[0] has role '{self.messages[0]['role']}', expected 'system'")
133
150
 
134
- # Update the system message with current mode
135
- self.messages[0]["content"] = self._build_system_prompt()
151
+ if variant is None:
152
+ from utils.settings import prompt_settings
153
+ variant = prompt_settings.variant
154
+
155
+ self.messages[0]["content"] = self._build_system_prompt(variant)
156
+ self.token_tracker.current_variant = variant
136
157
  self._update_context_tokens()
137
158
 
138
159
  def _load_agents_md(self) -> tuple[str, str]:
@@ -383,7 +404,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
383
404
 
384
405
  # ===== Tool Result Compaction =====
385
406
 
386
- def _find_tool_blocks(self):
407
+ def _find_tool_blocks(self, include_in_flight=False):
387
408
  """Find all tool-result blocks in message history.
388
409
 
389
410
  Handles both single-turn and multi-turn tool chains:
@@ -394,6 +415,12 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
394
415
  a single block spanning from the first assistant(tool_calls) to the
395
416
  final assistant(answer).
396
417
 
418
+ Args:
419
+ include_in_flight: If True, also return blocks that lack a final
420
+ assistant answer (in-flight tool chains). The 'end' field points
421
+ to the index after the last message in the chain (or the breaking
422
+ message index if the chain was interrupted).
423
+
397
424
  Returns:
398
425
  list: List of block dicts with keys: user_idx, start, end, tool_calls, tool_results
399
426
  """
@@ -441,14 +468,25 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
441
468
  # Non-tool, non-assistant message breaks the chain
442
469
  break
443
470
 
444
- if found_end and all_tool_calls:
445
- blocks.append({
446
- 'user_idx': user_idx,
447
- 'start': block_start,
448
- 'end': j,
449
- 'tool_calls': all_tool_calls,
450
- 'tool_results': all_tool_results
451
- })
471
+ if include_in_flight:
472
+ if all_tool_calls:
473
+ blocks.append({
474
+ 'user_idx': user_idx,
475
+ 'start': block_start,
476
+ 'end': j,
477
+ 'tool_calls': all_tool_calls,
478
+ 'tool_results': all_tool_results,
479
+ 'in_flight': not found_end,
480
+ })
481
+ else:
482
+ if found_end and all_tool_calls:
483
+ blocks.append({
484
+ 'user_idx': user_idx,
485
+ 'start': block_start,
486
+ 'end': j,
487
+ 'tool_calls': all_tool_calls,
488
+ 'tool_results': all_tool_results,
489
+ })
452
490
 
453
491
  # Continue scanning from after the final answer (or after the chain)
454
492
  # Guard: always advance at least one position to prevent infinite loops
@@ -635,68 +673,21 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
635
673
  def _find_in_flight_boundary(self):
636
674
  """Find the index where in-flight tool blocks begin.
637
675
 
638
- Scans from the end of messages for any assistant message with tool_calls
639
- that does NOT have a corresponding final assistant answer after it.
676
+ Delegates to _find_tool_blocks(include_in_flight=True) to find all
677
+ blocks, then returns the earliest start of any in-flight block.
640
678
  These messages must never be included in the compactable region.
641
679
 
642
680
  Returns:
643
681
  int: Index of the first in-flight message, or len(messages) if none.
644
682
  """
645
- n = len(self.messages)
646
- i = n - 1
647
-
648
- # Walk backward looking for the pattern: ...assistant(tool_calls) tool_results...
649
- # without a final assistant(answer) after the tool results.
650
- while i >= 0:
651
- msg = self.messages[i]
652
- if msg.get('role') == 'assistant' and msg.get('tool_calls'):
653
- # Found an assistant with tool_calls. Check if there's a final
654
- # answer (assistant without tool_calls) after it.
655
- has_final_answer = False
656
- j = i + 1
657
- while j < n:
658
- if self.messages[j].get('role') == 'assistant' and not self.messages[j].get('tool_calls'):
659
- has_final_answer = True
660
- break
661
- elif self.messages[j].get('role') == 'assistant' and self.messages[j].get('tool_calls'):
662
- # Another tool-calling assistant — skip over its tool results
663
- j += 1
664
- while j < n and self.messages[j].get('role') == 'tool':
665
- j += 1
666
- continue
667
- elif self.messages[j].get('role') == 'tool':
668
- j += 1
669
- continue
670
- else:
671
- break
672
-
673
- if not has_final_answer:
674
- # This is an in-flight tool block. Find its user question.
675
- user_idx = i - 1
676
- while user_idx >= 0 and self.messages[user_idx].get('role') != 'user':
677
- user_idx -= 1
678
- return max(0, user_idx)
679
- else:
680
- # Completed block — continue scanning backward
681
- # Skip past all the tool messages associated with this block
682
- j = i + 1
683
- while j < n:
684
- if self.messages[j].get('role') == 'tool':
685
- j += 1
686
- elif self.messages[j].get('role') == 'assistant' and self.messages[j].get('tool_calls'):
687
- j += 1
688
- while j < n and self.messages[j].get('role') == 'tool':
689
- j += 1
690
- continue
691
- else:
692
- break
693
- i = j - 1
694
- else:
695
- i -= 1
696
-
697
- return n
698
-
699
- def _compute_split_boundary(self, blocks, in_flight_start):
683
+ all_blocks = self._find_tool_blocks(include_in_flight=True)
684
+ in_flight = [b for b in all_blocks if b.get('in_flight')]
685
+ if in_flight:
686
+ return min(b['user_idx'] for b in in_flight)
687
+ return len(self.messages)
688
+
689
+ def _compute_split_boundary(self, blocks, in_flight_start,
690
+ uncompacted_tail_tokens=None, min_tool_blocks=None):
700
691
  """Compute the message index where the uncompacted tail begins.
701
692
 
702
693
  Three constraints determine the boundary (take the most conservative /
@@ -709,19 +700,23 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
709
700
  Args:
710
701
  blocks: List of tool block dicts from _find_tool_blocks()
711
702
  in_flight_start: Index of first in-flight message (from _find_in_flight_boundary)
703
+ uncompacted_tail_tokens: Override for the token budget (None = use settings)
704
+ min_tool_blocks: Override for minimum tool blocks to preserve (None = use settings)
712
705
 
713
706
  Returns:
714
707
  int: Message index where the uncompacted tail starts
715
708
  """
716
709
  tc = context_settings.tool_compaction
717
- token_budget = tc.uncompacted_tail_tokens
718
- min_blocks = tc.min_tool_blocks
710
+ token_budget = uncompacted_tail_tokens if uncompacted_tail_tokens is not None else tc.uncompacted_tail_tokens
711
+ min_blocks = min_tool_blocks if min_tool_blocks is not None else tc.min_tool_blocks
719
712
  n = len(self.messages)
720
713
 
721
714
  # The verbatim region ends at the first in-flight message (exclusive)
722
715
  verbatim_end = min(in_flight_start, n)
723
716
 
724
- # Constraint 1: Token budget — walk from verbatim_end backward
717
+ # Constraint 1: Token budget — walk from verbatim_end backward.
718
+ # Note: range stops at 1 (not 0) so the system prompt is never counted
719
+ # toward the budget — it is always preserved uncompacted.
725
720
  tokens_accumulated = 0
726
721
  token_boundary = 0
727
722
  for i in range(verbatim_end - 1, 0, -1):
@@ -734,25 +729,16 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
734
729
  token_boundary = 1
735
730
 
736
731
  # Constraint 2: Minimum tool blocks — ensure at least min_blocks completed
737
- # blocks are within the verbatim tail
732
+ # blocks are within the uncompacted tail. Take the min_blocks most recent
733
+ # completed blocks and set the boundary so they all fall at or after it.
738
734
  min_block_boundary = 1
739
- completed_blocks_in_tail = []
740
- for block in blocks:
741
- # A block is in the tail if its end is within the verbatim region
742
- if block['end'] < verbatim_end:
743
- completed_blocks_in_tail.append(block)
744
-
745
- if len(completed_blocks_in_tail) < min_blocks and len(completed_blocks_in_tail) < len(blocks):
746
- # Need to extend backward to include more blocks
747
- blocks_needed = min_blocks - len(completed_blocks_in_tail)
748
- # Take the blocks immediately before the current tail
749
- # Find blocks whose end < token_boundary (not already in tail)
750
- earlier_blocks = [b for b in blocks if b['end'] < token_boundary]
751
- # Sort by end index descending (most recent first)
752
- earlier_blocks.sort(key=lambda b: b['end'], reverse=True)
753
- # Extend boundary to include the earliest user_idx of the blocks we need
754
- for b in earlier_blocks[:blocks_needed]:
755
- min_block_boundary = min(min_block_boundary, b['user_idx'])
735
+ if min_blocks > 0 and len(blocks) >= min_blocks:
736
+ # Sort by end index descending (most recent first), take top min_blocks
737
+ sorted_blocks = sorted(blocks, key=lambda b: b['end'], reverse=True)
738
+ recent_blocks = sorted_blocks[:min_blocks]
739
+ # The boundary must be at or before the earliest user_idx of these blocks
740
+ # so that all of them satisfy user_idx >= boundary (i.e. block is fully in the tail)
741
+ min_block_boundary = min(b['user_idx'] for b in recent_blocks)
756
742
 
757
743
  # Constraint 3: Tool-call integrity — if token_boundary lands inside a
758
744
  # tool block, extend backward to include the complete block
@@ -763,13 +749,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
763
749
  integrity_boundary = min(integrity_boundary, block['user_idx'])
764
750
 
765
751
  # Take the most conservative (earliest) boundary
766
- boundary = min(token_boundary, integrity_boundary)
752
+ # integrity_boundary <= token_boundary always (starts equal, only decreases)
753
+ boundary = integrity_boundary
767
754
  if min_block_boundary < boundary:
768
755
  boundary = min_block_boundary
769
756
 
770
757
  return boundary
771
758
 
772
- def compact_tool_results(self):
759
+ def compact_tool_results(self, skip_token_update=False,
760
+ uncompacted_tail_tokens=None, min_tool_blocks=None):
773
761
  """Replace completed tool-result blocks with summaries using token-budget tail.
774
762
 
775
763
  Walks messages from the end, accumulating tokens until ~40k tokens are
@@ -779,6 +767,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
779
767
 
780
768
  Safe to call mid-loop (during tool execution) because it only compacts
781
769
  completed tool blocks — in-flight blocks are never touched.
770
+
771
+ Args:
772
+ skip_token_update: If True, skip the internal _update_context_tokens()
773
+ call. Use when the caller will update tokens with mode-specific
774
+ tools immediately after.
775
+ uncompacted_tail_tokens: Override for the token budget (None = use settings).
776
+ Use for aggressive compaction with a smaller tail.
777
+ min_tool_blocks: Override for minimum tool blocks to preserve (None = use settings).
778
+ Use for aggressive compaction with fewer preserved blocks.
782
779
  """
783
780
  # Skip if disabled (e.g. sub-agents preserving findings)
784
781
  if self._compaction_disabled:
@@ -801,7 +798,11 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
801
798
  in_flight_start = self._find_in_flight_boundary()
802
799
 
803
800
  # Compute the split boundary using token budget + constraints
804
- split_boundary = self._compute_split_boundary(blocks, in_flight_start)
801
+ split_boundary = self._compute_split_boundary(
802
+ blocks, in_flight_start,
803
+ uncompacted_tail_tokens=uncompacted_tail_tokens,
804
+ min_tool_blocks=min_tool_blocks,
805
+ )
805
806
 
806
807
  # Determine which blocks fall entirely before the split boundary
807
808
  # (those are the ones to compact)
@@ -864,7 +865,8 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
864
865
  new_messages.append(msg)
865
866
 
866
867
  self.messages = new_messages
867
- self._update_context_tokens()
868
+ if not skip_token_update:
869
+ self._update_context_tokens()
868
870
 
869
871
  # ===== AI-Based History Compaction =====
870
872
 
@@ -1101,16 +1103,12 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1101
1103
  # If compaction is NOT locked, try layers 1 and 2
1102
1104
  if not self._compaction_locked:
1103
1105
  # Layer 1: Aggressive tool result compaction (non-LLM, fast)
1104
- # Temporarily use very small token budget and min blocks for aggressive compaction
1105
- original_tail_tokens = context_settings.tool_compaction.uncompacted_tail_tokens
1106
- original_min_blocks = context_settings.tool_compaction.min_tool_blocks
1107
- try:
1108
- context_settings.tool_compaction.uncompacted_tail_tokens = 10_000
1109
- context_settings.tool_compaction.min_tool_blocks = 1
1110
- self.compact_tool_results()
1111
- finally:
1112
- context_settings.tool_compaction.uncompacted_tail_tokens = original_tail_tokens
1113
- context_settings.tool_compaction.min_tool_blocks = original_min_blocks
1106
+ # Use very small token budget and min blocks for aggressive compaction
1107
+ self.compact_tool_results(
1108
+ skip_token_update=True,
1109
+ uncompacted_tail_tokens=10_000,
1110
+ min_tool_blocks=1,
1111
+ )
1114
1112
 
1115
1113
  self._update_context_tokens()
1116
1114
  current_tokens = self.token_tracker.current_context_tokens
@@ -1399,6 +1397,10 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1399
1397
  server_path,
1400
1398
  "-m", model_path,
1401
1399
  "-ngl", str(server_settings.ngl_layers),
1400
+ "--threads", str(server_settings.threads),
1401
+ "--batch-size", str(server_settings.batch_size),
1402
+ "--ubatch-size", str(server_settings.ubatch_size),
1403
+ "--flash-attn" if server_settings.flash_attn else "--no-flash-attn",
1402
1404
  "--split-mode", "none",
1403
1405
  "--ctx-size", str(server_settings.ctx_size),
1404
1406
  "--n-predict", str(server_settings.n_predict),
@@ -1406,6 +1408,7 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1406
1408
  "--host", host,
1407
1409
  "--port", str(port),
1408
1410
  "--jinja",
1411
+ "--reasoning", "off",
1409
1412
  ]
1410
1413
 
1411
1414
  # Restrict to RTX 5070 Ti only (GPU 0)
@@ -1482,6 +1485,15 @@ Provide a concise summary (2-4 paragraphs) that captures all essential context f
1482
1485
  if self.markdown_logger:
1483
1486
  self.markdown_logger.log_message(message)
1484
1487
 
1488
+ # Log user messages to JSONL for dream memory processing (only if memory enabled)
1489
+ if message.get("role") == "user" and message.get("content"):
1490
+ from llm.config import MEMORY_SETTINGS
1491
+ if MEMORY_SETTINGS.get("enabled", True):
1492
+ self.user_message_logger.log_user_message(
1493
+ message["content"],
1494
+ project_dir=Path.cwd().resolve(),
1495
+ )
1496
+
1485
1497
  def sync_log(self):
1486
1498
  """Rewrite the entire conversation log to match current message state.
1487
1499
 
@@ -116,9 +116,11 @@ class ConfigManager:
116
116
  'bone': 'BONE_PROXY_MODEL',
117
117
  'openrouter': 'OPENROUTER_MODEL',
118
118
  'glm': 'GLM_MODEL',
119
+ 'glm_plan': 'GLM_PLAN_MODEL',
119
120
  'openai': 'OPENAI_MODEL',
120
121
  'gemini': 'GEMINI_MODEL',
121
122
  'minimax': 'MINIMAX_MODEL',
123
+ 'minimax_plan': 'MINIMAX_PLAN_MODEL',
122
124
  'anthropic': 'ANTHROPIC_MODEL',
123
125
  'kimi': 'KIMI_MODEL'
124
126
  }
@@ -144,9 +146,11 @@ class ConfigManager:
144
146
  'bone': 'BONE_PROXY_MODEL',
145
147
  'openrouter': 'OPENROUTER_MODEL',
146
148
  'glm': 'GLM_MODEL',
149
+ 'glm_plan': 'GLM_PLAN_MODEL',
147
150
  'openai': 'OPENAI_MODEL',
148
151
  'gemini': 'GEMINI_MODEL',
149
152
  'minimax': 'MINIMAX_MODEL',
153
+ 'minimax_plan': 'MINIMAX_PLAN_MODEL',
150
154
  'anthropic': 'ANTHROPIC_MODEL',
151
155
  'kimi': 'KIMI_MODEL'
152
156
  }
@@ -172,9 +176,11 @@ class ConfigManager:
172
176
  'openrouter': 'OPENROUTER_API_KEY',
173
177
  'bone': 'BONE_PROXY_API_KEY',
174
178
  'glm': 'GLM_API_KEY',
179
+ 'glm_plan': 'GLM_PLAN_API_KEY',
175
180
  'openai': 'OPENAI_API_KEY',
176
181
  'gemini': 'GEMINI_API_KEY',
177
182
  'minimax': 'MINIMAX_API_KEY',
183
+ 'minimax_plan': 'MINIMAX_PLAN_API_KEY',
178
184
  'anthropic': 'ANTHROPIC_API_KEY',
179
185
  'kimi': 'KIMI_API_KEY'
180
186
  }
package/src/core/cron.py CHANGED
@@ -273,6 +273,41 @@ def _write_job_log(job: CronJob, output: str, error: bool):
273
273
  logger.error("Failed to write cron log: %s", e)
274
274
 
275
275
 
276
+ # ── Dream job (auto-seeded) ─────────────────────────────────────────────
277
+
278
+ DREAM_JOB_ID = "dream"
279
+ DREAM_JOB_SCHEDULE = "daily at 4am"
280
+
281
+
282
+ def ensure_dream_job(config: CronConfig) -> None:
283
+ """Sync the dream memory job with the DREAM_SETTINGS.enabled config.
284
+
285
+ - Enabled and missing → seed the job
286
+ - Enabled and present → no-op
287
+ - Disabled and present → remove the job
288
+ - Disabled and missing → no-op
289
+ """
290
+ from utils.settings import dream_settings
291
+ from llm.config import MEMORY_SETTINGS
292
+
293
+ if dream_settings.enabled and MEMORY_SETTINGS.get("enabled", True):
294
+ if DREAM_JOB_ID in config.jobs:
295
+ return
296
+ job = CronJob(
297
+ id=DREAM_JOB_ID,
298
+ schedule=DREAM_JOB_SCHEDULE,
299
+ command="Run the dream memory consolidation process. Read yesterday's user messages from ~/.bone/conversations/, analyze them for preferences and patterns, and consolidate into memory files. Then clean up JSONL files older than 7 days.",
300
+ enabled=True,
301
+ description="Dream memory consolidation — scans user messages and updates memories",
302
+ )
303
+ config.add_job(job)
304
+ logger.info("Seeded dream memory cron job (daily at 4am)")
305
+ else:
306
+ if DREAM_JOB_ID in config.jobs:
307
+ config.remove_job(DREAM_JOB_ID)
308
+ logger.info("Removed dream memory cron job (disabled in config)")
309
+
310
+
276
311
  def run_single_job(job: CronJob, console=None, interactive=False) -> None:
277
312
  """Execute a single cron job without requiring a CronScheduler instance.
278
313
 
@@ -321,10 +356,27 @@ def run_single_job(job: CronJob, console=None, interactive=False) -> None:
321
356
  # Fresh ChatManager for this job
322
357
  chat_manager = ChatManager()
323
358
 
324
- # Build the prompt inject context about cron execution
359
+ # Dream job: auto-approve edits and run cleanup before agent starts
360
+ if job.id == DREAM_JOB_ID:
361
+ chat_manager.approve_mode = "accept_edits"
362
+ from utils.user_message_logger import UserMessageLogger
363
+ removed = UserMessageLogger.cleanup_old_files()
364
+ if removed:
365
+ logger.info("Dream job: removed %d old JSONL files", removed)
366
+
367
+ # Build the prompt — load dream.md for dream job, else use command field
368
+ if job.id == DREAM_JOB_ID:
369
+ dream_prompt_path = Path(__file__).resolve().parents[2] / "prompts" / "main" / "dream.md"
370
+ if dream_prompt_path.is_file():
371
+ command_text = dream_prompt_path.read_text(encoding="utf-8").strip()
372
+ else:
373
+ command_text = job.command
374
+ else:
375
+ command_text = job.command
376
+
325
377
  prompt = (
326
378
  f"[Cron job: {job.id}]\n"
327
- f"{job.command}"
379
+ f"{command_text}"
328
380
  )
329
381
 
330
382
  repo_root = Path.cwd().resolve()
@@ -372,6 +424,9 @@ class CronScheduler:
372
424
  self._lock = threading.Lock()
373
425
  self._running = False
374
426
 
427
+ # Auto-seed the dream memory job if it doesn't exist
428
+ ensure_dream_job(self.config)
429
+
375
430
  def start(self):
376
431
  """Start the cron scheduler background thread."""
377
432
  enabled_jobs = [j for j in self.config.jobs.values() if j.enabled]