principles-disciple 1.72.0 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +459 -439
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +59 -16
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +3 -1
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
package/src/hooks/gate.ts CHANGED
@@ -15,9 +15,11 @@ import { normalizePath, planStatus } from '../utils/io.js';
15
15
  import { WorkspaceContext } from '../core/workspace-context.js';
16
16
  import { recordGateBlockAndReturn } from './gate-block-helper.js';
17
17
  import { RuleHost } from '../core/rule-host.js';
18
- import type { RuleHostInput } from '../core/rule-host-types.js';
18
+ import type { RuleHostInput } from '@principles/core/runtime-v2';
19
+ import { validateCorrectionProposal, validateProposedPathBounds } from '@principles/core/runtime-v2';
19
20
  import type { PluginHookBeforeToolCallEvent, PluginHookToolContext, PluginHookBeforeToolCallResult, PluginLogger } from '../openclaw-sdk.js';
20
21
  import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js';
22
+ import { evaluateConfirmFirstGateSync } from '../core/confirm-first-gate.js';
21
23
  import { getSession, hasRecentThinking } from '../core/session-tracker.js';
22
24
  import { getEvolutionEngine } from '../core/evolution-engine.js';
23
25
  import { EventLogService } from '../core/event-log.js';
@@ -40,12 +42,48 @@ export function handleBeforeToolCall(
40
42
 
41
43
  const wctx = WorkspaceContext.fromHookContext(ctx);
42
44
 
45
+ // 1.5. Confirm-First Gate — runs BEFORE filePath resolution to catch apply_patch/no-path cases
46
+ try {
47
+ const cfResult = evaluateConfirmFirstGateSync(
48
+ ctx.sessionId,
49
+ event.toolName,
50
+ event.params,
51
+ );
52
+
53
+ if (cfResult.action === 'block') {
54
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
55
+ eventLog.recordConfirmFirstGateBlocked({
56
+ sessionId: ctx.sessionId ?? 'unknown',
57
+ workspaceDir: ctx.workspaceDir,
58
+ toolName: event.toolName,
59
+ reason: cfResult.reason ?? 'confirm_first_required',
60
+ principleId: cfResult.principleId ?? 'unknown',
61
+ nextAction: cfResult.nextAction ?? '',
62
+ });
63
+
64
+ // Use safe placeholder when filePath is unavailable (e.g., apply_patch with no path)
65
+ const safePath = (event.params?.file_path || event.params?.path || event.params?.file || event.params?.target)
66
+ ?? `<tool:${event.toolName}>`;
67
+
68
+ return recordGateBlockAndReturn(wctx, {
69
+ filePath: typeof safePath === 'string' ? safePath : `<tool:${event.toolName}>`,
70
+ reason: cfResult.reason ?? 'confirm_first_required',
71
+ toolName: event.toolName,
72
+ sessionId: ctx.sessionId,
73
+ blockSource: 'confirm-first-gate',
74
+ }, logger);
75
+ }
76
+ } catch (cfErr) {
77
+ // ERR-002: fail loud — log but do not crash the gate
78
+ logger?.warn?.(`[PD:ConfirmFirst] Gate evaluation failed (non-blocking): ${String(cfErr)}`);
79
+ }
80
+
43
81
  // 2. Resolve the target file path
44
- let filePath = event.params.file_path || event.params.path || event.params.file || event.params.target;
82
+ let filePath = event.params?.file_path || event.params?.path || event.params?.file || event.params?.target;
45
83
 
46
84
  // Heuristic for bash mutation detection
47
85
  if (isBash && !filePath) {
48
- const command = String(event.params.command || event.params.args || '');
86
+ const command = String(event.params?.command || event.params?.args || '');
49
87
  const mutationMatch = /(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/.exec(command);
50
88
 
51
89
  if (mutationMatch) {
@@ -67,7 +105,7 @@ export function handleBeforeToolCall(
67
105
  action: {
68
106
  toolName: event.toolName,
69
107
  normalizedPath: relPath,
70
- paramsSummary: _extractParamsSummary(event.params),
108
+ paramsSummary: _extractParamsSummary(event.params ?? {}),
71
109
  },
72
110
  workspace: {
73
111
  isRiskPath: false, // Rule Host determines risk dynamically
@@ -83,7 +121,7 @@ export function handleBeforeToolCall(
83
121
  epTier: _getEpTier(wctx.workspaceDir),
84
122
  },
85
123
  derived: {
86
- estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
124
+ estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params ?? {} }),
87
125
  bashRisk: _getBashRisk(event),
88
126
  },
89
127
  };
@@ -153,8 +191,170 @@ export function handleBeforeToolCall(
153
191
  logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval: ${String(evErr)}`);
154
192
  }
155
193
  }
194
+
195
+ if (hostResult?.decision === 'auto_correct' && hostResult.correctionProposal) {
196
+ const proposal = hostResult.correctionProposal;
197
+ let validation: { valid: boolean; errors: string[] };
198
+ try {
199
+ validation = validateCorrectionProposal(proposal);
200
+ } catch (validationError: unknown) {
201
+ validation = { valid: false, errors: [`Validator threw: ${String(validationError)}`] };
202
+ }
203
+
204
+ try {
205
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
206
+ const correctedFields = Array.isArray(proposal.correctedFields)
207
+ ? proposal.correctedFields.map((f: unknown) => typeof f === 'object' && f !== null ? String((f as { field?: string }).field) : String(f))
208
+ : [];
209
+ eventLog.recordRuleHostAutoCorrectProposed({
210
+ toolName: event.toolName,
211
+ filePath: relPath,
212
+ ruleId: String(proposal.ruleId ?? 'unknown'),
213
+ principleId: proposal.principleId != null ? String(proposal.principleId) : undefined,
214
+ confidence: typeof proposal.confidence === 'number' ? proposal.confidence : 0,
215
+ reason: hostResult.reason,
216
+ applicationMode: proposal.applicationMode === 'live' ? 'live' : 'shadow',
217
+ correctedFields,
218
+ validationValid: validation.valid,
219
+ });
220
+ } catch (evErr) {
221
+ logger?.warn?.(`[PD_GATE] Failed to record rulehost_auto_correct_proposed: ${String(evErr)}`);
222
+ }
223
+
224
+ if (proposal.applicationMode === 'live' && validation.valid) {
225
+ if (!event.params) {
226
+ return;
227
+ }
228
+ const originalParams = { ...event.params };
229
+ const nextParams: Record<string, unknown> = {};
230
+ const appliedFields: Array<{ field: string; original: unknown; applied: unknown }> = [];
231
+
232
+ try {
233
+ if (!Array.isArray(proposal.correctedFields)) {
234
+ throw new Error('proposal.correctedFields is not an array');
235
+ }
236
+
237
+ if (!proposal.proposedParams || typeof proposal.proposedParams !== 'object' || Array.isArray(proposal.proposedParams)) {
238
+ throw new Error('proposal.proposedParams must be an object');
239
+ }
240
+
241
+ const trustedWorkspaceDir = ctx.workspaceDir;
242
+ if (typeof trustedWorkspaceDir === 'string' && trustedWorkspaceDir.trim().length > 0) {
243
+ const pathBoundsResult = validateProposedPathBounds(proposal.proposedParams, trustedWorkspaceDir);
244
+ if (!pathBoundsResult.valid) {
245
+ throw new Error(`Path boundary violation: ${pathBoundsResult.reason}`);
246
+ }
247
+ } else {
248
+ const hasPathField = Object.keys(proposal.proposedParams).some(k => typeof proposal.proposedParams[k] === 'string' && (k === 'file_path' || k === 'path' || k === 'filePath'));
249
+ if (hasPathField) {
250
+ throw new Error('Cannot apply live auto-correction with path fields: no trusted workspace directory available');
251
+ }
252
+ }
253
+
254
+ for (const cf of proposal.correctedFields) {
255
+ if (typeof cf !== 'object' || cf === null || typeof cf.field !== 'string') {
256
+ throw new Error('correctedFields entry must be an object with a string field');
257
+ }
258
+ const field = cf.field;
259
+ if (!Object.hasOwn(event.params, field)) {
260
+ throw new Error(`Field '${field}' not found in event.params`);
261
+ }
262
+ if (!Object.hasOwn(proposal.proposedParams, field)) {
263
+ throw new Error(`Field '${field}' not found in proposal.proposedParams`);
264
+ }
265
+ }
266
+
267
+ for (const cf of proposal.correctedFields) {
268
+ if (typeof cf === 'object' && cf !== null && typeof cf.field === 'string') {
269
+ const field = cf.field;
270
+ const originalValue = event.params[field];
271
+ const appliedValue = proposal.proposedParams[field];
272
+ nextParams[field] = appliedValue;
273
+ appliedFields.push({
274
+ field,
275
+ original: originalValue,
276
+ applied: appliedValue,
277
+ });
278
+ }
279
+ }
280
+
281
+ Object.assign(event.params, nextParams);
282
+
283
+ try {
284
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
285
+ eventLog.recordRuleHostAutoCorrectApplied({
286
+ toolName: event.toolName,
287
+ filePath: relPath,
288
+ ruleId: String(proposal.ruleId ?? 'unknown'),
289
+ principleId: proposal.principleId != null ? String(proposal.principleId) : undefined,
290
+ confidence: typeof proposal.confidence === 'number' ? proposal.confidence : 0,
291
+ reason: hostResult.reason || proposal.correctedFields?.[0]?.reason || 'auto-correct applied',
292
+ correctedFields: appliedFields,
293
+ });
294
+ } catch (evErr) {
295
+ logger?.warn?.(`[PD_GATE] Failed to record rulehost_auto_correct_applied: ${String(evErr)}`);
296
+ }
297
+
298
+ if (proposal.notifyAgent === true && appliedFields.length > 0) {
299
+ const messages = appliedFields.map(f =>
300
+ `[PD Auto-Correct] Rule ${proposal.ruleId}: ${proposal.correctedFields?.[0]?.reason || 'correction applied'}. Parameter '${f.field}' was adjusted from ${JSON.stringify(f.original)} to ${JSON.stringify(f.applied)}.`
301
+ );
302
+ return {
303
+ toolArgs: event.toolArgs,
304
+ skipToolCall: false,
305
+ _pdAutoCorrectWarning: messages.join('\n'),
306
+ };
307
+ }
308
+ } catch (applyError: unknown) {
309
+ if (event.params) {
310
+ Object.assign(event.params, originalParams);
311
+ }
312
+ const errorMsg = String(applyError);
313
+ const isPathViolation = errorMsg.includes('Path boundary violation') || errorMsg.includes('no trusted workspace directory');
314
+ if (isPathViolation) {
315
+ logger?.warn?.(`[PD_GATE] Live auto-correction rejected — path out of bounds: ${errorMsg}`);
316
+ try {
317
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
318
+ eventLog.recordRuleHostAutoCorrectProposed({
319
+ toolName: event.toolName,
320
+ filePath: relPath,
321
+ ruleId: String(proposal.ruleId ?? 'unknown'),
322
+ principleId: proposal.principleId != null ? String(proposal.principleId) : undefined,
323
+ confidence: typeof proposal.confidence === 'number' ? proposal.confidence : 0,
324
+ reason: `Path boundary rejected: ${errorMsg}`,
325
+ applicationMode: 'shadow',
326
+ correctedFields: Array.isArray(proposal.correctedFields)
327
+ ? proposal.correctedFields.map((f: unknown) => typeof f === 'object' && f !== null ? String((f as { field?: string }).field) : String(f))
328
+ : [],
329
+ validationValid: false,
330
+ });
331
+ } catch (evErr) {
332
+ logger?.warn?.(`[PD_GATE] Failed to record path rejection telemetry: ${String(evErr)}`);
333
+ }
334
+ } else {
335
+ logger?.warn?.(`[PD_GATE] Failed to apply auto-correction, using original params: ${errorMsg}`);
336
+ }
337
+ }
338
+ }
339
+ } else if (hostResult?.decision === 'auto_correct') {
340
+ // auto_correct without correctionProposal — emit telemetry for observability
341
+ try {
342
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
343
+ eventLog.recordRuleHostAutoCorrectProposed({
344
+ toolName: event.toolName,
345
+ filePath: relPath,
346
+ ruleId: hostResult.ruleId ?? 'unknown',
347
+ confidence: 0,
348
+ reason: hostResult.reason ?? 'auto_correct without correctionProposal',
349
+ applicationMode: 'shadow',
350
+ correctedFields: [],
351
+ validationValid: false,
352
+ });
353
+ } catch (evErr) {
354
+ logger?.warn?.(`[PD_GATE] Failed to record rulehost_auto_correct_proposed (no proposal): ${String(evErr)}`);
355
+ }
356
+ }
156
357
  } catch (hostError: unknown) {
157
- // D-08: Conservative degradation — log and allow on Rule Host failure
158
358
  logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, allowing conservatively: ${String(hostError)}`);
159
359
  }
160
360
 
@@ -227,7 +427,7 @@ function _getEpTier(workspaceDir: string): number {
227
427
  function _getBashRisk(event: PluginHookBeforeToolCallEvent): 'safe' | 'normal' | 'dangerous' | 'unknown' {
228
428
  if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
229
429
  try {
230
- const command = String(event.params.command || event.params.args || '');
430
+ const command = String(event.params?.command || event.params?.args || '');
231
431
  const isDangerous = /\brm\s+-rf\b|\bchmod\b|\bchown\b|>\s*\/dev\//.test(command);
232
432
  if (isDangerous) return 'dangerous';
233
433
  const isMutation = /(?:>|>>|sed|rm|mv|mkdir|touch|cp|npm|yarn|pnpm|pip|cargo)/.test(command);
@@ -1,7 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as readline from 'readline';
4
- import { recordAndWritePainFlag } from '../core/pain.js';
5
4
  import { atomicWriteFileSync } from '../utils/io.js';
6
5
  import { WorkspaceContext } from '../core/workspace-context.js';
7
6
  import { PD_DIRS } from '../core/paths.js';
@@ -10,6 +9,7 @@ import {
10
9
  mergeWorkingMemory,
11
10
  } from '../core/focus-history.js';
12
11
  import type { PluginHookBeforeResetEvent, PluginHookBeforeCompactionEvent, PluginHookAfterCompactionEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
12
+ import { emitPainDetectedEvent } from './pain.js';
13
13
 
14
14
  export async function handleBeforeReset(
15
15
  event: PluginHookBeforeResetEvent,
@@ -66,7 +66,7 @@ export async function extractPainFromSessionFile(sessionFile: string, ctx: Plugi
66
66
  return;
67
67
  }
68
68
 
69
- if (ctx.logger) ctx.logger.info(`[Pain Extractor] Scanning session transcript for pain signals: ${sessionFile}`);
69
+ ctx.logger?.info?.(`[Pain Extractor] Scanning session transcript for pain signals: ${sessionFile}`);
70
70
 
71
71
  const fileStream = fs.createReadStream(sessionFile);
72
72
  const rl = readline.createInterface({
@@ -97,13 +97,13 @@ export async function extractPainFromSessionFile(sessionFile: string, ctx: Plugi
97
97
 
98
98
  if (msg.openclawAbort?.aborted) {
99
99
  const runIdSafe = msg.openclawAbort?.runId || 'unknown';
100
- if (ctx.logger) ctx.logger.info(`[Pain Extractor] Detected hard-abort snapshot (runId: ${runIdSafe})`);
100
+ ctx.logger?.info?.(`[Pain Extractor] Detected hard-abort snapshot (runId: ${runIdSafe})`);
101
101
  painPoints.push(`[FATAL INTERCEPT] 动作被沙箱防御机制强制击落。大模型被击落前的思考流 (未遂动机): ${text.substring(0, 250)}...`);
102
102
  continue;
103
103
  }
104
104
 
105
105
  if (msg.__openclaw?.truncated && msg.__openclaw?.reason === 'oversized') {
106
- if (ctx.logger) ctx.logger.info(`[Pain Extractor] Detected oversized data truncation placeholder`);
106
+ ctx.logger?.info?.(`[Pain Extractor] Detected oversized data truncation placeholder`);
107
107
  painPoints.push(`[COGNITIVE OVERLOAD] 大模型尝试读取极大体积的输入,已被底层守护程序抹除/折叠防爆。请反思是否读取了不当的文件或日志: ${text.substring(0, 150)}...`);
108
108
  continue;
109
109
  }
@@ -154,21 +154,19 @@ export async function extractPainFromSessionFile(sessionFile: string, ctx: Plugi
154
154
 
155
155
  const hasFatal = painPoints.some(p => p.includes('[FATAL INTERCEPT]'));
156
156
  if (hasFatal) {
157
- recordAndWritePainFlag(wctx, {
158
- sessionId: ctx.sessionId || 'unknown',
159
- source: 'intercept_extraction',
160
- score: 100,
161
- reason: 'Hard intercept detected in session history compaction.',
162
- severity: 'severe',
163
- origin: 'system_infer',
164
- }, {
165
- source: 'intercept_extraction',
166
- score: '100',
167
- reason: 'Hard intercept detected in session history compaction.',
168
- is_risky: true,
169
- trigger_text_preview: painPoints.find(p => p.includes('[FATAL INTERCEPT]'))?.substring(0, 150) || 'Fatal intercept',
170
- session_id: ctx.sessionId || '',
171
- agent_id: ctx.agentId || '',
157
+ // Emit via the Runtime v2 pain chain — no .pain_flag file written
158
+ emitPainDetectedEvent(wctx, {
159
+ ts: new Date().toISOString(),
160
+ type: 'pain_detected',
161
+ data: {
162
+ painId: `intercept_${Date.now()}`,
163
+ painType: 'tool_failure' as const,
164
+ source: 'intercept_extraction',
165
+ reason: 'Hard intercept detected in session history compaction.',
166
+ score: 100,
167
+ sessionId: ctx.sessionId || 'unknown',
168
+ agentId: ctx.agentId,
169
+ },
172
170
  });
173
171
  }
174
172
  } catch (err) {
@@ -200,13 +198,14 @@ export async function handleBeforeCompaction(
200
198
  }
201
199
 
202
200
  // 提取工作记忆(从 sessionFile)
203
- if (event.sessionFile) {
204
- await extractPainFromSessionFile(event.sessionFile, ctx);
201
+ const sessionFile = (event as { sessionFile?: string }).sessionFile;
202
+ if (sessionFile) {
203
+ await extractPainFromSessionFile(sessionFile, ctx);
205
204
 
206
205
  // 新增:提取并保存工作记忆
207
-
208
-
209
- await extractAndSaveWorkingMemory(event.sessionFile, ctx, wctx);
206
+
207
+
208
+ await extractAndSaveWorkingMemory(sessionFile, ctx, wctx);
210
209
  }
211
210
  }
212
211
 
@@ -296,20 +295,18 @@ async function extractAndSaveWorkingMemory(
296
295
  // 写入文件
297
296
  atomicWriteFileSync(focusPath, updatedContent);
298
297
 
299
- if (ctx.logger) {
300
- ctx.logger.info(`[WorkingMemory] Preserved ${snapshot.artifacts.length} artifacts, ` +
301
- `${snapshot.activeProblems.length} problems, ` +
302
- `${snapshot.nextActions.length} next actions to CURRENT_FOCUS.md`);
303
- }
298
+ ctx.logger?.info?.(`[WorkingMemory] Preserved ${snapshot.artifacts.length} artifacts, ` +
299
+ `${snapshot.activeProblems.length} problems, ` +
300
+ `${snapshot.nextActions.length} next actions to CURRENT_FOCUS.md`);
304
301
  } catch (err) {
305
302
  ctx.logger?.error?.(`[PD:Lifecycle] Failed to save working memory: ${String(err)}`);
306
-
303
+
307
304
  // 尝试恢复备份
308
305
  const backupPath = `${focusPath}.wm-backup`;
309
306
  if (fs.existsSync(backupPath)) {
310
307
  try {
311
308
  fs.copyFileSync(backupPath, focusPath);
312
- if (ctx.logger) ctx.logger.warn(`[WorkingMemory] Restored from backup after failure`);
309
+ ctx.logger?.warn?.(`[WorkingMemory] Restored from backup after failure`);
313
310
  } catch {
314
311
  // 忽略恢复错误
315
312
  }
@@ -325,8 +322,9 @@ export async function handleAfterCompaction(
325
322
 
326
323
  const [dateStrPost] = new Date().toISOString().split('T');
327
324
  const checkpointPath = path.join(ctx.workspaceDir, PD_DIRS.MEMORY, `${dateStrPost}.md`);
325
+ const messageCount = (event as { messageCount?: number }).messageCount ?? 0;
328
326
  const log =
329
- `- Post-Compaction Complete. Reduced active context to ${event.messageCount} messages.\n`;
327
+ `- Post-Compaction Complete. Reduced active context to ${messageCount} messages.\n`;
330
328
 
331
329
  try {
332
330
  fs.appendFileSync(checkpointPath, log, 'utf8');
package/src/hooks/llm.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import type { PluginHookLlmOutputEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
3
+ import type { PluginHookLlmOutputEvent, PluginHookAgentContext, TokenUsage } from '../openclaw-sdk.js';
4
4
  import { trackLlmOutput, recordThinkingCheckpoint, resetFriction } from '../core/session-tracker.js';
5
- import { recordAndWritePainFlag } from '../core/pain.js';
6
5
  import { normalizeSeverity } from '../core/empathy-types.js';
7
6
  import { ControlUiDatabase } from '../core/control-ui-db.js';
8
7
  import { DetectionService } from '../core/detection-service.js';
@@ -10,6 +9,8 @@ import { detectThinkingModelMatches, deriveThinkingScenarios } from '../core/thi
10
9
  import { WorkspaceContext } from '../core/workspace-context.js';
11
10
  import { sanitizeAssistantText } from './message-sanitize.js';
12
11
  import { atomicWriteFileSync } from '../utils/io.js';
12
+ import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
13
+ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
13
14
 
14
15
  export interface EmpathySignal {
15
16
  detected: boolean;
@@ -148,7 +149,12 @@ export function handleLlmOutput(
148
149
  const {eventLog} = wctx;
149
150
 
150
151
  // Track this turn in the core session memory
151
- const state = trackLlmOutput(ctx.sessionId, event.usage, config, ctx.workspaceDir, ctx.sessionKey, ctx.trigger);
152
+ const trigger = (event as { trigger?: string }).trigger ?? undefined;
153
+ const usage = event.usage as TokenUsage | undefined;
154
+ const sessionId = (ctx as { sessionId?: string }).sessionId ?? 'unknown';
155
+ const workspaceDir = (ctx as { workspaceDir?: string }).workspaceDir;
156
+ const sessionKey = (ctx as { sessionKey?: string }).sessionKey ?? 'unknown';
157
+ const state = trackLlmOutput(sessionId, usage, config, workspaceDir, sessionKey, trigger);
152
158
 
153
159
  // We need actual assistant text to analyze
154
160
  if (!event.assistantTexts || event.assistantTexts.length === 0) return;
@@ -160,9 +166,9 @@ export function handleLlmOutput(
160
166
  try {
161
167
  assistantTurnId = wctx.trajectory?.recordAssistantTurn?.({
162
168
  sessionId: ctx.sessionId,
163
- runId: event.runId,
164
- provider: event.provider,
165
- model: event.model,
169
+ runId: event.runId ?? 'unknown',
170
+ provider: event.provider ?? 'unknown',
171
+ model: event.model ?? 'unknown',
166
172
  rawText: text,
167
173
  sanitizedText: sanitizeAssistantText(text),
168
174
  usageJson: event.usage || {},
@@ -227,32 +233,33 @@ export function handleLlmOutput(
227
233
  matchedReason = `Agent is stuck in low-output loops (${state.stuckLoops} consecutive turns with tiny output but huge context), indicating cognitive paralysis.`;
228
234
  }
229
235
 
230
- // If a pain threshold is crossed, write the autonomous pain flag
236
+ // If a semantic pain threshold is crossed, only valuable episodes enter Runtime v2.
237
+ // Lower-signal detections remain in the event log/GFI layer for accumulation.
231
238
  const painTriggerThreshold = config.get('thresholds.pain_trigger') || 30;
232
- if (painScore >= painTriggerThreshold) {
233
- // Inject the actual text snippet that triggered this for the diagnostician to read later
234
- const snippet = text.length > 200 ? text.substring(0, 100) + '...' + text.substring(text.length - 100) : text;
235
239
 
236
- try {
237
- recordAndWritePainFlag(wctx, {
238
- sessionId: ctx.sessionId || 'unknown',
239
- source,
240
- score: painScore,
241
- reason: matchedReason,
242
- severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
243
- origin: 'system_infer',
244
- }, {
245
- source,
246
- score: String(painScore),
247
- reason: matchedReason,
248
- is_risky: false,
249
- trigger_text_preview: snippet,
250
- session_id: ctx.sessionId || '',
251
- agent_id: ctx.agentId || '',
252
- });
253
- } catch (e) {
254
- ctx.logger?.warn?.(`[PD:LLM] Failed to write pain flag: ${String(e)}`);
255
- }
240
+ // GFI-triggered pain: when accumulated friction crosses highGfi threshold,
241
+ // emit pain signal even if L1 detection didn't fire.
242
+ const highGfiThreshold = Math.max(config.get('severity_thresholds.high') || 70, painTriggerThreshold + 30);
243
+ if (state.currentGfi >= highGfiThreshold && painScore < painTriggerThreshold) {
244
+ painScore = Math.min(state.currentGfi, 60);
245
+ source = 'user_empathy';
246
+ matchedReason = `Accumulated GFI (${state.currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Source: empathy keyword friction.`;
247
+ }
248
+
249
+ if (painScore >= painTriggerThreshold) {
250
+ const gate = evaluatePainDiagnosticGate({
251
+ source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
252
+ score: painScore,
253
+ currentGfi: state.currentGfi,
254
+ consecutiveErrors: state.consecutiveErrors,
255
+ sessionId: ctx.sessionId || 'unknown',
256
+ errorHash: source,
257
+ thresholds: {
258
+ painTrigger: painTriggerThreshold,
259
+ highSeverity: config.get('severity_thresholds.high') || 70,
260
+ semanticPain: Math.max(painTriggerThreshold, 60),
261
+ },
262
+ });
256
263
 
257
264
  eventLog.recordPainSignal(ctx.sessionId, {
258
265
  score: painScore,
@@ -260,6 +267,27 @@ export function handleLlmOutput(
260
267
  reason: matchedReason,
261
268
  isRisky: false
262
269
  });
270
+
271
+ if (gate.shouldDiagnose) {
272
+ const evidence = buildTrajectoryEvidence(wctx, ctx.sessionId || 'unknown');
273
+ emitPainDetectedEvent(wctx, {
274
+ ts: new Date().toISOString(),
275
+ type: 'pain_detected',
276
+ data: {
277
+ painId: `llm_${Date.now()}`,
278
+ painType: 'user_frustration' as const,
279
+ source,
280
+ reason: `${matchedReason}; diagnosticGate=${gate.reason}`,
281
+ score: painScore,
282
+ sessionId: ctx.sessionId || 'unknown',
283
+ agentId: ctx.agentId,
284
+ provenance: 'openclaw_context_bound',
285
+ evidence,
286
+ },
287
+ });
288
+ } else {
289
+ ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
290
+ }
263
291
  }
264
292
 
265
293
  // ═══ Thinking OS: Mental Model Usage Tracking ═══
@@ -268,8 +296,8 @@ export function handleLlmOutput(
268
296
  trackThinkingModelUsage({
269
297
  text,
270
298
  wctx,
271
- sessionId: ctx.sessionId,
272
- runId: event.runId,
299
+ sessionId: ctx.sessionId ?? 'unknown',
300
+ runId: event.runId ?? 'unknown',
273
301
  assistantTurnId,
274
302
  createdAt,
275
303
  logger: ctx.logger,