principles-disciple 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADVANCED_CONFIG_ZH.md +97 -0
- package/AGENT_INSTALL.md +173 -0
- package/AGENT_INSTALL_EN.md +173 -0
- package/INSTALL.md +256 -0
- package/SKILL.md +63 -0
- package/docs/COMMAND_REFERENCE.md +76 -0
- package/docs/COMMAND_REFERENCE_EN.md +79 -0
- package/esbuild.config.js +75 -0
- package/openclaw.plugin.json +6 -1
- package/package.json +13 -15
- package/scripts/build-web.mjs +46 -0
- package/scripts/install-dependencies.cjs +47 -0
- package/scripts/sync-plugin.mjs +802 -0
- package/scripts/verify-build.mjs +109 -0
- package/src/agents/nocturnal-dreamer.md +152 -0
- package/src/agents/nocturnal-philosopher.md +138 -0
- package/src/agents/nocturnal-reflector.md +126 -0
- package/src/agents/nocturnal-scribe.md +164 -0
- package/src/commands/capabilities.ts +85 -0
- package/{dist/commands/context.js → src/commands/context.ts} +78 -38
- package/src/commands/evolution-status.ts +146 -0
- package/src/commands/export.ts +111 -0
- package/src/commands/focus.ts +533 -0
- package/src/commands/nocturnal-review.ts +311 -0
- package/src/commands/nocturnal-rollout.ts +763 -0
- package/src/commands/nocturnal-train.ts +1002 -0
- package/{dist/commands/pain.js → src/commands/pain.ts} +68 -49
- package/src/commands/principle-rollback.ts +27 -0
- package/{dist/commands/rollback.js → src/commands/rollback.ts} +44 -12
- package/src/commands/samples.ts +60 -0
- package/src/commands/strategy.ts +38 -0
- package/{dist/commands/thinking-os.js → src/commands/thinking-os.ts} +59 -36
- package/src/commands/workflow-debug.ts +128 -0
- package/{dist/config/defaults/runtime.js → src/config/defaults/runtime.ts} +12 -5
- package/src/config/errors.ts +163 -0
- package/{dist/config/index.d.ts → src/config/index.ts} +2 -1
- package/src/constants/diagnostician.ts +66 -0
- package/src/constants/tools.ts +62 -0
- package/src/core/adaptive-thresholds.ts +476 -0
- package/{dist/core/config-service.js → src/core/config-service.ts} +7 -4
- package/{dist/core/config.js → src/core/config.ts} +158 -46
- package/src/core/control-ui-db.ts +435 -0
- package/{dist/core/detection-funnel.js → src/core/detection-funnel.ts} +36 -21
- package/{dist/core/detection-service.js → src/core/detection-service.ts} +7 -4
- package/{dist/core/dictionary-service.js → src/core/dictionary-service.ts} +7 -4
- package/{dist/core/dictionary.js → src/core/dictionary.ts} +57 -34
- package/src/core/empathy-keyword-matcher.ts +327 -0
- package/src/core/empathy-types.ts +218 -0
- package/src/core/event-log.ts +544 -0
- package/src/core/evolution-engine.ts +612 -0
- package/src/core/evolution-logger.ts +353 -0
- package/src/core/evolution-migration.ts +77 -0
- package/src/core/evolution-reducer.ts +731 -0
- package/src/core/evolution-types.ts +456 -0
- package/src/core/external-training-contract.ts +527 -0
- package/src/core/focus-history.ts +1458 -0
- package/src/core/hygiene/tracker.ts +117 -0
- package/{dist/core/init.js → src/core/init.ts} +39 -26
- package/src/core/local-worker-routing.ts +617 -0
- package/{dist/core/migration.js → src/core/migration.ts} +18 -11
- package/src/core/model-deployment-registry.ts +722 -0
- package/src/core/model-training-registry.ts +813 -0
- package/src/core/nocturnal-arbiter.ts +706 -0
- package/src/core/nocturnal-candidate-scoring.ts +392 -0
- package/src/core/nocturnal-compliance.ts +1075 -0
- package/src/core/nocturnal-dataset.ts +668 -0
- package/src/core/nocturnal-executability.ts +428 -0
- package/src/core/nocturnal-export.ts +390 -0
- package/{dist/core/nocturnal-paths.js → src/core/nocturnal-paths.ts} +49 -23
- package/src/core/nocturnal-trajectory-extractor.ts +484 -0
- package/src/core/nocturnal-trinity.ts +1384 -0
- package/src/core/pain.ts +122 -0
- package/{dist/core/path-resolver.js → src/core/path-resolver.ts} +157 -36
- package/{dist/core/paths.js → src/core/paths.ts} +13 -4
- package/src/core/principle-training-state.ts +450 -0
- package/src/core/profile.ts +226 -0
- package/src/core/promotion-gate.ts +822 -0
- package/{dist/core/risk-calculator.js → src/core/risk-calculator.ts} +42 -16
- package/{dist/core/session-tracker.js → src/core/session-tracker.ts} +185 -63
- package/src/core/shadow-observation-registry.ts +534 -0
- package/{dist/core/system-logger.js → src/core/system-logger.ts} +9 -5
- package/src/core/thinking-models.ts +217 -0
- package/src/core/training-program.ts +630 -0
- package/src/core/trajectory-types.ts +243 -0
- package/src/core/trajectory.ts +1673 -0
- package/{dist/core/workspace-context.js → src/core/workspace-context.ts} +57 -32
- package/src/hooks/bash-risk.ts +171 -0
- package/src/hooks/edit-verification.ts +295 -0
- package/src/hooks/gate-block-helper.ts +160 -0
- package/src/hooks/gate.ts +210 -0
- package/src/hooks/gfi-gate.ts +177 -0
- package/src/hooks/lifecycle.ts +326 -0
- package/{dist/hooks/llm.js → src/hooks/llm.ts} +166 -139
- package/src/hooks/message-sanitize.ts +45 -0
- package/src/hooks/pain.ts +384 -0
- package/src/hooks/progressive-trust-gate.ts +174 -0
- package/src/hooks/prompt.ts +920 -0
- package/src/hooks/subagent.ts +207 -0
- package/src/hooks/thinking-checkpoint.ts +73 -0
- package/src/hooks/trajectory-collector.ts +290 -0
- package/src/http/principles-console-route.ts +716 -0
- package/src/i18n/commands.ts +117 -0
- package/src/index.ts +694 -0
- package/src/service/central-database.ts +831 -0
- package/src/service/control-ui-query-service.ts +888 -0
- package/src/service/evolution-query-service.ts +405 -0
- package/src/service/evolution-worker.ts +1646 -0
- package/src/service/health-query-service.ts +836 -0
- package/{dist/service/nocturnal-runtime.js → src/service/nocturnal-runtime.ts} +263 -36
- package/src/service/nocturnal-service.ts +1015 -0
- package/src/service/nocturnal-target-selector.ts +532 -0
- package/src/service/phase3-input-filter.ts +237 -0
- package/src/service/runtime-summary-service.ts +757 -0
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +513 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +603 -0
- package/src/service/subagent-workflow/index.ts +51 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +856 -0
- package/src/service/subagent-workflow/runtime-direct-driver.ts +166 -0
- package/src/service/subagent-workflow/types.ts +378 -0
- package/src/service/subagent-workflow/workflow-store.ts +328 -0
- package/src/service/trajectory-service.ts +15 -0
- package/{dist/tools/critique-prompt.js → src/tools/critique-prompt.ts} +25 -8
- package/src/tools/deep-reflect.ts +349 -0
- package/{dist/tools/model-index.js → src/tools/model-index.ts} +33 -17
- package/src/types/event-types.ts +453 -0
- package/src/types/hygiene-types.ts +31 -0
- package/src/types/principle-tree-schema.ts +244 -0
- package/src/types/runtime-summary.ts +49 -0
- package/src/types.ts +74 -0
- package/src/utils/file-lock.ts +391 -0
- package/{dist/utils/glob-match.js → src/utils/glob-match.ts} +21 -20
- package/{dist/utils/hashing.js → src/utils/hashing.ts} +6 -4
- package/src/utils/io.ts +110 -0
- package/{dist/utils/nlp.js → src/utils/nlp.ts} +19 -12
- package/{dist/utils/plugin-logger.js → src/utils/plugin-logger.ts} +33 -8
- package/src/utils/subagent-probe.ts +94 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +70 -1
- package/templates/pain_settings.json +2 -1
- package/tests/README.md +120 -0
- package/tests/build-artifacts.test.ts +111 -0
- package/tests/commands/evolution-status.test.ts +222 -0
- package/tests/commands/evolver.test.ts +22 -0
- package/tests/commands/export.test.ts +78 -0
- package/tests/commands/nocturnal-review.test.ts +448 -0
- package/tests/commands/nocturnal-train.test.ts +97 -0
- package/tests/commands/pain.test.ts +108 -0
- package/tests/commands/samples.test.ts +65 -0
- package/tests/commands/strategy.test.ts +34 -0
- package/tests/commands/thinking-os.test.ts +88 -0
- package/tests/core/adaptive-thresholds.test.ts +261 -0
- package/tests/core/config-service.test.ts +89 -0
- package/tests/core/config.test.ts +90 -0
- package/tests/core/control-ui-db.test.ts +75 -0
- package/tests/core/core-template-guidance.test.ts +21 -0
- package/tests/core/detection-funnel.test.ts +63 -0
- package/tests/core/detection-service.test.ts +50 -0
- package/tests/core/dictionary-service.test.ts +116 -0
- package/tests/core/dictionary.test.ts +168 -0
- package/tests/core/empathy-keyword-matcher.test.ts +209 -0
- package/tests/core/event-log.test.ts +181 -0
- package/tests/core/evolution-e2e.test.ts +58 -0
- package/tests/core/evolution-engine-gate-integration.test.ts +543 -0
- package/tests/core/evolution-engine.test.ts +562 -0
- package/tests/core/evolution-logger.test.ts +148 -0
- package/tests/core/evolution-migration.test.ts +50 -0
- package/tests/core/evolution-paths.test.ts +21 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +602 -0
- package/tests/core/evolution-reducer.test.ts +180 -0
- package/tests/core/evolution-types-loop.test.ts +48 -0
- package/tests/core/evolution-user-stories.e2e.test.ts +249 -0
- package/tests/core/external-training-contract.test.ts +463 -0
- package/tests/core/focus-history.test.ts +682 -0
- package/tests/core/init-flatten.test.ts +69 -0
- package/tests/core/init-refactor.test.ts +87 -0
- package/tests/core/init-v1.3.test.ts +46 -0
- package/tests/core/init.test.ts +190 -0
- package/tests/core/local-worker-routing.test.ts +757 -0
- package/tests/core/migration.test.ts +84 -0
- package/tests/core/model-deployment-registry.test.ts +845 -0
- package/tests/core/model-training-registry.test.ts +889 -0
- package/tests/core/nocturnal-arbiter.test.ts +494 -0
- package/tests/core/nocturnal-candidate-scoring.test.ts +400 -0
- package/tests/core/nocturnal-compliance.test.ts +646 -0
- package/tests/core/nocturnal-dataset.test.ts +892 -0
- package/tests/core/nocturnal-executability.test.ts +357 -0
- package/tests/core/nocturnal-export.test.ts +462 -0
- package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +428 -0
- package/tests/core/nocturnal-trajectory-extractor.test.ts +634 -0
- package/tests/core/nocturnal-trinity.test.ts +953 -0
- package/tests/core/pain.test.ts +33 -0
- package/tests/core/path-resolver.test.ts +57 -0
- package/tests/core/paths-refactor.test.ts +42 -0
- package/tests/core/phase7-rollout-integration.test.ts +477 -0
- package/tests/core/principle-training-state.test.ts +712 -0
- package/tests/core/profile.test.ts +56 -0
- package/tests/core/promotion-gate.test.ts +556 -0
- package/tests/core/risk-calculator.test.ts +168 -0
- package/tests/core/session-tracker.test.ts +191 -0
- package/tests/core/training-program.test.ts +472 -0
- package/tests/core/trajectory.test.ts +265 -0
- package/tests/core/workspace-context-factory.test.ts +18 -0
- package/tests/core/workspace-context.test.ts +134 -0
- package/tests/fixtures/nocturnal-reviewed-subset.json +183 -0
- package/tests/fixtures/production-compatibility.test.ts +147 -0
- package/tests/fixtures/production-mock-generator.ts +282 -0
- package/tests/hooks/bash-risk-integration.test.ts +137 -0
- package/tests/hooks/bash-risk.test.ts +81 -0
- package/tests/hooks/edit-verification.test.ts +678 -0
- package/tests/hooks/gate-edit-verification-p1.test.ts +632 -0
- package/tests/hooks/gate-edit-verification.test.ts +435 -0
- package/tests/hooks/gate-pipeline-integration.test.ts +404 -0
- package/tests/hooks/gate.test.ts +271 -0
- package/tests/hooks/gfi-gate-unit.test.ts +422 -0
- package/tests/hooks/gfi-gate.test.ts +669 -0
- package/tests/hooks/lifecycle.test.ts +248 -0
- package/tests/hooks/llm.test.ts +308 -0
- package/tests/hooks/message-sanitize.test.ts +36 -0
- package/tests/hooks/pain.test.ts +141 -0
- package/tests/hooks/progressive-trust-gate.test.ts +277 -0
- package/tests/hooks/prompt.test.ts +1411 -0
- package/tests/hooks/subagent.test.ts +467 -0
- package/tests/hooks/thinking-gate.test.ts +313 -0
- package/tests/http/principles-console-route.test.ts +140 -0
- package/tests/hygiene-tracker.test.ts +77 -0
- package/tests/index.integration.test.ts +179 -0
- package/tests/index.shadow-routing.integration.test.ts +140 -0
- package/tests/index.test.ts +9 -0
- package/tests/integration/empathy-workflow-integration.test.ts +627 -0
- package/tests/service/control-ui-query-service.test.ts +121 -0
- package/tests/service/empathy-observer-workflow-manager.test.ts +176 -0
- package/tests/service/evolution-worker.test.ts +585 -0
- package/tests/service/nocturnal-runtime.test.ts +470 -0
- package/tests/service/nocturnal-service.test.ts +577 -0
- package/tests/service/nocturnal-target-selector.test.ts +615 -0
- package/tests/service/nocturnal-workflow-manager.test.ts +439 -0
- package/tests/service/phase3-input-filter.test.ts +289 -0
- package/tests/service/runtime-summary-service.test.ts +919 -0
- package/tests/task-compliance.test.ts +166 -0
- package/tests/test-utils.ts +48 -0
- package/tests/tools/critique-prompt.test.ts +260 -0
- package/tests/tools/deep-reflect.test.ts +232 -0
- package/tests/tools/model-index.test.ts +246 -0
- package/tests/ui/app.test.tsx +114 -0
- package/tests/utils/file-lock.test.ts +407 -0
- package/tests/utils/hashing.test.ts +32 -0
- package/tests/utils/io.test.ts +39 -0
- package/tests/utils/nlp.test.ts +53 -0
- package/tests/utils/plugin-logger.test.ts +156 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/ui/src/App.tsx +45 -0
- package/ui/src/api.ts +216 -0
- package/ui/src/charts.tsx +586 -0
- package/ui/src/components/ErrorState.tsx +6 -0
- package/ui/src/components/Loading.tsx +13 -0
- package/ui/src/components/ProtectedRoute.tsx +12 -0
- package/ui/src/components/Shell.tsx +91 -0
- package/ui/src/components/WorkspaceConfig.tsx +146 -0
- package/ui/src/components/index.ts +5 -0
- package/ui/src/context/auth.tsx +80 -0
- package/ui/src/context/theme.tsx +66 -0
- package/ui/src/hooks/useAutoRefresh.ts +39 -0
- package/ui/src/i18n/ui.ts +363 -0
- package/ui/src/main.tsx +16 -0
- package/ui/src/pages/EvolutionPage.tsx +352 -0
- package/ui/src/pages/FeedbackPage.tsx +140 -0
- package/ui/src/pages/GateMonitorPage.tsx +136 -0
- package/ui/src/pages/LoginPage.tsx +88 -0
- package/ui/src/pages/OverviewPage.tsx +238 -0
- package/ui/src/pages/SamplesPage.tsx +174 -0
- package/ui/src/pages/ThinkingModelsPage.tsx +127 -0
- package/ui/src/styles.css +1661 -0
- package/ui/src/types.ts +368 -0
- package/ui/src/utils/format.ts +15 -0
- package/vitest.config.ts +23 -0
- package/dist/commands/capabilities.d.ts +0 -3
- package/dist/commands/capabilities.js +0 -73
- package/dist/commands/context.d.ts +0 -5
- package/dist/commands/evolution-status.d.ts +0 -4
- package/dist/commands/evolution-status.js +0 -117
- package/dist/commands/evolver.d.ts +0 -9
- package/dist/commands/evolver.js +0 -26
- package/dist/commands/export.d.ts +0 -2
- package/dist/commands/export.js +0 -98
- package/dist/commands/focus.d.ts +0 -14
- package/dist/commands/focus.js +0 -457
- package/dist/commands/nocturnal-review.d.ts +0 -24
- package/dist/commands/nocturnal-review.js +0 -265
- package/dist/commands/nocturnal-rollout.d.ts +0 -27
- package/dist/commands/nocturnal-rollout.js +0 -671
- package/dist/commands/nocturnal-train.d.ts +0 -25
- package/dist/commands/nocturnal-train.js +0 -919
- package/dist/commands/pain.d.ts +0 -5
- package/dist/commands/principle-rollback.d.ts +0 -4
- package/dist/commands/principle-rollback.js +0 -22
- package/dist/commands/rollback.d.ts +0 -19
- package/dist/commands/samples.d.ts +0 -2
- package/dist/commands/samples.js +0 -55
- package/dist/commands/strategy.d.ts +0 -3
- package/dist/commands/strategy.js +0 -29
- package/dist/commands/thinking-os.d.ts +0 -2
- package/dist/config/defaults/runtime.d.ts +0 -40
- package/dist/config/errors.d.ts +0 -84
- package/dist/config/errors.js +0 -94
- package/dist/config/index.js +0 -7
- package/dist/constants/diagnostician.d.ts +0 -12
- package/dist/constants/diagnostician.js +0 -56
- package/dist/constants/tools.d.ts +0 -17
- package/dist/constants/tools.js +0 -54
- package/dist/core/adaptive-thresholds.d.ts +0 -186
- package/dist/core/adaptive-thresholds.js +0 -300
- package/dist/core/config-service.d.ts +0 -15
- package/dist/core/config.d.ts +0 -127
- package/dist/core/control-ui-db.d.ts +0 -95
- package/dist/core/control-ui-db.js +0 -292
- package/dist/core/detection-funnel.d.ts +0 -33
- package/dist/core/detection-service.d.ts +0 -15
- package/dist/core/dictionary-service.d.ts +0 -15
- package/dist/core/dictionary.d.ts +0 -38
- package/dist/core/event-log.d.ts +0 -82
- package/dist/core/event-log.js +0 -463
- package/dist/core/evolution-engine.d.ts +0 -118
- package/dist/core/evolution-engine.js +0 -464
- package/dist/core/evolution-logger.d.ts +0 -137
- package/dist/core/evolution-logger.js +0 -256
- package/dist/core/evolution-migration.d.ts +0 -5
- package/dist/core/evolution-migration.js +0 -65
- package/dist/core/evolution-reducer.d.ts +0 -98
- package/dist/core/evolution-reducer.js +0 -465
- package/dist/core/evolution-types.d.ts +0 -287
- package/dist/core/evolution-types.js +0 -78
- package/dist/core/external-training-contract.d.ts +0 -276
- package/dist/core/external-training-contract.js +0 -269
- package/dist/core/focus-history.d.ts +0 -210
- package/dist/core/focus-history.js +0 -1185
- package/dist/core/hygiene/tracker.d.ts +0 -22
- package/dist/core/hygiene/tracker.js +0 -106
- package/dist/core/init.d.ts +0 -12
- package/dist/core/local-worker-routing.d.ts +0 -175
- package/dist/core/local-worker-routing.js +0 -525
- package/dist/core/migration.d.ts +0 -6
- package/dist/core/model-deployment-registry.d.ts +0 -218
- package/dist/core/model-deployment-registry.js +0 -503
- package/dist/core/model-training-registry.d.ts +0 -295
- package/dist/core/model-training-registry.js +0 -475
- package/dist/core/nocturnal-arbiter.d.ts +0 -159
- package/dist/core/nocturnal-arbiter.js +0 -534
- package/dist/core/nocturnal-candidate-scoring.d.ts +0 -137
- package/dist/core/nocturnal-candidate-scoring.js +0 -266
- package/dist/core/nocturnal-compliance.d.ts +0 -175
- package/dist/core/nocturnal-compliance.js +0 -824
- package/dist/core/nocturnal-dataset.d.ts +0 -224
- package/dist/core/nocturnal-dataset.js +0 -443
- package/dist/core/nocturnal-executability.d.ts +0 -85
- package/dist/core/nocturnal-executability.js +0 -331
- package/dist/core/nocturnal-export.d.ts +0 -124
- package/dist/core/nocturnal-export.js +0 -275
- package/dist/core/nocturnal-paths.d.ts +0 -124
- package/dist/core/nocturnal-trajectory-extractor.d.ts +0 -242
- package/dist/core/nocturnal-trajectory-extractor.js +0 -307
- package/dist/core/nocturnal-trinity.d.ts +0 -311
- package/dist/core/nocturnal-trinity.js +0 -880
- package/dist/core/pain.d.ts +0 -4
- package/dist/core/pain.js +0 -70
- package/dist/core/path-resolver.d.ts +0 -46
- package/dist/core/paths.d.ts +0 -65
- package/dist/core/principle-training-state.d.ts +0 -121
- package/dist/core/principle-training-state.js +0 -321
- package/dist/core/profile.d.ts +0 -62
- package/dist/core/profile.js +0 -210
- package/dist/core/promotion-gate.d.ts +0 -238
- package/dist/core/promotion-gate.js +0 -529
- package/dist/core/risk-calculator.d.ts +0 -22
- package/dist/core/session-tracker.d.ts +0 -99
- package/dist/core/shadow-observation-registry.d.ts +0 -217
- package/dist/core/shadow-observation-registry.js +0 -308
- package/dist/core/system-logger.d.ts +0 -8
- package/dist/core/thinking-models.d.ts +0 -38
- package/dist/core/thinking-models.js +0 -170
- package/dist/core/training-program.d.ts +0 -233
- package/dist/core/training-program.js +0 -433
- package/dist/core/trajectory.d.ts +0 -411
- package/dist/core/trajectory.js +0 -1307
- package/dist/core/workspace-context.d.ts +0 -71
- package/dist/hooks/bash-risk.d.ts +0 -57
- package/dist/hooks/bash-risk.js +0 -137
- package/dist/hooks/edit-verification.d.ts +0 -62
- package/dist/hooks/edit-verification.js +0 -256
- package/dist/hooks/gate-block-helper.d.ts +0 -44
- package/dist/hooks/gate-block-helper.js +0 -119
- package/dist/hooks/gate.d.ts +0 -24
- package/dist/hooks/gate.js +0 -173
- package/dist/hooks/gfi-gate.d.ts +0 -40
- package/dist/hooks/gfi-gate.js +0 -113
- package/dist/hooks/lifecycle.d.ts +0 -5
- package/dist/hooks/lifecycle.js +0 -284
- package/dist/hooks/llm.d.ts +0 -12
- package/dist/hooks/message-sanitize.d.ts +0 -3
- package/dist/hooks/message-sanitize.js +0 -37
- package/dist/hooks/pain.d.ts +0 -5
- package/dist/hooks/pain.js +0 -301
- package/dist/hooks/progressive-trust-gate.d.ts +0 -51
- package/dist/hooks/progressive-trust-gate.js +0 -89
- package/dist/hooks/prompt.d.ts +0 -47
- package/dist/hooks/prompt.js +0 -884
- package/dist/hooks/subagent.d.ts +0 -10
- package/dist/hooks/subagent.js +0 -387
- package/dist/hooks/thinking-checkpoint.d.ts +0 -37
- package/dist/hooks/thinking-checkpoint.js +0 -51
- package/dist/hooks/trajectory-collector.d.ts +0 -32
- package/dist/hooks/trajectory-collector.js +0 -256
- package/dist/http/principles-console-route.d.ts +0 -9
- package/dist/http/principles-console-route.js +0 -567
- package/dist/i18n/commands.d.ts +0 -26
- package/dist/i18n/commands.js +0 -116
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -581
- package/dist/service/central-database.d.ts +0 -104
- package/dist/service/central-database.js +0 -649
- package/dist/service/control-ui-query-service.d.ts +0 -221
- package/dist/service/control-ui-query-service.js +0 -543
- package/dist/service/empathy-observer-manager.d.ts +0 -52
- package/dist/service/empathy-observer-manager.js +0 -229
- package/dist/service/evolution-query-service.d.ts +0 -155
- package/dist/service/evolution-query-service.js +0 -258
- package/dist/service/evolution-worker.d.ts +0 -101
- package/dist/service/evolution-worker.js +0 -974
- package/dist/service/nocturnal-runtime.d.ts +0 -183
- package/dist/service/nocturnal-service.d.ts +0 -163
- package/dist/service/nocturnal-service.js +0 -787
- package/dist/service/nocturnal-target-selector.d.ts +0 -145
- package/dist/service/nocturnal-target-selector.js +0 -315
- package/dist/service/phase3-input-filter.d.ts +0 -73
- package/dist/service/phase3-input-filter.js +0 -172
- package/dist/service/runtime-summary-service.d.ts +0 -122
- package/dist/service/runtime-summary-service.js +0 -485
- package/dist/service/trajectory-service.d.ts +0 -2
- package/dist/service/trajectory-service.js +0 -15
- package/dist/tools/critique-prompt.d.ts +0 -14
- package/dist/tools/deep-reflect.d.ts +0 -39
- package/dist/tools/deep-reflect.js +0 -350
- package/dist/tools/model-index.d.ts +0 -9
- package/dist/types/event-types.d.ts +0 -306
- package/dist/types/event-types.js +0 -106
- package/dist/types/hygiene-types.d.ts +0 -20
- package/dist/types/hygiene-types.js +0 -12
- package/dist/types/runtime-summary.d.ts +0 -47
- package/dist/types/runtime-summary.js +0 -1
- package/dist/types.d.ts +0 -50
- package/dist/types.js +0 -22
- package/dist/utils/file-lock.d.ts +0 -71
- package/dist/utils/file-lock.js +0 -309
- package/dist/utils/glob-match.d.ts +0 -28
- package/dist/utils/hashing.d.ts +0 -9
- package/dist/utils/io.d.ts +0 -6
- package/dist/utils/io.js +0 -106
- package/dist/utils/nlp.d.ts +0 -9
- package/dist/utils/plugin-logger.d.ts +0 -39
- package/dist/utils/subagent-probe.d.ts +0 -34
- package/dist/utils/subagent-probe.js +0 -81
|
@@ -1,974 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
import { DictionaryService } from '../core/dictionary-service.js';
|
|
5
|
-
import { DetectionService } from '../core/detection-service.js';
|
|
6
|
-
import { ensureStateTemplates } from '../core/init.js';
|
|
7
|
-
import { extractCommonSubstring } from '../utils/nlp.js';
|
|
8
|
-
import { SystemLogger } from '../core/system-logger.js';
|
|
9
|
-
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
10
|
-
import { initPersistence, flushAllSessions } from '../core/session-tracker.js';
|
|
11
|
-
import { acquireLockAsync, releaseLock } from '../utils/file-lock.js';
|
|
12
|
-
import { getEvolutionLogger } from '../core/evolution-logger.js';
|
|
13
|
-
import { DIAGNOSTICIAN_PROTOCOL_SUMMARY } from '../constants/diagnostician.js';
|
|
14
|
-
import { LockUnavailableError } from '../config/index.js';
|
|
15
|
-
import { checkWorkspaceIdle, checkCooldown } from './nocturnal-runtime.js';
|
|
16
|
-
import { executeNocturnalReflectionAsync } from './nocturnal-service.js';
|
|
17
|
-
import { OpenClawTrinityRuntimeAdapter } from '../core/nocturnal-trinity.js';
|
|
18
|
-
let intervalId = null;
|
|
19
|
-
let timeoutId = null;
|
|
20
|
-
/**
|
|
21
|
-
* Default values for new V2 fields when migrating legacy items.
|
|
22
|
-
*/
|
|
23
|
-
const DEFAULT_TASK_KIND = 'pain_diagnosis';
|
|
24
|
-
const DEFAULT_PRIORITY = 'medium';
|
|
25
|
-
const DEFAULT_MAX_RETRIES = 3;
|
|
26
|
-
/**
|
|
27
|
-
* Migrate a legacy queue item to V2 schema.
|
|
28
|
-
* Old items without taskKind are assumed to be pain_diagnosis for backward compatibility.
|
|
29
|
-
*/
|
|
30
|
-
function migrateToV2(item) {
|
|
31
|
-
return {
|
|
32
|
-
id: item.id,
|
|
33
|
-
taskKind: item.taskKind || DEFAULT_TASK_KIND,
|
|
34
|
-
priority: item.priority || DEFAULT_PRIORITY,
|
|
35
|
-
source: item.source,
|
|
36
|
-
traceId: item.traceId,
|
|
37
|
-
task: item.task,
|
|
38
|
-
score: item.score,
|
|
39
|
-
reason: item.reason,
|
|
40
|
-
timestamp: item.timestamp,
|
|
41
|
-
enqueued_at: item.enqueued_at,
|
|
42
|
-
started_at: item.started_at,
|
|
43
|
-
completed_at: item.completed_at,
|
|
44
|
-
assigned_session_key: item.assigned_session_key,
|
|
45
|
-
trigger_text_preview: item.trigger_text_preview,
|
|
46
|
-
status: item.status || 'pending',
|
|
47
|
-
resolution: item.resolution,
|
|
48
|
-
session_id: item.session_id,
|
|
49
|
-
agent_id: item.agent_id,
|
|
50
|
-
retryCount: item.retryCount || 0,
|
|
51
|
-
maxRetries: item.maxRetries || DEFAULT_MAX_RETRIES,
|
|
52
|
-
lastError: item.lastError,
|
|
53
|
-
resultRef: item.resultRef,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if an item is a legacy (pre-V2) queue item.
|
|
58
|
-
*/
|
|
59
|
-
function isLegacyQueueItem(item) {
|
|
60
|
-
return item && typeof item === 'object' && !('taskKind' in item);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Migrate entire queue to V2 schema if needed.
|
|
64
|
-
* Returns a new array with all items migrated to V2 format.
|
|
65
|
-
*/
|
|
66
|
-
function migrateQueueToV2(queue) {
|
|
67
|
-
return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item) : item);
|
|
68
|
-
}
|
|
69
|
-
const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
|
|
70
|
-
// P0 fix: File lock constants and helper for queue operations (prevents TOCTOU race)
|
|
71
|
-
export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
|
|
72
|
-
export const PAIN_CANDIDATES_LOCK_SUFFIX = '.candidates.lock';
|
|
73
|
-
export const LOCK_MAX_RETRIES = 50;
|
|
74
|
-
export const LOCK_RETRY_DELAY_MS = 50;
|
|
75
|
-
export const LOCK_STALE_MS = 30_000;
|
|
76
|
-
const PAIN_CANDIDATE_MAX_SAMPLES = 5;
|
|
77
|
-
const PAIN_CANDIDATE_SAMPLE_LEN = 1000;
|
|
78
|
-
const PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN = 160;
|
|
79
|
-
const PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN = 80;
|
|
80
|
-
export function createEvolutionTaskId(source, score, preview, reason, now) {
|
|
81
|
-
// Keep ids short for prompt injection, but include enough entropy to avoid
|
|
82
|
-
// collisions between different pain events that share the same source/score/preview.
|
|
83
|
-
return createHash('md5')
|
|
84
|
-
.update(`${source}:${score}:${preview}:${reason}:${now}`)
|
|
85
|
-
.digest('hex')
|
|
86
|
-
.substring(0, 8);
|
|
87
|
-
}
|
|
88
|
-
function normalizePainCandidateText(text) {
|
|
89
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
90
|
-
}
|
|
91
|
-
export function shouldTrackPainCandidate(text) {
|
|
92
|
-
const normalized = normalizePainCandidateText(text);
|
|
93
|
-
if (!normalized)
|
|
94
|
-
return false;
|
|
95
|
-
if (normalized === 'NO_REPLY')
|
|
96
|
-
return false;
|
|
97
|
-
// Skip empathy observer payloads: they are classifier telemetry, not user/system pain patterns.
|
|
98
|
-
if (normalized.startsWith('{')
|
|
99
|
-
&& normalized.endsWith('}')
|
|
100
|
-
&& normalized.includes('"damageDetected"')
|
|
101
|
-
&& normalized.includes('"severity"')
|
|
102
|
-
&& normalized.includes('"confidence"')) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
export function createPainCandidateFingerprint(text) {
|
|
108
|
-
const normalized = normalizePainCandidateText(text);
|
|
109
|
-
const head = normalized.substring(0, PAIN_CANDIDATE_FINGERPRINT_HEAD_LEN);
|
|
110
|
-
const tail = normalized.slice(-PAIN_CANDIDATE_FINGERPRINT_TAIL_LEN);
|
|
111
|
-
return createHash('md5')
|
|
112
|
-
.update(`${normalized.length}:${head}:${tail}`)
|
|
113
|
-
.digest('hex')
|
|
114
|
-
.substring(0, 8);
|
|
115
|
-
}
|
|
116
|
-
export function summarizePainCandidateSample(text) {
|
|
117
|
-
return normalizePainCandidateText(text).substring(0, PAIN_CANDIDATE_SAMPLE_LEN);
|
|
118
|
-
}
|
|
119
|
-
function isPendingPainCandidate(status) {
|
|
120
|
-
return status === undefined || status === 'pending';
|
|
121
|
-
}
|
|
122
|
-
export async function acquireQueueLock(resourcePath, logger, lockSuffix = EVOLUTION_QUEUE_LOCK_SUFFIX) {
|
|
123
|
-
try {
|
|
124
|
-
const ctx = await acquireLockAsync(resourcePath, {
|
|
125
|
-
lockSuffix,
|
|
126
|
-
maxRetries: LOCK_MAX_RETRIES,
|
|
127
|
-
baseRetryDelayMs: LOCK_RETRY_DELAY_MS,
|
|
128
|
-
lockStaleMs: LOCK_STALE_MS,
|
|
129
|
-
});
|
|
130
|
-
return () => releaseLock(ctx);
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
logger?.warn?.(`[PD:EvolutionWorker] Failed to acquire lock for ${resourcePath}: ${String(error)}`);
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function requireQueueLock(resourcePath, logger, scope, lockSuffix = EVOLUTION_QUEUE_LOCK_SUFFIX) {
|
|
138
|
-
try {
|
|
139
|
-
return await acquireQueueLock(resourcePath, logger, lockSuffix);
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
throw new LockUnavailableError(resourcePath, scope, { cause: err });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export function extractEvolutionTaskId(task) {
|
|
146
|
-
if (!task)
|
|
147
|
-
return null;
|
|
148
|
-
const match = task.match(/\[ID:\s*([A-Za-z0-9_-]+)\]/);
|
|
149
|
-
return match?.[1] || null;
|
|
150
|
-
}
|
|
151
|
-
function findRecentDuplicateTask(queue, source, preview, now, reason) {
|
|
152
|
-
const key = normalizePainDedupKey(source, preview, reason);
|
|
153
|
-
return queue.find((task) => {
|
|
154
|
-
if (task.status === 'completed')
|
|
155
|
-
return false;
|
|
156
|
-
const taskTime = new Date(task.enqueued_at || task.timestamp).getTime();
|
|
157
|
-
if (!Number.isFinite(taskTime) || (now - taskTime) > PAIN_QUEUE_DEDUP_WINDOW_MS)
|
|
158
|
-
return false;
|
|
159
|
-
return normalizePainDedupKey(task.source, task.trigger_text_preview || '', task.reason) === key;
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
function normalizePainDedupKey(source, preview, reason) {
|
|
163
|
-
// Include reason in dedup key to match createEvolutionTaskId() behavior
|
|
164
|
-
// Different reasons for the same source/preview should create different tasks
|
|
165
|
-
const normalizedReason = (reason || '').trim().toLowerCase();
|
|
166
|
-
return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
|
|
167
|
-
}
|
|
168
|
-
export function hasRecentDuplicateTask(queue, source, preview, now, reason) {
|
|
169
|
-
return !!findRecentDuplicateTask(queue, source, preview, now, reason);
|
|
170
|
-
}
|
|
171
|
-
export function hasEquivalentPromotedRule(dictionary, phrase) {
|
|
172
|
-
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
173
|
-
return Object.values(dictionary.getAllRules()).some((rule) => {
|
|
174
|
-
if (rule.status !== 'active')
|
|
175
|
-
return false;
|
|
176
|
-
if (rule.type === 'exact_match' && Array.isArray(rule.phrases)) {
|
|
177
|
-
return rule.phrases.some((candidate) => candidate.trim().toLowerCase() === normalizedPhrase);
|
|
178
|
-
}
|
|
179
|
-
if (rule.type === 'regex' && typeof rule.pattern === 'string') {
|
|
180
|
-
return rule.pattern.trim().toLowerCase() === normalizedPhrase;
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Read recent pain context from PAIN_FLAG file.
|
|
187
|
-
* Returns structured pain metadata for attaching to sleep_reflection tasks.
|
|
188
|
-
* Returns null if no pain flag exists.
|
|
189
|
-
*/
|
|
190
|
-
function readRecentPainContext(wctx) {
|
|
191
|
-
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
192
|
-
if (!fs.existsSync(painFlagPath)) {
|
|
193
|
-
return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
const rawPain = fs.readFileSync(painFlagPath, 'utf8');
|
|
197
|
-
const lines = rawPain.split('\n');
|
|
198
|
-
let score = 0;
|
|
199
|
-
let source = '';
|
|
200
|
-
let reason = '';
|
|
201
|
-
let timestamp = '';
|
|
202
|
-
for (const line of lines) {
|
|
203
|
-
if (line.startsWith('score:'))
|
|
204
|
-
score = parseInt(line.split(':', 2)[1].trim(), 10) || 0;
|
|
205
|
-
if (line.startsWith('source:'))
|
|
206
|
-
source = line.split(':', 2)[1].trim();
|
|
207
|
-
if (line.startsWith('reason:'))
|
|
208
|
-
reason = line.slice('reason:'.length).trim();
|
|
209
|
-
if (line.startsWith('timestamp:'))
|
|
210
|
-
timestamp = line.slice('timestamp:'.length).trim();
|
|
211
|
-
}
|
|
212
|
-
if (score > 0) {
|
|
213
|
-
return {
|
|
214
|
-
mostRecent: { score, source, reason, timestamp },
|
|
215
|
-
recentPainCount: 1,
|
|
216
|
-
recentMaxPainScore: score,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
// Best effort — non-fatal
|
|
222
|
-
}
|
|
223
|
-
return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Enqueue a sleep_reflection task if one is not already pending.
|
|
227
|
-
* Phase 2.4: Called when workspace is idle to trigger nocturnal reflection.
|
|
228
|
-
*/
|
|
229
|
-
async function enqueueSleepReflectionTask(wctx, logger) {
|
|
230
|
-
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
231
|
-
const releaseLock = await requireQueueLock(queuePath, logger, 'enqueueSleepReflection', EVOLUTION_QUEUE_LOCK_SUFFIX);
|
|
232
|
-
try {
|
|
233
|
-
let rawQueue = [];
|
|
234
|
-
try {
|
|
235
|
-
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// Queue doesn't exist yet - create empty array
|
|
239
|
-
rawQueue = [];
|
|
240
|
-
}
|
|
241
|
-
const queue = migrateQueueToV2(rawQueue);
|
|
242
|
-
// Check if a sleep_reflection task is already pending
|
|
243
|
-
const hasPendingSleepReflection = queue.some(t => t.taskKind === 'sleep_reflection' && (t.status === 'pending' || t.status === 'in_progress'));
|
|
244
|
-
if (hasPendingSleepReflection) {
|
|
245
|
-
logger?.debug?.('[PD:EvolutionWorker] sleep_reflection task already pending/in-progress, skipping');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const now = Date.now();
|
|
249
|
-
const taskId = createEvolutionTaskId('nocturnal', 50, 'idle workspace', 'Sleep-mode reflection', now);
|
|
250
|
-
const nowIso = new Date(now).toISOString();
|
|
251
|
-
// Attach recent pain context if available
|
|
252
|
-
const recentPainContext = readRecentPainContext(wctx);
|
|
253
|
-
queue.push({
|
|
254
|
-
id: taskId,
|
|
255
|
-
taskKind: 'sleep_reflection',
|
|
256
|
-
priority: 'medium',
|
|
257
|
-
score: 50,
|
|
258
|
-
source: 'nocturnal',
|
|
259
|
-
reason: 'Sleep-mode reflection triggered by idle workspace',
|
|
260
|
-
trigger_text_preview: 'Idle workspace detected',
|
|
261
|
-
timestamp: nowIso,
|
|
262
|
-
enqueued_at: nowIso,
|
|
263
|
-
status: 'pending',
|
|
264
|
-
traceId: taskId,
|
|
265
|
-
retryCount: 0,
|
|
266
|
-
maxRetries: 1, // sleep_reflection doesn't retry
|
|
267
|
-
recentPainContext,
|
|
268
|
-
});
|
|
269
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
270
|
-
logger?.info?.(`[PD:EvolutionWorker] Enqueued sleep_reflection task ${taskId}`);
|
|
271
|
-
}
|
|
272
|
-
finally {
|
|
273
|
-
releaseLock();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
async function checkPainFlag(wctx, logger) {
|
|
277
|
-
try {
|
|
278
|
-
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
279
|
-
if (!fs.existsSync(painFlagPath))
|
|
280
|
-
return;
|
|
281
|
-
const rawPain = fs.readFileSync(painFlagPath, 'utf8');
|
|
282
|
-
const lines = rawPain.split('\n');
|
|
283
|
-
let score = 0;
|
|
284
|
-
let source = 'unknown';
|
|
285
|
-
let reason = 'Systemic pain detected';
|
|
286
|
-
let preview = '';
|
|
287
|
-
let isQueued = false;
|
|
288
|
-
let traceId = '';
|
|
289
|
-
let sessionId = '';
|
|
290
|
-
let agentId = '';
|
|
291
|
-
for (const line of lines) {
|
|
292
|
-
if (line.startsWith('score:'))
|
|
293
|
-
score = parseInt(line.split(':', 2)[1].trim(), 10) || 0;
|
|
294
|
-
if (line.startsWith('source:'))
|
|
295
|
-
source = line.split(':', 2)[1].trim();
|
|
296
|
-
if (line.startsWith('reason:'))
|
|
297
|
-
reason = line.slice('reason:'.length).trim();
|
|
298
|
-
if (line.startsWith('trigger_text_preview:'))
|
|
299
|
-
preview = line.slice('trigger_text_preview:'.length).trim();
|
|
300
|
-
if (line.startsWith('status: queued'))
|
|
301
|
-
isQueued = true;
|
|
302
|
-
if (line.startsWith('trace_id:'))
|
|
303
|
-
traceId = line.split(':', 2)[1].trim();
|
|
304
|
-
if (line.startsWith('session_id:'))
|
|
305
|
-
sessionId = line.slice('session_id:'.length).trim();
|
|
306
|
-
if (line.startsWith('agent_id:'))
|
|
307
|
-
agentId = line.slice('agent_id:'.length).trim();
|
|
308
|
-
}
|
|
309
|
-
if (isQueued || score < 30)
|
|
310
|
-
return;
|
|
311
|
-
if (logger)
|
|
312
|
-
logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
|
|
313
|
-
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
314
|
-
const releaseLock = await requireQueueLock(queuePath, logger, 'checkPainFlag');
|
|
315
|
-
try {
|
|
316
|
-
let queue = [];
|
|
317
|
-
if (fs.existsSync(queuePath)) {
|
|
318
|
-
try {
|
|
319
|
-
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
320
|
-
}
|
|
321
|
-
catch (e) {
|
|
322
|
-
if (logger)
|
|
323
|
-
logger.error(`[PD:EvolutionWorker] Failed to parse evolution queue: ${String(e)}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
const now = Date.now();
|
|
327
|
-
const duplicateTask = findRecentDuplicateTask(queue, source, preview, now, reason);
|
|
328
|
-
if (duplicateTask) {
|
|
329
|
-
logger?.info?.(`[PD:EvolutionWorker] Duplicate pain task skipped for source=${source} preview=${preview || 'N/A'}`);
|
|
330
|
-
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${duplicateTask.id}\n`, 'utf8');
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const taskId = createEvolutionTaskId(source, score, preview, reason, now);
|
|
334
|
-
const nowIso = new Date(now).toISOString();
|
|
335
|
-
const effectiveTraceId = traceId || taskId;
|
|
336
|
-
// V2: New queue items include all V2 fields with pain_diagnosis defaults
|
|
337
|
-
queue.push({
|
|
338
|
-
id: taskId,
|
|
339
|
-
taskKind: 'pain_diagnosis', // V2: All pain-flag triggered tasks are pain_diagnosis
|
|
340
|
-
priority: score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low', // V2: Priority based on score
|
|
341
|
-
score,
|
|
342
|
-
source,
|
|
343
|
-
reason,
|
|
344
|
-
trigger_text_preview: preview,
|
|
345
|
-
timestamp: nowIso,
|
|
346
|
-
enqueued_at: nowIso,
|
|
347
|
-
status: 'pending',
|
|
348
|
-
session_id: sessionId || undefined,
|
|
349
|
-
agent_id: agentId || undefined,
|
|
350
|
-
traceId: effectiveTraceId,
|
|
351
|
-
retryCount: 0, // V2: No retries yet
|
|
352
|
-
maxRetries: 3, // V2: Default max retries
|
|
353
|
-
});
|
|
354
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
355
|
-
fs.appendFileSync(painFlagPath, `\nstatus: queued\ntask_id: ${taskId}\n`, 'utf8');
|
|
356
|
-
// Log to EvolutionLogger
|
|
357
|
-
const evoLogger = getEvolutionLogger(wctx.workspaceDir, wctx.trajectory);
|
|
358
|
-
evoLogger.logQueued({
|
|
359
|
-
traceId: traceId || taskId,
|
|
360
|
-
taskId,
|
|
361
|
-
score,
|
|
362
|
-
source,
|
|
363
|
-
reason,
|
|
364
|
-
});
|
|
365
|
-
// Record to evolution_tasks table
|
|
366
|
-
wctx.trajectory?.recordEvolutionTask?.({
|
|
367
|
-
taskId,
|
|
368
|
-
traceId: traceId || taskId,
|
|
369
|
-
source,
|
|
370
|
-
reason,
|
|
371
|
-
score,
|
|
372
|
-
status: 'pending',
|
|
373
|
-
enqueuedAt: nowIso,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
finally {
|
|
377
|
-
releaseLock();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
catch (err) {
|
|
381
|
-
if (logger)
|
|
382
|
-
logger.warn(`[PD:EvolutionWorker] Error processing pain flag: ${String(err)}`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
async function processEvolutionQueue(wctx, logger, eventLog, api) {
|
|
386
|
-
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
387
|
-
if (!fs.existsSync(queuePath))
|
|
388
|
-
return;
|
|
389
|
-
const releaseLock = await requireQueueLock(queuePath, logger, 'processEvolutionQueue');
|
|
390
|
-
const evoLogger = getEvolutionLogger(wctx.workspaceDir, wctx.trajectory);
|
|
391
|
-
let lockReleased = false;
|
|
392
|
-
try {
|
|
393
|
-
let rawQueue = [];
|
|
394
|
-
try {
|
|
395
|
-
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
396
|
-
}
|
|
397
|
-
catch (e) {
|
|
398
|
-
// Backup corrupted file instead of silently discarding
|
|
399
|
-
const backupPath = `${queuePath}.corrupted.${Date.now()}`;
|
|
400
|
-
try {
|
|
401
|
-
fs.renameSync(queuePath, backupPath);
|
|
402
|
-
if (logger) {
|
|
403
|
-
logger.error(`[PD:EvolutionWorker] Evolution queue corrupted and backed up to ${backupPath}. All pending tasks have been preserved in the backup file. Parse error: ${String(e)}`);
|
|
404
|
-
}
|
|
405
|
-
SystemLogger.log(wctx.workspaceDir, 'QUEUE_CORRUPTED', `Queue file backed up to ${backupPath}. Error: ${String(e)}`);
|
|
406
|
-
}
|
|
407
|
-
catch (backupErr) {
|
|
408
|
-
if (logger) {
|
|
409
|
-
logger.error(`[PD:EvolutionWorker] Failed to backup corrupted queue: ${String(backupErr)}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
// V2: Migrate queue to current schema if needed
|
|
415
|
-
const queue = migrateQueueToV2(rawQueue);
|
|
416
|
-
let queueChanged = rawQueue.some(isLegacyQueueItem);
|
|
417
|
-
const config = wctx.config;
|
|
418
|
-
const timeout = config.get('intervals.task_timeout_ms') || (60 * 60 * 1000); // Default 1 hour
|
|
419
|
-
// V2: Recover stuck in_progress sleep_reflection tasks.
|
|
420
|
-
// If the worker crashes or the result write-back fails after Phase 1 claimed
|
|
421
|
-
// the task, it stays in_progress indefinitely. Detect via timeout and mark
|
|
422
|
-
// as failed so a fresh task can be enqueued on the next idle cycle.
|
|
423
|
-
for (const task of queue.filter(t => t.status === 'in_progress' && t.taskKind === 'sleep_reflection')) {
|
|
424
|
-
const startedAt = new Date(task.started_at || task.timestamp);
|
|
425
|
-
const age = Date.now() - startedAt.getTime();
|
|
426
|
-
if (age > timeout) {
|
|
427
|
-
task.status = 'failed';
|
|
428
|
-
task.completed_at = new Date().toISOString();
|
|
429
|
-
task.resolution = 'failed_max_retries';
|
|
430
|
-
task.lastError = `sleep_reflection timed out after ${Math.round(timeout / 60000)} minutes`;
|
|
431
|
-
task.retryCount = (task.retryCount ?? 0) + 1;
|
|
432
|
-
queueChanged = true;
|
|
433
|
-
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${task.id} timed out after ${Math.round(age / 60000)} minutes, marking as failed`);
|
|
434
|
-
evoLogger.logCompleted({
|
|
435
|
-
traceId: task.traceId || task.id,
|
|
436
|
-
taskId: task.id,
|
|
437
|
-
resolution: 'manual',
|
|
438
|
-
durationMs: age,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Check in_progress tasks for completion (only pain_diagnosis gets HEARTBEAT treatment)
|
|
443
|
-
for (const task of queue.filter(t => t.status === 'in_progress' && t.taskKind === 'pain_diagnosis')) {
|
|
444
|
-
const startedAt = new Date(task.started_at || task.timestamp);
|
|
445
|
-
// Condition 1: Check for marker file (created by diagnostician on completion)
|
|
446
|
-
const completeMarker = path.join(wctx.stateDir, `.evolution_complete_${task.id}`);
|
|
447
|
-
if (fs.existsSync(completeMarker)) {
|
|
448
|
-
if (logger)
|
|
449
|
-
logger.info(`[PD:EvolutionWorker] Task ${task.id} completed - marker file detected`);
|
|
450
|
-
task.status = 'completed';
|
|
451
|
-
task.completed_at = new Date().toISOString();
|
|
452
|
-
task.resolution = 'marker_detected';
|
|
453
|
-
try {
|
|
454
|
-
fs.unlinkSync(completeMarker); // Clean up marker file
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
// Best effort cleanup
|
|
458
|
-
}
|
|
459
|
-
// Log to EvolutionLogger
|
|
460
|
-
const durationMs = task.started_at
|
|
461
|
-
? Date.now() - new Date(task.started_at).getTime()
|
|
462
|
-
: undefined;
|
|
463
|
-
evoLogger.logCompleted({
|
|
464
|
-
traceId: task.traceId || task.id,
|
|
465
|
-
taskId: task.id,
|
|
466
|
-
resolution: 'marker_detected',
|
|
467
|
-
durationMs,
|
|
468
|
-
});
|
|
469
|
-
// Update evolution_tasks table
|
|
470
|
-
wctx.trajectory?.updateEvolutionTask?.(task.id, {
|
|
471
|
-
status: 'completed',
|
|
472
|
-
completedAt: task.completed_at,
|
|
473
|
-
resolution: 'marker_detected',
|
|
474
|
-
});
|
|
475
|
-
wctx.trajectory?.recordTaskOutcome({
|
|
476
|
-
sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
|
|
477
|
-
taskId: task.id,
|
|
478
|
-
outcome: 'ok',
|
|
479
|
-
summary: `Task ${task.id} completed - marker file detected.`
|
|
480
|
-
});
|
|
481
|
-
queueChanged = true;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
// Condition 2: Timeout auto-complete
|
|
485
|
-
const age = Date.now() - startedAt.getTime();
|
|
486
|
-
if (age > timeout) {
|
|
487
|
-
const timeoutMinutes = Math.round(timeout / 60000);
|
|
488
|
-
if (logger)
|
|
489
|
-
logger.info(`[PD:EvolutionWorker] Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout`);
|
|
490
|
-
task.status = 'completed';
|
|
491
|
-
task.completed_at = new Date().toISOString();
|
|
492
|
-
task.resolution = 'auto_completed_timeout';
|
|
493
|
-
// Log to EvolutionLogger
|
|
494
|
-
evoLogger.logCompleted({
|
|
495
|
-
traceId: task.traceId || task.id,
|
|
496
|
-
taskId: task.id,
|
|
497
|
-
resolution: 'auto_completed_timeout',
|
|
498
|
-
durationMs: age,
|
|
499
|
-
});
|
|
500
|
-
// Update evolution_tasks table
|
|
501
|
-
wctx.trajectory?.updateEvolutionTask?.(task.id, {
|
|
502
|
-
status: 'completed',
|
|
503
|
-
completedAt: task.completed_at,
|
|
504
|
-
resolution: 'auto_completed_timeout',
|
|
505
|
-
});
|
|
506
|
-
wctx.trajectory?.recordTaskOutcome({
|
|
507
|
-
sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
|
|
508
|
-
taskId: task.id,
|
|
509
|
-
outcome: 'timeout',
|
|
510
|
-
summary: `Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout.`
|
|
511
|
-
});
|
|
512
|
-
queueChanged = true;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
// V2: Process pain_diagnosis tasks FIRST (quick, inside lock),
|
|
516
|
-
// then sleep_reflection tasks (slow, lock released during execution).
|
|
517
|
-
// This order ensures pain tasks are never starved by long-running
|
|
518
|
-
// nocturnal reflection — sleep_reflection can safely return early
|
|
519
|
-
// because pain_diagnosis has already been handled.
|
|
520
|
-
const pendingTasks = queue.filter(t => t.status === 'pending' && t.taskKind === 'pain_diagnosis');
|
|
521
|
-
if (pendingTasks.length > 0) {
|
|
522
|
-
// V2: Also sort by priority within same score
|
|
523
|
-
const priorityWeight = { high: 3, medium: 2, low: 1 };
|
|
524
|
-
const highestScoreTask = pendingTasks.sort((a, b) => {
|
|
525
|
-
const scoreDiff = b.score - a.score;
|
|
526
|
-
if (scoreDiff !== 0)
|
|
527
|
-
return scoreDiff;
|
|
528
|
-
return (priorityWeight[b.priority] || 2) - (priorityWeight[a.priority] || 2);
|
|
529
|
-
})[0];
|
|
530
|
-
const nowIso = new Date().toISOString();
|
|
531
|
-
const taskDescription = `Diagnose systemic pain [ID: ${highestScoreTask.id}]. Source: ${highestScoreTask.source}. Reason: ${highestScoreTask.reason}. ` +
|
|
532
|
-
`Trigger text: "${highestScoreTask.trigger_text_preview || 'N/A'}"`;
|
|
533
|
-
// Prepare HEARTBEAT content first
|
|
534
|
-
// Use shared diagnostician protocol (consistent with pd-diagnostician skill)
|
|
535
|
-
const heartbeatPath = wctx.resolve('HEARTBEAT');
|
|
536
|
-
const markerFilePath = path.join(wctx.stateDir, `.evolution_complete_${highestScoreTask.id}`);
|
|
537
|
-
const heartbeatContent = [
|
|
538
|
-
`## Evolution Task [ID: ${highestScoreTask.id}]`,
|
|
539
|
-
``,
|
|
540
|
-
`**Pain Score**: ${highestScoreTask.score}`,
|
|
541
|
-
`**Source**: ${highestScoreTask.source}`,
|
|
542
|
-
`**Reason**: ${highestScoreTask.reason}`,
|
|
543
|
-
`**Trigger**: "${highestScoreTask.trigger_text_preview || 'N/A'}"`,
|
|
544
|
-
`**Queued At**: ${highestScoreTask.enqueued_at || nowIso}`,
|
|
545
|
-
`**Session ID**: ${highestScoreTask.session_id || 'N/A'}`,
|
|
546
|
-
`**Agent ID**: ${highestScoreTask.agent_id || 'main'}`,
|
|
547
|
-
``,
|
|
548
|
-
`---`,
|
|
549
|
-
``,
|
|
550
|
-
DIAGNOSTICIAN_PROTOCOL_SUMMARY,
|
|
551
|
-
``,
|
|
552
|
-
`---`,
|
|
553
|
-
``,
|
|
554
|
-
`After completing the analysis:`,
|
|
555
|
-
`1. Write the resulting principle(s) to PRINCIPLES.md`,
|
|
556
|
-
`2. Mark the task complete by creating an empty file: ${markerFilePath}`,
|
|
557
|
-
`3. Replace this HEARTBEAT.md content with "HEARTBEAT_OK"`,
|
|
558
|
-
].join('\n');
|
|
559
|
-
// Try to write HEARTBEAT.md FIRST
|
|
560
|
-
// Only mark task as in_progress after successful write to avoid stuck tasks
|
|
561
|
-
try {
|
|
562
|
-
fs.writeFileSync(heartbeatPath, heartbeatContent, 'utf8');
|
|
563
|
-
if (logger)
|
|
564
|
-
logger.info(`[PD:EvolutionWorker] Wrote diagnostician task to HEARTBEAT.md for task ${highestScoreTask.id}`);
|
|
565
|
-
// HEARTBEAT write succeeded, now mark task as in_progress
|
|
566
|
-
highestScoreTask.task = taskDescription;
|
|
567
|
-
highestScoreTask.status = 'in_progress';
|
|
568
|
-
highestScoreTask.started_at = nowIso;
|
|
569
|
-
delete highestScoreTask.completed_at;
|
|
570
|
-
// Use placeholder instead of deleting - allows subagent_ended hook to match
|
|
571
|
-
// This fixes task_outcomes being empty for HEARTBEAT-triggered diagnostician runs
|
|
572
|
-
highestScoreTask.assigned_session_key = `heartbeat:diagnostician:${highestScoreTask.id}`;
|
|
573
|
-
queueChanged = true;
|
|
574
|
-
// Log to EvolutionLogger
|
|
575
|
-
evoLogger.logStarted({
|
|
576
|
-
traceId: highestScoreTask.traceId || highestScoreTask.id,
|
|
577
|
-
taskId: highestScoreTask.id,
|
|
578
|
-
});
|
|
579
|
-
// Update evolution_tasks table
|
|
580
|
-
wctx.trajectory?.updateEvolutionTask?.(highestScoreTask.id, {
|
|
581
|
-
status: 'in_progress',
|
|
582
|
-
startedAt: nowIso,
|
|
583
|
-
});
|
|
584
|
-
if (eventLog) {
|
|
585
|
-
eventLog.recordEvolutionTask({
|
|
586
|
-
taskId: highestScoreTask.id,
|
|
587
|
-
taskType: highestScoreTask.source,
|
|
588
|
-
reason: highestScoreTask.reason
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
catch (heartbeatErr) {
|
|
593
|
-
// HEARTBEAT write failed - keep task as pending for next cycle retry
|
|
594
|
-
if (logger)
|
|
595
|
-
logger.error(`[PD:EvolutionWorker] Failed to write HEARTBEAT.md for task ${highestScoreTask.id}: ${String(heartbeatErr)}. Task will remain pending for next cycle.`);
|
|
596
|
-
SystemLogger.log(wctx.workspaceDir, 'HEARTBEAT_WRITE_FAILED', `Task ${highestScoreTask.id} HEARTBEAT write failed: ${String(heartbeatErr)}`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
// Phase 2.4: Process sleep_reflection tasks AFTER pain_diagnosis.
|
|
600
|
-
// Claim tasks inside the lock, execute reflection outside the lock,
|
|
601
|
-
// then re-acquire the lock to write results. This prevents the long-running
|
|
602
|
-
// nocturnal reflection from blocking all other queue consumers.
|
|
603
|
-
// Safe to return early here because pain_diagnosis was already handled above.
|
|
604
|
-
const sleepReflectionTasks = queue.filter(t => t.status === 'pending' && t.taskKind === 'sleep_reflection');
|
|
605
|
-
if (sleepReflectionTasks.length > 0) {
|
|
606
|
-
// --- Phase 1: Claim tasks (inside lock) ---
|
|
607
|
-
for (const sleepTask of sleepReflectionTasks) {
|
|
608
|
-
sleepTask.status = 'in_progress';
|
|
609
|
-
sleepTask.started_at = new Date().toISOString();
|
|
610
|
-
}
|
|
611
|
-
queueChanged = true;
|
|
612
|
-
// Write claimed state (includes any pain changes from above) and release lock
|
|
613
|
-
if (queueChanged) {
|
|
614
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
615
|
-
}
|
|
616
|
-
releaseLock();
|
|
617
|
-
for (const sleepTask of sleepReflectionTasks) {
|
|
618
|
-
try {
|
|
619
|
-
logger?.info?.(`[PD:EvolutionWorker] Processing sleep_reflection task ${sleepTask.id}`);
|
|
620
|
-
// Build runtime adapter for real Trinity execution if api is available
|
|
621
|
-
const runtimeAdapter = api ? new OpenClawTrinityRuntimeAdapter(api) : undefined;
|
|
622
|
-
// Call the nocturnal reflection service
|
|
623
|
-
const result = await executeNocturnalReflectionAsync(wctx.workspaceDir, wctx.stateDir, {
|
|
624
|
-
runtimeAdapter,
|
|
625
|
-
});
|
|
626
|
-
if (result.success && result.artifact) {
|
|
627
|
-
sleepTask.status = 'completed';
|
|
628
|
-
sleepTask.completed_at = new Date().toISOString();
|
|
629
|
-
sleepTask.resolution = 'marker_detected';
|
|
630
|
-
sleepTask.resultRef = result.diagnostics.persistedPath;
|
|
631
|
-
logger?.info?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} completed successfully`);
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
// Record failure with skip reason
|
|
635
|
-
const skipReason = result.skipReason || (result.noTargetSelected ? 'no_target' : 'validation_failed');
|
|
636
|
-
sleepTask.status = 'failed';
|
|
637
|
-
sleepTask.completed_at = new Date().toISOString();
|
|
638
|
-
sleepTask.resolution = 'failed_max_retries';
|
|
639
|
-
sleepTask.lastError = `Nocturnal reflection failed: ${result.validationFailures.join('; ') || skipReason}`;
|
|
640
|
-
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
641
|
-
logger?.warn?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} failed: ${sleepTask.lastError}`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
catch (taskErr) {
|
|
645
|
-
sleepTask.status = 'failed';
|
|
646
|
-
sleepTask.completed_at = new Date().toISOString();
|
|
647
|
-
sleepTask.resolution = 'failed_max_retries';
|
|
648
|
-
sleepTask.lastError = String(taskErr);
|
|
649
|
-
sleepTask.retryCount = (sleepTask.retryCount ?? 0) + 1;
|
|
650
|
-
logger?.error?.(`[PD:EvolutionWorker] sleep_reflection task ${sleepTask.id} threw: ${taskErr}`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
// --- Phase 3: Write results back (re-acquire lock) ---
|
|
654
|
-
try {
|
|
655
|
-
const resultLock = await requireQueueLock(queuePath, logger, 'sleepReflectionResult');
|
|
656
|
-
try {
|
|
657
|
-
// Re-read queue to merge with any changes made while lock was released
|
|
658
|
-
let freshQueue = [];
|
|
659
|
-
try {
|
|
660
|
-
freshQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
661
|
-
}
|
|
662
|
-
catch { /* empty queue if corrupted */ }
|
|
663
|
-
// Merge: update tasks by ID
|
|
664
|
-
for (const sleepTask of sleepReflectionTasks) {
|
|
665
|
-
const idx = freshQueue.findIndex((t) => t.id === sleepTask.id);
|
|
666
|
-
if (idx >= 0) {
|
|
667
|
-
freshQueue[idx] = sleepTask;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
fs.writeFileSync(queuePath, JSON.stringify(freshQueue, null, 2), 'utf8');
|
|
671
|
-
// Log completions to EvolutionLogger
|
|
672
|
-
for (const sleepTask of sleepReflectionTasks) {
|
|
673
|
-
if (sleepTask.status === 'completed' || sleepTask.status === 'failed') {
|
|
674
|
-
evoLogger.logCompleted({
|
|
675
|
-
traceId: sleepTask.traceId || sleepTask.id,
|
|
676
|
-
taskId: sleepTask.id,
|
|
677
|
-
resolution: sleepTask.status === 'completed'
|
|
678
|
-
? (sleepTask.resolution === 'marker_detected' ? 'marker_detected' : 'manual')
|
|
679
|
-
: 'manual',
|
|
680
|
-
durationMs: sleepTask.started_at
|
|
681
|
-
? Date.now() - new Date(sleepTask.started_at).getTime()
|
|
682
|
-
: undefined,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
finally {
|
|
688
|
-
resultLock();
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
catch (resultLockErr) {
|
|
692
|
-
// If we can't re-acquire lock, results are in memory but not persisted.
|
|
693
|
-
// Tasks will appear stuck as in_progress and will be retried on next cycle.
|
|
694
|
-
logger?.warn?.(`[PD:EvolutionWorker] Failed to write sleep_reflection results back: ${String(resultLockErr)}`);
|
|
695
|
-
}
|
|
696
|
-
// Safe to return — pain_diagnosis was already processed above.
|
|
697
|
-
lockReleased = true;
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (queueChanged) {
|
|
701
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
catch (err) {
|
|
705
|
-
if (logger)
|
|
706
|
-
logger.warn(`[PD:EvolutionWorker] Error processing evolution queue: ${String(err)}`);
|
|
707
|
-
}
|
|
708
|
-
finally {
|
|
709
|
-
if (!lockReleased) {
|
|
710
|
-
releaseLock();
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
async function processDetectionQueue(wctx, api, eventLog) {
|
|
715
|
-
const logger = api.logger;
|
|
716
|
-
try {
|
|
717
|
-
const funnel = DetectionService.get(wctx.stateDir);
|
|
718
|
-
const queue = funnel.flushQueue();
|
|
719
|
-
if (queue.length === 0)
|
|
720
|
-
return;
|
|
721
|
-
if (logger)
|
|
722
|
-
logger.info(`[PD:EvolutionWorker] Processing ${queue.length} items from detection funnel.`);
|
|
723
|
-
const dictionary = DictionaryService.get(wctx.stateDir);
|
|
724
|
-
for (const text of queue) {
|
|
725
|
-
const match = dictionary.match(text);
|
|
726
|
-
if (match) {
|
|
727
|
-
if (eventLog) {
|
|
728
|
-
eventLog.recordRuleMatch(undefined, {
|
|
729
|
-
ruleId: match.ruleId,
|
|
730
|
-
layer: 'L2',
|
|
731
|
-
severity: match.severity,
|
|
732
|
-
textPreview: text.substring(0, 100)
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
try {
|
|
738
|
-
const searchTool = api.runtime.tools?.createMemorySearchTool?.({ config: api.config });
|
|
739
|
-
if (searchTool) {
|
|
740
|
-
const searchResult = await searchTool.execute('pre-emptive-pain-check', {
|
|
741
|
-
query: text,
|
|
742
|
-
limit: 1,
|
|
743
|
-
threshold: 0.85
|
|
744
|
-
});
|
|
745
|
-
if (searchResult && searchResult.results?.length > 0) {
|
|
746
|
-
const hit = searchResult.results[0];
|
|
747
|
-
if (logger)
|
|
748
|
-
logger.info?.(`[PD:EvolutionWorker] L3 Semantic Hit: ${hit.id} (Score: ${hit.score})`);
|
|
749
|
-
funnel.updateCache(text, { detected: true, severity: 40 });
|
|
750
|
-
if (eventLog) {
|
|
751
|
-
eventLog.recordRuleMatch(undefined, {
|
|
752
|
-
ruleId: 'SEMANTIC_HIT',
|
|
753
|
-
layer: 'L3',
|
|
754
|
-
severity: 40,
|
|
755
|
-
textPreview: text.substring(0, 100)
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
catch (e) {
|
|
762
|
-
if (logger)
|
|
763
|
-
logger.debug?.(`[PD:EvolutionWorker] L3 Semantic search failed: ${String(e)}`);
|
|
764
|
-
}
|
|
765
|
-
await trackPainCandidate(text, wctx);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
catch (err) {
|
|
770
|
-
if (logger)
|
|
771
|
-
logger.warn(`[PD:EvolutionWorker] Detection queue failed: ${String(err)}`);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
export async function trackPainCandidate(text, wctx) {
|
|
775
|
-
if (!shouldTrackPainCandidate(text))
|
|
776
|
-
return;
|
|
777
|
-
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
778
|
-
const releaseLock = await requireQueueLock(candidatePath, console, 'trackPainCandidate', PAIN_CANDIDATES_LOCK_SUFFIX);
|
|
779
|
-
try {
|
|
780
|
-
let data = { candidates: {} };
|
|
781
|
-
if (fs.existsSync(candidatePath)) {
|
|
782
|
-
try {
|
|
783
|
-
data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
784
|
-
}
|
|
785
|
-
catch (e) {
|
|
786
|
-
// Keep going with empty data if parse fails, but log it
|
|
787
|
-
console.error(`[PD:EvolutionWorker] Failed to parse pain candidates: ${String(e)}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
const fingerprint = createPainCandidateFingerprint(text);
|
|
791
|
-
const now = new Date().toISOString();
|
|
792
|
-
if (!data.candidates[fingerprint]) {
|
|
793
|
-
data.candidates[fingerprint] = { count: 0, status: 'pending', firstSeen: now, lastSeen: now, samples: [] };
|
|
794
|
-
}
|
|
795
|
-
const cand = data.candidates[fingerprint];
|
|
796
|
-
cand.status = cand.status || 'pending';
|
|
797
|
-
cand.count++;
|
|
798
|
-
cand.lastSeen = now;
|
|
799
|
-
const sample = summarizePainCandidateSample(text);
|
|
800
|
-
if (cand.samples.length < PAIN_CANDIDATE_MAX_SAMPLES && !cand.samples.includes(sample)) {
|
|
801
|
-
cand.samples.push(sample);
|
|
802
|
-
}
|
|
803
|
-
fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
|
|
804
|
-
}
|
|
805
|
-
finally {
|
|
806
|
-
releaseLock();
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
export async function processPromotion(wctx, logger, eventLog) {
|
|
810
|
-
const candidatePath = wctx.resolve('PAIN_CANDIDATES');
|
|
811
|
-
if (!fs.existsSync(candidatePath))
|
|
812
|
-
return;
|
|
813
|
-
const releaseLock = await requireQueueLock(candidatePath, logger, 'processPromotion', PAIN_CANDIDATES_LOCK_SUFFIX);
|
|
814
|
-
try {
|
|
815
|
-
const config = wctx.config;
|
|
816
|
-
const dictionary = wctx.dictionary;
|
|
817
|
-
const data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
|
|
818
|
-
const countThreshold = config.get('thresholds.promotion_count_threshold') || 3;
|
|
819
|
-
let promotedCount = 0;
|
|
820
|
-
let changed = false;
|
|
821
|
-
for (const [fingerprint, cand] of Object.entries(data.candidates)) {
|
|
822
|
-
if (isPendingPainCandidate(cand.status) && cand.count >= countThreshold) {
|
|
823
|
-
// Normalize undefined status to 'pending'
|
|
824
|
-
if (cand.status !== 'pending') {
|
|
825
|
-
cand.status = 'pending';
|
|
826
|
-
changed = true;
|
|
827
|
-
}
|
|
828
|
-
const commonPhrases = extractCommonSubstring(cand.samples);
|
|
829
|
-
if (commonPhrases.length > 0) {
|
|
830
|
-
const phrase = commonPhrases[0];
|
|
831
|
-
const ruleId = `P_PROMOTED_${fingerprint.toUpperCase()}`;
|
|
832
|
-
if (hasEquivalentPromotedRule(dictionary, phrase)) {
|
|
833
|
-
cand.status = 'duplicate';
|
|
834
|
-
changed = true;
|
|
835
|
-
logger?.info?.(`[PD:EvolutionWorker] Skipping duplicate promoted rule for candidate ${fingerprint}: ${phrase}`);
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (logger)
|
|
839
|
-
logger.info(`[PD:EvolutionWorker] Promoting candidate ${fingerprint} to formal rule: ${ruleId}`);
|
|
840
|
-
SystemLogger.log(wctx.workspaceDir, 'RULE_PROMOTED', `Candidate ${fingerprint} promoted to rule ${ruleId}`);
|
|
841
|
-
dictionary.addRule(ruleId, {
|
|
842
|
-
type: 'exact_match',
|
|
843
|
-
phrases: [phrase],
|
|
844
|
-
severity: config.get('scores.default_confusion') || 35,
|
|
845
|
-
status: 'active'
|
|
846
|
-
});
|
|
847
|
-
cand.status = 'promoted';
|
|
848
|
-
promotedCount++;
|
|
849
|
-
changed = true;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (changed) {
|
|
854
|
-
fs.writeFileSync(candidatePath, JSON.stringify(data, null, 2), 'utf8');
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
catch (err) {
|
|
858
|
-
if (logger)
|
|
859
|
-
logger.warn(`[PD:EvolutionWorker] Error during rule promotion: ${String(err)}`);
|
|
860
|
-
}
|
|
861
|
-
finally {
|
|
862
|
-
releaseLock();
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
export async function registerEvolutionTaskSession(workspaceResolve, taskId, sessionKey, logger) {
|
|
866
|
-
const queuePath = workspaceResolve('EVOLUTION_QUEUE');
|
|
867
|
-
if (!fs.existsSync(queuePath))
|
|
868
|
-
return false;
|
|
869
|
-
const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
|
|
870
|
-
try {
|
|
871
|
-
let rawQueue;
|
|
872
|
-
try {
|
|
873
|
-
rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
874
|
-
}
|
|
875
|
-
catch (parseErr) {
|
|
876
|
-
logger?.warn?.(`[PD:EvolutionWorker] Failed to parse EVOLUTION_QUEUE for session registration: ${queuePath} - ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
// V2: Migrate queue to current schema
|
|
880
|
-
const queue = migrateQueueToV2(rawQueue);
|
|
881
|
-
const task = queue.find((item) => item.id === taskId && item.status === 'in_progress');
|
|
882
|
-
if (!task) {
|
|
883
|
-
logger?.warn?.(`[PD:EvolutionWorker] Could not find in-progress evolution task ${taskId} for session assignment`);
|
|
884
|
-
return false;
|
|
885
|
-
}
|
|
886
|
-
task.assigned_session_key = sessionKey;
|
|
887
|
-
if (!task.started_at) {
|
|
888
|
-
task.started_at = new Date().toISOString();
|
|
889
|
-
}
|
|
890
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
891
|
-
return true;
|
|
892
|
-
}
|
|
893
|
-
finally {
|
|
894
|
-
releaseLock();
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
export const EvolutionWorkerService = {
|
|
898
|
-
id: 'principles-evolution-worker',
|
|
899
|
-
api: null,
|
|
900
|
-
start(ctx) {
|
|
901
|
-
const logger = ctx?.logger || console;
|
|
902
|
-
const api = this.api;
|
|
903
|
-
const workspaceDir = ctx?.workspaceDir;
|
|
904
|
-
if (!workspaceDir) {
|
|
905
|
-
if (logger)
|
|
906
|
-
logger.warn('[PD:EvolutionWorker] workspaceDir not found in service config. Evolution cycle disabled.');
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
910
|
-
if (logger)
|
|
911
|
-
logger.info(`[PD:EvolutionWorker] Starting with workspaceDir=${wctx.workspaceDir}, stateDir=${wctx.stateDir}`);
|
|
912
|
-
initPersistence(wctx.stateDir);
|
|
913
|
-
const eventLog = wctx.eventLog;
|
|
914
|
-
const config = wctx.config;
|
|
915
|
-
const language = config.get('language') || 'en';
|
|
916
|
-
ensureStateTemplates({ logger }, wctx.stateDir, language);
|
|
917
|
-
const initialDelay = 5000;
|
|
918
|
-
const interval = config.get('intervals.worker_poll_ms') || (15 * 60 * 1000);
|
|
919
|
-
intervalId = setInterval(() => {
|
|
920
|
-
void (async () => {
|
|
921
|
-
// V2: Nocturnal idle check — logs workspace idle state on each cycle.
|
|
922
|
-
// This makes nocturnal-runtime a visible part of the worker lifecycle.
|
|
923
|
-
// Phase 2.4: Enqueue sleep_reflection when workspace is idle and not in cooldown.
|
|
924
|
-
const idleResult = checkWorkspaceIdle(wctx.workspaceDir, {});
|
|
925
|
-
if (idleResult.isIdle) {
|
|
926
|
-
logger?.debug?.(`[PD:EvolutionWorker] Workspace idle (${idleResult.idleForMs}ms since last activity)`);
|
|
927
|
-
// Phase 2.4: Enqueue sleep_reflection task if not in global cooldown
|
|
928
|
-
const cooldown = checkCooldown(wctx.stateDir);
|
|
929
|
-
if (!cooldown.globalCooldownActive && !cooldown.quotaExhausted) {
|
|
930
|
-
enqueueSleepReflectionTask(wctx, logger).catch((err) => {
|
|
931
|
-
logger?.error?.(`[PD:EvolutionWorker] Failed to enqueue sleep_reflection task: ${String(err)}`);
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
logger?.debug?.(`[PD:EvolutionWorker] Workspace active (last activity ${idleResult.idleForMs}ms ago)`);
|
|
937
|
-
}
|
|
938
|
-
await checkPainFlag(wctx, logger);
|
|
939
|
-
await processEvolutionQueue(wctx, logger, eventLog, api ?? undefined);
|
|
940
|
-
if (api) {
|
|
941
|
-
await processDetectionQueue(wctx, api, eventLog);
|
|
942
|
-
}
|
|
943
|
-
await processPromotion(wctx, logger, eventLog);
|
|
944
|
-
wctx.dictionary.flush();
|
|
945
|
-
flushAllSessions();
|
|
946
|
-
})().catch((err) => {
|
|
947
|
-
if (logger)
|
|
948
|
-
logger.error(`[PD:EvolutionWorker] Error in worker interval: ${String(err)}`);
|
|
949
|
-
});
|
|
950
|
-
}, interval);
|
|
951
|
-
timeoutId = setTimeout(() => {
|
|
952
|
-
void (async () => {
|
|
953
|
-
await checkPainFlag(wctx, logger);
|
|
954
|
-
await processEvolutionQueue(wctx, logger, eventLog, api ?? undefined);
|
|
955
|
-
if (api) {
|
|
956
|
-
await processDetectionQueue(wctx, api, eventLog);
|
|
957
|
-
}
|
|
958
|
-
await processPromotion(wctx, logger, eventLog);
|
|
959
|
-
})().catch((err) => {
|
|
960
|
-
if (logger)
|
|
961
|
-
logger.error(`[PD:EvolutionWorker] Startup worker cycle failed: ${String(err)}`);
|
|
962
|
-
});
|
|
963
|
-
}, initialDelay);
|
|
964
|
-
},
|
|
965
|
-
stop(ctx) {
|
|
966
|
-
if (ctx?.logger)
|
|
967
|
-
ctx.logger.info('[PD:EvolutionWorker] Stopping background service...');
|
|
968
|
-
if (intervalId)
|
|
969
|
-
clearInterval(intervalId);
|
|
970
|
-
if (timeoutId)
|
|
971
|
-
clearTimeout(timeoutId);
|
|
972
|
-
flushAllSessions();
|
|
973
|
-
}
|
|
974
|
-
};
|