@leverageaiapps/locus 2.2.8 → 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,CAqftE"}
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;
@@ -135,12 +298,8 @@ async function runAgentLoop(req) {
135
298
  // Mark task as running
136
299
  await proxy.updateTaskStatus('running');
137
300
  // Load context from Worker (conversation history, system prompt, skills)
138
- // Read local memory files first
301
+ // Read MEMORY.md only daily logs are accessed on-demand via search_memory/read_memory
139
302
  let memoryContent = '';
140
- let recentNotes = '';
141
- const MAX_DAILY_DAYS = 7;
142
- const MAX_NOTE_CHARS_PER_DAY = 1500;
143
- const MAX_TOTAL_NOTES_CHARS = 8000;
144
303
  try {
145
304
  await (0, promises_1.mkdir)(MEMORY_DIR, { recursive: true });
146
305
  memoryContent = await (0, promises_1.readFile)(`${MEMORY_DIR}/MEMORY.md`, 'utf-8').catch(() => '');
@@ -168,30 +327,16 @@ async function runAgentLoop(req) {
168
327
  console.warn(`[Memory] R2 bulk restore failed: ${e.message}`);
169
328
  }
170
329
  }
171
- // Read last 7 days of notes (with per-day + total cap)
172
- const today = new Date();
173
- let totalNotesChars = 0;
174
- for (let i = 0; i < MAX_DAILY_DAYS; i++) {
175
- if (totalNotesChars >= MAX_TOTAL_NOTES_CHARS)
176
- break;
177
- const d = new Date(today);
178
- d.setDate(d.getDate() - i);
179
- const dateStr = d.toISOString().split('T')[0];
180
- try {
181
- let note = await (0, promises_1.readFile)(`${MEMORY_DIR}/${dateStr}.md`, 'utf-8');
182
- if (note) {
183
- if (note.length > MAX_NOTE_CHARS_PER_DAY) {
184
- note = note.substring(0, MAX_NOTE_CHARS_PER_DAY) + '... (truncated)';
185
- }
186
- recentNotes += `\n--- ${dateStr} ---\n${note}`;
187
- totalNotesChars += note.length;
188
- }
189
- }
190
- catch { }
191
- }
192
330
  }
193
331
  catch { }
194
- const ctx = await proxy.loadContext(sandboxId, conversationId, isCronTask, memoryContent, recentNotes.trim(), 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}`);
195
340
  // Build messages array
196
341
  if (ctx.conversationHistory.length > 0) {
197
342
  messages.push(...ctx.conversationHistory);
@@ -211,14 +356,24 @@ async function runAgentLoop(req) {
211
356
  messages.push({ role: 'user', content: userContent.length === 1 ? cronPrefix + message : userContent });
212
357
  const systemPrompt = ctx.systemPrompt;
213
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
+ };
214
366
  // Emit connected event
215
367
  await emitEvent(proxy, taskId, userId, conversationId, 'connected', {
216
368
  request_id: taskId,
217
369
  sandbox_id: sandboxId,
218
370
  });
219
371
  const loopStartTime = Date.now();
372
+ let compactionCount = 0;
373
+ let lastTurn = 0;
220
374
  // ─── Agentic Loop ────────────────────────────────────────
221
375
  for (let turn = 0; turn < types_1.MAX_AGENT_TURNS; turn++) {
376
+ lastTurn = turn;
222
377
  if (cancelled) {
223
378
  await emitEvent(proxy, taskId, userId, conversationId, 'done', { finish_reason: 'cancelled' });
224
379
  break;
@@ -251,12 +406,15 @@ async function runAgentLoop(req) {
251
406
  const full = await (0, promises_1.readFile)(filePath, 'utf-8').catch(() => content);
252
407
  proxy.memoryOp('backup_memory', filePath, full).catch(() => { });
253
408
  proxy.memorySync(`${today}.md`, full).catch(() => { });
254
- });
409
+ }, compactionCount);
255
410
  const compacted = await (0, compaction_1.maybeCompactMessages)(messages, chatFn, systemPrompt);
256
411
  messages.length = 0;
257
412
  messages.push(...compacted);
258
413
  (0, compaction_1.repairToolUseResultPairing)(messages);
414
+ compactionCount++; // Increment AFTER compaction
259
415
  }
416
+ // Progressive tool result trimming (age-based gradual shrinking)
417
+ (0, compaction_1.progressiveTrimToolResults)(messages);
260
418
  // Image context compaction (strip old base64 images)
261
419
  if (turn >= 1) {
262
420
  (0, compaction_1.compactBrowserContext)(messages, 2);
@@ -398,6 +556,13 @@ async function runAgentLoop(req) {
398
556
  break;
399
557
  }
400
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
+ }
401
566
  // Turn 0: retry with tool_choice='any' if text-only
402
567
  const hasAnyToolUse = response.content.some((b) => b.type === 'tool_use' || b.type === 'server_tool_use');
403
568
  if (turn === 0 && !hasAnyToolUse && response.stop_reason === 'end_turn') {
@@ -415,6 +580,13 @@ async function runAgentLoop(req) {
415
580
  });
416
581
  if (retryResponse.content.length > 0) {
417
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
+ }
418
590
  }
419
591
  else {
420
592
  console.log('[Agent] Turn 0 retry returned empty, keeping original text response');
@@ -512,8 +684,58 @@ async function runAgentLoop(req) {
512
684
  console.error(`[Agent] Failed to save turn: ${err.message}`);
513
685
  }
514
686
  }
515
- // Mark task completed
516
- 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);
517
739
  // Finalize cron run if this was a scheduled task
518
740
  if (isCronTask && req.cronRunId && req.cronJobId) {
519
741
  try {
@@ -539,6 +761,22 @@ async function runAgentLoop(req) {
539
761
  console.error(`[Agent] Failed to save turn on failure: ${saveErr.message}`);
540
762
  }
541
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
+ }
542
780
  // Emit error event
543
781
  try {
544
782
  await emitEvent(proxy, taskId, userId, conversationId, 'error', {
@@ -602,7 +840,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
602
840
  // ── Local tools ──
603
841
  if (toolName === 'bash') {
604
842
  const cmd = (toolInput.command || 'echo "no command"');
605
- 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;
606
864
  }
607
865
  if (toolName === 'write_file') {
608
866
  const writePath = toolInput.path || toolInput.file_path;
@@ -679,27 +937,43 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
679
937
  try {
680
938
  const query = toolInput.query || '';
681
939
  const maxResults = Math.min(toolInput.max_results || 5, 10);
682
- // 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 = [];
683
945
  try {
684
- const results = await proxy.memorySearch(query, maxResults);
685
- if (results && results.length > 0) {
686
- const formatted = results.map((r) => `--- ${r.source}:${r.startLine}-${r.endLine} (score: ${(r.score || 0).toFixed(2)}) ---\n${r.content}`).join('\n\n');
687
- 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);
688
949
  }
689
950
  }
690
951
  catch (err) {
691
- console.warn(`[Memory] Vectorize search failed, falling back to keyword: ${err.message}`);
952
+ console.warn(`[Memory] Vectorize search failed: ${err.message}`);
692
953
  }
693
- // Fallback: local keyword search
694
- const queryLower = query.toLowerCase();
695
- 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
696
969
  try {
697
970
  const mem = await (0, promises_1.readFile)(`${MEMORY_DIR}/MEMORY.md`, 'utf-8');
698
- if (mem && mem.toLowerCase().includes(queryLower)) {
699
- results.push(`--- MEMORY.md ---\n${mem.substring(0, 3000)}`);
700
- }
971
+ const score = scoreContent(mem);
972
+ if (score > 0)
973
+ localResults.push({ file: 'MEMORY.md', content: mem.substring(0, 3000), score });
701
974
  }
702
975
  catch { }
976
+ // Search last 14 daily logs
703
977
  const today = new Date();
704
978
  for (let i = 0; i < 14; i++) {
705
979
  const d = new Date(today);
@@ -707,13 +981,13 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
707
981
  const dateStr = d.toISOString().split('T')[0];
708
982
  try {
709
983
  const note = await (0, promises_1.readFile)(`${MEMORY_DIR}/${dateStr}.md`, 'utf-8');
710
- if (note && note.toLowerCase().includes(queryLower)) {
711
- results.push(`--- ${dateStr}.md ---\n${note.substring(0, 1500)}`);
712
- }
984
+ const score = scoreContent(note);
985
+ if (score > 0)
986
+ localResults.push({ file: `${dateStr}.md`, content: note.substring(0, 1500), score });
713
987
  }
714
988
  catch { }
715
989
  }
716
- // Also search topic files locally
990
+ // Search topic files
717
991
  try {
718
992
  const { readdir } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
719
993
  const files = await readdir(MEMORY_DIR);
@@ -722,15 +996,27 @@ async function executeToolInner(proxy, taskId, userId, sandboxId, conversationId
722
996
  continue;
723
997
  try {
724
998
  const fc = await (0, promises_1.readFile)(`${MEMORY_DIR}/${f}`, 'utf-8');
725
- if (fc && fc.toLowerCase().includes(queryLower)) {
726
- results.push(`--- ${f} ---\n${fc.substring(0, 1500)}`);
727
- }
999
+ const score = scoreContent(fc);
1000
+ if (score > 0)
1001
+ localResults.push({ file: f, content: fc.substring(0, 1500), score });
728
1002
  }
729
1003
  catch { }
730
1004
  }
731
1005
  }
732
1006
  catch { }
733
- 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 };
734
1020
  }
735
1021
  catch (err) {
736
1022
  return { output: `Search failed: ${err.message}`, isError: true };
@@ -1007,6 +1293,25 @@ async function extractFileFromZip(buffer, targetName) {
1007
1293
  }
1008
1294
  return null;
1009
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
+ }
1010
1315
  // ─── Turn Summary Builder ────────────────────────────────────────
1011
1316
  function buildTurnSummary(messages, _originalMessage) {
1012
1317
  const textParts = [];