principles-disciple 1.7.1 → 1.7.3
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 +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const READ_ONLY_TOOL_NAMES: readonly ["read", "read_file", "read_many_files", "image_read", "search_file_content", "grep", "grep_search", "list_directory", "ls", "glob", "lsp_hover", "lsp_goto_definition", "lsp_find_references", "web_fetch", "web_search", "ref_search_documentation", "ref_read_url", "resolve-library-id", "get-library-docs", "memory_recall", "save_memory", "todo_read", "todo_write", "ask_user", "ask_user_question", "deep_reflect", "pd-status", "trust", "report"];
|
|
2
|
+
export declare const LOW_RISK_WRITE_TOOL_NAMES: readonly ["write", "write_file", "edit", "edit_file", "replace", "apply_patch", "insert", "patch"];
|
|
3
|
+
export declare const BASH_TOOL_NAMES: readonly ["bash", "run_shell_command", "exec", "execute", "shell", "cmd"];
|
|
4
|
+
export declare const HIGH_RISK_TOOL_NAMES: readonly ["delete_file", "move_file", "bash", "run_shell_command", "exec", "execute", "shell", "cmd"];
|
|
5
|
+
export declare const AGENT_TOOL_NAMES: readonly ["pd_run_worker", "sessions_spawn"];
|
|
6
|
+
export declare const CONTENT_LIMITED_TOOL_NAMES: readonly ["write", "write_file", "edit", "edit_file", "replace", "apply_patch", "insert", "patch"];
|
|
7
|
+
export declare const CONSTRUCTIVE_TOOL_NAMES: readonly ["write", "write_file", "edit", "edit_file", "replace", "apply_patch", "insert", "patch", "delete_file", "move_file", "bash", "run_shell_command", "exec", "execute", "shell", "cmd", "pd_run_worker", "sessions_spawn", "evolve-task", "init-strategy"];
|
|
8
|
+
export declare const EXPLORATORY_TOOL_NAMES: readonly ["read", "read_file", "read_many_files", "image_read", "search_file_content", "grep", "grep_search", "list_directory", "ls", "glob", "lsp_hover", "lsp_goto_definition", "lsp_find_references", "web_fetch", "web_search", "ref_search_documentation", "ref_read_url", "resolve-library-id", "get-library-docs", "memory_recall", "save_memory", "todo_read", "todo_write", "ask_user", "ask_user_question", "deep_reflect", "pd-status", "trust", "report"];
|
|
9
|
+
export declare const READ_ONLY_TOOLS: Set<string>;
|
|
10
|
+
export declare const LOW_RISK_WRITE_TOOLS: Set<string>;
|
|
11
|
+
export declare const HIGH_RISK_TOOLS: Set<string>;
|
|
12
|
+
export declare const BASH_TOOLS_SET: Set<string>;
|
|
13
|
+
export declare const AGENT_TOOLS: Set<string>;
|
|
14
|
+
export declare const CONTENT_LIMITED_TOOLS: Set<string>;
|
|
15
|
+
export declare const CONSTRUCTIVE_TOOLS: Set<string>;
|
|
16
|
+
export declare const EXPLORATORY_TOOLS: Set<string>;
|
|
17
|
+
export declare const WRITE_TOOLS: Set<string>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const READ_ONLY_TOOL_NAMES = [
|
|
2
|
+
'read', 'read_file', 'read_many_files', 'image_read',
|
|
3
|
+
'search_file_content', 'grep', 'grep_search', 'list_directory', 'ls', 'glob',
|
|
4
|
+
'lsp_hover', 'lsp_goto_definition', 'lsp_find_references',
|
|
5
|
+
'web_fetch', 'web_search', 'ref_search_documentation', 'ref_read_url',
|
|
6
|
+
'resolve-library-id', 'get-library-docs',
|
|
7
|
+
'memory_recall', 'save_memory', 'todo_read', 'todo_write',
|
|
8
|
+
'ask_user', 'ask_user_question',
|
|
9
|
+
'deep_reflect',
|
|
10
|
+
'pd-status', 'trust', 'report',
|
|
11
|
+
];
|
|
12
|
+
export const LOW_RISK_WRITE_TOOL_NAMES = [
|
|
13
|
+
'write', 'write_file',
|
|
14
|
+
'edit', 'edit_file', 'replace', 'apply_patch', 'insert', 'patch',
|
|
15
|
+
];
|
|
16
|
+
// BASH aliases must be defined before HIGH_RISK_TOOL_NAMES
|
|
17
|
+
export const BASH_TOOL_NAMES = [
|
|
18
|
+
'bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd',
|
|
19
|
+
];
|
|
20
|
+
export const HIGH_RISK_TOOL_NAMES = [
|
|
21
|
+
'delete_file', 'move_file',
|
|
22
|
+
// Include all BASH aliases for consistent high-risk classification
|
|
23
|
+
...BASH_TOOL_NAMES,
|
|
24
|
+
];
|
|
25
|
+
export const AGENT_TOOL_NAMES = [
|
|
26
|
+
'pd_run_worker', 'sessions_spawn',
|
|
27
|
+
];
|
|
28
|
+
export const CONTENT_LIMITED_TOOL_NAMES = [
|
|
29
|
+
...LOW_RISK_WRITE_TOOL_NAMES,
|
|
30
|
+
];
|
|
31
|
+
export const CONSTRUCTIVE_TOOL_NAMES = [
|
|
32
|
+
...LOW_RISK_WRITE_TOOL_NAMES,
|
|
33
|
+
'delete_file', 'move_file',
|
|
34
|
+
// Include all BASH aliases for consistent constructive classification
|
|
35
|
+
...BASH_TOOL_NAMES,
|
|
36
|
+
...AGENT_TOOL_NAMES,
|
|
37
|
+
'evolve-task', 'init-strategy',
|
|
38
|
+
];
|
|
39
|
+
export const EXPLORATORY_TOOL_NAMES = [
|
|
40
|
+
...READ_ONLY_TOOL_NAMES,
|
|
41
|
+
];
|
|
42
|
+
export const READ_ONLY_TOOLS = new Set(READ_ONLY_TOOL_NAMES);
|
|
43
|
+
export const LOW_RISK_WRITE_TOOLS = new Set(LOW_RISK_WRITE_TOOL_NAMES);
|
|
44
|
+
export const HIGH_RISK_TOOLS = new Set(HIGH_RISK_TOOL_NAMES);
|
|
45
|
+
export const BASH_TOOLS_SET = new Set(BASH_TOOL_NAMES);
|
|
46
|
+
export const AGENT_TOOLS = new Set(AGENT_TOOL_NAMES);
|
|
47
|
+
export const CONTENT_LIMITED_TOOLS = new Set(CONTENT_LIMITED_TOOL_NAMES);
|
|
48
|
+
export const CONSTRUCTIVE_TOOLS = new Set(CONSTRUCTIVE_TOOL_NAMES);
|
|
49
|
+
export const EXPLORATORY_TOOLS = new Set(EXPLORATORY_TOOL_NAMES);
|
|
50
|
+
export const WRITE_TOOLS = new Set([
|
|
51
|
+
...LOW_RISK_WRITE_TOOL_NAMES,
|
|
52
|
+
'delete_file',
|
|
53
|
+
'move_file',
|
|
54
|
+
]);
|
package/dist/core/event-log.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export declare class EventLog {
|
|
|
38
38
|
* Intended for live runtime summaries that should not lag behind disk snapshots.
|
|
39
39
|
*/
|
|
40
40
|
getBufferedEvents(): EventLogEntry[];
|
|
41
|
+
private getEventDedupKey;
|
|
42
|
+
private readPersistedEvents;
|
|
43
|
+
private getMergedEvents;
|
|
41
44
|
private flushEvents;
|
|
42
45
|
private flushStats;
|
|
43
46
|
/**
|
|
@@ -76,4 +79,5 @@ export declare class EventLogService {
|
|
|
76
79
|
private static instances;
|
|
77
80
|
static get(stateDir: string, logger?: PluginLogger): EventLog;
|
|
78
81
|
static flushAll(): void;
|
|
82
|
+
static disposeAll(): void;
|
|
79
83
|
}
|
package/dist/core/event-log.js
CHANGED
|
@@ -176,6 +176,56 @@ export class EventLog {
|
|
|
176
176
|
getBufferedEvents() {
|
|
177
177
|
return this.eventBuffer.map((entry) => ({ ...entry, data: { ...entry.data } }));
|
|
178
178
|
}
|
|
179
|
+
getEventDedupKey(entry) {
|
|
180
|
+
const eventId = typeof entry.data?.eventId === 'string'
|
|
181
|
+
? String(entry.data.eventId)
|
|
182
|
+
: null;
|
|
183
|
+
if (eventId) {
|
|
184
|
+
return `${entry.type}:${entry.sessionId ?? 'none'}:${eventId}`;
|
|
185
|
+
}
|
|
186
|
+
const data = entry.data ?? {};
|
|
187
|
+
return [
|
|
188
|
+
entry.ts ?? 'no-ts',
|
|
189
|
+
entry.type ?? 'no-type',
|
|
190
|
+
entry.category ?? 'no-category',
|
|
191
|
+
entry.sessionId ?? 'no-session',
|
|
192
|
+
typeof data.source === 'string' ? data.source : 'no-source',
|
|
193
|
+
typeof data.toolName === 'string' ? data.toolName : 'no-tool',
|
|
194
|
+
typeof data.reason === 'string' ? data.reason : 'no-reason',
|
|
195
|
+
].join('::');
|
|
196
|
+
}
|
|
197
|
+
readPersistedEvents() {
|
|
198
|
+
if (!fs.existsSync(this.eventsFile))
|
|
199
|
+
return [];
|
|
200
|
+
try {
|
|
201
|
+
const content = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
202
|
+
return content
|
|
203
|
+
.trim()
|
|
204
|
+
.split('\n')
|
|
205
|
+
.filter((line) => line.trim().length > 0)
|
|
206
|
+
.map((line) => {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(line);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
.filter((entry) => entry !== null);
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
if (this.logger)
|
|
218
|
+
this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
getMergedEvents() {
|
|
223
|
+
const merged = new Map();
|
|
224
|
+
for (const entry of [...this.readPersistedEvents(), ...this.getBufferedEvents()]) {
|
|
225
|
+
merged.set(this.getEventDedupKey(entry), entry);
|
|
226
|
+
}
|
|
227
|
+
return [...merged.values()].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
228
|
+
}
|
|
179
229
|
flushEvents() {
|
|
180
230
|
if (this.eventBuffer.length === 0)
|
|
181
231
|
return;
|
|
@@ -297,8 +347,7 @@ export class EventLog {
|
|
|
297
347
|
* Aggregate empathy stats for a specific session.
|
|
298
348
|
*/
|
|
299
349
|
aggregateSessionEmpathy(sessionId, result) {
|
|
300
|
-
|
|
301
|
-
for (const entry of this.eventBuffer) {
|
|
350
|
+
for (const entry of this.getMergedEvents()) {
|
|
302
351
|
if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
|
|
303
352
|
const data = entry.data;
|
|
304
353
|
if (data.source === 'user_empathy') {
|
|
@@ -332,71 +381,15 @@ export class EventLog {
|
|
|
332
381
|
result.rolledBackScore += data.originalScore || 0;
|
|
333
382
|
}
|
|
334
383
|
}
|
|
335
|
-
// Also check events file for persisted events
|
|
336
|
-
if (fs.existsSync(this.eventsFile)) {
|
|
337
|
-
try {
|
|
338
|
-
const content = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
339
|
-
const lines = content.trim().split('\n');
|
|
340
|
-
for (const line of lines) {
|
|
341
|
-
if (!line.trim())
|
|
342
|
-
continue;
|
|
343
|
-
try {
|
|
344
|
-
const entry = JSON.parse(line);
|
|
345
|
-
if (entry.sessionId === sessionId) {
|
|
346
|
-
if (entry.type === 'pain_signal') {
|
|
347
|
-
const data = entry.data;
|
|
348
|
-
if (data.source === 'user_empathy') {
|
|
349
|
-
if (data.deduped) {
|
|
350
|
-
result.dedupedCount++;
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
result.totalEvents++;
|
|
354
|
-
result.totalPenaltyScore += data.score || 0;
|
|
355
|
-
if (data.severity) {
|
|
356
|
-
result.bySeverity[data.severity]++;
|
|
357
|
-
result.scoreBySeverity[data.severity] += data.score || 0;
|
|
358
|
-
}
|
|
359
|
-
if (data.detection_mode)
|
|
360
|
-
result.byDetectionMode[data.detection_mode]++;
|
|
361
|
-
if (data.origin)
|
|
362
|
-
result.byOrigin[data.origin]++;
|
|
363
|
-
const conf = data.confidence ?? 1;
|
|
364
|
-
if (conf >= 0.8)
|
|
365
|
-
result.confidenceDistribution.high++;
|
|
366
|
-
else if (conf >= 0.5)
|
|
367
|
-
result.confidenceDistribution.medium++;
|
|
368
|
-
else
|
|
369
|
-
result.confidenceDistribution.low++;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
else if (entry.type === 'empathy_rollback') {
|
|
374
|
-
const data = entry.data;
|
|
375
|
-
result.rollbackCount++;
|
|
376
|
-
result.rolledBackScore += data.originalScore || 0;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
// Skip malformed lines
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch (e) {
|
|
386
|
-
if (this.logger)
|
|
387
|
-
this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
384
|
}
|
|
391
385
|
/**
|
|
392
386
|
* Rollback an empathy event by ID.
|
|
393
387
|
* Returns the rolled back score, or 0 if event not found.
|
|
394
388
|
*/
|
|
395
389
|
rollbackEmpathyEvent(eventId, sessionId, reason, triggeredBy) {
|
|
396
|
-
|
|
390
|
+
const allEvents = this.getMergedEvents();
|
|
397
391
|
let foundEvent = null;
|
|
398
|
-
|
|
399
|
-
for (const entry of this.eventBuffer) {
|
|
392
|
+
for (const entry of allEvents) {
|
|
400
393
|
if (entry.type === 'pain_signal') {
|
|
401
394
|
const data = entry.data;
|
|
402
395
|
if (entry.data.eventId === eventId && data.source === 'user_empathy') {
|
|
@@ -405,34 +398,6 @@ export class EventLog {
|
|
|
405
398
|
}
|
|
406
399
|
}
|
|
407
400
|
}
|
|
408
|
-
// If not in buffer, check file
|
|
409
|
-
if (!foundEvent && fs.existsSync(this.eventsFile)) {
|
|
410
|
-
try {
|
|
411
|
-
const content = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
412
|
-
const lines = content.trim().split('\n');
|
|
413
|
-
for (const line of lines) {
|
|
414
|
-
if (!line.trim())
|
|
415
|
-
continue;
|
|
416
|
-
try {
|
|
417
|
-
const entry = JSON.parse(line);
|
|
418
|
-
if (entry.type === 'pain_signal') {
|
|
419
|
-
const data = entry.data;
|
|
420
|
-
if (entry.data.eventId === eventId && data.source === 'user_empathy') {
|
|
421
|
-
foundEvent = { entry, data };
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
// Skip malformed lines
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
catch (e) {
|
|
432
|
-
if (this.logger)
|
|
433
|
-
this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
401
|
if (!foundEvent || foundEvent.data.deduped) {
|
|
437
402
|
return 0;
|
|
438
403
|
}
|
|
@@ -451,9 +416,9 @@ export class EventLog {
|
|
|
451
416
|
* Get the last empathy event ID for a session (for rollback).
|
|
452
417
|
*/
|
|
453
418
|
getLastEmpathyEventId(sessionId) {
|
|
454
|
-
|
|
455
|
-
for (let i =
|
|
456
|
-
const entry =
|
|
419
|
+
const allEvents = this.getMergedEvents();
|
|
420
|
+
for (let i = allEvents.length - 1; i >= 0; i--) {
|
|
421
|
+
const entry = allEvents[i];
|
|
457
422
|
if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
|
|
458
423
|
const data = entry.data;
|
|
459
424
|
if (data.source === 'user_empathy' && !data.deduped) {
|
|
@@ -461,33 +426,6 @@ export class EventLog {
|
|
|
461
426
|
}
|
|
462
427
|
}
|
|
463
428
|
}
|
|
464
|
-
// Check file
|
|
465
|
-
if (fs.existsSync(this.eventsFile)) {
|
|
466
|
-
try {
|
|
467
|
-
const content = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
468
|
-
const lines = content.trim().split('\n');
|
|
469
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
470
|
-
if (!lines[i].trim())
|
|
471
|
-
continue;
|
|
472
|
-
try {
|
|
473
|
-
const entry = JSON.parse(lines[i]);
|
|
474
|
-
if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
|
|
475
|
-
const data = entry.data;
|
|
476
|
-
if (data.source === 'user_empathy' && !data.deduped) {
|
|
477
|
-
return entry.data.eventId || null;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
// Skip malformed lines
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
catch (e) {
|
|
487
|
-
if (this.logger)
|
|
488
|
-
this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
429
|
return null;
|
|
492
430
|
}
|
|
493
431
|
/**
|
|
@@ -519,4 +457,10 @@ export class EventLogService {
|
|
|
519
457
|
instance.flush();
|
|
520
458
|
}
|
|
521
459
|
}
|
|
460
|
+
static disposeAll() {
|
|
461
|
+
for (const instance of this.instances.values()) {
|
|
462
|
+
instance.dispose();
|
|
463
|
+
}
|
|
464
|
+
this.instances.clear();
|
|
465
|
+
}
|
|
522
466
|
}
|
|
@@ -75,10 +75,6 @@ export declare class EvolutionEngine {
|
|
|
75
75
|
private addEvent;
|
|
76
76
|
private loadOrCreateScorecard;
|
|
77
77
|
private createNewScorecard;
|
|
78
|
-
/** 获取文件锁,返回释放函数 */
|
|
79
|
-
private acquireFileLock;
|
|
80
|
-
/** 检查锁文件是否过期(死锁检测) */
|
|
81
|
-
private isLockStale;
|
|
82
78
|
/** 持久化评分卡(含锁保护) */
|
|
83
79
|
private saveScorecard;
|
|
84
80
|
/** Per-instance retry queue (P0 fix: was static, causing cross-instance race) */
|
|
@@ -91,9 +87,12 @@ export declare class EvolutionEngine {
|
|
|
91
87
|
/** 无锁快速保存(用于重试) */
|
|
92
88
|
private saveScorecardImmediate;
|
|
93
89
|
private generateId;
|
|
90
|
+
dispose(): void;
|
|
94
91
|
}
|
|
95
92
|
/** 获取指定 workspace 的引擎实例 */
|
|
96
93
|
export declare function getEvolutionEngine(workspaceDir: string): EvolutionEngine;
|
|
94
|
+
export declare function disposeEvolutionEngine(workspaceDir: string): void;
|
|
95
|
+
export declare function disposeAllEvolutionEngines(): void;
|
|
97
96
|
/** 记录成功(便捷函数) */
|
|
98
97
|
export declare function recordEvolutionSuccess(workspaceDir: string, toolName: string, options?: {
|
|
99
98
|
filePath?: string;
|
|
@@ -13,37 +13,9 @@
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import { resolvePdPath } from './paths.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const LOCK_MAX_RETRIES = 50;
|
|
19
|
-
const LOCK_RETRY_DELAY_MS = 50;
|
|
20
|
-
const LOCK_STALE_MS = 30_000; // 30秒视为死锁
|
|
16
|
+
import { withLock } from '../utils/file-lock.js';
|
|
17
|
+
import { CONSTRUCTIVE_TOOLS, CONTENT_LIMITED_TOOLS, EXPLORATORY_TOOLS, HIGH_RISK_TOOLS, } from '../constants/tools.js';
|
|
21
18
|
import { EvolutionTier, DEFAULT_EVOLUTION_CONFIG, TIER_DEFINITIONS, TASK_DIFFICULTY_CONFIG, getTierDefinition, getTierByPoints, } from './evolution-types.js';
|
|
22
|
-
// ===== 工具分类(复用 Trust Engine 的分类) =====
|
|
23
|
-
const EXPLORATORY_TOOLS = new Set([
|
|
24
|
-
'read', 'read_file', 'read_many_files', 'image_read',
|
|
25
|
-
'search_file_content', 'grep', 'grep_search', 'list_directory', 'ls', 'glob',
|
|
26
|
-
'web_fetch', 'web_search',
|
|
27
|
-
'ask_user', 'ask_user_question',
|
|
28
|
-
'memory_recall', 'save_memory',
|
|
29
|
-
]);
|
|
30
|
-
const CONSTRUCTIVE_TOOLS = new Set([
|
|
31
|
-
'write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch',
|
|
32
|
-
'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
|
|
33
|
-
'pd_run_worker', 'sessions_spawn',
|
|
34
|
-
]);
|
|
35
|
-
// 高风险工具:需要 allowRiskPath 权限
|
|
36
|
-
// 注意:pd_run_worker 和 sessions_spawn 已从高风险中移出,它们由 allowSubagentSpawn 单独控制
|
|
37
|
-
const HIGH_RISK_TOOLS = new Set([
|
|
38
|
-
'run_shell_command', 'delete_file', 'move_file',
|
|
39
|
-
]);
|
|
40
|
-
// 内容行数限制仅适用于这些写操作工具
|
|
41
|
-
const CONTENT_LIMITED_TOOLS = new Set([
|
|
42
|
-
'write', 'write_file',
|
|
43
|
-
'edit', 'edit_file',
|
|
44
|
-
'replace', 'apply_patch',
|
|
45
|
-
'insert', 'patch',
|
|
46
|
-
]);
|
|
47
19
|
// ===== 主引擎 =====
|
|
48
20
|
export class EvolutionEngine {
|
|
49
21
|
scorecard;
|
|
@@ -357,77 +329,6 @@ export class EvolutionEngine {
|
|
|
357
329
|
lastUpdated: now,
|
|
358
330
|
};
|
|
359
331
|
}
|
|
360
|
-
// ===== 文件锁与原子写入(P0 修复) =====
|
|
361
|
-
/** 获取文件锁,返回释放函数 */
|
|
362
|
-
acquireFileLock(resourcePath) {
|
|
363
|
-
const lockPath = resourcePath + LOCK_SUFFIX;
|
|
364
|
-
let retries = 0;
|
|
365
|
-
while (retries < LOCK_MAX_RETRIES) {
|
|
366
|
-
try {
|
|
367
|
-
// 'wx' = 写入+排他,文件已存在则抛 EEXIST
|
|
368
|
-
const fd = fs.openSync(lockPath, 'wx');
|
|
369
|
-
fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
|
|
370
|
-
fs.closeSync(fd);
|
|
371
|
-
return () => {
|
|
372
|
-
try {
|
|
373
|
-
fs.unlinkSync(lockPath);
|
|
374
|
-
}
|
|
375
|
-
catch { /* ignore */ }
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
catch (err) {
|
|
379
|
-
if (err.code === 'EEXIST') {
|
|
380
|
-
// 检测死锁:锁文件过旧则强制清理
|
|
381
|
-
if (this.isLockStale(lockPath)) {
|
|
382
|
-
try {
|
|
383
|
-
fs.unlinkSync(lockPath);
|
|
384
|
-
continue; // 立即重试
|
|
385
|
-
}
|
|
386
|
-
catch { /* 抢占失败,继续等待 */ }
|
|
387
|
-
}
|
|
388
|
-
retries++;
|
|
389
|
-
// busy wait(可改为指数退避)
|
|
390
|
-
const start = Date.now();
|
|
391
|
-
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
throw new Error(`[Evolution] Lock acquisition failed after ${LOCK_MAX_RETRIES} retries: ${resourcePath}`);
|
|
398
|
-
}
|
|
399
|
-
/** 检查锁文件是否过期(死锁检测) */
|
|
400
|
-
isLockStale(lockPath) {
|
|
401
|
-
try {
|
|
402
|
-
const stat = fs.statSync(lockPath);
|
|
403
|
-
const mtimeMs = stat.mtimeMs;
|
|
404
|
-
// 读取 PID 检查进程是否存活
|
|
405
|
-
const content = fs.readFileSync(lockPath, 'utf8').trim();
|
|
406
|
-
const parts = content.split('\n');
|
|
407
|
-
const pid = parseInt(parts[0] || '0', 10);
|
|
408
|
-
// Unix: signal 0 仅检查进程存在性,Windows 也支持
|
|
409
|
-
if (pid > 0) {
|
|
410
|
-
try {
|
|
411
|
-
process.kill(pid, 0);
|
|
412
|
-
// 进程存活,检查时间
|
|
413
|
-
return Date.now() - mtimeMs > LOCK_STALE_MS;
|
|
414
|
-
}
|
|
415
|
-
catch (e) {
|
|
416
|
-
if (e.code === 'ESRCH') {
|
|
417
|
-
// 进程不存在,锁已死
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
// 无法确定,按时间判断
|
|
421
|
-
return Date.now() - mtimeMs > LOCK_STALE_MS;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// 无有效 PID,按时间判断
|
|
425
|
-
return Date.now() - mtimeMs > LOCK_STALE_MS;
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
332
|
/** 持久化评分卡(含锁保护) */
|
|
432
333
|
saveScorecard() {
|
|
433
334
|
this.scorecard.lastUpdated = new Date().toISOString();
|
|
@@ -441,27 +342,28 @@ export class EvolutionEngine {
|
|
|
441
342
|
...this.scorecard,
|
|
442
343
|
recentFailureHashes: Array.from(this.scorecard.recentFailureHashes.entries()),
|
|
443
344
|
};
|
|
444
|
-
const release = this.acquireFileLock(this.storagePath);
|
|
445
345
|
const tempPath = `${this.storagePath}.tmp.${Date.now()}.${process.pid}`;
|
|
446
346
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
347
|
+
withLock(this.storagePath, () => {
|
|
348
|
+
fs.writeFileSync(tempPath, JSON.stringify(serializable, null, 2), 'utf8');
|
|
349
|
+
// fsync 确保数据落盘
|
|
350
|
+
const fd = fs.openSync(tempPath, 'r+');
|
|
351
|
+
fs.fsyncSync(fd);
|
|
352
|
+
fs.closeSync(fd);
|
|
353
|
+
// 原子重命名
|
|
354
|
+
fs.renameSync(tempPath, this.storagePath);
|
|
355
|
+
}, {
|
|
356
|
+
lockSuffix: '.lock',
|
|
357
|
+
lockStaleMs: 30_000,
|
|
358
|
+
});
|
|
454
359
|
}
|
|
455
360
|
catch (e) {
|
|
456
361
|
console.error(`[Evolution] Failed to save scorecard: ${String(e)}`);
|
|
362
|
+
this.scheduleRetrySave();
|
|
457
363
|
try {
|
|
458
364
|
fs.unlinkSync(tempPath);
|
|
459
365
|
}
|
|
460
366
|
catch { }
|
|
461
|
-
throw e; // 重新抛出,让调用者知道保存失败
|
|
462
|
-
}
|
|
463
|
-
finally {
|
|
464
|
-
release();
|
|
465
367
|
}
|
|
466
368
|
}
|
|
467
369
|
/** Per-instance retry queue (P0 fix: was static, causing cross-instance race) */
|
|
@@ -506,16 +408,42 @@ export class EvolutionEngine {
|
|
|
506
408
|
recentFailureHashes: Array.from(this.scorecard.recentFailureHashes.entries()),
|
|
507
409
|
};
|
|
508
410
|
const tempPath = `${this.storagePath}.tmp.retry.${Date.now()}.${process.pid}`;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
411
|
+
withLock(this.storagePath, () => {
|
|
412
|
+
try {
|
|
413
|
+
fs.writeFileSync(tempPath, JSON.stringify(serializable, null, 2), 'utf8');
|
|
414
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
415
|
+
fs.fsyncSync(fd);
|
|
416
|
+
fs.closeSync(fd);
|
|
417
|
+
fs.renameSync(tempPath, this.storagePath);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
// Clean up temp file on failure
|
|
421
|
+
try {
|
|
422
|
+
if (fs.existsSync(tempPath)) {
|
|
423
|
+
fs.unlinkSync(tempPath);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Ignore cleanup errors
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}, {
|
|
432
|
+
lockSuffix: '.lock',
|
|
433
|
+
lockStaleMs: 30_000,
|
|
434
|
+
});
|
|
514
435
|
}
|
|
515
436
|
// ===== 工具方法 =====
|
|
516
437
|
generateId() {
|
|
517
438
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
518
439
|
}
|
|
440
|
+
dispose() {
|
|
441
|
+
if (this.retryTimer) {
|
|
442
|
+
clearTimeout(this.retryTimer);
|
|
443
|
+
this.retryTimer = null;
|
|
444
|
+
}
|
|
445
|
+
this.retryQueue = [];
|
|
446
|
+
}
|
|
519
447
|
}
|
|
520
448
|
// ===== 便捷函数 =====
|
|
521
449
|
// 使用 Map 按 workspace 隔离实例,避免多 workspace 场景状态串扰
|
|
@@ -528,6 +456,20 @@ export function getEvolutionEngine(workspaceDir) {
|
|
|
528
456
|
}
|
|
529
457
|
return _instances.get(resolved);
|
|
530
458
|
}
|
|
459
|
+
export function disposeEvolutionEngine(workspaceDir) {
|
|
460
|
+
const resolved = path.resolve(workspaceDir);
|
|
461
|
+
const instance = _instances.get(resolved);
|
|
462
|
+
if (!instance)
|
|
463
|
+
return;
|
|
464
|
+
instance.dispose();
|
|
465
|
+
_instances.delete(resolved);
|
|
466
|
+
}
|
|
467
|
+
export function disposeAllEvolutionEngines() {
|
|
468
|
+
for (const instance of _instances.values()) {
|
|
469
|
+
instance.dispose();
|
|
470
|
+
}
|
|
471
|
+
_instances.clear();
|
|
472
|
+
}
|
|
531
473
|
/** 记录成功(便捷函数) */
|
|
532
474
|
export function recordEvolutionSuccess(workspaceDir, toolName, options) {
|
|
533
475
|
return getEvolutionEngine(workspaceDir).recordSuccess(toolName, options);
|
package/dist/core/migration.js
CHANGED
|
@@ -27,7 +27,7 @@ export function migrateDirectoryStructure(api, workspaceDir) {
|
|
|
27
27
|
{ legacy: path.join(legacyStateDir, 'pain_settings.json'), newKey: 'PAIN_SETTINGS' },
|
|
28
28
|
{ legacy: path.join(legacyStateDir, 'thinking_os_usage.json'), newKey: 'THINKING_OS_USAGE' },
|
|
29
29
|
{ legacy: path.join(legacyStateDir, 'pain_candidates.json'), newKey: 'PAIN_CANDIDATES' },
|
|
30
|
-
{ legacy: path.join(legacyStateDir, 'evolution_directive.json'), newKey: '
|
|
30
|
+
{ legacy: path.join(legacyStateDir, 'evolution_directive.json'), newKey: 'EVOLUTION_DIRECTIVE' },
|
|
31
31
|
{ legacy: path.join(legacyStateDir, 'sessions'), newKey: 'SESSION_DIR' },
|
|
32
32
|
{ legacy: path.join(legacyStateDir, 'logs', 'events.jsonl'), newKey: 'SYSTEM_LOG' }, // Backup plan for logs
|
|
33
33
|
];
|