principles-disciple 1.51.0 ā 1.52.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/scripts/compile-principles.mjs +11 -1
- package/scripts/sync-plugin.mjs +183 -19
- package/src/core/pd-task-reconciler.ts +14 -11
- package/src/hooks/prompt.ts +15 -2
- package/src/service/event-log-auditor.ts +52 -39
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -18,10 +18,20 @@ import { fileURLToPath } from 'url';
|
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = dirname(__filename);
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Cross-platform home directory (Linux: HOME, Windows: USERPROFILE/HOMEDRIVE+HOMEPATH)
|
|
23
|
+
*/
|
|
24
|
+
function getHomeDir() {
|
|
25
|
+
return process.env.HOME
|
|
26
|
+
|| process.env.USERPROFILE
|
|
27
|
+
|| (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : null)
|
|
28
|
+
|| '.';
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// Resolve workspace directory: CLI arg > env var > default
|
|
22
32
|
const WORKSPACE_DIR = process.argv[2]
|
|
23
33
|
|| process.env.WORKSPACE_DIR
|
|
24
|
-
|| join(
|
|
34
|
+
|| join(getHomeDir(), '.openclaw', 'workspace');
|
|
25
35
|
|
|
26
36
|
const STATE_DIR = join(WORKSPACE_DIR, '.state');
|
|
27
37
|
|
package/scripts/sync-plugin.mjs
CHANGED
|
@@ -27,8 +27,36 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
27
27
|
const __dirname = dirname(__filename);
|
|
28
28
|
|
|
29
29
|
const SOURCE_DIR = join(__dirname, '..');
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cross-platform home directory resolution.
|
|
33
|
+
* Linux/macOS: HOME=/home/user
|
|
34
|
+
* Windows: USERPROFILE=C:\Users\user or HOMEDRIVE/HOMEPATH
|
|
35
|
+
*/
|
|
36
|
+
function getHomeDir() {
|
|
37
|
+
return process.env.HOME
|
|
38
|
+
|| process.env.USERPROFILE
|
|
39
|
+
|| (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : null)
|
|
40
|
+
|| '.';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const OPENCLAW_DIR = join(getHomeDir(), '.openclaw');
|
|
31
44
|
const INSTALL_DIR = join(OPENCLAW_DIR, 'extensions', 'principles-disciple');
|
|
45
|
+
function getConfiguredWorkspaceDir() {
|
|
46
|
+
const configPath = join(OPENCLAW_DIR, 'openclaw.json');
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
49
|
+
const config = JSON.parse(raw);
|
|
50
|
+
const workspace = config?.agents?.defaults?.workspace;
|
|
51
|
+
if (workspace && existsSync(workspace)) {
|
|
52
|
+
return workspace;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Fall through to fallback
|
|
56
|
+
}
|
|
57
|
+
// Fallback: try workspace-main (legacy default)
|
|
58
|
+
return join(OPENCLAW_DIR, 'workspace-main');
|
|
59
|
+
}
|
|
32
60
|
|
|
33
61
|
// Files and directories to sync
|
|
34
62
|
const SYNC_ITEMS = [
|
|
@@ -177,7 +205,11 @@ function checkPrerequisites() {
|
|
|
177
205
|
// Check for global package conflicts that cause module resolution traps
|
|
178
206
|
console.log('š Checking for global package conflicts...');
|
|
179
207
|
try {
|
|
180
|
-
|
|
208
|
+
// Cross-platform: use stdio: 'pipe' to capture stderr, then check output
|
|
209
|
+
const globalConflict = execSync('npm list -g principles-disciple --depth=0', {
|
|
210
|
+
encoding: 'utf-8',
|
|
211
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Capture all streams
|
|
212
|
+
});
|
|
181
213
|
if (globalConflict.includes('principles-disciple')) {
|
|
182
214
|
console.error('\nā CONFLICT DETECTED: A version of "principles-disciple" is installed globally via npm.');
|
|
183
215
|
console.error('This will block OpenClaw from loading the extension version you are trying to install.');
|
|
@@ -187,7 +219,17 @@ function checkPrerequisites() {
|
|
|
187
219
|
}
|
|
188
220
|
} catch (e) {
|
|
189
221
|
// npm list returns non-zero if not found, which is what we want
|
|
190
|
-
|
|
222
|
+
// Check if the error output contains the package name
|
|
223
|
+
const output = e.stdout || e.stderr || '';
|
|
224
|
+
if (!output.includes('principles-disciple')) {
|
|
225
|
+
console.log('ā
No global package conflicts detected.');
|
|
226
|
+
} else {
|
|
227
|
+
console.error('\nā CONFLICT DETECTED: A version of "principles-disciple" is installed globally via npm.');
|
|
228
|
+
console.error('This will block OpenClaw from loading the extension version you are trying to install.');
|
|
229
|
+
console.error('\nACTION REQUIRED: Please run the following command first:');
|
|
230
|
+
console.error(' npm uninstall -g principles-disciple\n');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
191
233
|
}
|
|
192
234
|
} catch {
|
|
193
235
|
console.error('ā npm not found. Please install Node.js with npm.');
|
|
@@ -481,7 +523,7 @@ function verifyInstalledFingerprint() {
|
|
|
481
523
|
}
|
|
482
524
|
|
|
483
525
|
/**
|
|
484
|
-
* Remove existing installation directory.
|
|
526
|
+
* Remove existing installation directory with Windows-friendly retry logic.
|
|
485
527
|
*/
|
|
486
528
|
function cleanTargetDir(force) {
|
|
487
529
|
if (!existsSync(INSTALL_DIR)) return;
|
|
@@ -495,7 +537,34 @@ function cleanTargetDir(force) {
|
|
|
495
537
|
}
|
|
496
538
|
|
|
497
539
|
console.log('\nšļø Removing existing installation...');
|
|
498
|
-
|
|
540
|
+
|
|
541
|
+
// Windows often returns EPERM due to file locks, add retry logic
|
|
542
|
+
const maxRetries = isWindows() ? 3 : 1;
|
|
543
|
+
let lastError = null;
|
|
544
|
+
|
|
545
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
546
|
+
try {
|
|
547
|
+
rmSync(INSTALL_DIR, { recursive: true, force: true });
|
|
548
|
+
console.log(' ā
Removed successfully.');
|
|
549
|
+
return;
|
|
550
|
+
} catch (err) {
|
|
551
|
+
lastError = err;
|
|
552
|
+
if (err.code === 'EPERM' && attempt < maxRetries) {
|
|
553
|
+
console.log(` ā ļø Attempt ${attempt}/${maxRetries} failed (EPERM), retrying in 2s...`);
|
|
554
|
+
// Synchronous sleep for retry
|
|
555
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// If all retries failed on Windows, try graceful fallback
|
|
561
|
+
if (isWindows() && lastError?.code === 'EPERM') {
|
|
562
|
+
console.log(' ā ļø Windows file lock detected, skipping removal.');
|
|
563
|
+
console.log(' š Will overwrite files in place.');
|
|
564
|
+
return; // Continue with overwrite installation
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
throw lastError;
|
|
499
568
|
}
|
|
500
569
|
|
|
501
570
|
/**
|
|
@@ -573,17 +642,115 @@ function cleanStaleBackups() {
|
|
|
573
642
|
}
|
|
574
643
|
|
|
575
644
|
/**
|
|
576
|
-
*
|
|
645
|
+
* Check if running on Windows.
|
|
646
|
+
*/
|
|
647
|
+
function isWindows() {
|
|
648
|
+
return process.platform === 'win32';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Get temporary directory path (cross-platform).
|
|
653
|
+
*/
|
|
654
|
+
function getTempDir() {
|
|
655
|
+
if (isWindows()) {
|
|
656
|
+
return process.env.TEMP || process.env.TMP || 'C:\\Windows\\Temp';
|
|
657
|
+
}
|
|
658
|
+
return '/tmp';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Restart OpenClaw Gateway (cross-platform).
|
|
577
663
|
*/
|
|
578
664
|
function restartGateway() {
|
|
579
665
|
console.log('\nš Restarting OpenClaw Gateway...');
|
|
666
|
+
|
|
667
|
+
if (isWindows()) {
|
|
668
|
+
return restartGatewayWindows();
|
|
669
|
+
} else {
|
|
670
|
+
return restartGatewayLinux();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Restart Gateway on Windows using PowerShell.
|
|
676
|
+
*/
|
|
677
|
+
function restartGatewayWindows() {
|
|
678
|
+
const logPath = join(getTempDir(), 'openclaw-auto-restart.log');
|
|
679
|
+
|
|
580
680
|
try {
|
|
681
|
+
// Step 1: Find and terminate existing gateway processes
|
|
682
|
+
console.log(' Looking for existing gateway processes...');
|
|
683
|
+
try {
|
|
684
|
+
// PowerShell command to find and kill openclaw gateway processes
|
|
685
|
+
// Note: Use single quotes inside -like pattern for proper escaping
|
|
686
|
+
const findCmd = "Get-Process -Name 'node' -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*openclaw*' } | Select-Object -ExpandProperty Id";
|
|
687
|
+
const pids = execSync(`powershell -NoProfile -Command "${findCmd}"`, { encoding: 'utf-8' }).trim();
|
|
688
|
+
|
|
689
|
+
if (pids) {
|
|
690
|
+
console.log(` Terminating existing gateway process(es): ${pids.replace(/\n/g, ', ')}...`);
|
|
691
|
+
// Kill by PID
|
|
692
|
+
const pidList = pids.split('\n').filter(p => p.trim());
|
|
693
|
+
for (const pid of pidList) {
|
|
694
|
+
try {
|
|
695
|
+
execSync(`taskkill /PID ${pid.trim()} /F`, { stdio: 'pipe' });
|
|
696
|
+
} catch { /* ignore if process already gone */ }
|
|
697
|
+
}
|
|
698
|
+
// Wait a moment for process to terminate
|
|
699
|
+
execSync('timeout /t 3 /nobreak > nul', { shell: true, stdio: 'ignore' });
|
|
700
|
+
}
|
|
701
|
+
} catch { /* no existing processes */ }
|
|
702
|
+
|
|
703
|
+
// Step 2: Start new gateway process in background
|
|
704
|
+
console.log(` Starting new gateway (logs: ${logPath})...`);
|
|
705
|
+
|
|
706
|
+
// Use openclaw CLI to start gateway (more reliable than direct node invocation)
|
|
707
|
+
const gatewayCmd = join(getHomeDir(), '.openclaw', 'gateway.cmd');
|
|
708
|
+
const startCmd = `Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${gatewayCmd}' -WindowStyle Hidden -RedirectStandardOutput '${logPath}' -RedirectStandardError '${join(getTempDir(), 'openclaw-auto-restart.err')}'`;
|
|
709
|
+
execSync(`powershell -NoProfile -Command "${startCmd}"`, { stdio: 'inherit' });
|
|
710
|
+
console.log('ā
Gateway restart triggered.');
|
|
711
|
+
|
|
712
|
+
// Step 3: Wait and verify
|
|
713
|
+
setTimeout(() => {
|
|
714
|
+
try {
|
|
715
|
+
if (existsSync(logPath)) {
|
|
716
|
+
const logs = readFileSync(logPath, 'utf-8');
|
|
717
|
+
if (logs.includes('Principles Disciple Plugin registered')) {
|
|
718
|
+
console.log('ā
SUCCESS: Principles Disciple plugin registered successfully!');
|
|
719
|
+
} else if (logs.includes('failed to load') || logs.includes('Error: Cannot find module')) {
|
|
720
|
+
console.error('\nā CRITICAL: Gateway started but PD plugin FAILED to load!');
|
|
721
|
+
console.error(' Check logs at: ' + logPath);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
} else {
|
|
724
|
+
console.warn('ā ļø Gateway started but PD registration not confirmed in recent logs.');
|
|
725
|
+
console.log(' Check logs at: ' + logPath);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch (e) {
|
|
729
|
+
console.warn(`ā ļø Post-restart verification skipped: ${e.message}`);
|
|
730
|
+
}
|
|
731
|
+
}, 8000);
|
|
732
|
+
|
|
733
|
+
} catch (error) {
|
|
734
|
+
console.error(`\nā Failed to restart gateway: ${error.message}`);
|
|
735
|
+
console.error(' You may need to manually restart OpenClaw Gateway.');
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Restart Gateway on Linux using systemctl or process management.
|
|
742
|
+
*/
|
|
743
|
+
function restartGatewayLinux() {
|
|
744
|
+
const logPath = '/tmp/openclaw-auto-restart.log';
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
// Try systemctl first (Linux systemd)
|
|
581
748
|
try {
|
|
582
749
|
execSync('systemctl --user is-active openclaw-gateway.service', { stdio: 'pipe' });
|
|
583
750
|
console.log(' Restarting via systemctl...');
|
|
584
751
|
execSync('systemctl --user restart openclaw-gateway.service', { stdio: 'inherit' });
|
|
585
752
|
console.log('ā
Gateway restarted via systemctl.');
|
|
586
|
-
|
|
753
|
+
|
|
587
754
|
console.log(' Waiting for Gateway to initialize and load PD plugin (8s)...');
|
|
588
755
|
setTimeout(() => {
|
|
589
756
|
try {
|
|
@@ -608,8 +775,9 @@ function restartGateway() {
|
|
|
608
775
|
}
|
|
609
776
|
}, 8000);
|
|
610
777
|
return;
|
|
611
|
-
} catch { /*
|
|
778
|
+
} catch { /* systemctl not available, fall through to manual restart */ }
|
|
612
779
|
|
|
780
|
+
// Manual process management
|
|
613
781
|
const pids = execSync('pgrep -f "openclaw-gateway|openclaw gateway"', { encoding: 'utf-8' }).trim();
|
|
614
782
|
if (pids) {
|
|
615
783
|
console.log(` Terminating existing gateway process(es)...`);
|
|
@@ -617,11 +785,10 @@ function restartGateway() {
|
|
|
617
785
|
execSync('sleep 3');
|
|
618
786
|
}
|
|
619
787
|
|
|
620
|
-
const logPath = '/tmp/openclaw-auto-restart.log';
|
|
621
788
|
console.log(` Starting new gateway (logs: ${logPath})...`);
|
|
622
789
|
execSync(`nohup openclaw gateway --force > ${logPath} 2>&1 &`, { stdio: 'ignore' });
|
|
623
790
|
console.log('ā
Gateway restart triggered.');
|
|
624
|
-
|
|
791
|
+
|
|
625
792
|
setTimeout(() => {
|
|
626
793
|
if (existsSync(logPath)) {
|
|
627
794
|
const logs = readFileSync(logPath, 'utf-8');
|
|
@@ -699,9 +866,10 @@ function main() {
|
|
|
699
866
|
if (existsSync(bootstrapScript)) {
|
|
700
867
|
console.log('\nš§ Synchronizing principles to active rules (Bootstrap)...');
|
|
701
868
|
try {
|
|
702
|
-
const
|
|
869
|
+
const workspaceDir = getConfiguredWorkspaceDir();
|
|
870
|
+
const targetStateDir = join(workspaceDir, '.state');
|
|
703
871
|
if (existsSync(targetStateDir)) {
|
|
704
|
-
execSync(`
|
|
872
|
+
execSync(`node scripts/bootstrap-rules.mjs`, { cwd: SOURCE_DIR, stdio: 'inherit', env: { ...process.env, STATE_DIR: targetStateDir, BOOTSTRAP_LIMIT: '100' } });
|
|
705
873
|
console.log('ā
Principles synchronized.');
|
|
706
874
|
}
|
|
707
875
|
} catch (e) {
|
|
@@ -713,7 +881,8 @@ function main() {
|
|
|
713
881
|
if (existsSync(compileScript)) {
|
|
714
882
|
console.log('\nāļø Compiling pain-derived principles into rules...');
|
|
715
883
|
try {
|
|
716
|
-
const
|
|
884
|
+
const workspaceDir = getConfiguredWorkspaceDir();
|
|
885
|
+
const targetWorkspaceDir = workspaceDir;
|
|
717
886
|
if (existsSync(targetWorkspaceDir)) {
|
|
718
887
|
execSync(`node scripts/compile-principles.mjs ${targetWorkspaceDir}`, { cwd: SOURCE_DIR, stdio: 'inherit' });
|
|
719
888
|
console.log('ā
Principle compilation complete.');
|
|
@@ -732,12 +901,6 @@ function main() {
|
|
|
732
901
|
verifyInstalledFingerprint();
|
|
733
902
|
if (args.dev || args.restart) cleanStaleBackups();
|
|
734
903
|
|
|
735
|
-
try {
|
|
736
|
-
const reloadSignal = join(OPENCLAW_DIR, '.plugin_reload_signal');
|
|
737
|
-
writeFileSync(reloadSignal, new Date().toISOString(), 'utf-8');
|
|
738
|
-
console.log(`\nš Reload signal sent to ${reloadSignal}`);
|
|
739
|
-
} catch { /* ignore */ }
|
|
740
|
-
|
|
741
904
|
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
742
905
|
console.log('ā ā
Installation Complete ā');
|
|
743
906
|
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
@@ -746,6 +909,7 @@ function main() {
|
|
|
746
909
|
restartGateway();
|
|
747
910
|
} else {
|
|
748
911
|
console.log('\nš” Restart OpenClaw Gateway to load the new version.');
|
|
912
|
+
console.log(' (Plugin code changes require a full gateway restart)');
|
|
749
913
|
}
|
|
750
914
|
}
|
|
751
915
|
|
|
@@ -144,7 +144,7 @@ function diff(declared: PDTaskSpec[], actual: CronJob[]): DiffAction[] {
|
|
|
144
144
|
function buildCronJob(
|
|
145
145
|
task: PDTaskSpec,
|
|
146
146
|
nowMs: number,
|
|
147
|
-
|
|
147
|
+
workspaceDir: string,
|
|
148
148
|
logger?: { info?: (_: string) => void },
|
|
149
149
|
): CronJob {
|
|
150
150
|
logger?.info?.(`[PD:Reconciler] Building cron job: ${task.name} (id=${task.id}, interval=${task.schedule.everyMs}ms)`);
|
|
@@ -159,9 +159,7 @@ function buildCronJob(
|
|
|
159
159
|
wakeMode: 'now',
|
|
160
160
|
payload: {
|
|
161
161
|
kind: 'agentTurn',
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
message: buildTaskPrompt(task, logger),
|
|
162
|
+
message: buildTaskPrompt(task, workspaceDir, logger),
|
|
165
163
|
lightContext: task.execution.lightContext ?? true,
|
|
166
164
|
timeoutSeconds: task.execution.timeoutSeconds ?? 120,
|
|
167
165
|
toolsAllow: task.execution.toolsAllow,
|
|
@@ -180,7 +178,12 @@ function buildCronJob(
|
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
|
|
183
|
-
function buildTaskPrompt(task: PDTaskSpec, logger?: { info?: (_: string) => void }): string {
|
|
181
|
+
function buildTaskPrompt(task: PDTaskSpec, workspaceDir: string, logger?: { info?: (_: string) => void }): string {
|
|
182
|
+
// Resolve paths dynamically from workspaceDir instead of hardcoding
|
|
183
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
184
|
+
const empathyKeywordsPath = path.join(stateDir, 'empathy_keywords.json');
|
|
185
|
+
const eventsJsonlPath = path.join(stateDir, 'logs', 'events.jsonl');
|
|
186
|
+
|
|
184
187
|
if (task.id === 'empathy-optimizer') {
|
|
185
188
|
logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
|
|
186
189
|
return `You are the Principles Disciple Empathy Keyword Optimizer.
|
|
@@ -195,7 +198,7 @@ Analyze the current empathy keyword store and recent user message logs to:
|
|
|
195
198
|
|
|
196
199
|
### Step 1: Read current keyword store
|
|
197
200
|
Use read_file to load:
|
|
198
|
-
|
|
201
|
+
\`${empathyKeywordsPath}\`
|
|
199
202
|
|
|
200
203
|
Examine the "terms" object. For each term note:
|
|
201
204
|
- weight (0.1-0.9): higher = stronger frustration signal
|
|
@@ -204,7 +207,7 @@ Examine the "terms" object. For each term note:
|
|
|
204
207
|
|
|
205
208
|
### Step 2: Read recent message logs
|
|
206
209
|
Use search_file_content to scan:
|
|
207
|
-
|
|
210
|
+
\`${eventsJsonlPath}\`
|
|
208
211
|
|
|
209
212
|
Look for user messages containing frustration signals:
|
|
210
213
|
- Negation: "äøåƹ", "éäŗ", "äøč”", "éå"
|
|
@@ -214,7 +217,7 @@ Look for user messages containing frustration signals:
|
|
|
214
217
|
|
|
215
218
|
### Step 3: Write updated keyword store
|
|
216
219
|
Use write_file to save the updated store back to:
|
|
217
|
-
|
|
220
|
+
\`${empathyKeywordsPath}\`
|
|
218
221
|
|
|
219
222
|
The file format is:
|
|
220
223
|
\`\`\`json
|
|
@@ -304,7 +307,7 @@ export async function reconcilePDTasks(
|
|
|
304
307
|
case 'CREATE':
|
|
305
308
|
if (action.task) {
|
|
306
309
|
if (!dryRun) {
|
|
307
|
-
const job = buildCronJob(action.task, nowMs, logger);
|
|
310
|
+
const job = buildCronJob(action.task, nowMs, workspaceDir, logger);
|
|
308
311
|
cronStore.jobs.push(job);
|
|
309
312
|
logger.info?.(`[PD:Reconciler] Created job: ${action.task.name}`);
|
|
310
313
|
}
|
|
@@ -315,7 +318,7 @@ export async function reconcilePDTasks(
|
|
|
315
318
|
if (action.task && action.job) {
|
|
316
319
|
if (!dryRun) {
|
|
317
320
|
const idx = cronStore.jobs.indexOf(action.job);
|
|
318
|
-
const newJob = buildCronJob(action.task, nowMs, logger);
|
|
321
|
+
const newJob = buildCronJob(action.task, nowMs, workspaceDir, logger);
|
|
319
322
|
newJob.id = action.job.id;
|
|
320
323
|
// Preserve original state ā only CronService should recalculate nextRunAtMs
|
|
321
324
|
newJob.state = {
|
|
@@ -449,7 +452,7 @@ export async function trigger(
|
|
|
449
452
|
existingJob.deleteAfterRun = undefined;
|
|
450
453
|
} else {
|
|
451
454
|
log(`Creating new job for manual trigger: ${task.name}`);
|
|
452
|
-
const newJob = buildCronJob(task, nowMs, { info: log });
|
|
455
|
+
const newJob = buildCronJob(task, nowMs, workspaceDir, { info: log });
|
|
453
456
|
newJob.enabled = true;
|
|
454
457
|
newJob.state.nextRunAtMs = nowMs;
|
|
455
458
|
cronStore.jobs.push(newJob);
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -533,8 +533,11 @@ The empathy observer subagent handles pain detection independently.
|
|
|
533
533
|
|
|
534
534
|
const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
|
|
535
535
|
logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
|
|
536
|
+
|
|
537
|
+
// Track if we should inject behavioral constraints (will be added to appendSystemContext later)
|
|
538
|
+
let shouldInjectBehavioralConstraints = false;
|
|
536
539
|
if (empathyEnabled && isUserInteraction && sessionId && api && !isAgentToAgent) {
|
|
537
|
-
|
|
540
|
+
shouldInjectBehavioralConstraints = true;
|
|
538
541
|
|
|
539
542
|
// āā Empathy Hybrid Matching (keyword + subagent sampling) āā
|
|
540
543
|
// Fast keyword scan on every turn, with strategic subagent sampling
|
|
@@ -935,9 +938,18 @@ ${taskBlocks}${processingNote}
|
|
|
935
938
|
}
|
|
936
939
|
|
|
937
940
|
// Build appendSystemContext with recency effect
|
|
938
|
-
// Content order (most important last): project_context -> working_memory -> reflection_log -> thinking_os -> principles
|
|
941
|
+
// Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
|
|
939
942
|
const appendParts: string[] = [];
|
|
940
943
|
|
|
944
|
+
// 0. Behavioral Constraints (empathy observer coordination)
|
|
945
|
+
// Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
|
|
946
|
+
// See: https://github.com/csuzngjh/principles/issues/XXX
|
|
947
|
+
if (shouldInjectBehavioralConstraints) {
|
|
948
|
+
appendParts.push(`<behavioral_constraints>
|
|
949
|
+
${empathySilenceConstraint}
|
|
950
|
+
</behavioral_constraints>`);
|
|
951
|
+
}
|
|
952
|
+
|
|
941
953
|
// 1. Project Context (lowest priority, goes first)
|
|
942
954
|
if (projectContextContent) {
|
|
943
955
|
appendParts.push(`<project_context>\n${projectContextContent}\n</project_context>`);
|
|
@@ -1087,6 +1099,7 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
|
|
|
1087
1099
|
---
|
|
1088
1100
|
|
|
1089
1101
|
**ćEXECUTION RULESć** (Priority: Low ā High):
|
|
1102
|
+
- \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
|
|
1090
1103
|
- \`<project_context>\` - Current priorities (can be overridden)
|
|
1091
1104
|
- \`<reflection_log>\` - Past lessons (inform your approach)
|
|
1092
1105
|
- \`<thinking_os>\` - Thinking models (guide your reasoning)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EventLog Auditor
|
|
3
|
-
*
|
|
2
|
+
* EventLog Auditor - Search and verify events across all .state directories
|
|
3
|
+
*
|
|
4
4
|
* This tool addresses a common debugging issue where hook events may be
|
|
5
5
|
* written to the wrong .state directory due to workspaceDir resolution bugs.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* const report = await auditEventLogs(openclawDir, ['after_tool_call', 'before_tool_call']);
|
|
9
9
|
* console.log(report.summary);
|
|
@@ -42,7 +42,7 @@ interface AuditReport {
|
|
|
42
42
|
*/
|
|
43
43
|
function findEventLogs(baseDir: string, maxDepth = 4): string[] {
|
|
44
44
|
const results: string[] = [];
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
function scan(dir: string, depth: number): void {
|
|
47
47
|
if (depth > maxDepth) return;
|
|
48
48
|
try {
|
|
@@ -58,37 +58,50 @@ function findEventLogs(baseDir: string, maxDepth = 4): string[] {
|
|
|
58
58
|
// Permission denied or directory doesn't exist
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
scan(baseDir, 0);
|
|
63
63
|
return results;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Find events.jsonl in well-known locations.
|
|
68
|
+
* Dynamically discovers workspace directories instead of hardcoding names.
|
|
68
69
|
*/
|
|
69
70
|
function findKnownEventLogPaths(): string[] {
|
|
70
71
|
const homeDir = os.homedir();
|
|
71
72
|
const candidates: string[] = [];
|
|
72
|
-
|
|
73
|
-
// Common patterns
|
|
74
|
-
const
|
|
73
|
+
|
|
74
|
+
// Common patterns (legacy, non-workspace paths)
|
|
75
|
+
const legacyPatterns = [
|
|
75
76
|
path.join(homeDir, '.state', 'logs', 'events.jsonl'),
|
|
76
77
|
path.join(homeDir, '.openclaw', '.state', 'logs', 'events.jsonl'),
|
|
77
|
-
path.join(homeDir, '.openclaw', 'workspace-main', '.state', 'logs', 'events.jsonl'),
|
|
78
|
-
path.join(homeDir, '.openclaw', 'workspace-builder', '.state', 'logs', 'events.jsonl'),
|
|
79
|
-
path.join(homeDir, '.openclaw', 'workspace-pm', '.state', 'logs', 'events.jsonl'),
|
|
80
|
-
path.join(homeDir, '.openclaw', 'workspace-hr', '.state', 'logs', 'events.jsonl'),
|
|
81
|
-
path.join(homeDir, '.openclaw', 'workspace-repair', '.state', 'logs', 'events.jsonl'),
|
|
82
|
-
path.join(homeDir, '.openclaw', 'workspace-research', '.state', 'logs', 'events.jsonl'),
|
|
83
|
-
path.join(homeDir, '.openclaw', 'workspace-scout', '.state', 'logs', 'events.jsonl'),
|
|
84
78
|
];
|
|
85
|
-
|
|
86
|
-
for (const p of
|
|
79
|
+
|
|
80
|
+
for (const p of legacyPatterns) {
|
|
87
81
|
if (fs.existsSync(p)) {
|
|
88
82
|
candidates.push(p);
|
|
89
83
|
}
|
|
90
84
|
}
|
|
91
|
-
|
|
85
|
+
|
|
86
|
+
// Dynamically discover workspace directories under ~/.openclaw/
|
|
87
|
+
const openclawDir = path.join(homeDir, '.openclaw');
|
|
88
|
+
if (fs.existsSync(openclawDir)) {
|
|
89
|
+
try {
|
|
90
|
+
const entries = fs.readdirSync(openclawDir, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isDirectory()) continue;
|
|
93
|
+
// Skip known non-workspace directories
|
|
94
|
+
if (entry.name.startsWith('.') || entry.name === 'extensions' || entry.name === 'memory') continue;
|
|
95
|
+
const eventLogPath = path.join(openclawDir, entry.name, '.state', 'logs', 'events.jsonl');
|
|
96
|
+
if (fs.existsSync(eventLogPath)) {
|
|
97
|
+
candidates.push(eventLogPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Directory read failed, skip dynamic discovery
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
return candidates;
|
|
93
106
|
}
|
|
94
107
|
|
|
@@ -133,25 +146,25 @@ function countAllHooks(filePath: string): Record<string, number> {
|
|
|
133
146
|
|
|
134
147
|
/**
|
|
135
148
|
* Audit all events.jsonl files.
|
|
136
|
-
*
|
|
149
|
+
*
|
|
137
150
|
* @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
|
|
138
151
|
* @param expectedToolHooks - Hook names that should appear in the primary workspace
|
|
139
152
|
*/
|
|
140
|
-
|
|
153
|
+
|
|
141
154
|
export async function auditEventLogs(
|
|
142
155
|
openclawDir: string,
|
|
143
156
|
expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
|
|
144
157
|
): Promise<AuditReport> {
|
|
145
158
|
const homeDir = os.homedir();
|
|
146
|
-
|
|
159
|
+
|
|
147
160
|
// Find all event logs
|
|
148
161
|
const knownPaths = findKnownEventLogPaths();
|
|
149
162
|
const scannedPaths = findEventLogs(homeDir, 4);
|
|
150
163
|
const allPaths = [...new Set([...knownPaths, ...scannedPaths])];
|
|
151
|
-
|
|
164
|
+
|
|
152
165
|
const locations: LocationReport[] = [];
|
|
153
166
|
let primaryPath: string | null = null;
|
|
154
|
-
|
|
167
|
+
|
|
155
168
|
for (const filePath of allPaths) {
|
|
156
169
|
try {
|
|
157
170
|
const stat = fs.statSync(filePath);
|
|
@@ -165,7 +178,7 @@ export async function auditEventLogs(
|
|
|
165
178
|
hookCounts: allCounts,
|
|
166
179
|
recentEntries: recent,
|
|
167
180
|
});
|
|
168
|
-
|
|
181
|
+
|
|
169
182
|
// Determine primary path (workspace-main or most recent)
|
|
170
183
|
if (filePath.includes('workspace-main') || filePath.includes('workspace-main')) {
|
|
171
184
|
primaryPath = filePath;
|
|
@@ -174,7 +187,7 @@ export async function auditEventLogs(
|
|
|
174
187
|
// Skip unreadable files
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
|
-
|
|
190
|
+
|
|
178
191
|
// If no primary found, use most recent
|
|
179
192
|
if (!primaryPath && locations.length > 0) {
|
|
180
193
|
locations.sort((a, b) => {
|
|
@@ -184,21 +197,21 @@ export async function auditEventLogs(
|
|
|
184
197
|
});
|
|
185
198
|
primaryPath = locations[0].path;
|
|
186
199
|
}
|
|
187
|
-
|
|
200
|
+
|
|
188
201
|
// Detect misplaced tool hook events
|
|
189
202
|
const misplacedEvents: { path: string; entries: EventLogEntry[] }[] = [];
|
|
190
203
|
for (const loc of locations) {
|
|
191
204
|
if (loc.path === primaryPath) continue;
|
|
192
|
-
|
|
193
|
-
const toolHookEntries = loc.recentEntries.filter(e =>
|
|
205
|
+
|
|
206
|
+
const toolHookEntries = loc.recentEntries.filter(e =>
|
|
194
207
|
e.type === 'hook_execution' && expectedToolHooks.includes(e.data?.hook as string)
|
|
195
208
|
);
|
|
196
|
-
|
|
209
|
+
|
|
197
210
|
if (toolHookEntries.length > 0) {
|
|
198
211
|
misplacedEvents.push({ path: loc.path, entries: toolHookEntries });
|
|
199
212
|
}
|
|
200
213
|
}
|
|
201
|
-
|
|
214
|
+
|
|
202
215
|
return {
|
|
203
216
|
searchedPaths: allPaths,
|
|
204
217
|
locations,
|
|
@@ -210,37 +223,37 @@ export async function auditEventLogs(
|
|
|
210
223
|
/**
|
|
211
224
|
* Format audit report for display.
|
|
212
225
|
*/
|
|
213
|
-
|
|
226
|
+
|
|
214
227
|
export function formatAuditReport(report: AuditReport): string {
|
|
215
228
|
const lines: string[] = [];
|
|
216
|
-
|
|
229
|
+
|
|
217
230
|
lines.push('=== Event Log Audit Report ===\n');
|
|
218
|
-
|
|
231
|
+
|
|
219
232
|
lines.push(`Searched ${report.searchedPaths.length} paths:\n`);
|
|
220
233
|
for (const p of report.searchedPaths) {
|
|
221
234
|
lines.push(` ${p}`);
|
|
222
235
|
}
|
|
223
236
|
lines.push('');
|
|
224
|
-
|
|
237
|
+
|
|
225
238
|
lines.push(`Primary: ${report.primaryPath ?? 'NOT FOUND'}\n`);
|
|
226
|
-
|
|
239
|
+
|
|
227
240
|
for (const loc of report.locations) {
|
|
228
241
|
const isPrimary = loc.path === report.primaryPath;
|
|
229
242
|
lines.push(`āāā ${isPrimary ? '[PRIMARY]' : '[OTHER] '}${loc.path}`);
|
|
230
243
|
lines.push(` Last modified: ${loc.lastModified?.toISOString() ?? 'never'}`);
|
|
231
244
|
lines.push(` Hook counts:`);
|
|
232
|
-
|
|
245
|
+
|
|
233
246
|
const hooks = Object.entries(loc.hookCounts).sort((a, b) => b[1] - a[1]);
|
|
234
247
|
for (const [hook, count] of hooks) {
|
|
235
248
|
lines.push(` ${hook}: ${count}`);
|
|
236
249
|
}
|
|
237
|
-
|
|
250
|
+
|
|
238
251
|
if (hooks.length === 0) {
|
|
239
252
|
lines.push(` (no hooks recorded)`);
|
|
240
253
|
}
|
|
241
254
|
lines.push('');
|
|
242
255
|
}
|
|
243
|
-
|
|
256
|
+
|
|
244
257
|
if (report.misplacedEvents.length > 0) {
|
|
245
258
|
lines.push('ā ļø MISPLACED tool hook events detected:');
|
|
246
259
|
for (const me of report.misplacedEvents) {
|
|
@@ -258,6 +271,6 @@ export function formatAuditReport(report: AuditReport): string {
|
|
|
258
271
|
} else {
|
|
259
272
|
lines.push('ā
No misplaced tool hook events detected.');
|
|
260
273
|
}
|
|
261
|
-
|
|
274
|
+
|
|
262
275
|
return lines.join('\n');
|
|
263
276
|
}
|