@leverageaiapps/locus 2.2.9 → 2.3.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent/agent-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,OAAO,KAAK,EACV,eAAe,EAChB,MAAM,SAAS,CAAC;AAcjB,wBAAgB,iBAAiB,IAAI,OAAO,CAI3C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAI7E;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAsCD,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAketE"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent/agent-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAsBH,OAAO,KAAK,EACV,eAAe,EAChB,MAAM,SAAS,CAAC;AA+KjB,wBAAgB,iBAAiB,IAAI,OAAO,CAI3C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAI7E;AAED,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAsCD,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAykBtE"}
@@ -52,6 +52,8 @@ exports.isRunning = isRunning;
52
52
  exports.runAgentLoop = runAgentLoop;
53
53
  const promises_1 = require("node:fs/promises");
54
54
  const node_os_1 = require("node:os");
55
+ const node_path_1 = require("node:path");
56
+ const node_child_process_1 = require("node:child_process");
55
57
  const worker_proxy_1 = require("./worker-proxy");
56
58
  const claude_client_1 = require("./claude-client");
57
59
  const tool_defs_1 = require("./tool-defs");
@@ -61,6 +63,167 @@ const types_1 = require("./types");
61
63
  const crypto = __importStar(require("node:crypto"));
62
64
  // ─── Memory Path ────────────────────────────────────────────────
63
65
  const MEMORY_DIR = `${(0, node_os_1.homedir)()}/.leverage/memory`;
66
+ // ─── Platform Detection (ported from container-agent) ────────────
67
+ function detectLocalPlatform() {
68
+ const p = (0, node_os_1.platform)();
69
+ if (p === 'darwin')
70
+ return 'macOS';
71
+ if (p === 'win32')
72
+ return 'Windows';
73
+ if (p === 'linux')
74
+ return 'Linux';
75
+ return p;
76
+ }
77
+ function macosVersion() {
78
+ try {
79
+ const res = (0, node_child_process_1.spawnSync)('sw_vers', ['-productVersion'], {
80
+ encoding: 'utf-8', timeout: 1000, stdio: ['pipe', 'pipe', 'pipe'],
81
+ });
82
+ const out = typeof res.stdout === 'string' ? res.stdout.trim() : '';
83
+ return out || (0, node_os_1.release)();
84
+ }
85
+ catch {
86
+ return (0, node_os_1.release)();
87
+ }
88
+ }
89
+ function detectShell() {
90
+ if ((0, node_os_1.platform)() === 'win32') {
91
+ try {
92
+ const res = (0, node_child_process_1.spawnSync)('pwsh', ['--version'], {
93
+ encoding: 'utf-8', timeout: 1000, stdio: ['pipe', 'pipe', 'pipe'],
94
+ });
95
+ if (res.status === 0)
96
+ return 'pwsh';
97
+ }
98
+ catch { }
99
+ return 'powershell';
100
+ }
101
+ const envShell = process.env.SHELL?.trim();
102
+ return envShell ? (0, node_path_1.basename)(envShell) : 'sh';
103
+ }
104
+ /** Build Runtime line for system prompt (like OpenClaw's buildRuntimeLine) */
105
+ function buildRuntimeLine() {
106
+ const p = (0, node_os_1.platform)();
107
+ const parts = [];
108
+ try {
109
+ parts.push(`host=${(0, node_os_1.hostname)()}`);
110
+ }
111
+ catch { }
112
+ let osLabel;
113
+ if (p === 'darwin') {
114
+ osLabel = `macOS ${macosVersion()}`;
115
+ }
116
+ else if (p === 'win32') {
117
+ osLabel = `Windows ${(0, node_os_1.release)()}`;
118
+ }
119
+ else {
120
+ osLabel = `${p} ${(0, node_os_1.release)()}`;
121
+ }
122
+ parts.push(`os=${osLabel} (${(0, node_os_1.arch)()})`);
123
+ parts.push(`shell=${detectShell()}`);
124
+ parts.push(`node=${process.version}`);
125
+ parts.push(`home=${(0, node_os_1.homedir)()}`);
126
+ // Timezone (useful for scheduling and user context)
127
+ try {
128
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
129
+ if (tz)
130
+ parts.push(`timezone=${tz}`);
131
+ }
132
+ catch { }
133
+ // Current time
134
+ try {
135
+ const now = new Date();
136
+ parts.push(`time=${now.toLocaleString('en-US', { hour12: false })}`);
137
+ }
138
+ catch { }
139
+ return `Runtime: ${parts.join(' | ')}`;
140
+ }
141
+ // ─── CJK keyword extraction (ported from container-agent) ────────
142
+ const LOCAL_STOP_EN = new Set([
143
+ 'a', 'an', 'the', 'is', 'am', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
144
+ 'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'may', 'might', 'must', 'can', 'could',
145
+ 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'it', 'its', 'they', 'them', 'their',
146
+ 'this', 'that', 'these', 'those', 'what', 'which', 'who', 'whom', 'where', 'when', 'how', 'why',
147
+ 'not', 'no', 'nor', 'if', 'then', 'than', 'too', 'very', 'just', 'about', 'all', 'also', 'and', 'any', 'as',
148
+ 'at', 'but', 'by', 'for', 'from', 'in', 'into', 'of', 'on', 'or', 'so', 'to', 'with',
149
+ 'please', 'help', 'find', 'show', 'tell', 'give', 'want', 'need', 'know', 'think', 'like',
150
+ ]);
151
+ const LOCAL_STOP_ZH = new Set([
152
+ '我', '我们', '你', '你们', '他', '她', '它', '他们', '这', '那', '这个', '那个',
153
+ '的', '了', '着', '过', '得', '地', '吗', '呢', '吧', '啊', '哦', '呀', '嗯',
154
+ '是', '有', '在', '做', '说', '看', '找', '想', '要', '能', '会', '可以',
155
+ '和', '与', '或', '但', '因为', '所以', '如果', '但是', '而且',
156
+ '把', '被', '让', '给', '到', '从', '对', '比', '跟', '为', '用',
157
+ '都', '也', '就', '才', '又', '再', '还', '已经', '很', '太', '最', '更',
158
+ '什么', '哪个', '怎么', '为什么', '请', '帮', '帮忙', '告诉', '知道',
159
+ ]);
160
+ const LOCAL_STOP_JA = new Set([
161
+ 'の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', 'ある', 'いる', 'も',
162
+ 'する', 'から', 'な', 'こと', 'い', 'や', 'など', 'なっ', 'ない', 'この', 'その', 'また',
163
+ 'です', 'ます', 'ました', 'でした', 'ません', 'それ', 'あれ', 'ここ', 'そこ', 'どこ',
164
+ ]);
165
+ const LOCAL_STOP_KO = new Set([
166
+ '이', '그', '저', '것', '수', '등', '들', '및', '에', '의', '가', '은', '는', '로', '를',
167
+ '와', '과', '도', '만', '에서', '으로', '하다', '있다', '되다', '않다', '이다', '합니다',
168
+ ]);
169
+ const LOCAL_STOP_ES = new Set([
170
+ 'el', 'la', 'los', 'las', 'un', 'una', 'y', 'o', 'pero', 'de', 'en', 'a', 'por', 'para', 'con', 'que', 'se', 'es', 'no',
171
+ ]);
172
+ const LOCAL_STOP_PT = new Set([
173
+ 'o', 'a', 'os', 'as', 'um', 'uma', 'e', 'ou', 'mas', 'de', 'em', 'por', 'para', 'com', 'que', 'se', 'é', 'não',
174
+ ]);
175
+ const RE_ANY_CJK_LOCAL = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/;
176
+ const RE_CJK_LOCAL = /[\u4e00-\u9fff]/;
177
+ const RE_HIRAGANA_LOCAL = /[\u3040-\u309f]/;
178
+ const RE_KATAKANA_LOCAL = /[\u30a0-\u30ff]/;
179
+ const RE_HANGUL_LOCAL = /[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]/;
180
+ function isLocalStopWord(t) {
181
+ return LOCAL_STOP_EN.has(t) || LOCAL_STOP_ZH.has(t) ||
182
+ LOCAL_STOP_JA.has(t) || LOCAL_STOP_KO.has(t) ||
183
+ LOCAL_STOP_ES.has(t) || LOCAL_STOP_PT.has(t);
184
+ }
185
+ function extractLocalKeywords(query) {
186
+ const tokens = [];
187
+ const segments = query.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]+|[^\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u1100-\u11ff\u3130-\u318f]+/g) || [];
188
+ for (const seg of segments) {
189
+ if (RE_ANY_CJK_LOCAL.test(seg)) {
190
+ const chars = Array.from(seg);
191
+ for (const c of chars) {
192
+ if (RE_CJK_LOCAL.test(c) || RE_HIRAGANA_LOCAL.test(c) || RE_KATAKANA_LOCAL.test(c) || RE_HANGUL_LOCAL.test(c)) {
193
+ tokens.push(c);
194
+ }
195
+ }
196
+ for (let i = 0; i < chars.length - 1; i++) {
197
+ const a = chars[i], b = chars[i + 1];
198
+ if ((RE_CJK_LOCAL.test(a) && RE_CJK_LOCAL.test(b)) ||
199
+ (RE_HIRAGANA_LOCAL.test(a) && RE_HIRAGANA_LOCAL.test(b)) ||
200
+ (RE_KATAKANA_LOCAL.test(a) && RE_KATAKANA_LOCAL.test(b)) ||
201
+ (RE_HANGUL_LOCAL.test(a) && RE_HANGUL_LOCAL.test(b))) {
202
+ tokens.push(a + b);
203
+ }
204
+ }
205
+ }
206
+ else {
207
+ const words = seg.toLowerCase().match(/[\p{L}\p{N}_]+/gu) || [];
208
+ tokens.push(...words);
209
+ }
210
+ }
211
+ const seen = new Set();
212
+ const kws = [];
213
+ for (const t of tokens) {
214
+ if (isLocalStopWord(t))
215
+ continue;
216
+ if (t.length < 2 && !RE_ANY_CJK_LOCAL.test(t))
217
+ continue;
218
+ if (/^\d+$/.test(t))
219
+ continue;
220
+ if (seen.has(t))
221
+ continue;
222
+ seen.add(t);
223
+ kws.push(t);
224
+ }
225
+ return kws;
226
+ }
64
227
  // ─── State ───────────────────────────────────────────────────────
65
228
  let cancelled = false;
66
229
  let currentTaskId = null;
@@ -166,7 +329,14 @@ async function runAgentLoop(req) {
166
329
  }
167
330
  }
168
331
  catch { }
169
- const ctx = await proxy.loadContext(sandboxId, conversationId, isCronTask, memoryContent, '', req.workingDirectory);
332
+ // Detect platform locally (more accurate than iOS-passed value)
333
+ const detectedPlatform = detectLocalPlatform();
334
+ const ctx = await proxy.loadContext(sandboxId, conversationId, isCronTask, memoryContent, '', req.workingDirectory, true, detectedPlatform);
335
+ // Inject runtime info into system prompt (like OpenClaw's buildRuntimeLine)
336
+ // Gives Claude exact details: OS version, arch, shell, node, home dir
337
+ const runtimeLine = buildRuntimeLine();
338
+ ctx.systemPrompt = runtimeLine + '\n\n' + ctx.systemPrompt;
339
+ console.log(`[Agent] ${runtimeLine}`);
170
340
  // Build messages array
171
341
  if (ctx.conversationHistory.length > 0) {
172
342
  messages.push(...ctx.conversationHistory);
@@ -186,6 +356,13 @@ async function runAgentLoop(req) {
186
356
  messages.push({ role: 'user', content: userContent.length === 1 ? cronPrefix + message : userContent });
187
357
  const systemPrompt = ctx.systemPrompt;
188
358
  let fullOutput = '';
359
+ // Usage accumulator — tracks total token usage across all API calls in this task
360
+ const accumulatedUsage = {
361
+ input_tokens: 0,
362
+ output_tokens: 0,
363
+ cache_read_input_tokens: 0,
364
+ cache_creation_input_tokens: 0,
365
+ };
189
366
  // Emit connected event
190
367
  await emitEvent(proxy, taskId, userId, conversationId, 'connected', {
191
368
  request_id: taskId,
@@ -193,8 +370,10 @@ async function runAgentLoop(req) {
193
370
  });
194
371
  const loopStartTime = Date.now();
195
372
  let compactionCount = 0;
373
+ let lastTurn = 0;
196
374
  // ─── Agentic Loop ────────────────────────────────────────
197
375
  for (let turn = 0; turn < types_1.MAX_AGENT_TURNS; turn++) {
376
+ lastTurn = turn;
198
377
  if (cancelled) {
199
378
  await emitEvent(proxy, taskId, userId, conversationId, 'done', { finish_reason: 'cancelled' });
200
379
  break;
@@ -377,6 +556,13 @@ async function runAgentLoop(req) {
377
556
  break;
378
557
  }
379
558
  console.log(`[Agent] Turn ${turn}: stop_reason=${response.stop_reason}, blocks=${response.content.length}, usage=${JSON.stringify(response.usage)}`);
559
+ // Accumulate usage from this API call
560
+ if (response.usage) {
561
+ accumulatedUsage.input_tokens += response.usage.input_tokens || 0;
562
+ accumulatedUsage.output_tokens += response.usage.output_tokens || 0;
563
+ accumulatedUsage.cache_read_input_tokens += response.usage.cache_read_input_tokens || 0;
564
+ accumulatedUsage.cache_creation_input_tokens += response.usage.cache_creation_input_tokens || 0;
565
+ }
380
566
  // Turn 0: retry with tool_choice='any' if text-only
381
567
  const hasAnyToolUse = response.content.some((b) => b.type === 'tool_use' || b.type === 'server_tool_use');
382
568
  if (turn === 0 && !hasAnyToolUse && response.stop_reason === 'end_turn') {
@@ -394,6 +580,13 @@ async function runAgentLoop(req) {
394
580
  });
395
581
  if (retryResponse.content.length > 0) {
396
582
  response = retryResponse;
583
+ // Accumulate retry usage
584
+ if (retryResponse.usage) {
585
+ accumulatedUsage.input_tokens += retryResponse.usage.input_tokens || 0;
586
+ accumulatedUsage.output_tokens += retryResponse.usage.output_tokens || 0;
587
+ accumulatedUsage.cache_read_input_tokens += retryResponse.usage.cache_read_input_tokens || 0;
588
+ accumulatedUsage.cache_creation_input_tokens += retryResponse.usage.cache_creation_input_tokens || 0;
589
+ }
397
590
  }
398
591
  else {
399
592
  console.log('[Agent] Turn 0 retry returned empty, keeping original text response');
@@ -491,8 +684,58 @@ async function runAgentLoop(req) {
491
684
  console.error(`[Agent] Failed to save turn: ${err.message}`);
492
685
  }
493
686
  }
494
- // Mark task completed
495
- await proxy.updateTaskStatus('completed');
687
+ // Transcript digest + session log: write to daily log for searchability
688
+ try {
689
+ const today = new Date().toISOString().split('T')[0];
690
+ const filePath = `${MEMORY_DIR}/${today}.md`;
691
+ const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
692
+ await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
693
+ // 1. Lightweight transcript digest
694
+ const digest = (0, compaction_1.buildTranscriptDigest)(messages, message, lastTurn + 1, accumulatedUsage);
695
+ await (0, promises_1.appendFile)(filePath, `\n## [transcript] ${timestamp}\n${digest}\n`, 'utf-8');
696
+ // 2. Session log for keyword searchability
697
+ const sessionLog = (0, compaction_1.buildEnrichedSnippet)(messages, 8000);
698
+ if (sessionLog.length > 200) {
699
+ await (0, promises_1.appendFile)(filePath, `\n## [session-log] ${timestamp}\n${sessionLog}\n`, 'utf-8');
700
+ }
701
+ // R2 backup (fire-and-forget)
702
+ const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => '');
703
+ proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
704
+ proxy.memorySync(`${today}.md`, full).catch(() => { });
705
+ }
706
+ catch { }
707
+ // Post-task memory flush: extract key info → daily log (fire-and-forget)
708
+ (0, compaction_1.postTaskMemoryFlush)(messages, chatFn, async (content) => {
709
+ const today = new Date().toISOString().split('T')[0];
710
+ const filePath = `${MEMORY_DIR}/${today}.md`;
711
+ const entry = `\n## [task-summary] ${new Date().toLocaleTimeString('en-US', { hour12: false })}\n${content}\n`;
712
+ try {
713
+ await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
714
+ await (0, promises_1.appendFile)(filePath, entry, 'utf-8');
715
+ // R2 backup (fire-and-forget)
716
+ const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => content);
717
+ proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
718
+ proxy.memorySync(`${today}.md`, full).catch(() => { });
719
+ console.log(`[Memory] Post-task flush: saved to ${filePath} + R2 backup`);
720
+ }
721
+ catch (writeErr) {
722
+ console.warn(`[Memory] Post-task flush write failed: ${writeErr.message}`);
723
+ }
724
+ }).catch(() => { });
725
+ // Auto-distill MEMORY.md if it's grown too large (fire-and-forget)
726
+ (0, compaction_1.autoDistillMemory)(MEMORY_DIR, chatFn, async (distilled) => {
727
+ try {
728
+ await (0, promises_1.writeFile)(`${MEMORY_DIR}/MEMORY.md`, distilled, 'utf-8');
729
+ proxy.memoryOp('backup_memory', `${MEMORY_DIR}/MEMORY.md`, distilled).catch(() => { });
730
+ proxy.memorySync('MEMORY.md', distilled).catch(() => { });
731
+ }
732
+ catch (err) {
733
+ console.warn(`[Memory] Failed to write distilled MEMORY.md: ${err.message}`);
734
+ }
735
+ }).catch(() => { });
736
+ // Mark task completed — pass accumulated usage for credit deduction
737
+ console.log(`[Agent] Task ${taskId} completed. Total usage: in=${accumulatedUsage.input_tokens} out=${accumulatedUsage.output_tokens} cacheRead=${accumulatedUsage.cache_read_input_tokens} cacheWrite=${accumulatedUsage.cache_creation_input_tokens}`);
738
+ await proxy.updateTaskStatus('completed', undefined, accumulatedUsage);
496
739
  // Finalize cron run if this was a scheduled task
497
740
  if (isCronTask && req.cronRunId && req.cronJobId) {
498
741
  try {
@@ -518,6 +761,22 @@ async function runAgentLoop(req) {
518
761
  console.error(`[Agent] Failed to save turn on failure: ${saveErr.message}`);
519
762
  }
520
763
  }
764
+ // Post-task memory flush on failure too (fire-and-forget)
765
+ if (messages.length >= 3) {
766
+ (0, compaction_1.postTaskMemoryFlush)(messages, chatFn, async (content) => {
767
+ const today = new Date().toISOString().split('T')[0];
768
+ const filePath = `${MEMORY_DIR}/${today}.md`;
769
+ const entry = `\n## [task-failed] ${new Date().toLocaleTimeString('en-US', { hour12: false })}\n${content}\n[Error: ${err.message}]\n`;
770
+ try {
771
+ await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
772
+ await (0, promises_1.appendFile)(filePath, entry, 'utf-8');
773
+ const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => content);
774
+ proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
775
+ proxy.memorySync(`${today}.md`, full).catch(() => { });
776
+ }
777
+ catch { }
778
+ }).catch(() => { });
779
+ }
521
780
  // Emit error event
522
781
  try {
523
782
  await emitEvent(proxy, taskId, userId, conversationId, 'error', {
@@ -581,7 +840,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
581
840
  // ── Local tools ──
582
841
  if (toolName === 'bash') {
583
842
  const cmd = (toolInput.command || 'echo "no command"');
584
- return await (0, local_tools_1.executeBash)(cmd);
843
+ const result = await (0, local_tools_1.executeBash)(cmd);
844
+ // Auto-detect package installs and save to environment.md (fire-and-forget)
845
+ if (!result.isError) {
846
+ const packages = detectInstalledPackages(cmd);
847
+ if (packages) {
848
+ const envFile = `${MEMORY_DIR}/environment.md`;
849
+ const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
850
+ const entry = `\n## ${timestamp}\n${packages}\n`;
851
+ (async () => {
852
+ try {
853
+ await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
854
+ await (0, promises_1.appendFile)(envFile, entry, 'utf-8');
855
+ const full = await (0, promises_1.readFile)(envFile, 'utf-8').catch(() => entry);
856
+ proxy.memoryOp('backup_memory', envFile, full).catch(() => { });
857
+ proxy.memorySync('environment.md', full).catch(() => { });
858
+ }
859
+ catch { }
860
+ })();
861
+ }
862
+ }
863
+ return result;
585
864
  }
586
865
  if (toolName === 'write_file') {
587
866
  const writePath = toolInput.path || toolInput.file_path;
@@ -658,27 +937,43 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
658
937
  try {
659
938
  const query = toolInput.query || '';
660
939
  const maxResults = Math.min(toolInput.max_results || 5, 10);
661
- // Try semantic search via Worker (Vectorize hybrid)
940
+ // Run Vectorize + local keyword search in parallel, merge results
941
+ // Local search is always needed because post-task flush writes locally
942
+ // and Vectorize indexing has latency — stale index misses fresh content
943
+ // 1. Vectorize search (may return stale results)
944
+ let vectorizeResults = [];
662
945
  try {
663
- const results = await proxy.memorySearch(query, maxResults);
664
- if (results && results.length > 0) {
665
- const formatted = results.map((r) => `--- ${r.source}:${r.startLine}-${r.endLine} (score: ${(r.score || 0).toFixed(2)}) ---\n${r.content}`).join('\n\n');
666
- return { output: formatted, isError: false };
946
+ const vResults = await proxy.memorySearch(query, maxResults);
947
+ if (vResults && vResults.length > 0) {
948
+ vectorizeResults = vResults.map((r) => r.content?.trim()).filter(Boolean);
667
949
  }
668
950
  }
669
951
  catch (err) {
670
- console.warn(`[Memory] Vectorize search failed, falling back to keyword: ${err.message}`);
952
+ console.warn(`[Memory] Vectorize search failed: ${err.message}`);
671
953
  }
672
- // Fallback: local keyword search
673
- const queryLower = query.toLowerCase();
674
- const results = [];
954
+ // 2. Local keyword search (always fresh — reads files directly)
955
+ const keywords = extractLocalKeywords(query);
956
+ const localResults = [];
957
+ const scoreContent = (text) => {
958
+ if (keywords.length === 0)
959
+ return text.toLowerCase().includes(query.toLowerCase()) ? 1 : 0;
960
+ const lower = text.toLowerCase();
961
+ let matched = 0;
962
+ for (const kw of keywords) {
963
+ if (lower.includes(kw))
964
+ matched++;
965
+ }
966
+ return matched / keywords.length;
967
+ };
968
+ // Search MEMORY.md
675
969
  try {
676
970
  const mem = await (0, promises_1.readFile)(`${MEMORY_DIR}/MEMORY.md`, 'utf-8');
677
- if (mem && mem.toLowerCase().includes(queryLower)) {
678
- results.push(`--- MEMORY.md ---\n${mem.substring(0, 3000)}`);
679
- }
971
+ const score = scoreContent(mem);
972
+ if (score > 0)
973
+ localResults.push({ file: 'MEMORY.md', content: mem.substring(0, 3000), score });
680
974
  }
681
975
  catch { }
976
+ // Search last 14 daily logs
682
977
  const today = new Date();
683
978
  for (let i = 0; i < 14; i++) {
684
979
  const d = new Date(today);
@@ -686,13 +981,13 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
686
981
  const dateStr = d.toISOString().split('T')[0];
687
982
  try {
688
983
  const note = await (0, promises_1.readFile)(`${MEMORY_DIR}/${dateStr}.md`, 'utf-8');
689
- if (note && note.toLowerCase().includes(queryLower)) {
690
- results.push(`--- ${dateStr}.md ---\n${note.substring(0, 1500)}`);
691
- }
984
+ const score = scoreContent(note);
985
+ if (score > 0)
986
+ localResults.push({ file: `${dateStr}.md`, content: note.substring(0, 1500), score });
692
987
  }
693
988
  catch { }
694
989
  }
695
- // Also search topic files locally
990
+ // Search topic files
696
991
  try {
697
992
  const { readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
698
993
  const files = await readdir(MEMORY_DIR);
@@ -701,15 +996,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
701
996
  continue;
702
997
  try {
703
998
  const fc = await (0, promises_1.readFile)(`${MEMORY_DIR}/${f}`, 'utf-8');
704
- if (fc && fc.toLowerCase().includes(queryLower)) {
705
- results.push(`--- ${f} ---\n${fc.substring(0, 1500)}`);
706
- }
999
+ const score = scoreContent(fc);
1000
+ if (score > 0)
1001
+ localResults.push({ file: f, content: fc.substring(0, 1500), score });
707
1002
  }
708
1003
  catch { }
709
1004
  }
710
1005
  }
711
1006
  catch { }
712
- return { output: results.join('\n\n') || 'No results found.', isError: false };
1007
+ // 3. Merge: local results first (always fresh), then Vectorize extras
1008
+ localResults.sort((a, b) => b.score - a.score);
1009
+ const localFormatted = localResults.map(r => `${r.content.trim()}\n\nSource: ${r.file}`);
1010
+ // Deduplicate: skip Vectorize results whose content overlaps with local
1011
+ const localText = localFormatted.join('\n');
1012
+ const vectorizeExtras = vectorizeResults.filter(v => {
1013
+ // Skip if >50% of first 200 chars appear in local results
1014
+ const snippet = v.substring(0, 200);
1015
+ return !localText.includes(snippet);
1016
+ });
1017
+ const merged = [...localFormatted, ...vectorizeExtras.map(v => `${v}\n\nSource: vectorize`)];
1018
+ const output = merged.slice(0, maxResults).join('\n\n---\n\n');
1019
+ return { output: output || 'No results found.', isError: false };
713
1020
  }
714
1021
  catch (err) {
715
1022
  return { output: `Search failed: ${err.message}`, isError: true };
@@ -986,6 +1293,25 @@ async function extractFileFromZip(buffer, targetName) {
986
1293
  }
987
1294
  return null;
988
1295
  }
1296
+ // ─── Package Detection ──────────────────────────────────────────
1297
+ function detectInstalledPackages(cmd) {
1298
+ const installs = [];
1299
+ const pipMatch = cmd.match(/pip3?\s+install\s+([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
1300
+ if (pipMatch)
1301
+ installs.push(`pip: ${pipMatch[1].trim()}`);
1302
+ const npmMatch = cmd.match(/npm\s+(?:install|i)\s+(?:-[gSD]\s+)?([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
1303
+ if (npmMatch)
1304
+ installs.push(`npm: ${npmMatch[1].trim()}`);
1305
+ const aptMatch = cmd.match(/apt(?:-get)?\s+install\s+(?:-y\s+)?([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
1306
+ if (aptMatch)
1307
+ installs.push(`apt: ${aptMatch[1].trim()}`);
1308
+ const brewMatch = cmd.match(/brew\s+install\s+([^\s|&;]+(?:\s+[^\s|&;-][^\s|&;]*)*)/);
1309
+ if (brewMatch)
1310
+ installs.push(`brew: ${brewMatch[1].trim()}`);
1311
+ if (installs.length === 0)
1312
+ return null;
1313
+ return `Installed packages: ${installs.join(', ')}`;
1314
+ }
989
1315
  // ─── Turn Summary Builder ────────────────────────────────────────
990
1316
  function buildTurnSummary(messages, _originalMessage) {
991
1317
  const textParts = [];