metame-cli 1.4.17 → 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 +350 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +57 -10
- package/scripts/daemon-command-router.js +241 -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 +213 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +371 -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 +158 -19
- 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,8 +49,27 @@ 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 },
|
|
55
74
|
{ name: 'skill-manager', pattern: /找技能|管理技能|更新技能|安装技能|skill manager|skill scout|(?:find|look for)\s+skills?/i },
|
|
56
75
|
{ name: 'skill-evolution-manager', pattern: /\/evolve\b|复盘一下|记录一下(这个)?经验|保存到\s*skill|skill evolution/i },
|
|
@@ -58,7 +77,10 @@ const SKILL_ROUTES = [
|
|
|
58
77
|
|
|
59
78
|
function routeSkill(prompt) {
|
|
60
79
|
for (const r of SKILL_ROUTES) {
|
|
61
|
-
|
|
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;
|
|
62
84
|
}
|
|
63
85
|
return null;
|
|
64
86
|
}
|
|
@@ -81,6 +103,12 @@ function routeAgent(prompt, config) {
|
|
|
81
103
|
|
|
82
104
|
const yaml = require('./resolve-yaml');
|
|
83
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');
|
|
84
112
|
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
85
113
|
const { createExecCommandHandler } = require('./daemon-exec-commands');
|
|
86
114
|
const { createOpsCommandHandler } = require('./daemon-ops-commands');
|
|
@@ -231,19 +259,56 @@ function restoreConfig() {
|
|
|
231
259
|
|
|
232
260
|
let _cachedState = null;
|
|
233
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
|
+
|
|
234
300
|
function _readStateFromDisk() {
|
|
235
301
|
try {
|
|
236
302
|
const s = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
237
|
-
|
|
238
|
-
return s;
|
|
303
|
+
return ensureStateShape(s);
|
|
239
304
|
} catch {
|
|
240
|
-
return {
|
|
305
|
+
return ensureStateShape({
|
|
241
306
|
pid: null,
|
|
242
307
|
budget: { date: null, tokens_used: 0 },
|
|
243
308
|
tasks: {},
|
|
244
309
|
sessions: {},
|
|
245
310
|
started_at: null,
|
|
246
|
-
};
|
|
311
|
+
});
|
|
247
312
|
}
|
|
248
313
|
}
|
|
249
314
|
|
|
@@ -253,9 +318,65 @@ function loadState() {
|
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
function saveState(state) {
|
|
256
|
-
|
|
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;
|
|
257
378
|
try {
|
|
258
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(
|
|
379
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
259
380
|
} catch (e) {
|
|
260
381
|
log('ERROR', `Failed to save state: ${e.message}`);
|
|
261
382
|
}
|
|
@@ -289,6 +410,7 @@ function buildProfilePreamble() {
|
|
|
289
410
|
// BUDGET TRACKING
|
|
290
411
|
// ---------------------------------------------------------
|
|
291
412
|
function checkBudget(config, state) {
|
|
413
|
+
state = ensureStateShape(state);
|
|
292
414
|
const today = new Date().toISOString().slice(0, 10);
|
|
293
415
|
if (state.budget.date !== today) {
|
|
294
416
|
state.budget.date = today;
|
|
@@ -299,14 +421,44 @@ function checkBudget(config, state) {
|
|
|
299
421
|
return state.budget.tokens_used < limit;
|
|
300
422
|
}
|
|
301
423
|
|
|
302
|
-
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());
|
|
303
429
|
const today = new Date().toISOString().slice(0, 10);
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
430
|
+
if (liveState.budget.date !== today) {
|
|
431
|
+
liveState.budget.date = today;
|
|
432
|
+
liveState.budget.tokens_used = 0;
|
|
307
433
|
}
|
|
308
|
-
|
|
309
|
-
|
|
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);
|
|
310
462
|
}
|
|
311
463
|
|
|
312
464
|
|
|
@@ -319,6 +471,10 @@ function getBudgetWarning(config, state) {
|
|
|
319
471
|
return 'ok';
|
|
320
472
|
}
|
|
321
473
|
|
|
474
|
+
const taskBoard = createTaskBoard({
|
|
475
|
+
logger: (msg) => log('WARN', msg),
|
|
476
|
+
});
|
|
477
|
+
|
|
322
478
|
// ---------------------------------------------------------
|
|
323
479
|
// AGENT DISPATCH — virtual chatId inter-agent communication
|
|
324
480
|
// ---------------------------------------------------------
|
|
@@ -351,25 +507,29 @@ function createNullBot(onOutput) {
|
|
|
351
507
|
* Forward bot: routes all calls to a real bot with a fixed chatId.
|
|
352
508
|
* Used for dispatch tasks so Claude's streaming output appears in the target's Feishu channel.
|
|
353
509
|
*/
|
|
354
|
-
function createStreamForwardBot(realBot, chatId) {
|
|
510
|
+
function createStreamForwardBot(realBot, chatId, onOutput = null) {
|
|
355
511
|
// Track edit-broken state independently so dispatch failures don't poison realBot's flag
|
|
356
512
|
let _editBroken = false;
|
|
357
513
|
return {
|
|
358
514
|
sendMessage: async (_, text) => {
|
|
359
515
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] msg: ${String(text).slice(0, 80)}`);
|
|
516
|
+
if (onOutput) onOutput(text);
|
|
360
517
|
return realBot.sendMessage(chatId, text);
|
|
361
518
|
},
|
|
362
519
|
sendMarkdown: async (_, text) => {
|
|
363
520
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] md: ${String(text).slice(0, 80)}`);
|
|
521
|
+
if (onOutput) onOutput(text);
|
|
364
522
|
return realBot.sendMarkdown(chatId, text);
|
|
365
523
|
},
|
|
366
524
|
sendCard: async (_, card) => {
|
|
367
525
|
const title = typeof card === 'object' ? (card.title || card.body || '').slice(0, 60) : String(card).slice(0, 60);
|
|
368
526
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] card: ${title}`);
|
|
527
|
+
if (onOutput) onOutput(typeof card === 'object' ? (card.body || card.title || JSON.stringify(card)) : card);
|
|
369
528
|
return realBot.sendCard(chatId, card);
|
|
370
529
|
},
|
|
371
530
|
sendRawCard: async (_, header, elements) => {
|
|
372
531
|
log('INFO', `[StreamBot→${chatId.slice(-8)}] rawcard: ${String(header).slice(0, 60)}`);
|
|
532
|
+
if (onOutput) onOutput(header);
|
|
373
533
|
return realBot.sendRawCard(chatId, header, elements);
|
|
374
534
|
},
|
|
375
535
|
sendButtons: async (_, text, buttons) => realBot.sendButtons(chatId, text, buttons),
|
|
@@ -393,6 +553,76 @@ function createStreamForwardBot(realBot, chatId) {
|
|
|
393
553
|
};
|
|
394
554
|
}
|
|
395
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
|
+
|
|
396
626
|
/**
|
|
397
627
|
* Dispatch a task/message to another agent via virtual chatId.
|
|
398
628
|
* @param {string} targetProject - project key (e.g. 'digital_me', 'desktop')
|
|
@@ -402,17 +632,56 @@ function createStreamForwardBot(realBot, chatId) {
|
|
|
402
632
|
*/
|
|
403
633
|
function dispatchTask(targetProject, message, config, replyFn, streamOptions = null) {
|
|
404
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
|
+
};
|
|
405
672
|
|
|
406
673
|
// Anti-storm: check chain depth
|
|
407
674
|
const chain = message.chain || [];
|
|
408
675
|
if (chain.length >= LIMITS.max_depth) {
|
|
409
676
|
log('WARN', `Dispatch blocked: max depth ${LIMITS.max_depth} reached (chain: ${chain.join('→')})`);
|
|
677
|
+
markTaskBlocked('max_depth_exceeded');
|
|
410
678
|
return { success: false, error: 'max_depth_exceeded' };
|
|
411
679
|
}
|
|
412
680
|
|
|
413
681
|
// Anti-storm: check for cycles
|
|
414
682
|
if (chain.includes(targetProject)) {
|
|
415
683
|
log('WARN', `Dispatch blocked: cycle detected (${chain.join('→')}→${targetProject})`);
|
|
684
|
+
markTaskBlocked('cycle_detected');
|
|
416
685
|
return { success: false, error: 'cycle_detected' };
|
|
417
686
|
}
|
|
418
687
|
|
|
@@ -427,10 +696,12 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
427
696
|
const toTarget = recent.filter(e => e.to === targetProject).length;
|
|
428
697
|
if (toTarget >= LIMITS.max_per_hour_per_target) {
|
|
429
698
|
log('WARN', `Dispatch blocked: rate limit to ${targetProject} (${toTarget}/${LIMITS.max_per_hour_per_target} per hour)`);
|
|
699
|
+
markTaskBlocked('rate_limit_target');
|
|
430
700
|
return { success: false, error: 'rate_limit_target' };
|
|
431
701
|
}
|
|
432
702
|
if (recent.length >= LIMITS.max_total_per_hour) {
|
|
433
703
|
log('WARN', `Dispatch blocked: total rate limit (${recent.length}/${LIMITS.max_total_per_hour} per hour)`);
|
|
704
|
+
markTaskBlocked('rate_limit_total');
|
|
434
705
|
return { success: false, error: 'rate_limit_total' };
|
|
435
706
|
}
|
|
436
707
|
}
|
|
@@ -440,6 +711,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
440
711
|
|
|
441
712
|
if (!_handleCommand) {
|
|
442
713
|
log('WARN', 'Dispatch: handleCommand not yet bound, dropping task');
|
|
714
|
+
markTaskBlocked('handler_not_ready');
|
|
443
715
|
return { success: false, error: 'handler_not_ready' };
|
|
444
716
|
}
|
|
445
717
|
|
|
@@ -449,18 +721,52 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
449
721
|
to: targetProject,
|
|
450
722
|
type: message.type || 'task',
|
|
451
723
|
priority: message.priority || 'normal',
|
|
452
|
-
payload
|
|
724
|
+
payload,
|
|
453
725
|
callback: message.callback || false,
|
|
454
726
|
new_session: !!message.new_session,
|
|
455
727
|
chain: [...chain, message.from || 'unknown'],
|
|
728
|
+
task_id: envelope ? envelope.task_id : null,
|
|
729
|
+
scope_id: envelope ? envelope.scope_id : null,
|
|
456
730
|
created_at: new Date().toISOString(),
|
|
457
731
|
};
|
|
458
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
|
+
|
|
459
763
|
// Write to dispatch log for audit / rate-limiting
|
|
460
764
|
if (!fs.existsSync(DISPATCH_DIR)) fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
461
765
|
fs.appendFileSync(DISPATCH_LOG, JSON.stringify({ ...fullMsg, dispatched_at: new Date().toISOString() }) + '\n', 'utf8');
|
|
462
766
|
|
|
463
|
-
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');
|
|
464
770
|
|
|
465
771
|
// Inject sender identity when dispatched by another agent (not directly from user)
|
|
466
772
|
const userSources = new Set(['unknown', 'claude_session', '_claude_session', 'user']);
|
|
@@ -478,18 +784,35 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
478
784
|
// Daemon sends the ack autonomously; Claude should just state its plan in the reply text.
|
|
479
785
|
prompt = `[行为要求:回复开头用1-2句「计划:xxx」说明执行方案,再开始执行。不要调用 dispatch_to,daemon 会自动转发你的回复。]\n\n${prompt}`;
|
|
480
786
|
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
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.
|
|
485
791
|
const forceNew = !!fullMsg.new_session;
|
|
486
|
-
const dispatchChatId =
|
|
792
|
+
const dispatchChatId = buildDispatchChatId(targetProject, envelope && envelope.scope_id);
|
|
487
793
|
const sessionMode = forceNew ? 'fresh session (forced)' : 'existing virtual session';
|
|
488
794
|
log('INFO', `Dispatching ${fullMsg.type} to ${targetProject} via ${sessionMode}: ${rawPrompt.slice(0, 80)}`);
|
|
489
795
|
|
|
796
|
+
let _taskFinalized = false;
|
|
490
797
|
const outputHandler = (output) => {
|
|
491
798
|
const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
|
|
492
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
|
+
}
|
|
493
816
|
if (replyFn && outStr.trim().length > 2) {
|
|
494
817
|
replyFn(outStr);
|
|
495
818
|
} else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
|
|
@@ -509,7 +832,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
509
832
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
510
833
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
511
834
|
const nullBot = streamOptions?.bot && streamOptions?.chatId
|
|
512
|
-
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId)
|
|
835
|
+
? createStreamForwardBot(streamOptions.bot, streamOptions.chatId, outputHandler)
|
|
513
836
|
: createNullBot(outputHandler);
|
|
514
837
|
// Permission inheritance: if daemon runs with dangerously_skip_permissions, dispatched agents
|
|
515
838
|
// inherit the same level — they need Write access for implementation tasks.
|
|
@@ -524,11 +847,19 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
524
847
|
}
|
|
525
848
|
}
|
|
526
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
|
+
}
|
|
527
854
|
_handleCommand(nullBot, dispatchChatId, prompt, config, null, null, dispatchReadOnly).catch(e => {
|
|
528
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
|
+
}
|
|
529
860
|
});
|
|
530
861
|
|
|
531
|
-
return { success: true, id: fullMsg.id };
|
|
862
|
+
return { success: true, id: fullMsg.id, task_id: envelope ? envelope.task_id : null };
|
|
532
863
|
}
|
|
533
864
|
|
|
534
865
|
/**
|
|
@@ -757,8 +1088,8 @@ const {
|
|
|
757
1088
|
*/
|
|
758
1089
|
function attachOrCreateSession(chatId, projCwd, name) {
|
|
759
1090
|
const state = loadState();
|
|
760
|
-
// Virtual
|
|
761
|
-
//
|
|
1091
|
+
// Virtual chatIds (_agent_* / _scope_*) are isolated from real user chats.
|
|
1092
|
+
// This avoids cross-context session collisions between user chat and dispatch flows.
|
|
762
1093
|
const newSess = createSession(chatId, projCwd, name || '');
|
|
763
1094
|
state.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
764
1095
|
saveState(state);
|
|
@@ -1034,6 +1365,8 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1034
1365
|
dispatchTask,
|
|
1035
1366
|
log,
|
|
1036
1367
|
skillEvolution,
|
|
1368
|
+
taskBoard,
|
|
1369
|
+
taskEnvelope,
|
|
1037
1370
|
});
|
|
1038
1371
|
|
|
1039
1372
|
const { handleSessionCommand } = createSessionCommandHandler({
|
|
@@ -1289,7 +1622,19 @@ async function main() {
|
|
|
1289
1622
|
|
|
1290
1623
|
// Config validation: warn on unknown/suspect fields
|
|
1291
1624
|
const KNOWN_SECTIONS = ['daemon', 'telegram', 'feishu', 'heartbeat', 'budget', 'projects'];
|
|
1292
|
-
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
|
+
];
|
|
1293
1638
|
const VALID_MODELS = ['sonnet', 'opus', 'haiku'];
|
|
1294
1639
|
for (const key of Object.keys(config)) {
|
|
1295
1640
|
if (!KNOWN_SECTIONS.includes(key)) log('WARN', `Config: unknown section "${key}" (typo?)`);
|