metame-cli 1.4.18 → 1.4.20
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/README.md +124 -38
- package/index.js +39 -1
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +91 -62
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -32
- package/scripts/daemon-command-router.js +32 -15
- package/scripts/daemon-default.yaml +18 -0
- package/scripts/daemon-exec-commands.js +6 -12
- package/scripts/daemon-file-browser.js +6 -5
- package/scripts/daemon-runtime-lifecycle.js +19 -5
- package/scripts/daemon-session-store.js +176 -41
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +43 -6
- package/scripts/distill.js +11 -12
- package/scripts/memory-gc.js +239 -0
- package/scripts/memory-index.js +103 -0
- package/scripts/memory-nightly-reflect.js +299 -0
- package/scripts/memory-write.js +192 -0
- package/scripts/memory.js +144 -6
- package/scripts/schema.js +30 -9
- package/scripts/self-reflect.js +121 -5
- package/scripts/session-analytics.js +9 -10
- package/scripts/task-board.js +9 -3
- package/scripts/telegram-adapter.js +77 -9
|
@@ -106,6 +106,59 @@ function createSessionStore(deps) {
|
|
|
106
106
|
|
|
107
107
|
function invalidateSessionCache() { _sessionCache = null; }
|
|
108
108
|
|
|
109
|
+
// 监听 ~/.claude/projects 目录,手机端新建 session 后桌面端无需重启即可感知
|
|
110
|
+
let _watcher = null;
|
|
111
|
+
let _invalidateDebounce = null;
|
|
112
|
+
|
|
113
|
+
function _debouncedInvalidate() {
|
|
114
|
+
if (_invalidateDebounce) return;
|
|
115
|
+
_invalidateDebounce = setTimeout(() => {
|
|
116
|
+
_sessionCache = null;
|
|
117
|
+
_invalidateDebounce = null;
|
|
118
|
+
}, 500);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function watchSessionFiles() {
|
|
122
|
+
// 先关闭旧 watcher,防止热重载时叠加
|
|
123
|
+
if (_watcher) { try { _watcher.close(); } catch (_) {} _watcher = null; }
|
|
124
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return;
|
|
125
|
+
try {
|
|
126
|
+
_watcher = fs.watch(CLAUDE_PROJECTS_DIR, { recursive: true }, (evt, filename) => {
|
|
127
|
+
if (filename && filename.endsWith('.jsonl')) _debouncedInvalidate();
|
|
128
|
+
});
|
|
129
|
+
_watcher.on('error', (e) => {
|
|
130
|
+
log('WARN', '[session-store] fs.watch error: ' + e.message);
|
|
131
|
+
_watcher = null;
|
|
132
|
+
});
|
|
133
|
+
log('INFO', '[session-store] fs.watch active on ' + CLAUDE_PROJECTS_DIR);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
log('WARN', '[session-store] fs.watch failed, fallback to TTL cache: ' + e.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stopWatchingSessionFiles() {
|
|
140
|
+
if (_watcher) { try { _watcher.close(); } catch (_) {} _watcher = null; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// [M3] 共享辅助:从 reversed JSONL 行数组中提取最后一条外部用户消息(统一规则)
|
|
144
|
+
function extractLastUserFromLines(lines) {
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (!line) continue;
|
|
147
|
+
try {
|
|
148
|
+
const d = JSON.parse(line);
|
|
149
|
+
if (d.type === 'user' && d.message && d.userType !== 'internal') {
|
|
150
|
+
const content = d.message.content;
|
|
151
|
+
let raw = typeof content === 'string' ? content
|
|
152
|
+
: Array.isArray(content) ? (content.find(c => c.type === 'text') || {}).text || '' : '';
|
|
153
|
+
raw = raw.replace(/\[System hints[\s\S]*/i, '')
|
|
154
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
155
|
+
if (raw.length > 2) return raw.slice(0, 80);
|
|
156
|
+
}
|
|
157
|
+
} catch { /* skip */ }
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
109
162
|
function scanAllSessions() {
|
|
110
163
|
if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
|
|
111
164
|
try {
|
|
@@ -162,49 +215,58 @@ function createSessionStore(deps) {
|
|
|
162
215
|
const ENRICH_LIMIT = 20;
|
|
163
216
|
for (let i = 0; i < Math.min(all.length, ENRICH_LIMIT); i++) {
|
|
164
217
|
const s = all[i];
|
|
165
|
-
|
|
218
|
+
// [M1] 用 _enriched 标志替代三字段联合判断
|
|
219
|
+
// customTitle 是可选的,无命名 session 合法值为 undefined,不能作为 skip 条件
|
|
220
|
+
if (s._enriched) continue;
|
|
166
221
|
try {
|
|
167
222
|
const sessionFile = findSessionFile(s.sessionId);
|
|
168
223
|
if (!sessionFile) continue;
|
|
169
224
|
const fd = fs.openSync(sessionFile, 'r');
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
try {
|
|
226
|
+
if (!s.firstPrompt) {
|
|
227
|
+
const headBuf = Buffer.alloc(8192);
|
|
228
|
+
const headBytes = fs.readSync(fd, headBuf, 0, 8192, 0);
|
|
229
|
+
const headStr = headBuf.toString('utf8', 0, headBytes);
|
|
230
|
+
for (const line of headStr.split('\n')) {
|
|
231
|
+
if (!line) continue;
|
|
232
|
+
try {
|
|
233
|
+
const d = JSON.parse(line);
|
|
234
|
+
if (d.type === 'user' && d.message && d.userType !== 'internal') {
|
|
235
|
+
const content = d.message.content;
|
|
236
|
+
let raw = '';
|
|
237
|
+
if (typeof content === 'string') raw = content;
|
|
238
|
+
else if (Array.isArray(content)) {
|
|
239
|
+
const txt = content.find(c => c.type === 'text');
|
|
240
|
+
if (txt) raw = txt.text;
|
|
241
|
+
}
|
|
242
|
+
raw = raw.replace(/\n?\[System hints[\s\S]*/i, '').replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
243
|
+
if (raw && raw.length > 2) { s.firstPrompt = raw.slice(0, 120); break; }
|
|
185
244
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
} catch { /* skip line */ }
|
|
245
|
+
} catch { /* skip line */ }
|
|
246
|
+
}
|
|
190
247
|
}
|
|
191
|
-
|
|
192
|
-
if (!s.customTitle) {
|
|
248
|
+
// 从尾部读取:customTitle + lastUser(256KB 覆盖 tool-heavy session)
|
|
193
249
|
const stat = fs.fstatSync(fd);
|
|
194
|
-
const tailSize = Math.min(
|
|
250
|
+
const tailSize = Math.min(262144, stat.size);
|
|
195
251
|
const tailBuf = Buffer.alloc(tailSize);
|
|
196
252
|
fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
const tailLines = tailBuf.toString('utf8').split('\n').reverse();
|
|
254
|
+
if (!s.customTitle) {
|
|
255
|
+
for (const line of tailLines) {
|
|
256
|
+
if (!line) continue;
|
|
257
|
+
try {
|
|
258
|
+
const d = JSON.parse(line);
|
|
259
|
+
if (d.type === 'custom-title' && d.customTitle) { s.customTitle = d.customTitle; break; }
|
|
260
|
+
} catch { /* skip */ }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!s.lastUser) {
|
|
264
|
+
s.lastUser = extractLastUserFromLines(tailLines);
|
|
205
265
|
}
|
|
266
|
+
} finally {
|
|
267
|
+
fs.closeSync(fd);
|
|
206
268
|
}
|
|
207
|
-
|
|
269
|
+
s._enriched = true; // [M1] 标记已完成富化,下次跳过
|
|
208
270
|
} catch { /* non-fatal */ }
|
|
209
271
|
}
|
|
210
272
|
|
|
@@ -265,20 +327,34 @@ function createSessionStore(deps) {
|
|
|
265
327
|
.replace(/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F\uFFFD\uD800-\uDFFF]/g, '')
|
|
266
328
|
.replace(/\s+/g, ' ')
|
|
267
329
|
.trim();
|
|
330
|
+
|
|
331
|
+
// 优先级:name > summary > tags > firstPrompt > sessionId 前缀
|
|
268
332
|
if (s.customTitle) return sanitize(s.customTitle).slice(0, maxLen);
|
|
333
|
+
|
|
334
|
+
if (s.summary) {
|
|
335
|
+
const t = sanitize(s.summary);
|
|
336
|
+
if (t.length > 2) return t.slice(0, maxLen);
|
|
337
|
+
}
|
|
338
|
+
|
|
269
339
|
const tagEntry = sessionTags && sessionTags[s.sessionId];
|
|
270
340
|
if (tagEntry && tagEntry.name) return sanitize(tagEntry.name).slice(0, maxLen);
|
|
341
|
+
|
|
271
342
|
if (s.firstPrompt) {
|
|
272
343
|
const clean = s.firstPrompt
|
|
273
344
|
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
274
|
-
.replace(
|
|
275
|
-
.replace(
|
|
276
|
-
|
|
345
|
+
.replace(/\[System hints[\s\S]*/i, '')
|
|
346
|
+
.replace(/^[\s\S]*?<\/[^>]+>\s*/s, '') // 剥离 XML 头部标签
|
|
347
|
+
.trim();
|
|
348
|
+
// 取第一行非空、有实际内容(非纯符号/空格)的行
|
|
349
|
+
const firstLine = clean.split('\n')
|
|
350
|
+
.map(l => l.trim())
|
|
351
|
+
.find(l => l.length > 4 && /\p{L}/u.test(l)) || '';
|
|
277
352
|
const sanitized = sanitize(firstLine);
|
|
278
353
|
if (sanitized && sanitized.length > 2) return sanitized.slice(0, maxLen);
|
|
279
354
|
}
|
|
280
|
-
|
|
281
|
-
|
|
355
|
+
|
|
356
|
+
// 最终兜底:显示 session ID 前缀而非空白
|
|
357
|
+
return s.sessionId ? s.sessionId.slice(0, 8) : '';
|
|
282
358
|
}
|
|
283
359
|
|
|
284
360
|
function sessionRichLabel(s, index, sessionTags) {
|
|
@@ -291,11 +367,17 @@ function createSessionStore(deps) {
|
|
|
291
367
|
const shortId = s.sessionId.slice(0, 8);
|
|
292
368
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
|
|
293
369
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
370
|
+
// [M2] 转义 markdown 特殊字符,防止用户历史消息破坏渲染
|
|
371
|
+
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
372
|
+
// fallback to firstPrompt when lastUser not found in tail
|
|
373
|
+
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
374
|
+
let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`; // [M4] title 已有 sessionId 兜底,不会为空
|
|
297
375
|
if (tags.length) line += ` ${tags.map(t => `#${t}`).join(' ')}`;
|
|
298
376
|
line += `\n 📁${proj} · ${ago}`;
|
|
377
|
+
if (snippetRaw && snippetRaw.length > 2) {
|
|
378
|
+
const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
|
|
379
|
+
line += `\n 💬 ${snippet}${snippetRaw.length > 60 ? '…' : ''}`;
|
|
380
|
+
}
|
|
299
381
|
line += `\n /resume ${shortId}`;
|
|
300
382
|
return line;
|
|
301
383
|
}
|
|
@@ -312,8 +394,15 @@ function createSessionStore(deps) {
|
|
|
312
394
|
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
313
395
|
const shortId = s.sessionId.slice(0, 6);
|
|
314
396
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
|
|
315
|
-
|
|
397
|
+
// [M2] 转义 markdown 特殊字符;[M4] title 已有 sessionId 兜底
|
|
398
|
+
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
399
|
+
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
400
|
+
let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago}`;
|
|
316
401
|
if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
|
|
402
|
+
if (snippetRaw && snippetRaw.length > 2) {
|
|
403
|
+
const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
|
|
404
|
+
desc += `\n💬 ${snippet}${snippetRaw.length > 60 ? '…' : ''}`;
|
|
405
|
+
}
|
|
317
406
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
|
|
318
407
|
elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `▶️ Switch #${shortId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
|
|
319
408
|
});
|
|
@@ -393,6 +482,49 @@ function createSessionStore(deps) {
|
|
|
393
482
|
}
|
|
394
483
|
}
|
|
395
484
|
|
|
485
|
+
/**
|
|
486
|
+
* 读取 session 最近一条用户消息 + 最近一条 AI 回复
|
|
487
|
+
* 用于 /resume 后帮助确认切换到正确 session
|
|
488
|
+
*/
|
|
489
|
+
function getSessionRecentContext(sessionId) {
|
|
490
|
+
try {
|
|
491
|
+
const sessionFile = findSessionFile(sessionId);
|
|
492
|
+
if (!sessionFile) return null;
|
|
493
|
+
const stat = fs.statSync(sessionFile);
|
|
494
|
+
const tailSize = Math.min(262144, stat.size); // 256KB for better coverage of tool-heavy sessions
|
|
495
|
+
const buf = Buffer.alloc(tailSize);
|
|
496
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
497
|
+
try {
|
|
498
|
+
fs.readSync(fd, buf, 0, tailSize, stat.size - tailSize);
|
|
499
|
+
} finally {
|
|
500
|
+
fs.closeSync(fd);
|
|
501
|
+
}
|
|
502
|
+
const lines = buf.toString('utf8').split('\n').reverse();
|
|
503
|
+
// [M3] 复用共享函数,统一截取逻辑
|
|
504
|
+
const lastUser = extractLastUserFromLines(lines);
|
|
505
|
+
let lastAssistant = '';
|
|
506
|
+
for (const line of lines) {
|
|
507
|
+
if (!line.trim()) continue;
|
|
508
|
+
try {
|
|
509
|
+
const d = JSON.parse(line);
|
|
510
|
+
if (!lastAssistant && d.type === 'assistant' && d.message) {
|
|
511
|
+
const content = d.message.content;
|
|
512
|
+
if (Array.isArray(content)) {
|
|
513
|
+
for (const c of content) {
|
|
514
|
+
if (c.type === 'text' && c.text && c.text.trim().length > 2) {
|
|
515
|
+
lastAssistant = c.text.trim().slice(0, 80);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (lastAssistant) break;
|
|
522
|
+
} catch { /* skip bad line */ }
|
|
523
|
+
}
|
|
524
|
+
return (lastUser || lastAssistant) ? { lastUser, lastAssistant } : null;
|
|
525
|
+
} catch { return null; }
|
|
526
|
+
}
|
|
527
|
+
|
|
396
528
|
function markSessionStarted(chatId) {
|
|
397
529
|
const state = loadState();
|
|
398
530
|
if (state.sessions[chatId]) {
|
|
@@ -405,6 +537,8 @@ function createSessionStore(deps) {
|
|
|
405
537
|
findSessionFile,
|
|
406
538
|
clearSessionFileCache,
|
|
407
539
|
truncateSessionToCheckpoint,
|
|
540
|
+
watchSessionFiles,
|
|
541
|
+
stopWatchingSessionFiles,
|
|
408
542
|
listRecentSessions,
|
|
409
543
|
loadSessionTags,
|
|
410
544
|
getSessionFileMtime,
|
|
@@ -417,6 +551,7 @@ function createSessionStore(deps) {
|
|
|
417
551
|
getSessionName,
|
|
418
552
|
writeSessionName,
|
|
419
553
|
markSessionStarted,
|
|
554
|
+
getSessionRecentContext,
|
|
420
555
|
};
|
|
421
556
|
}
|
|
422
557
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const { classifyTaskUsage } = require('./usage-classifier');
|
|
4
5
|
|
|
5
6
|
const WEEKDAY_INDEX = Object.freeze({
|
|
@@ -229,25 +230,29 @@ function createTaskScheduler(deps) {
|
|
|
229
230
|
if (!task || !task.memory_log) return;
|
|
230
231
|
try {
|
|
231
232
|
const memory = require('./memory');
|
|
232
|
-
|
|
233
|
-
const projectKey = (task._project && task._project.key) || 'heartbeat';
|
|
233
|
+
memory.acquire();
|
|
234
234
|
const memoryId = `${task.name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
235
|
+
try {
|
|
236
|
+
const nowIso = new Date().toISOString();
|
|
237
|
+
const projectKey = (task._project && task._project.key) || 'heartbeat';
|
|
238
|
+
const summaryText = String(output || '(no output)').trim() || '(no output)';
|
|
239
|
+
const summary = [
|
|
240
|
+
`[heartbeat task] ${task.name}`,
|
|
241
|
+
sessionId ? `session: ${sessionId}` : '',
|
|
242
|
+
summaryText,
|
|
243
|
+
].filter(Boolean).join('\n').slice(0, 8000);
|
|
244
|
+
const keywords = [task.name, 'heartbeat', 'evolution', nowIso.slice(0, 10)].join(',');
|
|
245
|
+
memory.saveSession({
|
|
246
|
+
sessionId: memoryId,
|
|
247
|
+
project: projectKey,
|
|
248
|
+
summary,
|
|
249
|
+
keywords,
|
|
250
|
+
mood: '',
|
|
251
|
+
tokenCost: Number(tokenCost) || 0,
|
|
252
|
+
});
|
|
253
|
+
} finally {
|
|
254
|
+
memory.release();
|
|
255
|
+
}
|
|
251
256
|
log('INFO', `Task ${task.name}: memory_log saved (${memoryId})`);
|
|
252
257
|
} catch (e) {
|
|
253
258
|
log('WARN', `Task ${task.name}: memory_log failed: ${e.message}`);
|
|
@@ -281,7 +286,7 @@ function createTaskScheduler(deps) {
|
|
|
281
286
|
|
|
282
287
|
// Workflow tasks: multi-step skill chain via --resume session
|
|
283
288
|
if (task.type === 'workflow') {
|
|
284
|
-
return executeWorkflow(task, config);
|
|
289
|
+
return executeWorkflow(task, config, precheck);
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
// Script tasks: run a local script directly (e.g. distill.js), no claude -p
|
|
@@ -488,18 +493,13 @@ function createTaskScheduler(deps) {
|
|
|
488
493
|
|
|
489
494
|
// parseInterval — imported from ./utils
|
|
490
495
|
|
|
491
|
-
function executeWorkflow(task, config) {
|
|
496
|
+
function executeWorkflow(task, config, precheck) {
|
|
492
497
|
const state = loadState();
|
|
493
498
|
if (!checkBudget(config, state)) {
|
|
494
499
|
log('WARN', `Budget exceeded, skipping workflow: ${task.name}`);
|
|
495
500
|
return { success: false, error: 'budget_exceeded', output: '' };
|
|
496
501
|
}
|
|
497
|
-
|
|
498
|
-
if (!precheck.pass) {
|
|
499
|
-
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'skipped', output_preview: 'Precondition not met' };
|
|
500
|
-
saveState(state);
|
|
501
|
-
return { success: true, output: '(skipped)', skipped: true };
|
|
502
|
-
}
|
|
502
|
+
// precheck.pass is guaranteed true here — executeTask() already returns early when false
|
|
503
503
|
const steps = task.steps || [];
|
|
504
504
|
if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
|
|
505
505
|
|
|
@@ -518,6 +518,7 @@ function createTaskScheduler(deps) {
|
|
|
518
518
|
|
|
519
519
|
log('INFO', `Workflow ${task.name}: ${steps.length} steps, session ${sessionId.slice(0, 8)}${mcpConfig ? ', mcp: ' + path.basename(mcpConfig) : ''}`);
|
|
520
520
|
|
|
521
|
+
let loopState = loadState();
|
|
521
522
|
for (let i = 0; i < steps.length; i++) {
|
|
522
523
|
const step = steps[i];
|
|
523
524
|
let prompt = (step.skill ? `/${step.skill} ` : '') + (step.prompt || '');
|
|
@@ -546,19 +547,19 @@ function createTaskScheduler(deps) {
|
|
|
546
547
|
totalTokens += tk;
|
|
547
548
|
outputs.push({ step: i + 1, skill: step.skill || null, output: output.slice(0, 500), tokens: tk });
|
|
548
549
|
log('INFO', `Workflow ${task.name} step ${i + 1} done (${tk} tokens)`);
|
|
549
|
-
if (!checkBudget(config,
|
|
550
|
+
if (!checkBudget(config, loopState)) { log('WARN', 'Budget exceeded mid-workflow'); break; }
|
|
550
551
|
} catch (e) {
|
|
551
552
|
log('ERROR', `Workflow ${task.name} step ${i + 1} failed: ${e.message.slice(0, 200)}`);
|
|
552
553
|
outputs.push({ step: i + 1, skill: step.skill || null, error: e.message.slice(0, 200) });
|
|
553
554
|
if (!step.optional) {
|
|
554
|
-
recordTokens(
|
|
555
|
+
recordTokens(loopState, totalTokens, { category: classifyTaskUsage(task) });
|
|
555
556
|
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'error', error: `Step ${i + 1} failed`, steps_completed: i, steps_total: steps.length };
|
|
556
557
|
saveState(state);
|
|
557
558
|
return { success: false, error: `Step ${i + 1} failed`, output: outputs.map(o => `Step ${o.step}: ${o.error ? 'FAILED' : 'OK'}`).join('\n'), tokens: totalTokens };
|
|
558
559
|
}
|
|
559
560
|
}
|
|
560
561
|
}
|
|
561
|
-
recordTokens(
|
|
562
|
+
recordTokens(loopState, totalTokens, { category: classifyTaskUsage(task) });
|
|
562
563
|
const lastOk = [...outputs].reverse().find(o => !o.error);
|
|
563
564
|
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'success', output_preview: (lastOk ? lastOk.output : '').slice(0, 200), steps_completed: outputs.filter(o => !o.error).length, steps_total: steps.length };
|
|
564
565
|
saveState(state);
|