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.
@@ -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
+ ]);
@@ -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
  }
@@ -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
- // Check event buffer first
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
- // Search for the event in buffer and file
390
+ const allEvents = this.getMergedEvents();
397
391
  let foundEvent = null;
398
- // Check buffer first
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
- // Check buffer in reverse
455
- for (let i = this.eventBuffer.length - 1; i >= 0; i--) {
456
- const entry = this.eventBuffer[i];
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
- const LOCK_SUFFIX = '.lock';
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
- fs.writeFileSync(tempPath, JSON.stringify(serializable, null, 2), 'utf8');
448
- // fsync 确保数据落盘
449
- const fd = fs.openSync(tempPath, 'r+');
450
- fs.fsyncSync(fd);
451
- fs.closeSync(fd);
452
- // 原子重命名
453
- fs.renameSync(tempPath, this.storagePath);
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
- fs.writeFileSync(tempPath, JSON.stringify(serializable, null, 2), 'utf8');
510
- const fd = fs.openSync(tempPath, 'r');
511
- fs.fsyncSync(fd);
512
- fs.closeSync(fd);
513
- fs.renameSync(tempPath, this.storagePath);
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);
@@ -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: 'EVOLUTION_QUEUE' },
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
  ];
@@ -13,6 +13,7 @@ export interface SessionState {
13
13
  llmTurns: number;
14
14
  blockedAttempts: number;
15
15
  lastActivityAt: number;
16
+ lastControlActivityAt: number;
16
17
  totalInputTokens: number;
17
18
  totalOutputTokens: number;
18
19
  cacheHits: number;