@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.
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +356 -51
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-client.d.ts.map +1 -1
- package/dist/agent/claude-client.js +16 -9
- package/dist/agent/claude-client.js.map +1 -1
- package/dist/agent/compaction.d.ts +33 -1
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +512 -48
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/tool-defs.js +1 -1
- package/dist/agent/tool-defs.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/worker-proxy.d.ts +7 -2
- package/dist/agent/worker-proxy.d.ts.map +1 -1
- package/dist/agent/worker-proxy.js +5 -2
- package/dist/agent/worker-proxy.js.map +1 -1
- package/dist/vortex-tunnel.d.ts.map +1 -1
- package/dist/vortex-tunnel.js +13 -1
- package/dist/vortex-tunnel.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../src/agent/agent-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
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"}
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
685
|
-
if (
|
|
686
|
-
|
|
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
|
|
952
|
+
console.warn(`[Memory] Vectorize search failed: ${err.message}`);
|
|
692
953
|
}
|
|
693
|
-
//
|
|
694
|
-
const
|
|
695
|
-
const
|
|
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
|
-
|
|
699
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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 = [];
|