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.
- package/openclaw.plugin.json +10 -5
- package/package.json +17 -19
- package/scripts/acceptance-test.mjs +16 -73
- package/scripts/sync-plugin.mjs +382 -77
- package/src/commands/archive-impl.ts +2 -1
- package/src/commands/capabilities.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/disable-impl.ts +2 -1
- package/src/commands/evolution-status.ts +16 -16
- package/src/commands/export.ts +12 -67
- package/src/commands/pain.ts +91 -1
- package/src/commands/principle-rollback.ts +2 -1
- package/src/commands/promote-impl.ts +7 -43
- package/src/commands/rollback-impl.ts +2 -1
- package/src/commands/rollback.ts +2 -1
- package/src/commands/samples.ts +2 -1
- package/src/commands/thinking-os.ts +2 -1
- package/src/config/errors.ts +18 -2
- package/src/constants/diagnostician.ts +2 -2
- package/src/constants/tools.ts +2 -1
- package/src/core/__tests__/focus-history.test.ts +210 -0
- package/src/core/config.ts +1 -1
- package/src/core/confirm-first-gate.ts +255 -0
- package/src/core/correction-cue-learner.ts +2 -136
- package/src/core/correction-types.ts +16 -88
- package/src/core/dictionary.ts +19 -20
- package/src/core/empathy-keyword-matcher.ts +17 -289
- package/src/core/empathy-types.ts +18 -229
- package/src/core/event-log.ts +38 -132
- package/src/core/evolution-reducer.ts +21 -2
- package/src/core/evolution-types.ts +76 -464
- package/src/core/file-store.ts +80 -0
- package/src/core/focus-history.ts +228 -955
- package/src/core/local-worker-routing.ts +34 -314
- package/src/core/merge-gate-audit.ts +0 -195
- package/src/core/pain-diagnostic-gate.ts +154 -0
- package/src/core/pain-signal.ts +21 -138
- package/src/core/pain.ts +15 -88
- package/src/core/pd-task-reconciler.ts +26 -115
- package/src/core/pd-task-service.ts +9 -9
- package/src/core/pd-task-types.ts +23 -127
- package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
- package/src/core/principle-compiler/code-validator.ts +15 -42
- package/src/core/principle-compiler/compiler.ts +100 -15
- package/src/core/principle-compiler/index.ts +5 -2
- package/src/core/principle-compiler/template-generator.ts +4 -104
- package/src/core/principle-injection.ts +10 -202
- package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
- package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
- package/src/core/principle-tree-ledger-adapter.ts +145 -0
- package/src/core/principle-tree-ledger.ts +8 -6
- package/src/core/reflection/reflection-context.ts +14 -109
- package/src/core/replay-engine.ts +8 -500
- package/src/core/rule-host-helpers.ts +5 -35
- package/src/core/rule-host-types.ts +10 -82
- package/src/core/rule-host.ts +6 -63
- package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
- package/src/core/session-tracker.ts +87 -101
- package/src/core/shadow-observation-registry.ts +19 -48
- package/src/core/trajectory.ts +3 -1
- package/src/core/workflow-funnel-loader.ts +62 -68
- package/src/core/workspace-context.ts +46 -0
- package/src/core/workspace-dir-service.ts +1 -1
- package/src/core/workspace-dir-validation.ts +18 -9
- package/src/hooks/AGENTS.md +1 -1
- package/src/hooks/gate-block-helper.ts +46 -44
- package/src/hooks/gate.ts +207 -7
- package/src/hooks/lifecycle.ts +30 -32
- package/src/hooks/llm.ts +60 -32
- package/src/hooks/pain.ts +297 -103
- package/src/hooks/prompt.ts +459 -439
- package/src/hooks/subagent.ts +2 -29
- package/src/i18n/commands.ts +2 -10
- package/src/index.ts +95 -85
- package/src/openclaw-sdk.ts +311 -0
- package/src/service/central-database.ts +8 -4
- package/src/service/evolution-queue-migration.ts +2 -1
- package/src/service/evolution-worker.ts +163 -1786
- package/src/service/internalization-trigger-adapter.ts +302 -0
- package/src/service/keyword-optimization-service.ts +4 -4
- package/src/service/monitoring-query-service.ts +1 -215
- package/src/service/queue-io.ts +60 -331
- package/src/service/runtime-summary-service.ts +59 -16
- package/src/service/subagent-workflow/index.ts +0 -41
- package/src/service/subagent-workflow/types.ts +9 -120
- package/src/service/subagent-workflow/workflow-store.ts +2 -119
- package/src/service/workflow-watchdog.ts +0 -43
- package/src/types/event-payload.ts +16 -74
- package/src/types/event-types.ts +39 -547
- package/src/types/hygiene-types.ts +7 -30
- package/src/types/principle-tree-schema.ts +20 -222
- package/src/types/queue.ts +15 -70
- package/src/types/runtime-summary.ts +5 -49
- package/src/utils/io.ts +10 -0
- package/src/utils/retry.ts +1 -1
- package/src/utils/shadow-fingerprint.ts +2 -2
- package/src/utils/workspace-resolver.ts +50 -0
- package/templates/langs/en/core/AGENTS.md +2 -2
- package/templates/langs/en/core/BOOT.md +1 -1
- package/templates/langs/en/core/HEARTBEAT.md +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
- package/templates/langs/zh/core/AGENTS.md +2 -2
- package/templates/langs/zh/core/BOOT.md +1 -1
- package/templates/langs/zh/core/HEARTBEAT.md +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
- package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
- package/tests/build-artifacts.test.ts +1 -3
- package/tests/commands/evolution-status.test.ts +0 -118
- package/tests/core/bootstrap-rules.test.ts +1 -1
- package/tests/core/config.test.ts +1 -1
- package/tests/core/event-log.test.ts +35 -0
- package/tests/core/evolution-engine.test.ts +610 -0
- package/tests/core/file-store.test.ts +102 -0
- package/tests/core/focus-history.test.ts +203 -11
- package/tests/core/merge-gate-audit.test.ts +2 -169
- package/tests/core/model-deployment-registry.test.ts +7 -1
- package/tests/core/model-training-registry.test.ts +19 -0
- package/tests/core/observability.test.ts +0 -1
- package/tests/core/pain-diagnostic-gate.test.ts +498 -0
- package/tests/core/pain.test.ts +0 -1
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
- package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
- package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
- package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
- package/tests/core/reflection-context.test.ts +0 -14
- package/tests/core/replay-engine.test.ts +127 -215
- package/tests/core/rule-host-helpers.test.ts +2 -2
- package/tests/core/rule-implementation-runtime.test.ts +0 -27
- package/tests/core/workflow-funnel-loader.test.ts +162 -0
- package/tests/core/workspace-dir-validation.test.ts +8 -1
- package/tests/core-anti-growth.test.ts +192 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
- package/tests/hooks/confirm-first-gate.test.ts +333 -0
- package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
- package/tests/hooks/gate-auto-correct.test.ts +665 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
- package/tests/hooks/pain.test.ts +269 -12
- package/tests/hooks/prompt-characterization.test.ts +500 -0
- package/tests/hooks/prompt-size-guard.test.ts +32 -17
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
- package/tests/index.test.ts +94 -1
- package/tests/integration/auto-entry-gate.test.ts +248 -0
- package/tests/integration/internalization-trigger-guard.test.ts +69 -0
- package/tests/integration/m8-legacy-paths.test.ts +63 -0
- package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
- package/tests/plugin-config-resolution-cutover.test.ts +359 -0
- package/tests/runtime-v2-discovery-guard.test.ts +154 -0
- package/tests/service/central-database.test.ts +457 -0
- package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
- package/tests/service/evolution-worker.timeout.test.ts +11 -129
- package/tests/service/internalization-trigger-adapter.test.ts +251 -0
- package/tests/service/monitoring-query-service.test.ts +1 -47
- package/tests/service/queue-io.test.ts +1 -62
- package/tests/service/runtime-summary-service.test.ts +3 -1
- package/tests/service/workflow-watchdog.test.ts +0 -91
- package/tests/utils/file-lock.test.ts +5 -3
- package/tests/utils/session-key.test.ts +52 -0
- package/tests/utils/subagent-probe.test.ts +48 -1
- package/vitest.config.ts +4 -11
- package/.planning/codebase/ARCHITECTURE.md +0 -157
- package/.planning/codebase/CONCERNS.md +0 -145
- package/.planning/codebase/CONVENTIONS.md +0 -148
- package/.planning/codebase/INTEGRATIONS.md +0 -81
- package/.planning/codebase/STACK.md +0 -87
- package/.planning/codebase/STRUCTURE.md +0 -193
- package/.planning/codebase/TESTING.md +0 -243
- package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
- package/docs/COMMAND_REFERENCE.md +0 -76
- package/docs/COMMAND_REFERENCE_EN.md +0 -79
- package/scripts/build-web.mjs +0 -46
- package/scripts/diagnose-nocturnal.mjs +0 -537
- package/scripts/seed-nocturnal-scenarios.mjs +0 -384
- package/src/commands/nocturnal-review.ts +0 -322
- package/src/commands/nocturnal-rollout.ts +0 -790
- package/src/commands/nocturnal-train.ts +0 -986
- package/src/commands/pd-reflect.ts +0 -88
- package/src/core/adaptive-thresholds.ts +0 -478
- package/src/core/diagnostician-task-store.ts +0 -192
- package/src/core/nocturnal-arbiter.ts +0 -715
- package/src/core/nocturnal-artifact-lineage.ts +0 -116
- package/src/core/nocturnal-artificer.ts +0 -257
- package/src/core/nocturnal-candidate-scoring.ts +0 -530
- package/src/core/nocturnal-compliance.ts +0 -1146
- package/src/core/nocturnal-dataset.ts +0 -763
- package/src/core/nocturnal-executability.ts +0 -428
- package/src/core/nocturnal-export.ts +0 -499
- package/src/core/nocturnal-paths.ts +0 -240
- package/src/core/nocturnal-reasoning-deriver.ts +0 -343
- package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
- package/src/core/nocturnal-snapshot-contract.ts +0 -99
- package/src/core/nocturnal-trajectory-extractor.ts +0 -512
- package/src/core/nocturnal-trinity-types.ts +0 -218
- package/src/core/nocturnal-trinity.ts +0 -2680
- package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
- package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
- package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
- package/src/http/principles-console-route.ts +0 -709
- package/src/service/central-health-service.ts +0 -49
- package/src/service/central-overview-service.ts +0 -138
- package/src/service/control-ui-query-service.ts +0 -900
- package/src/service/cooldown-strategy.ts +0 -97
- package/src/service/evolution-pain-context.ts +0 -79
- package/src/service/evolution-query-service.ts +0 -407
- package/src/service/health-query-service.ts +0 -1038
- package/src/service/nocturnal-config.ts +0 -214
- package/src/service/nocturnal-runtime.ts +0 -734
- package/src/service/nocturnal-service.ts +0 -1605
- package/src/service/nocturnal-target-selector.ts +0 -545
- package/src/service/sleep-cycle.ts +0 -157
- package/src/service/startup-reconciler.ts +0 -112
- package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
- package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
- package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
- package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
- package/src/tools/write-pain-flag.ts +0 -215
- package/tests/commands/nocturnal-review.test.ts +0 -448
- package/tests/commands/nocturnal-train.test.ts +0 -97
- package/tests/commands/pd-reflect.test.ts +0 -49
- package/tests/core/adaptive-thresholds.test.ts +0 -261
- package/tests/core/nocturnal-arbiter.test.ts +0 -559
- package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
- package/tests/core/nocturnal-artificer.test.ts +0 -241
- package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
- package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
- package/tests/core/nocturnal-compliance.test.ts +0 -646
- package/tests/core/nocturnal-dataset.test.ts +0 -892
- package/tests/core/nocturnal-e2e.test.ts +0 -234
- package/tests/core/nocturnal-executability.test.ts +0 -357
- package/tests/core/nocturnal-export.test.ts +0 -517
- package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
- package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
- package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
- package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
- package/tests/core/nocturnal-trinity.test.ts +0 -2053
- package/tests/core/pain-auto-repair.test.ts +0 -96
- package/tests/core/pain-integration.test.ts +0 -510
- package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
- package/tests/http/principles-console-route.test.ts +0 -162
- package/tests/integration/chaos-resilience.test.ts +0 -348
- package/tests/integration/empathy-workflow-integration.test.ts +0 -626
- package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
- package/tests/service/control-ui-query-service.test.ts +0 -121
- package/tests/service/cooldown-strategy.test.ts +0 -164
- package/tests/service/data-endpoints-regression.test.ts +0 -834
- package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
- package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
- package/tests/service/nocturnal-runtime.test.ts +0 -473
- package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
- package/tests/service/nocturnal-target-selector.test.ts +0 -615
- package/tests/service/startup-reconciler.test.ts +0 -148
- package/tests/tools/write-pain-flag.test.ts +0 -358
- package/ui/src/App.tsx +0 -45
- package/ui/src/api.ts +0 -220
- package/ui/src/charts.tsx +0 -955
- package/ui/src/components/ErrorState.tsx +0 -6
- package/ui/src/components/Loading.tsx +0 -13
- package/ui/src/components/ProtectedRoute.tsx +0 -12
- package/ui/src/components/Shell.tsx +0 -91
- package/ui/src/components/WorkspaceConfig.tsx +0 -178
- package/ui/src/components/index.ts +0 -5
- package/ui/src/context/auth.tsx +0 -80
- package/ui/src/context/theme.tsx +0 -66
- package/ui/src/hooks/useAutoRefresh.ts +0 -39
- package/ui/src/i18n/ui.ts +0 -473
- package/ui/src/main.tsx +0 -16
- package/ui/src/pages/EvolutionPage.tsx +0 -333
- package/ui/src/pages/FeedbackPage.tsx +0 -138
- package/ui/src/pages/GateMonitorPage.tsx +0 -136
- package/ui/src/pages/LoginPage.tsx +0 -89
- package/ui/src/pages/OverviewPage.tsx +0 -599
- package/ui/src/pages/SamplesPage.tsx +0 -174
- package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
- package/ui/src/styles.css +0 -2020
- package/ui/src/types.ts +0 -384
- package/ui/src/utils/format.ts +0 -15
package/src/hooks/prompt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
@@ -8,33 +8,29 @@ import { WorkspaceContext } from '../core/workspace-context.js';
|
|
|
8
8
|
import type { ContextInjectionConfig} from '../types.js';
|
|
9
9
|
import { defaultContextConfig } from '../types.js';
|
|
10
10
|
import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js';
|
|
11
|
+
import { detectApprovalMarker, setConfirmFirstApproval, setConfirmFirstDirective, hydrateFromStore, pruneStoreStaleRows, setConfirmFirstStore, resetConfirmFirst } from '../core/confirm-first-gate.js';
|
|
11
12
|
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
|
|
12
|
-
import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
|
|
13
13
|
import { PathResolver } from '../core/path-resolver.js';
|
|
14
14
|
import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/principle-injection.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler, SqliteConfirmFirstStateStore, SqliteConnection } from '@principles/core/runtime-v2';
|
|
16
|
+
import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
|
|
17
|
+
import { PromptActivationReader, RUNTIME_V2_PRINCIPLE_BUDGET } from '../core/runtime-v2-prompt-activation-reader.js';
|
|
17
18
|
import {
|
|
18
19
|
matchEmpathyKeywords,
|
|
19
20
|
loadKeywordStore,
|
|
20
21
|
saveKeywordStore,
|
|
21
|
-
shouldTriggerOptimization,
|
|
22
22
|
getKeywordStoreSummary,
|
|
23
23
|
} from '../core/empathy-keyword-matcher.js';
|
|
24
24
|
import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
|
|
25
|
+
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
26
|
+
import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
|
|
25
27
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
function toWorkflowSubagent(
|
|
34
|
-
subagent: NonNullable<OpenClawPluginApi['runtime']>['subagent']
|
|
35
|
-
): PluginRuntimeSubagent {
|
|
36
|
-
return subagent as unknown as PluginRuntimeSubagent;
|
|
37
|
-
}
|
|
28
|
+
import {
|
|
29
|
+
buildAttitudeDirective,
|
|
30
|
+
detectCorrectionCue as coreDetectCorrectionCue,
|
|
31
|
+
extractMessageContent,
|
|
32
|
+
isMinimalTrigger,
|
|
33
|
+
} from '@principles/core/prompt-builder';
|
|
38
34
|
|
|
39
35
|
// ---------------------------------------------------------------------------
|
|
40
36
|
// Static file cache — avoids re-reading rarely-changing files every message
|
|
@@ -81,6 +77,7 @@ function cachedReadFile(filePath: string): string {
|
|
|
81
77
|
// Module-level empathy state — shared across calls to avoid per-turn I/O
|
|
82
78
|
let _empathyTurnCounter = 0;
|
|
83
79
|
let _empathyKeywordCache: { store: ReturnType<typeof loadKeywordStore>; lang: string } | null = null;
|
|
80
|
+
let _confirmFirstHydrationCounter = 0;
|
|
84
81
|
|
|
85
82
|
/**
|
|
86
83
|
* Model configuration with primary model and optional fallback models
|
|
@@ -129,43 +126,7 @@ interface PromptHookApi {
|
|
|
129
126
|
}
|
|
130
127
|
|
|
131
128
|
function getTextContent(message: unknown): string {
|
|
132
|
-
|
|
133
|
-
const record = message as { content?: unknown };
|
|
134
|
-
if (typeof record.content === 'string') return record.content;
|
|
135
|
-
if (Array.isArray(record.content)) {
|
|
136
|
-
return record.content
|
|
137
|
-
.filter((part: unknown) => part && typeof part === 'object' && (part as { type?: unknown }).type === 'text')
|
|
138
|
-
.map((part) => String((part as { text?: unknown }).text ?? ''))
|
|
139
|
-
.join('\n')
|
|
140
|
-
.trim();
|
|
141
|
-
}
|
|
142
|
-
return '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function detectCorrectionCue(text: string): string | null {
|
|
146
|
-
const normalized = text
|
|
147
|
-
.trim()
|
|
148
|
-
.toLowerCase()
|
|
149
|
-
.replace(/[.,!?;:,。!?;:]/g, '');
|
|
150
|
-
const cues = [
|
|
151
|
-
'不是这个',
|
|
152
|
-
'不对',
|
|
153
|
-
'错了',
|
|
154
|
-
'搞错了',
|
|
155
|
-
'理解错了',
|
|
156
|
-
'你理解错了',
|
|
157
|
-
'重新来',
|
|
158
|
-
'再试一次',
|
|
159
|
-
'you are wrong',
|
|
160
|
-
'wrong file',
|
|
161
|
-
'not this',
|
|
162
|
-
'redo',
|
|
163
|
-
'try again',
|
|
164
|
-
'again',
|
|
165
|
-
'please redo',
|
|
166
|
-
'please try again',
|
|
167
|
-
];
|
|
168
|
-
return cues.find((cue) => normalized.includes(cue)) ?? null;
|
|
129
|
+
return extractMessageContent(message);
|
|
169
130
|
}
|
|
170
131
|
|
|
171
132
|
/**
|
|
@@ -297,64 +258,25 @@ export function getDiagnosticianModel(api: PromptHookApi | null, logger?: Plugin
|
|
|
297
258
|
/**
|
|
298
259
|
* Extract recent user messages for keyword optimization context.
|
|
299
260
|
*/
|
|
300
|
-
|
|
301
|
-
if (!Array.isArray(messages)) return [];
|
|
302
|
-
const userMessages: string[] = [];
|
|
303
|
-
|
|
304
|
-
for (let i = messages.length - 1; i >= 0 && userMessages.length < limit; i--) {
|
|
305
|
-
const msg = messages[i] as { role?: string; content?: unknown };
|
|
306
|
-
if (msg?.role !== 'user') continue;
|
|
307
|
-
|
|
308
|
-
let text = '';
|
|
309
|
-
if (typeof msg.content === 'string') {
|
|
310
|
-
text = msg.content;
|
|
311
|
-
} else if (Array.isArray(msg.content)) {
|
|
312
|
-
text = msg.content
|
|
313
|
-
.filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text' && typeof (part as { text?: unknown }).text === 'string')
|
|
314
|
-
.map((part: unknown) => (part as { text: string }).text)
|
|
315
|
-
.join('\n')
|
|
316
|
-
.trim();
|
|
317
|
-
}
|
|
318
|
-
if (text) userMessages.unshift(text.substring(0, 500));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return userMessages;
|
|
322
|
-
}
|
|
261
|
+
|
|
323
262
|
|
|
324
263
|
/**
|
|
325
264
|
* Build prompt for keyword optimization subagent.
|
|
326
265
|
*/
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Return STRICT JSON (no markdown):
|
|
340
|
-
|
|
341
|
-
{"updates": {"TERM": {"action": "add|update|remove", "weight": number, "falsePositiveRate": number, "reasoning": "string"}}}
|
|
342
|
-
|
|
343
|
-
## Current Keyword Store (${Object.keys(keywordStore.terms).length} terms):
|
|
344
|
-
${currentTerms}
|
|
345
|
-
|
|
346
|
-
## Recent User Messages (${recentMessages.length} messages):
|
|
347
|
-
${recentMessages.map((m, i) => `${i + 1}. "${m}"`).join('\n')}
|
|
348
|
-
|
|
349
|
-
## Rules:
|
|
350
|
-
- ADD: If a message contains frustration/empathy signals not in current terms
|
|
351
|
-
- UPDATE: If a term's weight should change (high hits → increase weight, low hits → decrease)
|
|
352
|
-
- REMOVE: If a term has 0 hits after many turns AND high false positive rate (>0.3)
|
|
353
|
-
- Keep reasoning concise (max 100 chars)
|
|
354
|
-
- Weight range: 0.1-0.9
|
|
355
|
-
- falsePositiveRate range: 0.05-0.5
|
|
356
|
-
`;
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
function ensureConfirmFirstStore(workspaceDir: string): void {
|
|
269
|
+
if (!_confirmFirstStoreInitialized) {
|
|
270
|
+
try {
|
|
271
|
+
const connection = new SqliteConnection({ workspaceDir, readonly: false });
|
|
272
|
+
setConfirmFirstStore(new SqliteConfirmFirstStateStore(connection));
|
|
273
|
+
_confirmFirstStoreInitialized = true;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.warn(`[PD:ConfirmFirst] Failed to initialize store: ${String(err)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
357
278
|
}
|
|
279
|
+
let _confirmFirstStoreInitialized = false;
|
|
358
280
|
|
|
359
281
|
export async function handleBeforePromptBuild(
|
|
360
282
|
event: PluginHookBeforePromptBuildEvent,
|
|
@@ -364,23 +286,29 @@ export async function handleBeforePromptBuild(
|
|
|
364
286
|
const logger = ctx.api?.logger;
|
|
365
287
|
logger?.info?.(`[PD:Prompt] handleBeforePromptBuild called: workspaceDir=${!!workspaceDir}, trigger=${ctx.trigger}, sessionId=${ctx.sessionId?.substring(0, 20)}`);
|
|
366
288
|
if (!workspaceDir) {
|
|
367
|
-
logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping
|
|
289
|
+
logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping PD context injection`);
|
|
368
290
|
return;
|
|
369
291
|
}
|
|
370
292
|
|
|
371
|
-
// ──── DEBUG: Verify subagent availability in this context ────
|
|
372
|
-
const subagent = ctx.api?.runtime?.subagent;
|
|
373
|
-
logger?.info?.(`[PD:DEBUG:SubagentCheck] trigger=${ctx.trigger}, subagent_exists=${!!subagent}, subagent.run_exists=${!!subagent?.run}`);
|
|
374
|
-
if (subagent?.run) {
|
|
375
|
-
logger?.info?.('[PD:DEBUG:SubagentCheck] run entrypoint is callable');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
293
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
379
|
-
const { trigger, sessionId
|
|
294
|
+
const { trigger, sessionId } = ctx as { trigger: string | undefined; sessionId: string | undefined };
|
|
295
|
+
const api = ctx.api;
|
|
380
296
|
if (sessionId) {
|
|
381
297
|
wctx.trajectory?.recordSession?.({ sessionId });
|
|
382
298
|
}
|
|
383
299
|
|
|
300
|
+
if (sessionId) {
|
|
301
|
+
ensureConfirmFirstStore(workspaceDir);
|
|
302
|
+
hydrateFromStore(sessionId);
|
|
303
|
+
_confirmFirstHydrationCounter++;
|
|
304
|
+
if (_confirmFirstHydrationCounter % 100 === 0) {
|
|
305
|
+
const pruned = pruneStoreStaleRows();
|
|
306
|
+
if (pruned > 0) {
|
|
307
|
+
logger?.info?.(`[PD:ConfirmFirst] Pruned ${pruned} stale rows from confirm_first_state`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
384
312
|
if (sessionId && trigger === 'user' && Array.isArray(event.messages) && event.messages.length > 0) {
|
|
385
313
|
const latestUserIndex = [...event.messages]
|
|
386
314
|
.map((message, index) => ({ message, index }))
|
|
@@ -389,6 +317,26 @@ export async function handleBeforePromptBuild(
|
|
|
389
317
|
|
|
390
318
|
if (latestUserIndex) {
|
|
391
319
|
const userText = getTextContent(latestUserIndex.message);
|
|
320
|
+
|
|
321
|
+
// ── Confirm-first approval detection ──
|
|
322
|
+
// If user sends approval language, mark session as approved for confirm-first gate
|
|
323
|
+
if (sessionId && detectApprovalMarker(userText)) {
|
|
324
|
+
setConfirmFirstApproval(sessionId);
|
|
325
|
+
// P2: Emit approval telemetry for observability (ERR-002)
|
|
326
|
+
try {
|
|
327
|
+
wctx.eventLog.recordConfirmFirstGateApproved({
|
|
328
|
+
sessionId,
|
|
329
|
+
workspaceDir: wctx.workspaceDir,
|
|
330
|
+
toolName: '(approval)',
|
|
331
|
+
reason: 'user_approval_detected',
|
|
332
|
+
principleId: 'confirm-first',
|
|
333
|
+
nextAction: 'mutating tools now permitted',
|
|
334
|
+
});
|
|
335
|
+
} catch (logErr) {
|
|
336
|
+
logger?.warn?.(`[PD:ConfirmFirst] Failed to emit approval event: ${String(logErr)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
392
340
|
// Use CorrectionCueLearner for detection — supports learned keywords, not just hardcoded list
|
|
393
341
|
let correctionCue: string | null = null;
|
|
394
342
|
try {
|
|
@@ -406,7 +354,7 @@ export async function handleBeforePromptBuild(
|
|
|
406
354
|
}
|
|
407
355
|
} catch (learnerErr) {
|
|
408
356
|
// Fallback to hardcoded detection if learner fails — log for observability
|
|
409
|
-
correctionCue =
|
|
357
|
+
correctionCue = coreDetectCorrectionCue(userText);
|
|
410
358
|
logger?.warn?.(`[PD:Prompt] CorrectionCueLearner.match() failed (${String(learnerErr)}), fallback=${correctionCue ? `matched="${correctionCue}"` : 'no-match'}`);
|
|
411
359
|
}
|
|
412
360
|
let referencesAssistantTurnId: number | null = null;
|
|
@@ -432,10 +380,10 @@ export async function handleBeforePromptBuild(
|
|
|
432
380
|
}
|
|
433
381
|
|
|
434
382
|
// Load context injection configuration
|
|
435
|
-
const contextConfig = loadContextInjectionConfig(workspaceDir);
|
|
383
|
+
const contextConfig = loadContextInjectionConfig(wctx.workspaceDir);
|
|
436
384
|
|
|
437
385
|
// Minimal mode: heartbeat and subagents skip most context to reduce tokens
|
|
438
|
-
const isMinimalMode = trigger
|
|
386
|
+
const isMinimalMode = isMinimalTrigger(trigger as string | undefined, sessionId as string | undefined);
|
|
439
387
|
|
|
440
388
|
const session = sessionId ? getSession(sessionId) : undefined;
|
|
441
389
|
|
|
@@ -535,7 +483,9 @@ The empathy observer subagent handles pain detection independently.
|
|
|
535
483
|
|
|
536
484
|
const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
|
|
537
485
|
|
|
486
|
+
// Empathy Observer: keyword fast-path + optional LLM deep analysis (zero latency async dispatch)
|
|
538
487
|
const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
|
|
488
|
+
|
|
539
489
|
logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
|
|
540
490
|
|
|
541
491
|
// Track if we should inject behavioral constraints (will be added to appendSystemContext later)
|
|
@@ -564,23 +514,6 @@ The empathy observer subagent handles pain detection independently.
|
|
|
564
514
|
_empathyTurnCounter++;
|
|
565
515
|
const turnCount = _empathyTurnCounter;
|
|
566
516
|
|
|
567
|
-
// Decision: should we call subagent?
|
|
568
|
-
let shouldCallSubagent = false;
|
|
569
|
-
let samplingReason = '';
|
|
570
|
-
|
|
571
|
-
if (matchResult.score >= 0.8) {
|
|
572
|
-
// High confidence — keyword match is reliable, no subagent needed
|
|
573
|
-
shouldCallSubagent = false;
|
|
574
|
-
} else if (matchResult.score >= 0.3) {
|
|
575
|
-
// Boundary case — 30% sampling for subagent verification
|
|
576
|
-
shouldCallSubagent = Math.random() < 0.3;
|
|
577
|
-
samplingReason = 'boundary_verification';
|
|
578
|
-
} else {
|
|
579
|
-
// No keyword hit — 5% random sampling to discover new expressions
|
|
580
|
-
shouldCallSubagent = Math.random() < 0.05;
|
|
581
|
-
samplingReason = 'random_discovery';
|
|
582
|
-
}
|
|
583
|
-
|
|
584
517
|
if (matchResult.matched) {
|
|
585
518
|
const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
|
|
586
519
|
// trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
|
|
@@ -588,93 +521,194 @@ The empathy observer subagent handles pain detection independently.
|
|
|
588
521
|
source: 'user_empathy',
|
|
589
522
|
});
|
|
590
523
|
|
|
591
|
-
logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
subagent: toWorkflowSubagent(runtimeSubagent),
|
|
616
|
-
});
|
|
617
|
-
empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
|
|
618
|
-
parentSessionId: sessionId,
|
|
619
|
-
workspaceDir,
|
|
620
|
-
taskInput: latestUserMessage,
|
|
621
|
-
}).catch((err) => {
|
|
622
|
-
if (!isExpectedSubagentError(err)) {
|
|
623
|
-
api.logger?.warn?.(`[PD:Empathy] subagent sample failed: ${String(err)}`);
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
}
|
|
524
|
+
logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}`);
|
|
525
|
+
|
|
526
|
+
const currentSession = getSession(sessionId);
|
|
527
|
+
const currentGfi = currentSession?.currentGfi ?? 0;
|
|
528
|
+
const painTrigger = wctx.config.get('thresholds.pain_trigger') || 40;
|
|
529
|
+
const highGfiThreshold = Math.max(wctx.config.get('severity_thresholds.high') || 70, painTrigger + 30);
|
|
530
|
+
|
|
531
|
+
if (currentGfi >= highGfiThreshold) {
|
|
532
|
+
const gfiPainScore = Math.min(Math.round(currentGfi), 60);
|
|
533
|
+
logger?.info?.(`[PD:Empathy] GFI-TRIGGERED: currentGfi=${currentGfi.toFixed(1)} >= highGfi=${highGfiThreshold}, emitting pain signal (score=${gfiPainScore})`);
|
|
534
|
+
|
|
535
|
+
const gate = evaluatePainDiagnosticGate({
|
|
536
|
+
source: 'user_empathy',
|
|
537
|
+
score: gfiPainScore,
|
|
538
|
+
currentGfi,
|
|
539
|
+
consecutiveErrors: currentSession?.consecutiveErrors ?? 0,
|
|
540
|
+
sessionId,
|
|
541
|
+
errorHash: 'empathy_gfi_threshold',
|
|
542
|
+
thresholds: {
|
|
543
|
+
painTrigger,
|
|
544
|
+
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
545
|
+
semanticPain: Math.max(painTrigger, 60),
|
|
546
|
+
},
|
|
547
|
+
});
|
|
627
548
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
549
|
+
wctx.eventLog.recordPainSignal(sessionId, {
|
|
550
|
+
score: gfiPainScore,
|
|
551
|
+
source: 'user_empathy',
|
|
552
|
+
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
553
|
+
isRisky: false,
|
|
554
|
+
});
|
|
634
555
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
556
|
+
if (gate.shouldDiagnose) {
|
|
557
|
+
logger?.info?.(`[PD:Empathy] Gate approved, calling emitPainDetectedEvent...`);
|
|
558
|
+
try {
|
|
559
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
560
|
+
await emitPainDetectedEvent(wctx, {
|
|
561
|
+
ts: new Date().toISOString(),
|
|
562
|
+
type: 'pain_detected',
|
|
563
|
+
data: {
|
|
564
|
+
painId: `empathy_gfi_${Date.now()}`,
|
|
565
|
+
painType: 'user_frustration',
|
|
566
|
+
source: 'user_empathy',
|
|
567
|
+
reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
|
|
568
|
+
score: gfiPainScore,
|
|
569
|
+
sessionId,
|
|
570
|
+
agentId: 'main',
|
|
571
|
+
provenance: 'openclaw_context_bound',
|
|
572
|
+
evidence,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
|
|
576
|
+
} catch (emitErr) {
|
|
577
|
+
console.error(`[PD:Empathy] FAILED to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
578
|
+
logger?.warn?.(`[PD:Empathy] Failed to emit GFI-triggered pain event: ${String(emitErr)}`);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
logger?.info?.(`[PD:Empathy] GFI-triggered gate rejected: ${gate.detail}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
639
584
|
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
workspaceDir,
|
|
649
|
-
logger: api.logger ?? console,
|
|
650
|
-
|
|
651
|
-
subagent: toWorkflowSubagent(api.runtime.subagent),
|
|
585
|
+
// Trigger asynchronous background Empathy Observer deep analysis (Zero Latency)
|
|
586
|
+
const observer = resolveEmpathyObserver(wctx, logger);
|
|
587
|
+
if (observer) {
|
|
588
|
+
const scheduler = new AgentScheduler();
|
|
589
|
+
scheduler.register({
|
|
590
|
+
agentId: 'empathy-observer',
|
|
591
|
+
mode: 'realtime',
|
|
592
|
+
runner: observer,
|
|
652
593
|
});
|
|
594
|
+
|
|
595
|
+
logger?.info?.(`[PD:Empathy] Triggering background Empathy Observer deep analysis for message: "${msgPreview}"`);
|
|
653
596
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
597
|
+
void scheduler.dispatch('empathy-observer', { userMessage: latestUserMessage })
|
|
598
|
+
.then(async (result) => {
|
|
599
|
+
if (result.damageDetected) {
|
|
600
|
+
logger?.info?.(`[PD:Empathy] Background Empathy Observer detected damage. Severity: ${result.severity}, Reason: ${result.reason}`);
|
|
601
|
+
|
|
602
|
+
// ── Persistence Contract ──
|
|
603
|
+
const painScore = scoreFromSeverityForSpec(result.severity, wctx);
|
|
604
|
+
|
|
605
|
+
trackFriction(
|
|
606
|
+
sessionId,
|
|
607
|
+
painScore,
|
|
608
|
+
`observer_empathy_${result.severity}`,
|
|
609
|
+
workspaceDir,
|
|
610
|
+
{ source: 'user_empathy' }
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
614
|
+
wctx.eventLog.recordPainSignal(sessionId, {
|
|
615
|
+
score: painScore,
|
|
616
|
+
source: 'user_empathy',
|
|
617
|
+
reason: result.reason || 'Empathy observer detected likely user frustration.',
|
|
618
|
+
isRisky: false,
|
|
619
|
+
origin: 'system_infer',
|
|
620
|
+
severity: result.severity,
|
|
621
|
+
confidence: result.confidence,
|
|
622
|
+
detection_mode: 'structured',
|
|
623
|
+
deduped: false,
|
|
624
|
+
trigger_text_excerpt: latestUserMessage.substring(0, 120),
|
|
625
|
+
raw_score: painScore,
|
|
626
|
+
calibrated_score: painScore,
|
|
627
|
+
eventId,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
632
|
+
sessionId,
|
|
633
|
+
source: 'user_empathy',
|
|
634
|
+
score: painScore,
|
|
635
|
+
reason: result.reason || 'Empathy observer detected likely user frustration.',
|
|
636
|
+
severity: result.severity,
|
|
637
|
+
origin: 'system_infer',
|
|
638
|
+
confidence: result.confidence,
|
|
639
|
+
text: latestUserMessage,
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
logger?.warn?.(`[PD:Empathy] Failed to persist trajectory: ${String(error)}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Check if GFI triggers a pain event post-LLM validation
|
|
646
|
+
const freshSession = getSession(sessionId);
|
|
647
|
+
const freshGfi = freshSession?.currentGfi ?? 0;
|
|
648
|
+
if (freshGfi >= highGfiThreshold) {
|
|
649
|
+
const freshGfiPainScore = Math.min(Math.round(freshGfi), 60);
|
|
650
|
+
const gate = evaluatePainDiagnosticGate({
|
|
651
|
+
source: 'user_empathy',
|
|
652
|
+
score: freshGfiPainScore,
|
|
653
|
+
currentGfi: freshGfi,
|
|
654
|
+
consecutiveErrors: freshSession?.consecutiveErrors ?? 0,
|
|
655
|
+
sessionId,
|
|
656
|
+
errorHash: 'empathy_gfi_threshold',
|
|
657
|
+
thresholds: {
|
|
658
|
+
painTrigger,
|
|
659
|
+
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
660
|
+
semanticPain: Math.max(painTrigger, 60),
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (gate.shouldDiagnose) {
|
|
665
|
+
logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
|
|
666
|
+
try {
|
|
667
|
+
const evidence = buildTrajectoryEvidence(wctx, sessionId);
|
|
668
|
+
await emitPainDetectedEvent(wctx, {
|
|
669
|
+
ts: new Date().toISOString(),
|
|
670
|
+
type: 'pain_detected',
|
|
671
|
+
data: {
|
|
672
|
+
painId: `empathy_gfi_${Date.now()}`,
|
|
673
|
+
painType: 'user_frustration',
|
|
674
|
+
source: 'user_empathy',
|
|
675
|
+
reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
|
|
676
|
+
score: freshGfiPainScore,
|
|
677
|
+
sessionId,
|
|
678
|
+
agentId: 'main',
|
|
679
|
+
provenance: 'openclaw_context_bound',
|
|
680
|
+
evidence,
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
} catch (emitErr) {
|
|
684
|
+
logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
logger?.info?.(`[PD:Empathy] Background Empathy Observer did not detect any damage.`);
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
.catch((err) => {
|
|
693
|
+
logger?.warn?.(`[PD:Empathy] Background analysis failed or rejected: ${String(err)}`);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
// Log unmatched messages periodically for coverage analysis
|
|
698
|
+
if (turnCount > 0 && turnCount % 50 === 0) {
|
|
699
|
+
const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
|
|
700
|
+
logger?.debug?.(`[PD:Empathy] NO_MATCH: "${sampleMsg}" (turn ${turnCount}, keywords_in_store=${Object.keys(keywordStore.terms).length})`);
|
|
667
701
|
}
|
|
668
702
|
}
|
|
669
703
|
|
|
670
704
|
// Periodic summary (every 100 turns)
|
|
671
705
|
if (turnCount > 0 && turnCount % 100 === 0) {
|
|
672
|
-
|
|
706
|
+
const s = getKeywordStoreSummary(keywordStore);
|
|
707
|
+
const highFP = s.highFalsePositiveTerms.slice(0, 5).map(t => `${t.term}(${t.falsePositiveRate.toFixed(2)})`).join(', ');
|
|
708
|
+
logger?.info?.(`[PD:Empathy] SUMMARY(turn=${turnCount}): terms=${s.totalTerms}, hits=${keywordStore.stats.totalHits}, zero_hit=${s.totalTerms - (s.seedTerms + s.discoveredTerms)}, high_fp=[${highFP}]`);
|
|
673
709
|
}
|
|
674
710
|
|
|
675
|
-
// Save keyword store on every match
|
|
676
|
-
// Previously used turnCount % 50 gate which caused hitCount loss because
|
|
677
|
-
// module-level state resets on plugin reload before reaching turn 50.
|
|
711
|
+
// Save keyword store on every match
|
|
678
712
|
if (matchResult.matched) {
|
|
679
713
|
saveKeywordStore(wctx.stateDir, keywordStore);
|
|
680
714
|
const {totalHits} = keywordStore.stats;
|
|
@@ -685,21 +719,6 @@ The empathy observer subagent handles pain detection independently.
|
|
|
685
719
|
}
|
|
686
720
|
}
|
|
687
721
|
|
|
688
|
-
// Empathy Observer: analyze user message for frustration signals (legacy, disabled)
|
|
689
|
-
// The keyword matching approach above is now the primary empathy detection method.
|
|
690
|
-
// The subagent-based observer is kept for periodic keyword optimization only.
|
|
691
|
-
// if (workspaceDir) {
|
|
692
|
-
// const empathyManager = new EmpathyObserverWorkflowManager({
|
|
693
|
-
// workspaceDir,
|
|
694
|
-
// logger: api.logger,
|
|
695
|
-
// subagent: api.runtime.subagent as any,
|
|
696
|
-
// });
|
|
697
|
-
// empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
|
|
698
|
-
// parentSessionId: sessionId,
|
|
699
|
-
// workspaceDir,
|
|
700
|
-
// taskInput: latestUserMessage,
|
|
701
|
-
// }).catch((err) => api.logger.warn(`[PD:Empathy] workflow failed: ${String(err)}`));
|
|
702
|
-
// }
|
|
703
722
|
}
|
|
704
723
|
|
|
705
724
|
// ──── 4. Heartbeat-specific checklist (also fires for cron-triggered sessions) ────
|
|
@@ -728,121 +747,12 @@ ${heartbeatChecklist}
|
|
|
728
747
|
}
|
|
729
748
|
}
|
|
730
749
|
|
|
731
|
-
// ──── 4b. Inject pending diagnostician tasks (compact summary) ────
|
|
732
|
-
// FIX (#283/#380): The evolution worker writes pain diagnosis tasks to
|
|
733
|
-
// diagnostician_tasks.json. The heartbeat prompt hook must read and inject
|
|
734
|
-
// them so the LLM (acting as diagnostician) can process them.
|
|
735
|
-
//
|
|
736
|
-
// INJECTION FORMAT: Compact summary (not full prompt) to stay well within
|
|
737
|
-
// OpenClaw's ~10 000 char platform limit. Full task.prompt can be 2–4 KB;
|
|
738
|
-
// the compact block is < 400 chars. The agent is instructed to read the
|
|
739
|
-
// original from diagnostician_tasks.json if it needs the full context.
|
|
740
|
-
try {
|
|
741
|
-
const pendingTasks = getPendingDiagnosticianTasks(wctx.stateDir);
|
|
742
|
-
if (pendingTasks.length > 0) {
|
|
743
|
-
pendingDiagTaskCount = pendingTasks.length;
|
|
744
|
-
|
|
745
|
-
// Build compact summary blocks — one per task (only first is processed per heartbeat)
|
|
746
|
-
const taskBlocks = pendingTasks
|
|
747
|
-
.slice(0, 1)
|
|
748
|
-
.map(({ id, task }) => {
|
|
749
|
-
// Extract summary fields; reason is truncated to 200 chars to keep
|
|
750
|
-
// the injected block small and stable.
|
|
751
|
-
const reason = (task.prompt
|
|
752
|
-
.match(/reason["\s:]+([^\n]{0,240})/i)?.[1]
|
|
753
|
-
?? task.prompt.slice(0, 200)
|
|
754
|
-
).slice(0, 200);
|
|
755
|
-
|
|
756
|
-
const safeId = escapeXml(id);
|
|
757
|
-
const safeReason = escapeXml(reason);
|
|
758
|
-
const safeCreatedAt = escapeXml(task.createdAt);
|
|
759
|
-
const markerFile = `.evolution_complete_${safeId}`;
|
|
760
|
-
const reportFile = `.diagnostician_report_${safeId}.json`;
|
|
761
|
-
|
|
762
|
-
return `<diagnostician_task id="${safeId}">
|
|
763
|
-
task_id: ${safeId}
|
|
764
|
-
reason: ${safeReason}
|
|
765
|
-
marker: ${markerFile}
|
|
766
|
-
report: ${reportFile}
|
|
767
|
-
queued_at: ${safeCreatedAt}
|
|
768
|
-
action: Analyze pain signal → identify violated principles → write ${markerFile} + ${reportFile}
|
|
769
|
-
</diagnostician_task>`;
|
|
770
|
-
})
|
|
771
|
-
.join('\n\n');
|
|
772
|
-
|
|
773
|
-
const processingNote = pendingDiagTaskCount > 1
|
|
774
|
-
? `\n\nNOTE: ${pendingDiagTaskCount - 1} more task(s) are queued. ` +
|
|
775
|
-
`Process one at a time; remaining tasks are handled on subsequent heartbeats.`
|
|
776
|
-
: '';
|
|
777
|
-
|
|
778
|
-
prependContext += `<diagnostician_tasks pending="${pendingDiagTaskCount}">
|
|
779
|
-
You are acting as a **Pain Diagnostician**. For each task:
|
|
780
|
-
1. Read the full prompt from: ${escapeXml(wctx.stateDir)}/diagnostician_tasks.json [task_id=${escapeXml(pendingTasks[0]?.id ?? '')}]
|
|
781
|
-
2. Analyze the pain signal and its context
|
|
782
|
-
3. Identify the root cause and violated principles
|
|
783
|
-
4. Write a completion marker: .evolution_complete_<TASK_ID>
|
|
784
|
-
5. Write a diagnostic report: .diagnostician_report_<TASK_ID>.json
|
|
785
|
-
|
|
786
|
-
${taskBlocks}${processingNote}
|
|
787
|
-
</diagnostician_tasks>\n`;
|
|
788
|
-
|
|
789
|
-
logger?.info?.(
|
|
790
|
-
`[PD:Prompt] Injected compact diagnostician task block ` +
|
|
791
|
-
`(task=${pendingTasks[0]?.id}, total_pending=${pendingDiagTaskCount})`
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
// C: Record heartbeat_diagnosis event for observability
|
|
795
|
-
try {
|
|
796
|
-
const eventLog = EventLogService.get(wctx.stateDir, logger);
|
|
797
|
-
eventLog.recordHeartbeatDiagnosis({
|
|
798
|
-
taskCount: pendingDiagTaskCount,
|
|
799
|
-
taskIds: pendingTasks.slice(0, 1).map(t => t.id),
|
|
800
|
-
trigger: 'heartbeat',
|
|
801
|
-
});
|
|
802
|
-
} catch (evErr) {
|
|
803
|
-
logger?.warn?.(`[PD:Prompt] Failed to record heartbeat_diagnosis event: ${String(evErr)}`);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
} catch (e) {
|
|
807
|
-
logger?.warn?.(`[PD:Prompt] Failed to read diagnostician tasks: ${String(e)}`);
|
|
808
|
-
}
|
|
809
750
|
}
|
|
810
751
|
|
|
811
752
|
// ──── 6. Dynamic Attitude Matrix (based on GFI) ────
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
let attitudeDirective: string;
|
|
753
|
+
|
|
815
754
|
const currentGfi = session?.currentGfi || 0;
|
|
816
|
-
|
|
817
|
-
if (currentGfi >= 70) {
|
|
818
|
-
attitudeDirective = `
|
|
819
|
-
### 【SYSTEM_MODE: HUMBLE_RECOVERY】
|
|
820
|
-
**CURRENT STATUS**: Severe system friction / User frustration detected (GFI: ${currentGfi.toFixed(0)}).
|
|
821
|
-
**BEHAVIORAL OVERRIDE**:
|
|
822
|
-
- You have failed to meet expectations. Humility is your primary directive.
|
|
823
|
-
- **STOP** aggressive file modifications.
|
|
824
|
-
- **START** every response with a sincere, non-defensive apology.
|
|
825
|
-
- **ACTION**: Explain why you failed, and propose a highly cautious recovery plan.
|
|
826
|
-
`;
|
|
827
|
-
} else if (currentGfi >= 40) {
|
|
828
|
-
attitudeDirective = `
|
|
829
|
-
### 【SYSTEM_MODE: CONCILIATORY】
|
|
830
|
-
**CURRENT STATUS**: Moderate friction detected (GFI: ${currentGfi.toFixed(0)}).
|
|
831
|
-
**BEHAVIORAL OVERRIDE**:
|
|
832
|
-
- User is frustrated. Be more explanatory and cautious.
|
|
833
|
-
- Before executing any tool, clearly state what you intend to do and **WAIT** for implicit or explicit user consent.
|
|
834
|
-
- Avoid technical jargon; focus on the business/project value of your changes.
|
|
835
|
-
`;
|
|
836
|
-
} else {
|
|
837
|
-
attitudeDirective = `
|
|
838
|
-
### 【SYSTEM_MODE: EFFICIENT】
|
|
839
|
-
**CURRENT STATUS**: System healthy (GFI: ${currentGfi.toFixed(0)}).
|
|
840
|
-
**BEHAVIORAL OVERRIDE**:
|
|
841
|
-
- Maintain peak efficiency.
|
|
842
|
-
- Be concise. Prefer action over long explanations.
|
|
843
|
-
- Follow the "Principles > Directives" rule strictly.
|
|
844
|
-
`;
|
|
845
|
-
}
|
|
755
|
+
const attitudeDirective = buildAttitudeDirective(currentGfi);
|
|
846
756
|
|
|
847
757
|
// ──── 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ────
|
|
848
758
|
// NOTE: Principles is ALWAYS injected (not configurable)
|
|
@@ -918,7 +828,7 @@ ${taskBlocks}${processingNote}
|
|
|
918
828
|
projectContextContent = extractSummary(finalContent, 30);
|
|
919
829
|
} else {
|
|
920
830
|
// Full mode: current version + recent history (3 versions)
|
|
921
|
-
const historyVersions = getHistoryVersions(focusPath, 3);
|
|
831
|
+
const historyVersions = await getHistoryVersions(focusPath, 3);
|
|
922
832
|
if (historyVersions.length > 0) {
|
|
923
833
|
const historySections = historyVersions.map((v, i) =>
|
|
924
834
|
`\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`
|
|
@@ -943,13 +853,33 @@ ${taskBlocks}${processingNote}
|
|
|
943
853
|
const allActive = reducer.getActivePrinciples();
|
|
944
854
|
const allProbation = reducer.getProbationPrinciples();
|
|
945
855
|
|
|
856
|
+
// Pruning mask: exclude principles whose latest review is archive-candidate
|
|
857
|
+
let maskedIds = new Set<string>();
|
|
858
|
+
try {
|
|
859
|
+
maskedIds = getCachedMaskedPrincipleSet(wctx.workspaceDir);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
// Safe degradation: if review log unreadable, inject all principles
|
|
862
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
863
|
+
if (logger?.info) {
|
|
864
|
+
logger.info(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
|
|
865
|
+
} else {
|
|
866
|
+
console.error(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
946
870
|
// Budget-aware selection: prioritize P0>P1>P2 and recency
|
|
947
|
-
const activeSelection = selectPrinciplesForInjection(
|
|
871
|
+
const activeSelection = selectPrinciplesForInjection(
|
|
872
|
+
allActive.filter(p => !maskedIds.has(p.id)),
|
|
873
|
+
DEFAULT_PRINCIPLE_BUDGET,
|
|
874
|
+
);
|
|
948
875
|
const active = activeSelection.selected;
|
|
949
876
|
|
|
950
877
|
// Probation principles get a smaller sub-budget (1000 chars)
|
|
951
878
|
const probationBudget = 1000;
|
|
952
|
-
const probationSelection = selectPrinciplesForInjection(
|
|
879
|
+
const probationSelection = selectPrinciplesForInjection(
|
|
880
|
+
allProbation.filter(p => !maskedIds.has(p.id)),
|
|
881
|
+
probationBudget,
|
|
882
|
+
);
|
|
953
883
|
const probation = probationSelection.selected;
|
|
954
884
|
|
|
955
885
|
if (activeSelection.wasTruncated || probationSelection.wasTruncated) {
|
|
@@ -986,13 +916,105 @@ ${taskBlocks}${processingNote}
|
|
|
986
916
|
logger?.warn?.(`[PD:Prompt] Failed to load evolution principles: ${String(e)}`);
|
|
987
917
|
}
|
|
988
918
|
|
|
919
|
+
let runtimeV2PrinciplesContent = '';
|
|
920
|
+
const runtimeV2PrincipleIds = new Set<string>();
|
|
921
|
+
// Hoisted so the owner_approved_behavior_directives section can access them
|
|
922
|
+
let dedupedV2: Array<{ principleId: string; text: string; artifactId: string; activationId: string }> = [];
|
|
923
|
+
try {
|
|
924
|
+
const reader = new PromptActivationReader(wctx.workspaceDir, { logger });
|
|
925
|
+
const v2Result = await reader.readActivatedPrinciples();
|
|
926
|
+
|
|
927
|
+
if (v2Result.warnings.length > 0) {
|
|
928
|
+
logger?.info?.(`[PD:RuntimeV2] Activation read warnings: ${v2Result.warnings.join('; ')}`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const legacyActiveIds = new Set<string>();
|
|
932
|
+
try {
|
|
933
|
+
const legacyActive = wctx.evolutionReducer.getActivePrinciples();
|
|
934
|
+
for (const p of legacyActive) {
|
|
935
|
+
legacyActiveIds.add(p.id);
|
|
936
|
+
}
|
|
937
|
+
const legacyProbation = wctx.evolutionReducer.getProbationPrinciples();
|
|
938
|
+
for (const p of legacyProbation) {
|
|
939
|
+
legacyActiveIds.add(p.id);
|
|
940
|
+
}
|
|
941
|
+
} catch {
|
|
942
|
+
// best-effort dedup
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
dedupedV2 = v2Result.principles.filter((p) => !legacyActiveIds.has(p.principleId));
|
|
946
|
+
|
|
947
|
+
if (dedupedV2.length > 0) {
|
|
948
|
+
let remaining = RUNTIME_V2_PRINCIPLE_BUDGET;
|
|
949
|
+
const lines: string[] = [];
|
|
950
|
+
lines.push('Runtime V2 activated principles (owner-approved):');
|
|
951
|
+
remaining -= 'Runtime V2 activated principles (owner-approved):'.length;
|
|
952
|
+
|
|
953
|
+
for (const p of dedupedV2) {
|
|
954
|
+
const entry = `- [${escapeXml(p.principleId)}] ${escapeXml(p.text)}`;
|
|
955
|
+
if (remaining < entry.length + 1) {
|
|
956
|
+
logger?.info?.(`[PD:RuntimeV2] Principle budget reached (${RUNTIME_V2_PRINCIPLE_BUDGET}c) — truncating after ${lines.length - 1} principles`);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
lines.push(entry);
|
|
960
|
+
remaining -= entry.length + 1;
|
|
961
|
+
runtimeV2PrincipleIds.add(p.principleId);
|
|
962
|
+
}
|
|
963
|
+
runtimeV2PrinciplesContent = lines.join('\n');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ── Emit structured observability event ──
|
|
967
|
+
try {
|
|
968
|
+
const eventLog = wctx.eventLog;
|
|
969
|
+
eventLog.recordRuntimeV2ActivationsInjected({
|
|
970
|
+
sessionId: sessionId ?? 'unknown',
|
|
971
|
+
workspaceDir: wctx.workspaceDir,
|
|
972
|
+
principleIds: [...runtimeV2PrincipleIds],
|
|
973
|
+
activationIds: dedupedV2.map((p) => p.activationId),
|
|
974
|
+
artifactIds: dedupedV2.map((p) => p.artifactId),
|
|
975
|
+
injectedCount: runtimeV2PrincipleIds.size,
|
|
976
|
+
skippedWarnings: v2Result.warnings,
|
|
977
|
+
injectedCharCount: runtimeV2PrinciplesContent.length,
|
|
978
|
+
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
979
|
+
...(runtimeV2PrincipleIds.size === 0
|
|
980
|
+
? {
|
|
981
|
+
skipReason: v2Result.principles.length === 0
|
|
982
|
+
? 'no_validated_activations'
|
|
983
|
+
: 'all_deduped_against_legacy',
|
|
984
|
+
nextAction: v2Result.principles.length === 0
|
|
985
|
+
? 'check activations table for prompt channel rows with validated artifacts'
|
|
986
|
+
: 'legacy evolution reducer already contains these principle IDs',
|
|
987
|
+
}
|
|
988
|
+
: {}),
|
|
989
|
+
});
|
|
990
|
+
} catch (logErr) {
|
|
991
|
+
logger?.warn?.(`[PD:RuntimeV2] Failed to emit activation observability event: ${String(logErr)}`);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ── Set confirm-first directive state for gate enforcement ──
|
|
995
|
+
if (sessionId) {
|
|
996
|
+
const cfPrinciple = dedupedV2.find(
|
|
997
|
+
(p) =>
|
|
998
|
+
p.principleId === 'princ-mvp-acceptance-confirm-first' ||
|
|
999
|
+
(p.text.toLowerCase().includes('confirm requirements') &&
|
|
1000
|
+
p.text.toLowerCase().includes('owner approval')),
|
|
1001
|
+
);
|
|
1002
|
+
setConfirmFirstDirective(sessionId, !!cfPrinciple, cfPrinciple?.principleId);
|
|
1003
|
+
}
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
logger?.warn?.(`[PD:RuntimeV2] Failed to read Runtime V2 prompt activations: ${String(e)}`);
|
|
1006
|
+
if (sessionId) {
|
|
1007
|
+
resetConfirmFirst(sessionId);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
989
1011
|
// Build appendSystemContext with recency effect
|
|
990
1012
|
// Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
|
|
991
1013
|
const appendParts: string[] = [];
|
|
992
1014
|
|
|
993
1015
|
// 0. Behavioral Constraints (empathy observer coordination)
|
|
994
1016
|
// Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
|
|
995
|
-
//
|
|
1017
|
+
// Behavioral constraints: empathy observer coordination
|
|
996
1018
|
if (shouldInjectBehavioralConstraints) {
|
|
997
1019
|
appendParts.push(`<behavioral_constraints>
|
|
998
1020
|
${empathySilenceConstraint}
|
|
@@ -1014,11 +1036,35 @@ ${empathySilenceConstraint}
|
|
|
1014
1036
|
appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
|
|
1015
1037
|
}
|
|
1016
1038
|
|
|
1017
|
-
// 3. Evolution Loop principles (active/probation)
|
|
1039
|
+
// 3. Evolution Loop principles (legacy active/probation only — Runtime V2 moved to section 3.5)
|
|
1018
1040
|
if (evolutionPrinciplesContent) {
|
|
1019
1041
|
appendParts.push(`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`);
|
|
1020
1042
|
}
|
|
1021
1043
|
|
|
1044
|
+
// 3.5. Owner-Approved Behavior Directives (Runtime V2 activated principles)
|
|
1045
|
+
// PLACED IN prependSystemContext (before gateway system prompt) for highest LLM attention.
|
|
1046
|
+
// These are owner-reviewed, validated behavior constraints — not background context.
|
|
1047
|
+
if (runtimeV2PrincipleIds.size > 0) {
|
|
1048
|
+
const directiveLines: string[] = [];
|
|
1049
|
+
directiveLines.push('');
|
|
1050
|
+
directiveLines.push('## 【OWNER-APPROVED BEHAVIOR DIRECTIVES】');
|
|
1051
|
+
directiveLines.push('');
|
|
1052
|
+
directiveLines.push('Owner-approved behavior directives are active operating constraints learned from prior owner corrections.');
|
|
1053
|
+
directiveLines.push('These directives are mandatory for this session unless they conflict with safety, security, or higher-priority system policy.');
|
|
1054
|
+
directiveLines.push('For ambiguous coding or file-changing tasks, follow these directives before using mutating tools.');
|
|
1055
|
+
directiveLines.push('');
|
|
1056
|
+
for (const p of dedupedV2) {
|
|
1057
|
+
if (!runtimeV2PrincipleIds.has(p.principleId)) continue;
|
|
1058
|
+
directiveLines.push(`<directive id="${escapeXml(p.principleId)}" source="runtime_v2_activation">`);
|
|
1059
|
+
directiveLines.push(`MANDATORY: ${escapeXml(p.text)}`);
|
|
1060
|
+
directiveLines.push('Apply this as an active behavior constraint. Do not treat this as background context.');
|
|
1061
|
+
directiveLines.push('</directive>');
|
|
1062
|
+
directiveLines.push('');
|
|
1063
|
+
}
|
|
1064
|
+
directiveLines.push('Note: These directives do not override safety, security, or core system policy.');
|
|
1065
|
+
prependSystemContext += directiveLines.join('\n');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1022
1068
|
// Routing Guidance (section 5 — injected between evolution principles and core principles)
|
|
1023
1069
|
// Inject delegation guidance when task is bounded + deployment allowed + not high-entropy.
|
|
1024
1070
|
// This is a non-authoritative suggestion — the main agent decides whether to follow.
|
|
@@ -1047,7 +1093,6 @@ ${empathySilenceConstraint}
|
|
|
1047
1093
|
const routingInput: RoutingInput = {
|
|
1048
1094
|
taskIntent: toolMatches[0] ?? undefined,
|
|
1049
1095
|
taskDescription: latestUserText.trim(),
|
|
1050
|
-
requestedTools: toolMatches.length > 0 ? toolMatches : undefined,
|
|
1051
1096
|
requestedFiles: fileMatches.length > 0 ? fileMatches : undefined,
|
|
1052
1097
|
};
|
|
1053
1098
|
|
|
@@ -1158,110 +1203,34 @@ ${attitudeDirective}
|
|
|
1158
1203
|
}
|
|
1159
1204
|
|
|
1160
1205
|
// ──── 8. SIZE GUARD ────
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const truncationLog: string[] = [];
|
|
1172
|
-
|
|
1173
|
-
// Deterministically remove low-priority context blocks in priority order.
|
|
1174
|
-
// In diagnostician-priority mode we aggressively strip everything except
|
|
1175
|
-
// the task block and minimum behavioral constraints.
|
|
1176
|
-
const inDiagMode = pendingDiagTaskCount > 0;
|
|
1177
|
-
|
|
1178
|
-
// Step 1 — strip project_context (largest, lowest priority) — always in diag mode,
|
|
1179
|
-
// only strip in normal mode if we are already over limit
|
|
1180
|
-
if (projectContextContent && appendSystemContext.includes('<project_context>')) {
|
|
1181
|
-
appendSystemContext = appendSystemContext.replace(
|
|
1182
|
-
`<project_context>\n${projectContextContent}\n</project_context>`,
|
|
1183
|
-
'<project_context>\n[stripped: project_context]\n</project_context>'
|
|
1184
|
-
);
|
|
1185
|
-
truncationLog.push('project_context');
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Steps 2-4: only strip in diagnostician priority mode (inDiagMode)
|
|
1189
|
-
// In normal mode we stop after project_context to preserve context quality
|
|
1190
|
-
if (inDiagMode) {
|
|
1191
|
-
// Step 2 — strip thinking_os
|
|
1192
|
-
if (thinkingOsContent && appendSystemContext.includes('<thinking_os>')) {
|
|
1193
|
-
appendSystemContext = appendSystemContext.replace(
|
|
1194
|
-
`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`,
|
|
1195
|
-
'<thinking_os>\n[stripped: thinking_os]\n</thinking_os>'
|
|
1196
|
-
);
|
|
1197
|
-
truncationLog.push('thinking_os');
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// Step 3 — strip evolution_principles (keep core_principles only)
|
|
1201
|
-
if (evolutionPrinciplesContent && appendSystemContext.includes('<evolution_principles>')) {
|
|
1202
|
-
appendSystemContext = appendSystemContext.replace(
|
|
1203
|
-
`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`,
|
|
1204
|
-
'<evolution_principles>\n[stripped: evolution_principles]\n</evolution_principles>'
|
|
1205
|
-
);
|
|
1206
|
-
truncationLog.push('evolution_principles');
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Step 4 — strip reflection_log if present
|
|
1210
|
-
if (appendSystemContext.includes('<reflection_log>')) {
|
|
1211
|
-
appendSystemContext = appendSystemContext.replace(
|
|
1212
|
-
/<reflection_log>[\s\S]*?<\/reflection_log>/,
|
|
1213
|
-
'<reflection_log>\n[stripped: reflection_log]\n</reflection_log>'
|
|
1214
|
-
);
|
|
1215
|
-
truncationLog.push('reflection_log');
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Step 5 — re-evaluate: check if still over limit
|
|
1220
|
-
let newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
|
|
1221
|
-
if (newSize > MAX_INJECTION_SIZE) {
|
|
1222
|
-
// Truncate the injected reason field by finding the "reason:" line prefix
|
|
1223
|
-
// and cutting to 120 chars. This is safe because the full prompt is
|
|
1224
|
-
// still available in diagnostician_tasks.json for the agent to read.
|
|
1225
|
-
prependContext = prependContext
|
|
1226
|
-
.split('\n')
|
|
1227
|
-
.map((line) => {
|
|
1228
|
-
if (line.startsWith('reason: ') && line.length > 129) {
|
|
1229
|
-
return line.slice(0, 129) + '...[truncated]';
|
|
1230
|
-
}
|
|
1231
|
-
return line;
|
|
1232
|
-
})
|
|
1233
|
-
.join('\n');
|
|
1234
|
-
newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
|
|
1235
|
-
truncationLog.push('diagnostician_reason');
|
|
1206
|
+
// Delegates to @principles/core/prompt-builder/truncateInjectionToBudget
|
|
1207
|
+
// which handles priority stripping: project_context → thinking_os →
|
|
1208
|
+
// evolution_principles → reflection_log → reason: truncation → fallback.
|
|
1209
|
+
const result = truncateInjectionToBudget(
|
|
1210
|
+
prependSystemContext,
|
|
1211
|
+
prependContext,
|
|
1212
|
+
appendSystemContext,
|
|
1213
|
+
{
|
|
1214
|
+
diagnosticianMode: pendingDiagTaskCount > 0,
|
|
1215
|
+
blocks: { projectContextContent, thinkingOsContent, evolutionPrinciplesContent },
|
|
1236
1216
|
}
|
|
1217
|
+
);
|
|
1237
1218
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// prependSystemContext) and log a hard error.
|
|
1242
|
-
if (newSize > MAX_INJECTION_SIZE) {
|
|
1243
|
-
const fallbackContext = `
|
|
1244
|
-
## 【CONTEXT SECTIONS】
|
|
1245
|
-
|
|
1246
|
-
[WARNING: Context sections stripped due to prompt size constraints.
|
|
1247
|
-
This is a diagnostician-priority session — see diagnostician_tasks.json for full task context.]
|
|
1248
|
-
|
|
1249
|
-
${attitudeDirective}
|
|
1250
|
-
`.trim();
|
|
1251
|
-
|
|
1252
|
-
appendSystemContext = fallbackContext;
|
|
1253
|
-
newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
|
|
1219
|
+
prependSystemContext = result.prependSystemContext;
|
|
1220
|
+
prependContext = result.prependContext;
|
|
1221
|
+
appendSystemContext = result.appendSystemContext;
|
|
1254
1222
|
|
|
1223
|
+
if (result.truncated) {
|
|
1224
|
+
const logEntry = result.truncationLog.join(', ');
|
|
1225
|
+
if (result.appendSystemContext.includes('[WARNING: Context sections stripped')) {
|
|
1255
1226
|
logger?.error(
|
|
1256
1227
|
`[PD:Prompt] PROMPT OVER LIMIT AFTER ALL REDUCTIONS — using fallback. ` +
|
|
1257
|
-
`
|
|
1258
|
-
`Stripped: ${truncationLog.join(', ')}. Diagnostician mode: ${inDiagMode}.`
|
|
1228
|
+
`Diagnostician mode: ${pendingDiagTaskCount > 0}. Stripped: ${logEntry}.`
|
|
1259
1229
|
);
|
|
1260
1230
|
} else {
|
|
1261
1231
|
logger?.warn(
|
|
1262
|
-
`[PD:Prompt] Injection size exceeded: ${
|
|
1263
|
-
`
|
|
1264
|
-
`diagnostician mode: ${inDiagMode}`
|
|
1232
|
+
`[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}, ` +
|
|
1233
|
+
`diagnostician mode: ${pendingDiagTaskCount > 0}`
|
|
1265
1234
|
);
|
|
1266
1235
|
}
|
|
1267
1236
|
}
|
|
@@ -1272,3 +1241,54 @@ ${attitudeDirective}
|
|
|
1272
1241
|
appendSystemContext
|
|
1273
1242
|
};
|
|
1274
1243
|
}
|
|
1244
|
+
|
|
1245
|
+
// ── Empathy Observer Hybrid Deep Analysis Helpers (Unified SDK Migration) ──
|
|
1246
|
+
|
|
1247
|
+
function scoreFromSeverityForSpec(severity: string | undefined, wctx: WorkspaceContext): number {
|
|
1248
|
+
if (severity === 'severe') return Number(wctx.config.get('empathy_engine.penalties.severe') ?? 40);
|
|
1249
|
+
if (severity === 'moderate') return Number(wctx.config.get('empathy_engine.penalties.moderate') ?? 25);
|
|
1250
|
+
return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function resolveEmpathyObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): EmpathyObserver | null {
|
|
1254
|
+
try {
|
|
1255
|
+
const loader = new WorkflowFunnelLoader(wctx.stateDir);
|
|
1256
|
+
const funnel = loader.getFunnel('pd-empathy-observer');
|
|
1257
|
+
const policy = funnel?.policy;
|
|
1258
|
+
if (!policy || policy.runtimeKind !== 'pi-ai') {
|
|
1259
|
+
logger?.debug?.('[PD:Empathy] workflows.yaml pd-empathy-observer policy not found. Falling back to environment variables.');
|
|
1260
|
+
const provider = process.env.PD_EMPATHY_PROVIDER || 'anthropic';
|
|
1261
|
+
const model = process.env.PD_EMPATHY_MODEL || 'anthropic/claude-3-5-sonnet';
|
|
1262
|
+
const apiKeyEnv = process.env.PD_EMPATHY_API_KEY_ENV || 'ANTHROPIC_API_KEY';
|
|
1263
|
+
const baseUrl = process.env.PD_EMPATHY_BASE_URL;
|
|
1264
|
+
|
|
1265
|
+
if (!process.env[apiKeyEnv]) {
|
|
1266
|
+
logger?.debug?.(`[PD:Empathy] Empathy observer API key env ${apiKeyEnv} is not set. Background analysis disabled.`);
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const adapter = new PiAiRuntimeAdapter({
|
|
1271
|
+
provider,
|
|
1272
|
+
model,
|
|
1273
|
+
apiKeyEnv,
|
|
1274
|
+
baseUrl,
|
|
1275
|
+
workspace: wctx.workspaceDir,
|
|
1276
|
+
});
|
|
1277
|
+
return new EmpathyObserver({ runtimeAdapter: adapter });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const adapter = new PiAiRuntimeAdapter({
|
|
1281
|
+
provider: String(policy.provider),
|
|
1282
|
+
model: String(policy.model),
|
|
1283
|
+
apiKeyEnv: String(policy.apiKeyEnv),
|
|
1284
|
+
maxRetries: policy.maxRetries,
|
|
1285
|
+
timeoutMs: policy.timeoutMs ?? 30_000,
|
|
1286
|
+
baseUrl: policy.baseUrl,
|
|
1287
|
+
workspace: wctx.workspaceDir,
|
|
1288
|
+
});
|
|
1289
|
+
return new EmpathyObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
logger?.warn?.(`[PD:Empathy] Failed to resolve EmpathyObserver: ${String(err)}`);
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
}
|