principles-disciple 1.7.1 → 1.7.2
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/dist/constants/tools.d.ts +17 -0
- package/dist/constants/tools.js +54 -0
- package/dist/core/event-log.d.ts +4 -0
- package/dist/core/event-log.js +62 -118
- package/dist/core/evolution-engine.d.ts +3 -4
- package/dist/core/evolution-engine.js +60 -118
- package/dist/core/migration.js +1 -1
- package/dist/core/session-tracker.d.ts +1 -0
- package/dist/core/session-tracker.js +39 -11
- package/dist/core/trust-engine.d.ts +1 -2
- package/dist/core/trust-engine.js +4 -23
- package/dist/hooks/gate.js +4 -25
- package/dist/hooks/prompt.js +12 -1
- package/dist/hooks/subagent.js +109 -63
- package/dist/service/control-ui-query-service.d.ts +2 -0
- package/dist/service/control-ui-query-service.js +2 -0
- package/dist/service/evolution-worker.d.ts +12 -8
- package/dist/service/evolution-worker.js +153 -123
- package/dist/service/runtime-summary-service.d.ts +4 -0
- package/dist/service/runtime-summary-service.js +43 -4
- package/dist/tools/agent-spawn.js +23 -0
- package/dist/utils/file-lock.d.ts +7 -0
- package/dist/utils/file-lock.js +66 -27
- package/openclaw.plugin.json +13 -12
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { extractCommonSubstring } from '../utils/nlp.js';
|
|
|
7
7
|
import { SystemLogger } from '../core/system-logger.js';
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
10
|
+
import { acquireLockAsync, releaseLock } from '../utils/file-lock.js';
|
|
10
11
|
let intervalId = null;
|
|
11
12
|
const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
|
|
12
13
|
// P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
|
|
@@ -61,84 +62,55 @@ export function summarizePainCandidateSample(text) {
|
|
|
61
62
|
function isPendingPainCandidate(status) {
|
|
62
63
|
return status === undefined || status === 'pending';
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
fs.unlinkSync(lockPath);
|
|
79
|
-
}
|
|
80
|
-
catch { /* ignore */ }
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
if (err.code === 'EEXIST') {
|
|
85
|
-
// Check if lock is stale
|
|
86
|
-
try {
|
|
87
|
-
const stat = fs.statSync(lockPath);
|
|
88
|
-
const content = fs.readFileSync(lockPath, 'utf8').trim();
|
|
89
|
-
const pid = parseInt(content.split('\n')[0] || '0', 10);
|
|
90
|
-
let isStale = false;
|
|
91
|
-
if (pid > 0) {
|
|
92
|
-
try {
|
|
93
|
-
process.kill(pid, 0);
|
|
94
|
-
}
|
|
95
|
-
catch (e) {
|
|
96
|
-
if (e.code === 'ESRCH')
|
|
97
|
-
isStale = true;
|
|
98
|
-
}
|
|
99
|
-
if (!isStale && Date.now() - stat.mtimeMs > LOCK_STALE_MS)
|
|
100
|
-
isStale = true;
|
|
101
|
-
}
|
|
102
|
-
else if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
103
|
-
isStale = true;
|
|
104
|
-
}
|
|
105
|
-
if (isStale) {
|
|
106
|
-
fs.unlinkSync(lockPath);
|
|
107
|
-
retries++;
|
|
108
|
-
const start = Date.now();
|
|
109
|
-
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch { /* stat/read failed, treat as busy */ }
|
|
114
|
-
retries++;
|
|
115
|
-
const start = Date.now();
|
|
116
|
-
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
throw err;
|
|
120
|
-
}
|
|
65
|
+
export async function acquireQueueLock(resourcePath, logger, lockSuffix = EVOLUTION_QUEUE_LOCK_SUFFIX) {
|
|
66
|
+
try {
|
|
67
|
+
const ctx = await acquireLockAsync(resourcePath, {
|
|
68
|
+
lockSuffix,
|
|
69
|
+
maxRetries: LOCK_MAX_RETRIES,
|
|
70
|
+
baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
|
|
71
|
+
lockStaleMs: LOCK_STALE_MS,
|
|
72
|
+
});
|
|
73
|
+
return () => releaseLock(ctx);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock for ${resourcePath}: ${String(error)}`);
|
|
77
|
+
throw error;
|
|
121
78
|
}
|
|
122
|
-
logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock after ${LOCK_MAX_RETRIES} retries: ${lockPath}`);
|
|
123
|
-
return null;
|
|
124
79
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
80
|
+
async function requireQueueLock(resourcePath, logger, scope, lockSuffix = EVOLUTION_QUEUE_LOCK_SUFFIX) {
|
|
81
|
+
try {
|
|
82
|
+
return await acquireQueueLock(resourcePath, logger, lockSuffix);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error(`[PD:EvolutionWorker] ${scope}: queue lock unavailable for ${resourcePath}`);
|
|
86
|
+
}
|
|
130
87
|
}
|
|
131
|
-
export function
|
|
88
|
+
export function extractEvolutionTaskId(task) {
|
|
89
|
+
if (!task)
|
|
90
|
+
return null;
|
|
91
|
+
const match = task.match(/\[ID:\s*([A-Za-z0-9_-]+)\]/);
|
|
92
|
+
return match?.[1] || null;
|
|
93
|
+
}
|
|
94
|
+
function findRecentDuplicateTask(queue, source, preview, now, reason) {
|
|
132
95
|
const key = normalizePainDedupKey(source, preview, reason);
|
|
133
|
-
return queue.
|
|
96
|
+
return queue.find((task) => {
|
|
134
97
|
if (task.status === 'completed')
|
|
135
98
|
return false;
|
|
136
|
-
const taskTime = new Date(task.timestamp).getTime();
|
|
99
|
+
const taskTime = new Date(task.enqueued_at || task.timestamp).getTime();
|
|
137
100
|
if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
|
|
138
101
|
return false;
|
|
139
102
|
return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
|
|
140
103
|
});
|
|
141
104
|
}
|
|
105
|
+
function normalizePainDedupKey(source, preview, reason) {
|
|
106
|
+
// Include reason in dedup key to match createEvolutionTaskId() behavior
|
|
107
|
+
// Different reasons for the same source/preview should create different tasks
|
|
108
|
+
const normalizedReason = (reason || '').trim().toLowerCase();
|
|
109
|
+
return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
|
|
110
|
+
}
|
|
111
|
+
export function hasRecentDuplicateTask(queue, source, preview, now, reason) {
|
|
112
|
+
return !!findRecentDuplicateTask(queue, source, preview, now, reason);
|
|
113
|
+
}
|
|
142
114
|
export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
143
115
|
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
144
116
|
return Object.values(dictionary.getAllRules()).some((rule) => {
|
|
@@ -153,7 +125,7 @@ export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
|
153
125
|
return false;
|
|
154
126
|
});
|
|
155
127
|
}
|
|
156
|
-
function checkPainFlag(wctx, logger) {
|
|
128
|
+
async function checkPainFlag(wctx, logger) {
|
|
157
129
|
try {
|
|
158
130
|
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
159
131
|
if (!fs.existsSync(painFlagPath))
|
|
@@ -182,10 +154,7 @@ function checkPainFlag(wctx, logger) {
|
|
|
182
154
|
if (logger)
|
|
183
155
|
logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
|
|
184
156
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
185
|
-
const
|
|
186
|
-
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
187
|
-
if (!releaseLock)
|
|
188
|
-
return; // Could not acquire lock
|
|
157
|
+
const releaseLock = await requireQueueLock(queuePath, logger, 'checkPainFlag');
|
|
189
158
|
try {
|
|
190
159
|
let queue = [];
|
|
191
160
|
if (fs.existsSync(queuePath)) {
|
|
@@ -198,9 +167,10 @@ function checkPainFlag(wctx, logger) {
|
|
|
198
167
|
}
|
|
199
168
|
}
|
|
200
169
|
const now = Date.now();
|
|
201
|
-
|
|
170
|
+
const duplicateTask = findRecentDuplicateTask(queue, source, preview, now, reason);
|
|
171
|
+
if (duplicateTask) {
|
|
202
172
|
logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
|
|
203
|
-
fs.appendFileSync(painFlagPath, `\nstatus: queued\n`, 'utf8');
|
|
173
|
+
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${duplicateTask.id}\n`, 'utf8');
|
|
204
174
|
return;
|
|
205
175
|
}
|
|
206
176
|
const taskId = createEvolutionTaskId(source, score, preview, reason, now);
|
|
@@ -211,10 +181,11 @@ function checkPainFlag(wctx, logger) {
|
|
|
211
181
|
reason,
|
|
212
182
|
trigger_text_preview: preview,
|
|
213
183
|
timestamp: new Date(now).toISOString(),
|
|
184
|
+
enqueued_at: new Date(now).toISOString(),
|
|
214
185
|
status: 'pending'
|
|
215
186
|
});
|
|
216
187
|
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
217
|
-
fs.appendFileSync(painFlagPath,
|
|
188
|
+
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
|
|
218
189
|
}
|
|
219
190
|
finally {
|
|
220
191
|
releaseLock();
|
|
@@ -225,14 +196,12 @@ function checkPainFlag(wctx, logger) {
|
|
|
225
196
|
logger.warn(`[PD:EvolutionWorker] Error processing pain flag: ${String(err)}`);
|
|
226
197
|
}
|
|
227
198
|
}
|
|
228
|
-
function processEvolutionQueue(wctx, logger, eventLog) {
|
|
199
|
+
async function processEvolutionQueue(wctx, logger, eventLog) {
|
|
229
200
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
230
201
|
if (!fs.existsSync(queuePath))
|
|
231
202
|
return;
|
|
232
|
-
const
|
|
233
|
-
const releaseLock =
|
|
234
|
-
if (!releaseLock)
|
|
235
|
-
return; // Could not acquire lock
|
|
203
|
+
const directivePath = wctx.resolve('EVOLUTION_DIRECTIVE');
|
|
204
|
+
const releaseLock = await requireQueueLock(queuePath, logger, 'processEvolutionQueue');
|
|
236
205
|
try {
|
|
237
206
|
let queue = [];
|
|
238
207
|
try {
|
|
@@ -248,29 +217,29 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
248
217
|
const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
|
|
249
218
|
for (const task of queue) {
|
|
250
219
|
if (task.status === 'in_progress' && task.timestamp) {
|
|
251
|
-
const
|
|
220
|
+
const startedAt = task.started_at || task.timestamp;
|
|
221
|
+
const age = Date.now() - new Date(startedAt).getTime();
|
|
252
222
|
if (age > timeout) {
|
|
253
223
|
if (logger)
|
|
254
224
|
logger.info(`[PD:EvolutionWorker] Resetting timed-out task: ${task.id}`);
|
|
255
225
|
task.status = 'pending';
|
|
226
|
+
delete task.started_at;
|
|
227
|
+
delete task.assigned_session_key;
|
|
256
228
|
queueChanged = true;
|
|
257
229
|
}
|
|
258
230
|
}
|
|
259
231
|
}
|
|
260
232
|
const pendingTasks = queue.filter(t => t.status === 'pending');
|
|
261
233
|
if (pendingTasks.length > 0) {
|
|
262
|
-
const directivePath = wctx.resolve('EVOLUTION_DIRECTIVE');
|
|
263
234
|
const highestScoreTask = pendingTasks.sort((a, b) => b.score - a.score)[0];
|
|
235
|
+
const nowIso = new Date().toISOString();
|
|
264
236
|
const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
|
|
265
237
|
`Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
|
|
266
|
-
const directive = {
|
|
267
|
-
active: true,
|
|
268
|
-
task: taskDescription,
|
|
269
|
-
timestamp: new Date().toISOString()
|
|
270
|
-
};
|
|
271
|
-
fs.writeFileSync(directivePath, JSON.stringify(directive, null, 2), 'utf8');
|
|
272
238
|
highestScoreTask.task = taskDescription;
|
|
273
239
|
highestScoreTask.status = 'in_progress';
|
|
240
|
+
highestScoreTask.started_at = nowIso;
|
|
241
|
+
delete highestScoreTask.completed_at;
|
|
242
|
+
delete highestScoreTask.assigned_session_key;
|
|
274
243
|
queueChanged = true;
|
|
275
244
|
if (eventLog) {
|
|
276
245
|
eventLog.recordEvolutionTask({
|
|
@@ -279,6 +248,43 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
279
248
|
reason: highestScoreTask.reason
|
|
280
249
|
});
|
|
281
250
|
}
|
|
251
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
252
|
+
queueChanged = false;
|
|
253
|
+
const directive = {
|
|
254
|
+
active: true,
|
|
255
|
+
taskId: highestScoreTask.id,
|
|
256
|
+
task: taskDescription,
|
|
257
|
+
timestamp: nowIso
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
fs.writeFileSync(directivePath, JSON.stringify(directive, null, 2), 'utf8');
|
|
261
|
+
}
|
|
262
|
+
catch (directiveError) {
|
|
263
|
+
highestScoreTask.status = 'pending';
|
|
264
|
+
delete highestScoreTask.started_at;
|
|
265
|
+
delete highestScoreTask.task;
|
|
266
|
+
delete highestScoreTask.assigned_session_key;
|
|
267
|
+
try {
|
|
268
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
269
|
+
}
|
|
270
|
+
catch (rollbackError) {
|
|
271
|
+
throw new Error(`[PD:EvolutionWorker] Failed to persist directive and failed to roll back queue: ${String(directiveError)}; rollback=${String(rollbackError)}`);
|
|
272
|
+
}
|
|
273
|
+
throw directiveError;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const hasInProgressTask = queue.some((task) => task.status === 'in_progress');
|
|
278
|
+
if (!hasInProgressTask && fs.existsSync(directivePath)) {
|
|
279
|
+
const clearedAt = new Date().toISOString();
|
|
280
|
+
fs.writeFileSync(directivePath, JSON.stringify({
|
|
281
|
+
active: false,
|
|
282
|
+
task: null,
|
|
283
|
+
taskId: null,
|
|
284
|
+
timestamp: clearedAt,
|
|
285
|
+
clearedAt,
|
|
286
|
+
}, null, 2), 'utf8');
|
|
287
|
+
}
|
|
282
288
|
}
|
|
283
289
|
if (queueChanged) {
|
|
284
290
|
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
@@ -343,7 +349,7 @@ async function processDetectionQueue(wctx, api, eventLog) {
|
|
|
343
349
|
if (logger)
|
|
344
350
|
logger.debug?.(`[PD:EvolutionWorker] L3 Semantic search failed: ${String(e)}`);
|
|
345
351
|
}
|
|
346
|
-
trackPainCandidate(text, wctx);
|
|
352
|
+
await trackPainCandidate(text, wctx);
|
|
347
353
|
}
|
|
348
354
|
}
|
|
349
355
|
}
|
|
@@ -352,16 +358,11 @@ async function processDetectionQueue(wctx, api, eventLog) {
|
|
|
352
358
|
logger.warn(`[PD:EvolutionWorker] Detection queue failed: ${String(err)}`);
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
|
-
export function trackPainCandidate(text, wctx) {
|
|
361
|
+
export async function trackPainCandidate(text, wctx) {
|
|
356
362
|
if (!shouldTrackPainCandidate(text))
|
|
357
363
|
return;
|
|
358
364
|
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
359
|
-
const
|
|
360
|
-
const releaseLock = acquireQueueLock(lockPath, console);
|
|
361
|
-
if (!releaseLock) {
|
|
362
|
-
console.warn('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping track');
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
+
const releaseLock = await requireQueueLock(candidatePath, console, 'trackPainCandidate', PAIN_CANDIDATES_LOCK_SUFFIX);
|
|
365
366
|
try {
|
|
366
367
|
let data = { candidates: {} };
|
|
367
368
|
if (fs.existsSync(candidatePath)) {
|
|
@@ -392,16 +393,11 @@ export function trackPainCandidate(text, wctx) {
|
|
|
392
393
|
releaseLock();
|
|
393
394
|
}
|
|
394
395
|
}
|
|
395
|
-
export function processPromotion(wctx, logger, eventLog) {
|
|
396
|
+
export async function processPromotion(wctx, logger, eventLog) {
|
|
396
397
|
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
397
398
|
if (!fs.existsSync(candidatePath))
|
|
398
399
|
return;
|
|
399
|
-
const
|
|
400
|
-
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
401
|
-
if (!releaseLock) {
|
|
402
|
-
logger?.warn?.('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping promotion');
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
400
|
+
const releaseLock = await requireQueueLock(candidatePath, logger, 'processPromotion', PAIN_CANDIDATES_LOCK_SUFFIX);
|
|
405
401
|
try {
|
|
406
402
|
const config = wctx.config;
|
|
407
403
|
const dictionary = wctx.dictionary;
|
|
@@ -453,6 +449,36 @@ export function processPromotion(wctx, logger, eventLog) {
|
|
|
453
449
|
releaseLock();
|
|
454
450
|
}
|
|
455
451
|
}
|
|
452
|
+
export async function registerEvolutionTaskSession(workspaceResolve, taskId, sessionKey, logger) {
|
|
453
|
+
const queuePath = workspaceResolve('EVOLUTION_QUEUE');
|
|
454
|
+
if (!fs.existsSync(queuePath))
|
|
455
|
+
return false;
|
|
456
|
+
const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
|
|
457
|
+
try {
|
|
458
|
+
let queue;
|
|
459
|
+
try {
|
|
460
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
461
|
+
}
|
|
462
|
+
catch (parseErr) {
|
|
463
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to parse EVOLUTION_QUEUE for session registration: ${queuePath} - ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
const task = queue.find((item) => item.id === taskId && item.status === 'in_progress');
|
|
467
|
+
if (!task) {
|
|
468
|
+
logger?.warn?.(`[PD:EvolutionWorker] Could not find in-progress evolution task ${taskId} for session assignment`);
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
task.assigned_session_key = sessionKey;
|
|
472
|
+
if (!task.started_at) {
|
|
473
|
+
task.started_at = new Date().toISOString();
|
|
474
|
+
}
|
|
475
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
releaseLock();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
456
482
|
export const EvolutionWorkerService = {
|
|
457
483
|
id: 'principles-evolution-worker',
|
|
458
484
|
api: null,
|
|
@@ -476,28 +502,32 @@ export const EvolutionWorkerService = {
|
|
|
476
502
|
const initialDelay = 5000;
|
|
477
503
|
const interval = config.get('intervals.worker_poll_ms') || (15 * 60 * 1000);
|
|
478
504
|
intervalId = setInterval(() => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
505
|
+
void (async () => {
|
|
506
|
+
await checkPainFlag(wctx, logger);
|
|
507
|
+
await processEvolutionQueue(wctx, logger, eventLog);
|
|
508
|
+
if (api) {
|
|
509
|
+
await processDetectionQueue(wctx, api, eventLog);
|
|
510
|
+
}
|
|
511
|
+
await processPromotion(wctx, logger, eventLog);
|
|
512
|
+
wctx.dictionary.flush();
|
|
513
|
+
flushAllSessions();
|
|
514
|
+
})().catch((err) => {
|
|
515
|
+
if (logger)
|
|
516
|
+
logger.error(`[PD:EvolutionWorker] Error in worker interval: ${String(err)}`);
|
|
517
|
+
});
|
|
490
518
|
}, interval);
|
|
491
519
|
setTimeout(() => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
520
|
+
void (async () => {
|
|
521
|
+
await checkPainFlag(wctx, logger);
|
|
522
|
+
await processEvolutionQueue(wctx, logger, eventLog);
|
|
523
|
+
if (api) {
|
|
524
|
+
await processDetectionQueue(wctx, api, eventLog);
|
|
525
|
+
}
|
|
526
|
+
await processPromotion(wctx, logger, eventLog);
|
|
527
|
+
})().catch((err) => {
|
|
528
|
+
if (logger)
|
|
529
|
+
logger.error(`[PD:EvolutionWorker] Startup worker cycle failed: ${String(err)}`);
|
|
530
|
+
});
|
|
501
531
|
}, initialDelay);
|
|
502
532
|
},
|
|
503
533
|
stop(ctx) {
|
|
@@ -73,6 +73,10 @@ export declare class RuntimeSummaryService {
|
|
|
73
73
|
private static buildGfiSources;
|
|
74
74
|
private static findLastPainSignal;
|
|
75
75
|
private static buildGateStats;
|
|
76
|
+
private static resolveSessionSortTime;
|
|
77
|
+
private static mergeEvents;
|
|
78
|
+
private static getEventDedupKey;
|
|
79
|
+
private static resolveEvolutionDataQuality;
|
|
76
80
|
private static readJsonFile;
|
|
77
81
|
private static asFiniteNumber;
|
|
78
82
|
}
|
|
@@ -27,7 +27,7 @@ export class RuntimeSummaryService {
|
|
|
27
27
|
const bufferedEvents = hasBufferedEventAccess
|
|
28
28
|
? wctx.eventLog.getBufferedEvents()
|
|
29
29
|
: [];
|
|
30
|
-
const events =
|
|
30
|
+
const events = this.mergeEvents(persistedEvents, bufferedEvents);
|
|
31
31
|
const dailyStats = this.readJsonFile(path.join(wctx.stateDir, 'logs', 'daily-stats.json'), warnings, false);
|
|
32
32
|
const today = generatedAt.slice(0, 10);
|
|
33
33
|
const dailyGfiPeak = dailyStats?.[today]?.gfi?.peak;
|
|
@@ -69,7 +69,7 @@ export class RuntimeSummaryService {
|
|
|
69
69
|
evolution: {
|
|
70
70
|
queue: queueStats,
|
|
71
71
|
directive: directiveSummary,
|
|
72
|
-
dataQuality: queue
|
|
72
|
+
dataQuality: this.resolveEvolutionDataQuality(queue, queueStats, directiveSummary),
|
|
73
73
|
},
|
|
74
74
|
pain: {
|
|
75
75
|
activeFlag: Object.keys(painFlag).length > 0,
|
|
@@ -109,7 +109,7 @@ export class RuntimeSummaryService {
|
|
|
109
109
|
pushWarning(warnings, `Failed to parse session snapshot: ${file}`);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
return sessions.sort((a, b) => (b
|
|
112
|
+
return sessions.sort((a, b) => this.resolveSessionSortTime(b) - this.resolveSessionSortTime(a));
|
|
113
113
|
}
|
|
114
114
|
static selectSession(sessions, explicitSessionId) {
|
|
115
115
|
if (explicitSessionId) {
|
|
@@ -133,9 +133,12 @@ export class RuntimeSummaryService {
|
|
|
133
133
|
currentGfi: Number.isFinite(live.currentGfi) ? Number(live.currentGfi) : persisted?.currentGfi,
|
|
134
134
|
dailyGfiPeak: Number.isFinite(live.dailyGfiPeak) ? Number(live.dailyGfiPeak) : persisted?.dailyGfiPeak,
|
|
135
135
|
lastActivityAt: Number.isFinite(live.lastActivityAt) ? Number(live.lastActivityAt) : persisted?.lastActivityAt,
|
|
136
|
+
lastControlActivityAt: Number.isFinite(live.lastControlActivityAt)
|
|
137
|
+
? Number(live.lastControlActivityAt)
|
|
138
|
+
: persisted?.lastControlActivityAt,
|
|
136
139
|
});
|
|
137
140
|
}
|
|
138
|
-
return [...merged.values()].sort((a, b) => (b
|
|
141
|
+
return [...merged.values()].sort((a, b) => this.resolveSessionSortTime(b) - this.resolveSessionSortTime(a));
|
|
139
142
|
}
|
|
140
143
|
static buildQueueStats(queue) {
|
|
141
144
|
const stats = { pending: 0, inProgress: 0, completed: 0 };
|
|
@@ -298,6 +301,42 @@ export class RuntimeSummaryService {
|
|
|
298
301
|
dataQuality: scoped.length > 0 ? 'authoritative' : 'partial',
|
|
299
302
|
};
|
|
300
303
|
}
|
|
304
|
+
static resolveSessionSortTime(session) {
|
|
305
|
+
return session.lastControlActivityAt ?? session.lastActivityAt ?? 0;
|
|
306
|
+
}
|
|
307
|
+
static mergeEvents(persistedEvents, bufferedEvents) {
|
|
308
|
+
const merged = new Map();
|
|
309
|
+
for (const entry of [...persistedEvents, ...bufferedEvents]) {
|
|
310
|
+
merged.set(this.getEventDedupKey(entry), entry);
|
|
311
|
+
}
|
|
312
|
+
return [...merged.values()].sort((a, b) => (a.ts || '').localeCompare(b.ts || ''));
|
|
313
|
+
}
|
|
314
|
+
static getEventDedupKey(entry) {
|
|
315
|
+
const eventId = typeof entry.data?.eventId === 'string' ? entry.data.eventId : null;
|
|
316
|
+
if (eventId) {
|
|
317
|
+
return `${entry.type}:${entry.sessionId ?? 'none'}:${eventId}`;
|
|
318
|
+
}
|
|
319
|
+
return [
|
|
320
|
+
entry.ts ?? 'no-ts',
|
|
321
|
+
entry.type ?? 'no-type',
|
|
322
|
+
entry.category ?? 'no-category',
|
|
323
|
+
entry.sessionId ?? 'no-session',
|
|
324
|
+
typeof entry.data?.source === 'string' ? entry.data.source : 'no-source',
|
|
325
|
+
typeof entry.data?.toolName === 'string' ? entry.data.toolName : 'no-tool',
|
|
326
|
+
typeof entry.data?.reason === 'string' ? entry.data.reason : 'no-reason',
|
|
327
|
+
].join('::');
|
|
328
|
+
}
|
|
329
|
+
static resolveEvolutionDataQuality(queue, queueStats, directive) {
|
|
330
|
+
if (!queue)
|
|
331
|
+
return 'partial';
|
|
332
|
+
if (queueStats.inProgress > 0 && (!directive.exists || directive.active !== true)) {
|
|
333
|
+
return 'partial';
|
|
334
|
+
}
|
|
335
|
+
if (directive.active && queueStats.inProgress === 0 && queueStats.pending === 0) {
|
|
336
|
+
return 'partial';
|
|
337
|
+
}
|
|
338
|
+
return 'authoritative';
|
|
339
|
+
}
|
|
301
340
|
static readJsonFile(filePath, warnings, warnOnMissing) {
|
|
302
341
|
if (!fs.existsSync(filePath)) {
|
|
303
342
|
if (warnOnMissing) {
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* Uses the low-level OpenClaw Subagent API.
|
|
6
6
|
*/
|
|
7
7
|
import { Type } from '@sinclair/typebox';
|
|
8
|
+
import * as fs from 'fs';
|
|
8
9
|
import { randomUUID } from 'node:crypto';
|
|
9
10
|
import { loadAgentDefinition, listAvailableAgents } from '../core/agent-loader.js';
|
|
11
|
+
import { resolvePdPath } from '../core/paths.js';
|
|
12
|
+
import { extractEvolutionTaskId, registerEvolutionTaskSession } from '../service/evolution-worker.js';
|
|
10
13
|
/**
|
|
11
14
|
* Extract assistant text from session messages
|
|
12
15
|
*/
|
|
@@ -40,6 +43,23 @@ function extractAssistantText(messages) {
|
|
|
40
43
|
}
|
|
41
44
|
return '';
|
|
42
45
|
}
|
|
46
|
+
async function registerDiagnosticianRun(api, task, sessionKey) {
|
|
47
|
+
const taskId = extractEvolutionTaskId(task);
|
|
48
|
+
if (!taskId)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
const workspaceDir = api.resolvePath('.');
|
|
52
|
+
const queuePath = resolvePdPath(workspaceDir, 'EVOLUTION_QUEUE');
|
|
53
|
+
if (!fs.existsSync(queuePath)) {
|
|
54
|
+
api.logger?.warn?.(`[PD:AgentSpawn] Evolution task ${taskId} not registered because queue file is missing`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await registerEvolutionTaskSession((key) => resolvePdPath(workspaceDir, key), taskId, sessionKey, api.logger);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
api.logger?.warn?.(`[PD:AgentSpawn] Failed to register evolution task session: ${String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
/**
|
|
44
64
|
* Build the full system prompt for a subagent
|
|
45
65
|
* Combines the agent definition with any context-specific additions
|
|
@@ -285,6 +305,9 @@ pd_run_worker(
|
|
|
285
305
|
deliver: false, // Critical: don't send directly to external channels
|
|
286
306
|
idempotencyKey: randomUUID(),
|
|
287
307
|
});
|
|
308
|
+
if (agentType === 'diagnostician') {
|
|
309
|
+
await registerDiagnosticianRun(api, task, sessionKey);
|
|
310
|
+
}
|
|
288
311
|
if (runAsync) {
|
|
289
312
|
const duration = Date.now() - startTime;
|
|
290
313
|
return {
|
|
@@ -28,6 +28,11 @@ export interface LockContext {
|
|
|
28
28
|
/** 获取锁的时间 */
|
|
29
29
|
acquiredAt: number;
|
|
30
30
|
}
|
|
31
|
+
export declare class LockAcquisitionError extends Error {
|
|
32
|
+
readonly filePath: string;
|
|
33
|
+
readonly lockPath: string;
|
|
34
|
+
constructor(message: string, filePath: string, lockPath: string);
|
|
35
|
+
}
|
|
31
36
|
/**
|
|
32
37
|
* 获取文件锁
|
|
33
38
|
*
|
|
@@ -37,6 +42,7 @@ export interface LockContext {
|
|
|
37
42
|
* @throws Error 如果无法获取锁
|
|
38
43
|
*/
|
|
39
44
|
export declare function acquireLock(filePath: string, options?: LockOptions): LockContext;
|
|
45
|
+
export declare function acquireLockAsync(filePath: string, options?: LockOptions): Promise<LockContext>;
|
|
40
46
|
/**
|
|
41
47
|
* 释放文件锁
|
|
42
48
|
*
|
|
@@ -52,6 +58,7 @@ export declare function releaseLock(ctx: LockContext): void;
|
|
|
52
58
|
* @returns 操作的返回值
|
|
53
59
|
*/
|
|
54
60
|
export declare function withLock<T>(filePath: string, fn: () => T, options?: LockOptions): T;
|
|
61
|
+
export declare function withLockAsync<T>(filePath: string, fn: () => Promise<T>, options?: LockOptions): Promise<T>;
|
|
55
62
|
export declare function withAsyncLock<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
|
|
56
63
|
/**
|
|
57
64
|
* 检查锁状态(用于调试)
|