@alan512/experienceengine 0.1.3 → 0.2.1

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 (206) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +213 -130
  3. package/README.zh-CN.md +250 -119
  4. package/dist/adapters/claude-code/session-store.d.ts +1 -0
  5. package/dist/adapters/claude-code/session-store.js +24 -1
  6. package/dist/adapters/claude-code/session-store.js.map +1 -1
  7. package/dist/adapters/codex/action-registry.d.ts +84 -0
  8. package/dist/adapters/codex/action-registry.js +277 -0
  9. package/dist/adapters/codex/action-registry.js.map +1 -0
  10. package/dist/adapters/codex/broker-tools.d.ts +114 -0
  11. package/dist/adapters/codex/broker-tools.js +130 -0
  12. package/dist/adapters/codex/broker-tools.js.map +1 -0
  13. package/dist/adapters/codex/mcp-server.d.ts +21 -0
  14. package/dist/adapters/codex/mcp-server.js +103 -423
  15. package/dist/adapters/codex/mcp-server.js.map +1 -1
  16. package/dist/analyzer/candidate-signals.d.ts +3 -1
  17. package/dist/analyzer/candidate-signals.js +159 -0
  18. package/dist/analyzer/candidate-signals.js.map +1 -1
  19. package/dist/analyzer/llm-learning-gate.d.ts +12 -1
  20. package/dist/analyzer/llm-learning-gate.js +633 -16
  21. package/dist/analyzer/llm-learning-gate.js.map +1 -1
  22. package/dist/cli/commands/claude-hook.js +11 -4
  23. package/dist/cli/commands/claude-hook.js.map +1 -1
  24. package/dist/cli/commands/codex.d.ts +60 -0
  25. package/dist/cli/commands/codex.js +188 -0
  26. package/dist/cli/commands/codex.js.map +1 -0
  27. package/dist/cli/commands/doctor.js +35 -2
  28. package/dist/cli/commands/doctor.js.map +1 -1
  29. package/dist/cli/commands/evaluate.d.ts +9 -3
  30. package/dist/cli/commands/evaluate.js +31 -5
  31. package/dist/cli/commands/evaluate.js.map +1 -1
  32. package/dist/cli/commands/init.js +21 -8
  33. package/dist/cli/commands/init.js.map +1 -1
  34. package/dist/cli/commands/inspect.js +73 -4
  35. package/dist/cli/commands/inspect.js.map +1 -1
  36. package/dist/cli/commands/repair.js +3 -3
  37. package/dist/cli/commands/repair.js.map +1 -1
  38. package/dist/cli/commands/status.js +38 -0
  39. package/dist/cli/commands/status.js.map +1 -1
  40. package/dist/cli/dispatch.js +16 -4
  41. package/dist/cli/dispatch.js.map +1 -1
  42. package/dist/config/config-schema.d.ts +177 -0
  43. package/dist/config/config-schema.js +142 -1
  44. package/dist/config/config-schema.js.map +1 -1
  45. package/dist/config/default-config.js +19 -1
  46. package/dist/config/default-config.js.map +1 -1
  47. package/dist/config/load-config.js +72 -1
  48. package/dist/config/load-config.js.map +1 -1
  49. package/dist/config/settings-store.d.ts +19 -0
  50. package/dist/config/settings-store.js +11 -0
  51. package/dist/config/settings-store.js.map +1 -1
  52. package/dist/controller/candidate-retriever.d.ts +16 -1
  53. package/dist/controller/candidate-retriever.js +199 -137
  54. package/dist/controller/candidate-retriever.js.map +1 -1
  55. package/dist/controller/injection-scorecard.d.ts +2 -14
  56. package/dist/controller/injection-scorecard.js +29 -0
  57. package/dist/controller/injection-scorecard.js.map +1 -1
  58. package/dist/controller/intervention-controller.d.ts +3 -15
  59. package/dist/controller/intervention-controller.js +219 -57
  60. package/dist/controller/intervention-controller.js.map +1 -1
  61. package/dist/controller/policy-enricher.d.ts +10 -0
  62. package/dist/controller/policy-enricher.js +186 -0
  63. package/dist/controller/policy-enricher.js.map +1 -0
  64. package/dist/controller/retrieval-context.d.ts +3 -0
  65. package/dist/controller/retrieval-context.js +37 -0
  66. package/dist/controller/retrieval-context.js.map +1 -0
  67. package/dist/controller/second-opinion-gate.d.ts +41 -0
  68. package/dist/controller/second-opinion-gate.js +225 -0
  69. package/dist/controller/second-opinion-gate.js.map +1 -0
  70. package/dist/controller/trigger-evaluator.d.ts +6 -1
  71. package/dist/controller/trigger-evaluator.js +31 -1
  72. package/dist/controller/trigger-evaluator.js.map +1 -1
  73. package/dist/distillation/prompt-contract.d.ts +1 -1
  74. package/dist/distillation/prompt-contract.js +3 -1
  75. package/dist/distillation/prompt-contract.js.map +1 -1
  76. package/dist/distillation/providers/gemini.js +5 -1
  77. package/dist/distillation/providers/gemini.js.map +1 -1
  78. package/dist/distillation/queue-worker.js +22 -3
  79. package/dist/distillation/queue-worker.js.map +1 -1
  80. package/dist/evaluation/codex-lifecycle-validation.d.ts +60 -0
  81. package/dist/evaluation/codex-lifecycle-validation.js +233 -0
  82. package/dist/evaluation/codex-lifecycle-validation.js.map +1 -0
  83. package/dist/evaluation/hybrid-phase1-rollout-summary.d.ts +63 -0
  84. package/dist/evaluation/hybrid-phase1-rollout-summary.js +108 -0
  85. package/dist/evaluation/hybrid-phase1-rollout-summary.js.map +1 -0
  86. package/dist/evaluation/hybrid-phase3-gate-metrics.d.ts +26 -0
  87. package/dist/evaluation/hybrid-phase3-gate-metrics.js +23 -0
  88. package/dist/evaluation/hybrid-phase3-gate-metrics.js.map +1 -0
  89. package/dist/evaluation/openclaw-baseline.d.ts +8 -0
  90. package/dist/evaluation/openclaw-baseline.js +27 -0
  91. package/dist/evaluation/openclaw-baseline.js.map +1 -1
  92. package/dist/experience-management/governance-observability.d.ts +13 -0
  93. package/dist/experience-management/governance-observability.js +37 -0
  94. package/dist/experience-management/governance-observability.js.map +1 -0
  95. package/dist/experience-management/node-lifecycle-governance.d.ts +8 -0
  96. package/dist/experience-management/node-lifecycle-governance.js +80 -0
  97. package/dist/experience-management/node-lifecycle-governance.js.map +1 -0
  98. package/dist/experience-management/task-management-signals.d.ts +29 -0
  99. package/dist/experience-management/task-management-signals.js +148 -0
  100. package/dist/experience-management/task-management-signals.js.map +1 -0
  101. package/dist/feedback/feedback-manager.d.ts +4 -1
  102. package/dist/feedback/feedback-manager.js +11 -22
  103. package/dist/feedback/feedback-manager.js.map +1 -1
  104. package/dist/feedback/state-transition.d.ts +6 -1
  105. package/dist/feedback/state-transition.js +6 -3
  106. package/dist/feedback/state-transition.js.map +1 -1
  107. package/dist/hybrid/capsule-builder.d.ts +23 -0
  108. package/dist/hybrid/capsule-builder.js +114 -0
  109. package/dist/hybrid/capsule-builder.js.map +1 -0
  110. package/dist/hybrid/explain-provider-client.d.ts +19 -0
  111. package/dist/hybrid/explain-provider-client.js +34 -0
  112. package/dist/hybrid/explain-provider-client.js.map +1 -0
  113. package/dist/hybrid/postmortem-provider-client.d.ts +19 -0
  114. package/dist/hybrid/postmortem-provider-client.js +34 -0
  115. package/dist/hybrid/postmortem-provider-client.js.map +1 -0
  116. package/dist/hybrid/rollout.d.ts +9 -0
  117. package/dist/hybrid/rollout.js +49 -0
  118. package/dist/hybrid/rollout.js.map +1 -0
  119. package/dist/hybrid/router.d.ts +4 -0
  120. package/dist/hybrid/router.js +62 -0
  121. package/dist/hybrid/router.js.map +1 -0
  122. package/dist/hybrid/types.d.ts +140 -0
  123. package/dist/hybrid/types.js +2 -0
  124. package/dist/hybrid/types.js.map +1 -0
  125. package/dist/hybrid/validators.d.ts +5 -0
  126. package/dist/hybrid/validators.js +94 -0
  127. package/dist/hybrid/validators.js.map +1 -0
  128. package/dist/hybrid/worker-client.d.ts +61 -0
  129. package/dist/hybrid/worker-client.js +196 -0
  130. package/dist/hybrid/worker-client.js.map +1 -0
  131. package/dist/hybrid/workers/explain-decision-llm.d.ts +8 -0
  132. package/dist/hybrid/workers/explain-decision-llm.js +152 -0
  133. package/dist/hybrid/workers/explain-decision-llm.js.map +1 -0
  134. package/dist/hybrid/workers/explain-decision.d.ts +2 -0
  135. package/dist/hybrid/workers/explain-decision.js +40 -0
  136. package/dist/hybrid/workers/explain-decision.js.map +1 -0
  137. package/dist/hybrid/workers/postmortem-review-llm.d.ts +8 -0
  138. package/dist/hybrid/workers/postmortem-review-llm.js +398 -0
  139. package/dist/hybrid/workers/postmortem-review-llm.js.map +1 -0
  140. package/dist/hybrid/workers/postmortem-review.d.ts +2 -0
  141. package/dist/hybrid/workers/postmortem-review.js +66 -0
  142. package/dist/hybrid/workers/postmortem-review.js.map +1 -0
  143. package/dist/install/claude-code-doctor.d.ts +1 -0
  144. package/dist/install/claude-code-doctor.js +20 -4
  145. package/dist/install/claude-code-doctor.js.map +1 -1
  146. package/dist/install/claude-code-installer.js +50 -1
  147. package/dist/install/claude-code-installer.js.map +1 -1
  148. package/dist/install/codex-cli.d.ts +15 -0
  149. package/dist/install/codex-cli.js +55 -3
  150. package/dist/install/codex-cli.js.map +1 -1
  151. package/dist/install/codex-installer.d.ts +7 -0
  152. package/dist/install/codex-installer.js +22 -0
  153. package/dist/install/codex-installer.js.map +1 -1
  154. package/dist/install/openclaw-cli.d.ts +11 -0
  155. package/dist/install/openclaw-cli.js.map +1 -1
  156. package/dist/install/openclaw-installer.d.ts +12 -7
  157. package/dist/install/openclaw-installer.js +197 -46
  158. package/dist/install/openclaw-installer.js.map +1 -1
  159. package/dist/interaction/service.d.ts +15 -0
  160. package/dist/interaction/service.js +189 -31
  161. package/dist/interaction/service.js.map +1 -1
  162. package/dist/plugin/hooks/before-prompt-build.d.ts +1 -0
  163. package/dist/plugin/hooks/before-prompt-build.js +4 -1
  164. package/dist/plugin/hooks/before-prompt-build.js.map +1 -1
  165. package/dist/plugin/openclaw-install-state.d.ts +39 -0
  166. package/dist/plugin/openclaw-install-state.js +24 -0
  167. package/dist/plugin/openclaw-install-state.js.map +1 -0
  168. package/dist/plugin/openclaw-plugin.d.ts +125 -0
  169. package/dist/plugin/openclaw-plugin.js +18 -7
  170. package/dist/plugin/openclaw-plugin.js.map +1 -1
  171. package/dist/plugin/openclaw-routine-interaction.d.ts +2 -1
  172. package/dist/plugin/openclaw-routine-interaction.js +12 -7
  173. package/dist/plugin/openclaw-routine-interaction.js.map +1 -1
  174. package/dist/plugin/openclaw-runtime-defaults.d.ts +16 -0
  175. package/dist/plugin/openclaw-runtime-defaults.js +16 -0
  176. package/dist/plugin/openclaw-runtime-defaults.js.map +1 -0
  177. package/dist/runtime/service.d.ts +34 -5
  178. package/dist/runtime/service.js +474 -49
  179. package/dist/runtime/service.js.map +1 -1
  180. package/dist/store/sqlite/db.js +28 -0
  181. package/dist/store/sqlite/db.js.map +1 -1
  182. package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.d.ts +11 -0
  183. package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.js +76 -0
  184. package/dist/store/sqlite/repositories/hybrid-invocation-trace-repo.js.map +1 -0
  185. package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.d.ts +11 -0
  186. package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.js +73 -0
  187. package/dist/store/sqlite/repositories/hybrid-review-artifact-repo.js.map +1 -0
  188. package/dist/store/sqlite/repositories/input-record-repo.d.ts +1 -0
  189. package/dist/store/sqlite/repositories/input-record-repo.js +13 -0
  190. package/dist/store/sqlite/repositories/input-record-repo.js.map +1 -1
  191. package/dist/store/sqlite/repositories/node-repo.d.ts +4 -0
  192. package/dist/store/sqlite/repositories/node-repo.js +54 -6
  193. package/dist/store/sqlite/repositories/node-repo.js.map +1 -1
  194. package/dist/store/sqlite/schema.sql +40 -0
  195. package/dist/store/vector/embeddings.js +26 -8
  196. package/dist/store/vector/embeddings.js.map +1 -1
  197. package/dist/types/domain.d.ts +151 -2
  198. package/dist/types/plugin.d.ts +2 -1
  199. package/docs/releases/v0.1.3.md +3 -2
  200. package/docs/releases/v0.2.0.md +85 -0
  201. package/docs/releases/v0.2.1.md +21 -0
  202. package/docs/user-guide.md +44 -13
  203. package/openclaw.plugin.json +81 -1
  204. package/package.json +11 -2
  205. package/plugins/claude-code-experienceengine/.claude-plugin/plugin.json +1 -1
  206. package/plugins/claude-code-experienceengine/scripts/install-deps.sh +1 -1
@@ -1,4 +1,3 @@
1
- import { LlmLearningGate } from "../analyzer/llm-learning-gate.js";
2
1
  import { buildCandidateSignals } from "../analyzer/candidate-signals.js";
3
2
  import { buildInjectionScorecard } from "../controller/injection-scorecard.js";
4
3
  import { classifyFailureAttributionReason } from "../feedback/automatic-attribution.js";
@@ -6,13 +5,16 @@ import { applyFeedback } from "../feedback/feedback-manager.js";
6
5
  import { detectHarm } from "../feedback/harm-detector.js";
7
6
  import { createEmptyStats, updateStats } from "../feedback/stats-updater.js";
8
7
  import { buildExperienceInput } from "../input/input-adapter.js";
8
+ import { buildRetrievalContext } from "../controller/retrieval-context.js";
9
9
  import { resolveScope } from "../input/scope-resolver.js";
10
10
  import { decideIntervention } from "../controller/intervention-controller.js";
11
11
  import { renderInlineNotice } from "../controller/inline-notice.js";
12
+ import { applyGovernedNodeFeedback, deriveNodeOriginProfileForNode } from "../experience-management/node-lifecycle-governance.js";
13
+ import { resolveHybridRolloutState } from "../hybrid/rollout.js";
14
+ import { selectHybridRoute } from "../hybrid/router.js";
12
15
  import { nowIso } from "../utils/clock.js";
13
16
  import { createId, stableId } from "../utils/ids.js";
14
17
  import { bootstrapDatabase, openDatabase, withTransaction } from "../store/sqlite/db.js";
15
- import { DistillationQueueWorker } from "../distillation/queue-worker.js";
16
18
  import { CandidateRepository } from "../store/sqlite/repositories/candidate-repo.js";
17
19
  import { DistillationJobRepository } from "../store/sqlite/repositories/distillation-job-repo.js";
18
20
  import { InputRecordRepository } from "../store/sqlite/repositories/input-record-repo.js";
@@ -23,9 +25,16 @@ import { ScopeRepository } from "../store/sqlite/repositories/scope-repo.js";
23
25
  import { StatsRepository } from "../store/sqlite/repositories/stats-repo.js";
24
26
  import { TaskRunRepository } from "../store/sqlite/repositories/task-run-repo.js";
25
27
  import { InjectionRepository } from "../store/sqlite/repositories/injection-repo.js";
28
+ import { HybridInvocationTraceRepository } from "../store/sqlite/repositories/hybrid-invocation-trace-repo.js";
26
29
  import { RuntimeCaptureWriter } from "../plugin/runtime-capture.js";
27
30
  import { normalizeToolResult } from "../plugin/hooks/tool-result-persist.js";
28
31
  import { extractToolResultsFromPayload } from "../plugin/runtime-helpers.js";
32
+ import { HybridReviewArtifactRepository } from "../store/sqlite/repositories/hybrid-review-artifact-repo.js";
33
+ const loadLlmLearningGate = async () => import("../analyzer/llm-learning-gate.js");
34
+ const loadDistillationQueueWorker = async () => import("../distillation/queue-worker.js");
35
+ const loadHybridWorkerClientModule = async () => import("../hybrid/worker-client.js");
36
+ const loadHybridCapsuleBuilder = async () => import("../hybrid/capsule-builder.js");
37
+ const loadHybridPostmortemProviderClient = async () => import("../hybrid/postmortem-provider-client.js");
29
38
  const toEvidence = (input) => input.tool_events.map((event) => [event.tool_name, event.status, event.error_signature ?? event.output_summary]
30
39
  .filter(Boolean)
31
40
  .join(": "));
@@ -82,9 +91,57 @@ const buildCandidateSourceSignal = (input) => {
82
91
  failure_signature: signals.failure_signature,
83
92
  retry_count: signals.retry_count,
84
93
  correction_signals: signals.correction_signals,
94
+ directional_correction: signals.directional_correction,
95
+ evidence_driven_reversal: signals.evidence_driven_reversal,
85
96
  tool_event_summary: signals.tool_event_summary
86
97
  };
87
98
  };
99
+ const mergeDirectionalCorrectionIntoSourceSignal = (sourceSignal, draft) => {
100
+ const directionalCorrection = sourceSignal.directional_correction;
101
+ if (!directionalCorrection) {
102
+ return sourceSignal;
103
+ }
104
+ const semanticDetected = Boolean(draft.experience_kind === "expectation_correction" &&
105
+ draft.correction_category &&
106
+ draft.deviation_pattern &&
107
+ draft.corrected_constraint);
108
+ if (!semanticDetected) {
109
+ return sourceSignal;
110
+ }
111
+ return {
112
+ ...sourceSignal,
113
+ directional_correction: {
114
+ ...directionalCorrection,
115
+ semantic_detected: true,
116
+ correction_category: draft.correction_category,
117
+ deviation_pattern: draft.deviation_pattern,
118
+ corrected_constraint: draft.corrected_constraint
119
+ }
120
+ };
121
+ };
122
+ const mergeEvidenceDrivenReversalIntoSourceSignal = (sourceSignal, draft) => {
123
+ const reversal = sourceSignal.evidence_driven_reversal;
124
+ if (!reversal) {
125
+ return sourceSignal;
126
+ }
127
+ const semanticDetected = Boolean(draft.experience_kind === "expectation_correction" &&
128
+ draft.correction_category &&
129
+ draft.deviation_pattern &&
130
+ draft.corrected_constraint);
131
+ if (!semanticDetected) {
132
+ return sourceSignal;
133
+ }
134
+ return {
135
+ ...sourceSignal,
136
+ evidence_driven_reversal: {
137
+ ...reversal,
138
+ semantic_detected: true,
139
+ correction_category: draft.correction_category,
140
+ deviation_pattern: draft.deviation_pattern,
141
+ corrected_constraint: draft.corrected_constraint
142
+ }
143
+ };
144
+ };
88
145
  const summarizeRawCandidate = (sourceSignal) => {
89
146
  const fragments = [...sourceSignal.tool_event_summary];
90
147
  if (sourceSignal.failure_signature) {
@@ -104,9 +161,14 @@ const resolveCandidateKind = (input, sourceSignal) => {
104
161
  }
105
162
  return "failure";
106
163
  };
107
- const draftToCandidate = (draft, input, originRecordId, taskRunId) => {
164
+ const draftToCandidate = (draft, input, originRecordId, taskRunId, directionalCorrectionSignal, evidenceDrivenReversalSignal) => {
108
165
  const timestamp = nowIso();
109
- const sourceSignal = buildCandidateSourceSignal(input);
166
+ const baseSourceSignal = buildCandidateSourceSignal(input);
167
+ const sourceSignal = mergeEvidenceDrivenReversalIntoSourceSignal(mergeDirectionalCorrectionIntoSourceSignal({
168
+ ...baseSourceSignal,
169
+ directional_correction: directionalCorrectionSignal ?? baseSourceSignal.directional_correction,
170
+ evidence_driven_reversal: evidenceDrivenReversalSignal ?? baseSourceSignal.evidence_driven_reversal
171
+ }, draft), draft);
110
172
  const candidateId = stableId("candidate", [draft.scope_id, draft.task_type, draft.node_type, draft.compact_hint, originRecordId].join(":"));
111
173
  return {
112
174
  id: candidateId,
@@ -138,6 +200,7 @@ const candidateToInitialJob = (candidate, extractorProfile) => {
138
200
  };
139
201
  };
140
202
  const mergeContext = (existing, incoming) => ({
203
+ host: incoming.host ?? existing?.host,
141
204
  sessionId: incoming.sessionId ?? existing?.sessionId,
142
205
  cwd: incoming.cwd ?? existing?.cwd,
143
206
  userMessage: incoming.userMessage || existing?.userMessage || "",
@@ -186,6 +249,23 @@ const resolveDeliveryMode = (evaluationMode, holdoutRate, sessionId, taskSummary
186
249
  delivered: true
187
250
  };
188
251
  };
252
+ const HYBRID_LIGHTWEIGHT_PATTERN = /\b(wording-only|wording only|copy-only|copy only|copy pass|inline notice wording|expression-layer refinement)\b/i;
253
+ const isLightweightHybridExcludedTask = (input) => HYBRID_LIGHTWEIGHT_PATTERN.test(`${input.task_summary} ${input.context_summary ?? ""}`);
254
+ export const decidePosttaskHybridRoute = (config, input, signals, rolloutKey = input.task_summary) => {
255
+ const rollout = resolveHybridRolloutState(config, rolloutKey);
256
+ return selectHybridRoute({
257
+ ...signals,
258
+ explicitExplanationRequest: false,
259
+ existingConservativePathRequired: false,
260
+ lightweightOrExcludedTask: signals.lightweightOrExcludedTask || isLightweightHybridExcludedTask(input),
261
+ rolloutAllowsAsyncPostmortem: config.hybridAsyncPostmortemEnabled && rollout.hybridActive
262
+ }, {
263
+ enabled: config.hybridEnabled && rollout.hybridActive,
264
+ syncExplainEnabled: false,
265
+ asyncPostmortemEnabled: config.hybridAsyncPostmortemEnabled && rollout.hybridActive,
266
+ policyVersion: config.hybridRoutePolicyVersion
267
+ });
268
+ };
189
269
  export class ExperienceRuntimeService {
190
270
  config;
191
271
  db;
@@ -202,12 +282,21 @@ export class ExperienceRuntimeService {
202
282
  reviewEventRepo;
203
283
  statsRepo;
204
284
  injectionRepo;
205
- distillationWorker;
206
- learningGate;
285
+ hybridReviewArtifactRepo;
286
+ hybridTraceRepo;
287
+ runtimeOptions;
288
+ backgroundLearningEnabled;
289
+ hybridPosttaskEnabled;
290
+ distillationWorkerPromise;
291
+ learningGatePromise;
292
+ hybridWorkerClientPromise;
207
293
  pendingLearningTasks = new Set();
208
294
  captureWriter;
209
295
  constructor(config, logger, runtimeOptions = {}) {
210
296
  this.config = config;
297
+ this.runtimeOptions = runtimeOptions;
298
+ this.backgroundLearningEnabled = !runtimeOptions.disableBackgroundLearning;
299
+ this.hybridPosttaskEnabled = !runtimeOptions.disableHybridPosttask;
211
300
  this.logger = logger ?? {};
212
301
  this.db = openDatabase(config);
213
302
  bootstrapDatabase(this.db);
@@ -221,8 +310,8 @@ export class ExperienceRuntimeService {
221
310
  this.reviewEventRepo = new ReviewEventRepository(this.db);
222
311
  this.statsRepo = new StatsRepository(this.db);
223
312
  this.injectionRepo = new InjectionRepository(this.db);
224
- this.distillationWorker = new DistillationQueueWorker(config, this.candidateRepo, this.jobRepo, this.nodeRepo, runtimeOptions);
225
- this.learningGate = new LlmLearningGate(config, runtimeOptions);
313
+ this.hybridReviewArtifactRepo = new HybridReviewArtifactRepository(this.db);
314
+ this.hybridTraceRepo = new HybridInvocationTraceRepository(this.db);
226
315
  this.captureWriter = new RuntimeCaptureWriter(config, this.logger);
227
316
  }
228
317
  getSession(sessionId) {
@@ -247,8 +336,12 @@ export class ExperienceRuntimeService {
247
336
  session.toolEventKeys.add(key);
248
337
  session.toolEvents.push(toolEvent);
249
338
  }
250
- resolveScopedNodes(scopeId) {
251
- return this.nodeRepo.listInjectableByScope(scopeId);
339
+ // The shipped runtime path stays exact-scope-only in this rollout.
340
+ resolveExactScopeInjectableNodes(scopeId) {
341
+ return this.nodeRepo.listLiveInjectableByExactScope(scopeId);
342
+ }
343
+ resolveConservativeCrossScopeCandidates(scopeId) {
344
+ return this.nodeRepo.listConservativeCrossScopeCandidates(scopeId);
252
345
  }
253
346
  recoverToolEvents(sessionId, payload) {
254
347
  for (const toolResult of extractToolResultsFromPayload(payload)) {
@@ -301,8 +394,245 @@ export class ExperienceRuntimeService {
301
394
  async waitForBackgroundLearning() {
302
395
  await Promise.allSettled([...this.pendingLearningTasks]);
303
396
  }
397
+ async getLearningGate() {
398
+ if (!this.backgroundLearningEnabled) {
399
+ return undefined;
400
+ }
401
+ this.learningGatePromise ??= loadLlmLearningGate().then(({ LlmLearningGate: LoadedLlmLearningGate }) => new LoadedLlmLearningGate(this.config, this.runtimeOptions));
402
+ return this.learningGatePromise;
403
+ }
404
+ async getDistillationWorker() {
405
+ if (!this.backgroundLearningEnabled) {
406
+ return undefined;
407
+ }
408
+ this.distillationWorkerPromise ??= loadDistillationQueueWorker().then(({ DistillationQueueWorker: LoadedDistillationQueueWorker }) => new LoadedDistillationQueueWorker(this.config, this.candidateRepo, this.jobRepo, this.nodeRepo, this.runtimeOptions));
409
+ return this.distillationWorkerPromise;
410
+ }
411
+ async getHybridWorkerClient() {
412
+ if (!this.hybridPosttaskEnabled) {
413
+ return undefined;
414
+ }
415
+ this.hybridWorkerClientPromise ??= loadHybridWorkerClientModule().then(({ HybridWorkerClient: LoadedHybridWorkerClient }) => new LoadedHybridWorkerClient({
416
+ explainDecisionEnabled: this.config.hybridEnabled && this.config.hybridSyncExplainEnabled,
417
+ postmortemReviewEnabled: this.config.hybridEnabled && this.config.hybridAsyncPostmortemEnabled,
418
+ postmortemReviewLlmEnabled: this.config.hybridEnabled
419
+ && this.config.hybridAsyncPostmortemEnabled
420
+ && this.config.hybridAsyncPostmortemLlmEnabled,
421
+ ...this.runtimeOptions.hybridWorkerClientOptions
422
+ }));
423
+ return this.hybridWorkerClientPromise;
424
+ }
425
+ buildPostmortemArtifact(input) {
426
+ const timestamp = nowIso();
427
+ return {
428
+ id: createId("hybridreview"),
429
+ task_run_id: input.taskRun.id,
430
+ scope_id: input.taskRun.scope_id,
431
+ worker_task: "postmortem_review",
432
+ approval_class: input.result.approvalClass === "policy_gated" ? "policy_gated" : "review_artifact",
433
+ schema_version: this.config.hybridCapsuleSchemaVersion,
434
+ route_policy_version: input.routeDecision.policyVersion,
435
+ worker_profile_version: this.config.hybridPostmortemReviewProfileVersion,
436
+ recommendation: input.result.value.candidate_recommendation,
437
+ summary: input.result.value.review_artifact?.summary ?? input.result.value.reason,
438
+ payload: input.result.value,
439
+ created_at: timestamp,
440
+ updated_at: timestamp
441
+ };
442
+ }
443
+ applyPostmortemDeliveryRecommendation(node, recommendation) {
444
+ if (recommendation === "keep" || recommendation === "review") {
445
+ return node;
446
+ }
447
+ if (recommendation === "quarantine") {
448
+ return {
449
+ ...node,
450
+ delivery_state: "quarantined",
451
+ quarantined_at: node.quarantined_at ?? nowIso(),
452
+ quarantine_reason: node.quarantine_reason ?? "postmortem_review"
453
+ };
454
+ }
455
+ if (node.delivery_state === "quarantined") {
456
+ return node;
457
+ }
458
+ return {
459
+ ...node,
460
+ delivery_state: node.delivery_state === "shadow_only" ? "shadow_only" : "conservative_only"
461
+ };
462
+ }
463
+ applyAcceptedPostmortemNodeReviews(input) {
464
+ const reviews = input.result.value.injected_node_reviews ?? [];
465
+ if (!reviews.length || input.taskRun.final_status === "cancelled") {
466
+ return false;
467
+ }
468
+ const allowedIds = new Set(input.experienceInput.injected_node_ids);
469
+ const existingEvents = this.reviewEventRepo.listByTaskRunId(input.taskRun.id);
470
+ let applied = false;
471
+ for (const review of reviews) {
472
+ if (!allowedIds.has(review.node_id) || review.confidence === "low") {
473
+ continue;
474
+ }
475
+ const current = this.nodeRepo.getById(review.node_id);
476
+ if (!current) {
477
+ continue;
478
+ }
479
+ const existingNodeEvents = existingEvents.filter((event) => event.node_id === review.node_id && event.source === "automatic");
480
+ const alreadyMarkedHelped = existingNodeEvents.some((event) => event.event_type === "mark_helped");
481
+ const alreadyMarkedHarmed = existingNodeEvents.some((event) => event.event_type === "mark_harmed");
482
+ let nextNode = current;
483
+ let feedbackEventType;
484
+ if (review.feedback_verdict === "helped" && !alreadyMarkedHelped) {
485
+ nextNode = applyGovernedNodeFeedback(nextNode, "helped", deriveNodeOriginProfileForNode(this.inputRepo, nextNode));
486
+ feedbackEventType = "mark_helped";
487
+ }
488
+ else if (review.feedback_verdict === "harmed" && !alreadyMarkedHarmed) {
489
+ nextNode = applyGovernedNodeFeedback(nextNode, "harmed", deriveNodeOriginProfileForNode(this.inputRepo, nextNode));
490
+ feedbackEventType = "mark_harmed";
491
+ }
492
+ const nodeAfterDelivery = this.applyPostmortemDeliveryRecommendation(nextNode, review.delivery_recommendation);
493
+ if (feedbackEventType
494
+ || nodeAfterDelivery.delivery_state !== current.delivery_state
495
+ || nodeAfterDelivery.state !== current.state
496
+ || nodeAfterDelivery.helped_count !== current.helped_count
497
+ || nodeAfterDelivery.harmed_count !== current.harmed_count
498
+ || nodeAfterDelivery.last_feedback_verdict !== current.last_feedback_verdict) {
499
+ this.nodeRepo.upsert(nodeAfterDelivery);
500
+ applied = true;
501
+ }
502
+ if (feedbackEventType) {
503
+ this.reviewEventRepo.upsert({
504
+ id: createId("review"),
505
+ node_id: review.node_id,
506
+ task_run_id: input.taskRun.id,
507
+ event_type: feedbackEventType,
508
+ source: "automatic",
509
+ created_at: nowIso()
510
+ });
511
+ }
512
+ if (current.delivery_state !== "quarantined" && nodeAfterDelivery.delivery_state === "quarantined") {
513
+ this.reviewEventRepo.upsert({
514
+ id: createId("review"),
515
+ node_id: review.node_id,
516
+ task_run_id: input.taskRun.id,
517
+ event_type: "quarantine",
518
+ source: "automatic",
519
+ created_at: nowIso()
520
+ });
521
+ }
522
+ }
523
+ return applied;
524
+ }
525
+ async persistHybridPostmortemArtifactAsync(input) {
526
+ if (!this.hybridPosttaskEnabled) {
527
+ return;
528
+ }
529
+ if (this.hybridReviewArtifactRepo.getByTaskRunId(input.taskRun.id)) {
530
+ return;
531
+ }
532
+ const hybridWorkerClient = await this.getHybridWorkerClient();
533
+ if (!hybridWorkerClient) {
534
+ return;
535
+ }
536
+ const candidateSignals = buildCandidateSignals(input.experienceInput);
537
+ const [{ buildPostmortemReviewCapsule }, { resolveHybridPostmortemProviderEndpoint }] = await Promise.all([
538
+ loadHybridCapsuleBuilder(),
539
+ loadHybridPostmortemProviderClient()
540
+ ]);
541
+ const capsule = buildPostmortemReviewCapsule({
542
+ schemaVersion: this.config.hybridCapsuleSchemaVersion,
543
+ routeDecision: input.routeDecision,
544
+ taskRun: input.taskRun,
545
+ outcomeSignal: input.experienceInput.outcome_signal,
546
+ triggers: {
547
+ directionalCorrectionPresent: candidateSignals.directional_correction?.detected === true
548
+ || candidateSignals.evidence_driven_reversal?.detected === true,
549
+ injectedNodeInteractionPresent: input.experienceInput.injected_node_ids.length > 0,
550
+ retryOrInvalidationSignaturePresent: candidateSignals.retry_count > 0 || candidateSignals.evidence_driven_reversal?.invalidating_evidence === true,
551
+ meaningfulFailureSignaturePresent: Boolean(candidateSignals.failure_signature),
552
+ conservativeTransitionReviewWorthy: input.experienceInput.outcome_signal === "success" && input.experienceInput.injected_node_ids.length > 0
553
+ },
554
+ injectedNodes: input.experienceInput.injected_node_ids
555
+ .map((id) => this.nodeRepo.getById(id))
556
+ .filter((node) => Boolean(node)),
557
+ toolEvents: input.toolEvents
558
+ });
559
+ const providerResolution = this.config.hybridAsyncPostmortemLlmEnabled
560
+ ? resolveHybridPostmortemProviderEndpoint(this.config)
561
+ : { status: "disabled", reason: "Phase 3 provider-backed postmortem review is disabled." };
562
+ const result = this.config.hybridAsyncPostmortemLlmEnabled && providerResolution.status === "unavailable"
563
+ ? {
564
+ status: "fallback",
565
+ reason: "provider_unavailable"
566
+ }
567
+ : await hybridWorkerClient.runPostmortemReview(capsule, providerResolution.status === "configured"
568
+ ? {
569
+ mode: "provider",
570
+ endpoint: providerResolution.endpoint
571
+ }
572
+ : undefined);
573
+ const timestamp = nowIso();
574
+ const persistAcceptedArtifact = result.status === "accepted"
575
+ && input.rolloutMode !== "shadow"
576
+ && (result.approvalClass === "review_artifact" || result.approvalClass === "policy_gated");
577
+ const appliedNodeWriteback = result.status === "accepted" && input.rolloutMode !== "shadow"
578
+ ? this.applyAcceptedPostmortemNodeReviews({
579
+ taskRun: input.taskRun,
580
+ experienceInput: input.experienceInput,
581
+ result
582
+ })
583
+ : false;
584
+ this.hybridTraceRepo.upsert({
585
+ id: createId("hybridtrace"),
586
+ surface: "runtime",
587
+ session_id: input.taskRun.session_id,
588
+ scope_id: input.taskRun.scope_id,
589
+ worker_task: "postmortem_review",
590
+ route: input.routeDecision.route,
591
+ route_policy_version: input.routeDecision.policyVersion,
592
+ capsule_schema_version: this.config.hybridCapsuleSchemaVersion,
593
+ worker_profile_version: this.config.hybridAsyncPostmortemLlmEnabled
594
+ ? this.config.hybridPostmortemModelProfileVersion
595
+ : this.config.hybridPostmortemReviewProfileVersion,
596
+ rollout_mode: input.rolloutMode,
597
+ rollout_reason: input.rolloutReason,
598
+ worker_ran: result.status !== "fallback" || result.reason !== "provider_unavailable",
599
+ validation_status: result.status === "accepted" ? "accepted" : "fallback",
600
+ output_action: persistAcceptedArtifact || appliedNodeWriteback ? "stored" : "rejected",
601
+ fallback_reason: result.status === "accepted" ? undefined : result.reason,
602
+ created_at: timestamp
603
+ });
604
+ if (result.status !== "accepted") {
605
+ this.logger.debug?.("experienceengine.hybrid_postmortem_skipped", {
606
+ taskRunId: input.taskRun.id,
607
+ reason: result.reason
608
+ });
609
+ return;
610
+ }
611
+ if (persistAcceptedArtifact) {
612
+ this.hybridReviewArtifactRepo.upsert(this.buildPostmortemArtifact({
613
+ taskRun: input.taskRun,
614
+ result,
615
+ routeDecision: input.routeDecision
616
+ }));
617
+ }
618
+ }
304
619
  async persistCandidatesAsync(input, originRecordId, taskRunId, sessionId) {
305
- const result = await this.learningGate.generateCandidateDrafts(input);
620
+ const learningGate = await this.getLearningGate();
621
+ if (!learningGate) {
622
+ if (taskRunId) {
623
+ const taskRun = this.taskRunRepo.getById(taskRunId);
624
+ if (taskRun) {
625
+ this.taskRunRepo.upsert({
626
+ ...taskRun,
627
+ learning_status: "not_applicable",
628
+ learning_reason: "background learning disabled",
629
+ updated_at: nowIso()
630
+ });
631
+ }
632
+ }
633
+ return;
634
+ }
635
+ const result = await learningGate.generateCandidateDrafts(input);
306
636
  if (taskRunId) {
307
637
  const taskRun = this.taskRunRepo.getById(taskRunId);
308
638
  if (taskRun) {
@@ -327,7 +657,7 @@ export class ExperienceRuntimeService {
327
657
  });
328
658
  return;
329
659
  }
330
- const persistedCandidates = result.drafts.map((draft) => draftToCandidate(draft, input, originRecordId, taskRunId));
660
+ const persistedCandidates = result.drafts.map((draft) => draftToCandidate(draft, input, originRecordId, taskRunId, result.directionalCorrectionSignal, result.evidenceDrivenReversalSignal));
331
661
  withTransaction(this.db, () => {
332
662
  for (const candidate of persistedCandidates) {
333
663
  this.candidateRepo.upsert(candidate);
@@ -342,7 +672,11 @@ export class ExperienceRuntimeService {
342
672
  reason: result.reason
343
673
  });
344
674
  if (this.config.distillationAutoDrain) {
345
- await this.distillationWorker.drain().catch((error) => {
675
+ const distillationWorker = await this.getDistillationWorker();
676
+ if (!distillationWorker) {
677
+ return;
678
+ }
679
+ await distillationWorker.drain().catch((error) => {
346
680
  this.logger.error?.("experienceengine.distillation_drain_failed", {
347
681
  sessionId,
348
682
  error: error instanceof Error ? error.message : String(error)
@@ -350,7 +684,7 @@ export class ExperienceRuntimeService {
350
684
  });
351
685
  }
352
686
  }
353
- updateInjectedNodes(input, attributionRecordId, taskRunId) {
687
+ updateInjectedNodes(input, attributionRecordId, taskRunId, injectionEvent) {
354
688
  if (!input.injected_node_ids.length) {
355
689
  return;
356
690
  }
@@ -362,7 +696,7 @@ export class ExperienceRuntimeService {
362
696
  if (input.outcome_signal === "success") {
363
697
  return {
364
698
  nodeId: node.id,
365
- eventType: "mark_helped"
699
+ eventType: "mark_uncertain"
366
700
  };
367
701
  }
368
702
  if (detectHarm(input, node)) {
@@ -374,8 +708,35 @@ export class ExperienceRuntimeService {
374
708
  return undefined;
375
709
  })
376
710
  .filter((value) => Boolean(value));
377
- for (const node of applyFeedback(input, touched, attributionRecordId)) {
378
- this.nodeRepo.upsert(node);
711
+ const originProfilesByNodeId = Object.fromEntries(touched.map((node) => {
712
+ return [node.id, deriveNodeOriginProfileForNode(this.inputRepo, node)];
713
+ }));
714
+ const highMatchPromotionIds = new Set(injectionEvent?.scorecard?.topCandidates
715
+ ?.filter((candidate) => candidate.matchScorecard?.scopeMatch === "same" &&
716
+ candidate.matchScorecard.overallMatchBand === "high" &&
717
+ candidate.matchScorecard.negativeEvidence.length === 0)
718
+ .map((candidate) => candidate.id) ?? []);
719
+ const promotedNodeIds = [];
720
+ for (const node of applyFeedback(input, touched, attributionRecordId, { originProfilesByNodeId })) {
721
+ const shouldPromoteSameScopeHighMatch = input.outcome_signal === "success" &&
722
+ input.scope_id === node.scope_id &&
723
+ highMatchPromotionIds.has(node.id) &&
724
+ node.state === "priority_candidate" &&
725
+ node.delivery_state === "conservative_only" &&
726
+ node.harmed_count === 0;
727
+ const nextNode = shouldPromoteSameScopeHighMatch
728
+ ? {
729
+ ...node,
730
+ state: "active",
731
+ delivery_state: "eligible",
732
+ validation_state: node.validation_state ?? "validated_by_reuse",
733
+ promotion_reason: node.promotion_reason ?? "same_scope_high_match_success"
734
+ }
735
+ : node;
736
+ if (shouldPromoteSameScopeHighMatch) {
737
+ promotedNodeIds.push(node.id);
738
+ }
739
+ this.nodeRepo.upsert(nextNode);
379
740
  }
380
741
  for (const event of automaticEvents) {
381
742
  this.reviewEventRepo.upsert({
@@ -387,12 +748,23 @@ export class ExperienceRuntimeService {
387
748
  created_at: nowIso()
388
749
  });
389
750
  }
751
+ for (const nodeId of promotedNodeIds) {
752
+ this.reviewEventRepo.upsert({
753
+ id: createId("review"),
754
+ node_id: nodeId,
755
+ task_run_id: taskRunId,
756
+ event_type: "promote_eligible",
757
+ source: "automatic",
758
+ created_at: nowIso()
759
+ });
760
+ }
390
761
  }
391
762
  async beforePromptBuild(context) {
392
763
  const sessionId = context.sessionId ?? "global";
393
764
  const session = this.getSession(sessionId);
394
765
  session.context = mergeContext(session.context, context);
395
766
  const input = buildExperienceInput(session.context, session.toolEvents);
767
+ const retrievalContext = buildRetrievalContext(input, session.context);
396
768
  const resolvedScope = resolveScope(session.context.cwd);
397
769
  const existingScope = this.scopeRepo.getById(resolvedScope.scope_id);
398
770
  if (existingScope?.is_disabled) {
@@ -410,6 +782,7 @@ export class ExperienceRuntimeService {
410
782
  mode: "skip",
411
783
  text: undefined,
412
784
  notice: undefined,
785
+ retrievalContext,
413
786
  input: {
414
787
  ...input,
415
788
  scope_id: existingScope.scope_id,
@@ -418,8 +791,13 @@ export class ExperienceRuntimeService {
418
791
  };
419
792
  }
420
793
  const stats = input.task_type !== "unknown" ? this.statsRepo.get(input.scope_id, input.task_type) : undefined;
421
- const nodes = input.task_type !== "unknown" ? this.resolveScopedNodes(input.scope_id) : [];
422
- const decision = await decideIntervention(input, nodes, stats, this.config.triggerThreshold, this.config.maxHints, this.config);
794
+ const nodes = input.task_type !== "unknown"
795
+ ? [
796
+ ...this.resolveExactScopeInjectableNodes(input.scope_id),
797
+ ...this.resolveConservativeCrossScopeCandidates(input.scope_id)
798
+ ]
799
+ : [];
800
+ const decision = await decideIntervention(input, nodes, stats, this.config.triggerThreshold, this.config.maxHints, this.config, retrievalContext);
423
801
  const selectedNodeIds = decision.selected.map((node) => node.id);
424
802
  const delivery = resolveDeliveryMode(this.config.evaluationMode, this.config.holdoutRate, sessionId, input.task_summary, decision.mode !== "skip" && selectedNodeIds.length > 0);
425
803
  session.injectedNodeIds = delivery.delivered ? selectedNodeIds : [];
@@ -427,30 +805,27 @@ export class ExperienceRuntimeService {
427
805
  ...session.context,
428
806
  injectedNodeIds: session.injectedNodeIds
429
807
  };
430
- if (decision.mode !== "skip") {
431
- const scorecard = buildInjectionScorecard(input, decision.mode, decision.selected, sessionId, decision.diagnostics);
432
- const injectionEvent = {
433
- injection_id: createId("inject"),
434
- session_id: sessionId,
435
- scope_id: input.scope_id,
436
- task_type: input.task_type === "unknown" ? "general" : input.task_type,
437
- task_summary: input.task_summary,
438
- mode: decision.mode,
439
- delivery_mode: delivery.deliveryMode,
440
- delivered: delivery.delivered,
441
- injected_node_ids: selectedNodeIds,
442
- injection_count: selectedNodeIds.length,
443
- scorecard,
444
- was_successful: null,
445
- harm_observed: null,
446
- created_at: nowIso()
447
- };
448
- this.injectionRepo.upsert(injectionEvent);
449
- session.lastInjectionEvent = injectionEvent;
450
- }
451
- else {
452
- session.lastInjectionEvent = undefined;
453
- }
808
+ const scorecard = decision.mode !== "skip"
809
+ ? buildInjectionScorecard(input, decision.mode, decision.selected, sessionId, decision.diagnostics)
810
+ : undefined;
811
+ const injectionEvent = {
812
+ injection_id: createId(decision.mode === "skip" ? "decision" : "inject"),
813
+ session_id: sessionId,
814
+ scope_id: input.scope_id,
815
+ task_type: input.task_type === "unknown" ? "general" : input.task_type,
816
+ task_summary: input.task_summary,
817
+ mode: decision.mode,
818
+ delivery_mode: delivery.deliveryMode,
819
+ delivered: delivery.delivered,
820
+ injected_node_ids: selectedNodeIds,
821
+ injection_count: selectedNodeIds.length,
822
+ scorecard,
823
+ was_successful: null,
824
+ harm_observed: null,
825
+ created_at: nowIso()
826
+ };
827
+ this.injectionRepo.upsert(injectionEvent);
828
+ session.lastInjectionEvent = injectionEvent;
454
829
  this.logger.debug?.("experienceengine.before_prompt_build", {
455
830
  sessionId,
456
831
  mode: decision.mode,
@@ -466,6 +841,7 @@ export class ExperienceRuntimeService {
466
841
  scorecard: decision.mode !== "skip" ? session.lastInjectionEvent?.scorecard : undefined,
467
842
  deliveryMode: decision.mode !== "skip" ? delivery.deliveryMode : undefined,
468
843
  delivered: decision.mode !== "skip" ? delivery.delivered : undefined,
844
+ retrievalContext,
469
845
  input: {
470
846
  ...input,
471
847
  injected_node_ids: session.injectedNodeIds
@@ -499,7 +875,7 @@ export class ExperienceRuntimeService {
499
875
  const taskRun = toTaskRun(input, sessionId, context);
500
876
  this.taskRunRepo.upsert(taskRun);
501
877
  this.outcomeRepo.upsert(toOutcomeRecord(taskRun, input));
502
- this.updateInjectedNodes(input, record.record_id, taskRun.id);
878
+ this.updateInjectedNodes(input, record.record_id, taskRun.id, session.lastInjectionEvent);
503
879
  if (session.lastInjectionEvent) {
504
880
  const touchedNodes = session.lastInjectionEvent.injected_node_ids
505
881
  .map((id) => this.nodeRepo.getById(id))
@@ -527,22 +903,71 @@ export class ExperienceRuntimeService {
527
903
  input,
528
904
  originRecordId: record.record_id,
529
905
  taskRunId: taskRun.id,
530
- sessionId
906
+ sessionId,
907
+ taskRun,
908
+ toolEvents: [...session.toolEvents]
531
909
  };
532
910
  });
533
911
  this.sessions.delete(sessionId);
534
- if (learningTaskContext) {
535
- this.trackLearningTask(this.persistCandidatesAsync(learningTaskContext.input, learningTaskContext.originRecordId, learningTaskContext.taskRunId, learningTaskContext.sessionId));
912
+ const rollout = resolveHybridRolloutState(this.config, `${sessionId}:${input.task_summary}`);
913
+ const hybridPosttaskRoute = decidePosttaskHybridRoute(this.config, input, {
914
+ taskStage: "posttask",
915
+ completedRun: true,
916
+ terminalOutcomeRecorded: true,
917
+ boundedPosttaskCapsuleAvailable: Boolean(input.task_summary),
918
+ postmortemAlreadyRecorded: learningTaskContext
919
+ ? Boolean(this.hybridReviewArtifactRepo.getByTaskRunId(learningTaskContext.taskRun.id))
920
+ : false,
921
+ lightweightOrExcludedTask: false,
922
+ directionalCorrectionPresent: Boolean(learningTaskContext
923
+ ? buildCandidateSignals(learningTaskContext.input).directional_correction?.detected
924
+ || buildCandidateSignals(learningTaskContext.input).evidence_driven_reversal?.detected
925
+ : false),
926
+ injectedNodeInteractionPresent: input.injected_node_ids.length > 0,
927
+ retryOrInvalidationSignaturePresent: Boolean(learningTaskContext
928
+ ? buildCandidateSignals(learningTaskContext.input).retry_count > 0
929
+ || buildCandidateSignals(learningTaskContext.input).evidence_driven_reversal?.invalidating_evidence
930
+ : false),
931
+ meaningfulFailureSignaturePresent: Boolean(learningTaskContext
932
+ ? buildCandidateSignals(learningTaskContext.input).failure_signature
933
+ : input.outcome_signal === "failure"),
934
+ conservativeTransitionReviewWorthy: false
935
+ }, `${sessionId}:${input.task_summary}`);
936
+ if (learningTaskContext && this.backgroundLearningEnabled) {
937
+ this.trackLearningTask((async () => {
938
+ await this.persistCandidatesAsync(learningTaskContext.input, learningTaskContext.originRecordId, learningTaskContext.taskRunId, learningTaskContext.sessionId);
939
+ if (hybridPosttaskRoute.route !== "ESCALATE_ASYNC_POSTMORTEM") {
940
+ return;
941
+ }
942
+ const refreshedTaskRun = this.taskRunRepo.getById(learningTaskContext.taskRun.id) ?? learningTaskContext.taskRun;
943
+ await this.persistHybridPostmortemArtifactAsync({
944
+ taskRun: refreshedTaskRun,
945
+ experienceInput: learningTaskContext.input,
946
+ routeDecision: hybridPosttaskRoute,
947
+ toolEvents: learningTaskContext.toolEvents,
948
+ rolloutMode: rollout.effectiveMode,
949
+ rolloutReason: rollout.reason
950
+ });
951
+ })());
536
952
  }
537
953
  this.logger.info?.("experienceengine.finalize", {
538
954
  sessionId,
539
955
  taskType: input.task_type,
540
- outcome: input.outcome_signal
956
+ outcome: input.outcome_signal,
957
+ hybridPosttaskRoute: hybridPosttaskRoute.route,
958
+ hybridPosttaskRouteReason: hybridPosttaskRoute.reasonCode,
959
+ hybridRoutePolicyVersion: hybridPosttaskRoute.policyVersion,
960
+ hybridRolloutMode: rollout.effectiveMode,
961
+ hybridRolloutReason: rollout.reason
541
962
  });
542
963
  return input;
543
964
  }
544
965
  async drainDistillationQueue(limit) {
545
- return this.distillationWorker.drain(limit);
966
+ const distillationWorker = await this.getDistillationWorker();
967
+ if (!distillationWorker) {
968
+ return 0;
969
+ }
970
+ return distillationWorker.drain(limit);
546
971
  }
547
972
  }
548
973
  //# sourceMappingURL=service.js.map