principles-disciple 1.5.4 → 1.7.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/dist/commands/context.d.ts +5 -0
- package/dist/commands/context.js +312 -0
- package/dist/commands/evolution-status.d.ts +4 -0
- package/dist/commands/evolution-status.js +138 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +45 -0
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +582 -0
- package/dist/commands/pain.js +143 -6
- package/dist/commands/principle-rollback.d.ts +4 -0
- package/dist/commands/principle-rollback.js +22 -0
- package/dist/commands/rollback.d.ts +19 -0
- package/dist/commands/rollback.js +119 -0
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/core/config.d.ts +37 -0
- package/dist/core/config.js +47 -0
- package/dist/core/control-ui-db.d.ts +68 -0
- package/dist/core/control-ui-db.js +274 -0
- package/dist/core/detection-funnel.d.ts +1 -1
- package/dist/core/detection-funnel.js +4 -0
- package/dist/core/dictionary.d.ts +2 -0
- package/dist/core/dictionary.js +13 -0
- package/dist/core/event-log.d.ts +22 -1
- package/dist/core/event-log.js +319 -0
- package/dist/core/evolution-engine.d.ts +5 -5
- package/dist/core/evolution-engine.js +18 -18
- package/dist/core/evolution-migration.d.ts +5 -0
- package/dist/core/evolution-migration.js +65 -0
- package/dist/core/evolution-reducer.d.ts +69 -0
- package/dist/core/evolution-reducer.js +369 -0
- package/dist/core/evolution-types.d.ts +103 -0
- package/dist/core/focus-history.d.ts +65 -0
- package/dist/core/focus-history.js +266 -0
- package/dist/core/init.js +30 -7
- package/dist/core/migration.js +0 -2
- package/dist/core/path-resolver.d.ts +3 -0
- package/dist/core/path-resolver.js +90 -31
- package/dist/core/paths.d.ts +7 -8
- package/dist/core/paths.js +48 -40
- package/dist/core/profile.js +1 -1
- package/dist/core/session-tracker.d.ts +4 -0
- package/dist/core/session-tracker.js +15 -0
- package/dist/core/thinking-models.d.ts +38 -0
- package/dist/core/thinking-models.js +170 -0
- package/dist/core/trajectory.d.ts +184 -0
- package/dist/core/trajectory.js +817 -0
- package/dist/core/trust-engine.d.ts +2 -0
- package/dist/core/trust-engine.js +30 -4
- package/dist/core/workspace-context.d.ts +13 -0
- package/dist/core/workspace-context.js +50 -7
- package/dist/hooks/gate.js +301 -30
- package/dist/hooks/llm.d.ts +8 -0
- package/dist/hooks/llm.js +347 -69
- package/dist/hooks/message-sanitize.d.ts +3 -0
- package/dist/hooks/message-sanitize.js +37 -0
- package/dist/hooks/pain.js +105 -5
- package/dist/hooks/prompt.d.ts +20 -11
- package/dist/hooks/prompt.js +558 -158
- package/dist/hooks/subagent.d.ts +9 -2
- package/dist/hooks/subagent.js +40 -3
- package/dist/http/principles-console-route.d.ts +2 -0
- package/dist/http/principles-console-route.js +257 -0
- package/dist/i18n/commands.js +48 -20
- package/dist/index.js +264 -8
- package/dist/service/control-ui-query-service.d.ts +217 -0
- package/dist/service/control-ui-query-service.js +537 -0
- package/dist/service/empathy-observer-manager.d.ts +42 -0
- package/dist/service/empathy-observer-manager.js +147 -0
- package/dist/service/evolution-worker.d.ts +10 -0
- package/dist/service/evolution-worker.js +156 -24
- package/dist/service/trajectory-service.d.ts +2 -0
- package/dist/service/trajectory-service.js +15 -0
- package/dist/tools/agent-spawn.d.ts +27 -6
- package/dist/tools/agent-spawn.js +339 -87
- package/dist/tools/deep-reflect.d.ts +27 -7
- package/dist/tools/deep-reflect.js +282 -113
- package/dist/types/event-types.d.ts +84 -2
- package/dist/types/event-types.js +33 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +24 -1
- package/openclaw.plugin.json +43 -11
- package/package.json +16 -6
- package/templates/langs/zh/core/HEARTBEAT.md +28 -4
- package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
- package/templates/pain_settings.json +54 -2
- package/templates/workspace/.principles/PROFILE.json +2 -0
- package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
2
|
+
import { trackFriction } from '../core/session-tracker.js';
|
|
3
|
+
const OBSERVER_SESSION_PREFIX = 'empathy_obs:';
|
|
4
|
+
export class EmpathyObserverManager {
|
|
5
|
+
static instance;
|
|
6
|
+
sessionLocks = new Map();
|
|
7
|
+
constructor() { }
|
|
8
|
+
static getInstance() {
|
|
9
|
+
if (!EmpathyObserverManager.instance) {
|
|
10
|
+
EmpathyObserverManager.instance = new EmpathyObserverManager();
|
|
11
|
+
}
|
|
12
|
+
return EmpathyObserverManager.instance;
|
|
13
|
+
}
|
|
14
|
+
shouldTrigger(api, sessionId) {
|
|
15
|
+
if (!api || !sessionId)
|
|
16
|
+
return false;
|
|
17
|
+
const enabled = api.config?.empathy_engine?.enabled !== false;
|
|
18
|
+
if (!enabled)
|
|
19
|
+
return false;
|
|
20
|
+
return !this.sessionLocks.has(sessionId);
|
|
21
|
+
}
|
|
22
|
+
async spawn(api, sessionId, userMessage) {
|
|
23
|
+
if (!api)
|
|
24
|
+
return null;
|
|
25
|
+
if (!this.shouldTrigger(api, sessionId))
|
|
26
|
+
return null;
|
|
27
|
+
if (!userMessage?.trim())
|
|
28
|
+
return null;
|
|
29
|
+
const timestamp = Date.now();
|
|
30
|
+
const sessionKey = `${OBSERVER_SESSION_PREFIX}${sessionId}:${timestamp}`;
|
|
31
|
+
this.sessionLocks.set(sessionId, sessionKey);
|
|
32
|
+
const prompt = [
|
|
33
|
+
'You are an empathy observer.',
|
|
34
|
+
'Analyze ONLY the user message and return strict JSON (no markdown):',
|
|
35
|
+
'{"damageDetected": boolean, "severity": "mild|moderate|severe", "confidence": number, "reason": string}',
|
|
36
|
+
`User message: ${JSON.stringify(userMessage.trim())}`,
|
|
37
|
+
].join('\n');
|
|
38
|
+
try {
|
|
39
|
+
await api.runtime.subagent.run({
|
|
40
|
+
sessionKey,
|
|
41
|
+
message: prompt,
|
|
42
|
+
lane: 'subagent',
|
|
43
|
+
deliver: false,
|
|
44
|
+
idempotencyKey: `${sessionId}:${timestamp}`,
|
|
45
|
+
});
|
|
46
|
+
api.logger.info(`[PD:EmpathyObserver] Spawned observer ${sessionKey}`);
|
|
47
|
+
return sessionKey;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.sessionLocks.delete(sessionId);
|
|
51
|
+
api.logger.warn(`[PD:EmpathyObserver] Failed to spawn observer for ${sessionId}: ${String(error)}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async reap(api, targetSessionKey, workspaceDir) {
|
|
56
|
+
if (!api || !workspaceDir || !this.isObserverSession(targetSessionKey))
|
|
57
|
+
return;
|
|
58
|
+
const sessionId = this.extractParentSessionId(targetSessionKey);
|
|
59
|
+
const unlock = () => {
|
|
60
|
+
if (sessionId && this.sessionLocks.get(sessionId) === targetSessionKey) {
|
|
61
|
+
this.sessionLocks.delete(sessionId);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const messages = await api.runtime.subagent.getSessionMessages({
|
|
66
|
+
sessionKey: targetSessionKey,
|
|
67
|
+
limit: 20,
|
|
68
|
+
});
|
|
69
|
+
const rawText = this.extractAssistantText(messages.messages, messages.assistantTexts);
|
|
70
|
+
const parsed = this.parseJsonPayload(rawText, api.logger);
|
|
71
|
+
if (parsed?.damageDetected && sessionId) {
|
|
72
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
73
|
+
const score = this.scoreFromSeverity(parsed.severity, wctx.config);
|
|
74
|
+
trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir);
|
|
75
|
+
api.logger.info(`[PD:EmpathyObserver] Applied GFI +${score} for ${sessionId}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
api.logger.warn(`[PD:EmpathyObserver] Failed to reap ${targetSessionKey}: ${String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
unlock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
isObserverSession(sessionKey) {
|
|
86
|
+
return typeof sessionKey === 'string' && sessionKey.startsWith(OBSERVER_SESSION_PREFIX);
|
|
87
|
+
}
|
|
88
|
+
extractParentSessionId(sessionKey) {
|
|
89
|
+
if (!this.isObserverSession(sessionKey))
|
|
90
|
+
return null;
|
|
91
|
+
const rest = sessionKey.slice(OBSERVER_SESSION_PREFIX.length);
|
|
92
|
+
const marker = rest.lastIndexOf(':');
|
|
93
|
+
if (marker <= 0)
|
|
94
|
+
return null;
|
|
95
|
+
return rest.slice(0, marker);
|
|
96
|
+
}
|
|
97
|
+
parseJsonPayload(rawText, logger) {
|
|
98
|
+
if (!rawText?.trim())
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(rawText.trim());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
const match = rawText.match(/\{[\s\S]*\}/);
|
|
105
|
+
if (!match) {
|
|
106
|
+
logger?.warn('[PD:EmpathyObserver] Observer payload is not valid JSON, skipping.');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(match[0]);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
logger?.warn('[PD:EmpathyObserver] Failed to parse observer JSON payload, skipping.');
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
extractAssistantText(messages, assistantTexts) {
|
|
119
|
+
if (assistantTexts && assistantTexts.length > 0) {
|
|
120
|
+
return assistantTexts[assistantTexts.length - 1] || '';
|
|
121
|
+
}
|
|
122
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
123
|
+
const msg = messages[i];
|
|
124
|
+
if (msg?.role !== 'assistant')
|
|
125
|
+
continue;
|
|
126
|
+
if (typeof msg.content === 'string')
|
|
127
|
+
return msg.content;
|
|
128
|
+
if (Array.isArray(msg.content)) {
|
|
129
|
+
const txt = msg.content
|
|
130
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
131
|
+
.map((part) => part.text)
|
|
132
|
+
.join('\n');
|
|
133
|
+
if (txt)
|
|
134
|
+
return txt;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
scoreFromSeverity(severity, config) {
|
|
140
|
+
if (severity === 'severe')
|
|
141
|
+
return Number(config.get('empathy_engine.penalties.severe') ?? 40);
|
|
142
|
+
if (severity === 'moderate')
|
|
143
|
+
return Number(config.get('empathy_engine.penalties.moderate') ?? 25);
|
|
144
|
+
return Number(config.get('empathy_engine.penalties.mild') ?? 10);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export const empathyObserverManager = EmpathyObserverManager.getInstance();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginServiceContext, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
2
2
|
export interface EvolutionQueueItem {
|
|
3
3
|
id: string;
|
|
4
|
+
task?: string;
|
|
4
5
|
score: number;
|
|
5
6
|
source: string;
|
|
6
7
|
reason: string;
|
|
@@ -8,6 +9,15 @@ export interface EvolutionQueueItem {
|
|
|
8
9
|
trigger_text_preview?: string;
|
|
9
10
|
status: 'pending' | 'in_progress' | 'completed';
|
|
10
11
|
}
|
|
12
|
+
export declare function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number): boolean;
|
|
13
|
+
export declare function hasEquivalentPromotedRule(dictionary: {
|
|
14
|
+
getAllRules(): Record<string, {
|
|
15
|
+
type: string;
|
|
16
|
+
phrases?: string[];
|
|
17
|
+
pattern?: string;
|
|
18
|
+
status: string;
|
|
19
|
+
}>;
|
|
20
|
+
}, phrase: string): boolean;
|
|
11
21
|
export interface ExtendedEvolutionWorkerService {
|
|
12
22
|
id: string;
|
|
13
23
|
api: OpenClawPluginApi | null;
|
|
@@ -8,6 +8,101 @@ import { SystemLogger } from '../core/system-logger.js';
|
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
10
10
|
let intervalId = null;
|
|
11
|
+
const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
|
|
12
|
+
// P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
|
|
13
|
+
const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
14
|
+
const LOCK_MAX_RETRIES = 50;
|
|
15
|
+
const LOCK_RETRY_DELAY_MS = 50;
|
|
16
|
+
const LOCK_STALE_MS = 30_000;
|
|
17
|
+
/**
|
|
18
|
+
* Acquire an exclusive file lock for the given resource.
|
|
19
|
+
* Returns a release function. Uses 'wx' flag for atomic exclusive create.
|
|
20
|
+
* Detects stale locks by checking PID and mtime.
|
|
21
|
+
*/
|
|
22
|
+
function acquireQueueLock(lockPath, logger) {
|
|
23
|
+
let retries = 0;
|
|
24
|
+
while (retries < LOCK_MAX_RETRIES) {
|
|
25
|
+
try {
|
|
26
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
27
|
+
fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
|
|
28
|
+
fs.closeSync(fd);
|
|
29
|
+
return () => {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(lockPath);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err.code === 'EEXIST') {
|
|
38
|
+
// Check if lock is stale
|
|
39
|
+
try {
|
|
40
|
+
const stat = fs.statSync(lockPath);
|
|
41
|
+
const content = fs.readFileSync(lockPath, 'utf8').trim();
|
|
42
|
+
const pid = parseInt(content.split('\n')[0] || '0', 10);
|
|
43
|
+
let isStale = false;
|
|
44
|
+
if (pid > 0) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e.code === 'ESRCH')
|
|
50
|
+
isStale = true;
|
|
51
|
+
}
|
|
52
|
+
if (!isStale && Date.now() - stat.mtimeMs > LOCK_STALE_MS)
|
|
53
|
+
isStale = true;
|
|
54
|
+
}
|
|
55
|
+
else if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
56
|
+
isStale = true;
|
|
57
|
+
}
|
|
58
|
+
if (isStale) {
|
|
59
|
+
fs.unlinkSync(lockPath);
|
|
60
|
+
retries++;
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch { /* stat/read failed, treat as busy */ }
|
|
67
|
+
retries++;
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock after ${LOCK_MAX_RETRIES} retries: ${lockPath}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function normalizePainDedupKey(source, preview) {
|
|
79
|
+
return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}`;
|
|
80
|
+
}
|
|
81
|
+
export function hasRecentDuplicateTask(queue, source, preview, now) {
|
|
82
|
+
const key = normalizePainDedupKey(source, preview);
|
|
83
|
+
return queue.some((task) => {
|
|
84
|
+
if (task.status === 'completed')
|
|
85
|
+
return false;
|
|
86
|
+
const taskTime = new Date(task.timestamp).getTime();
|
|
87
|
+
if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
|
|
88
|
+
return false;
|
|
89
|
+
return normalizePainDedupKey(task.source, task.trigger_text_preview || '') === key;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
93
|
+
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
94
|
+
return Object.values(dictionary.getAllRules()).some((rule) => {
|
|
95
|
+
if (rule.status !== 'active')
|
|
96
|
+
return false;
|
|
97
|
+
if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
|
|
98
|
+
return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
|
|
99
|
+
}
|
|
100
|
+
if (rule.type === 'regex' && typeof rule.pattern === 'string') {
|
|
101
|
+
return rule.pattern.trim().toLowerCase() === normalizedPhrase;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
11
106
|
function checkPainFlag(wctx, logger) {
|
|
12
107
|
try {
|
|
13
108
|
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
@@ -37,28 +132,43 @@ function checkPainFlag(wctx, logger) {
|
|
|
37
132
|
if (logger)
|
|
38
133
|
logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
|
|
39
134
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
135
|
+
const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
|
|
136
|
+
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
137
|
+
if (!releaseLock)
|
|
138
|
+
return; // Could not acquire lock
|
|
139
|
+
try {
|
|
140
|
+
let queue = [];
|
|
141
|
+
if (fs.existsSync(queuePath)) {
|
|
142
|
+
try {
|
|
143
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
if (logger)
|
|
147
|
+
logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
|
|
148
|
+
}
|
|
44
149
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (hasRecentDuplicateTask(queue, source, preview, now)) {
|
|
152
|
+
logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
|
|
153
|
+
fs.appendFileSync(painFlagPath, `\nstatus: queued\n`, 'utf8');
|
|
154
|
+
return;
|
|
48
155
|
}
|
|
156
|
+
const taskId = createHash('md5').update(`${source}:${score}:${preview}`).digest('hex').substring(0, 8);
|
|
157
|
+
queue.push({
|
|
158
|
+
id: taskId,
|
|
159
|
+
score,
|
|
160
|
+
source,
|
|
161
|
+
reason,
|
|
162
|
+
trigger_text_preview: preview,
|
|
163
|
+
timestamp: new Date(now).toISOString(),
|
|
164
|
+
status: 'pending'
|
|
165
|
+
});
|
|
166
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
167
|
+
fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
releaseLock();
|
|
49
171
|
}
|
|
50
|
-
const taskId = createHash('md5').update(`${source}:${score}:${new Date().toISOString()}`).digest('hex').substring(0, 8);
|
|
51
|
-
queue.push({
|
|
52
|
-
id: taskId,
|
|
53
|
-
score,
|
|
54
|
-
source,
|
|
55
|
-
reason,
|
|
56
|
-
trigger_text_preview: preview,
|
|
57
|
-
timestamp: new Date().toISOString(),
|
|
58
|
-
status: 'pending'
|
|
59
|
-
});
|
|
60
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
61
|
-
fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
|
|
62
172
|
}
|
|
63
173
|
catch (err) {
|
|
64
174
|
if (logger)
|
|
@@ -66,11 +176,23 @@ function checkPainFlag(wctx, logger) {
|
|
|
66
176
|
}
|
|
67
177
|
}
|
|
68
178
|
function processEvolutionQueue(wctx, logger, eventLog) {
|
|
179
|
+
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
180
|
+
if (!fs.existsSync(queuePath))
|
|
181
|
+
return;
|
|
182
|
+
const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
|
|
183
|
+
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
184
|
+
if (!releaseLock)
|
|
185
|
+
return; // Could not acquire lock
|
|
69
186
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
187
|
+
let queue = [];
|
|
188
|
+
try {
|
|
189
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (logger)
|
|
193
|
+
logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
|
|
72
194
|
return;
|
|
73
|
-
|
|
195
|
+
}
|
|
74
196
|
let queueChanged = false;
|
|
75
197
|
const config = wctx.config;
|
|
76
198
|
const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
|
|
@@ -89,13 +211,15 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
89
211
|
if (pendingTasks.length > 0) {
|
|
90
212
|
const directivePath = wctx.resolve('EVOLUTION_DIRECTIVE');
|
|
91
213
|
const highestScoreTask = pendingTasks.sort((a, b) => b.score - a.score)[0];
|
|
214
|
+
const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
|
|
215
|
+
`Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
|
|
92
216
|
const directive = {
|
|
93
217
|
active: true,
|
|
94
|
-
task:
|
|
95
|
-
`Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`,
|
|
218
|
+
task: taskDescription,
|
|
96
219
|
timestamp: new Date().toISOString()
|
|
97
220
|
};
|
|
98
221
|
fs.writeFileSync(directivePath, JSON.stringify(directive, null, 2), 'utf8');
|
|
222
|
+
highestScoreTask.task = taskDescription;
|
|
99
223
|
highestScoreTask.status = 'in_progress';
|
|
100
224
|
queueChanged = true;
|
|
101
225
|
if (eventLog) {
|
|
@@ -114,6 +238,9 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
114
238
|
if (logger)
|
|
115
239
|
logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
|
|
116
240
|
}
|
|
241
|
+
finally {
|
|
242
|
+
releaseLock();
|
|
243
|
+
}
|
|
117
244
|
}
|
|
118
245
|
async function processDetectionQueue(wctx, api, eventLog) {
|
|
119
246
|
const logger = api.logger;
|
|
@@ -213,6 +340,11 @@ function processPromotion(wctx, logger, eventLog) {
|
|
|
213
340
|
if (commonPhrases.length > 0) {
|
|
214
341
|
const phrase = commonPhrases[0];
|
|
215
342
|
const ruleId = `P_PROMOTED_${fingerprint.toUpperCase()}`;
|
|
343
|
+
if (hasEquivalentPromotedRule(dictionary, phrase)) {
|
|
344
|
+
cand.status = 'duplicate';
|
|
345
|
+
logger?.info?.(`[PD:EvolutionWorker] Skipping duplicate promoted rule for candidate ${fingerprint}: ${phrase}`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
216
348
|
if (logger)
|
|
217
349
|
logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
|
|
218
350
|
SystemLogger.log(wctx.workspaceDir, 'RULE_PROMOTED', `Candidate ${fingerprint} promoted to rule ${ruleId}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
2
|
+
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
3
|
+
export const TrajectoryService = {
|
|
4
|
+
id: 'principles-disciple-trajectory',
|
|
5
|
+
start(ctx) {
|
|
6
|
+
if (!ctx.workspaceDir)
|
|
7
|
+
return;
|
|
8
|
+
WorkspaceContext.fromHookContext(ctx).trajectory;
|
|
9
|
+
},
|
|
10
|
+
stop(ctx) {
|
|
11
|
+
if (!ctx.workspaceDir)
|
|
12
|
+
return;
|
|
13
|
+
TrajectoryRegistry.dispose(ctx.workspaceDir);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -6,22 +6,43 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
8
8
|
/**
|
|
9
|
-
* Agent Spawn Tool
|
|
9
|
+
* Create Agent Spawn Tool
|
|
10
|
+
*
|
|
11
|
+
* Uses factory pattern to capture `api` in closure, following OpenClaw plugin SDK conventions.
|
|
12
|
+
* The execute signature must be: async (_toolCallId: string, rawParams: Record<string, unknown>)
|
|
10
13
|
*/
|
|
11
|
-
export declare
|
|
14
|
+
export declare function createAgentSpawnTool(api: OpenClawPluginApi): {
|
|
12
15
|
name: string;
|
|
13
16
|
description: string;
|
|
14
17
|
parameters: import("@sinclair/typebox").TObject<{
|
|
15
18
|
agentType: import("@sinclair/typebox").TString;
|
|
16
19
|
task: import("@sinclair/typebox").TString;
|
|
20
|
+
runInBackground: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
17
21
|
}>;
|
|
18
22
|
/**
|
|
19
23
|
* Execution logic for the agent spawn tool
|
|
24
|
+
*
|
|
25
|
+
* OpenClaw tool execute signature:
|
|
26
|
+
* - First parameter: _toolCallId (string) - the tool call ID
|
|
27
|
+
* - Second parameter: rawParams (Record<string, unknown>) - the actual parameters
|
|
28
|
+
* - Third parameter (optional): signal (AbortSignal) - for cancellation
|
|
20
29
|
*/
|
|
21
|
-
execute(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
execute(_toolCallId: string, rawParams: Record<string, unknown>): Promise<{
|
|
31
|
+
content: Array<{
|
|
32
|
+
type: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
export declare const agentSpawnTool: {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
parameters: import("@sinclair/typebox").TObject<{
|
|
41
|
+
agentType: import("@sinclair/typebox").TString;
|
|
42
|
+
task: import("@sinclair/typebox").TString;
|
|
43
|
+
runInBackground: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
44
|
+
}>;
|
|
45
|
+
execute: () => never;
|
|
25
46
|
};
|
|
26
47
|
/**
|
|
27
48
|
* Batch spawn multiple agents in sequence
|