principles-disciple 1.34.0 → 1.34.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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.34.0",
5
+ "version": "1.34.1",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -13,13 +13,15 @@
13
13
 
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import {
16
+ import type {
17
17
  CorrectionKeyword,
18
18
  CorrectionKeywordStore,
19
- CorrectionMatchResult,
19
+ CorrectionMatchResult} from './correction-types.js';
20
+ import {
20
21
  CORRECTION_SEED_KEYWORDS,
21
22
  MAX_CORRECTION_KEYWORDS,
22
23
  } from './correction-types.js';
24
+ import { checkCooldown, recordCooldown } from '../service/nocturnal-runtime.js';
23
25
 
24
26
  const KEYWORD_STORE_FILE = 'correction_keywords.json';
25
27
 
@@ -128,8 +130,8 @@ export function _resetCorrectionCueLearnerInstance(): void {
128
130
  // =========================================================================
129
131
 
130
132
  export class CorrectionCueLearner {
131
- private store: CorrectionKeywordStore;
132
- private stateDir: string;
133
+ private readonly store: CorrectionKeywordStore;
134
+ private readonly stateDir: string;
133
135
 
134
136
  constructor(stateDir: string) {
135
137
  this.stateDir = stateDir;
@@ -142,7 +144,7 @@ export class CorrectionCueLearner {
142
144
  * Checks whether text contains a correction cue (D-11).
143
145
  * Normalisation is equivalent to the original detectCorrectionCue():
144
146
  * trim → lowercase → strip punctuation
145
- * Returns the first matched term only (first-match semantics).
147
+ * Returns weighted score based on keyword accuracy (D-39-03, D-39-04).
146
148
  */
147
149
  match(text: string): CorrectionMatchResult {
148
150
  const normalized = text
@@ -150,13 +152,98 @@ export class CorrectionCueLearner {
150
152
  .toLowerCase()
151
153
  .replace(/[.,!?;:,。!?;:]/g, '');
152
154
 
155
+ const matchedTerms: string[] = [];
156
+ let totalScore = 0;
157
+
153
158
  for (const keyword of this.store.keywords) {
154
159
  if (normalized.includes(keyword.term.toLowerCase())) {
155
- return { matched: true, matchedTerms: [keyword.term], score: keyword.weight, confidence: 0.9 };
160
+ // D-39-03, D-39-04: Weighted score formula
161
+ // score = weight x ((TP + 1) / (TP + FP + 2))
162
+ // +2 smoothing: new keywords (TP=0, FP=0) get accuracy=0.5
163
+ const tp = keyword.truePositiveCount ?? 0;
164
+ const fp = keyword.falsePositiveCount ?? 0;
165
+ const accuracy = (tp + 1) / (tp + fp + 2);
166
+ const score = keyword.weight * accuracy;
167
+
168
+ totalScore += score;
169
+ matchedTerms.push(keyword.term);
170
+
171
+ // Increment hitCount
172
+ keyword.hitCount = (keyword.hitCount ?? 0) + 1;
173
+ keyword.lastHitAt = new Date().toISOString();
156
174
  }
157
175
  }
158
176
 
159
- return { matched: false, matchedTerms: [], score: 0.0, confidence: 0.0 };
177
+ const cappedScore = Math.min(1, totalScore);
178
+ const isMatched = matchedTerms.length > 0;
179
+
180
+ // D-39-04: Confidence derived from multiple signals
181
+ const termConfidence = Math.min(1, matchedTerms.length / 3);
182
+ const scoreConfidence = Math.min(1, cappedScore / 0.8);
183
+ const confidence = Math.max(termConfidence, scoreConfidence);
184
+
185
+ return {
186
+ matched: isMatched,
187
+ matchedTerms: matchedTerms.slice(0, 5),
188
+ score: cappedScore,
189
+ confidence,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Records a confirmed true positive for the given keyword term.
195
+ * Increments both hitCount and truePositiveCount.
196
+ */
197
+ recordTruePositive(term: string): void {
198
+ const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
199
+ if (!keyword) return;
200
+
201
+ keyword.truePositiveCount = (keyword.truePositiveCount ?? 0) + 1;
202
+ keyword.hitCount = (keyword.hitCount ?? 0) + 1;
203
+ keyword.lastHitAt = new Date().toISOString();
204
+
205
+ this.flush();
206
+ }
207
+
208
+ /**
209
+ * Records a confirmed false positive for the given keyword term.
210
+ * CORR-10: Decreases keyword weight by 20% (x0.8 multiplicative factor).
211
+ */
212
+ recordFalsePositive(term: string): void {
213
+ const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
214
+ if (!keyword) return;
215
+
216
+ keyword.falsePositiveCount = (keyword.falsePositiveCount ?? 0) + 1;
217
+ keyword.hitCount = (keyword.hitCount ?? 0) + 1;
218
+
219
+ // D-39-15: Multiplicative weight decay x0.8 on confirmed FP
220
+ keyword.weight = Math.max(0.1, keyword.weight * 0.8);
221
+ keyword.lastHitAt = new Date().toISOString();
222
+
223
+ this.flush();
224
+ }
225
+
226
+ /**
227
+ * Returns true if optimization is allowed (within daily throttle limit).
228
+ * CORR-08: Max 4 optimizations per day across all triggers.
229
+ */
230
+ canRunKeywordOptimization(): boolean {
231
+ // D-39-12, D-39-13: Per-workspace throttle, 4 calls/day
232
+ const cooldown = checkCooldown(this.stateDir, 'keyword_optimization', {
233
+ maxRunsPerWindow: 4,
234
+ quotaWindowMs: 24 * 60 * 60 * 1000,
235
+ });
236
+ return !cooldown.globalCooldownActive && !cooldown.quotaExhausted;
237
+ }
238
+
239
+ /**
240
+ * Records that an optimization was performed.
241
+ * Increments the daily throttle counter and updates lastOptimizedAt.
242
+ */
243
+ async recordOptimizationPerformed(): Promise<void> {
244
+ await recordCooldown(this.stateDir, 24 * 60 * 60 * 1000);
245
+ this.store.lastOptimizedAt = new Date().toISOString();
246
+ this.flush();
160
247
  }
161
248
 
162
249
  /**
@@ -177,11 +264,48 @@ export class CorrectionCueLearner {
177
264
  this.flush();
178
265
  }
179
266
 
267
+ /**
268
+ * Updates the weight of an existing keyword.
269
+ * Weight is clamped to 0.1-0.9 range.
270
+ * Throws if keyword not found.
271
+ */
272
+ updateWeight(term: string, weight: number): void {
273
+ const idx = this.store.keywords.findIndex(
274
+ k => k.term.toLowerCase() === term.toLowerCase()
275
+ );
276
+ if (idx < 0) {
277
+ throw new Error(`Keyword not found: ${term}`);
278
+ }
279
+
280
+ this.store.keywords[idx].weight = Math.max(0.1, Math.min(0.9, weight));
281
+ this.flush();
282
+ }
283
+
284
+ /**
285
+ * Removes a keyword from the store by term.
286
+ * Throws if keyword not found.
287
+ */
288
+ remove(term: string): void {
289
+ const idx = this.store.keywords.findIndex(
290
+ k => k.term.toLowerCase() === term.toLowerCase()
291
+ );
292
+ if (idx < 0) {
293
+ throw new Error(`Keyword not found: ${term}`);
294
+ }
295
+ this.store.keywords.splice(idx, 1);
296
+ this.flush();
297
+ }
298
+
180
299
  /** Returns a reference to the in-memory store. */
181
300
  getStore(): CorrectionKeywordStore {
182
301
  return this.store;
183
302
  }
184
303
 
304
+ /** Returns the lastOptimizedAt timestamp. */
305
+ getLastOptimizedAt(): string {
306
+ return this.store.lastOptimizedAt;
307
+ }
308
+
185
309
  /** Persists the current in-memory store to disk atomically. */
186
310
  flush(): void {
187
311
  saveCorrectionKeywordStore(this.stateDir, this.store);
@@ -117,7 +117,7 @@ export interface TrajectorySessionInput {
117
117
  }
118
118
 
119
119
  // V2: Task kind and priority types for queue schema
120
- export type TaskKind = 'pain_diagnosis' | 'sleep_reflection' | 'model_eval';
120
+ export type TaskKind = 'pain_diagnosis' | 'sleep_reflection' | 'model_eval' | 'keyword_optimization';
121
121
  export type TaskPriority = 'high' | 'medium' | 'low';
122
122
 
123
123
  // V2: EvolutionTaskInput with all V2 fields
@@ -29,6 +29,17 @@ export interface CorrectionObserverPayload {
29
29
  };
30
30
  /** Recent user messages for pattern analysis */
31
31
  recentMessages: string[];
32
+
33
+ /**
34
+ * Trajectory history: user turns where correctionDetected=true (D-40-08).
35
+ * Includes term matched, timestamp, sessionId for FPR trend analysis.
36
+ */
37
+ trajectoryHistory: Array<{
38
+ sessionId: string;
39
+ timestamp: string;
40
+ term: string;
41
+ userMessage: string;
42
+ }>;
32
43
  }
33
44
 
34
45
  /**
@@ -21,7 +21,6 @@ import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
21
21
  import type {
22
22
  CorrectionObserverPayload,
23
23
  CorrectionObserverResult,
24
- CorrectionObserverWorkflowSpec,
25
24
  } from './correction-observer-types.js';
26
25
 
27
26
  const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-correction-';
@@ -92,7 +91,7 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
92
91
 
93
92
  buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
94
93
  const payload = taskInput as CorrectionObserverPayload;
95
- const { keywordStoreSummary, recentMessages } = payload;
94
+ const { keywordStoreSummary, recentMessages, trajectoryHistory } = payload;
96
95
 
97
96
  const termsList = keywordStoreSummary.terms
98
97
  .map(t => ` - term="${t.term}", weight=${t.weight}, hits=${t.hitCount}, TP=${t.truePositiveCount}, FP=${t.falsePositiveCount}`)
@@ -102,6 +101,11 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
102
101
  ? recentMessages.map(m => ` - ${JSON.stringify(m)}`).join('\n')
103
102
  : ' (none)';
104
103
 
104
+ const trajectory = trajectoryHistory.length > 0
105
+ ? trajectoryHistory.map(t => ` - [${t.sessionId}] ${t.term} (${t.timestamp}): ${t.userMessage.substring(0, 80)}`)
106
+ .join('\n')
107
+ : ' (none)';
108
+
105
109
  return [
106
110
  'You are a correction keyword optimizer.',
107
111
  '',
@@ -115,6 +119,9 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
115
119
  '## Recent User Messages (' + recentMessages.length + ' messages):',
116
120
  messages,
117
121
  '',
122
+ '## Correction Trajectory (recent confirmed corrections, D-40-08):',
123
+ trajectory,
124
+ '',
118
125
  '## Rules:',
119
126
  '- ADD: If a correction pattern is detected in messages but not in store',
120
127
  '- UPDATE: If a term\'s weight should change based on TP/FP ratio',
@@ -187,7 +194,7 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
187
194
  return super.startWorkflow(spec, options);
188
195
  }
189
196
 
190
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
197
+
191
198
  protected override createWorkflowMetadata<TResult>(
192
199
  spec: SubagentWorkflowSpec<TResult>,
193
200
  options: {
@@ -207,6 +214,28 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
207
214
  ...options.metadata,
208
215
  };
209
216
  }
217
+
218
+ /**
219
+ * Retrieves and parses the workflow result for a completed workflow.
220
+ * Called by evolution-worker.ts after getWorkflowDebugSummary reports state=completed.
221
+ */
222
+ async getWorkflowResult(workflowId: string): Promise<CorrectionObserverResult | null> {
223
+ const workflow = this.store.getWorkflow(workflowId);
224
+ if (!workflow) return null;
225
+
226
+ const result = await this.driver.getResult({
227
+ sessionKey: workflow.child_session_key,
228
+ limit: 20,
229
+ });
230
+
231
+ const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
232
+ return correctionObserverWorkflowSpec.parseResult({
233
+ messages: result.messages,
234
+ assistantTexts: result.assistantTexts,
235
+ metadata,
236
+ waitStatus: 'ok',
237
+ });
238
+ }
210
239
  }
211
240
 
212
241
  // ── Factory ─────────────────────────────────────────────────────────────────