tuna-agent 0.1.58 → 0.1.60

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.
@@ -43,6 +43,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
43
43
  getMetrics(): Record<string, unknown>;
44
44
  /** Register an agent folder path for rules parsing (called from daemon). */
45
45
  registerAgentFolder(agentId: string, folderPath: string): void;
46
+ /** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
47
+ seedMemoryCounts(): Promise<void>;
46
48
  checkHealth(): Promise<{
47
49
  ok: boolean;
48
50
  message: string;
@@ -67,6 +69,10 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
67
69
  * Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
68
70
  * Runs every N tasks to evolve the agent's permanent knowledge.
69
71
  */
72
+ /** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
73
+ private static extractKeyPhrases;
74
+ /** Check if two rules are semantically similar (>40% key phrase overlap). */
75
+ private static isSimilarRule;
70
76
  runSelfImprovement(cwd: string): Promise<void>;
71
77
  /**
72
78
  * Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
@@ -78,6 +78,25 @@ export class ClaudeCodeAdapter {
78
78
  registerAgentFolder(agentId, folderPath) {
79
79
  this.agentFolderMap.set(agentId, folderPath);
80
80
  }
81
+ /** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
82
+ async seedMemoryCounts() {
83
+ for (const [agentId, folder] of this.agentFolderMap) {
84
+ const agentName = path.basename(folder);
85
+ try {
86
+ const count = await fetchMem0Count(agentName);
87
+ if (count > 0) {
88
+ const savedId = this.currentAgentId;
89
+ this.currentAgentId = agentId;
90
+ if (count > this.metrics.memoryCount) {
91
+ this.metrics.memoryCount = count;
92
+ }
93
+ this.currentAgentId = savedId;
94
+ console.log(`[Metrics] Seeded memoryCount=${count} for "${agentName}"`);
95
+ }
96
+ }
97
+ catch { /* ignore */ }
98
+ }
99
+ }
81
100
  async checkHealth() {
82
101
  try {
83
102
  execSync('which claude', { stdio: 'ignore' });
@@ -827,6 +846,24 @@ export class ClaudeCodeAdapter {
827
846
  * Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
828
847
  * Runs every N tasks to evolve the agent's permanent knowledge.
829
848
  */
849
+ /** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
850
+ static extractKeyPhrases(text) {
851
+ const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 2);
852
+ const phrases = [];
853
+ for (let i = 0; i <= words.length - 3; i++) {
854
+ phrases.push(words.slice(i, i + 3).join(' '));
855
+ }
856
+ return phrases;
857
+ }
858
+ /** Check if two rules are semantically similar (>40% key phrase overlap). */
859
+ static isSimilarRule(ruleA, ruleB) {
860
+ const phrasesA = ClaudeCodeAdapter.extractKeyPhrases(ruleA);
861
+ const phrasesB = new Set(ClaudeCodeAdapter.extractKeyPhrases(ruleB));
862
+ if (phrasesA.length === 0)
863
+ return false;
864
+ const overlap = phrasesA.filter(p => phrasesB.has(p)).length;
865
+ return overlap / phrasesA.length > 0.4;
866
+ }
830
867
  async runSelfImprovement(cwd) {
831
868
  if (!process.env.MEM0_SSH_HOST)
832
869
  return;
@@ -841,41 +878,76 @@ export class ClaudeCodeAdapter {
841
878
  console.log(`[Self-Improve] No patterns detected yet`);
842
879
  return;
843
880
  }
844
- // Read current CLAUDE.md to check for existing rules
881
+ // Read current CLAUDE.md and extract existing rules
845
882
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
846
883
  let existingContent = '';
847
884
  if (fs.existsSync(claudeMdPath)) {
848
885
  existingContent = fs.readFileSync(claudeMdPath, 'utf-8');
849
886
  }
850
- // Filter out patterns that are already in CLAUDE.md
887
+ // Parse existing rules from "## Learned Rules" section
888
+ const SECTION_HEADER = '## Learned Rules';
889
+ const existingRules = [];
890
+ const sectionIdx = existingContent.indexOf(SECTION_HEADER);
891
+ if (sectionIdx !== -1) {
892
+ const sectionContent = existingContent.substring(sectionIdx + SECTION_HEADER.length);
893
+ const nextSection = sectionContent.indexOf('\n## ');
894
+ const rulesText = nextSection !== -1 ? sectionContent.substring(0, nextSection) : sectionContent;
895
+ for (const line of rulesText.split('\n')) {
896
+ const trimmed = line.trim();
897
+ if (trimmed.startsWith('- ')) {
898
+ // Strip confidence suffix
899
+ const ruleText = trimmed.replace(/\s*\(confidence:\s*[\d.]+\)\s*$/, '').substring(2);
900
+ if (ruleText)
901
+ existingRules.push(ruleText);
902
+ }
903
+ }
904
+ }
905
+ // Filter: skip patterns similar to ANY existing rule
851
906
  const newPatterns = patterns.filter(p => {
852
- // Simple dedup: check if rule text (or close match) already exists
853
- const ruleNormalized = p.rule.toLowerCase().replace(/[^a-z0-9\s]/g, '');
854
- return !existingContent.toLowerCase().includes(ruleNormalized.substring(0, 50));
907
+ return !existingRules.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing));
855
908
  });
856
- if (newPatterns.length === 0) {
857
- console.log(`[Self-Improve] ${patterns.length} patterns found but all already in CLAUDE.md`);
909
+ // Also dedup among new patterns themselves
910
+ const dedupedPatterns = [];
911
+ for (const p of newPatterns) {
912
+ const isDup = dedupedPatterns.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing.rule));
913
+ if (!isDup)
914
+ dedupedPatterns.push(p);
915
+ }
916
+ if (dedupedPatterns.length === 0) {
917
+ console.log(`[Self-Improve] ${patterns.length} patterns found but all similar to existing rules`);
858
918
  return;
859
919
  }
860
- // Append new rules to CLAUDE.md under "## Learned Rules"
861
- const SECTION_HEADER = '## Learned Rules';
862
- const rulesBlock = newPatterns.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
920
+ // Cap at 3 new rules per run to avoid bloating
921
+ const toAdd = dedupedPatterns.slice(0, 3);
922
+ // Append new rules at the END of the Learned Rules section
923
+ const rulesBlock = toAdd.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
863
924
  if (existingContent.includes(SECTION_HEADER)) {
864
- // Append to existing section
865
- const updatedContent = existingContent.replace(SECTION_HEADER, `${SECTION_HEADER}\n${rulesBlock}`);
866
- fs.writeFileSync(claudeMdPath, updatedContent);
925
+ // Find end of Learned Rules section (next ## or end of file)
926
+ const headerIdx = existingContent.indexOf(SECTION_HEADER);
927
+ const afterHeader = existingContent.substring(headerIdx + SECTION_HEADER.length);
928
+ const nextSectionIdx = afterHeader.indexOf('\n## ');
929
+ if (nextSectionIdx !== -1) {
930
+ // Insert before next section
931
+ const insertPos = headerIdx + SECTION_HEADER.length + nextSectionIdx;
932
+ const updatedContent = existingContent.substring(0, insertPos) + '\n' + rulesBlock + existingContent.substring(insertPos);
933
+ fs.writeFileSync(claudeMdPath, updatedContent);
934
+ }
935
+ else {
936
+ // Append at end of file
937
+ const ending = existingContent.endsWith('\n') ? '' : '\n';
938
+ fs.writeFileSync(claudeMdPath, existingContent + ending + rulesBlock + '\n');
939
+ }
867
940
  }
868
941
  else {
869
- // Add new section at the end
870
942
  const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
871
943
  fs.writeFileSync(claudeMdPath, existingContent + separator + `${SECTION_HEADER}\n${rulesBlock}\n`);
872
944
  }
873
- this.metrics.patternsLearnedCount += newPatterns.length;
874
- this.metrics.rulesCount += newPatterns.length;
945
+ this.metrics.patternsLearnedCount += toAdd.length;
946
+ this.metrics.rulesCount += toAdd.length;
875
947
  this.metrics.lastPatternAt = new Date().toISOString();
876
- this.metrics.latestLearnedRule = newPatterns[newPatterns.length - 1].rule.substring(0, 500);
877
- console.log(`[Self-Improve] Added ${newPatterns.length} new rules to CLAUDE.md:`);
878
- newPatterns.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
948
+ this.metrics.latestLearnedRule = toAdd[toAdd.length - 1].rule.substring(0, 500);
949
+ console.log(`[Self-Improve] Added ${toAdd.length} new rules to CLAUDE.md:`);
950
+ toAdd.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
879
951
  // Sync learned rules to heartbeat metrics
880
952
  this.parseLearnedRules(claudeMdPath);
881
953
  }
@@ -193,6 +193,8 @@ export async function startDaemon(config) {
193
193
  }
194
194
  }
195
195
  console.log(`[Daemon] Registered ${msg.agentFolders.length} agent folder(s) for rules sync`);
196
+ // Seed memory counts from Mem0 so they appear on agent detail immediately
197
+ ccAdapter.seedMemoryCounts().catch(() => { });
196
198
  }
197
199
  // Recover orphaned tasks — pass activeTaskId so API won't fail a task we're still running
198
200
  ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"