principles-disciple 1.72.0 → 1.73.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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +459 -439
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +59 -16
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +3 -1
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
@@ -0,0 +1,457 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
+ import { rmSync, mkdirSync, readdirSync, existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import os from 'os';
5
+ import Database from 'better-sqlite3';
6
+
7
+ import { CentralDatabase, getCentralDatabase, resetCentralDatabase } from '../../src/service/central-database.js';
8
+
9
+ // ── Home dir ────────────────────────────────────────────────────────────────
10
+
11
+ const REAL_HOME = os.homedir();
12
+ const OPENCLAW_DIR = join(REAL_HOME, '.openclaw');
13
+ const CENTRAL_DB_DIR = '.central';
14
+
15
+ /** Each test gets its own DB file via PD_CENTRAL_DB_PATH env var. */
16
+ let tempDbDir = '';
17
+
18
+ function makeTempDbPath(): string {
19
+ const dir = join(os.tmpdir(), `pd-cdb-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
20
+ mkdirSync(dir, { recursive: true });
21
+ return join(dir, 'aggregated.db');
22
+ }
23
+
24
+ function cleanupTestDirs() {
25
+ // Clean up workspace dirs (cdb-test-* names) and .central/
26
+ if (existsSync(OPENCLAW_DIR)) {
27
+ for (const entry of readdirSync(OPENCLAW_DIR)) {
28
+ if (entry.startsWith('cdb-test-') || entry === '.central') {
29
+ try {
30
+ rmSync(join(OPENCLAW_DIR, entry), { recursive: true, force: true });
31
+ } catch { /* noop */ }
32
+ }
33
+ }
34
+ }
35
+ // Clean up temp DB dirs from this run
36
+ if (tempDbDir) {
37
+ try { rmSync(tempDbDir, { recursive: true, force: true }); } catch { /* noop */ }
38
+ }
39
+ }
40
+
41
+ beforeEach(() => {
42
+ cleanupTestDirs();
43
+ const dbPath = makeTempDbPath();
44
+ tempDbDir = dirname(dbPath);
45
+ process.env.PD_CENTRAL_DB_PATH = dbPath;
46
+ resetCentralDatabase();
47
+ });
48
+
49
+ afterEach(() => {
50
+ resetCentralDatabase();
51
+ delete process.env.PD_CENTRAL_DB_PATH;
52
+ cleanupTestDirs();
53
+ });
54
+
55
+ function makeWorkspaceName(prefix: string) {
56
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
57
+ }
58
+
59
+ // ── Trajectory DB helper ────────────────────────────────────────────────────
60
+
61
+ function makeTrajectoryDb(workspaceName: string) {
62
+ // openclawDir = ~/.openclaw, so workspace dir is ~/.openclaw/<workspaceName>
63
+ const wsDir = join(OPENCLAW_DIR, workspaceName);
64
+ mkdirSync(wsDir, { recursive: true });
65
+ const stateDir = join(wsDir, '.state');
66
+ mkdirSync(stateDir, { recursive: true });
67
+ const db = new Database(join(stateDir, 'trajectory.db'));
68
+ db.exec(`
69
+ CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, updated_at TEXT NOT NULL);
70
+ CREATE TABLE IF NOT EXISTS tool_calls (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, outcome TEXT NOT NULL, duration_ms INTEGER, error_type TEXT, error_message TEXT, created_at TEXT NOT NULL);
71
+ CREATE TABLE IF NOT EXISTS pain_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, source TEXT NOT NULL, score REAL NOT NULL, reason TEXT, created_at TEXT NOT NULL);
72
+ CREATE TABLE IF NOT EXISTS user_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, correction_cue TEXT, correction_detected INTEGER DEFAULT 0, created_at TEXT NOT NULL);
73
+ CREATE TABLE IF NOT EXISTS principle_events (id INTEGER PRIMARY KEY AUTOINCREMENT, principle_id TEXT, event_type TEXT NOT NULL, created_at TEXT NOT NULL);
74
+ CREATE TABLE IF NOT EXISTS thinking_model_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, model_id TEXT NOT NULL, matched_pattern TEXT NOT NULL, created_at TEXT NOT NULL);
75
+ CREATE TABLE IF NOT EXISTS correction_samples (sample_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, bad_assistant_turn_id INTEGER NOT NULL, quality_score REAL, review_status TEXT, created_at TEXT NOT NULL);
76
+ CREATE TABLE IF NOT EXISTS task_outcomes (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, task_id TEXT, outcome TEXT NOT NULL, created_at TEXT NOT NULL);
77
+ `);
78
+ return { db, wsDir };
79
+ }
80
+
81
+ function seedSessions(db: Database.Database, count: number) {
82
+ for (let i = 0; i < count; i++) {
83
+ db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
84
+ .run(`sess-${i}`, '2026-04-01T00:00:00Z', '2026-04-01T00:10:00Z');
85
+ }
86
+ }
87
+
88
+ function seedToolCalls(db: Database.Database, successes = 0, failures = 0, errorType = 'ENOENT') {
89
+ for (let i = 0; i < successes; i++) {
90
+ db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, created_at) VALUES (?, ?, ?, ?, ?)')
91
+ .run('sess-0', 'write_file', 'success', 100, '2026-04-01T00:00:00Z');
92
+ }
93
+ for (let i = 0; i < failures; i++) {
94
+ db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, error_type, created_at) VALUES (?, ?, ?, ?, ?, ?)')
95
+ .run('sess-0', 'bash', 'failure', 100, errorType, '2026-04-01T00:00:00Z');
96
+ }
97
+ }
98
+
99
+ function seedPainEvents(db: Database.Database, count: number) {
100
+ for (let i = 0; i < count; i++) {
101
+ db.prepare('INSERT INTO pain_events (session_id, source, score, reason, created_at) VALUES (?, ?, ?, ?, ?)')
102
+ .run('sess-0', 'tool_failure', 75, 'test', '2026-04-01T00:00:00Z');
103
+ }
104
+ }
105
+
106
+ function seedCorrections(db: Database.Database, count: number) {
107
+ for (let i = 0; i < count; i++) {
108
+ db.prepare('INSERT INTO user_turns (session_id, correction_cue, correction_detected, created_at) VALUES (?, ?, 1, ?)')
109
+ .run('sess-0', 'stop doing that', '2026-04-01T00:00:00Z');
110
+ }
111
+ }
112
+
113
+ function seedPrincipleEvents(db: Database.Database, count: number) {
114
+ for (let i = 0; i < count; i++) {
115
+ db.prepare('INSERT INTO principle_events (principle_id, event_type, created_at) VALUES (?, ?, ?)')
116
+ .run(`p-${i}`, 'candidate_created', '2026-04-01T00:00:00Z');
117
+ }
118
+ }
119
+
120
+ function seedThinkingEvents(db: Database.Database, count: number) {
121
+ for (let i = 0; i < count; i++) {
122
+ db.prepare('INSERT INTO thinking_model_events (session_id, model_id, matched_pattern, created_at) VALUES (?, ?, ?, ?)')
123
+ .run('sess-0', 'claude-3-5-sonnet', 'error_pattern', '2026-04-01T00:00:00Z');
124
+ }
125
+ }
126
+
127
+ function seedSamples(db: Database.Database) {
128
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
129
+ .run('s1', 'sess-0', 1, 0.9, 'pending', '2026-04-01T00:00:00Z');
130
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
131
+ .run('s2', 'sess-0', 1, 0.7, 'approved', '2026-04-01T00:00:00Z');
132
+ db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
133
+ .run('s3', 'sess-0', 1, 0.5, 'rejected', '2026-04-01T00:00:00Z');
134
+ }
135
+
136
+ function seedTaskOutcomes(db: Database.Database, count: number) {
137
+ for (let i = 0; i < count; i++) {
138
+ db.prepare('INSERT INTO task_outcomes (session_id, task_id, outcome, created_at) VALUES (?, ?, ?, ?)')
139
+ .run('sess-0', `task-${i}`, 'success', '2026-04-01T00:00:00Z');
140
+ }
141
+ }
142
+
143
+ // ── Tests ──────────────────────────────────────────────────────────────────
144
+
145
+ describe('CentralDatabase', () => {
146
+ test('creates schema on construction', () => {
147
+ const db = new CentralDatabase();
148
+ expect(() => db.getOverviewStats()).not.toThrow();
149
+ db.dispose();
150
+ });
151
+
152
+ test('isClosed reflects disposal state', () => {
153
+ const db = new CentralDatabase();
154
+ expect(db.isClosed).toBe(false);
155
+ db.dispose();
156
+ expect(db.isClosed).toBe(true);
157
+ });
158
+
159
+ test('dispose prevents further operations', () => {
160
+ const db = new CentralDatabase();
161
+ db.dispose();
162
+ expect(() => db.getOverviewStats()).toThrow();
163
+ });
164
+
165
+ test('getOverviewStats returns zeros when no data', () => {
166
+ const db = new CentralDatabase();
167
+ const stats = db.getOverviewStats();
168
+ expect(stats.totalSessions).toBe(0);
169
+ expect(stats.totalToolCalls).toBe(0);
170
+ expect(stats.totalFailures).toBe(0);
171
+ expect(stats.totalPainEvents).toBe(0);
172
+ db.dispose();
173
+ });
174
+
175
+ test('getTopRegressions returns empty array when no failures', () => {
176
+ const db = new CentralDatabase();
177
+ expect(db.getTopRegressions()).toEqual([]);
178
+ db.dispose();
179
+ });
180
+
181
+ test('addCustomWorkspace inserts and enables workspace', () => {
182
+ const db = new CentralDatabase();
183
+ db.addCustomWorkspace('my-ws', '/custom/path');
184
+ const configs = db.getWorkspaceConfigs();
185
+ const found = configs.find(c => c.workspaceName === 'my-ws');
186
+ expect(found).toBeDefined();
187
+ expect(found?.enabled).toBe(true);
188
+ expect(found?.syncEnabled).toBe(true);
189
+ db.dispose();
190
+ });
191
+
192
+ test('updateWorkspaceConfig updates displayName and enabled', () => {
193
+ const db = new CentralDatabase();
194
+ db.addCustomWorkspace('cfg-ws', '/path');
195
+ db.updateWorkspaceConfig('cfg-ws', { displayName: 'Display', enabled: false });
196
+ const configs = db.getWorkspaceConfigs();
197
+ const found = configs.find(c => c.workspaceName === 'cfg-ws');
198
+ expect(found?.displayName).toBe('Display');
199
+ expect(found?.enabled).toBe(false);
200
+ db.dispose();
201
+ });
202
+
203
+ test('isWorkspaceEnabled returns true when no config', () => {
204
+ const db = new CentralDatabase();
205
+ expect(db.isWorkspaceEnabled('unknown')).toBe(true);
206
+ db.dispose();
207
+ });
208
+
209
+ test('isWorkspaceEnabled returns false when disabled', () => {
210
+ const db = new CentralDatabase();
211
+ db.addCustomWorkspace('dis-ws', '/path');
212
+ db.updateWorkspaceConfig('dis-ws', { enabled: false });
213
+ expect(db.isWorkspaceEnabled('dis-ws')).toBe(false);
214
+ db.dispose();
215
+ });
216
+
217
+ test('removeWorkspace disables workspace', () => {
218
+ const db = new CentralDatabase();
219
+ db.addCustomWorkspace('rem-ws', '/path');
220
+ db.removeWorkspace('rem-ws');
221
+ expect(db.isWorkspaceEnabled('rem-ws')).toBe(false);
222
+ db.dispose();
223
+ });
224
+
225
+ test('setGlobalConfig and getGlobalConfig round-trip', () => {
226
+ const db = new CentralDatabase();
227
+ db.setGlobalConfig('key', 'value');
228
+ expect(db.getGlobalConfig('key')).toBe('value');
229
+ db.setGlobalConfig('key', 'value2');
230
+ expect(db.getGlobalConfig('key')).toBe('value2');
231
+ expect(db.getGlobalConfig('missing')).toBeNull();
232
+ db.dispose();
233
+ });
234
+
235
+ test('clearAll deletes all aggregated data', () => {
236
+ const db = new CentralDatabase();
237
+ db.clearAll();
238
+ expect(db.getOverviewStats().totalSessions).toBe(0);
239
+ db.dispose();
240
+ });
241
+
242
+ // ── syncWorkspace ─────────────────────────────────────────────────────────
243
+
244
+ test('syncWorkspace throws for unknown workspace', () => {
245
+ const db = new CentralDatabase();
246
+ expect(() => db.syncWorkspace('nonexistent')).toThrow();
247
+ db.dispose();
248
+ });
249
+
250
+ test('syncWorkspace returns 0 when no trajectory.db', () => {
251
+ const wsName = makeWorkspaceName('cdb-test-empty');
252
+ const db = new CentralDatabase();
253
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
254
+ // Create workspace dir without trajectory.db
255
+ mkdirSync(join(OPENCLAW_DIR, wsName, '.state'), { recursive: true });
256
+ expect(db.syncWorkspace(wsName)).toBe(0);
257
+ db.dispose();
258
+ });
259
+
260
+ test('syncWorkspace syncs sessions', () => {
261
+ const wsName = makeWorkspaceName('cdb-test-sessions');
262
+ const { db: trajDb } = makeTrajectoryDb(wsName);
263
+ seedSessions(trajDb, 5);
264
+ trajDb.close();
265
+ const db = new CentralDatabase();
266
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
267
+ db.syncWorkspace(wsName);
268
+ expect(db.getOverviewStats().totalSessions).toBeGreaterThanOrEqual(5);
269
+ db.dispose();
270
+ });
271
+
272
+ test('syncWorkspace syncs tool calls with failures', () => {
273
+ const wsName = makeWorkspaceName('cdb-test-tools');
274
+ const { db: trajDb } = makeTrajectoryDb(wsName);
275
+ seedSessions(trajDb, 1);
276
+ seedToolCalls(trajDb, 3, 2, 'ENOENT');
277
+ trajDb.close();
278
+ const db = new CentralDatabase();
279
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
280
+ db.syncWorkspace(wsName);
281
+ const stats = db.getOverviewStats();
282
+ expect(stats.totalToolCalls).toBeGreaterThanOrEqual(5);
283
+ expect(stats.totalFailures).toBeGreaterThanOrEqual(2);
284
+ db.dispose();
285
+ });
286
+
287
+ test('syncWorkspace syncs pain events', () => {
288
+ const wsName = makeWorkspaceName('cdb-test-pain');
289
+ const { db: trajDb } = makeTrajectoryDb(wsName);
290
+ seedSessions(trajDb, 1);
291
+ seedPainEvents(trajDb, 4);
292
+ trajDb.close();
293
+ const db = new CentralDatabase();
294
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
295
+ db.syncWorkspace(wsName);
296
+ expect(db.getOverviewStats().totalPainEvents).toBeGreaterThanOrEqual(4);
297
+ db.dispose();
298
+ });
299
+
300
+ test('syncWorkspace syncs user corrections', () => {
301
+ const wsName = makeWorkspaceName('cdb-test-corr');
302
+ const { db: trajDb } = makeTrajectoryDb(wsName);
303
+ seedSessions(trajDb, 1);
304
+ seedCorrections(trajDb, 3);
305
+ trajDb.close();
306
+ const db = new CentralDatabase();
307
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
308
+ db.syncWorkspace(wsName);
309
+ expect(db.getOverviewStats().totalCorrections).toBeGreaterThanOrEqual(3);
310
+ db.dispose();
311
+ });
312
+
313
+ test('syncWorkspace syncs principle events', () => {
314
+ const wsName = makeWorkspaceName('cdb-test-principle');
315
+ const { db: trajDb } = makeTrajectoryDb(wsName);
316
+ seedSessions(trajDb, 1);
317
+ seedPrincipleEvents(trajDb, 2);
318
+ trajDb.close();
319
+ const db = new CentralDatabase();
320
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
321
+ db.syncWorkspace(wsName);
322
+ expect(db.getPrincipleEventCount()).toBeGreaterThanOrEqual(2);
323
+ db.dispose();
324
+ });
325
+
326
+ test('syncWorkspace syncs thinking model events', () => {
327
+ const wsName = makeWorkspaceName('cdb-test-thinking');
328
+ const { db: trajDb } = makeTrajectoryDb(wsName);
329
+ seedSessions(trajDb, 1);
330
+ seedThinkingEvents(trajDb, 3);
331
+ trajDb.close();
332
+ const db = new CentralDatabase();
333
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
334
+ db.syncWorkspace(wsName);
335
+ expect(db.getThinkingModelStats().totalModels).toBeGreaterThanOrEqual(1);
336
+ db.dispose();
337
+ });
338
+
339
+ test('syncWorkspace syncs correction samples with review_status', () => {
340
+ const wsName = makeWorkspaceName('cdb-test-samples');
341
+ const { db: trajDb } = makeTrajectoryDb(wsName);
342
+ seedSessions(trajDb, 1);
343
+ seedSamples(trajDb);
344
+ trajDb.close();
345
+ const db = new CentralDatabase();
346
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
347
+ db.syncWorkspace(wsName);
348
+ const counters = db.getSampleCountersByStatus();
349
+ expect(counters['pending']).toBeGreaterThanOrEqual(1);
350
+ expect(counters['approved']).toBeGreaterThanOrEqual(1);
351
+ expect(counters['rejected']).toBeGreaterThanOrEqual(1);
352
+ db.dispose();
353
+ });
354
+
355
+ test('syncWorkspace syncs task outcomes', () => {
356
+ const wsName = makeWorkspaceName('cdb-test-tasks');
357
+ const { db: trajDb } = makeTrajectoryDb(wsName);
358
+ seedSessions(trajDb, 1);
359
+ seedTaskOutcomes(trajDb, 4);
360
+ trajDb.close();
361
+ const db = new CentralDatabase();
362
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
363
+ db.syncWorkspace(wsName);
364
+ expect(db.getTaskOutcomes()).toBeGreaterThanOrEqual(4);
365
+ db.dispose();
366
+ });
367
+
368
+ test('syncWorkspace updates last_sync timestamp', () => {
369
+ const wsName = makeWorkspaceName('cdb-test-sync');
370
+ const { db: trajDb } = makeTrajectoryDb(wsName);
371
+ seedSessions(trajDb, 1);
372
+ trajDb.close();
373
+ const db = new CentralDatabase();
374
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
375
+ db.syncWorkspace(wsName);
376
+ const ws = db.getWorkspaces().find(w => w.name === wsName);
377
+ expect(ws?.lastSync).not.toBeNull();
378
+ db.dispose();
379
+ });
380
+
381
+ test('getTopRegressions returns error types ordered by occurrences', () => {
382
+ const wsName = makeWorkspaceName('cdb-test-regr');
383
+ const { db: trajDb } = makeTrajectoryDb(wsName);
384
+ seedSessions(trajDb, 1);
385
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
386
+ .run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:00:00Z');
387
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
388
+ .run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:01:00Z');
389
+ trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
390
+ .run('sess-0', 'bash', 'failure', 'EPERM', 100, '2026-04-01T00:02:00Z');
391
+ trajDb.close();
392
+ const db = new CentralDatabase();
393
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
394
+ db.syncWorkspace(wsName);
395
+ const regressions = db.getTopRegressions(5);
396
+ expect(regressions[0].errorType).toBe('ENOENT');
397
+ expect(regressions[0].occurrences).toBe(2);
398
+ db.dispose();
399
+ });
400
+
401
+ test('getSamplePreview returns recent pending/approved samples', () => {
402
+ const wsName = makeWorkspaceName('cdb-test-preview');
403
+ const { db: trajDb } = makeTrajectoryDb(wsName);
404
+ seedSessions(trajDb, 1);
405
+ seedSamples(trajDb);
406
+ trajDb.close();
407
+ const db = new CentralDatabase();
408
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
409
+ db.syncWorkspace(wsName);
410
+ const preview = db.getSamplePreview(5);
411
+ expect(preview.length).toBeGreaterThanOrEqual(2);
412
+ db.dispose();
413
+ });
414
+
415
+ test('getMostRecentSync returns null when no sync', () => {
416
+ const db = new CentralDatabase();
417
+ expect(db.getMostRecentSync()).toBeNull();
418
+ db.dispose();
419
+ });
420
+
421
+ test('getMostRecentSync returns timestamp after sync', () => {
422
+ const wsName = makeWorkspaceName('cdb-test-recent');
423
+ const { db: trajDb } = makeTrajectoryDb(wsName);
424
+ seedSessions(trajDb, 1);
425
+ trajDb.close();
426
+ const db = new CentralDatabase();
427
+ db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
428
+ db.syncWorkspace(wsName);
429
+ expect(db.getMostRecentSync()).not.toBeNull();
430
+ db.dispose();
431
+ });
432
+
433
+ // ── Singleton ────────────────────────────────────────────────────────────
434
+
435
+ test('getCentralDatabase returns same instance', () => {
436
+ const db1 = getCentralDatabase();
437
+ const db2 = getCentralDatabase();
438
+ expect(db1).toBe(db2);
439
+ resetCentralDatabase();
440
+ });
441
+
442
+ test('resetCentralDatabase clears instance', () => {
443
+ const db1 = getCentralDatabase();
444
+ resetCentralDatabase();
445
+ const db2 = getCentralDatabase();
446
+ expect(db1).not.toBe(db2);
447
+ resetCentralDatabase();
448
+ });
449
+
450
+ test('getCentralDatabase reopens after close', () => {
451
+ const db1 = getCentralDatabase();
452
+ db1.dispose();
453
+ const db2 = getCentralDatabase();
454
+ expect(db2.isClosed).toBe(false);
455
+ resetCentralDatabase();
456
+ });
457
+ });
@@ -0,0 +1,173 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { WorkspaceContext } from '../../src/core/workspace-context.js';
6
+ import { ConfigService } from '../../src/core/config-service.js';
7
+
8
+ // Shared mocks
9
+ const mockLearner = {
10
+ getStore: vi.fn(() => ({ keywords: [{ term: 'wrong', weight: 0.5 }] })),
11
+ };
12
+
13
+ const mockDb = {
14
+ listRecentSessions: vi.fn(() => [{ sessionId: 'session-1' }]),
15
+ listUserTurnsForSession: vi.fn(() => [{ rawExcerpt: 'User said wrong input', correctionDetected: true, correctionCue: 'wrong' }]),
16
+ };
17
+
18
+ const mockOptimizationService = {
19
+ buildTrajectoryHistory: vi.fn(async () => [
20
+ { sessionId: 'session-1', timestamp: 'now', term: 'wrong', userMessage: '' }
21
+ ]),
22
+ applyResult: vi.fn(),
23
+ };
24
+
25
+ // Mock dependencies
26
+ vi.mock('../../src/core/correction-cue-learner.js', () => ({
27
+ CorrectionCueLearner: { get: vi.fn(() => mockLearner) },
28
+ }));
29
+
30
+ vi.mock('../../src/core/trajectory.js', () => ({
31
+ TrajectoryRegistry: {
32
+ get: vi.fn(() => mockDb),
33
+ clear: vi.fn(),
34
+ },
35
+ }));
36
+
37
+ vi.mock('../../src/service/keyword-optimization-service.js', () => ({
38
+ KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
39
+ }));
40
+
41
+ // Mock principles-core runtime-v2 observer/scheduler classes
42
+ const mockDispatch = vi.fn().mockResolvedValue({
43
+ updated: true,
44
+ summary: 'Keyword store optimized',
45
+ updates: { wrong: { action: 'update', weight: 0.4, reasoning: 'slightly high FP' } }
46
+ });
47
+
48
+ const mockRegister = vi.fn();
49
+
50
+ vi.mock('@principles/core/runtime-v2', () => {
51
+ return {
52
+ WorkflowFunnelLoader: class {
53
+ getFunnel = vi.fn(() => ({
54
+ policy: {
55
+ runtimeKind: 'pi-ai',
56
+ provider: 'anthropic',
57
+ model: 'anthropic/claude-3-5-sonnet',
58
+ apiKeyEnv: 'ANTHROPIC_API_KEY',
59
+ timeoutMs: 30000,
60
+ }
61
+ }));
62
+ },
63
+ PiAiRuntimeAdapter: class {},
64
+ CorrectionObserver: class {},
65
+ AgentScheduler: class {
66
+ register = mockRegister;
67
+ dispatch = mockDispatch;
68
+ }
69
+ };
70
+ });
71
+
72
+ // Import EvolutionWorkerService
73
+ import { EvolutionWorkerService } from '../../src/service/evolution-worker.js';
74
+ import { safeRmDir } from '../test-utils.js';
75
+
76
+ function createMockApi() {
77
+ return {
78
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
79
+ runtime: {
80
+ agent: { runEmbeddedPiAgent: vi.fn() },
81
+ system: {
82
+ requestHeartbeatNow: vi.fn(),
83
+ runHeartbeatOnce: vi.fn(),
84
+ },
85
+ },
86
+ } as any;
87
+ }
88
+
89
+ const fastPollConfig = { get: (k: string) => k === 'intervals.worker_poll_ms' ? 1000 : undefined };
90
+
91
+ describe('EvolutionWorkerService Correction Observer Integration', () => {
92
+ beforeEach(() => {
93
+ vi.useFakeTimers();
94
+ vi.clearAllMocks();
95
+ EvolutionWorkerService.api = null;
96
+ });
97
+
98
+ afterEach(() => {
99
+ vi.useRealTimers();
100
+ EvolutionWorkerService.api = null;
101
+ });
102
+
103
+ it('runs Correction Observer on heartbeat and applies updates when configured', async () => {
104
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-worker-corr-'));
105
+ const stateDir = path.join(workspaceDir, '.state');
106
+ fs.mkdirSync(stateDir, { recursive: true });
107
+
108
+ // Initialize an empty queue to avoid processing actual queue items
109
+ fs.writeFileSync(
110
+ path.join(stateDir, 'evolution_queue.json'),
111
+ JSON.stringify([], null, 2),
112
+ 'utf8'
113
+ );
114
+
115
+ // Initialize pain settings with fast poll interval
116
+ fs.writeFileSync(
117
+ path.join(stateDir, 'pain_settings.json'),
118
+ JSON.stringify({
119
+ intervals: {
120
+ worker_poll_ms: 1000
121
+ }
122
+ }, null, 2),
123
+ 'utf8'
124
+ );
125
+
126
+ // Invalidate workspace cache to load the newly written settings
127
+ WorkspaceContext.clearCache();
128
+ ConfigService.reset();
129
+
130
+ const mockApi = createMockApi();
131
+ EvolutionWorkerService.api = mockApi;
132
+
133
+ try {
134
+ EvolutionWorkerService.start({
135
+ workspaceDir,
136
+ stateDir,
137
+ logger: mockApi.logger,
138
+ config: fastPollConfig,
139
+ api: mockApi,
140
+ } as any);
141
+
142
+ // 1. Advance to startup timer (5000ms) and wait for microtasks to settle
143
+ await vi.advanceTimersByTimeAsync(5000);
144
+ for (let i = 0; i < 20; i++) {
145
+ await Promise.resolve();
146
+ }
147
+
148
+ // 2. Advance past the poll interval (1000ms) to trigger runCycle
149
+ await vi.advanceTimersByTimeAsync(1050);
150
+ for (let i = 0; i < 20; i++) {
151
+ await Promise.resolve();
152
+ }
153
+
154
+ // Verify that the scheduler dispatch was called with correct payload
155
+ expect(mockRegister).toHaveBeenCalled();
156
+ expect(mockDispatch).toHaveBeenCalledWith('correction-observer', expect.objectContaining({
157
+ parentSessionId: 'evolution-worker',
158
+ workspaceDir,
159
+ recentMessages: ['User said wrong input'],
160
+ }));
161
+
162
+ // Verify updates were applied using KeywordOptimizationService
163
+ expect(mockOptimizationService.applyResult).toHaveBeenCalledWith(expect.objectContaining({
164
+ updated: true,
165
+ summary: 'Keyword store optimized',
166
+ }));
167
+
168
+ } finally {
169
+ EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
170
+ safeRmDir(workspaceDir);
171
+ }
172
+ });
173
+ });