principles-disciple 1.6.0 → 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.js +7 -3
- 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.js +9 -6
- package/dist/commands/pain.js +8 -0
- package/dist/commands/principle-rollback.d.ts +4 -0
- package/dist/commands/principle-rollback.js +22 -0
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/core/config.d.ts +5 -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 +2 -1
- package/dist/core/event-log.js +3 -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/path-resolver.js +75 -36
- 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 +117 -48
- package/dist/hooks/llm.js +114 -69
- package/dist/hooks/pain.js +105 -5
- package/dist/hooks/prompt.d.ts +11 -14
- package/dist/hooks/prompt.js +283 -57
- package/dist/hooks/subagent.js +27 -1
- package/dist/http/principles-console-route.d.ts +2 -0
- package/dist/http/principles-console-route.js +257 -0
- package/dist/i18n/commands.js +16 -0
- package/dist/index.js +83 -4
- package/dist/service/control-ui-query-service.d.ts +217 -0
- package/dist/service/control-ui-query-service.js +537 -0
- package/dist/service/evolution-worker.d.ts +9 -0
- package/dist/service/evolution-worker.js +152 -22
- 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 +210 -121
- package/dist/types/event-types.d.ts +9 -2
- package/dist/types.d.ts +10 -0
- package/dist/types.js +5 -0
- package/openclaw.plugin.json +43 -11
- package/package.json +14 -4
- package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
|
@@ -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);
|
|
@@ -116,6 +238,9 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
116
238
|
if (logger)
|
|
117
239
|
logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
|
|
118
240
|
}
|
|
241
|
+
finally {
|
|
242
|
+
releaseLock();
|
|
243
|
+
}
|
|
119
244
|
}
|
|
120
245
|
async function processDetectionQueue(wctx, api, eventLog) {
|
|
121
246
|
const logger = api.logger;
|
|
@@ -215,6 +340,11 @@ function processPromotion(wctx, logger, eventLog) {
|
|
|
215
340
|
if (commonPhrases.length > 0) {
|
|
216
341
|
const phrase = commonPhrases[0];
|
|
217
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
|
+
}
|
|
218
348
|
if (logger)
|
|
219
349
|
logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
|
|
220
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
|