principles-disciple 1.44.0 → 1.45.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.
@@ -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.44.0",
5
+ "version": "1.45.0",
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.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -402,9 +402,10 @@ export async function handleBeforePromptBuild(
402
402
  learner.flush();
403
403
  }
404
404
  }
405
- } catch {
406
- // Fallback to hardcoded detection if learner fails
405
+ } catch (learnerErr) {
406
+ // Fallback to hardcoded detection if learner fails — log for observability
407
407
  correctionCue = detectCorrectionCue(userText);
408
+ logger?.warn?.(`[PD:Prompt] CorrectionCueLearner.match() failed (${String(learnerErr)}), fallback=${correctionCue ? `matched="${correctionCue}"` : 'no-match'}`);
408
409
  }
409
410
  let referencesAssistantTurnId: number | null = null;
410
411
  const hasPriorAssistant = event.messages
@@ -31,12 +31,13 @@ export class KeywordOptimizationService {
31
31
  applyResult(result: CorrectionObserverResult): void {
32
32
  const learner = CorrectionCueLearner.get(this.stateDir);
33
33
 
34
- if (!result.updated || !result.updates) {
34
+ const updates = result.updates ?? {};
35
+ if (!result.updated || Object.keys(updates).length === 0) {
35
36
  this.logger?.info?.('[KeywordOptimizationService] No updates to apply');
36
37
  return;
37
38
  }
38
39
 
39
- for (const [term, update] of Object.entries(result.updates)) {
40
+ for (const [term, update] of Object.entries(updates)) {
40
41
  try {
41
42
  switch (update.action) {
42
43
  case 'add': {
@@ -74,6 +75,27 @@ export class KeywordOptimizationService {
74
75
  this.logger?.warn?.(`[KeywordOptimizationService] ${update.action.toUpperCase()} failed for term="${term}": ${String(opErr)}`);
75
76
  }
76
77
  }
78
+
79
+ // H-1: Record confirmed false positives — terms where correctionDetected fired
80
+ // but trajectory analysis shows user wasn't actually expressing frustration.
81
+ if (result.fpAnalysisStatus === 'completed' && result.fpTerms && result.fpTerms.length > 0) {
82
+ // Normalize: trim, lowercase, dedupe, sanity cap
83
+ const MAX_FP_TERMS = 20;
84
+ const normalizedFpTerms = [...new Set(
85
+ result.fpTerms
86
+ .map(t => t.trim().toLowerCase())
87
+ .filter(t => t.length > 0)
88
+ )].slice(0, MAX_FP_TERMS);
89
+
90
+ for (const term of normalizedFpTerms) {
91
+ try {
92
+ learner.recordFalsePositive(term);
93
+ this.logger?.info?.(`[KeywordOptimizationService] FP recorded for term="${term}" (weight x0.8)`);
94
+ } catch (fpErr) {
95
+ this.logger?.warn?.(`[KeywordOptimizationService} recordFalsePositive failed for term="${term}": ${String(fpErr)}`);
96
+ }
97
+ }
98
+ }
77
99
  }
78
100
 
79
101
  /**
@@ -55,6 +55,19 @@ export interface CorrectionObserverResult {
55
55
  falsePositiveRate?: number;
56
56
  reasoning: string;
57
57
  }>;
58
+ /**
59
+ * Terms identified as false positives — user message didn't actually indicate
60
+ * frustration/correction despite correctionDetected firing for these terms.
61
+ * CORR-10 / H-1: Calling recordFalsePositive() decays weight by x0.8 per term.
62
+ * Only meaningful when fpAnalysisStatus='completed'.
63
+ */
64
+ fpTerms?: string[];
65
+ /**
66
+ * Whether FP analysis was performed. 'skipped' means the LLM did not run
67
+ * trajectory analysis (e.g., trajectory was empty). 'completed' means fpTerms
68
+ * contains the LLM's FP findings (may be empty if no FPs were found).
69
+ */
70
+ fpAnalysisStatus?: 'completed' | 'skipped';
58
71
  /** Human-readable summary */
59
72
  summary: string;
60
73
  }
@@ -115,6 +115,7 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
115
115
  '## TASK',
116
116
  'Analyze the current correction keyword store and recent user messages.',
117
117
  'Recommend ADD/UPDATE/REMOVE actions to improve correction cue accuracy.',
118
+ 'Also identify terms that triggered false positives (correctionDetected fired but user message doesn\'t indicate actual frustration).',
118
119
  '',
119
120
  '## Current Keyword Store (' + keywordStoreSummary.totalKeywords + ' terms):',
120
121
  termsList,
@@ -129,11 +130,14 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
129
130
  '- ADD: If a correction pattern is detected in messages but not in store',
130
131
  '- UPDATE: If a term\'s weight should change based on TP/FP ratio',
131
132
  '- REMOVE: If a term has 0 hits after many uses AND high false positive rate (>0.3)',
133
+ '- FALSE POSITIVE: If a term appears in trajectory but the user message doesn\'t actually express frustration (e.g., user said "wrong" but in a factual context, not emotional)',
134
+ '- fpAnalysisStatus: set to "completed" if you performed trajectory analysis (even if no FPs found), or "skipped" if trajectory was empty/unavailable',
132
135
  '- Keep reasoning concise (max 100 chars)',
133
136
  '- Weight range: 0.1-0.9',
134
137
  '',
135
138
  'Return strict JSON (no markdown):',
136
- '{"updated": boolean, "updates": {...}, "summary": string}',
139
+ '{"updated": boolean, "updates": {...}, "fpTerms": ["term1", ...], "fpAnalysisStatus": "completed" | "skipped", "summary": string}',
140
+ 'Note: fpTerms is optional — only include if you identified clear false positives.',
137
141
  ].join('\n');
138
142
  },
139
143
 
@@ -378,8 +378,15 @@ describe('ModelDeploymentRegistry getDeployment / listDeployments', () => {
378
378
 
379
379
  const deployments = listDeployments(tmpDir);
380
380
  expect(deployments).toHaveLength(2);
381
- // Most recently updated is last in the list (sorted asc by updatedAt in memory)
382
- expect(deployments[0].workerProfile).toBe('local-editor'); // bound second
381
+ // Verify sort order is descending by updatedAt (most recent first)
382
+ const [first, second] = deployments;
383
+ const firstUpdated = new Date(first.updatedAt).getTime();
384
+ const secondUpdated = new Date(second.updatedAt).getTime();
385
+ expect(firstUpdated).toBeGreaterThanOrEqual(secondUpdated);
386
+ // Also verify both expected profiles are present (order-independent)
387
+ const profiles = deployments.map(d => d.workerProfile);
388
+ expect(profiles).toContain('local-reader');
389
+ expect(profiles).toContain('local-editor');
383
390
  });
384
391
 
385
392
  it('listDeployments filters by workerProfile', () => {