principles-disciple 1.107.0 → 1.108.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 +2 -2
- package/src/core/init.ts +3 -1
- package/src/core/workspace-dir-validation.ts +3 -3
- package/tests/core-anti-growth.test.ts +0 -13
- package/tests/hooks/prompt-characterization.test.ts +1 -11
- package/tests/hooks/prompt-diet.test.ts +3 -11
- package/tests/hooks/prompt-size-guard.test.ts +0 -10
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +0 -10
- package/tests/index.test.ts +1 -1
- package/tests/runtime-v2-discovery-guard.test.ts +1 -2
- package/vitest.config.ts +2 -3
- package/vitest.unit.config.ts +12 -0
- package/src/core/evolution-hook.ts +0 -74
- package/src/core/file-storage-adapter.ts +0 -203
- package/src/core/merge-gate-audit.ts +0 -314
- package/src/core/pain-context-extractor.ts +0 -306
- package/src/core/pain-lifecycle.ts +0 -38
- package/src/core/pain-signal-adapter.ts +0 -42
- package/src/core/pain-signal.ts +0 -22
- package/src/core/principle-injector.ts +0 -84
- package/src/core/principle-tree-migration.ts +0 -196
- package/src/core/storage-adapter.ts +0 -65
- package/src/core/telemetry-event.ts +0 -109
- package/src/core/training-program.ts +0 -632
- package/src/core/workspace-dir-service.ts +0 -119
- package/src/hooks/lifecycle-routing.ts +0 -125
- package/src/service/event-log-auditor.ts +0 -284
- package/src/service/evolution-queue-lock.ts +0 -47
- package/src/service/failure-classifier.ts +0 -79
- package/src/service/internalization-trigger-adapter.ts +0 -302
- package/src/service/monitoring-query-service.ts +0 -67
- package/src/service/subagent-workflow/index.ts +0 -17
- package/src/tools/critique-prompt.ts +0 -1
- package/src/tools/model-index.ts +0 -1
- package/src/types/event-payload.ts +0 -16
- package/src/utils/glob-match.ts +0 -50
- package/src/utils/nlp.ts +0 -25
- package/src/utils/plugin-logger.ts +0 -97
- package/src/utils/subagent-probe.ts +0 -81
- package/tests/core/evolution-hook.test.ts +0 -123
- package/tests/core/file-storage-adapter.test.ts +0 -285
- package/tests/core/merge-gate-audit.test.ts +0 -117
- package/tests/core/pain-context-extractor.test.ts +0 -279
- package/tests/core/pain-lifecycle.test.ts +0 -38
- package/tests/core/pain-signal-adapter.test.ts +0 -116
- package/tests/core/pain-signal.test.ts +0 -190
- package/tests/core/principle-injector.test.ts +0 -90
- package/tests/core/principle-tree-migration.test.ts +0 -77
- package/tests/core/storage-conformance.test.ts +0 -429
- package/tests/core/telemetry-event.test.ts +0 -119
- package/tests/core/training-program.test.ts +0 -472
- package/tests/core/workspace-dir-service.test.ts +0 -68
- package/tests/core/workspace-dir-validation.test.ts +0 -143
- package/tests/integration/internalization-trigger-guard.test.ts +0 -69
- package/tests/integration/pain-lifecycle-e2e.test.ts +0 -75
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +0 -209
- package/tests/service/failure-classifier.test.ts +0 -171
- package/tests/service/internalization-trigger-adapter.test.ts +0 -251
- package/tests/service/monitoring-query-service.test.ts +0 -67
- package/tests/utils/nlp.test.ts +0 -35
- package/tests/utils/plugin-logger.test.ts +0 -156
- package/tests/utils/subagent-probe.test.ts +0 -79
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
|
|
2
|
-
import { validateWorkspaceDir, type WorkspaceResolutionContext } from './workspace-dir-validation.js';
|
|
3
|
-
|
|
4
|
-
export interface WorkspaceResolutionOptions {
|
|
5
|
-
source?: string;
|
|
6
|
-
required?: boolean;
|
|
7
|
-
fallbackAgentId?: string;
|
|
8
|
-
logger?: PluginLogger;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function buildResolutionFailureMessage(source: string, attempts: string[]): string {
|
|
12
|
-
const suffix = attempts.length > 0 ? ` Attempts: ${attempts.join(' | ')}` : '';
|
|
13
|
-
return `[PD:WorkspaceDir] ${source}: unable to resolve a valid workspace directory.${suffix}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function tryResolveFromAgent(
|
|
17
|
-
api: OpenClawPluginApi,
|
|
18
|
-
agentId: string,
|
|
19
|
-
attempts: string[],
|
|
20
|
-
): string | undefined {
|
|
21
|
-
try {
|
|
22
|
-
const resolved = api.runtime?.agent?.resolveAgentWorkspaceDir?.(api.config, agentId);
|
|
23
|
-
const issue = validateWorkspaceDir(resolved);
|
|
24
|
-
if (!issue) {
|
|
25
|
-
return resolved;
|
|
26
|
-
}
|
|
27
|
-
attempts.push(`agent:${agentId} invalid (${issue})`);
|
|
28
|
-
} catch (error) {
|
|
29
|
-
attempts.push(`agent:${agentId} threw (${String(error)})`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveWorkspaceDir(
|
|
36
|
-
api: OpenClawPluginApi,
|
|
37
|
-
ctx: WorkspaceResolutionContext,
|
|
38
|
-
options: WorkspaceResolutionOptions = {},
|
|
39
|
-
): string | undefined {
|
|
40
|
-
const source = options.source ?? 'unknown';
|
|
41
|
-
const logger = options.logger ?? api.logger;
|
|
42
|
-
const attempts: string[] = [];
|
|
43
|
-
|
|
44
|
-
if (ctx.workspaceDir) {
|
|
45
|
-
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
46
|
-
if (!issue) {
|
|
47
|
-
return ctx.workspaceDir;
|
|
48
|
-
}
|
|
49
|
-
attempts.push(`ctx.workspaceDir invalid (${issue})`);
|
|
50
|
-
} else {
|
|
51
|
-
attempts.push('ctx.workspaceDir missing');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const agentCandidates = [ctx.agentId, options.fallbackAgentId]
|
|
55
|
-
.filter((value, index, all): value is string => !!value && all.indexOf(value) === index);
|
|
56
|
-
|
|
57
|
-
for (const agentId of agentCandidates) {
|
|
58
|
-
const resolved = tryResolveFromAgent(api, agentId, attempts);
|
|
59
|
-
if (resolved) {
|
|
60
|
-
return resolved;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const message = buildResolutionFailureMessage(source, attempts);
|
|
65
|
-
if (options.required) {
|
|
66
|
-
logger.error(message);
|
|
67
|
-
throw new Error(message);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
logger.warn(message);
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function resolveRequiredWorkspaceDir(
|
|
75
|
-
api: OpenClawPluginApi,
|
|
76
|
-
ctx: WorkspaceResolutionContext,
|
|
77
|
-
options: Omit<WorkspaceResolutionOptions, 'required'> = {},
|
|
78
|
-
): string {
|
|
79
|
-
return resolveWorkspaceDir(api, ctx, { ...options, required: true }) as string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Re-export helpers that live in workspace-dir-validation.ts for API compatibility
|
|
83
|
-
export { validateWorkspaceDir } from './workspace-dir-validation.js';
|
|
84
|
-
export type { WorkspaceResolutionContext } from './workspace-dir-validation.js';
|
|
85
|
-
|
|
86
|
-
export function resolveValidWorkspaceDir(
|
|
87
|
-
ctx: WorkspaceResolutionContext,
|
|
88
|
-
api: {
|
|
89
|
-
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
90
|
-
config: unknown;
|
|
91
|
-
logger: PluginLogger;
|
|
92
|
-
},
|
|
93
|
-
options?: { source?: string; fallbackAgentId?: string },
|
|
94
|
-
): string | undefined {
|
|
95
|
-
return resolveWorkspaceDir(api as never, ctx, {
|
|
96
|
-
source: options?.source,
|
|
97
|
-
fallbackAgentId: options?.fallbackAgentId,
|
|
98
|
-
logger: api.logger,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function logWorkspaceDirHealth(
|
|
103
|
-
ctx: WorkspaceResolutionContext,
|
|
104
|
-
source: string,
|
|
105
|
-
api: {
|
|
106
|
-
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
107
|
-
config: unknown;
|
|
108
|
-
logger: PluginLogger;
|
|
109
|
-
},
|
|
110
|
-
): void {
|
|
111
|
-
const resolved = resolveValidWorkspaceDir(ctx, api, { source, fallbackAgentId: 'main' });
|
|
112
|
-
const issue = validateWorkspaceDir(resolved);
|
|
113
|
-
|
|
114
|
-
if (issue) {
|
|
115
|
-
api.logger.error(`[PD:health] ${source}: workspaceDir="${resolved}" - ${issue}`);
|
|
116
|
-
} else {
|
|
117
|
-
api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}" OK`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lifecycle Routing Hook — Natural Language Intent Detection
|
|
3
|
-
* ==========================================================
|
|
4
|
-
*
|
|
5
|
-
* PURPOSE: Detect natural language intent for promotion, disable, and rollback
|
|
6
|
-
* of implementations. Supports both English and Chinese phrases.
|
|
7
|
-
*
|
|
8
|
-
* PATTERN: Extends the existing rollback natural language detection pattern
|
|
9
|
-
* from rollback.ts.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Natural Language Patterns
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
const PROMOTE_PATTERNS_CN = [
|
|
17
|
-
/促[进推]/,
|
|
18
|
-
/启[用用]/,
|
|
19
|
-
/激活/,
|
|
20
|
-
/设为活动/,
|
|
21
|
-
/启用.*实现/,
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const PROMOTE_PATTERNS_EN = [
|
|
25
|
-
/promote\s+(this|the|implementation)/i,
|
|
26
|
-
/activate\s+(this|the|implementation)/i,
|
|
27
|
-
/enable\s+(this|the|implementation)/i,
|
|
28
|
-
/set\s+(as|to)\s+active/i,
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const DISABLE_PATTERNS_CN = [
|
|
32
|
-
/禁[用止]/,
|
|
33
|
-
/关闭.*实现/,
|
|
34
|
-
/停止.*实现/,
|
|
35
|
-
/停用/,
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const DISABLE_PATTERNS_EN = [
|
|
39
|
-
/disable\s+(this|the|implementation)/i,
|
|
40
|
-
/turn\s+off\s+(this|the|implementation)/i,
|
|
41
|
-
/deactivate\s+(this|the|implementation)/i,
|
|
42
|
-
/stop\s+(this|the|implementation)/i,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const ROLLBACK_PATTERNS_CN = [
|
|
46
|
-
/回滚/,
|
|
47
|
-
/撤销.*实现/,
|
|
48
|
-
/恢复.*实现/,
|
|
49
|
-
/退回.*实现/,
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const ROLLBACK_PATTERNS_EN = [
|
|
53
|
-
/rollback\s+(this|the|implementation)/i,
|
|
54
|
-
/revert\s+(this|the|implementation)/i,
|
|
55
|
-
/undo\s+(this|the|implementation)/i,
|
|
56
|
-
/restore\s+(previous|last|implementation)/i,
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Intent Detection
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export type LifecycleIntent = 'promote' | 'disable' | 'rollback' | null;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Detect implementation lifecycle intent from user message.
|
|
67
|
-
* Returns the detected intent type or null.
|
|
68
|
-
*/
|
|
69
|
-
|
|
70
|
-
export function detectLifecycleIntent(message: string): LifecycleIntent {
|
|
71
|
-
// Check promote patterns
|
|
72
|
-
for (const p of PROMOTE_PATTERNS_EN) {
|
|
73
|
-
if (p.test(message)) return 'promote';
|
|
74
|
-
}
|
|
75
|
-
for (const p of PROMOTE_PATTERNS_CN) {
|
|
76
|
-
if (p.test(message)) return 'promote';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check disable patterns
|
|
80
|
-
for (const p of DISABLE_PATTERNS_EN) {
|
|
81
|
-
if (p.test(message)) return 'disable';
|
|
82
|
-
}
|
|
83
|
-
for (const p of DISABLE_PATTERNS_CN) {
|
|
84
|
-
if (p.test(message)) return 'disable';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check rollback patterns
|
|
88
|
-
for (const p of ROLLBACK_PATTERNS_EN) {
|
|
89
|
-
if (p.test(message)) return 'rollback';
|
|
90
|
-
}
|
|
91
|
-
for (const p of ROLLBACK_PATTERNS_CN) {
|
|
92
|
-
if (p.test(message)) return 'rollback';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Route a natural language lifecycle intent to the appropriate command handler.
|
|
100
|
-
* Returns command name and normalized message, or null if no intent detected.
|
|
101
|
-
*/
|
|
102
|
-
export function routeLifecycleIntent(
|
|
103
|
-
message: string
|
|
104
|
-
): { command: string; normalizedMessage: string } | null {
|
|
105
|
-
const intent = detectLifecycleIntent(message);
|
|
106
|
-
if (!intent) return null;
|
|
107
|
-
|
|
108
|
-
switch (intent) {
|
|
109
|
-
case 'promote':
|
|
110
|
-
return {
|
|
111
|
-
command: 'pd-promote-impl',
|
|
112
|
-
normalizedMessage: 'list',
|
|
113
|
-
};
|
|
114
|
-
case 'disable':
|
|
115
|
-
return {
|
|
116
|
-
command: 'pd-disable-impl',
|
|
117
|
-
normalizedMessage: 'list',
|
|
118
|
-
};
|
|
119
|
-
case 'rollback':
|
|
120
|
-
return {
|
|
121
|
-
command: 'pd-rollback-impl',
|
|
122
|
-
normalizedMessage: 'list',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventLog Auditor - Search and verify events across all .state directories
|
|
3
|
-
*
|
|
4
|
-
* This tool addresses a common debugging issue where hook events may be
|
|
5
|
-
* written to the wrong .state directory due to workspaceDir resolution bugs.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const report = await auditEventLogs(openclawDir, ['after_tool_call', 'before_tool_call']);
|
|
9
|
-
* console.log(report.summary);
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import * as os from 'os';
|
|
15
|
-
|
|
16
|
-
interface EventLogEntry {
|
|
17
|
-
ts: string;
|
|
18
|
-
date: string;
|
|
19
|
-
type: string;
|
|
20
|
-
category: string;
|
|
21
|
-
sessionId?: string;
|
|
22
|
-
data: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface LocationReport {
|
|
26
|
-
path: string;
|
|
27
|
-
lastModified: Date | null;
|
|
28
|
-
totalEntries: number;
|
|
29
|
-
hookCounts: Record<string, number>;
|
|
30
|
-
recentEntries: EventLogEntry[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface AuditReport {
|
|
34
|
-
searchedPaths: string[];
|
|
35
|
-
locations: LocationReport[];
|
|
36
|
-
primaryPath: string | null;
|
|
37
|
-
misplacedEvents: { path: string; entries: EventLogEntry[] }[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Find all events.jsonl files under a directory tree.
|
|
42
|
-
*/
|
|
43
|
-
function findEventLogs(baseDir: string, maxDepth = 4): string[] {
|
|
44
|
-
const results: string[] = [];
|
|
45
|
-
|
|
46
|
-
function scan(dir: string, depth: number): void {
|
|
47
|
-
if (depth > maxDepth) return;
|
|
48
|
-
try {
|
|
49
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
50
|
-
for (const entry of entries) {
|
|
51
|
-
if (entry.name === 'events.jsonl') {
|
|
52
|
-
results.push(path.join(dir, entry.name));
|
|
53
|
-
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
54
|
-
scan(path.join(dir, entry.name), depth + 1);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// Permission denied or directory doesn't exist
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
scan(baseDir, 0);
|
|
63
|
-
return results;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Find events.jsonl in well-known locations.
|
|
68
|
-
* Dynamically discovers workspace directories instead of hardcoding names.
|
|
69
|
-
*/
|
|
70
|
-
function findKnownEventLogPaths(): string[] {
|
|
71
|
-
const homeDir = os.homedir();
|
|
72
|
-
const candidates: string[] = [];
|
|
73
|
-
|
|
74
|
-
// Common patterns (legacy, non-workspace paths)
|
|
75
|
-
const legacyPatterns = [
|
|
76
|
-
path.join(homeDir, '.state', 'logs', 'events.jsonl'),
|
|
77
|
-
path.join(homeDir, '.openclaw', '.state', 'logs', 'events.jsonl'),
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
for (const p of legacyPatterns) {
|
|
81
|
-
if (fs.existsSync(p)) {
|
|
82
|
-
candidates.push(p);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
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
|
-
|
|
105
|
-
return candidates;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Read the last N entries from an events.jsonl file.
|
|
110
|
-
*/
|
|
111
|
-
function readRecentEntries(filePath: string, count = 50): EventLogEntry[] {
|
|
112
|
-
try {
|
|
113
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
114
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
115
|
-
const recent = lines.slice(-count);
|
|
116
|
-
return recent.map(line => JSON.parse(line));
|
|
117
|
-
} catch {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Count all hooks in the entire file (for summary).
|
|
124
|
-
*/
|
|
125
|
-
function countAllHooks(filePath: string): Record<string, number> {
|
|
126
|
-
const counts: Record<string, number> = {};
|
|
127
|
-
try {
|
|
128
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
129
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
try {
|
|
132
|
-
const entry = JSON.parse(line) as EventLogEntry;
|
|
133
|
-
if (entry.type === 'hook_execution' && entry.data?.hook) {
|
|
134
|
-
const hook = entry.data.hook as string;
|
|
135
|
-
counts[hook] = (counts[hook] || 0) + 1;
|
|
136
|
-
}
|
|
137
|
-
} catch {
|
|
138
|
-
// Skip malformed lines
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
// File doesn't exist or can't be read
|
|
143
|
-
}
|
|
144
|
-
return counts;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Audit all events.jsonl files.
|
|
149
|
-
*
|
|
150
|
-
* @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
|
|
151
|
-
* @param expectedToolHooks - Hook names that should appear in the primary workspace
|
|
152
|
-
*/
|
|
153
|
-
|
|
154
|
-
export async function auditEventLogs(
|
|
155
|
-
openclawDir: string,
|
|
156
|
-
expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
|
|
157
|
-
): Promise<AuditReport> {
|
|
158
|
-
const homeDir = os.homedir();
|
|
159
|
-
|
|
160
|
-
// Find all event logs
|
|
161
|
-
const knownPaths = findKnownEventLogPaths();
|
|
162
|
-
const scannedPaths = findEventLogs(homeDir, 4);
|
|
163
|
-
const allPaths = [...new Set([...knownPaths, ...scannedPaths])];
|
|
164
|
-
|
|
165
|
-
const locations: LocationReport[] = [];
|
|
166
|
-
let primaryPath: string | null = null;
|
|
167
|
-
|
|
168
|
-
for (const filePath of allPaths) {
|
|
169
|
-
try {
|
|
170
|
-
const stat = fs.statSync(filePath);
|
|
171
|
-
const allCounts = countAllHooks(filePath);
|
|
172
|
-
const recent = readRecentEntries(filePath, 30);
|
|
173
|
-
|
|
174
|
-
locations.push({
|
|
175
|
-
path: filePath,
|
|
176
|
-
lastModified: stat.mtime,
|
|
177
|
-
totalEntries: Object.values(allCounts).reduce((a, b) => a + b, 0),
|
|
178
|
-
hookCounts: allCounts,
|
|
179
|
-
recentEntries: recent,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Determine primary path - prefer configured workspace over workspace-main
|
|
183
|
-
// The configured workspace path is {openclawDir}/workspace (without -main suffix)
|
|
184
|
-
const workspaceDir = path.join(openclawDir, 'workspace') + path.sep;
|
|
185
|
-
const workspaceMainDir = path.join(openclawDir, 'workspace-main') + path.sep;
|
|
186
|
-
|
|
187
|
-
if (filePath.startsWith(workspaceDir)) {
|
|
188
|
-
// Configured workspace (e.g., ~/.openclaw/workspace/) takes priority
|
|
189
|
-
primaryPath = filePath;
|
|
190
|
-
} else if (!primaryPath && filePath.startsWith(workspaceMainDir)) {
|
|
191
|
-
// Fallback to workspace-main only if no configured workspace found yet
|
|
192
|
-
primaryPath = filePath;
|
|
193
|
-
}
|
|
194
|
-
} catch {
|
|
195
|
-
// Skip unreadable files
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// If no primary found, use most recent
|
|
200
|
-
if (!primaryPath && locations.length > 0) {
|
|
201
|
-
locations.sort((a, b) => {
|
|
202
|
-
if (!a.lastModified) return 1;
|
|
203
|
-
if (!b.lastModified) return -1;
|
|
204
|
-
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
205
|
-
});
|
|
206
|
-
primaryPath = locations[0].path;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Detect misplaced tool hook events
|
|
210
|
-
const misplacedEvents: { path: string; entries: EventLogEntry[] }[] = [];
|
|
211
|
-
for (const loc of locations) {
|
|
212
|
-
if (loc.path === primaryPath) continue;
|
|
213
|
-
|
|
214
|
-
const toolHookEntries = loc.recentEntries.filter(e =>
|
|
215
|
-
e.type === 'hook_execution' && expectedToolHooks.includes(e.data?.hook as string)
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (toolHookEntries.length > 0) {
|
|
219
|
-
misplacedEvents.push({ path: loc.path, entries: toolHookEntries });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
searchedPaths: allPaths,
|
|
225
|
-
locations,
|
|
226
|
-
primaryPath,
|
|
227
|
-
misplacedEvents,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Format audit report for display.
|
|
233
|
-
*/
|
|
234
|
-
|
|
235
|
-
export function formatAuditReport(report: AuditReport): string {
|
|
236
|
-
const lines: string[] = [];
|
|
237
|
-
|
|
238
|
-
lines.push('=== Event Log Audit Report ===\n');
|
|
239
|
-
|
|
240
|
-
lines.push(`Searched ${report.searchedPaths.length} paths:\n`);
|
|
241
|
-
for (const p of report.searchedPaths) {
|
|
242
|
-
lines.push(` ${p}`);
|
|
243
|
-
}
|
|
244
|
-
lines.push('');
|
|
245
|
-
|
|
246
|
-
lines.push(`Primary: ${report.primaryPath ?? 'NOT FOUND'}\n`);
|
|
247
|
-
|
|
248
|
-
for (const loc of report.locations) {
|
|
249
|
-
const isPrimary = loc.path === report.primaryPath;
|
|
250
|
-
lines.push(`─── ${isPrimary ? '[PRIMARY]' : '[OTHER] '}${loc.path}`);
|
|
251
|
-
lines.push(` Last modified: ${loc.lastModified?.toISOString() ?? 'never'}`);
|
|
252
|
-
lines.push(` Hook counts:`);
|
|
253
|
-
|
|
254
|
-
const hooks = Object.entries(loc.hookCounts).sort((a, b) => b[1] - a[1]);
|
|
255
|
-
for (const [hook, count] of hooks) {
|
|
256
|
-
lines.push(` ${hook}: ${count}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (hooks.length === 0) {
|
|
260
|
-
lines.push(` (no hooks recorded)`);
|
|
261
|
-
}
|
|
262
|
-
lines.push('');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (report.misplacedEvents.length > 0) {
|
|
266
|
-
lines.push('⚠️ MISPLACED tool hook events detected:');
|
|
267
|
-
for (const me of report.misplacedEvents) {
|
|
268
|
-
lines.push(`\n ${me.path}:`);
|
|
269
|
-
for (const entry of me.entries.slice(0, 5)) {
|
|
270
|
-
lines.push(` ${entry.ts} - ${entry.data.hook}`);
|
|
271
|
-
}
|
|
272
|
-
if (me.entries.length > 5) {
|
|
273
|
-
lines.push(` ... and ${me.entries.length - 5} more`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
lines.push('');
|
|
277
|
-
lines.push('This means tool hooks are writing events to the wrong .state directory.');
|
|
278
|
-
lines.push('Check workspaceDir resolution in the hook handler.');
|
|
279
|
-
} else {
|
|
280
|
-
lines.push('✅ No misplaced tool hook events detected.');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return lines.join('\n');
|
|
284
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evolution Queue Lock Utilities
|
|
3
|
-
*
|
|
4
|
-
* File locking for safe concurrent queue access.
|
|
5
|
-
* Extracted from evolution-worker.ts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { acquireLockAsync, releaseLock as releaseImportedLock, type LockContext } from '../utils/file-lock.js';
|
|
9
|
-
import { LockUnavailableError } from '../config/index.js';
|
|
10
|
-
|
|
11
|
-
export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
12
|
-
export const LOCK_MAX_RETRIES = 50;
|
|
13
|
-
export const LOCK_RETRY_DELAY_MS = 50;
|
|
14
|
-
export const LOCK_STALE_MS = 30_000;
|
|
15
|
-
|
|
16
|
-
export async function acquireQueueLock(
|
|
17
|
-
resourcePath: string,
|
|
18
|
-
logger: { warn?: (message: string) => void; info?: (message: string) => void } | undefined,
|
|
19
|
-
lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX,
|
|
20
|
-
): Promise<() => void> {
|
|
21
|
-
try {
|
|
22
|
-
const ctx: LockContext = await acquireLockAsync(resourcePath, {
|
|
23
|
-
lockSuffix,
|
|
24
|
-
maxRetries: LOCK_MAX_RETRIES,
|
|
25
|
-
baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
|
|
26
|
-
lockStaleMs: LOCK_STALE_MS,
|
|
27
|
-
});
|
|
28
|
-
return () => releaseImportedLock(ctx);
|
|
29
|
-
} catch (error: unknown) {
|
|
30
|
-
const warn = logger?.warn;
|
|
31
|
-
warn?.(`[PD:EvolutionWorker] Failed to acquire lock for ${resourcePath}: ${String(error)}`);
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function requireQueueLock(
|
|
37
|
-
resourcePath: string,
|
|
38
|
-
logger: { warn?: (message: string) => void; info?: (message: string) => void } | undefined,
|
|
39
|
-
scope: string,
|
|
40
|
-
lockSuffix: string = EVOLUTION_QUEUE_LOCK_SUFFIX,
|
|
41
|
-
): Promise<() => void> {
|
|
42
|
-
try {
|
|
43
|
-
return await acquireQueueLock(resourcePath, logger, lockSuffix);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
throw new LockUnavailableError(resourcePath, scope, { cause: err });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Failure Classifier -- Pure stateless classification of task failure patterns
|
|
3
|
-
* ===========================================================
|
|
4
|
-
*
|
|
5
|
-
* Classifies consecutive task failures from the evolution queue as transient
|
|
6
|
-
* or persistent. This module is pure (no file I/O) and testable in isolation.
|
|
7
|
-
*
|
|
8
|
-
* The classifier reads the evolution queue to count consecutive failures per
|
|
9
|
-
* task kind. When the count reaches the configured threshold (default 3),
|
|
10
|
-
* the pattern is classified as "persistent" and the cooldown strategy should
|
|
11
|
-
* be invoked.
|
|
12
|
-
*
|
|
13
|
-
* IMPORTANT: Only `status === 'failed'` counts as a failure.
|
|
14
|
-
* `status === 'completed'` (including stub_fallback and skipped_thin_violation
|
|
15
|
-
* resolutions) counts as a success and breaks the consecutive failure chain.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { EvolutionQueueItem } from './evolution-worker.js';
|
|
19
|
-
|
|
20
|
-
/** Task kinds subject to failure classification.
|
|
21
|
-
* Only sleep_reflection and keyword_optimization have outcome handling in
|
|
22
|
-
* evolution-worker.ts. pain_diagnosis and model_eval are excluded. */
|
|
23
|
-
export type ClassifiableTaskKind = 'sleep_reflection' | 'keyword_optimization';
|
|
24
|
-
|
|
25
|
-
export interface FailureClassificationResult {
|
|
26
|
-
/** Whether the failure pattern is transient or persistent */
|
|
27
|
-
classification: 'transient' | 'persistent';
|
|
28
|
-
/** Number of consecutive failures for this task kind */
|
|
29
|
-
consecutiveFailures: number;
|
|
30
|
-
/** The task kind that was analyzed */
|
|
31
|
-
taskKind: ClassifiableTaskKind;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Classify the failure pattern for a given task kind based on recent task
|
|
36
|
-
* outcomes in the evolution queue.
|
|
37
|
-
*
|
|
38
|
-
* Algorithm:
|
|
39
|
-
* 1. Filter queue to tasks of the specified taskKind with status 'completed' or 'failed'
|
|
40
|
-
* 2. Sort by completed_at (or timestamp fallback) descending (newest first)
|
|
41
|
-
* 3. Count consecutive 'failed' tasks from the top
|
|
42
|
-
* 4. First non-'failed' task (i.e., 'completed') breaks the chain
|
|
43
|
-
* 5. If consecutive count >= threshold, classify as 'persistent'
|
|
44
|
-
*
|
|
45
|
-
* @param queue - Current evolution queue (array of EvolutionQueueItem)
|
|
46
|
-
* @param taskKind - Task kind to classify
|
|
47
|
-
* @param threshold - Consecutive failure threshold for "persistent" (default: 3)
|
|
48
|
-
* @returns FailureClassificationResult
|
|
49
|
-
*/
|
|
50
|
-
export function classifyFailure(
|
|
51
|
-
queue: EvolutionQueueItem[],
|
|
52
|
-
taskKind: ClassifiableTaskKind,
|
|
53
|
-
threshold: number = 3,
|
|
54
|
-
): FailureClassificationResult {
|
|
55
|
-
// Filter to this task kind, only terminal states (completed or failed)
|
|
56
|
-
const relevantTasks = queue
|
|
57
|
-
.filter(t => t.taskKind === taskKind)
|
|
58
|
-
.filter(t => t.status === 'completed' || t.status === 'failed')
|
|
59
|
-
.sort((a, b) => {
|
|
60
|
-
const aTime = new Date(a.completed_at || a.timestamp).getTime();
|
|
61
|
-
const bTime = new Date(b.completed_at || b.timestamp).getTime();
|
|
62
|
-
return bTime - aTime; // newest first
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
let consecutive = 0;
|
|
66
|
-
for (const task of relevantTasks) {
|
|
67
|
-
if (task.status === 'failed') {
|
|
68
|
-
consecutive++;
|
|
69
|
-
} else {
|
|
70
|
-
break; // completed (any resolution including stub_fallback) breaks the chain
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
classification: consecutive >= threshold ? 'persistent' : 'transient',
|
|
76
|
-
consecutiveFailures: consecutive,
|
|
77
|
-
taskKind,
|
|
78
|
-
};
|
|
79
|
-
}
|