principles-disciple 1.37.0 → 1.38.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/capabilities.ts +2 -1
- package/src/commands/context.ts +5 -4
- package/src/commands/focus.ts +5 -4
- package/src/commands/nocturnal-train.ts +13 -12
- package/src/commands/pd-reflect.ts +4 -3
- package/src/commands/promote-impl.ts +2 -1
- package/src/commands/rollback-impl.ts +6 -5
- package/src/core/adaptive-thresholds.ts +3 -4
- package/src/core/code-implementation-storage.ts +6 -5
- package/src/core/config.ts +3 -2
- package/src/core/correction-cue-learner.ts +42 -14
- package/src/core/correction-types.ts +1 -1
- package/src/core/diagnostician-task-store.ts +5 -8
- package/src/core/dictionary.ts +3 -2
- package/src/core/empathy-keyword-matcher.ts +5 -4
- package/src/core/event-log.ts +2 -1
- package/src/core/evolution-reducer.ts +4 -3
- package/src/core/focus-history.ts +20 -17
- package/src/core/hygiene/tracker.ts +2 -1
- package/src/core/init.ts +2 -1
- package/src/core/model-deployment-registry.ts +3 -4
- package/src/core/model-training-registry.ts +3 -4
- package/src/core/nocturnal-artifact-lineage.ts +2 -3
- package/src/core/nocturnal-dataset.ts +4 -5
- package/src/core/nocturnal-export.ts +8 -7
- package/src/core/pain.ts +6 -6
- package/src/core/path-resolver.ts +2 -1
- package/src/core/pd-task-reconciler.ts +4 -5
- package/src/core/pd-task-store.ts +3 -4
- package/src/core/principle-tree-ledger.ts +6 -5
- package/src/core/promotion-gate.ts +8 -9
- package/src/core/replay-engine.ts +10 -10
- package/src/core/session-tracker.ts +2 -1
- package/src/core/shadow-observation-registry.ts +3 -4
- package/src/core/training-program.ts +2 -1
- package/src/core/trajectory.ts +8 -7
- package/src/hooks/lifecycle.ts +3 -2
- package/src/hooks/llm.ts +3 -2
- package/src/hooks/prompt.ts +77 -18
- package/src/service/cooldown-strategy.ts +97 -0
- package/src/service/evolution-worker.ts +127 -31
- package/src/service/failure-classifier.ts +79 -0
- package/src/service/keyword-optimization-service.ts +22 -15
- package/src/service/nocturnal-config.ts +149 -7
- package/src/service/nocturnal-runtime.ts +24 -4
- package/src/service/nocturnal-service.ts +1 -8
- package/src/service/startup-reconciler.ts +112 -0
- package/src/service/subagent-workflow/correction-observer-types.ts +2 -2
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/index.ts +0 -14
- package/src/tools/write-pain-flag.ts +2 -3
- package/src/utils/io.ts +43 -1
- package/tests/core/dictionary.test.ts +4 -1
- package/tests/service/cooldown-strategy.test.ts +163 -0
- package/tests/service/failure-classifier.test.ts +171 -0
- package/tests/service/startup-reconciler.test.ts +148 -0
- package/src/service/correction-observer-types.ts +0 -69
- package/src/service/correction-observer-workflow-manager.ts +0 -247
package/src/core/event-log.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
EmpathyRollbackEventData,
|
|
20
20
|
} from '../types/event-types.js';
|
|
21
21
|
import { createEmptyDailyStats } from '../types/event-types.js';
|
|
22
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
22
23
|
import type { PluginLogger } from '../openclaw-sdk.js';
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -389,7 +390,7 @@ export class EventLog {
|
|
|
389
390
|
});
|
|
390
391
|
|
|
391
392
|
try {
|
|
392
|
-
|
|
393
|
+
atomicWriteFileSync(this.statsFile, JSON.stringify(data, null, 2));
|
|
393
394
|
} catch (e) {
|
|
394
395
|
if (this.logger) this.logger.error(`[PD] Failed to flush daily-stats.json: ${String(e)}`);
|
|
395
396
|
}
|
|
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { withLock } from '../utils/file-lock.js';
|
|
5
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
5
6
|
import { PathResolver } from './path-resolver.js';
|
|
6
7
|
import { SystemLogger } from './system-logger.js';
|
|
7
8
|
import { shouldIgnorePainProtocolText } from './dictionary.js';
|
|
@@ -441,7 +442,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
441
442
|
if (!fs.existsSync(parentDir)) {
|
|
442
443
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
443
444
|
}
|
|
444
|
-
|
|
445
|
+
atomicWriteFileSync(this.principlesPath, content);
|
|
445
446
|
SystemLogger.log(this.workspaceDir, 'PRINCIPLES_CREATED', `Created PRINCIPLES.md`);
|
|
446
447
|
}
|
|
447
448
|
|
|
@@ -460,7 +461,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
460
461
|
content = content + separator + formatted;
|
|
461
462
|
}
|
|
462
463
|
|
|
463
|
-
|
|
464
|
+
atomicWriteFileSync(this.principlesPath, content);
|
|
464
465
|
SystemLogger.log(this.workspaceDir, 'PRINCIPLE_SYNCED', `Principle ${principle.id} synced to PRINCIPLES.md`);
|
|
465
466
|
}, { lockStaleMs: 10000 });
|
|
466
467
|
return true;
|
|
@@ -752,7 +753,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
752
753
|
private persistBlacklist(entry: { painId?: string; pattern?: string; reason: string; rolledBackAt: string }): void {
|
|
753
754
|
const list = this.loadBlacklist();
|
|
754
755
|
list.push(entry);
|
|
755
|
-
|
|
756
|
+
atomicWriteFileSync(this.blacklistPath, JSON.stringify(list, null, 2));
|
|
756
757
|
}
|
|
757
758
|
|
|
758
759
|
private loadBlacklist(): { painId?: string; pattern?: string; reason: string; rolledBackAt: string }[] {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
13
14
|
|
|
14
15
|
// ============================================================================
|
|
15
16
|
// 工作记忆数据结构
|
|
@@ -124,7 +125,7 @@ export function backupToHistory(focusPath: string, content: string): string | nu
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
try {
|
|
127
|
-
|
|
128
|
+
atomicWriteFileSync(backupPath, content);
|
|
128
129
|
return backupPath;
|
|
129
130
|
} catch (error) {
|
|
130
131
|
logError(`Failed to write backup file: ${backupPath}`, error);
|
|
@@ -236,7 +237,7 @@ export function compressFocus(focusPath: string, newContent: string): {
|
|
|
236
237
|
.replace(/\*\*更新\*\*:\s*\d{4}-\d{2}-\d{2}/, `**更新**: ${today}`);
|
|
237
238
|
|
|
238
239
|
// 写入新内容
|
|
239
|
-
|
|
240
|
+
atomicWriteFileSync(focusPath, updatedContent);
|
|
240
241
|
|
|
241
242
|
// 清理过期历史
|
|
242
243
|
const historyDir = getHistoryDir(focusPath);
|
|
@@ -430,7 +431,7 @@ export function extractWorkingMemory(
|
|
|
430
431
|
|
|
431
432
|
// 尝试从文本中提取描述
|
|
432
433
|
|
|
433
|
-
|
|
434
|
+
|
|
434
435
|
const description = extractDescription(text, filePath);
|
|
435
436
|
|
|
436
437
|
snapshot.artifacts.push({
|
|
@@ -446,23 +447,23 @@ export function extractWorkingMemory(
|
|
|
446
447
|
|
|
447
448
|
// 从文本中提取文件操作(备用方式)
|
|
448
449
|
|
|
449
|
-
|
|
450
|
+
|
|
450
451
|
extractFileArtifacts(text, snapshot.artifacts, workspaceDir);
|
|
451
452
|
|
|
452
453
|
// 提取问题
|
|
453
454
|
|
|
454
|
-
|
|
455
|
+
|
|
455
456
|
extractProblems(text, snapshot.activeProblems);
|
|
456
457
|
|
|
457
458
|
// 提取下一步
|
|
458
459
|
|
|
459
|
-
|
|
460
|
+
|
|
460
461
|
extractNextActions(text, snapshot.nextActions);
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
// 去重和限制数量
|
|
464
465
|
|
|
465
|
-
|
|
466
|
+
|
|
466
467
|
snapshot.artifacts = deduplicateArtifacts(snapshot.artifacts).slice(-MAX_ARTIFACTS);
|
|
467
468
|
snapshot.activeProblems = snapshot.activeProblems.slice(-MAX_PROBLEMS);
|
|
468
469
|
snapshot.nextActions = snapshot.nextActions.slice(-MAX_NEXT_ACTIONS);
|
|
@@ -483,7 +484,7 @@ function extractFileArtifacts(
|
|
|
483
484
|
const filePathRegex = /(?:file_path|absolute_path)["']?\s*[:=]\s*["']([^"']+\.(ts|js|json|md|yaml|yml|py|sh|mjs|cjs))["']/gi;
|
|
484
485
|
|
|
485
486
|
|
|
486
|
-
|
|
487
|
+
|
|
487
488
|
let match;
|
|
488
489
|
while ((match = filePathRegex.exec(text)) !== null) {
|
|
489
490
|
const [, filePath] = match;
|
|
@@ -514,7 +515,7 @@ function extractFileArtifacts(
|
|
|
514
515
|
|
|
515
516
|
// 尝试提取描述(从附近的文本)
|
|
516
517
|
|
|
517
|
-
|
|
518
|
+
|
|
518
519
|
const description = extractDescription(text, filePath);
|
|
519
520
|
|
|
520
521
|
artifacts.push({
|
|
@@ -548,7 +549,7 @@ function extractFileArtifacts(
|
|
|
548
549
|
}
|
|
549
550
|
|
|
550
551
|
|
|
551
|
-
|
|
552
|
+
|
|
552
553
|
const description = extractDescription(text, filePath);
|
|
553
554
|
|
|
554
555
|
artifacts.push({
|
|
@@ -597,7 +598,7 @@ function extractProblems(
|
|
|
597
598
|
// 问题模式(匹配问题描述)
|
|
598
599
|
const problemPattern = /(?:问题|problem|error|错误|失败|failed)[::]\s*([^\n]{5,100})/gi;
|
|
599
600
|
|
|
600
|
-
|
|
601
|
+
|
|
601
602
|
let match;
|
|
602
603
|
while ((match = problemPattern.exec(text)) !== null) {
|
|
603
604
|
const content = match[1].trim();
|
|
@@ -641,7 +642,7 @@ function extractNextActions(text: string, actions: string[]): void {
|
|
|
641
642
|
|
|
642
643
|
for (const pattern of patterns) {
|
|
643
644
|
|
|
644
|
-
|
|
645
|
+
|
|
645
646
|
let match;
|
|
646
647
|
while ((match = pattern.exec(text)) !== null) {
|
|
647
648
|
const action = match[1].trim();
|
|
@@ -701,7 +702,7 @@ export function parseWorkingMemorySection(content: string): WorkingMemorySnapsho
|
|
|
701
702
|
// | 文件路径 | 操作 | 描述 |
|
|
702
703
|
const tableRegex = /\|\s*`?([^`|\n]+)`?\s*\|\s*(created|modified|deleted)\s*\|\s*([^|\n]*)\s*\|/gi;
|
|
703
704
|
|
|
704
|
-
|
|
705
|
+
|
|
705
706
|
let match;
|
|
706
707
|
while ((match = tableRegex.exec(wmContent)) !== null) {
|
|
707
708
|
snapshot.artifacts.push({
|
|
@@ -741,7 +742,7 @@ export function mergeWorkingMemory(content: string, snapshot: WorkingMemorySnaps
|
|
|
741
742
|
|
|
742
743
|
// 生成 Working Memory 章节
|
|
743
744
|
|
|
744
|
-
|
|
745
|
+
|
|
745
746
|
const wmSection = generateWorkingMemorySection(snapshot);
|
|
746
747
|
|
|
747
748
|
if (wmIndex === -1) {
|
|
@@ -958,7 +959,7 @@ function recordCompressTime(stateDir: string): void {
|
|
|
958
959
|
if (!fs.existsSync(stateDir)) {
|
|
959
960
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
960
961
|
}
|
|
961
|
-
|
|
962
|
+
atomicWriteFileSync(path.join(stateDir, LAST_COMPRESS_FILE), Date.now().toString());
|
|
962
963
|
} catch (error) {
|
|
963
964
|
logError('Failed to record compress time', error);
|
|
964
965
|
}
|
|
@@ -1171,6 +1172,7 @@ export function autoCompressFocus(
|
|
|
1171
1172
|
milestonesArchived: boolean;
|
|
1172
1173
|
backupPath: string | null;
|
|
1173
1174
|
reason: string;
|
|
1175
|
+
newContent?: string;
|
|
1174
1176
|
} {
|
|
1175
1177
|
// 检查文件是否存在
|
|
1176
1178
|
if (!fs.existsSync(focusPath)) {
|
|
@@ -1250,7 +1252,7 @@ export function autoCompressFocus(
|
|
|
1250
1252
|
cleanupHistory(focusPath);
|
|
1251
1253
|
|
|
1252
1254
|
// 8. 写入新内容
|
|
1253
|
-
|
|
1255
|
+
atomicWriteFileSync(focusPath, newContent);
|
|
1254
1256
|
|
|
1255
1257
|
// 9. 记录压缩时间
|
|
1256
1258
|
if (stateDir) {
|
|
@@ -1265,6 +1267,7 @@ export function autoCompressFocus(
|
|
|
1265
1267
|
newLines,
|
|
1266
1268
|
milestonesArchived,
|
|
1267
1269
|
backupPath,
|
|
1270
|
+
newContent,
|
|
1268
1271
|
reason: `Auto-compressed: ${oldLines} → ${newLines} lines`
|
|
1269
1272
|
};
|
|
1270
1273
|
}
|
|
@@ -1393,7 +1396,7 @@ export function recoverFromTemplate(
|
|
|
1393
1396
|
}
|
|
1394
1397
|
|
|
1395
1398
|
// 写入恢复的内容
|
|
1396
|
-
|
|
1399
|
+
atomicWriteFileSync(focusPath, template);
|
|
1397
1400
|
|
|
1398
1401
|
return {
|
|
1399
1402
|
success: true,
|
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import type { HygieneStats, PersistenceAction } from '../../types/hygiene-types.js';
|
|
4
4
|
import { createEmptyHygieneStats } from '../../types/hygiene-types.js';
|
|
5
|
+
import { atomicWriteFileSync } from '../../utils/io.js';
|
|
5
6
|
import type { PluginLogger } from '../../openclaw-sdk.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -79,7 +80,7 @@ export class HygieneTracker {
|
|
|
79
80
|
|
|
80
81
|
try {
|
|
81
82
|
// Use a temporary file for atomic write if possible, or simple write
|
|
82
|
-
|
|
83
|
+
atomicWriteFileSync(this.statsFile, JSON.stringify(allStats, null, 2));
|
|
83
84
|
} catch (e) {
|
|
84
85
|
this.logger?.error(`[PD] Failed to write hygiene-stats.json: ${String(e)}`);
|
|
85
86
|
}
|
package/src/core/init.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
|
|
|
5
5
|
import { PD_DIRS } from './paths.js';
|
|
6
6
|
import { defaultContextConfig } from '../types.js';
|
|
7
7
|
import { loadStore, setPrincipleState, type PrincipleTrainingState } from './principle-training-state.js';
|
|
8
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Default PROFILE.json content
|
|
@@ -102,7 +103,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
|
|
|
102
103
|
if (!fs.existsSync(principlesDir)) {
|
|
103
104
|
fs.mkdirSync(principlesDir, { recursive: true });
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
atomicWriteFileSync(profilePath, JSON.stringify(DEFAULT_PROFILE, null, 2));
|
|
106
107
|
api.logger.info(`[PD] Initialized PROFILE.json with default contextInjection config`);
|
|
107
108
|
}
|
|
108
109
|
} catch (err) {
|
|
@@ -33,6 +33,7 @@ import * as fs from 'fs';
|
|
|
33
33
|
import * as path from 'path';
|
|
34
34
|
import * as crypto from 'crypto';
|
|
35
35
|
import { withLock } from '../utils/file-lock.js';
|
|
36
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
36
37
|
import type { Checkpoint } from './model-training-registry.js';
|
|
37
38
|
import {
|
|
38
39
|
getCheckpoint,
|
|
@@ -241,9 +242,7 @@ function readRegistry(stateDir: string): ModelDeploymentRegistry {
|
|
|
241
242
|
function writeRegistry(stateDir: string, registry: ModelDeploymentRegistry): void {
|
|
242
243
|
ensureRegistryDir(stateDir);
|
|
243
244
|
const registryPath = getRegistryPath(stateDir);
|
|
244
|
-
|
|
245
|
-
fs.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), 'utf-8');
|
|
246
|
-
fs.renameSync(tmpPath, registryPath);
|
|
245
|
+
atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
/**
|
|
@@ -350,7 +349,7 @@ export function assertPromotionGatePassed(stateDir: string, checkpointId: string
|
|
|
350
349
|
* @throws Error if checkpoint's targetModelFamily violates profile constraints
|
|
351
350
|
*/
|
|
352
351
|
|
|
353
|
-
|
|
352
|
+
|
|
354
353
|
export function bindCheckpointToWorkerProfile(
|
|
355
354
|
stateDir: string,
|
|
356
355
|
workerProfile: WorkerProfile,
|
|
@@ -34,6 +34,7 @@ import * as fs from 'fs';
|
|
|
34
34
|
import * as path from 'path';
|
|
35
35
|
import * as crypto from 'crypto';
|
|
36
36
|
import { withLock } from '../utils/file-lock.js';
|
|
37
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// Constants
|
|
@@ -239,9 +240,7 @@ function readRegistry(stateDir: string): ModelTrainingRegistry {
|
|
|
239
240
|
function writeRegistry(stateDir: string, registry: ModelTrainingRegistry): void {
|
|
240
241
|
ensureRegistryDir(stateDir);
|
|
241
242
|
const registryPath = getRegistryPath(stateDir);
|
|
242
|
-
|
|
243
|
-
fs.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), 'utf-8');
|
|
244
|
-
fs.renameSync(tmpPath, registryPath);
|
|
243
|
+
atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
/**
|
|
@@ -322,7 +321,7 @@ export function registerTrainingRun(
|
|
|
322
321
|
* @throws Error if run not found or transition is invalid
|
|
323
322
|
*/
|
|
324
323
|
|
|
325
|
-
|
|
324
|
+
|
|
326
325
|
export function updateTrainingRunStatus(
|
|
327
326
|
stateDir: string,
|
|
328
327
|
trainRunId: string,
|
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { resolveNocturnalDir } from './nocturnal-paths.js';
|
|
4
4
|
import { withLock } from '../utils/file-lock.js';
|
|
5
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
5
6
|
|
|
6
7
|
export type ArtifactKind = 'behavioral-sample' | 'rule-implementation-candidate';
|
|
7
8
|
|
|
@@ -56,9 +57,7 @@ function writeArtifactLineageRegistry(
|
|
|
56
57
|
): void {
|
|
57
58
|
const registryPath = getLineageRegistryPath(workspaceDir);
|
|
58
59
|
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
59
|
-
|
|
60
|
-
fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2), 'utf-8');
|
|
61
|
-
fs.renameSync(tmpPath, registryPath);
|
|
60
|
+
atomicWriteFileSync(registryPath, JSON.stringify(records, null, 2));
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
export function appendArtifactLineageRecord(
|
|
@@ -31,6 +31,7 @@ import * as crypto from 'crypto';
|
|
|
31
31
|
import { NocturnalPathResolver, resolveNocturnalDir } from './nocturnal-paths.js';
|
|
32
32
|
import type { NocturnalArtifact } from './nocturnal-arbiter.js';
|
|
33
33
|
import { withLock } from '../utils/file-lock.js';
|
|
34
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Types
|
|
@@ -246,9 +247,7 @@ function readRegistry(workspaceDir: string): NocturnalDatasetRecord[] {
|
|
|
246
247
|
function writeRegistry(workspaceDir: string, records: NocturnalDatasetRecord[]): void {
|
|
247
248
|
ensureRegistryDir(workspaceDir);
|
|
248
249
|
const registryPath = getRegistryPath(workspaceDir);
|
|
249
|
-
|
|
250
|
-
fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2), 'utf-8');
|
|
251
|
-
fs.renameSync(tmpPath, registryPath);
|
|
250
|
+
atomicWriteFileSync(registryPath, JSON.stringify(records, null, 2));
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
// ---------------------------------------------------------------------------
|
|
@@ -284,7 +283,7 @@ function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatas
|
|
|
284
283
|
* @returns RegisterSampleResult
|
|
285
284
|
*/
|
|
286
285
|
|
|
287
|
-
|
|
286
|
+
|
|
288
287
|
export function registerSample(
|
|
289
288
|
workspaceDir: string,
|
|
290
289
|
artifact: NocturnalArtifact,
|
|
@@ -427,7 +426,7 @@ const VALID_TRANSITIONS: Record<NocturnalReviewStatus, NocturnalReviewStatus[]>
|
|
|
427
426
|
* @throws Error if transition is invalid
|
|
428
427
|
*/
|
|
429
428
|
|
|
430
|
-
|
|
429
|
+
|
|
431
430
|
export function updateReviewStatus(
|
|
432
431
|
workspaceDir: string,
|
|
433
432
|
sampleFingerprint: string,
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
import * as fs from 'fs';
|
|
48
48
|
import * as path from 'path';
|
|
49
49
|
import * as crypto from 'crypto';
|
|
50
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
50
51
|
import {
|
|
51
52
|
listDatasetRecords,
|
|
52
53
|
readDatasetArtifact,
|
|
@@ -158,7 +159,7 @@ function computeDatasetFingerprint(sampleFingerprints: string[]): string {
|
|
|
158
159
|
* Caller guarantees record.targetModelFamily is non-null.
|
|
159
160
|
*/
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
function serializeORPOSample(
|
|
163
164
|
record: NocturnalDatasetRecord,
|
|
164
165
|
artifact: ReturnType<typeof readDatasetArtifact>,
|
|
@@ -167,7 +168,7 @@ function serializeORPOSample(
|
|
|
167
168
|
datasetFingerprint: string
|
|
168
169
|
): ORPOSample {
|
|
169
170
|
const now = new Date().toISOString();
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
const rejected = buildEvidenceBoundedRejected(artifact, evidenceSummary);
|
|
172
173
|
|
|
173
174
|
return {
|
|
@@ -180,7 +181,7 @@ function serializeORPOSample(
|
|
|
180
181
|
prompt: rejected,
|
|
181
182
|
chosen: artifact.betterDecision,
|
|
182
183
|
rejected,
|
|
183
|
-
|
|
184
|
+
|
|
184
185
|
rationale: buildEvidenceBoundedRationale(evidenceSummary),
|
|
185
186
|
datasetMetadata: {
|
|
186
187
|
sampleFingerprint: record.sampleFingerprint,
|
|
@@ -295,7 +296,7 @@ export function exportORPOSamples(
|
|
|
295
296
|
});
|
|
296
297
|
|
|
297
298
|
|
|
298
|
-
|
|
299
|
+
|
|
299
300
|
let eligibleRecords: typeof allApprovedRecords;
|
|
300
301
|
|
|
301
302
|
if (targetModelFamily !== undefined && targetModelFamily !== null) {
|
|
@@ -345,7 +346,7 @@ export function exportORPOSamples(
|
|
|
345
346
|
|
|
346
347
|
// Read artifact (throws on error — distinguishes read failure from missing artifact)
|
|
347
348
|
|
|
348
|
-
|
|
349
|
+
|
|
349
350
|
let artifact;
|
|
350
351
|
try {
|
|
351
352
|
artifact = readDatasetArtifact(workspaceDir, record.sampleFingerprint);
|
|
@@ -385,7 +386,7 @@ export function exportORPOSamples(
|
|
|
385
386
|
const exportsDir = NocturnalPathResolver.exportsDir(workspaceDir);
|
|
386
387
|
const jsonlPath = path.join(exportsDir, `${exportId}.jsonl`);
|
|
387
388
|
const lines = orpoSamples.map((s) => JSON.stringify(s)).join('\n') + '\n';
|
|
388
|
-
|
|
389
|
+
atomicWriteFileSync(jsonlPath, lines);
|
|
389
390
|
|
|
390
391
|
// Step 8: Write manifest
|
|
391
392
|
const manifest: ORPOExportManifest = {
|
|
@@ -404,7 +405,7 @@ export function exportORPOSamples(
|
|
|
404
405
|
})),
|
|
405
406
|
};
|
|
406
407
|
|
|
407
|
-
|
|
408
|
+
atomicWriteFileSync(manifest.manifestPath, JSON.stringify(manifest, null, 2));
|
|
408
409
|
|
|
409
410
|
return {
|
|
410
411
|
success: true,
|
package/src/core/pain.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { serializeKvLines, parseKvLines } from '../utils/io.js';
|
|
3
|
+
import { serializeKvLines, parseKvLines, atomicWriteFileSync } from '../utils/io.js';
|
|
4
4
|
import { resolvePdPath } from './paths.js';
|
|
5
5
|
import { ConfigService } from './config-service.js';
|
|
6
6
|
import { SystemLogger } from './system-logger.js';
|
|
@@ -98,7 +98,7 @@ export function validatePainFlag(data: Record<string, string>): string[] {
|
|
|
98
98
|
return missing;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
export function computePainScore(rc: number, isSpiral: boolean, missingTestCommand: boolean, softScore: number, projectDir?: string): number {
|
|
103
103
|
let score = Math.max(0, softScore || 0);
|
|
104
104
|
|
|
@@ -155,7 +155,7 @@ export function writePainFlag(projectDir: string, painData: PainFlagData): void
|
|
|
155
155
|
if (!fs.existsSync(dir)) {
|
|
156
156
|
fs.mkdirSync(dir, { recursive: true });
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
atomicWriteFileSync(painFlagPath, serializeKvLines(painData));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
@@ -212,7 +212,7 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
|
|
|
212
212
|
|
|
213
213
|
// Detect JSON format (wrong — should be KV)
|
|
214
214
|
if (content.startsWith('{')) {
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
let json: Record<string, unknown>;
|
|
217
217
|
try {
|
|
218
218
|
json = JSON.parse(content);
|
|
@@ -225,7 +225,7 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
|
|
|
225
225
|
const kvData = convertJsonToKv(json);
|
|
226
226
|
|
|
227
227
|
const repaired = serializeKvLines(kvData);
|
|
228
|
-
|
|
228
|
+
atomicWriteFileSync(painFlagPath, repaired);
|
|
229
229
|
SystemLogger.log(projectDir, 'PAIN_FLAG_AUTO_REPAIRED', `Auto-repaired pain flag from JSON to KV format (${Object.keys(json).length} fields)`);
|
|
230
230
|
return kvData;
|
|
231
231
|
}
|
|
@@ -281,7 +281,7 @@ export function readPainFlagContract(projectDir: string): PainFlagContractResult
|
|
|
281
281
|
* Errors are silently ignored to avoid disrupting the pain pipeline.
|
|
282
282
|
*/
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
export function trackPrincipleValue(
|
|
286
286
|
workspaceDir: string,
|
|
287
287
|
painData: { reason?: string; source?: string; score?: string },
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import type { OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
5
5
|
import { PathResolutionError } from '../config/index.js';
|
|
6
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
6
7
|
|
|
7
8
|
export interface PathResolverOptions {
|
|
8
9
|
workspaceDir?: string;
|
|
@@ -391,7 +392,7 @@ export function createDefaultConfig(targetPath?: string): string {
|
|
|
391
392
|
fs.mkdirSync(dir, { recursive: true });
|
|
392
393
|
}
|
|
393
394
|
|
|
394
|
-
|
|
395
|
+
atomicWriteFileSync(target, JSON.stringify(defaultConfig, null, 2));
|
|
395
396
|
|
|
396
397
|
console.log(`✅ Created default config at: ${target}`);
|
|
397
398
|
console.log(` You can edit this file to customize paths.`);
|
|
@@ -5,6 +5,7 @@ import type { PDTaskSpec } from './pd-task-types.js';
|
|
|
5
5
|
import { BUILTIN_PD_TASKS } from './pd-task-types.js';
|
|
6
6
|
import { readTasks, writeTasks } from './pd-task-store.js';
|
|
7
7
|
import { withLockAsync } from '../utils/file-lock.js';
|
|
8
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
8
9
|
|
|
9
10
|
const CRON_STORE_PATH = path.join(
|
|
10
11
|
os.homedir(),
|
|
@@ -97,9 +98,7 @@ async function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: s
|
|
|
97
98
|
|
|
98
99
|
async function writeCronStore(store: CronStoreFile): Promise<void> {
|
|
99
100
|
await withLockAsync(CRON_STORE_PATH, async () => {
|
|
100
|
-
|
|
101
|
-
fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), 'utf-8');
|
|
102
|
-
fs.renameSync(tmpPath, CRON_STORE_PATH);
|
|
101
|
+
atomicWriteFileSync(CRON_STORE_PATH, JSON.stringify(store, null, 2));
|
|
103
102
|
});
|
|
104
103
|
}
|
|
105
104
|
|
|
@@ -160,7 +159,7 @@ function buildCronJob(
|
|
|
160
159
|
payload: {
|
|
161
160
|
kind: 'agentTurn',
|
|
162
161
|
|
|
163
|
-
|
|
162
|
+
|
|
164
163
|
message: buildTaskPrompt(task, logger),
|
|
165
164
|
lightContext: task.execution.lightContext ?? true,
|
|
166
165
|
timeoutSeconds: task.execution.timeoutSeconds ?? 120,
|
|
@@ -295,7 +294,7 @@ export async function reconcilePDTasks(
|
|
|
295
294
|
|
|
296
295
|
const cronStore = await readCronStore(logger);
|
|
297
296
|
|
|
298
|
-
|
|
297
|
+
|
|
299
298
|
const healthUpdated = healthCheck(declared, cronStore, logger);
|
|
300
299
|
const actions = diff(healthUpdated, cronStore.jobs);
|
|
301
300
|
|
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import type { PDTaskSpec } from './pd-task-types.js';
|
|
4
4
|
import { withLockAsync } from '../utils/file-lock.js';
|
|
5
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
5
6
|
|
|
6
7
|
const PD_TASKS_FILENAME = 'pd_tasks.json';
|
|
7
8
|
|
|
@@ -39,9 +40,7 @@ export async function writeTasks(workspaceDir: string, tasks: PDTaskSpec[]): Pro
|
|
|
39
40
|
ensureStateDir(workspaceDir);
|
|
40
41
|
|
|
41
42
|
await withLockAsync(filePath, async () => {
|
|
42
|
-
|
|
43
|
-
fs.writeFileSync(tmpPath, JSON.stringify(tasks, null, 2), 'utf-8');
|
|
44
|
-
fs.renameSync(tmpPath, filePath);
|
|
43
|
+
atomicWriteFileSync(filePath, JSON.stringify(tasks, null, 2));
|
|
45
44
|
});
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -56,7 +55,7 @@ export function initTaskMeta(task: PDTaskSpec): PDTaskSpec {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
|
|
60
59
|
export function updateSyncMeta(
|
|
61
60
|
task: PDTaskSpec,
|
|
62
61
|
status: 'ok' | 'error',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { withLock, withLockAsync } from '../utils/file-lock.js';
|
|
4
|
+
import { atomicWriteFileSync } from '../utils/io.js';
|
|
4
5
|
import type {
|
|
5
6
|
Implementation,
|
|
6
7
|
ImplementationLifecycleState,
|
|
@@ -78,7 +79,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
|
|
82
83
|
function clampFloat(value: unknown, min: number, max: number, fallback: number): number {
|
|
83
84
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
84
85
|
return fallback;
|
|
@@ -87,7 +88,7 @@ function clampFloat(value: unknown, min: number, max: number, fallback: number):
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
|
|
92
93
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
93
94
|
return fallback;
|
|
@@ -314,13 +315,13 @@ function readLedgerFromFile(filePath: string): HybridLedgerStore {
|
|
|
314
315
|
|
|
315
316
|
function writeLedgerUnlocked(filePath: string, store: HybridLedgerStore): void {
|
|
316
317
|
ensureParentDir(filePath);
|
|
317
|
-
|
|
318
|
+
atomicWriteFileSync(filePath, serializeLedger(store));
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
|
|
321
322
|
function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) => T): T {
|
|
322
323
|
|
|
323
|
-
|
|
324
|
+
|
|
324
325
|
const filePath = getLedgerFilePath(stateDir);
|
|
325
326
|
return withLock(filePath, () => {
|
|
326
327
|
const store = readLedgerFromFile(filePath);
|
|
@@ -333,7 +334,7 @@ function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) =>
|
|
|
333
334
|
|
|
334
335
|
async function mutateLedgerAsync<T>(stateDir: string, mutate: (store: HybridLedgerStore) => Promise<T>): Promise<T> {
|
|
335
336
|
|
|
336
|
-
|
|
337
|
+
|
|
337
338
|
const filePath = getLedgerFilePath(stateDir);
|
|
338
339
|
return withLockAsync(filePath, async () => {
|
|
339
340
|
const store = readLedgerFromFile(filePath);
|