metame-cli 1.4.15 → 1.4.18
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 +9 -6
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +441 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +71 -22
- package/scripts/daemon-command-router.js +242 -3
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +216 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +374 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +288 -38
- package/scripts/skill-evolution.test.js +107 -0
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
package/scripts/daemon.js
CHANGED
|
@@ -49,14 +49,38 @@ try { skillEvolution = require('./skill-evolution'); } catch { /* graceful fallb
|
|
|
49
49
|
// ---------------------------------------------------------
|
|
50
50
|
// SKILL ROUTING (keyword → /skillname prefix, like metame-desktop)
|
|
51
51
|
// ---------------------------------------------------------
|
|
52
|
+
function isMacLocalOrchestratorIntent(prompt) {
|
|
53
|
+
const text = String(prompt || '').trim();
|
|
54
|
+
if (!text) return false;
|
|
55
|
+
|
|
56
|
+
// Explicit macOS automation keywords.
|
|
57
|
+
if (/\b(?:mac|macos|applescript|osascript|jxa|hammerspoon|aerospace|yabai|skhd|raycast|launchctl|keyboard maestro)\b/i.test(text)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (/(自动化|辅助功能|系统设置|隐私|权限|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|音量)/.test(text)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// General verbs must be paired with explicit macOS targets to avoid over-routing.
|
|
65
|
+
const hasAction = /(?:打开|关闭|启动|退出|切到|唤起|锁屏|锁定屏幕|睡眠|休眠|静音|取消静音|调(?:高|低|整)?音量|open|launch|quit|activate|lock\s*screen|sleep|mute|unmute)/i.test(text);
|
|
66
|
+
const hasTarget = /(?:微信|WeChat|飞书|Feishu|Finder|Safari|Terminal|iTerm|系统设置|System Settings|电脑|System Events|mac)/i.test(text);
|
|
67
|
+
return hasAction && hasTarget;
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
const SKILL_ROUTES = [
|
|
53
71
|
{ name: 'macos-mail-calendar', pattern: /邮件|邮箱|收件箱|日历|日程|会议|schedule|email|mail|calendar|unread|inbox/i },
|
|
72
|
+
{ name: 'macos-local-orchestrator', match: isMacLocalOrchestratorIntent },
|
|
54
73
|
{ name: 'heartbeat-task-manager', pattern: /提醒|remind|闹钟|定时|每[天周月]/i },
|
|
74
|
+
{ name: 'skill-manager', pattern: /找技能|管理技能|更新技能|安装技能|skill manager|skill scout|(?:find|look for)\s+skills?/i },
|
|
75
|
+
{ name: 'skill-evolution-manager', pattern: /\/evolve\b|复盘一下|记录一下(这个)?经验|保存到\s*skill|skill evolution/i },
|
|
55
76
|
];
|
|
56
77
|
|
|
57
78
|
function routeSkill(prompt) {
|
|
58
79
|
for (const r of SKILL_ROUTES) {
|
|
59
|
-
|
|
80
|
+
const matched = typeof r.match === 'function'
|
|
81
|
+
? r.match(prompt)
|
|
82
|
+
: (r.pattern ? r.pattern.test(prompt) : false);
|
|
83
|
+
if (matched) return r.name;
|
|
60
84
|
}
|
|
61
85
|
return null;
|
|
62
86
|
}
|
|
@@ -79,6 +103,12 @@ function routeAgent(prompt, config) {
|
|
|
79
103
|
|
|
80
104
|
const yaml = require('./resolve-yaml');
|
|
81
105
|
const { parseInterval, formatRelativeTime, createPathMap } = require('./utils');
|
|
106
|
+
const {
|
|
107
|
+
USAGE_RETENTION_DAYS_DEFAULT,
|
|
108
|
+
normalizeUsageCategory,
|
|
109
|
+
} = require('./usage-classifier');
|
|
110
|
+
const { createTaskBoard } = require('./task-board');
|
|
111
|
+
const taskEnvelope = require('./daemon-task-envelope');
|
|
82
112
|
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
83
113
|
const { createExecCommandHandler } = require('./daemon-exec-commands');
|
|
84
114
|
const { createOpsCommandHandler } = require('./daemon-ops-commands');
|
|
@@ -229,19 +259,56 @@ function restoreConfig() {
|
|
|
229
259
|
|
|
230
260
|
let _cachedState = null;
|
|
231
261
|
|
|
262
|
+
function ensureUsageShape(state) {
|
|
263
|
+
if (!state.usage || typeof state.usage !== 'object') state.usage = {};
|
|
264
|
+
if (!state.usage.categories || typeof state.usage.categories !== 'object') state.usage.categories = {};
|
|
265
|
+
if (!state.usage.daily || typeof state.usage.daily !== 'object') state.usage.daily = {};
|
|
266
|
+
const keepDays = Number(state.usage.retention_days);
|
|
267
|
+
state.usage.retention_days = Number.isFinite(keepDays) && keepDays >= 7
|
|
268
|
+
? Math.floor(keepDays)
|
|
269
|
+
: USAGE_RETENTION_DAYS_DEFAULT;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function ensureStateShape(state) {
|
|
273
|
+
if (!state || typeof state !== 'object') return {
|
|
274
|
+
pid: null,
|
|
275
|
+
budget: { date: null, tokens_used: 0 },
|
|
276
|
+
tasks: {},
|
|
277
|
+
sessions: {},
|
|
278
|
+
started_at: null,
|
|
279
|
+
usage: { retention_days: USAGE_RETENTION_DAYS_DEFAULT, categories: {}, daily: {} },
|
|
280
|
+
};
|
|
281
|
+
if (!state.budget || typeof state.budget !== 'object') state.budget = { date: null, tokens_used: 0 };
|
|
282
|
+
if (typeof state.budget.tokens_used !== 'number') state.budget.tokens_used = Number(state.budget.tokens_used) || 0;
|
|
283
|
+
if (!Object.prototype.hasOwnProperty.call(state.budget, 'date')) state.budget.date = null;
|
|
284
|
+
if (!state.tasks || typeof state.tasks !== 'object') state.tasks = {};
|
|
285
|
+
if (!state.sessions || typeof state.sessions !== 'object') state.sessions = {};
|
|
286
|
+
ensureUsageShape(state);
|
|
287
|
+
return state;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function pruneDailyUsage(usage, todayIso) {
|
|
291
|
+
const keepDays = usage.retention_days || USAGE_RETENTION_DAYS_DEFAULT;
|
|
292
|
+
const cutoff = new Date(`${todayIso}T00:00:00.000Z`);
|
|
293
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - (keepDays - 1));
|
|
294
|
+
const cutoffIso = cutoff.toISOString().slice(0, 10);
|
|
295
|
+
for (const day of Object.keys(usage.daily || {})) {
|
|
296
|
+
if (day < cutoffIso) delete usage.daily[day];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
232
300
|
function _readStateFromDisk() {
|
|
233
301
|
try {
|
|
234
302
|
const s = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
235
|
-
|
|
236
|
-
return s;
|
|
303
|
+
return ensureStateShape(s);
|
|
237
304
|
} catch {
|
|
238
|
-
return {
|
|
305
|
+
return ensureStateShape({
|
|
239
306
|
pid: null,
|
|
240
307
|
budget: { date: null, tokens_used: 0 },
|
|
241
308
|
tasks: {},
|
|
242
309
|
sessions: {},
|
|
243
310
|
started_at: null,
|
|
244
|
-
};
|
|
311
|
+
});
|
|
245
312
|
}
|
|
246
313
|
}
|
|
247
314
|
|
|
@@ -251,9 +318,65 @@ function loadState() {
|
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
function saveState(state) {
|
|
254
|
-
|
|
321
|
+
const next = ensureStateShape(state);
|
|
322
|
+
if (_cachedState && _cachedState !== next) {
|
|
323
|
+
const current = ensureStateShape(_cachedState);
|
|
324
|
+
|
|
325
|
+
const currentBudgetDate = String(current.budget.date || '');
|
|
326
|
+
const nextBudgetDate = String(next.budget.date || '');
|
|
327
|
+
const currentBudgetTokens = Math.max(0, Math.floor(Number(current.budget.tokens_used) || 0));
|
|
328
|
+
const nextBudgetTokens = Math.max(0, Math.floor(Number(next.budget.tokens_used) || 0));
|
|
329
|
+
if (currentBudgetDate && (!nextBudgetDate || currentBudgetDate > nextBudgetDate)) {
|
|
330
|
+
next.budget.date = currentBudgetDate;
|
|
331
|
+
next.budget.tokens_used = currentBudgetTokens;
|
|
332
|
+
} else if (currentBudgetDate && currentBudgetDate === nextBudgetDate) {
|
|
333
|
+
next.budget.tokens_used = Math.max(currentBudgetTokens, nextBudgetTokens);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const currentKeepDays = Number(current.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
|
|
337
|
+
const nextKeepDays = Number(next.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
|
|
338
|
+
next.usage.retention_days = Math.max(currentKeepDays, nextKeepDays);
|
|
339
|
+
|
|
340
|
+
for (const [category, curMeta] of Object.entries(current.usage.categories || {})) {
|
|
341
|
+
if (!next.usage.categories[category] || typeof next.usage.categories[category] !== 'object') {
|
|
342
|
+
next.usage.categories[category] = {};
|
|
343
|
+
}
|
|
344
|
+
const curTotal = Math.max(0, Math.floor(Number(curMeta && curMeta.total) || 0));
|
|
345
|
+
const nextTotal = Math.max(0, Math.floor(Number(next.usage.categories[category].total) || 0));
|
|
346
|
+
if (curTotal > nextTotal) next.usage.categories[category].total = curTotal;
|
|
347
|
+
|
|
348
|
+
const curUpdated = String(curMeta && curMeta.updated_at || '');
|
|
349
|
+
const nextUpdated = String(next.usage.categories[category].updated_at || '');
|
|
350
|
+
if (curUpdated && curUpdated > nextUpdated) next.usage.categories[category].updated_at = curUpdated;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const [day, curDayUsageRaw] of Object.entries(current.usage.daily || {})) {
|
|
354
|
+
const curDayUsage = (curDayUsageRaw && typeof curDayUsageRaw === 'object') ? curDayUsageRaw : {};
|
|
355
|
+
if (!next.usage.daily[day] || typeof next.usage.daily[day] !== 'object') {
|
|
356
|
+
next.usage.daily[day] = {};
|
|
357
|
+
}
|
|
358
|
+
const nextDayUsage = next.usage.daily[day];
|
|
359
|
+
for (const [key, curValue] of Object.entries(curDayUsage)) {
|
|
360
|
+
const curNum = Math.max(0, Math.floor(Number(curValue) || 0));
|
|
361
|
+
const nextNum = Math.max(0, Math.floor(Number(nextDayUsage[key]) || 0));
|
|
362
|
+
if (curNum > nextNum) nextDayUsage[key] = curNum;
|
|
363
|
+
}
|
|
364
|
+
const categorySum = Object.entries(nextDayUsage)
|
|
365
|
+
.filter(([key]) => key !== 'total')
|
|
366
|
+
.reduce((sum, [, value]) => sum + Math.max(0, Math.floor(Number(value) || 0)), 0);
|
|
367
|
+
nextDayUsage.total = Math.max(Math.max(0, Math.floor(Number(nextDayUsage.total) || 0)), categorySum);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const currentUsageUpdated = String(current.usage.updated_at || '');
|
|
371
|
+
const nextUsageUpdated = String(next.usage.updated_at || '');
|
|
372
|
+
if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
|
|
373
|
+
next.usage.updated_at = currentUsageUpdated;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_cachedState = next;
|
|
255
378
|
try {
|
|
256
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(
|
|
379
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
257
380
|
} catch (e) {
|
|
258
381
|
log('ERROR', `Failed to save state: ${e.message}`);
|
|
259
382
|
}
|
|
@@ -287,6 +410,7 @@ function buildProfilePreamble() {
|
|
|
287
410
|
// BUDGET TRACKING
|
|
288
411
|
// ---------------------------------------------------------
|
|
289
412
|
function checkBudget(config, state) {
|
|
413
|
+
state = ensureStateShape(state);
|
|
290
414
|
const today = new Date().toISOString().slice(0, 10);
|
|
291
415
|
if (state.budget.date !== today) {
|
|
292
416
|
state.budget.date = today;
|
|
@@ -297,14 +421,44 @@ function checkBudget(config, state) {
|
|
|
297
421
|
return state.budget.tokens_used < limit;
|
|
298
422
|
}
|
|
299
423
|
|
|
300
|
-
function recordTokens(state, tokens) {
|
|
424
|
+
function recordTokens(state, tokens, meta = null) {
|
|
425
|
+
const amount = Math.max(0, Math.floor(Number(tokens) || 0));
|
|
426
|
+
if (!amount) return;
|
|
427
|
+
|
|
428
|
+
const liveState = ensureStateShape(loadState());
|
|
301
429
|
const today = new Date().toISOString().slice(0, 10);
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
430
|
+
if (liveState.budget.date !== today) {
|
|
431
|
+
liveState.budget.date = today;
|
|
432
|
+
liveState.budget.tokens_used = 0;
|
|
305
433
|
}
|
|
306
|
-
|
|
307
|
-
|
|
434
|
+
liveState.budget.tokens_used += amount;
|
|
435
|
+
|
|
436
|
+
const category = normalizeUsageCategory(meta && meta.category, {
|
|
437
|
+
logger: (msg) => log('WARN', `[USAGE] ${msg}`),
|
|
438
|
+
});
|
|
439
|
+
ensureUsageShape(liveState);
|
|
440
|
+
|
|
441
|
+
if (!liveState.usage.categories[category] || typeof liveState.usage.categories[category] !== 'object') {
|
|
442
|
+
liveState.usage.categories[category] = { total: 0 };
|
|
443
|
+
}
|
|
444
|
+
liveState.usage.categories[category].total = (Number(liveState.usage.categories[category].total) || 0) + amount;
|
|
445
|
+
liveState.usage.categories[category].updated_at = new Date().toISOString();
|
|
446
|
+
|
|
447
|
+
if (!liveState.usage.daily[today] || typeof liveState.usage.daily[today] !== 'object') {
|
|
448
|
+
liveState.usage.daily[today] = { total: 0 };
|
|
449
|
+
}
|
|
450
|
+
const dayUsage = liveState.usage.daily[today];
|
|
451
|
+
dayUsage.total = (Number(dayUsage.total) || 0) + amount;
|
|
452
|
+
dayUsage[category] = (Number(dayUsage[category]) || 0) + amount;
|
|
453
|
+
liveState.usage.updated_at = new Date().toISOString();
|
|
454
|
+
pruneDailyUsage(liveState.usage, today);
|
|
455
|
+
|
|
456
|
+
if (state && typeof state === 'object' && state !== liveState) {
|
|
457
|
+
state.budget = liveState.budget;
|
|
458
|
+
state.usage = liveState.usage;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
saveState(liveState);
|
|
308
462
|
}
|
|
309
463
|
|
|
310
464
|
|
|
@@ -317,6 +471,10 @@ function getBudgetWarning(config, state) {
|
|
|
317
471
|
return 'ok';
|
|
318
472
|
}
|
|
319
473
|
|
|
474
|
+
const taskBoard = createTaskBoard({
|
|
475
|
+
logger: (msg) => log('WARN', msg),
|
|
476
|
+
});
|
|
477
|
+
|
|
320
478
|
// ---------------------------------------------------------
|
|
321
479
|
// AGENT DISPATCH — virtual chatId inter-agent communication
|
|
322
480
|
// ---------------------------------------------------------
|
|
@@ -349,25 +507,29 @@ function createNullBot(onOutput) {
|
|
|
349
507
|
* Forward bot: routes all calls to a real bot with a fixed chatId.
|
|
350
508
|
* Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
|
|
351
509
|
*/
|
|
352
|
-
function createStreamForwardBot(realBot, chatId) {
|
|
510
|
+
function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
353
511
|
// Track edit-broken state independently so dispatch failures don't poison realBot's flag
|
|
354
512
|
let _editBroken = false;
|
|
355
513
|
return {
|
|
356
514
|
sendMessage: async (_, text) => {
|
|
357
515
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
|
|
516
|
+
if (onOutput) onOutput(text);
|
|
358
517
|
return realBot.sendMessage(chatId, text);
|
|
359
518
|
},
|
|
360
519
|
sendMarkdown: async (_, text) => {
|
|
361
520
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] md: ${String(text).slice(0, 80)}`);
|
|
521
|
+
if (onOutput) onOutput(text);
|
|
362
522
|
return realBot.sendMarkdown(chatId, text);
|
|
363
523
|
},
|
|
364
524
|
sendCard: async (_, card) => {
|
|
365
525
|
const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
|
|
366
526
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
|
|
527
|
+
if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
|
|
367
528
|
return realBot.sendCard(chatId, card);
|
|
368
529
|
},
|
|
369
530
|
sendRawCard: async (_, header, elements) => {
|
|
370
531
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
|
|
532
|
+
if (onOutput) onOutput(header);
|
|
371
533
|
return realBot.sendRawCard(chatId, header, elements);
|
|
372
534
|
},
|
|
373
535
|
sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
|
|
@@ -391,6 +553,76 @@ function createStreamForwardBot(realBot, chatId) {
|
|
|
391
553
|
};
|
|
392
554
|
}
|
|
393
555
|
|
|
556
|
+
function extractArtifactPaths(text) {
|
|
557
|
+
const out = new Set();
|
|
558
|
+
const src = String(text || '');
|
|
559
|
+
const re = /\[\[FILE:([^\]]+)\]\]/g;
|
|
560
|
+
let m;
|
|
561
|
+
while ((m = re.exec(src)) !== null) {
|
|
562
|
+
const v = String(m[1] || '').trim();
|
|
563
|
+
if (v) out.add(v.slice(0, 500));
|
|
564
|
+
}
|
|
565
|
+
return [...out];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function inferTaskStatusFromOutput(text) {
|
|
569
|
+
const s = String(text || '');
|
|
570
|
+
if (/(^|\b)(blocked|卡住|阻塞|waiting for|等待)(\b|$)/i.test(s)) return 'blocked';
|
|
571
|
+
if (/(^|\b)(failed|失败|error|异常|报错)(\b|$)/i.test(s)) return 'failed';
|
|
572
|
+
return 'done';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function summarizeTaskInputs(inputs) {
|
|
576
|
+
if (!inputs || typeof inputs !== 'object') return '(none)';
|
|
577
|
+
const lines = [];
|
|
578
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
579
|
+
if (typeof v === 'string') lines.push(`- ${k}: ${v}`);
|
|
580
|
+
else lines.push(`- ${k}: ${JSON.stringify(v)}`);
|
|
581
|
+
if (lines.length >= 8) break;
|
|
582
|
+
}
|
|
583
|
+
return lines.length > 0 ? lines.join('\n') : '(none)';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function buildDispatchChatId(targetProject, scopeId) {
|
|
587
|
+
const target = String(targetProject || '').trim();
|
|
588
|
+
if (!target) return '_agent_unknown';
|
|
589
|
+
const safeScope = taskEnvelope && taskEnvelope.normalizeScopeId
|
|
590
|
+
? taskEnvelope.normalizeScopeId(scopeId, '')
|
|
591
|
+
: '';
|
|
592
|
+
if (safeScope) return `_scope_${safeScope}__${target}`;
|
|
593
|
+
return `_agent_${target}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function buildPromptFromTaskEnvelope(envelope, fallbackPrompt) {
|
|
597
|
+
const goal = envelope.goal || fallbackPrompt || 'No goal provided';
|
|
598
|
+
const dod = Array.isArray(envelope.definition_of_done) && envelope.definition_of_done.length > 0
|
|
599
|
+
? envelope.definition_of_done.map((x, i) => `${i + 1}. ${x}`).join('\n')
|
|
600
|
+
: '1. 给出可执行的结果与关键结论\n2. 给出相关产物路径(如有)';
|
|
601
|
+
const inputs = summarizeTaskInputs(envelope.inputs || {});
|
|
602
|
+
const taskId = envelope.task_id || 'unknown';
|
|
603
|
+
const scopeId = envelope.scope_id || taskId;
|
|
604
|
+
const participants = Array.isArray(envelope.participants) && envelope.participants.length > 0
|
|
605
|
+
? envelope.participants.join(', ')
|
|
606
|
+
: `${envelope.from_agent || 'unknown'}, ${envelope.to_agent || 'unknown'}`;
|
|
607
|
+
return [
|
|
608
|
+
`任务ID: ${taskId}`,
|
|
609
|
+
`协作Scope: ${scopeId}`,
|
|
610
|
+
`参与Agent: ${participants}`,
|
|
611
|
+
`任务目标: ${goal}`,
|
|
612
|
+
'',
|
|
613
|
+
'完成标准 (DoD):',
|
|
614
|
+
dod,
|
|
615
|
+
'',
|
|
616
|
+
'输入上下文:',
|
|
617
|
+
inputs,
|
|
618
|
+
'',
|
|
619
|
+
'执行要求:',
|
|
620
|
+
'1. 先用1-2句“计划:...”说明方案',
|
|
621
|
+
'2. 再执行任务',
|
|
622
|
+
'3. 结尾给出“结果摘要:...”和“产物:...”',
|
|
623
|
+
].join('\n');
|
|
624
|
+
}
|
|
625
|
+
|
|
394
626
|
/**
|
|
395
627
|
* Dispatch a task/message to another agent via virtual chatId.
|
|
396
628
|
* @param {string} targetProject - project key (e.g. 'digital_me', 'desktop')
|
|
@@ -400,17 +632,56 @@ function createStreamForwardBot(realBot, chatId) {
|
|
|
400
632
|
*/
|
|
401
633
|
function dispatchTask(targetProject, message, config, replyFn, streamOptions = null) {
|
|
402
634
|
const LIMITS = { max_per_hour_per_target: 20, max_total_per_hour: 60, max_depth: 2 };
|
|
635
|
+
const payload = (message && message.payload && typeof message.payload === 'object')
|
|
636
|
+
? message.payload
|
|
637
|
+
: {};
|
|
638
|
+
|
|
639
|
+
let envelope = null;
|
|
640
|
+
if (payload.task_envelope) {
|
|
641
|
+
try {
|
|
642
|
+
envelope = taskEnvelope.normalizeTaskEnvelope(payload.task_envelope, {
|
|
643
|
+
from_agent: message.from || 'unknown',
|
|
644
|
+
to_agent: targetProject,
|
|
645
|
+
task_kind: payload.task_envelope && payload.task_envelope.task_kind ? payload.task_envelope.task_kind : 'team',
|
|
646
|
+
});
|
|
647
|
+
const checked = taskEnvelope.validateTaskEnvelope(envelope);
|
|
648
|
+
if (!checked.ok) {
|
|
649
|
+
log('WARN', `Dispatch blocked: invalid task_envelope (${checked.error})`);
|
|
650
|
+
return { success: false, error: `invalid_task_envelope:${checked.error}` };
|
|
651
|
+
}
|
|
652
|
+
} catch (e) {
|
|
653
|
+
log('WARN', `Dispatch blocked: task_envelope parse failed (${e.message})`);
|
|
654
|
+
return { success: false, error: 'invalid_task_envelope' };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const markTaskBlocked = (reason) => {
|
|
659
|
+
if (!envelope || !taskBoard) return;
|
|
660
|
+
const nowIso = new Date().toISOString();
|
|
661
|
+
taskBoard.upsertTask({
|
|
662
|
+
...envelope,
|
|
663
|
+
status: 'blocked',
|
|
664
|
+
last_error: reason,
|
|
665
|
+
updated_at: nowIso,
|
|
666
|
+
});
|
|
667
|
+
taskBoard.appendTaskEvent(envelope.task_id, 'dispatch_blocked', message.from || 'system', {
|
|
668
|
+
reason,
|
|
669
|
+
target: targetProject,
|
|
670
|
+
});
|
|
671
|
+
};
|
|
403
672
|
|
|
404
673
|
// Anti-storm: check chain depth
|
|
405
674
|
const chain = message.chain || [];
|
|
406
675
|
if (chain.length >= LIMITS.max_depth) {
|
|
407
676
|
log('WARN', `Dispatch blocked: max depth ${LIMITS.max_depth} reached (chain: ${chain.join('→')})`);
|
|
677
|
+
markTaskBlocked('max_depth_exceeded');
|
|
408
678
|
return { success: false, error: 'max_depth_exceeded' };
|
|
409
679
|
}
|
|
410
680
|
|
|
411
681
|
// Anti-storm: check for cycles
|
|
412
682
|
if (chain.includes(targetProject)) {
|
|
413
683
|
log('WARN', `Dispatch blocked: cycle detected (${chain.join('→')}→${targetProject})`);
|
|
684
|
+
markTaskBlocked('cycle_detected');
|
|
414
685
|
return { success: false, error: 'cycle_detected' };
|
|
415
686
|
}
|
|
416
687
|
|
|
@@ -425,10 +696,12 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
425
696
|
const toTarget = recent.filter(e => e.to === targetProject).length;
|
|
426
697
|
if (toTarget >= LIMITS.max_per_hour_per_target) {
|
|
427
698
|
log('WARN', `Dispatch blocked: rate limit to ${targetProject} (${toTarget}/${LIMITS.max_per_hour_per_target} per hour)`);
|
|
699
|
+
markTaskBlocked('rate_limit_target');
|
|
428
700
|
return { success: false, error: 'rate_limit_target' };
|
|
429
701
|
}
|
|
430
702
|
if (recent.length >= LIMITS.max_total_per_hour) {
|
|
431
703
|
log('WARN', `Dispatch blocked: total rate limit (${recent.length}/${LIMITS.max_total_per_hour} per hour)`);
|
|
704
|
+
markTaskBlocked('rate_limit_total');
|
|
432
705
|
return { success: false, error: 'rate_limit_total' };
|
|
433
706
|
}
|
|
434
707
|
}
|
|
@@ -438,6 +711,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
438
711
|
|
|
439
712
|
if (!_handleCommand) {
|
|
440
713
|
log('WARN', 'Dispatch: handleCommand not yet bound, dropping task');
|
|
714
|
+
markTaskBlocked('handler_not_ready');
|
|
441
715
|
return { success: false, error: 'handler_not_ready' };
|
|
442
716
|
}
|
|
443
717
|
|
|
@@ -447,18 +721,52 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
447
721
|
to: targetProject,
|
|
448
722
|
type: message.type || 'task',
|
|
449
723
|
priority: message.priority || 'normal',
|
|
450
|
-
payload
|
|
724
|
+
payload,
|
|
451
725
|
callback: message.callback || false,
|
|
452
726
|
new_session: !!message.new_session,
|
|
453
727
|
chain: [...chain, message.from || 'unknown'],
|
|
728
|
+
task_id: envelope ? envelope.task_id : null,
|
|
729
|
+
scope_id: envelope ? envelope.scope_id : null,
|
|
454
730
|
created_at: new Date().toISOString(),
|
|
455
731
|
};
|
|
456
732
|
|
|
733
|
+
if (envelope && taskBoard) {
|
|
734
|
+
const nowIso = new Date().toISOString();
|
|
735
|
+
taskBoard.upsertTask({
|
|
736
|
+
...envelope,
|
|
737
|
+
status: 'queued',
|
|
738
|
+
updated_at: nowIso,
|
|
739
|
+
});
|
|
740
|
+
taskBoard.appendTaskEvent(envelope.task_id, 'dispatch_enqueued', fullMsg.from || 'system', {
|
|
741
|
+
dispatch_id: fullMsg.id,
|
|
742
|
+
target: targetProject,
|
|
743
|
+
priority: fullMsg.priority,
|
|
744
|
+
scope_id: envelope.scope_id,
|
|
745
|
+
});
|
|
746
|
+
taskBoard.recordHandoff({
|
|
747
|
+
handoff_id: taskEnvelope.newHandoffId(),
|
|
748
|
+
task_id: envelope.task_id,
|
|
749
|
+
from_agent: envelope.from_agent || fullMsg.from || 'unknown',
|
|
750
|
+
to_agent: targetProject,
|
|
751
|
+
payload: {
|
|
752
|
+
dispatch_id: fullMsg.id,
|
|
753
|
+
scope_id: envelope.scope_id,
|
|
754
|
+
title: payload.title || '',
|
|
755
|
+
prompt: payload.prompt || '',
|
|
756
|
+
},
|
|
757
|
+
status: 'sent',
|
|
758
|
+
created_at: nowIso,
|
|
759
|
+
updated_at: nowIso,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
457
763
|
// Write to dispatch log for audit / rate-limiting
|
|
458
764
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
459
765
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
460
766
|
|
|
461
|
-
const rawPrompt =
|
|
767
|
+
const rawPrompt = envelope
|
|
768
|
+
? buildPromptFromTaskEnvelope(envelope, fullMsg.payload.prompt || fullMsg.payload.title || '')
|
|
769
|
+
: (fullMsg.payload.prompt || fullMsg.payload.title || 'No prompt provided');
|
|
462
770
|
|
|
463
771
|
// Inject sender identity when dispatched by another agent (not directly from user)
|
|
464
772
|
const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
@@ -476,18 +784,35 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
476
784
|
// Daemon sends the ack autonomously; Claude should just state its plan in the reply text.
|
|
477
785
|
prompt = `[行为要求:回复开头用1-2句「计划:xxx」说明执行方案,再开始执行。不要调用 dispatch_to,daemon 会自动转发你的回复。]\n\n${prompt}`;
|
|
478
786
|
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
787
|
+
// team task with scope_id uses scoped virtual chatId:
|
|
788
|
+
// _scope_<scope_id>__<agent>
|
|
789
|
+
// which allows N-agent collaboration under the same task scope while
|
|
790
|
+
// keeping per-agent execution sessions isolated.
|
|
483
791
|
const forceNew = !!fullMsg.new_session;
|
|
484
|
-
const dispatchChatId =
|
|
792
|
+
const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
|
|
485
793
|
const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
|
|
486
794
|
log('INFO', `Dispatching ${fullMsg.type} to ${targetProject} via ${sessionMode}: ${rawPrompt.slice(0, 80)}`);
|
|
487
795
|
|
|
796
|
+
let _taskFinalized = false;
|
|
488
797
|
const outputHandler = (output) => {
|
|
489
798
|
const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
|
|
490
799
|
log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
|
|
800
|
+
if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
|
|
801
|
+
const status = inferTaskStatusFromOutput(outStr);
|
|
802
|
+
const artifacts = extractArtifactPaths(outStr);
|
|
803
|
+
const update = {
|
|
804
|
+
summary: outStr.slice(0, 1200),
|
|
805
|
+
artifacts,
|
|
806
|
+
};
|
|
807
|
+
if (status === 'failed') update.last_error = outStr.slice(0, 400);
|
|
808
|
+
taskBoard.markTaskStatus(envelope.task_id, status, update);
|
|
809
|
+
taskBoard.appendTaskEvent(envelope.task_id, 'task_result', targetProject, {
|
|
810
|
+
status,
|
|
811
|
+
preview: outStr.slice(0, 240),
|
|
812
|
+
artifact_count: artifacts.length,
|
|
813
|
+
});
|
|
814
|
+
_taskFinalized = true;
|
|
815
|
+
}
|
|
491
816
|
if (replyFn && outStr.trim().length > 2) {
|
|
492
817
|
replyFn(outStr);
|
|
493
818
|
} else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
|
|
@@ -507,7 +832,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
507
832
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
508
833
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
509
834
|
const nullBot = streamOptions?.bot && streamOptions?.chatId
|
|
510
|
-
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId)
|
|
835
|
+
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler)
|
|
511
836
|
: createNullBot(outputHandler);
|
|
512
837
|
// Permission inheritance: if daemon runs with dangerously_skip_permissions, dispatched agents
|
|
513
838
|
// inherit the same level — they need Write access for implementation tasks.
|
|
@@ -522,11 +847,19 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
522
847
|
}
|
|
523
848
|
}
|
|
524
849
|
const dispatchReadOnly = !(config.daemon && config.daemon.dangerously_skip_permissions);
|
|
850
|
+
if (envelope && taskBoard) {
|
|
851
|
+
taskBoard.markTaskStatus(envelope.task_id, 'running', { summary: `dispatched via ${sessionMode}` });
|
|
852
|
+
taskBoard.appendTaskEvent(envelope.task_id, 'task_started', targetProject, { session_mode: sessionMode });
|
|
853
|
+
}
|
|
525
854
|
_handleCommand(nullBot, dispatchChatId, prompt, config, null, null, dispatchReadOnly).catch(e => {
|
|
526
855
|
log('ERROR', `Dispatch handleCommand failed for ${targetProject}: ${e.message}`);
|
|
856
|
+
if (envelope && taskBoard) {
|
|
857
|
+
taskBoard.markTaskStatus(envelope.task_id, 'failed', { last_error: e.message, summary: 'dispatch execution failed' });
|
|
858
|
+
taskBoard.appendTaskEvent(envelope.task_id, 'task_failed', targetProject, { error: e.message.slice(0, 200) });
|
|
859
|
+
}
|
|
527
860
|
});
|
|
528
861
|
|
|
529
|
-
return { success: true, id: fullMsg.id };
|
|
862
|
+
return { success: true, id: fullMsg.id, task_id: envelope ? envelope.task_id : null };
|
|
530
863
|
}
|
|
531
864
|
|
|
532
865
|
/**
|
|
@@ -755,8 +1088,8 @@ const {
|
|
|
755
1088
|
*/
|
|
756
1089
|
function attachOrCreateSession(chatId, projCwd, name) {
|
|
757
1090
|
const state = loadState();
|
|
758
|
-
// Virtual
|
|
759
|
-
//
|
|
1091
|
+
// Virtual chatIds (_agent_* / _scope_*) are isolated from real user chats.
|
|
1092
|
+
// This avoids cross-context session collisions between user chat and dispatch flows.
|
|
760
1093
|
const newSess = createSession(chatId, projCwd, name || '');
|
|
761
1094
|
state.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
762
1095
|
saveState(state);
|
|
@@ -1031,6 +1364,9 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1031
1364
|
getAllTasks,
|
|
1032
1365
|
dispatchTask,
|
|
1033
1366
|
log,
|
|
1367
|
+
skillEvolution,
|
|
1368
|
+
taskBoard,
|
|
1369
|
+
taskEnvelope,
|
|
1034
1370
|
});
|
|
1035
1371
|
|
|
1036
1372
|
const { handleSessionCommand } = createSessionCommandHandler({
|
|
@@ -1286,7 +1622,19 @@ async function main() {
|
|
|
1286
1622
|
|
|
1287
1623
|
// Config validation: warn on unknown/suspect fields
|
|
1288
1624
|
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
1289
|
-
const KNOWN_DAEMON = [
|
|
1625
|
+
const KNOWN_DAEMON = [
|
|
1626
|
+
'model',
|
|
1627
|
+
'log_max_size',
|
|
1628
|
+
'heartbeat_check_interval',
|
|
1629
|
+
'session_allowed_tools',
|
|
1630
|
+
'dangerously_skip_permissions',
|
|
1631
|
+
'cooldown_seconds',
|
|
1632
|
+
'agent_flow_ttl_ms',
|
|
1633
|
+
'agent_bind_ttl_ms',
|
|
1634
|
+
'mac_control_mode',
|
|
1635
|
+
'enable_nl_mac_control',
|
|
1636
|
+
'enable_nl_mac_fallback',
|
|
1637
|
+
];
|
|
1290
1638
|
const VALID_MODELS = ['sonnet', 'opus', 'haiku'];
|
|
1291
1639
|
for (const key of Object.keys(config)) {
|
|
1292
1640
|
if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
|