principles-disciple 1.6.0 → 1.7.1
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 +134 -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/rollback.js +9 -3
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/commands/trust.js +64 -81
- 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 +7 -1
- package/dist/core/event-log.js +10 -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 +14 -2
- package/dist/core/session-tracker.js +75 -9
- 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 +6 -0
- package/dist/core/trust-engine.js +50 -29
- package/dist/core/workspace-context.d.ts +13 -0
- package/dist/core/workspace-context.js +50 -7
- package/dist/hooks/gate.js +171 -87
- package/dist/hooks/llm.js +119 -71
- 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 +69 -28
- package/dist/hooks/trajectory-collector.d.ts +32 -0
- package/dist/hooks/trajectory-collector.js +256 -0
- 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 +105 -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/empathy-observer-manager.d.ts +2 -0
- package/dist/service/empathy-observer-manager.js +43 -1
- package/dist/service/evolution-worker.d.ts +27 -0
- package/dist/service/evolution-worker.js +256 -41
- package/dist/service/runtime-summary-service.d.ts +79 -0
- package/dist/service/runtime-summary-service.js +319 -0
- 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 +10 -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,151 @@ 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
|
+
export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
14
|
+
export const PAIN_CANDIDATES_LOCK_SUFFIX = '.candidates.lock';
|
|
15
|
+
export const LOCK_MAX_RETRIES = 50;
|
|
16
|
+
export const LOCK_RETRY_DELAY_MS = 50;
|
|
17
|
+
export const LOCK_STALE_MS = 30_000;
|
|
18
|
+
const PAIN_CANDIDATE_MAX_SAMPLES = 5;
|
|
19
|
+
const PAIN_CANDIDATE_SAMPLE_LEN = 1000;
|
|
20
|
+
const PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN = 160;
|
|
21
|
+
const PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN = 80;
|
|
22
|
+
export function createEvolutionTaskId(source, score, preview, reason, now) {
|
|
23
|
+
// Keep ids short for prompt injection, but include enough entropy to avoid
|
|
24
|
+
// collisions between different pain events that share the same source/score/preview.
|
|
25
|
+
return createHash('md5')
|
|
26
|
+
.update(`${source}:${score}:${preview}:${reason}:${now}`)
|
|
27
|
+
.digest('hex')
|
|
28
|
+
.substring(0, 8);
|
|
29
|
+
}
|
|
30
|
+
function normalizePainCandidateText(text) {
|
|
31
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
32
|
+
}
|
|
33
|
+
export function shouldTrackPainCandidate(text) {
|
|
34
|
+
const normalized = normalizePainCandidateText(text);
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return false;
|
|
37
|
+
if (normalized === 'NO_REPLY')
|
|
38
|
+
return false;
|
|
39
|
+
// Skip empathy observer payloads: they are classifier telemetry, not user/system pain patterns.
|
|
40
|
+
if (normalized.startsWith('{')
|
|
41
|
+
&& normalized.endsWith('}')
|
|
42
|
+
&& normalized.includes('"damageDetected"')
|
|
43
|
+
&& normalized.includes('"severity"')
|
|
44
|
+
&& normalized.includes('"confidence"')) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
export function createPainCandidateFingerprint(text) {
|
|
50
|
+
const normalized = normalizePainCandidateText(text);
|
|
51
|
+
const head = normalized.substring(0, PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN);
|
|
52
|
+
const tail = normalized.slice(-PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN);
|
|
53
|
+
return createHash('md5')
|
|
54
|
+
.update(`${normalized.length}:${head}:${tail}`)
|
|
55
|
+
.digest('hex')
|
|
56
|
+
.substring(0, 8);
|
|
57
|
+
}
|
|
58
|
+
export function summarizePainCandidateSample(text) {
|
|
59
|
+
return normalizePainCandidateText(text).substring(0, PAIN_CANDIDATE_SAMPLE_LEN);
|
|
60
|
+
}
|
|
61
|
+
function isPendingPainCandidate(status) {
|
|
62
|
+
return status === undefined || status === 'pending';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Acquire an exclusive file lock for the given resource.
|
|
66
|
+
* Returns a release function. Uses 'wx' flag for atomic exclusive create.
|
|
67
|
+
* Detects stale locks by checking PID and mtime.
|
|
68
|
+
*/
|
|
69
|
+
export function acquireQueueLock(lockPath, logger) {
|
|
70
|
+
let retries = 0;
|
|
71
|
+
while (retries < LOCK_MAX_RETRIES) {
|
|
72
|
+
try {
|
|
73
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
74
|
+
fs.writeSync(fd, `${process.pid}\n${Date.now()}`);
|
|
75
|
+
fs.closeSync(fd);
|
|
76
|
+
return () => {
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(lockPath);
|
|
79
|
+
}
|
|
80
|
+
catch { /* ignore */ }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err.code === 'EEXIST') {
|
|
85
|
+
// Check if lock is stale
|
|
86
|
+
try {
|
|
87
|
+
const stat = fs.statSync(lockPath);
|
|
88
|
+
const content = fs.readFileSync(lockPath, 'utf8').trim();
|
|
89
|
+
const pid = parseInt(content.split('\n')[0] || '0', 10);
|
|
90
|
+
let isStale = false;
|
|
91
|
+
if (pid > 0) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (e.code === 'ESRCH')
|
|
97
|
+
isStale = true;
|
|
98
|
+
}
|
|
99
|
+
if (!isStale && Date.now() - stat.mtimeMs > LOCK_STALE_MS)
|
|
100
|
+
isStale = true;
|
|
101
|
+
}
|
|
102
|
+
else if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
103
|
+
isStale = true;
|
|
104
|
+
}
|
|
105
|
+
if (isStale) {
|
|
106
|
+
fs.unlinkSync(lockPath);
|
|
107
|
+
retries++;
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* stat/read failed, treat as busy */ }
|
|
114
|
+
retries++;
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
while (Date.now() - start < LOCK_RETRY_DELAY_MS) { /* spin */ }
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock after ${LOCK_MAX_RETRIES} retries: ${lockPath}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function normalizePainDedupKey(source, preview, reason) {
|
|
126
|
+
// Include reason in dedup key to match createEvolutionTaskId() behavior
|
|
127
|
+
// Different reasons for the same source/preview should create different tasks
|
|
128
|
+
const normalizedReason = (reason || '').trim().toLowerCase();
|
|
129
|
+
return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
|
|
130
|
+
}
|
|
131
|
+
export function hasRecentDuplicateTask(queue, source, preview, now, reason) {
|
|
132
|
+
const key = normalizePainDedupKey(source, preview, reason);
|
|
133
|
+
return queue.some((task) => {
|
|
134
|
+
if (task.status === 'completed')
|
|
135
|
+
return false;
|
|
136
|
+
const taskTime = new Date(task.timestamp).getTime();
|
|
137
|
+
if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
|
|
138
|
+
return false;
|
|
139
|
+
return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
143
|
+
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
144
|
+
return Object.values(dictionary.getAllRules()).some((rule) => {
|
|
145
|
+
if (rule.status !== 'active')
|
|
146
|
+
return false;
|
|
147
|
+
if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
|
|
148
|
+
return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
|
|
149
|
+
}
|
|
150
|
+
if (rule.type === 'regex' && typeof rule.pattern === 'string') {
|
|
151
|
+
return rule.pattern.trim().toLowerCase() === normalizedPhrase;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
11
156
|
function checkPainFlag(wctx, logger) {
|
|
12
157
|
try {
|
|
13
158
|
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
@@ -37,28 +182,43 @@ function checkPainFlag(wctx, logger) {
|
|
|
37
182
|
if (logger)
|
|
38
183
|
logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
|
|
39
184
|
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
185
|
+
const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
|
|
186
|
+
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
187
|
+
if (!releaseLock)
|
|
188
|
+
return; // Could not acquire lock
|
|
189
|
+
try {
|
|
190
|
+
let queue = [];
|
|
191
|
+
if (fs.existsSync(queuePath)) {
|
|
192
|
+
try {
|
|
193
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (logger)
|
|
197
|
+
logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
|
|
198
|
+
}
|
|
44
199
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
if (hasRecentDuplicateTask(queue, source, preview, now, reason)) {
|
|
202
|
+
logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
|
|
203
|
+
fs.appendFileSync(painFlagPath, `\nstatus: queued\n`, 'utf8');
|
|
204
|
+
return;
|
|
48
205
|
}
|
|
206
|
+
const taskId = createEvolutionTaskId(source, score, preview, reason, now);
|
|
207
|
+
queue.push({
|
|
208
|
+
id: taskId,
|
|
209
|
+
score,
|
|
210
|
+
source,
|
|
211
|
+
reason,
|
|
212
|
+
trigger_text_preview: preview,
|
|
213
|
+
timestamp: new Date(now).toISOString(),
|
|
214
|
+
status: 'pending'
|
|
215
|
+
});
|
|
216
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
217
|
+
fs.appendFileSync(painFlagPath, '\nstatus: queued\n', 'utf8');
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
releaseLock();
|
|
49
221
|
}
|
|
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
222
|
}
|
|
63
223
|
catch (err) {
|
|
64
224
|
if (logger)
|
|
@@ -66,11 +226,23 @@ function checkPainFlag(wctx, logger) {
|
|
|
66
226
|
}
|
|
67
227
|
}
|
|
68
228
|
function processEvolutionQueue(wctx, logger, eventLog) {
|
|
229
|
+
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
230
|
+
if (!fs.existsSync(queuePath))
|
|
231
|
+
return;
|
|
232
|
+
const lockPath = queuePath + EVOLUTION_QUEUE_LOCK_SUFFIX;
|
|
233
|
+
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
234
|
+
if (!releaseLock)
|
|
235
|
+
return; // Could not acquire lock
|
|
69
236
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
237
|
+
let queue = [];
|
|
238
|
+
try {
|
|
239
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (logger)
|
|
243
|
+
logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
|
|
72
244
|
return;
|
|
73
|
-
|
|
245
|
+
}
|
|
74
246
|
let queueChanged = false;
|
|
75
247
|
const config = wctx.config;
|
|
76
248
|
const timeout = config.get('intervals.task_timeout_ms') || (30 * 60 * 1000);
|
|
@@ -116,6 +288,9 @@ function processEvolutionQueue(wctx, logger, eventLog) {
|
|
|
116
288
|
if (logger)
|
|
117
289
|
logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
|
|
118
290
|
}
|
|
291
|
+
finally {
|
|
292
|
+
releaseLock();
|
|
293
|
+
}
|
|
119
294
|
}
|
|
120
295
|
async function processDetectionQueue(wctx, api, eventLog) {
|
|
121
296
|
const logger = api.logger;
|
|
@@ -177,44 +352,80 @@ async function processDetectionQueue(wctx, api, eventLog) {
|
|
|
177
352
|
logger.warn(`[PD:EvolutionWorker] Detection queue failed: ${String(err)}`);
|
|
178
353
|
}
|
|
179
354
|
}
|
|
180
|
-
function trackPainCandidate(text, wctx) {
|
|
355
|
+
export function trackPainCandidate(text, wctx) {
|
|
356
|
+
if (!shouldTrackPainCandidate(text))
|
|
357
|
+
return;
|
|
181
358
|
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
359
|
+
const lockPath = candidatePath + PAIN_CANDIDATES_LOCK_SUFFIX;
|
|
360
|
+
const releaseLock = acquireQueueLock(lockPath, console);
|
|
361
|
+
if (!releaseLock) {
|
|
362
|
+
console.warn('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping track');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
let data = { candidates: {} };
|
|
367
|
+
if (fs.existsSync(candidatePath)) {
|
|
368
|
+
try {
|
|
369
|
+
data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
// Keep going with empty data if parse fails, but log it
|
|
373
|
+
console.error(`[PD:EvolutionWorker] Failed to parse pain candidates: ${String(e)}`);
|
|
374
|
+
}
|
|
186
375
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
376
|
+
const fingerprint = createPainCandidateFingerprint(text);
|
|
377
|
+
const now = new Date().toISOString();
|
|
378
|
+
if (!data.candidates[fingerprint]) {
|
|
379
|
+
data.candidates[fingerprint] = { count: 0, status: 'pending', firstSeen: now, lastSeen: now, samples: [] };
|
|
190
380
|
}
|
|
381
|
+
const cand = data.candidates[fingerprint];
|
|
382
|
+
cand.status = cand.status || 'pending';
|
|
383
|
+
cand.count++;
|
|
384
|
+
cand.lastSeen = now;
|
|
385
|
+
const sample = summarizePainCandidateSample(text);
|
|
386
|
+
if (cand.samples.length < PAIN_CANDIDATE_MAX_SAMPLES && !cand.samples.includes(sample)) {
|
|
387
|
+
cand.samples.push(sample);
|
|
388
|
+
}
|
|
389
|
+
fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
|
|
191
390
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
data.candidates[fingerprint] = { count: 0, firstSeen: new Date().toISOString(), samples: [] };
|
|
391
|
+
finally {
|
|
392
|
+
releaseLock();
|
|
195
393
|
}
|
|
196
|
-
const cand = data.candidates[fingerprint];
|
|
197
|
-
cand.count++;
|
|
198
|
-
if (cand.samples.length < 5)
|
|
199
|
-
cand.samples.push(text.substring(0, 200));
|
|
200
|
-
fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
|
|
201
394
|
}
|
|
202
|
-
function processPromotion(wctx, logger, eventLog) {
|
|
395
|
+
export function processPromotion(wctx, logger, eventLog) {
|
|
203
396
|
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
204
397
|
if (!fs.existsSync(candidatePath))
|
|
205
398
|
return;
|
|
399
|
+
const lockPath = candidatePath + PAIN_CANDIDATES_LOCK_SUFFIX;
|
|
400
|
+
const releaseLock = acquireQueueLock(lockPath, logger);
|
|
401
|
+
if (!releaseLock) {
|
|
402
|
+
logger?.warn?.('[PD:EvolutionWorker] Failed to acquire pain candidates lock, skipping promotion');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
206
405
|
try {
|
|
207
406
|
const config = wctx.config;
|
|
208
407
|
const dictionary = wctx.dictionary;
|
|
209
408
|
const data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
210
409
|
const countThreshold = config.get('thresholds.promotion_count_threshold') || 3;
|
|
211
410
|
let promotedCount = 0;
|
|
411
|
+
let changed = false;
|
|
212
412
|
for (const [fingerprint, cand] of Object.entries(data.candidates)) {
|
|
213
|
-
if (cand.status
|
|
413
|
+
if (isPendingPainCandidate(cand.status) && cand.count >= countThreshold) {
|
|
414
|
+
// Normalize undefined status to 'pending'
|
|
415
|
+
if (cand.status !== 'pending') {
|
|
416
|
+
cand.status = 'pending';
|
|
417
|
+
changed = true;
|
|
418
|
+
}
|
|
214
419
|
const commonPhrases = extractCommonSubstring(cand.samples);
|
|
215
420
|
if (commonPhrases.length > 0) {
|
|
216
421
|
const phrase = commonPhrases[0];
|
|
217
422
|
const ruleId = `P_PROMOTED_${fingerprint.toUpperCase()}`;
|
|
423
|
+
if (hasEquivalentPromotedRule(dictionary, phrase)) {
|
|
424
|
+
cand.status = 'duplicate';
|
|
425
|
+
changed = true;
|
|
426
|
+
logger?.info?.(`[PD:EvolutionWorker] Skipping duplicate promoted rule for candidate ${fingerprint}: ${phrase}`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
218
429
|
if (logger)
|
|
219
430
|
logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
|
|
220
431
|
SystemLogger.log(wctx.workspaceDir, 'RULE_PROMOTED', `Candidate ${fingerprint} promoted to rule ${ruleId}`);
|
|
@@ -226,10 +437,11 @@ function processPromotion(wctx, logger, eventLog) {
|
|
|
226
437
|
});
|
|
227
438
|
cand.status = 'promoted';
|
|
228
439
|
promotedCount++;
|
|
440
|
+
changed = true;
|
|
229
441
|
}
|
|
230
442
|
}
|
|
231
443
|
}
|
|
232
|
-
if (
|
|
444
|
+
if (changed) {
|
|
233
445
|
fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
|
|
234
446
|
}
|
|
235
447
|
}
|
|
@@ -237,6 +449,9 @@ function processPromotion(wctx, logger, eventLog) {
|
|
|
237
449
|
if (logger)
|
|
238
450
|
logger.warn(`[PD:EvolutionWorker] Error during rule promotion: ${String(err)}`);
|
|
239
451
|
}
|
|
452
|
+
finally {
|
|
453
|
+
releaseLock();
|
|
454
|
+
}
|
|
240
455
|
}
|
|
241
456
|
export const EvolutionWorkerService = {
|
|
242
457
|
id: 'principles-evolution-worker',
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type RuntimeDataQuality = 'authoritative' | 'partial';
|
|
2
|
+
export type RuntimeRewardPolicy = 'frozen_all_positive' | 'frozen_atomic_positive_keep_plan_ready';
|
|
3
|
+
interface RuntimeSummarySource {
|
|
4
|
+
source: string;
|
|
5
|
+
score?: number;
|
|
6
|
+
ts?: string;
|
|
7
|
+
confidence?: number;
|
|
8
|
+
origin?: string;
|
|
9
|
+
}
|
|
10
|
+
interface RuntimePainSignal {
|
|
11
|
+
source: string;
|
|
12
|
+
ts: string | null;
|
|
13
|
+
reason: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface RuntimeSummary {
|
|
16
|
+
gfi: {
|
|
17
|
+
current: number | null;
|
|
18
|
+
peak: number | null;
|
|
19
|
+
sources: RuntimeSummarySource[];
|
|
20
|
+
dataQuality: RuntimeDataQuality;
|
|
21
|
+
};
|
|
22
|
+
legacyTrust: {
|
|
23
|
+
score: number | null;
|
|
24
|
+
stage: 1 | 2 | 3 | 4 | null;
|
|
25
|
+
frozen: true;
|
|
26
|
+
lastUpdated: string | null;
|
|
27
|
+
rewardPolicy: RuntimeRewardPolicy;
|
|
28
|
+
};
|
|
29
|
+
evolution: {
|
|
30
|
+
queue: {
|
|
31
|
+
pending: number;
|
|
32
|
+
inProgress: number;
|
|
33
|
+
completed: number;
|
|
34
|
+
};
|
|
35
|
+
directive: {
|
|
36
|
+
exists: boolean;
|
|
37
|
+
active: boolean | null;
|
|
38
|
+
ageSeconds: number | null;
|
|
39
|
+
taskPreview: string | null;
|
|
40
|
+
};
|
|
41
|
+
dataQuality: RuntimeDataQuality;
|
|
42
|
+
};
|
|
43
|
+
pain: {
|
|
44
|
+
activeFlag: boolean;
|
|
45
|
+
activeFlagSource: string | null;
|
|
46
|
+
candidates: number | null;
|
|
47
|
+
lastSignal: RuntimePainSignal | null;
|
|
48
|
+
};
|
|
49
|
+
gate: {
|
|
50
|
+
recentBlocks: number | null;
|
|
51
|
+
recentBypasses: number | null;
|
|
52
|
+
dataQuality: RuntimeDataQuality;
|
|
53
|
+
};
|
|
54
|
+
metadata: {
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
workspaceDir: string;
|
|
57
|
+
sessionId: string | null;
|
|
58
|
+
selectedSessionReason: 'explicit' | 'latest_active' | 'none';
|
|
59
|
+
warnings: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export declare class RuntimeSummaryService {
|
|
63
|
+
static getSummary(workspaceDir: string, options?: {
|
|
64
|
+
sessionId?: string | null;
|
|
65
|
+
}): RuntimeSummary;
|
|
66
|
+
private static readSessions;
|
|
67
|
+
private static selectSession;
|
|
68
|
+
private static mergeSessionSnapshots;
|
|
69
|
+
private static buildQueueStats;
|
|
70
|
+
private static buildDirectiveSummary;
|
|
71
|
+
private static readLegacyTrust;
|
|
72
|
+
private static readEvents;
|
|
73
|
+
private static buildGfiSources;
|
|
74
|
+
private static findLastPainSignal;
|
|
75
|
+
private static buildGateStats;
|
|
76
|
+
private static readJsonFile;
|
|
77
|
+
private static asFiniteNumber;
|
|
78
|
+
}
|
|
79
|
+
export {};
|