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.
Files changed (75) 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 +134 -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/rollback.js +9 -3
  11. package/dist/commands/samples.d.ts +2 -0
  12. package/dist/commands/samples.js +55 -0
  13. package/dist/commands/trust.js +64 -81
  14. package/dist/core/config.d.ts +5 -0
  15. package/dist/core/control-ui-db.d.ts +68 -0
  16. package/dist/core/control-ui-db.js +274 -0
  17. package/dist/core/detection-funnel.d.ts +1 -1
  18. package/dist/core/detection-funnel.js +4 -0
  19. package/dist/core/dictionary.d.ts +2 -0
  20. package/dist/core/dictionary.js +13 -0
  21. package/dist/core/event-log.d.ts +7 -1
  22. package/dist/core/event-log.js +10 -0
  23. package/dist/core/evolution-engine.d.ts +5 -5
  24. package/dist/core/evolution-engine.js +18 -18
  25. package/dist/core/evolution-migration.d.ts +5 -0
  26. package/dist/core/evolution-migration.js +65 -0
  27. package/dist/core/evolution-reducer.d.ts +69 -0
  28. package/dist/core/evolution-reducer.js +369 -0
  29. package/dist/core/evolution-types.d.ts +103 -0
  30. package/dist/core/path-resolver.js +75 -36
  31. package/dist/core/paths.d.ts +7 -8
  32. package/dist/core/paths.js +48 -40
  33. package/dist/core/profile.js +1 -1
  34. package/dist/core/session-tracker.d.ts +14 -2
  35. package/dist/core/session-tracker.js +75 -9
  36. package/dist/core/thinking-models.d.ts +38 -0
  37. package/dist/core/thinking-models.js +170 -0
  38. package/dist/core/trajectory.d.ts +184 -0
  39. package/dist/core/trajectory.js +817 -0
  40. package/dist/core/trust-engine.d.ts +6 -0
  41. package/dist/core/trust-engine.js +50 -29
  42. package/dist/core/workspace-context.d.ts +13 -0
  43. package/dist/core/workspace-context.js +50 -7
  44. package/dist/hooks/gate.js +171 -87
  45. package/dist/hooks/llm.js +119 -71
  46. package/dist/hooks/pain.js +105 -5
  47. package/dist/hooks/prompt.d.ts +11 -14
  48. package/dist/hooks/prompt.js +283 -57
  49. package/dist/hooks/subagent.js +69 -28
  50. package/dist/hooks/trajectory-collector.d.ts +32 -0
  51. package/dist/hooks/trajectory-collector.js +256 -0
  52. package/dist/http/principles-console-route.d.ts +2 -0
  53. package/dist/http/principles-console-route.js +257 -0
  54. package/dist/i18n/commands.js +16 -0
  55. package/dist/index.js +105 -4
  56. package/dist/service/control-ui-query-service.d.ts +217 -0
  57. package/dist/service/control-ui-query-service.js +537 -0
  58. package/dist/service/empathy-observer-manager.d.ts +2 -0
  59. package/dist/service/empathy-observer-manager.js +43 -1
  60. package/dist/service/evolution-worker.d.ts +27 -0
  61. package/dist/service/evolution-worker.js +256 -41
  62. package/dist/service/runtime-summary-service.d.ts +79 -0
  63. package/dist/service/runtime-summary-service.js +319 -0
  64. package/dist/service/trajectory-service.d.ts +2 -0
  65. package/dist/service/trajectory-service.js +15 -0
  66. package/dist/tools/agent-spawn.d.ts +27 -6
  67. package/dist/tools/agent-spawn.js +339 -87
  68. package/dist/tools/deep-reflect.d.ts +27 -7
  69. package/dist/tools/deep-reflect.js +210 -121
  70. package/dist/types/event-types.d.ts +10 -2
  71. package/dist/types.d.ts +10 -0
  72. package/dist/types.js +5 -0
  73. package/openclaw.plugin.json +43 -11
  74. package/package.json +14 -4
  75. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
@@ -0,0 +1,537 @@
1
+ import { ControlUiDatabase } from '../core/control-ui-db.js';
2
+ import { getThinkingModel, listThinkingModels } from '../core/thinking-models.js';
3
+ import { WorkspaceContext } from '../core/workspace-context.js';
4
+ /** Time window (in minutes) for querying principle events related to a sample */
5
+ const PRINCIPLE_EVENT_WINDOW_MINUTES = 10;
6
+ function parseJson(raw, fallback) {
7
+ if (!raw)
8
+ return fallback;
9
+ try {
10
+ return JSON.parse(raw);
11
+ }
12
+ catch {
13
+ return fallback;
14
+ }
15
+ }
16
+ function roundRate(numerator, denominator) {
17
+ if (!denominator)
18
+ return 0;
19
+ return Number((numerator / denominator).toFixed(4));
20
+ }
21
+ function clampPageSize(input) {
22
+ if (!Number.isFinite(input))
23
+ return 20;
24
+ return Math.min(100, Math.max(1, Number(input)));
25
+ }
26
+ function summarizeRecommendation(model) {
27
+ if (model.hits === 0)
28
+ return 'archive';
29
+ if (model.failureRate > model.successRate || model.correctionRate >= 0.35 || model.painRate >= 0.3) {
30
+ return 'rework';
31
+ }
32
+ return 'reinforce';
33
+ }
34
+ export class ControlUiQueryService {
35
+ workspaceDir;
36
+ trajectory;
37
+ uiDb;
38
+ constructor(workspaceDir) {
39
+ this.workspaceDir = workspaceDir;
40
+ this.trajectory = WorkspaceContext.fromHookContext({ workspaceDir }).trajectory;
41
+ this.uiDb = new ControlUiDatabase({ workspaceDir });
42
+ }
43
+ dispose() {
44
+ this.uiDb.dispose();
45
+ }
46
+ getOverview() {
47
+ const stats = this.trajectory.getDataStats();
48
+ const regressionRows = this.uiDb.all('SELECT tool_name, error_type, occurrences FROM v_error_clusters ORDER BY occurrences DESC LIMIT 5');
49
+ const failureStats = this.uiDb.get(`
50
+ SELECT
51
+ COALESCE(SUM(occurrences), 0) AS total_failures,
52
+ COALESCE(SUM(CASE WHEN occurrences > 1 THEN occurrences ELSE 0 END), 0) AS repeated_failures
53
+ FROM v_error_clusters
54
+ `) ?? { total_failures: 0, repeated_failures: 0 };
55
+ const correctionTotal = this.uiDb.get('SELECT COUNT(*) AS count FROM user_turns WHERE correction_detected = 1')?.count ?? 0;
56
+ const principleEventCount = this.uiDb.get('SELECT COUNT(*) AS count FROM principle_events')?.count ?? 0;
57
+ const sampleCounters = this.uiDb.all('SELECT review_status, total FROM v_sample_queue');
58
+ const samplePreview = this.uiDb.all(`
59
+ SELECT sample_id, session_id, quality_score, review_status, created_at
60
+ FROM correction_samples
61
+ ORDER BY created_at DESC
62
+ LIMIT 5
63
+ `);
64
+ const coverageRow = this.uiDb.get(`
65
+ SELECT
66
+ COUNT(DISTINCT assistant_turn_id) AS thinking_turns,
67
+ (SELECT COUNT(*) FROM assistant_turns) AS assistant_turns
68
+ FROM thinking_model_events
69
+ `) ?? { thinking_turns: 0, assistant_turns: 0 };
70
+ const effectiveCount = this.uiDb.all('SELECT events, success_windows, failure_windows, pain_windows, correction_windows FROM v_thinking_model_effectiveness')
71
+ .filter((row) => summarizeRecommendation({
72
+ hits: Number(row.events),
73
+ successRate: roundRate(Number(row.success_windows), Number(row.events)),
74
+ failureRate: roundRate(Number(row.failure_windows), Number(row.events)),
75
+ painRate: roundRate(Number(row.pain_windows), Number(row.events)),
76
+ correctionRate: roundRate(Number(row.correction_windows), Number(row.events)),
77
+ }) === 'reinforce').length;
78
+ const dailyTrend = this.uiDb.all(`
79
+ WITH thinking_daily AS (
80
+ SELECT substr(created_at, 1, 10) AS day, COUNT(DISTINCT assistant_turn_id) AS thinking_turns
81
+ FROM thinking_model_events
82
+ GROUP BY substr(created_at, 1, 10)
83
+ )
84
+ SELECT
85
+ dm.day AS day,
86
+ dm.tool_calls AS tool_calls,
87
+ dm.failures AS failures,
88
+ dm.user_corrections AS user_corrections,
89
+ COALESCE(td.thinking_turns, 0) AS thinking_turns
90
+ FROM v_daily_metrics dm
91
+ LEFT JOIN thinking_daily td ON td.day = dm.day
92
+ ORDER BY dm.day DESC
93
+ LIMIT 14
94
+ `).reverse();
95
+ const counters = Object.fromEntries(sampleCounters.map((row) => [row.review_status, Number(row.total)]));
96
+ const activeModels = this.uiDb.get('SELECT COUNT(DISTINCT model_id) AS count FROM thinking_model_events')?.count ?? 0;
97
+ return {
98
+ workspaceDir: this.workspaceDir,
99
+ generatedAt: new Date().toISOString(),
100
+ dataFreshness: stats.lastIngestAt,
101
+ summary: {
102
+ repeatErrorRate: roundRate(Number(failureStats.repeated_failures), Number(failureStats.total_failures)),
103
+ userCorrectionRate: roundRate(correctionTotal, stats.userTurns),
104
+ pendingSamples: stats.pendingSamples,
105
+ approvedSamples: stats.approvedSamples,
106
+ thinkingCoverageRate: roundRate(coverageRow.thinking_turns, coverageRow.assistant_turns),
107
+ painEvents: stats.painEvents,
108
+ principleEventCount,
109
+ },
110
+ dailyTrend: dailyTrend.map((row) => ({
111
+ day: row.day,
112
+ toolCalls: Number(row.tool_calls),
113
+ failures: Number(row.failures),
114
+ userCorrections: Number(row.user_corrections),
115
+ thinkingTurns: Number(row.thinking_turns),
116
+ })),
117
+ topRegressions: regressionRows.map((row) => ({
118
+ toolName: row.tool_name,
119
+ errorType: row.error_type,
120
+ occurrences: Number(row.occurrences),
121
+ })),
122
+ sampleQueue: {
123
+ counters,
124
+ preview: samplePreview.map((row) => ({
125
+ sampleId: row.sample_id,
126
+ sessionId: row.session_id,
127
+ qualityScore: Number(row.quality_score),
128
+ reviewStatus: row.review_status,
129
+ createdAt: row.created_at,
130
+ })),
131
+ },
132
+ thinkingSummary: {
133
+ activeModels,
134
+ dormantModels: Math.max(0, listThinkingModels().length - activeModels),
135
+ effectiveModels: effectiveCount,
136
+ coverageRate: roundRate(coverageRow.thinking_turns, coverageRow.assistant_turns),
137
+ },
138
+ };
139
+ }
140
+ listSamples(filters = {}) {
141
+ const page = Math.max(1, Number(filters.page ?? 1));
142
+ const pageSize = clampPageSize(filters.pageSize);
143
+ const offset = (page - 1) * pageSize;
144
+ const where = [];
145
+ const params = [];
146
+ if (filters.status && filters.status !== 'all') {
147
+ where.push('cs.review_status = ?');
148
+ params.push(filters.status);
149
+ }
150
+ if (Number.isFinite(filters.qualityMin)) {
151
+ where.push('cs.quality_score >= ?');
152
+ params.push(Number(filters.qualityMin));
153
+ }
154
+ if (filters.dateFrom) {
155
+ where.push('cs.created_at >= ?');
156
+ params.push(filters.dateFrom);
157
+ }
158
+ if (filters.dateTo) {
159
+ where.push('cs.created_at <= ?');
160
+ params.push(filters.dateTo);
161
+ }
162
+ if (filters.failureMode) {
163
+ where.push(`
164
+ COALESCE(
165
+ (
166
+ SELECT COALESCE(tc.error_type, tc.tool_name)
167
+ FROM tool_calls tc
168
+ WHERE tc.session_id = cs.session_id
169
+ AND tc.outcome = 'failure'
170
+ AND tc.created_at <= ut.created_at
171
+ ORDER BY tc.created_at DESC
172
+ LIMIT 1
173
+ ),
174
+ 'unknown'
175
+ ) = ?
176
+ `);
177
+ params.push(filters.failureMode);
178
+ }
179
+ const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
180
+ const total = Number(this.uiDb.get(`
181
+ SELECT COUNT(*) AS count
182
+ FROM correction_samples cs
183
+ JOIN user_turns ut ON ut.id = cs.user_correction_turn_id
184
+ ${whereClause}
185
+ `, ...params)?.count ?? 0);
186
+ const items = this.uiDb.all(`
187
+ SELECT
188
+ cs.sample_id,
189
+ cs.session_id,
190
+ cs.review_status,
191
+ cs.quality_score,
192
+ cs.created_at,
193
+ cs.updated_at,
194
+ cs.diff_excerpt,
195
+ COALESCE(
196
+ (
197
+ SELECT COALESCE(tc.error_type, tc.tool_name)
198
+ FROM tool_calls tc
199
+ WHERE tc.session_id = cs.session_id
200
+ AND tc.outcome = 'failure'
201
+ AND tc.created_at <= ut.created_at
202
+ ORDER BY tc.created_at DESC
203
+ LIMIT 1
204
+ ),
205
+ 'unknown'
206
+ ) AS failure_mode,
207
+ (
208
+ SELECT COUNT(*)
209
+ FROM thinking_model_events tme
210
+ JOIN assistant_turns at2 ON at2.id = cs.bad_assistant_turn_id
211
+ WHERE tme.session_id = cs.session_id
212
+ AND tme.created_at >= at2.created_at
213
+ AND tme.created_at <= ut.created_at
214
+ ) AS related_thinking_count
215
+ FROM correction_samples cs
216
+ JOIN user_turns ut ON ut.id = cs.user_correction_turn_id
217
+ ${whereClause}
218
+ ORDER BY cs.created_at DESC
219
+ LIMIT ? OFFSET ?
220
+ `, ...params, pageSize, offset);
221
+ const counters = this.uiDb.all(`
222
+ SELECT review_status, COUNT(*) AS count
223
+ FROM correction_samples
224
+ GROUP BY review_status
225
+ `);
226
+ return {
227
+ counters: Object.fromEntries(counters.map((row) => [row.review_status, Number(row.count)])),
228
+ items: items.map((row) => ({
229
+ sampleId: row.sample_id,
230
+ sessionId: row.session_id,
231
+ reviewStatus: row.review_status,
232
+ qualityScore: Number(row.quality_score),
233
+ failureMode: row.failure_mode,
234
+ relatedThinkingCount: Number(row.related_thinking_count),
235
+ createdAt: row.created_at,
236
+ updatedAt: row.updated_at,
237
+ diffExcerpt: row.diff_excerpt,
238
+ })),
239
+ pagination: {
240
+ page,
241
+ pageSize,
242
+ total,
243
+ totalPages: total === 0 ? 0 : Math.ceil(total / pageSize),
244
+ },
245
+ };
246
+ }
247
+ getSampleDetail(sampleId) {
248
+ const row = this.uiDb.get(`
249
+ SELECT
250
+ cs.sample_id,
251
+ cs.session_id,
252
+ cs.review_status,
253
+ cs.quality_score,
254
+ cs.created_at,
255
+ cs.updated_at,
256
+ cs.recovery_tool_span_json,
257
+ cs.principle_ids_json,
258
+ at.id AS bad_turn_id,
259
+ at.raw_text AS bad_raw_text,
260
+ at.blob_ref AS bad_blob_ref,
261
+ at.sanitized_text AS bad_sanitized_text,
262
+ at.created_at AS bad_created_at,
263
+ ut.id AS user_turn_id,
264
+ ut.raw_text AS user_raw_text,
265
+ ut.blob_ref AS user_blob_ref,
266
+ ut.correction_cue AS user_correction_cue,
267
+ ut.created_at AS user_created_at
268
+ FROM correction_samples cs
269
+ JOIN assistant_turns at ON at.id = cs.bad_assistant_turn_id
270
+ JOIN user_turns ut ON ut.id = cs.user_correction_turn_id
271
+ WHERE cs.sample_id = ?
272
+ `, sampleId);
273
+ if (!row)
274
+ return null;
275
+ const reviewHistory = this.uiDb.all(`
276
+ SELECT review_status, note, created_at
277
+ FROM sample_reviews
278
+ WHERE sample_id = ?
279
+ ORDER BY created_at DESC
280
+ `, sampleId);
281
+ const relatedThinkingHits = this.uiDb.all(`
282
+ SELECT id, model_id, matched_pattern, scenario_json, created_at, trigger_excerpt
283
+ FROM thinking_model_events
284
+ WHERE session_id = ?
285
+ AND created_at >= ?
286
+ AND created_at <= ?
287
+ ORDER BY created_at DESC
288
+ LIMIT 20
289
+ `, row.session_id, row.bad_created_at, row.user_created_at);
290
+ const relatedPrinciples = this.uiDb.all(`
291
+ SELECT principle_id, event_type, created_at
292
+ FROM principle_events
293
+ WHERE created_at >= ?
294
+ AND created_at <= datetime(?, '+' || ? || ' minutes')
295
+ ORDER BY created_at DESC
296
+ LIMIT 20
297
+ `, row.bad_created_at, row.user_created_at, PRINCIPLE_EVENT_WINDOW_MINUTES);
298
+ const seededPrincipleIds = parseJson(row.principle_ids_json, []).map((principleId) => ({
299
+ principleId,
300
+ eventType: 'seeded_from_sample',
301
+ createdAt: row.created_at,
302
+ }));
303
+ return {
304
+ sampleId: row.sample_id,
305
+ sessionId: row.session_id,
306
+ reviewStatus: row.review_status,
307
+ qualityScore: Number(row.quality_score),
308
+ createdAt: row.created_at,
309
+ updatedAt: row.updated_at,
310
+ badAttempt: {
311
+ assistantTurnId: Number(row.bad_turn_id),
312
+ rawText: this.uiDb.restoreRawText(row.bad_raw_text, row.bad_blob_ref),
313
+ sanitizedText: row.bad_sanitized_text,
314
+ createdAt: row.bad_created_at,
315
+ },
316
+ userCorrection: {
317
+ userTurnId: Number(row.user_turn_id),
318
+ rawText: this.uiDb.restoreRawText(row.user_raw_text, row.user_blob_ref),
319
+ correctionCue: row.user_correction_cue,
320
+ createdAt: row.user_created_at,
321
+ },
322
+ recoveryToolSpan: parseJson(row.recovery_tool_span_json, []),
323
+ relatedPrinciples: [
324
+ ...seededPrincipleIds,
325
+ ...relatedPrinciples.map((item) => ({
326
+ principleId: item.principle_id,
327
+ eventType: item.event_type,
328
+ createdAt: item.created_at,
329
+ })),
330
+ ],
331
+ relatedThinkingHits: relatedThinkingHits.map((item) => ({
332
+ id: Number(item.id),
333
+ modelId: item.model_id,
334
+ modelName: getThinkingModel(item.model_id)?.name ?? item.model_id,
335
+ matchedPattern: item.matched_pattern,
336
+ scenarios: parseJson(item.scenario_json, []),
337
+ createdAt: item.created_at,
338
+ triggerExcerpt: item.trigger_excerpt,
339
+ })),
340
+ reviewHistory: reviewHistory.map((item) => ({
341
+ reviewStatus: item.review_status,
342
+ note: item.note,
343
+ createdAt: item.created_at,
344
+ })),
345
+ };
346
+ }
347
+ reviewSample(sampleId, decision, note) {
348
+ return this.trajectory.reviewCorrectionSample(sampleId, decision, note);
349
+ }
350
+ exportCorrections(mode) {
351
+ return this.trajectory.exportCorrections({ mode, approvedOnly: true });
352
+ }
353
+ getThinkingOverview() {
354
+ const topModels = this.loadThinkingModelSummaries();
355
+ const knownModels = listThinkingModels();
356
+ const activeIds = new Set(topModels.filter((model) => model.hits > 0).map((model) => model.modelId));
357
+ const dormantModels = knownModels
358
+ .filter((model) => !activeIds.has(model.id))
359
+ .map((model) => ({
360
+ modelId: model.id,
361
+ name: model.name,
362
+ description: model.description,
363
+ }));
364
+ const coverageRow = this.uiDb.get(`
365
+ SELECT
366
+ COUNT(DISTINCT assistant_turn_id) AS thinking_turns,
367
+ (SELECT COUNT(*) FROM assistant_turns) AS assistant_turns
368
+ FROM thinking_model_events
369
+ `) ?? { thinking_turns: 0, assistant_turns: 0 };
370
+ const coverageTrend = this.uiDb.all(`
371
+ WITH assistant_daily AS (
372
+ SELECT substr(created_at, 1, 10) AS day, COUNT(*) AS assistant_turns
373
+ FROM assistant_turns
374
+ GROUP BY substr(created_at, 1, 10)
375
+ ),
376
+ thinking_daily AS (
377
+ SELECT substr(created_at, 1, 10) AS day, COUNT(DISTINCT assistant_turn_id) AS thinking_turns
378
+ FROM thinking_model_events
379
+ GROUP BY substr(created_at, 1, 10)
380
+ )
381
+ SELECT
382
+ assistant_daily.day AS day,
383
+ assistant_daily.assistant_turns AS assistant_turns,
384
+ COALESCE(thinking_daily.thinking_turns, 0) AS thinking_turns
385
+ FROM assistant_daily
386
+ LEFT JOIN thinking_daily ON thinking_daily.day = assistant_daily.day
387
+ ORDER BY assistant_daily.day ASC
388
+ `);
389
+ const scenarioMatrix = this.uiDb.all('SELECT model_id, scenario, hits FROM v_thinking_model_scenarios ORDER BY hits DESC, model_id ASC');
390
+ return {
391
+ summary: {
392
+ totalModels: knownModels.length,
393
+ activeModels: activeIds.size,
394
+ dormantModels: dormantModels.length,
395
+ effectiveModels: topModels.filter((model) => model.recommendation === 'reinforce').length,
396
+ coverageRate: roundRate(coverageRow.thinking_turns, coverageRow.assistant_turns),
397
+ },
398
+ topModels,
399
+ dormantModels,
400
+ effectiveModels: topModels.filter((model) => model.recommendation === 'reinforce'),
401
+ scenarioMatrix: scenarioMatrix.map((row) => ({
402
+ modelId: row.model_id,
403
+ modelName: getThinkingModel(row.model_id)?.name ?? row.model_id,
404
+ scenario: row.scenario,
405
+ hits: Number(row.hits),
406
+ })),
407
+ coverageTrend: coverageTrend.map((row) => ({
408
+ day: row.day,
409
+ assistantTurns: Number(row.assistant_turns),
410
+ thinkingTurns: Number(row.thinking_turns),
411
+ coverageRate: roundRate(Number(row.thinking_turns), Number(row.assistant_turns)),
412
+ })),
413
+ };
414
+ }
415
+ getThinkingModelDetail(modelId) {
416
+ if (!getThinkingModel(modelId)) {
417
+ return null;
418
+ }
419
+ const summary = this.loadThinkingModelSummaries().find((item) => item.modelId === modelId) ?? {
420
+ modelId,
421
+ name: getThinkingModel(modelId)?.name ?? modelId,
422
+ description: getThinkingModel(modelId)?.description ?? 'Unknown thinking model.',
423
+ hits: 0,
424
+ coverageRate: 0,
425
+ successRate: 0,
426
+ failureRate: 0,
427
+ painRate: 0,
428
+ correctionRate: 0,
429
+ correctionSampleRate: 0,
430
+ commonScenarios: [],
431
+ recommendation: 'archive',
432
+ };
433
+ const usageTrend = this.uiDb.all(`
434
+ SELECT day, hits
435
+ FROM v_thinking_model_daily_trend
436
+ WHERE model_id = ?
437
+ ORDER BY day ASC
438
+ `, modelId);
439
+ const scenarioDistribution = this.uiDb.all(`
440
+ SELECT scenario, hits
441
+ FROM v_thinking_model_scenarios
442
+ WHERE model_id = ?
443
+ ORDER BY hits DESC, scenario ASC
444
+ `, modelId);
445
+ const effect = this.uiDb.get('SELECT * FROM v_thinking_model_effectiveness WHERE model_id = ?', modelId) ?? {
446
+ events: 0,
447
+ success_windows: 0,
448
+ failure_windows: 0,
449
+ pain_windows: 0,
450
+ correction_windows: 0,
451
+ correction_sample_windows: 0,
452
+ };
453
+ const recentEvents = this.uiDb.all(`
454
+ SELECT id, created_at, matched_pattern, scenario_json, trigger_excerpt,
455
+ tool_context_json, pain_context_json, principle_context_json
456
+ FROM thinking_model_events
457
+ WHERE model_id = ?
458
+ ORDER BY created_at DESC
459
+ LIMIT 20
460
+ `, modelId);
461
+ return {
462
+ modelMeta: {
463
+ modelId: summary.modelId,
464
+ name: summary.name,
465
+ description: summary.description,
466
+ hits: summary.hits,
467
+ coverageRate: summary.coverageRate,
468
+ recommendation: summary.recommendation,
469
+ },
470
+ usageTrend: usageTrend.map((row) => ({
471
+ day: row.day,
472
+ hits: Number(row.hits),
473
+ })),
474
+ scenarioDistribution: scenarioDistribution.map((row) => ({
475
+ scenario: row.scenario,
476
+ hits: Number(row.hits),
477
+ })),
478
+ outcomeStats: {
479
+ events: Number(effect.events),
480
+ successRate: roundRate(Number(effect.success_windows), Number(effect.events)),
481
+ failureRate: roundRate(Number(effect.failure_windows), Number(effect.events)),
482
+ painRate: roundRate(Number(effect.pain_windows), Number(effect.events)),
483
+ correctionRate: roundRate(Number(effect.correction_windows), Number(effect.events)),
484
+ correctionSampleRate: roundRate(Number(effect.correction_sample_windows), Number(effect.events)),
485
+ },
486
+ recentEvents: recentEvents.map((row) => ({
487
+ id: Number(row.id),
488
+ createdAt: row.created_at,
489
+ matchedPattern: row.matched_pattern,
490
+ scenarios: parseJson(row.scenario_json, []),
491
+ triggerExcerpt: row.trigger_excerpt,
492
+ toolContext: parseJson(row.tool_context_json, []),
493
+ painContext: parseJson(row.pain_context_json, []),
494
+ principleContext: parseJson(row.principle_context_json, []),
495
+ })),
496
+ };
497
+ }
498
+ loadThinkingModelSummaries() {
499
+ const knownModels = listThinkingModels();
500
+ const usageRows = new Map(this.uiDb.all('SELECT model_id, hits, coverage_rate FROM v_thinking_model_usage').map((row) => [row.model_id, row]));
501
+ const effectRows = new Map(this.uiDb.all('SELECT * FROM v_thinking_model_effectiveness').map((row) => [row.model_id, row]));
502
+ const scenarioRows = this.uiDb.all('SELECT model_id, scenario, hits FROM v_thinking_model_scenarios ORDER BY hits DESC');
503
+ return knownModels.map((model) => {
504
+ const usage = usageRows.get(model.id);
505
+ const effect = effectRows.get(model.id);
506
+ const events = Number(effect?.events ?? usage?.hits ?? 0);
507
+ const successRate = roundRate(Number(effect?.success_windows ?? 0), events);
508
+ const failureRate = roundRate(Number(effect?.failure_windows ?? 0), events);
509
+ const painRate = roundRate(Number(effect?.pain_windows ?? 0), events);
510
+ const correctionRate = roundRate(Number(effect?.correction_windows ?? 0), events);
511
+ const correctionSampleRate = roundRate(Number(effect?.correction_sample_windows ?? 0), events);
512
+ return {
513
+ modelId: model.id,
514
+ name: model.name,
515
+ description: model.description,
516
+ hits: Number(usage?.hits ?? 0),
517
+ coverageRate: Number(usage?.coverage_rate ?? 0),
518
+ successRate,
519
+ failureRate,
520
+ painRate,
521
+ correctionRate,
522
+ correctionSampleRate,
523
+ commonScenarios: scenarioRows
524
+ .filter((row) => row.model_id === model.id)
525
+ .slice(0, 3)
526
+ .map((row) => row.scenario),
527
+ recommendation: summarizeRecommendation({
528
+ hits: Number(usage?.hits ?? 0),
529
+ successRate,
530
+ failureRate,
531
+ painRate,
532
+ correctionRate,
533
+ }),
534
+ };
535
+ }).sort((left, right) => right.hits - left.hits || left.modelId.localeCompare(right.modelId));
536
+ }
537
+ }
@@ -38,5 +38,7 @@ export declare class EmpathyObserverManager {
38
38
  private parseJsonPayload;
39
39
  private extractAssistantText;
40
40
  private scoreFromSeverity;
41
+ private normalizeSeverity;
42
+ private normalizeConfidence;
41
43
  }
42
44
  export declare const empathyObserverManager: EmpathyObserverManager;
@@ -71,7 +71,37 @@ export class EmpathyObserverManager {
71
71
  if (parsed?.damageDetected && sessionId) {
72
72
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
73
73
  const score = this.scoreFromSeverity(parsed.severity, wctx.config);
74
- trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir);
74
+ trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir, { source: 'user_empathy' });
75
+ const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
76
+ wctx.eventLog.recordPainSignal(sessionId, {
77
+ score,
78
+ source: 'user_empathy',
79
+ reason: parsed.reason || 'Empathy observer detected likely user frustration.',
80
+ isRisky: false,
81
+ origin: 'system_infer',
82
+ severity: this.normalizeSeverity(parsed.severity),
83
+ confidence: this.normalizeConfidence(parsed.confidence),
84
+ detection_mode: 'structured',
85
+ deduped: false,
86
+ trigger_text_excerpt: rawText.substring(0, 120),
87
+ raw_score: score,
88
+ calibrated_score: score,
89
+ eventId,
90
+ });
91
+ try {
92
+ wctx.trajectory?.recordPainEvent?.({
93
+ sessionId,
94
+ source: 'user_empathy',
95
+ score,
96
+ reason: parsed.reason || 'Empathy observer detected likely user frustration.',
97
+ severity: this.normalizeSeverity(parsed.severity),
98
+ origin: 'system_infer',
99
+ confidence: this.normalizeConfidence(parsed.confidence),
100
+ });
101
+ }
102
+ catch (error) {
103
+ api.logger.warn(`[PD:EmpathyObserver] Failed to persist observer pain event for ${sessionId}: ${String(error)}`);
104
+ }
75
105
  api.logger.info(`[PD:EmpathyObserver] Applied GFI +${score} for ${sessionId}`);
76
106
  }
77
107
  }
@@ -143,5 +173,17 @@ export class EmpathyObserverManager {
143
173
  return Number(config.get('empathy_engine.penalties.moderate') ?? 25);
144
174
  return Number(config.get('empathy_engine.penalties.mild') ?? 10);
145
175
  }
176
+ normalizeSeverity(severity) {
177
+ if (severity === 'severe')
178
+ return 'severe';
179
+ if (severity === 'moderate')
180
+ return 'moderate';
181
+ return 'mild';
182
+ }
183
+ normalizeConfidence(value) {
184
+ if (!Number.isFinite(value))
185
+ return 1;
186
+ return Math.max(0, Math.min(1, Number(value)));
187
+ }
146
188
  }
147
189
  export const empathyObserverManager = EmpathyObserverManager.getInstance();
@@ -1,4 +1,5 @@
1
1
  import type { OpenClawPluginServiceContext, OpenClawPluginApi } from '../openclaw-sdk.js';
2
+ import { WorkspaceContext } from '../core/workspace-context.js';
2
3
  export interface EvolutionQueueItem {
3
4
  id: string;
4
5
  task?: string;
@@ -9,6 +10,32 @@ export interface EvolutionQueueItem {
9
10
  trigger_text_preview?: string;
10
11
  status: 'pending' | 'in_progress' | 'completed';
11
12
  }
13
+ export declare const EVOLUTION_QUEUE_LOCK_SUFFIX = ".lock";
14
+ export declare const PAIN_CANDIDATES_LOCK_SUFFIX = ".candidates.lock";
15
+ export declare const LOCK_MAX_RETRIES = 50;
16
+ export declare const LOCK_RETRY_DELAY_MS = 50;
17
+ export declare const LOCK_STALE_MS = 30000;
18
+ export declare function createEvolutionTaskId(source: string, score: number, preview: string, reason: string, now: number): string;
19
+ export declare function shouldTrackPainCandidate(text: string): boolean;
20
+ export declare function createPainCandidateFingerprint(text: string): string;
21
+ export declare function summarizePainCandidateSample(text: string): string;
22
+ /**
23
+ * Acquire an exclusive file lock for the given resource.
24
+ * Returns a release function. Uses 'wx' flag for atomic exclusive create.
25
+ * Detects stale locks by checking PID and mtime.
26
+ */
27
+ export declare function acquireQueueLock(lockPath: string, logger: any): (() => void) | null;
28
+ export declare function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number, reason?: string): boolean;
29
+ export declare function hasEquivalentPromotedRule(dictionary: {
30
+ getAllRules(): Record<string, {
31
+ type: string;
32
+ phrases?: string[];
33
+ pattern?: string;
34
+ status: string;
35
+ }>;
36
+ }, phrase: string): boolean;
37
+ export declare function trackPainCandidate(text: string, wctx: WorkspaceContext): void;
38
+ export declare function processPromotion(wctx: WorkspaceContext, logger: any, eventLog: any): void;
12
39
  export interface ExtendedEvolutionWorkerService {
13
40
  id: string;
14
41
  api: OpenClawPluginApi | null;