openclaw-node-harness 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lane-watchdog.js +54 -23
- package/bin/mesh-agent.js +49 -18
- package/bin/mesh-bridge.js +3 -2
- package/bin/mesh-deploy.js +4 -0
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +14 -4
- package/bin/mesh.js +17 -43
- package/install.sh +3 -2
- package/lib/agent-activity.js +2 -2
- package/lib/exec-safety.js +163 -0
- package/lib/kanban-io.js +20 -33
- package/lib/llm-providers.js +27 -0
- package/lib/mcp-knowledge/core.mjs +7 -5
- package/lib/mcp-knowledge/server.mjs +8 -1
- package/lib/mesh-collab.js +274 -250
- package/lib/mesh-harness.js +6 -0
- package/lib/mesh-plans.js +84 -45
- package/lib/mesh-tasks.js +113 -81
- package/lib/nats-resolve.js +4 -4
- package/lib/pre-compression-flush.mjs +2 -0
- package/lib/session-store.mjs +6 -3
- package/mission-control/package-lock.json +4188 -3698
- package/mission-control/package.json +2 -2
- package/mission-control/src/app/api/diagnostics/route.ts +8 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
- package/mission-control/src/app/api/memory/graph/route.ts +34 -18
- package/mission-control/src/app/api/memory/search/route.ts +9 -5
- package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
- package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
- package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +49 -12
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +24 -5
- package/mission-control/src/app/api/souls/route.ts +6 -4
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +7 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
- package/mission-control/src/app/api/tasks/route.ts +68 -9
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/lib/config.ts +11 -2
- package/mission-control/src/lib/db/index.ts +16 -1
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/sync/tasks.ts +4 -1
- package/mission-control/src/middleware.ts +82 -0
- package/package.json +1 -1
- package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +5 -4
- package/uninstall.sh +37 -9
package/lib/mesh-harness.js
CHANGED
|
@@ -22,6 +22,7 @@ const fs = require('fs');
|
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const { execSync } = require('child_process');
|
|
24
24
|
const { globMatch } = require('./rule-loader');
|
|
25
|
+
const { validateExecCommand } = require('./exec-safety');
|
|
25
26
|
|
|
26
27
|
// ── Rule Loading ─────────────────────────────────────
|
|
27
28
|
|
|
@@ -241,6 +242,11 @@ function preCommitSecretScan(worktreePath) {
|
|
|
241
242
|
function postCommitValidate(worktreePath, command) {
|
|
242
243
|
if (!worktreePath || !command) return { passed: true, output: '' };
|
|
243
244
|
|
|
245
|
+
const validation = validateExecCommand(command);
|
|
246
|
+
if (!validation.allowed) {
|
|
247
|
+
return { passed: false, output: `Validation command blocked: ${validation.reason}` };
|
|
248
|
+
}
|
|
249
|
+
|
|
244
250
|
try {
|
|
245
251
|
const output = execSync(command, {
|
|
246
252
|
cwd: worktreePath, timeout: 10000, encoding: 'utf-8', stdio: 'pipe',
|
package/lib/mesh-plans.js
CHANGED
|
@@ -38,6 +38,13 @@ const SUBTASK_STATUS = {
|
|
|
38
38
|
|
|
39
39
|
// ── Delegation Modes ───────────────────────────────
|
|
40
40
|
|
|
41
|
+
const PLAN_TRANSITIONS = {
|
|
42
|
+
approve: new Set(['review', 'draft']),
|
|
43
|
+
startExecuting: new Set(['approved']),
|
|
44
|
+
markCompleted: new Set(['executing']),
|
|
45
|
+
markAborted: new Set(['draft', 'review', 'approved', 'executing']),
|
|
46
|
+
};
|
|
47
|
+
|
|
41
48
|
const DELEGATION_MODE = {
|
|
42
49
|
SOLO_MESH: 'solo_mesh',
|
|
43
50
|
COLLAB_MESH: 'collab_mesh',
|
|
@@ -127,6 +134,11 @@ function createPlan({
|
|
|
127
134
|
// Compute wave assignments
|
|
128
135
|
assignWaves(enriched);
|
|
129
136
|
|
|
137
|
+
// Mark cycle-blocked subtasks (wave === -1) so they don't prevent plan completion
|
|
138
|
+
for (const st of enriched) {
|
|
139
|
+
if (st.wave === -1 && st.status === 'pending') st.status = 'blocked';
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
const totalBudget = enriched.reduce((sum, st) => sum + st.budget_minutes, 0);
|
|
131
143
|
const maxWave = enriched.reduce((max, st) => Math.max(max, st.wave), 0);
|
|
132
144
|
|
|
@@ -210,6 +222,14 @@ function assignWaves(subtasks) {
|
|
|
210
222
|
wave++;
|
|
211
223
|
currentWave = nextWave;
|
|
212
224
|
}
|
|
225
|
+
|
|
226
|
+
// Detect cycles: any node with remaining in-degree > 0 is in a cycle
|
|
227
|
+
for (const [taskId, degree] of inDegree.entries()) {
|
|
228
|
+
if (degree > 0) {
|
|
229
|
+
const subtask = idMap.get(taskId);
|
|
230
|
+
if (subtask) subtask.wave = -1; // blocked by cycle
|
|
231
|
+
}
|
|
232
|
+
}
|
|
213
233
|
}
|
|
214
234
|
|
|
215
235
|
// ── Delegation Decision Tree ───────────────────────
|
|
@@ -351,6 +371,29 @@ class PlanStore {
|
|
|
351
371
|
return JSON.parse(sc.decode(entry.value));
|
|
352
372
|
}
|
|
353
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Compare-and-swap helper: read → mutate → write with optimistic concurrency.
|
|
376
|
+
* Re-reads and retries on conflict (up to maxRetries).
|
|
377
|
+
* mutateFn receives the parsed data and must return the updated object, or falsy to skip.
|
|
378
|
+
*/
|
|
379
|
+
async _updateWithCAS(key, mutateFn, maxRetries = 3) {
|
|
380
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
381
|
+
const entry = await this.kv.get(key);
|
|
382
|
+
if (!entry) return null;
|
|
383
|
+
const data = JSON.parse(sc.decode(entry.value));
|
|
384
|
+
const updated = mutateFn(data);
|
|
385
|
+
if (!updated) return null;
|
|
386
|
+
try {
|
|
387
|
+
await this.kv.put(key, sc.encode(JSON.stringify(updated)), { previousSeq: entry.revision });
|
|
388
|
+
return updated;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const isCasConflict = err.code === '10071' || (err.message && err.message.includes('wrong last sequence'));
|
|
391
|
+
if (!isCasConflict || attempt === maxRetries - 1) throw err;
|
|
392
|
+
// CAS conflict — retry
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
354
397
|
async delete(planId) {
|
|
355
398
|
await this.kv.delete(planId);
|
|
356
399
|
}
|
|
@@ -389,69 +432,65 @@ class PlanStore {
|
|
|
389
432
|
// ── Lifecycle ───────────────────────────────────
|
|
390
433
|
|
|
391
434
|
async submitForReview(planId) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
436
|
+
if (plan.status !== 'draft') return null;
|
|
437
|
+
plan.status = PLAN_STATUS.REVIEW;
|
|
438
|
+
return plan;
|
|
439
|
+
});
|
|
397
440
|
}
|
|
398
441
|
|
|
399
442
|
async approve(planId, approvedBy = 'gui') {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
443
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
444
|
+
if (!PLAN_TRANSITIONS.approve.has(plan.status)) return null;
|
|
445
|
+
plan.status = PLAN_STATUS.APPROVED;
|
|
446
|
+
plan.approved_by = approvedBy;
|
|
447
|
+
plan.approved_at = new Date().toISOString();
|
|
448
|
+
return plan;
|
|
449
|
+
});
|
|
407
450
|
}
|
|
408
451
|
|
|
409
452
|
async startExecuting(planId) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
453
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
454
|
+
if (!PLAN_TRANSITIONS.startExecuting.has(plan.status)) return null;
|
|
455
|
+
plan.status = PLAN_STATUS.EXECUTING;
|
|
456
|
+
plan.started_at = new Date().toISOString();
|
|
457
|
+
return plan;
|
|
458
|
+
});
|
|
416
459
|
}
|
|
417
460
|
|
|
418
461
|
async markCompleted(planId) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
462
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
463
|
+
if (!PLAN_TRANSITIONS.markCompleted.has(plan.status)) return null;
|
|
464
|
+
plan.status = PLAN_STATUS.COMPLETED;
|
|
465
|
+
plan.completed_at = new Date().toISOString();
|
|
466
|
+
return plan;
|
|
467
|
+
});
|
|
425
468
|
}
|
|
426
469
|
|
|
427
470
|
async markAborted(planId, reason) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
471
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
472
|
+
if (!PLAN_TRANSITIONS.markAborted.has(plan.status)) return null;
|
|
473
|
+
plan.status = PLAN_STATUS.ABORTED;
|
|
474
|
+
plan.completed_at = new Date().toISOString();
|
|
475
|
+
for (const st of plan.subtasks) {
|
|
476
|
+
if (st.status === SUBTASK_STATUS.PENDING || st.status === SUBTASK_STATUS.QUEUED) {
|
|
477
|
+
st.status = SUBTASK_STATUS.BLOCKED;
|
|
478
|
+
st.result = { success: false, summary: `Plan aborted: ${reason}` };
|
|
479
|
+
}
|
|
437
480
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
return plan;
|
|
481
|
+
return plan;
|
|
482
|
+
});
|
|
441
483
|
}
|
|
442
484
|
|
|
443
485
|
// ── Subtask Management ──────────────────────────
|
|
444
486
|
|
|
445
487
|
async updateSubtask(planId, subtaskId, updates) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Object.assign(st, updates);
|
|
453
|
-
await this.put(plan);
|
|
454
|
-
return plan;
|
|
488
|
+
return this._updateWithCAS(planId, (plan) => {
|
|
489
|
+
const st = plan.subtasks.find(s => s.subtask_id === subtaskId);
|
|
490
|
+
if (!st) return null;
|
|
491
|
+
Object.assign(st, updates);
|
|
492
|
+
return plan;
|
|
493
|
+
});
|
|
455
494
|
}
|
|
456
495
|
|
|
457
496
|
/**
|
package/lib/mesh-tasks.js
CHANGED
|
@@ -46,6 +46,14 @@ const TASK_STATUS = {
|
|
|
46
46
|
REJECTED: 'rejected',
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const TERMINAL_STATES = new Set([
|
|
50
|
+
TASK_STATUS.COMPLETED,
|
|
51
|
+
TASK_STATUS.FAILED,
|
|
52
|
+
TASK_STATUS.RELEASED,
|
|
53
|
+
TASK_STATUS.CANCELLED,
|
|
54
|
+
TASK_STATUS.REJECTED,
|
|
55
|
+
]);
|
|
56
|
+
|
|
49
57
|
/**
|
|
50
58
|
* Create a new task with the enriched schema.
|
|
51
59
|
* Karpathy-inspired fields: budget_minutes, metric, on_fail, scope.
|
|
@@ -142,6 +150,29 @@ class TaskStore {
|
|
|
142
150
|
return JSON.parse(sc.decode(entry.value));
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Compare-and-swap helper: read → mutate → write with optimistic concurrency.
|
|
155
|
+
* Re-reads and retries on conflict (up to maxRetries).
|
|
156
|
+
* mutateFn receives the parsed data and must return the updated object, or falsy to skip.
|
|
157
|
+
*/
|
|
158
|
+
async _updateWithCAS(key, mutateFn, maxRetries = 3) {
|
|
159
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
160
|
+
const entry = await this.kv.get(key);
|
|
161
|
+
if (!entry) return null;
|
|
162
|
+
const data = JSON.parse(sc.decode(entry.value));
|
|
163
|
+
const updated = mutateFn(data);
|
|
164
|
+
if (!updated) return null;
|
|
165
|
+
try {
|
|
166
|
+
await this.kv.put(key, sc.encode(JSON.stringify(updated)), { previousSeq: entry.revision });
|
|
167
|
+
return updated;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const isCasConflict = err.code === '10071' || (err.message && err.message.includes('wrong last sequence'));
|
|
170
|
+
if (!isCasConflict || attempt === maxRetries - 1) throw err;
|
|
171
|
+
// CAS conflict — retry
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
145
176
|
async delete(taskId) {
|
|
146
177
|
await this.kv.delete(taskId);
|
|
147
178
|
}
|
|
@@ -213,39 +244,42 @@ class TaskStore {
|
|
|
213
244
|
if (claimable.length === 0) return null;
|
|
214
245
|
|
|
215
246
|
const task = claimable[0];
|
|
216
|
-
task.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
247
|
+
const result = await this._updateWithCAS(task.task_id, (t) => {
|
|
248
|
+
// Re-check status under CAS — another node may have claimed it
|
|
249
|
+
if (t.status !== TASK_STATUS.QUEUED) return null;
|
|
250
|
+
t.status = TASK_STATUS.CLAIMED;
|
|
251
|
+
t.owner = nodeId;
|
|
252
|
+
t.claimed_at = new Date().toISOString();
|
|
253
|
+
const budgetMs = (t.budget_minutes || 30) * 60 * 1000;
|
|
254
|
+
t.budget_deadline = new Date(Date.now() + budgetMs).toISOString();
|
|
255
|
+
return t;
|
|
256
|
+
});
|
|
257
|
+
return result;
|
|
224
258
|
}
|
|
225
259
|
|
|
226
260
|
/**
|
|
227
261
|
* Mark a task as running (agent started work).
|
|
228
262
|
*/
|
|
229
263
|
async markRunning(taskId) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
264
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
265
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
266
|
+
task.status = TASK_STATUS.RUNNING;
|
|
267
|
+
task.started_at = new Date().toISOString();
|
|
268
|
+
return task;
|
|
269
|
+
});
|
|
236
270
|
}
|
|
237
271
|
|
|
238
272
|
/**
|
|
239
273
|
* Mark a task as completed with result.
|
|
240
274
|
*/
|
|
241
275
|
async markCompleted(taskId, result) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
276
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
277
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
278
|
+
task.status = TASK_STATUS.COMPLETED;
|
|
279
|
+
task.completed_at = new Date().toISOString();
|
|
280
|
+
task.result = result;
|
|
281
|
+
return task;
|
|
282
|
+
});
|
|
249
283
|
}
|
|
250
284
|
|
|
251
285
|
/**
|
|
@@ -253,70 +287,68 @@ class TaskStore {
|
|
|
253
287
|
* Stores the result but doesn't transition to completed.
|
|
254
288
|
*/
|
|
255
289
|
async markPendingReview(taskId, result) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
290
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
291
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
292
|
+
task.status = TASK_STATUS.PENDING_REVIEW;
|
|
293
|
+
task.result = result;
|
|
294
|
+
task.review_requested_at = new Date().toISOString();
|
|
295
|
+
return task;
|
|
296
|
+
});
|
|
263
297
|
}
|
|
264
298
|
|
|
265
299
|
/**
|
|
266
300
|
* Approve a pending_review task → completed.
|
|
267
301
|
*/
|
|
268
302
|
async markApproved(taskId) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return task;
|
|
303
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
304
|
+
if (task.status !== TASK_STATUS.PENDING_REVIEW) return null;
|
|
305
|
+
task.status = TASK_STATUS.COMPLETED;
|
|
306
|
+
task.completed_at = new Date().toISOString();
|
|
307
|
+
task.reviewed_by = 'human';
|
|
308
|
+
return task;
|
|
309
|
+
});
|
|
277
310
|
}
|
|
278
311
|
|
|
279
312
|
/**
|
|
280
313
|
* Reject a pending_review task → re-queue with reason.
|
|
281
314
|
*/
|
|
282
315
|
async markRejected(taskId, reason) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return task;
|
|
316
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
317
|
+
if (task.status !== TASK_STATUS.PENDING_REVIEW) return null;
|
|
318
|
+
task.status = TASK_STATUS.QUEUED;
|
|
319
|
+
task.rejection_reason = reason;
|
|
320
|
+
task.result = null;
|
|
321
|
+
task.review_requested_at = null;
|
|
322
|
+
return task;
|
|
323
|
+
});
|
|
292
324
|
}
|
|
293
325
|
|
|
294
326
|
/**
|
|
295
327
|
* Mark a task as failed with reason.
|
|
296
328
|
*/
|
|
297
329
|
async markFailed(taskId, reason, attempts = []) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
330
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
331
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
332
|
+
task.status = TASK_STATUS.FAILED;
|
|
333
|
+
task.completed_at = new Date().toISOString();
|
|
334
|
+
task.result = { success: false, summary: reason };
|
|
335
|
+
task.attempts = attempts;
|
|
336
|
+
return task;
|
|
337
|
+
});
|
|
306
338
|
}
|
|
307
339
|
|
|
308
340
|
/**
|
|
309
341
|
* Log an attempt on a task (agent tried something, may or may not have worked).
|
|
310
342
|
*/
|
|
311
343
|
async logAttempt(taskId, attempt) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
344
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
345
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
346
|
+
task.attempts.push({
|
|
347
|
+
...attempt,
|
|
348
|
+
timestamp: new Date().toISOString(),
|
|
349
|
+
});
|
|
350
|
+
return task;
|
|
317
351
|
});
|
|
318
|
-
await this.put(task);
|
|
319
|
-
return task;
|
|
320
352
|
}
|
|
321
353
|
|
|
322
354
|
/**
|
|
@@ -324,25 +356,25 @@ class TaskStore {
|
|
|
324
356
|
* Distinct from failed: failed = "didn't work", released = "we tried everything."
|
|
325
357
|
*/
|
|
326
358
|
async markReleased(taskId, reason, attempts = []) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
359
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
360
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
361
|
+
task.status = TASK_STATUS.RELEASED;
|
|
362
|
+
task.completed_at = new Date().toISOString();
|
|
363
|
+
task.result = { success: false, summary: reason, released: true };
|
|
364
|
+
if (attempts.length > 0) task.attempts = attempts;
|
|
365
|
+
return task;
|
|
366
|
+
});
|
|
335
367
|
}
|
|
336
368
|
|
|
337
369
|
/**
|
|
338
370
|
* Update last_activity timestamp (agent heartbeat).
|
|
339
371
|
*/
|
|
340
372
|
async touchActivity(taskId) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
373
|
+
return this._updateWithCAS(taskId, (task) => {
|
|
374
|
+
if (TERMINAL_STATES.has(task.status)) return null;
|
|
375
|
+
task.last_activity = new Date().toISOString();
|
|
376
|
+
return task;
|
|
377
|
+
});
|
|
346
378
|
}
|
|
347
379
|
|
|
348
380
|
/**
|
|
@@ -363,25 +395,25 @@ class TaskStore {
|
|
|
363
395
|
}
|
|
364
396
|
|
|
365
397
|
/**
|
|
366
|
-
* Find running tasks with no activity for `stallMinutes`.
|
|
398
|
+
* Find running or claimed tasks with no activity for `stallMinutes`.
|
|
367
399
|
* Stall detection is separate from budget — a task can be within budget
|
|
368
400
|
* but the agent process may have died silently.
|
|
401
|
+
* Claimed tasks that never transition to running (agent crashed after claim)
|
|
402
|
+
* are also detected and released back to queued.
|
|
369
403
|
*/
|
|
370
404
|
async findStalled(stallMinutes = 5) {
|
|
371
405
|
const running = await this.list({ status: TASK_STATUS.RUNNING });
|
|
406
|
+
const claimed = await this.list({ status: TASK_STATUS.CLAIMED });
|
|
372
407
|
const cutoff = Date.now() - stallMinutes * 60 * 1000;
|
|
373
|
-
return running.filter(t => {
|
|
374
|
-
const lastSignal = t.last_activity || t.started_at;
|
|
408
|
+
return [...running, ...claimed].filter(t => {
|
|
409
|
+
const lastSignal = t.last_activity || t.started_at || t.claimed_at;
|
|
375
410
|
return lastSignal && new Date(lastSignal) < cutoff;
|
|
376
411
|
});
|
|
377
412
|
}
|
|
378
413
|
|
|
379
414
|
async _checkDeps(depIds) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!dep || dep.status !== TASK_STATUS.COMPLETED) return false;
|
|
383
|
-
}
|
|
384
|
-
return true;
|
|
415
|
+
const deps = await Promise.all(depIds.map(id => this.get(id)));
|
|
416
|
+
return deps.every(dep => dep && dep.status === TASK_STATUS.COMPLETED);
|
|
385
417
|
}
|
|
386
418
|
}
|
|
387
419
|
|
package/lib/nats-resolve.js
CHANGED
|
@@ -37,7 +37,7 @@ function resolveNatsUrl() {
|
|
|
37
37
|
if (fs.existsSync(envFile)) {
|
|
38
38
|
const content = fs.readFileSync(envFile, 'utf8');
|
|
39
39
|
const match = content.match(/^\s*OPENCLAW_NATS\s*=\s*(.+)/m);
|
|
40
|
-
if (match && match[1].trim()) return match[1].trim();
|
|
40
|
+
if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, '');
|
|
41
41
|
}
|
|
42
42
|
} catch {
|
|
43
43
|
// File unreadable — fall through silently
|
|
@@ -49,7 +49,7 @@ function resolveNatsUrl() {
|
|
|
49
49
|
if (fs.existsSync(meshConfig)) {
|
|
50
50
|
const content = fs.readFileSync(meshConfig, 'utf8');
|
|
51
51
|
const match = content.match(/^\s*OPENCLAW_NATS\s*=\s*(.+)/m);
|
|
52
|
-
if (match && match[1].trim()) return match[1].trim();
|
|
52
|
+
if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, '');
|
|
53
53
|
}
|
|
54
54
|
} catch {
|
|
55
55
|
// File unreadable — fall through silently
|
|
@@ -73,7 +73,7 @@ function resolveNatsToken() {
|
|
|
73
73
|
if (fs.existsSync(envFile)) {
|
|
74
74
|
const content = fs.readFileSync(envFile, 'utf8');
|
|
75
75
|
const match = content.match(/^\s*OPENCLAW_NATS_TOKEN\s*=\s*(.+)/m);
|
|
76
|
-
if (match && match[1].trim()) return match[1].trim();
|
|
76
|
+
if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, '');
|
|
77
77
|
}
|
|
78
78
|
} catch {}
|
|
79
79
|
|
|
@@ -83,7 +83,7 @@ function resolveNatsToken() {
|
|
|
83
83
|
if (fs.existsSync(meshConfig)) {
|
|
84
84
|
const content = fs.readFileSync(meshConfig, 'utf8');
|
|
85
85
|
const match = content.match(/^\s*OPENCLAW_NATS_TOKEN\s*=\s*(.+)/m);
|
|
86
|
-
if (match && match[1].trim()) return match[1].trim();
|
|
86
|
+
if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, '');
|
|
87
87
|
}
|
|
88
88
|
} catch {}
|
|
89
89
|
|
|
@@ -74,6 +74,8 @@ export async function shouldFlush(jsonlPath, opts = {}) {
|
|
|
74
74
|
const { contextWindowTokens = 200000, flushPct = 0.75 } = opts;
|
|
75
75
|
const threshold = Math.floor(contextWindowTokens * flushPct);
|
|
76
76
|
|
|
77
|
+
if (!fs.existsSync(jsonlPath)) return { shouldFlush: false, estimatedTokens: 0, pctUsed: 0, threshold };
|
|
78
|
+
|
|
77
79
|
const stat = fs.statSync(jsonlPath);
|
|
78
80
|
// Quick estimate from file size — ~4 chars/token, but JSONL has overhead (~2x)
|
|
79
81
|
const quickEstimate = Math.ceil(stat.size / (CHARS_PER_TOKEN * 2));
|
package/lib/session-store.mjs
CHANGED
|
@@ -398,10 +398,13 @@ export class SessionStore {
|
|
|
398
398
|
* - Escapes quotes
|
|
399
399
|
*/
|
|
400
400
|
#buildFtsQuery(query) {
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
if (!query || !query.trim()) return null;
|
|
402
|
+
// Escape double quotes, then strip FTS5 operators to prevent query injection
|
|
403
|
+
const escaped = query.replace(/"/g, '""').trim();
|
|
404
|
+
const safe = escaped.replace(/[*(){}^]/g, '');
|
|
405
|
+
if (!safe) return null;
|
|
403
406
|
|
|
404
|
-
const words =
|
|
407
|
+
const words = safe.split(/\s+/).filter(w => w.length > 0);
|
|
405
408
|
if (words.length === 0) return null;
|
|
406
409
|
|
|
407
410
|
if (words.length === 1) {
|