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.
Files changed (67) hide show
  1. package/dist/commands/context.js +7 -3
  2. package/dist/commands/evolution-status.d.ts +4 -0
  3. package/dist/commands/evolution-status.js +138 -0
  4. package/dist/commands/export.d.ts +2 -0
  5. package/dist/commands/export.js +45 -0
  6. package/dist/commands/focus.js +9 -6
  7. package/dist/commands/pain.js +8 -0
  8. package/dist/commands/principle-rollback.d.ts +4 -0
  9. package/dist/commands/principle-rollback.js +22 -0
  10. package/dist/commands/samples.d.ts +2 -0
  11. package/dist/commands/samples.js +55 -0
  12. package/dist/core/config.d.ts +5 -0
  13. package/dist/core/control-ui-db.d.ts +68 -0
  14. package/dist/core/control-ui-db.js +274 -0
  15. package/dist/core/detection-funnel.d.ts +1 -1
  16. package/dist/core/detection-funnel.js +4 -0
  17. package/dist/core/dictionary.d.ts +2 -0
  18. package/dist/core/dictionary.js +13 -0
  19. package/dist/core/event-log.d.ts +2 -1
  20. package/dist/core/event-log.js +3 -0
  21. package/dist/core/evolution-engine.d.ts +5 -5
  22. package/dist/core/evolution-engine.js +18 -18
  23. package/dist/core/evolution-migration.d.ts +5 -0
  24. package/dist/core/evolution-migration.js +65 -0
  25. package/dist/core/evolution-reducer.d.ts +69 -0
  26. package/dist/core/evolution-reducer.js +369 -0
  27. package/dist/core/evolution-types.d.ts +103 -0
  28. package/dist/core/path-resolver.js +75 -36
  29. package/dist/core/paths.d.ts +7 -8
  30. package/dist/core/paths.js +48 -40
  31. package/dist/core/profile.js +1 -1
  32. package/dist/core/session-tracker.d.ts +4 -0
  33. package/dist/core/session-tracker.js +15 -0
  34. package/dist/core/thinking-models.d.ts +38 -0
  35. package/dist/core/thinking-models.js +170 -0
  36. package/dist/core/trajectory.d.ts +184 -0
  37. package/dist/core/trajectory.js +817 -0
  38. package/dist/core/trust-engine.d.ts +2 -0
  39. package/dist/core/trust-engine.js +30 -4
  40. package/dist/core/workspace-context.d.ts +13 -0
  41. package/dist/core/workspace-context.js +50 -7
  42. package/dist/hooks/gate.js +117 -48
  43. package/dist/hooks/llm.js +114 -69
  44. package/dist/hooks/pain.js +105 -5
  45. package/dist/hooks/prompt.d.ts +11 -14
  46. package/dist/hooks/prompt.js +283 -57
  47. package/dist/hooks/subagent.js +27 -1
  48. package/dist/http/principles-console-route.d.ts +2 -0
  49. package/dist/http/principles-console-route.js +257 -0
  50. package/dist/i18n/commands.js +16 -0
  51. package/dist/index.js +83 -4
  52. package/dist/service/control-ui-query-service.d.ts +217 -0
  53. package/dist/service/control-ui-query-service.js +537 -0
  54. package/dist/service/evolution-worker.d.ts +9 -0
  55. package/dist/service/evolution-worker.js +152 -22
  56. package/dist/service/trajectory-service.d.ts +2 -0
  57. package/dist/service/trajectory-service.js +15 -0
  58. package/dist/tools/agent-spawn.d.ts +27 -6
  59. package/dist/tools/agent-spawn.js +339 -87
  60. package/dist/tools/deep-reflect.d.ts +27 -7
  61. package/dist/tools/deep-reflect.js +210 -121
  62. package/dist/types/event-types.d.ts +9 -2
  63. package/dist/types.d.ts +10 -0
  64. package/dist/types.js +5 -0
  65. package/openclaw.plugin.json +43 -11
  66. package/package.json +14 -4
  67. 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
- let queue = [];
41
- if (fs.existsSync(queuePath)) {
42
- try {
43
- queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
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
- catch (e) {
46
- if (logger)
47
- logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
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
- const queuePath = wctx.resolve('EVOLUTION_QUEUE');
71
- if (!fs.existsSync(queuePath))
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
- const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
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,2 @@
1
+ import type { OpenClawPluginService } from '../openclaw-sdk.js';
2
+ export declare const TrajectoryService: OpenClawPluginService;
@@ -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 definition
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 const agentSpawnTool: {
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(params: {
22
- agentType: string;
23
- task: string;
24
- }, api: OpenClawPluginApi, _workspaceDir?: string): Promise<string>;
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