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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
857
|
-
|
|
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
|
-
//
|
|
861
|
-
const
|
|
862
|
-
|
|
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
|
-
//
|
|
865
|
-
const
|
|
866
|
-
|
|
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 +=
|
|
874
|
-
this.metrics.rulesCount +=
|
|
945
|
+
this.metrics.patternsLearnedCount += toAdd.length;
|
|
946
|
+
this.metrics.rulesCount += toAdd.length;
|
|
875
947
|
this.metrics.lastPatternAt = new Date().toISOString();
|
|
876
|
-
this.metrics.latestLearnedRule =
|
|
877
|
-
console.log(`[Self-Improve] Added ${
|
|
878
|
-
|
|
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
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -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 });
|