openclaw-node-harness 2.0.0 → 2.0.1
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/bin/mesh-agent.js +417 -94
- package/bin/mesh-join-token.js +129 -0
- package/bin/mesh-node-remove.js +277 -0
- package/bin/mesh-task-daemon.js +723 -15
- package/bin/openclaw-node-init.js +674 -0
- package/lib/llm-providers.js +262 -0
- package/lib/mesh-collab.js +549 -0
- package/lib/mesh-plans.js +528 -0
- package/lib/mesh-tasks.js +50 -34
- package/package.json +1 -1
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mesh-plans.js — Plan decomposition and delegation routing for mesh tasks.
|
|
3
|
+
*
|
|
4
|
+
* A plan decomposes a parent task into subtasks with delegation routing
|
|
5
|
+
* (solo_mesh, collab_mesh, local, soul, human), dependency waves, and
|
|
6
|
+
* execution contracts. Plans are stored in MESH_PLANS JetStream KV bucket.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle: draft → review → approved → executing → completed | aborted
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { StringCodec } = require('nats');
|
|
12
|
+
const sc = StringCodec();
|
|
13
|
+
|
|
14
|
+
const PLANS_KV_BUCKET = 'MESH_PLANS';
|
|
15
|
+
|
|
16
|
+
// ── Plan Statuses ──────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const PLAN_STATUS = {
|
|
19
|
+
DRAFT: 'draft',
|
|
20
|
+
REVIEW: 'review',
|
|
21
|
+
APPROVED: 'approved',
|
|
22
|
+
EXECUTING: 'executing',
|
|
23
|
+
COMPLETED: 'completed',
|
|
24
|
+
ABORTED: 'aborted',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ── Subtask Statuses ───────────────────────────────
|
|
28
|
+
|
|
29
|
+
const SUBTASK_STATUS = {
|
|
30
|
+
PENDING: 'pending', // not yet dispatched (waiting for wave)
|
|
31
|
+
QUEUED: 'queued', // dispatched to queue
|
|
32
|
+
RUNNING: 'running', // actively being worked
|
|
33
|
+
COMPLETED: 'completed',
|
|
34
|
+
FAILED: 'failed',
|
|
35
|
+
BLOCKED: 'blocked',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ── Delegation Modes ───────────────────────────────
|
|
39
|
+
|
|
40
|
+
const DELEGATION_MODE = {
|
|
41
|
+
SOLO_MESH: 'solo_mesh',
|
|
42
|
+
COLLAB_MESH: 'collab_mesh',
|
|
43
|
+
LOCAL: 'local',
|
|
44
|
+
SOUL: 'soul',
|
|
45
|
+
HUMAN: 'human',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── High-Criticality Detection ─────────────────────
|
|
49
|
+
|
|
50
|
+
const HIGH_CRIT_PATHS = ['contracts/', 'auth/', 'payments/', 'migration/'];
|
|
51
|
+
const HIGH_CRIT_KEYWORDS = [
|
|
52
|
+
'security', 'audit', 'authentication', 'payment',
|
|
53
|
+
'migration', 'selfdestruct', 'upgrade', 'proxy',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── Soul Routing Guide (from DELEGATION.md) ────────
|
|
57
|
+
|
|
58
|
+
const SOUL_ROUTING = {
|
|
59
|
+
'smart-contract': 'blockchain-auditor',
|
|
60
|
+
'narrative': 'lore-writer',
|
|
61
|
+
'lore': 'lore-writer',
|
|
62
|
+
'cicd': 'infra-ops',
|
|
63
|
+
'deployment': 'infra-ops',
|
|
64
|
+
'monitoring': 'infra-ops',
|
|
65
|
+
'identity': 'identity-architect',
|
|
66
|
+
'sbt': 'identity-architect',
|
|
67
|
+
'trust': 'identity-architect',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ── Plan Factory ───────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new plan from a parent task.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} parentTaskId — the task being decomposed
|
|
76
|
+
* @param {object} opts — plan metadata
|
|
77
|
+
* @param {Array} subtaskSpecs — array of subtask definitions
|
|
78
|
+
*/
|
|
79
|
+
function createPlan({
|
|
80
|
+
parent_task_id,
|
|
81
|
+
title,
|
|
82
|
+
description = '',
|
|
83
|
+
planner = 'daedalus',
|
|
84
|
+
planner_soul = null,
|
|
85
|
+
requires_approval = true,
|
|
86
|
+
subtasks = [],
|
|
87
|
+
}) {
|
|
88
|
+
const planId = `PLAN-${parent_task_id}-${Date.now()}`;
|
|
89
|
+
|
|
90
|
+
// Compute waves from dependency graph
|
|
91
|
+
const enriched = subtasks.map((st, idx) => {
|
|
92
|
+
const subtaskId = st.subtask_id || `${planId}-S${String(idx + 1).padStart(2, '0')}`;
|
|
93
|
+
return {
|
|
94
|
+
subtask_id: subtaskId,
|
|
95
|
+
title: st.title || '',
|
|
96
|
+
description: st.description || '',
|
|
97
|
+
|
|
98
|
+
delegation: st.delegation || {
|
|
99
|
+
mode: DELEGATION_MODE.LOCAL,
|
|
100
|
+
soul_id: null,
|
|
101
|
+
collaboration: null,
|
|
102
|
+
reason: 'default fallback',
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
budget_minutes: parseInt(st.budget_minutes) || 15,
|
|
106
|
+
// Track who set the budget estimate and how (for audit/debugging routing decisions)
|
|
107
|
+
budget_source: st.budget_source || 'planner_estimate', // 'planner_estimate' | 'user_override' | 'metric_based'
|
|
108
|
+
metric: st.metric || null,
|
|
109
|
+
scope: st.scope || [],
|
|
110
|
+
success_criteria: st.success_criteria || [],
|
|
111
|
+
|
|
112
|
+
depends_on: st.depends_on || [],
|
|
113
|
+
wave: 0, // computed below
|
|
114
|
+
|
|
115
|
+
status: SUBTASK_STATUS.PENDING,
|
|
116
|
+
mesh_task_id: null,
|
|
117
|
+
kanban_task_id: null,
|
|
118
|
+
owner: null,
|
|
119
|
+
result: null,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Compute wave assignments
|
|
124
|
+
assignWaves(enriched);
|
|
125
|
+
|
|
126
|
+
const totalBudget = enriched.reduce((sum, st) => sum + st.budget_minutes, 0);
|
|
127
|
+
const maxWave = enriched.reduce((max, st) => Math.max(max, st.wave), 0);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
plan_id: planId,
|
|
131
|
+
parent_task_id,
|
|
132
|
+
title,
|
|
133
|
+
description,
|
|
134
|
+
|
|
135
|
+
status: PLAN_STATUS.DRAFT,
|
|
136
|
+
|
|
137
|
+
planner,
|
|
138
|
+
planner_soul,
|
|
139
|
+
|
|
140
|
+
subtasks: enriched,
|
|
141
|
+
|
|
142
|
+
total_budget_minutes: totalBudget,
|
|
143
|
+
estimated_waves: maxWave + 1,
|
|
144
|
+
|
|
145
|
+
requires_approval,
|
|
146
|
+
approved_by: null,
|
|
147
|
+
approved_at: null,
|
|
148
|
+
|
|
149
|
+
created_at: new Date().toISOString(),
|
|
150
|
+
started_at: null,
|
|
151
|
+
completed_at: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Wave Computation ───────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Assign wave numbers to subtasks based on dependency DAG.
|
|
159
|
+
* Mutates subtasks in place. Same algorithm as MC's computeWaves.
|
|
160
|
+
*/
|
|
161
|
+
function assignWaves(subtasks) {
|
|
162
|
+
const idMap = new Map(subtasks.map(st => [st.subtask_id, st]));
|
|
163
|
+
|
|
164
|
+
// BFS topological layers
|
|
165
|
+
const inDegree = new Map();
|
|
166
|
+
const successors = new Map();
|
|
167
|
+
|
|
168
|
+
for (const st of subtasks) {
|
|
169
|
+
inDegree.set(st.subtask_id, 0);
|
|
170
|
+
successors.set(st.subtask_id, []);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const st of subtasks) {
|
|
174
|
+
for (const depId of st.depends_on) {
|
|
175
|
+
if (idMap.has(depId)) {
|
|
176
|
+
inDegree.set(st.subtask_id, (inDegree.get(st.subtask_id) || 0) + 1);
|
|
177
|
+
const succs = successors.get(depId) || [];
|
|
178
|
+
succs.push(st.subtask_id);
|
|
179
|
+
successors.set(depId, succs);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let wave = 0;
|
|
185
|
+
let currentWave = [];
|
|
186
|
+
|
|
187
|
+
for (const [id, deg] of inDegree) {
|
|
188
|
+
if (deg === 0) currentWave.push(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
while (currentWave.length > 0) {
|
|
192
|
+
for (const id of currentWave) {
|
|
193
|
+
idMap.get(id).wave = wave;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nextWave = [];
|
|
197
|
+
for (const id of currentWave) {
|
|
198
|
+
for (const succ of successors.get(id) || []) {
|
|
199
|
+
const newDeg = (inDegree.get(succ) || 1) - 1;
|
|
200
|
+
inDegree.set(succ, newDeg);
|
|
201
|
+
if (newDeg === 0) nextWave.push(succ);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
wave++;
|
|
206
|
+
currentWave = nextWave;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Delegation Decision Tree ───────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Route a subtask to the appropriate delegation mode.
|
|
214
|
+
* Returns a delegation object: { mode, soul_id, collaboration, reason }.
|
|
215
|
+
*/
|
|
216
|
+
function routeDelegation(subtask) {
|
|
217
|
+
const title = (subtask.title || '').toLowerCase();
|
|
218
|
+
const desc = (subtask.description || '').toLowerCase();
|
|
219
|
+
const scope = subtask.scope || [];
|
|
220
|
+
const combined = `${title} ${desc}`;
|
|
221
|
+
|
|
222
|
+
// 1. Is it trivial? (< 2 min, indicated by budget)
|
|
223
|
+
// NOTE: budget_minutes is typically a planner estimate (Claude's guess during decomposition).
|
|
224
|
+
// This gate is only as reliable as the estimate. budget_source tracks provenance.
|
|
225
|
+
if (subtask.budget_minutes && subtask.budget_minutes <= 2) {
|
|
226
|
+
return {
|
|
227
|
+
mode: DELEGATION_MODE.LOCAL,
|
|
228
|
+
soul_id: null,
|
|
229
|
+
collaboration: null,
|
|
230
|
+
reason: `Trivial task (budget=${subtask.budget_minutes}min, source=${subtask.budget_source || 'planner_estimate'}), Daedalus inline`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 2. Does it require human judgment?
|
|
235
|
+
if (combined.includes('approve') || combined.includes('decision') ||
|
|
236
|
+
combined.includes('choose between') || combined.includes('user input')) {
|
|
237
|
+
return {
|
|
238
|
+
mode: DELEGATION_MODE.HUMAN,
|
|
239
|
+
soul_id: null,
|
|
240
|
+
collaboration: null,
|
|
241
|
+
reason: 'Requires human judgment or approval',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3. Does it match a specialist soul domain?
|
|
246
|
+
for (const [keyword, soulId] of Object.entries(SOUL_ROUTING)) {
|
|
247
|
+
if (combined.includes(keyword)) {
|
|
248
|
+
return {
|
|
249
|
+
mode: DELEGATION_MODE.SOUL,
|
|
250
|
+
soul_id: soulId,
|
|
251
|
+
collaboration: null,
|
|
252
|
+
reason: `Domain match: "${keyword}" → ${soulId}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 4. Is it high-criticality? → collab_mesh with review mode
|
|
258
|
+
const isHighCrit = HIGH_CRIT_PATHS.some(p => scope.some(s => s.includes(p)))
|
|
259
|
+
|| HIGH_CRIT_KEYWORDS.some(kw => combined.includes(kw));
|
|
260
|
+
|
|
261
|
+
if (isHighCrit) {
|
|
262
|
+
return {
|
|
263
|
+
mode: DELEGATION_MODE.COLLAB_MESH,
|
|
264
|
+
soul_id: null,
|
|
265
|
+
collaboration: {
|
|
266
|
+
mode: 'review',
|
|
267
|
+
min_nodes: 2,
|
|
268
|
+
max_nodes: 3,
|
|
269
|
+
join_window_s: 30,
|
|
270
|
+
max_rounds: 3,
|
|
271
|
+
convergence: { type: 'unanimous' },
|
|
272
|
+
scope_strategy: 'leader_only',
|
|
273
|
+
},
|
|
274
|
+
reason: 'High-criticality path detected, N-node review mode',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 5. Does it have broad scope? → collab_mesh parallel
|
|
279
|
+
if (scope.length > 3) {
|
|
280
|
+
return {
|
|
281
|
+
mode: DELEGATION_MODE.COLLAB_MESH,
|
|
282
|
+
soul_id: null,
|
|
283
|
+
collaboration: {
|
|
284
|
+
mode: 'parallel',
|
|
285
|
+
min_nodes: 2,
|
|
286
|
+
max_nodes: null,
|
|
287
|
+
join_window_s: 30,
|
|
288
|
+
max_rounds: 5,
|
|
289
|
+
convergence: { type: 'majority', threshold: 0.66 },
|
|
290
|
+
scope_strategy: 'partitioned',
|
|
291
|
+
},
|
|
292
|
+
reason: 'Broad scope (>3 paths), parallel collab',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 6. Mechanically verifiable? → solo_mesh
|
|
297
|
+
if (subtask.metric) {
|
|
298
|
+
return {
|
|
299
|
+
mode: DELEGATION_MODE.SOLO_MESH,
|
|
300
|
+
soul_id: null,
|
|
301
|
+
collaboration: null,
|
|
302
|
+
reason: 'Has mechanical metric, solo mesh agent',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 7. Default → local (safest fallback)
|
|
307
|
+
return {
|
|
308
|
+
mode: DELEGATION_MODE.LOCAL,
|
|
309
|
+
soul_id: null,
|
|
310
|
+
collaboration: null,
|
|
311
|
+
reason: 'Default fallback, Daedalus local execution',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Auto-route all subtasks in a plan that don't already have delegation set.
|
|
317
|
+
* Mutates subtasks in place.
|
|
318
|
+
*/
|
|
319
|
+
function autoRoutePlan(plan) {
|
|
320
|
+
for (const st of plan.subtasks) {
|
|
321
|
+
if (!st.delegation || !st.delegation.mode || st.delegation.mode === 'auto') {
|
|
322
|
+
st.delegation = routeDelegation(st);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return plan;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── PlanStore (KV-backed) ──────────────────────────
|
|
329
|
+
|
|
330
|
+
class PlanStore {
|
|
331
|
+
constructor(kv) {
|
|
332
|
+
this.kv = kv;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async put(plan) {
|
|
336
|
+
await this.kv.put(plan.plan_id, sc.encode(JSON.stringify(plan)));
|
|
337
|
+
return plan;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async get(planId) {
|
|
341
|
+
const entry = await this.kv.get(planId);
|
|
342
|
+
if (!entry || !entry.value) return null;
|
|
343
|
+
return JSON.parse(sc.decode(entry.value));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async delete(planId) {
|
|
347
|
+
await this.kv.delete(planId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async list(filter = {}) {
|
|
351
|
+
const plans = [];
|
|
352
|
+
const allKeys = [];
|
|
353
|
+
const keys = await this.kv.keys();
|
|
354
|
+
for await (const key of keys) {
|
|
355
|
+
allKeys.push(key);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const key of allKeys) {
|
|
359
|
+
const entry = await this.kv.get(key);
|
|
360
|
+
if (!entry || !entry.value) continue;
|
|
361
|
+
const plan = JSON.parse(sc.decode(entry.value));
|
|
362
|
+
|
|
363
|
+
if (filter.status && plan.status !== filter.status) continue;
|
|
364
|
+
if (filter.parent_task_id && plan.parent_task_id !== filter.parent_task_id) continue;
|
|
365
|
+
|
|
366
|
+
plans.push(plan);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
plans.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
370
|
+
return plans;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Find plan by parent task ID.
|
|
375
|
+
*/
|
|
376
|
+
async findByParentTask(parentTaskId) {
|
|
377
|
+
const plans = await this.list({ parent_task_id: parentTaskId });
|
|
378
|
+
return plans[0] || null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Lifecycle ───────────────────────────────────
|
|
382
|
+
|
|
383
|
+
async submitForReview(planId) {
|
|
384
|
+
const plan = await this.get(planId);
|
|
385
|
+
if (!plan) return null;
|
|
386
|
+
plan.status = PLAN_STATUS.REVIEW;
|
|
387
|
+
await this.put(plan);
|
|
388
|
+
return plan;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async approve(planId, approvedBy = 'gui') {
|
|
392
|
+
const plan = await this.get(planId);
|
|
393
|
+
if (!plan) return null;
|
|
394
|
+
plan.status = PLAN_STATUS.APPROVED;
|
|
395
|
+
plan.approved_by = approvedBy;
|
|
396
|
+
plan.approved_at = new Date().toISOString();
|
|
397
|
+
await this.put(plan);
|
|
398
|
+
return plan;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async startExecuting(planId) {
|
|
402
|
+
const plan = await this.get(planId);
|
|
403
|
+
if (!plan) return null;
|
|
404
|
+
plan.status = PLAN_STATUS.EXECUTING;
|
|
405
|
+
plan.started_at = new Date().toISOString();
|
|
406
|
+
await this.put(plan);
|
|
407
|
+
return plan;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async markCompleted(planId) {
|
|
411
|
+
const plan = await this.get(planId);
|
|
412
|
+
if (!plan) return null;
|
|
413
|
+
plan.status = PLAN_STATUS.COMPLETED;
|
|
414
|
+
plan.completed_at = new Date().toISOString();
|
|
415
|
+
await this.put(plan);
|
|
416
|
+
return plan;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async markAborted(planId, reason) {
|
|
420
|
+
const plan = await this.get(planId);
|
|
421
|
+
if (!plan) return null;
|
|
422
|
+
plan.status = PLAN_STATUS.ABORTED;
|
|
423
|
+
plan.completed_at = new Date().toISOString();
|
|
424
|
+
// Mark all pending subtasks as blocked
|
|
425
|
+
for (const st of plan.subtasks) {
|
|
426
|
+
if (st.status === SUBTASK_STATUS.PENDING || st.status === SUBTASK_STATUS.QUEUED) {
|
|
427
|
+
st.status = SUBTASK_STATUS.BLOCKED;
|
|
428
|
+
st.result = { success: false, summary: `Plan aborted: ${reason}` };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await this.put(plan);
|
|
432
|
+
return plan;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Subtask Management ──────────────────────────
|
|
436
|
+
|
|
437
|
+
async updateSubtask(planId, subtaskId, updates) {
|
|
438
|
+
const plan = await this.get(planId);
|
|
439
|
+
if (!plan) return null;
|
|
440
|
+
|
|
441
|
+
const st = plan.subtasks.find(s => s.subtask_id === subtaskId);
|
|
442
|
+
if (!st) return null;
|
|
443
|
+
|
|
444
|
+
Object.assign(st, updates);
|
|
445
|
+
await this.put(plan);
|
|
446
|
+
return plan;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get subtasks ready for the next wave.
|
|
451
|
+
* Returns subtasks whose dependencies are all completed and status is pending.
|
|
452
|
+
*/
|
|
453
|
+
getNextWaveSubtasks(plan) {
|
|
454
|
+
if (plan.status !== PLAN_STATUS.EXECUTING) return [];
|
|
455
|
+
|
|
456
|
+
const completedIds = new Set(
|
|
457
|
+
plan.subtasks
|
|
458
|
+
.filter(st => st.status === SUBTASK_STATUS.COMPLETED)
|
|
459
|
+
.map(st => st.subtask_id)
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return plan.subtasks.filter(st => {
|
|
463
|
+
if (st.status !== SUBTASK_STATUS.PENDING) return false;
|
|
464
|
+
return st.depends_on.every(depId => completedIds.has(depId));
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Check if a plan is fully completed (all subtasks done or failed).
|
|
470
|
+
*/
|
|
471
|
+
isPlanComplete(plan) {
|
|
472
|
+
return plan.subtasks.every(
|
|
473
|
+
st => st.status === SUBTASK_STATUS.COMPLETED ||
|
|
474
|
+
st.status === SUBTASK_STATUS.FAILED ||
|
|
475
|
+
st.status === SUBTASK_STATUS.BLOCKED
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Check if a plan has any failed subtasks (needs attention).
|
|
481
|
+
*/
|
|
482
|
+
hasFailures(plan) {
|
|
483
|
+
return plan.subtasks.some(st => st.status === SUBTASK_STATUS.FAILED);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get plan summary for reporting.
|
|
488
|
+
*/
|
|
489
|
+
getSummary(plan) {
|
|
490
|
+
const byStatus = {};
|
|
491
|
+
for (const st of plan.subtasks) {
|
|
492
|
+
byStatus[st.status] = (byStatus[st.status] || 0) + 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const byMode = {};
|
|
496
|
+
for (const st of plan.subtasks) {
|
|
497
|
+
const mode = st.delegation?.mode || 'unknown';
|
|
498
|
+
byMode[mode] = (byMode[mode] || 0) + 1;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
plan_id: plan.plan_id,
|
|
503
|
+
parent_task_id: plan.parent_task_id,
|
|
504
|
+
title: plan.title,
|
|
505
|
+
status: plan.status,
|
|
506
|
+
total_subtasks: plan.subtasks.length,
|
|
507
|
+
subtask_status: byStatus,
|
|
508
|
+
delegation_modes: byMode,
|
|
509
|
+
estimated_waves: plan.estimated_waves,
|
|
510
|
+
total_budget_minutes: plan.total_budget_minutes,
|
|
511
|
+
duration_ms: plan.completed_at
|
|
512
|
+
? new Date(plan.completed_at) - new Date(plan.created_at)
|
|
513
|
+
: Date.now() - new Date(plan.created_at),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
module.exports = {
|
|
519
|
+
createPlan,
|
|
520
|
+
assignWaves,
|
|
521
|
+
routeDelegation,
|
|
522
|
+
autoRoutePlan,
|
|
523
|
+
PlanStore,
|
|
524
|
+
PLAN_STATUS,
|
|
525
|
+
SUBTASK_STATUS,
|
|
526
|
+
DELEGATION_MODE,
|
|
527
|
+
PLANS_KV_BUCKET,
|
|
528
|
+
};
|
package/lib/mesh-tasks.js
CHANGED
|
@@ -48,6 +48,11 @@ function createTask({
|
|
|
48
48
|
priority = 0,
|
|
49
49
|
depends_on = [],
|
|
50
50
|
tags = [],
|
|
51
|
+
collaboration = null,
|
|
52
|
+
preferred_nodes = [],
|
|
53
|
+
exclude_nodes = [],
|
|
54
|
+
llm_provider = null,
|
|
55
|
+
llm_model = null,
|
|
51
56
|
}) {
|
|
52
57
|
return {
|
|
53
58
|
task_id,
|
|
@@ -66,9 +71,22 @@ function createTask({
|
|
|
66
71
|
depends_on,
|
|
67
72
|
tags,
|
|
68
73
|
|
|
74
|
+
// Node routing
|
|
75
|
+
preferred_nodes, // try these nodes first (ordered by preference)
|
|
76
|
+
exclude_nodes, // never assign to these nodes
|
|
77
|
+
|
|
78
|
+
// LLM selection (null = use agent default)
|
|
79
|
+
llm_provider, // 'claude' | 'openai' | 'shell' | custom
|
|
80
|
+
llm_model, // model override (e.g. 'gpt-4.1', 'opus', 'sonnet')
|
|
81
|
+
|
|
82
|
+
// N-node collaboration (null = solo task, backward compatible)
|
|
83
|
+
// When set, mesh-task-daemon creates a collab session and N nodes
|
|
84
|
+
// coordinate via rounds/reflections instead of single-agent execution.
|
|
85
|
+
collaboration, // { mode, min_nodes, max_nodes, join_window_s, max_rounds, convergence, scope_strategy }
|
|
86
|
+
|
|
69
87
|
// State (managed by daemon)
|
|
70
88
|
status: TASK_STATUS.QUEUED,
|
|
71
|
-
owner: null, // node_id that claimed it
|
|
89
|
+
owner: null, // node_id that claimed it (solo) or first claimer (collab)
|
|
72
90
|
created_at: new Date().toISOString(),
|
|
73
91
|
claimed_at: null,
|
|
74
92
|
started_at: null,
|
|
@@ -94,14 +112,10 @@ class TaskStore {
|
|
|
94
112
|
return task;
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
async get(taskId
|
|
115
|
+
async get(taskId) {
|
|
98
116
|
const entry = await this.kv.get(taskId);
|
|
99
117
|
if (!entry || !entry.value) return null;
|
|
100
|
-
|
|
101
|
-
if (withRevision) {
|
|
102
|
-
return { task, revision: entry.revision };
|
|
103
|
-
}
|
|
104
|
-
return task;
|
|
118
|
+
return JSON.parse(sc.decode(entry.value));
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
async delete(taskId) {
|
|
@@ -142,46 +156,48 @@ class TaskStore {
|
|
|
142
156
|
|
|
143
157
|
/**
|
|
144
158
|
* Claim the highest-priority available task for a node.
|
|
145
|
-
*
|
|
159
|
+
* Respects exclude_nodes (hard block) and preferred_nodes (soft priority).
|
|
146
160
|
* Returns the claimed task, or null if nothing available.
|
|
147
161
|
*/
|
|
148
162
|
async claim(nodeId) {
|
|
149
163
|
const available = await this.list({ status: TASK_STATUS.QUEUED });
|
|
150
164
|
|
|
151
|
-
//
|
|
165
|
+
// Filter and sort: excluded tasks removed, preferred tasks first
|
|
166
|
+
const claimable = [];
|
|
152
167
|
for (const task of available) {
|
|
168
|
+
// Hard exclusion
|
|
169
|
+
if (task.exclude_nodes && task.exclude_nodes.includes(nodeId)) continue;
|
|
170
|
+
|
|
171
|
+
// Respect dependencies
|
|
153
172
|
if (task.depends_on.length > 0) {
|
|
154
173
|
const depsReady = await this._checkDeps(task.depends_on);
|
|
155
174
|
if (!depsReady) continue;
|
|
156
175
|
}
|
|
157
176
|
|
|
158
|
-
|
|
159
|
-
const result = await this.get(task.task_id, { withRevision: true });
|
|
160
|
-
if (!result || result.task.status !== TASK_STATUS.QUEUED) continue;
|
|
161
|
-
|
|
162
|
-
// Claim it
|
|
163
|
-
result.task.status = TASK_STATUS.CLAIMED;
|
|
164
|
-
result.task.owner = nodeId;
|
|
165
|
-
result.task.claimed_at = new Date().toISOString();
|
|
166
|
-
result.task.budget_deadline = new Date(
|
|
167
|
-
Date.now() + result.task.budget_minutes * 60 * 1000
|
|
168
|
-
).toISOString();
|
|
169
|
-
|
|
170
|
-
// Atomic CAS update — fails if another agent claimed it first
|
|
171
|
-
try {
|
|
172
|
-
await this.kv.update(
|
|
173
|
-
task.task_id,
|
|
174
|
-
sc.encode(JSON.stringify(result.task)),
|
|
175
|
-
result.revision
|
|
176
|
-
);
|
|
177
|
-
return result.task;
|
|
178
|
-
} catch (err) {
|
|
179
|
-
// Revision mismatch — another agent got there first, try next task
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
177
|
+
claimable.push(task);
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
|
|
180
|
+
// Sort: preferred tasks for this node first, then by existing priority/date order
|
|
181
|
+
claimable.sort((a, b) => {
|
|
182
|
+
const aPreferred = a.preferred_nodes && a.preferred_nodes.includes(nodeId) ? 1 : 0;
|
|
183
|
+
const bPreferred = b.preferred_nodes && b.preferred_nodes.includes(nodeId) ? 1 : 0;
|
|
184
|
+
if (bPreferred !== aPreferred) return bPreferred - aPreferred;
|
|
185
|
+
// Fall back to existing sort (priority desc, then created_at asc — already applied by list())
|
|
186
|
+
return 0;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (claimable.length === 0) return null;
|
|
190
|
+
|
|
191
|
+
const task = claimable[0];
|
|
192
|
+
task.status = TASK_STATUS.CLAIMED;
|
|
193
|
+
task.owner = nodeId;
|
|
194
|
+
task.claimed_at = new Date().toISOString();
|
|
195
|
+
task.budget_deadline = new Date(
|
|
196
|
+
Date.now() + task.budget_minutes * 60 * 1000
|
|
197
|
+
).toISOString();
|
|
198
|
+
|
|
199
|
+
await this.put(task);
|
|
200
|
+
return task;
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
/**
|
package/package.json
CHANGED