principles-disciple 1.11.0 → 1.13.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 (233) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +250 -6
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +21 -47
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +110 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +319 -61
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -21,6 +21,12 @@ export class CentralDatabase {
21
21
  private readonly dbPath: string;
22
22
  private readonly db: Database.Database;
23
23
  private readonly workspaces: WorkspaceInfo[] = [];
24
+ private _closed = false;
25
+
26
+ /** Whether this connection has been closed. Used by the singleton to auto-reopen. */
27
+ get isClosed(): boolean {
28
+ return this._closed;
29
+ }
24
30
 
25
31
  constructor() {
26
32
  const openClawDir = os.homedir();
@@ -39,9 +45,10 @@ export class CentralDatabase {
39
45
 
40
46
  dispose(): void {
41
47
  this.db.close();
48
+ this._closed = true;
42
49
  }
43
50
 
44
- private tableExists(db: Database.Database, tableName: string): boolean {
51
+ private static tableExists(db: Database.Database, tableName: string): boolean {
45
52
  const result = db.prepare(`
46
53
  SELECT name FROM sqlite_master WHERE type='table' AND name=?
47
54
  `).get(tableName);
@@ -211,7 +218,7 @@ export class CentralDatabase {
211
218
  // Sync sessions
212
219
  const sessions = sourceDb.prepare(`
213
220
  SELECT session_id, started_at, updated_at FROM sessions
214
- `).all() as Array<{session_id: string; started_at: string; updated_at: string}>;
221
+ `).all() as {session_id: string; started_at: string; updated_at: string}[];
215
222
 
216
223
  const insertSession = this.db.prepare(`
217
224
  INSERT OR REPLACE INTO aggregated_sessions (session_id, workspace, started_at, updated_at)
@@ -227,11 +234,11 @@ export class CentralDatabase {
227
234
  const toolCalls = sourceDb.prepare(`
228
235
  SELECT session_id, tool_name, outcome, duration_ms, error_type, error_message, created_at
229
236
  FROM tool_calls
230
- `).all() as Array<{
237
+ `).all() as {
231
238
  session_id: string; tool_name: string; outcome: string;
232
239
  duration_ms: number | null; error_type: string | null;
233
240
  error_message: string | null; created_at: string
234
- }>;
241
+ }[];
235
242
 
236
243
  const insertTool = this.db.prepare(`
237
244
  INSERT INTO aggregated_tool_calls
@@ -250,10 +257,10 @@ export class CentralDatabase {
250
257
  // Sync pain_events
251
258
  const painEvents = sourceDb.prepare(`
252
259
  SELECT session_id, source, score, reason, created_at FROM pain_events
253
- `).all() as Array<{
260
+ `).all() as {
254
261
  session_id: string; source: string; score: number;
255
262
  reason: string | null; created_at: string
256
- }>;
263
+ }[];
257
264
 
258
265
  const insertPain = this.db.prepare(`
259
266
  INSERT INTO aggregated_pain_events (workspace, session_id, source, score, reason, created_at)
@@ -269,9 +276,9 @@ export class CentralDatabase {
269
276
  const corrections = sourceDb.prepare(`
270
277
  SELECT session_id, correction_cue, created_at FROM user_turns
271
278
  WHERE correction_detected = 1
272
- `).all() as Array<{
279
+ `).all() as {
273
280
  session_id: string; correction_cue: string | null; created_at: string
274
- }>;
281
+ }[];
275
282
 
276
283
  const insertCorr = this.db.prepare(`
277
284
  INSERT INTO aggregated_user_corrections (workspace, session_id, correction_cue, created_at)
@@ -286,9 +293,9 @@ export class CentralDatabase {
286
293
  // Sync principle_events
287
294
  const principles = sourceDb.prepare(`
288
295
  SELECT principle_id, event_type, created_at FROM principle_events
289
- `).all() as Array<{
296
+ `).all() as {
290
297
  principle_id: string | null; event_type: string; created_at: string
291
- }>;
298
+ }[];
292
299
 
293
300
  const insertPrinciple = this.db.prepare(`
294
301
  INSERT INTO aggregated_principle_events (workspace, principle_id, event_type, created_at)
@@ -301,12 +308,12 @@ export class CentralDatabase {
301
308
  }
302
309
 
303
310
  // Sync thinking_model_events (may not exist in older workspaces)
304
- if (this.tableExists(sourceDb, 'thinking_model_events')) {
311
+ if (CentralDatabase.tableExists(sourceDb, 'thinking_model_events')) {
305
312
  const thinking = sourceDb.prepare(`
306
313
  SELECT session_id, model_id, matched_pattern, created_at FROM thinking_model_events
307
- `).all() as Array<{
314
+ `).all() as {
308
315
  session_id: string; model_id: string; matched_pattern: string; created_at: string
309
- }>;
316
+ }[];
310
317
 
311
318
  const insertThinking = this.db.prepare(`
312
319
  INSERT INTO aggregated_thinking_events (workspace, session_id, model_id, matched_pattern, created_at)
@@ -323,10 +330,10 @@ export class CentralDatabase {
323
330
  const samples = sourceDb.prepare(`
324
331
  SELECT sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at
325
332
  FROM correction_samples
326
- `).all() as Array<{
333
+ `).all() as {
327
334
  sample_id: string; session_id: string; bad_assistant_turn_id: number;
328
335
  quality_score: number | null; review_status: string | null; created_at: string
329
- }>;
336
+ }[];
330
337
 
331
338
  const insertSample = this.db.prepare(`
332
339
  INSERT OR REPLACE INTO aggregated_correction_samples
@@ -345,9 +352,9 @@ export class CentralDatabase {
345
352
  // Sync task_outcomes
346
353
  const outcomes = sourceDb.prepare(`
347
354
  SELECT session_id, task_id, outcome, created_at FROM task_outcomes
348
- `).all() as Array<{
355
+ `).all() as {
349
356
  session_id: string; task_id: string | null; outcome: string; created_at: string
350
- }>;
357
+ }[];
351
358
 
352
359
  const insertOutcome = this.db.prepare(`
353
360
  INSERT INTO aggregated_task_outcomes (workspace, session_id, task_id, outcome, created_at)
@@ -475,7 +482,7 @@ export class CentralDatabase {
475
482
 
476
483
  const workspaces = this.db.prepare(`
477
484
  SELECT name FROM workspaces ORDER BY name
478
- `).all() as Array<{ name: string }>;
485
+ `).all() as { name: string }[];
479
486
 
480
487
  const enabledConfigs = this.getWorkspaceConfigs().filter(c => c.enabled && c.syncEnabled);
481
488
  const enabledWorkspaceNames = enabledConfigs.map(c => c.workspaceName);
@@ -501,16 +508,16 @@ export class CentralDatabase {
501
508
  /**
502
509
  * Get daily trend data
503
510
  */
504
- getDailyTrend(days: number = 7): Array<{
511
+ getDailyTrend(days = 7): {
505
512
  day: string;
506
513
  toolCalls: number;
507
514
  failures: number;
508
515
  userCorrections: number;
509
516
  thinkingTurns: number;
510
- }> {
517
+ }[] {
511
518
  const cutoffDate = new Date();
512
519
  cutoffDate.setDate(cutoffDate.getDate() - days);
513
- const cutoffStr = cutoffDate.toISOString().split('T')[0];
520
+ const [cutoffStr] = cutoffDate.toISOString().split('T');
514
521
 
515
522
  const toolDaily = this.db.prepare(`
516
523
  SELECT
@@ -521,9 +528,9 @@ export class CentralDatabase {
521
528
  WHERE substr(created_at, 1, 10) >= ?
522
529
  GROUP BY substr(created_at, 1, 10)
523
530
  ORDER BY day
524
- `).all(cutoffStr) as Array<{
531
+ `).all(cutoffStr) as {
525
532
  day: string; tool_calls: number; failures: number
526
- }>;
533
+ }[];
527
534
 
528
535
  const correctionsDaily = this.db.prepare(`
529
536
  SELECT
@@ -532,9 +539,9 @@ export class CentralDatabase {
532
539
  FROM aggregated_user_corrections
533
540
  WHERE substr(created_at, 1, 10) >= ?
534
541
  GROUP BY substr(created_at, 1, 10)
535
- `).all(cutoffStr) as Array<{
542
+ `).all(cutoffStr) as {
536
543
  day: string; corrections: number
537
- }>;
544
+ }[];
538
545
 
539
546
  const thinkingDaily = this.db.prepare(`
540
547
  SELECT
@@ -543,9 +550,9 @@ export class CentralDatabase {
543
550
  FROM aggregated_thinking_events
544
551
  WHERE substr(created_at, 1, 10) >= ?
545
552
  GROUP BY substr(created_at, 1, 10)
546
- `).all(cutoffStr) as Array<{
553
+ `).all(cutoffStr) as {
547
554
  day: string; thinking_turns: number
548
- }>;
555
+ }[];
549
556
 
550
557
  // Merge all trends
551
558
  const dayMap = new Map<string, {
@@ -602,11 +609,11 @@ export class CentralDatabase {
602
609
  /**
603
610
  * Get top regressions
604
611
  */
605
- getTopRegressions(limit: number = 5): Array<{
612
+ getTopRegressions(limit = 5): {
606
613
  toolName: string;
607
614
  errorType: string;
608
615
  occurrences: number;
609
- }> {
616
+ }[] {
610
617
  return this.db.prepare(`
611
618
  SELECT
612
619
  tool_name as toolName,
@@ -617,11 +624,11 @@ export class CentralDatabase {
617
624
  GROUP BY tool_name, error_type
618
625
  ORDER BY occurrences DESC
619
626
  LIMIT ?
620
- `).all(limit) as Array<{
627
+ `).all(limit) as {
621
628
  toolName: string;
622
629
  errorType: string;
623
630
  occurrences: number;
624
- }>;
631
+ }[];
625
632
  }
626
633
 
627
634
  /**
@@ -630,11 +637,11 @@ export class CentralDatabase {
630
637
  getThinkingModelStats(): {
631
638
  totalModels: number;
632
639
  activeModels: number;
633
- models: Array<{
640
+ models: {
634
641
  modelId: string;
635
642
  hits: number;
636
643
  coverageRate: number;
637
- }>;
644
+ }[];
638
645
  } {
639
646
  const totalModels = this.db.prepare(`
640
647
  SELECT COUNT(DISTINCT model_id) as count FROM aggregated_thinking_events
@@ -661,11 +668,7 @@ export class CentralDatabase {
661
668
  FROM aggregated_thinking_events
662
669
  GROUP BY model_id
663
670
  ORDER BY hits DESC
664
- `).all() as Array<{ modelId: string; hits: number }>;
665
-
666
- const coverageRate = totalToolCalls.count > 0
667
- ? models.reduce((sum, m) => sum + m.hits, 0) / totalToolCalls.count
668
- : 0;
671
+ `).all() as { modelId: string; hits: number }[];
669
672
 
670
673
  return {
671
674
  totalModels: totalModels.count,
@@ -686,22 +689,22 @@ export class CentralDatabase {
686
689
  `).all() as WorkspaceInfo[];
687
690
  }
688
691
 
689
- getWorkspaceConfigs(): Array<{
692
+ getWorkspaceConfigs(): {
690
693
  workspaceName: string;
691
694
  enabled: boolean;
692
695
  displayName: string | null;
693
696
  syncEnabled: boolean;
694
- }> {
697
+ }[] {
695
698
  const configs = this.db.prepare(`
696
699
  SELECT workspace_name, enabled, display_name, sync_enabled
697
700
  FROM workspace_config
698
701
  ORDER BY workspace_name
699
- `).all() as Array<{
702
+ `).all() as {
700
703
  workspace_name: string;
701
704
  enabled: number;
702
705
  display_name: string | null;
703
706
  sync_enabled: number;
704
- }>;
707
+ }[];
705
708
 
706
709
  return configs.map(c => ({
707
710
  workspaceName: c.workspace_name,
@@ -818,13 +821,98 @@ export class CentralDatabase {
818
821
  DELETE FROM sync_log;
819
822
  `);
820
823
  }
824
+
825
+ /**
826
+ * Get total task outcomes count across enabled workspaces (D-02)
827
+ */
828
+ getTaskOutcomes(): number {
829
+ const filter = this.getEnabledWorkspaceFilter();
830
+ const row = this.db.prepare(`
831
+ SELECT COUNT(*) as count FROM aggregated_task_outcomes
832
+ WHERE workspace IN (${filter})
833
+ `).get() as { count: number } | undefined;
834
+ return row?.count ?? 0;
835
+ }
836
+
837
+ /**
838
+ * Get total principle events count across enabled workspaces (D-03)
839
+ */
840
+ getPrincipleEventCount(): number {
841
+ const filter = this.getEnabledWorkspaceFilter();
842
+ const row = this.db.prepare(`
843
+ SELECT COUNT(*) as count FROM aggregated_principle_events
844
+ WHERE workspace IN (${filter})
845
+ `).get() as { count: number } | undefined;
846
+ return row?.count ?? 0;
847
+ }
848
+
849
+ /**
850
+ * Get sample counts grouped by review_status across enabled workspaces (D-06)
851
+ */
852
+ getSampleCountersByStatus(): Record<string, number> {
853
+ const filter = this.getEnabledWorkspaceFilter();
854
+ const rows = this.db.prepare(`
855
+ SELECT review_status, COUNT(*) as count
856
+ FROM aggregated_correction_samples
857
+ WHERE workspace IN (${filter})
858
+ GROUP BY review_status
859
+ `).all() as { review_status: string; count: number }[];
860
+ return Object.fromEntries(rows.map(r => [r.review_status, r.count]));
861
+ }
862
+
863
+ /**
864
+ * Get top N most recent pending/approved samples across all enabled workspaces (D-04)
865
+ */
866
+ getSamplePreview(limit = 5): {
867
+ sampleId: string;
868
+ sessionId: string;
869
+ workspace: string;
870
+ qualityScore: number;
871
+ reviewStatus: string;
872
+ createdAt: string;
873
+ }[] {
874
+ const filter = this.getEnabledWorkspaceFilter();
875
+ const rows = this.db.prepare(`
876
+ SELECT sample_id, session_id, workspace, quality_score, review_status, created_at
877
+ FROM aggregated_correction_samples
878
+ WHERE workspace IN (${filter})
879
+ AND review_status IN ('pending', 'approved')
880
+ ORDER BY created_at DESC
881
+ LIMIT ?
882
+ `).all(limit) as {
883
+ sample_id: string;
884
+ session_id: string;
885
+ workspace: string;
886
+ quality_score: number;
887
+ review_status: string;
888
+ created_at: string;
889
+ }[];
890
+ return rows.map(r => ({
891
+ sampleId: r.sample_id,
892
+ sessionId: r.session_id,
893
+ workspace: r.workspace,
894
+ qualityScore: r.quality_score ?? 0,
895
+ reviewStatus: r.review_status ?? 'pending',
896
+ createdAt: r.created_at,
897
+ }));
898
+ }
899
+
900
+ /**
901
+ * Get the most recent lastSync timestamp across all workspaces (D-05)
902
+ */
903
+ getMostRecentSync(): string | null {
904
+ const row = this.db.prepare(`
905
+ SELECT MAX(last_sync) as lastSync FROM workspaces
906
+ `).get() as { lastSync: string | null } | undefined;
907
+ return row?.lastSync ?? null;
908
+ }
821
909
  }
822
910
 
823
911
  // Singleton instance
824
912
  let centralDbInstance: CentralDatabase | null = null;
825
913
 
826
914
  export function getCentralDatabase(): CentralDatabase {
827
- if (!centralDbInstance) {
915
+ if (!centralDbInstance || centralDbInstance.isClosed) {
828
916
  centralDbInstance = new CentralDatabase();
829
917
  }
830
918
  return centralDbInstance;
@@ -0,0 +1,47 @@
1
+ import { getCentralDatabase } from './central-database.js';
2
+ import { HealthQueryService } from './health-query-service.js';
3
+
4
+ export interface WorkspaceHealthEntry {
5
+ workspaceName: string;
6
+ health: ReturnType<HealthQueryService['getOverviewHealth']>;
7
+ }
8
+
9
+ export interface CentralHealthResponse {
10
+ workspaces: WorkspaceHealthEntry[];
11
+ generatedAt: string;
12
+ }
13
+
14
+ /**
15
+ * Aggregates health data across all enabled workspaces.
16
+ * Each workspace gets its own HealthQueryService instance so GFI, Trust,
17
+ * Evolution, Principles, and Queue stats are workspace-specific.
18
+ */
19
+ export class CentralHealthService {
20
+ /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: utility method that doesn't need instance state */
21
+ getAllWorkspaceHealth(): CentralHealthResponse {
22
+ const centralDb = getCentralDatabase();
23
+ const workspaces: WorkspaceHealthEntry[] = [];
24
+ const enabled = centralDb.getEnabledWorkspaces();
25
+
26
+ for (const ws of enabled) {
27
+ try {
28
+ const hqs = new HealthQueryService(ws.path);
29
+ try {
30
+ const health = hqs.getOverviewHealth();
31
+ workspaces.push({ workspaceName: ws.name, health });
32
+ } finally {
33
+ hqs.dispose();
34
+ }
35
+ } catch (error) {
36
+ console.warn(
37
+ `[CentralHealthService] Could not get health for workspace "${ws.name}": ${String(error)}`,
38
+ );
39
+ }
40
+ }
41
+
42
+ return {
43
+ workspaces,
44
+ generatedAt: new Date().toISOString(),
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,135 @@
1
+ import { getCentralDatabase, type CentralDatabase } from './central-database.js';
2
+ import { getThinkingModelDefinitions } from '../core/thinking-models.js';
3
+ import type { OverviewResponse } from './control-ui-query-service.js';
4
+
5
+ export { OverviewResponse };
6
+
7
+ export interface CentralOverviewResponse
8
+ extends Omit<OverviewResponse, 'dataSource' | 'runtimeControlPlaneSource'> {
9
+ dataSource: string;
10
+ runtimeControlPlaneSource: string;
11
+ centralInfo: {
12
+ workspaceCount: number;
13
+ enabledWorkspaceCount: number;
14
+ workspaces: string[];
15
+ enabledWorkspaces: string[];
16
+ };
17
+ }
18
+
19
+ export class CentralOverviewService {
20
+ private readonly centralDb: CentralDatabase;
21
+
22
+ constructor() {
23
+ this.centralDb = getCentralDatabase();
24
+ }
25
+
26
+ /* eslint-disable @typescript-eslint/class-methods-use-this -- Reason: intentionally no-op, centralDb is a process-wide singleton, see comment below */
27
+ dispose(): void {
28
+ // Do NOT dispose centralDb — it's a singleton shared across all requests.
29
+ // Individual services that open per-request connections (e.g. HealthQueryService)
30
+ // must dispose their own connections, but the central aggregated DB lives for
31
+ // the lifetime of the process.
32
+ }
33
+ /* eslint-enable @typescript-eslint/class-methods-use-this */
34
+
35
+ getOverview(days = 30): CentralOverviewResponse {
36
+ const stats = this.centralDb.getOverviewStats();
37
+ const trend = this.centralDb.getDailyTrend(days);
38
+ const regressions = this.centralDb.getTopRegressions(5);
39
+ const thinkingStats = this.centralDb.getThinkingModelStats();
40
+ const samplePreviewRows = this.centralDb.getSamplePreview(5);
41
+ const mostRecentSync = this.centralDb.getMostRecentSync();
42
+
43
+ // D-02: Query aggregated_task_outcomes for real taskOutcomes (not hardcoded 0)
44
+ let taskOutcomes = 0;
45
+ try {
46
+ taskOutcomes = this.centralDb.getTaskOutcomes();
47
+ } catch {
48
+ console.warn('[CentralOverviewService] Could not query aggregated_task_outcomes, defaulting taskOutcomes to 0');
49
+ }
50
+
51
+ // D-03: Query aggregated_principle_events for principleEventCount
52
+ // gate_blocks has no equivalent in central DB -- hardcode to 0 with warning
53
+ let principleEventCount = 0;
54
+ let gateBlocks = 0;
55
+ try {
56
+ principleEventCount = this.centralDb.getPrincipleEventCount();
57
+ } catch {
58
+ console.warn('[CentralOverviewService] Could not query aggregated_principle_events, defaulting principleEventCount to 0');
59
+ }
60
+ // gate_blocks: no equivalent in aggregated DB schema; hardcode to 0
61
+
62
+ // D-06: sampleQueue.counters from aggregated_correction_samples GROUP BY review_status
63
+ // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment in try/catch
64
+ let sampleCounters: Record<string, number> = {};
65
+ try {
66
+ sampleCounters = this.centralDb.getSampleCountersByStatus();
67
+ } catch {
68
+ // Fallback to stats-based counters if query fails
69
+ sampleCounters = {
70
+ pending: stats.pendingSamples,
71
+ approved: stats.approvedSamples,
72
+ rejected: stats.rejectedSamples,
73
+ };
74
+ }
75
+
76
+ // D-04: sampleQueue.preview from samplePreviewRows (not [])
77
+ // D-05: dataFreshness from mostRecentSync (not workspaces[0])
78
+
79
+ return {
80
+ workspaceDir: 'central',
81
+ generatedAt: new Date().toISOString(),
82
+ dataFreshness: mostRecentSync,
83
+ dataSource: 'central_aggregated_db',
84
+ runtimeControlPlaneSource: 'all_workspaces',
85
+ summary: {
86
+ repeatErrorRate: stats.totalToolCalls > 0
87
+ ? stats.totalFailures / stats.totalToolCalls
88
+ : 0,
89
+ userCorrectionRate: stats.totalToolCalls > 0
90
+ ? stats.totalCorrections / stats.totalToolCalls
91
+ : 0,
92
+ pendingSamples: stats.pendingSamples,
93
+ approvedSamples: stats.approvedSamples,
94
+ thinkingCoverageRate: stats.totalToolCalls > 0
95
+ ? stats.totalThinkingEvents / stats.totalToolCalls
96
+ : 0,
97
+ painEvents: stats.totalPainEvents,
98
+ principleEventCount,
99
+ gateBlocks,
100
+ taskOutcomes,
101
+ },
102
+ dailyTrend: trend,
103
+ topRegressions: regressions,
104
+ sampleQueue: {
105
+ counters: sampleCounters,
106
+ preview: samplePreviewRows.map(row => ({
107
+ sampleId: row.sampleId,
108
+ sessionId: row.sessionId,
109
+ qualityScore: Number(row.qualityScore),
110
+ reviewStatus: row.reviewStatus,
111
+ createdAt: row.createdAt,
112
+ })),
113
+ },
114
+ thinkingSummary: {
115
+ activeModels: thinkingStats.activeModels,
116
+ dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
117
+ effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
118
+ coverageRate: stats.totalToolCalls > 0
119
+ ? stats.totalThinkingEvents / stats.totalToolCalls
120
+ : 0,
121
+ modelBreakdown: thinkingStats.models.map(m => ({
122
+ modelId: m.modelId,
123
+ hits: m.hits,
124
+ })),
125
+ modelDefinitions: getThinkingModelDefinitions(),
126
+ },
127
+ centralInfo: {
128
+ workspaceCount: stats.workspaceCount,
129
+ enabledWorkspaceCount: stats.enabledWorkspaceCount,
130
+ workspaces: stats.workspaceNames,
131
+ enabledWorkspaces: stats.enabledWorkspaceNames,
132
+ },
133
+ };
134
+ }
135
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * CentralSyncService - Periodically sync workspace data to central database.
3
+ *
4
+ * Ensures thinking_model_events and other workspace data are aggregated
5
+ * into the central database for cross-workspace queries and WebUI display.
6
+ */
7
+
8
+ import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from '../openclaw-sdk.js';
9
+ import { CentralDatabase } from './central-database.js';
10
+
11
+ let syncInterval: ReturnType<typeof setInterval> | null = null;
12
+ let logger: PluginLogger | undefined = undefined;
13
+ let centralDb: CentralDatabase | null = null;
14
+
15
+ /**
16
+ * Default sync interval: 5 minutes.
17
+ * Can be overridden via config: intervals.central_sync_ms
18
+ */
19
+ const DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1000;
20
+
21
+ async function runSyncCycle(): Promise<void> {
22
+ if (!centralDb) {
23
+ logger?.warn?.('[PD:CentralSync] CentralDatabase not initialized, skipping sync');
24
+ return;
25
+ }
26
+
27
+ try {
28
+ const results = centralDb.syncAll();
29
+ const totalSynced = Array.from(results.values()).reduce((sum, count) => sum + count, 0);
30
+ const workspacesSynced = Array.from(results.entries())
31
+ .filter(([, count]) => count > 0)
32
+ .map(([name, count]) => `${name}:${count}`)
33
+ .join(', ');
34
+
35
+ if (totalSynced > 0) {
36
+ logger?.info?.(`[PD:CentralSync] Synced ${totalSynced} records from workspaces: ${workspacesSynced}`);
37
+ } else {
38
+ logger?.debug?.(`[PD:CentralSync] No new records to sync`);
39
+ }
40
+ } catch (err) {
41
+ logger?.error?.(`[PD:CentralSync] Sync failed: ${String(err)}`);
42
+ }
43
+ }
44
+
45
+ export const CentralSyncService: OpenClawPluginService = {
46
+ id: 'principles-central-sync',
47
+
48
+ async start(ctx: OpenClawPluginServiceContext): Promise<void> {
49
+ const { logger: ctxLogger, config } = ctx;
50
+ logger = ctxLogger;
51
+
52
+ const { intervals } = config as { intervals?: { central_sync_ms?: number } };
53
+ const intervalMs = intervals?.central_sync_ms ?? DEFAULT_SYNC_INTERVAL_MS;
54
+
55
+ // Initialize CentralDatabase
56
+ centralDb = new CentralDatabase();
57
+
58
+ // Initial sync on start
59
+ logger?.info?.(`[PD:CentralSync] Starting with interval ${intervalMs}ms`);
60
+ await runSyncCycle();
61
+
62
+ // Schedule periodic sync
63
+ syncInterval = setInterval(runSyncCycle, intervalMs);
64
+
65
+ logger?.info?.(`[PD:CentralSync] Service started, syncing every ${intervalMs / 1000}s`);
66
+ },
67
+
68
+ async stop(ctx: OpenClawPluginServiceContext): Promise<void> {
69
+ if (syncInterval) {
70
+ clearInterval(syncInterval);
71
+ syncInterval = null;
72
+ }
73
+
74
+ // Final sync on stop
75
+ if (centralDb) {
76
+ try {
77
+ centralDb.syncAll();
78
+ ctx.logger?.info?.(`[PD:CentralSync] Final sync completed`);
79
+ } catch (err) {
80
+ ctx.logger?.error?.(`[PD:CentralSync] Final sync failed: ${String(err)}`);
81
+ }
82
+ }
83
+
84
+ centralDb = null;
85
+ ctx.logger?.info?.(`[PD:CentralSync] Service stopped`);
86
+ },
87
+ };