principles-disciple 1.43.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.
Files changed (44) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/commands/capabilities.ts +1 -1
  4. package/src/commands/context.ts +1 -1
  5. package/src/commands/export.ts +6 -6
  6. package/src/commands/nocturnal-train.ts +1 -1
  7. package/src/commands/thinking-os.ts +1 -1
  8. package/src/core/adaptive-thresholds.ts +3 -3
  9. package/src/core/config.ts +2 -2
  10. package/src/core/dictionary.ts +1 -1
  11. package/src/core/evolution-engine.ts +1 -1
  12. package/src/core/nocturnal-compliance.ts +7 -7
  13. package/src/core/path-resolver.ts +1 -1
  14. package/src/core/pd-task-reconciler.ts +1 -1
  15. package/src/core/pd-task-store.ts +1 -1
  16. package/src/core/profile.ts +1 -1
  17. package/src/core/promotion-gate.ts +0 -2
  18. package/src/core/replay-engine.ts +1 -1
  19. package/src/core/session-tracker.ts +1 -1
  20. package/src/core/trajectory.ts +0 -4
  21. package/src/core/workspace-context.ts +1 -1
  22. package/src/hooks/edit-verification.ts +1 -1
  23. package/src/hooks/prompt.ts +4 -7
  24. package/src/index.ts +8 -8
  25. package/src/service/central-health-service.ts +1 -1
  26. package/src/service/central-overview-service.ts +1 -1
  27. package/src/service/evolution-query-service.ts +2 -2
  28. package/src/service/evolution-worker.ts +4 -13
  29. package/src/service/health-query-service.ts +1 -1
  30. package/src/service/keyword-optimization-service.ts +24 -2
  31. package/src/service/nocturnal-runtime.ts +0 -3
  32. package/src/service/nocturnal-service.ts +8 -8
  33. package/src/service/queue-io.ts +5 -5
  34. package/src/service/sleep-cycle.ts +1 -1
  35. package/src/service/subagent-workflow/correction-observer-types.ts +13 -0
  36. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +5 -1
  37. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  38. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +1 -1
  39. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +1 -1
  40. package/src/service/subagent-workflow/workflow-store.ts +1 -1
  41. package/src/utils/file-lock.ts +1 -1
  42. package/src/utils/io.ts +1 -1
  43. package/src/utils/retry.ts +2 -2
  44. package/tests/core/model-deployment-registry.test.ts +9 -2
@@ -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.43.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.43.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",
@@ -14,7 +14,7 @@ const TOOLS_TO_SCAN = [
14
14
  { name: 'shellcheck', cmd: ['shellcheck', '--version'] },
15
15
  ];
16
16
 
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: third-party API execSync returns dynamic output - type structure unknown at call site
17
+
18
18
  function scanEnvironment(wctx: WorkspaceContext): any {
19
19
  const tools: Record<string, { available: boolean; version?: string }> = {};
20
20
 
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
@@ -46,12 +46,12 @@ export function handleExportCommand(ctx: PluginCommandContext): PluginCommandRes
46
46
 
47
47
  return {
48
48
  text: zh
49
- ? `已导出 ORPO 决策点样本到 ${result.manifest!.exportPath},` + // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
50
- `共 ${result.manifest!.sampleCount} 条,模型家族: ${result.manifest!.targetModelFamily},` + // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
51
- `数据集指纹: ${result.manifest!.datasetFingerprint.substring(0, 16)}...` // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
52
- : `Exported ORPO decision-point samples to ${result.manifest!.exportPath}, ` + // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
53
- `${result.manifest!.sampleCount} samples, target: ${result.manifest!.targetModelFamily}, ` + // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
54
- `dataset fingerprint: ${result.manifest!.datasetFingerprint.substring(0, 16)}...`, // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: caller guarantees manifest exists via success check
49
+ ? `已导出 ORPO 决策点样本到 ${result.manifest!.exportPath},` +
50
+ `共 ${result.manifest!.sampleCount} 条,模型家族: ${result.manifest!.targetModelFamily},` +
51
+ `数据集指纹: ${result.manifest!.datasetFingerprint.substring(0, 16)}...`
52
+ : `Exported ORPO decision-point samples to ${result.manifest!.exportPath}, ` +
53
+ `${result.manifest!.sampleCount} samples, target: ${result.manifest!.targetModelFamily}, ` +
54
+ `dataset fingerprint: ${result.manifest!.datasetFingerprint.substring(0, 16)}...`,
55
55
  };
56
56
  }
57
57
 
@@ -538,7 +538,7 @@ Next steps:
538
538
  }
539
539
  }
540
540
 
541
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: JSON.parse returns dynamic JSON - type unknown at parse time, narrowed via type narrowing below
541
+
542
542
  let result: any;
543
543
  try {
544
544
  result = JSON.parse(resultJson);
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
4
4
  import { WorkspaceContext } from '../core/workspace-context.js';
@@ -441,7 +441,7 @@ export function adjustThresholdsFromSignals(
441
441
  currentThresholds.principleAlignmentMin + adjustment,
442
442
  `High arbiter reject rate (${signals.arbiterRejectRate.toFixed(2)}) → tightening alignment threshold`
443
443
  );
444
- if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
444
+ if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) {
445
445
  bestResult = result;
446
446
  }
447
447
  }
@@ -455,7 +455,7 @@ export function adjustThresholdsFromSignals(
455
455
  currentThresholds.executabilityMin + adjustment,
456
456
  `High executability reject rate (${signals.executabilityRejectRate.toFixed(2)}) → tightening executability threshold`
457
457
  );
458
- if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
458
+ if (result.changed && (!bestResult.changed || ((result.newValue! - result.oldValue!) > 0))) {
459
459
  bestResult = result;
460
460
  }
461
461
  }
@@ -469,7 +469,7 @@ export function adjustThresholdsFromSignals(
469
469
  Math.max(currentThresholds.aggregateMin - reward, THRESHOLD_MIN),
470
470
  `Positive quality delta (${signals.qualityDelta.toFixed(2)}) → rewarding with slightly lower aggregate threshold`
471
471
  );
472
- if (result.changed && (!bestResult.changed || ((result.oldValue! - result.newValue!) > 0))) { // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: changed flag guarantees newValue/oldValue are defined
472
+ if (result.changed && (!bestResult.changed || ((result.oldValue! - result.newValue!) > 0))) {
473
473
  bestResult = result;
474
474
  }
475
475
  }
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { atomicWriteFileSync } from '../utils/io.js';
@@ -266,7 +266,7 @@ export class PainConfig {
266
266
  }
267
267
  }
268
268
 
269
- /* eslint-disable @typescript-eslint/no-explicit-any */
269
+
270
270
  // Reason: deepMerge handles arbitrary nested object structures where static typing cannot precisely capture recursive object shapes
271
271
  private deepMerge(target: any, source: any): any {
272
272
  const output = { ...target };
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { atomicWriteFileSync } from '../utils/io.js';
@@ -557,7 +557,7 @@ export function getEvolutionEngine(workspaceDir: string): EvolutionEngine {
557
557
  if (!_instances.has(resolved)) {
558
558
  _instances.set(resolved, new EvolutionEngine(resolved));
559
559
  }
560
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: set() above guarantees get() returns non-null
560
+
561
561
  return _instances.get(resolved)!;
562
562
  }
563
563
 
@@ -384,7 +384,7 @@ function detectT05Opportunity(session: SessionEvents): OpportunityMatch {
384
384
  if (RISKY_TOOLS.has(call.toolName)) return true;
385
385
  // Check bash for dangerous patterns
386
386
  if (call.toolName === 'bash' && call.errorMessage) {
387
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: call.errorMessage guard above ensures truthiness
387
+
388
388
  return DANGEROUS_BASH_PATTERNS.some((p) => p.test(call.errorMessage!));
389
389
  }
390
390
  return false;
@@ -432,7 +432,7 @@ function detectT06Opportunity(session: SessionEvents): OpportunityMatch {
432
432
  function detectT07Opportunity(session: SessionEvents): OpportunityMatch {
433
433
  const filePaths = session.toolCalls
434
434
  .filter((call) => call.filePath !== undefined)
435
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
435
+
436
436
  .map((call) => normalizePathPosix(call.filePath!));
437
437
  const uniqueFiles = new Set(filePaths);
438
438
  if (uniqueFiles.size >= 3) {
@@ -478,7 +478,7 @@ function detectT09Opportunity(session: SessionEvents): OpportunityMatch {
478
478
  const uniqueFiles = new Set(
479
479
  session.toolCalls
480
480
  .filter((call) => call.filePath !== undefined)
481
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
481
+
482
482
  .map((call) => normalizePathPosix(call.filePath!))
483
483
  );
484
484
  const hasComplexity = toolCallCount >= 5 || uniqueFiles.size >= 3;
@@ -592,7 +592,7 @@ function detectT01Violation(session: SessionEvents): ViolationMatch {
592
592
  const readFiles = new Set(
593
593
  session.toolCalls
594
594
  .filter((call) => READ_TOOLS.has(call.toolName) && call.filePath !== undefined)
595
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
595
+
596
596
  .map((call) => normalizePathPosix(call.filePath!))
597
597
  );
598
598
 
@@ -786,7 +786,7 @@ function detectT07Violation(session: SessionEvents): ViolationMatch {
786
786
  const modifiedFiles = new Set(
787
787
  session.toolCalls
788
788
  .filter((call) => EDIT_TOOLS.has(call.toolName) && call.filePath !== undefined)
789
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
789
+
790
790
  .map((call) => normalizePathPosix(call.filePath!))
791
791
  );
792
792
 
@@ -843,7 +843,7 @@ function detectT09Violation(session: SessionEvents): ViolationMatch {
843
843
  const uniqueFiles = new Set(
844
844
  session.toolCalls
845
845
  .filter((call) => call.filePath !== undefined)
846
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
846
+
847
847
  .map((call) => normalizePathPosix(call.filePath!))
848
848
  );
849
849
 
@@ -1086,7 +1086,7 @@ export function groupEventsIntoSessions(events: RawEventEntry[]): Map<string, Se
1086
1086
  });
1087
1087
  }
1088
1088
 
1089
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: set() above guarantees get() returns non-null
1089
+
1090
1090
  const session = sessionMap.get(sessionId)!;
1091
1091
 
1092
1092
  switch (event.type) {
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import * as fs from 'fs';
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import type { PDTaskSpec } from './pd-task-types.js';
@@ -57,7 +57,7 @@ export const PROFILE_DEFAULTS = {
57
57
  custom_guards: [] as { pattern: string; message: string; severity: string }[],
58
58
  };
59
59
 
60
- /* eslint-disable @typescript-eslint/no-explicit-any */
60
+
61
61
  // Reason: normalizeProfile handles arbitrary JSON profile shapes where static typing cannot capture runtime field existence
62
62
  export function normalizeProfile(rawProfile: any): any {
63
63
  const defaults = JSON.parse(JSON.stringify(PROFILE_DEFAULTS));
@@ -323,8 +323,6 @@ export function evaluatePromotionGate(
323
323
  ): PromotionGateResult {
324
324
  const {
325
325
  checkpointId,
326
-
327
- targetProfile: _targetProfile,
328
326
  baselineMetrics,
329
327
  minDelta = DEFAULT_MIN_DELTA,
330
328
  allowedMargin = DEFAULT_ALLOWED_MARGIN,
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { withLock } from '../utils/file-lock.js';
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { atomicWriteFileSync } from '../utils/io.js';
@@ -209,10 +209,6 @@ export class TrajectoryDatabase {
209
209
  recordToolCall(input: TrajectoryToolCallInput): number {
210
210
  this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
211
211
  const createdAt = input.createdAt ?? nowIso();
212
- // Extract filePath from paramsJson if provided and is an object with filePath
213
- const paramsObj = input.paramsJson as Record<string, unknown> | undefined;
214
-
215
- const _filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
216
212
  const rowId = this.withWrite(() => {
217
213
  const result = this.db.prepare(`
218
214
  INSERT INTO tool_calls (
@@ -173,7 +173,7 @@ export class WorkspaceContext {
173
173
  * Uses PathResolver to handle path normalization and fallback logic.
174
174
  * @throws Error if workspaceDir is missing and no fallback available.
175
175
  */
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: OpenClaw plugin framework hook context has dynamic shape - type not available
176
+
177
177
  static fromHookContext(ctx: any): WorkspaceContext {
178
178
  const {logger} = ctx;
179
179
  const log = (msg: string) => logger?.info?.(msg);
@@ -131,7 +131,7 @@ This is enforced by P-03 (精确匹配前验证原则).`;
131
131
  export function handleEditVerification(
132
132
  event: PluginHookBeforeToolCallEvent,
133
133
  wctx: WorkspaceContext,
134
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: logger is typed as any by plugin framework - type not available
134
+
135
135
  ctx: { logger?: any; sessionId?: string },
136
136
  config: EditVerificationConfig = {}
137
137
  ): PluginHookBeforeToolCallResult | void {
@@ -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
@@ -981,12 +982,8 @@ ${taskBlocks}${processingNote}
981
982
  const filePattern = /\b([a-zA-Z]:\\?[^\s,]+\.[a-z]{2,10}|[./][^\s,]+\.[a-z]{2,10})\b/gi;
982
983
  const toolMatches = toolPatterns.flatMap(({ pattern, tool }) => {
983
984
  const matches: string[] = [];
984
-
985
-
986
- let _m;
987
985
  const r = new RegExp(pattern.source, pattern.flags);
988
-
989
- while ((_m = r.exec(latestUserText)) !== null) matches.push(tool);
986
+ while (r.exec(latestUserText) !== null) matches.push(tool);
990
987
  return matches;
991
988
  });
992
989
  const fileMatches = latestUserText.match(filePattern) ?? [];
package/src/index.ts CHANGED
@@ -283,7 +283,7 @@ const plugin = {
283
283
 
284
284
  if (shouldRecordShadow) {
285
285
  const observation = recordShadowRouting(workspaceDir, {
286
- checkpointId: decision.activeCheckpointId!, // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: !!decision.activeCheckpointId guard above ensures truthiness
286
+ checkpointId: decision.activeCheckpointId!,
287
287
  workerProfile: agentId as WorkerProfile,
288
288
  taskFingerprint: computeRuntimeShadowTaskFingerprint(event),
289
289
  });
@@ -362,7 +362,7 @@ const plugin = {
362
362
 
363
363
  // ── Slash Commands ──
364
364
  // Register command with optional short alias
365
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
365
+
366
366
  const registerCommandWithAlias = (name: string, alias: string | null, desc: string, handler: any, opts?: { acceptsArgs?: boolean }) => {
367
367
  const base = {
368
368
  name,
@@ -380,17 +380,17 @@ const plugin = {
380
380
  }
381
381
  };
382
382
 
383
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+
384
384
  registerCommandWithAlias('pd-init', 'pdi', getCommandDescription('pd-init', language), (ctx: any) => handleInitStrategy(ctx));
385
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
+
386
386
  registerCommandWithAlias('pd-okr', 'pdk', getCommandDescription('pd-okr', language), (ctx: any) => handleManageOkr(ctx));
387
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
+
388
388
  registerCommandWithAlias('pd-bootstrap', 'pdb', getCommandDescription('pd-bootstrap', language), (ctx: any) => handleBootstrapTools(ctx));
389
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
389
+
390
390
  registerCommandWithAlias('pd-research', 'pdr', getCommandDescription('pd-research', language), (ctx: any) => handleResearchTools(ctx));
391
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
+
392
392
  registerCommandWithAlias('pd-thinking', 'pdt', getCommandDescription('pd-thinking', language), (ctx: any) => handleThinkingOs(ctx), { acceptsArgs: true });
393
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
393
+
394
394
  registerCommandWithAlias('pd-reflect', 'pdrl', getCommandDescription('pd-reflect', language), (ctx: any) => {
395
395
  try {
396
396
  // Resolve agentId from sessionKey (if available), fallback to 'main'
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import { getCentralDatabase } from './central-database.js';
3
3
  import { HealthQueryService } from './health-query-service.js';
4
4
 
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import { getCentralDatabase, type CentralDatabase } from './central-database.js';
3
3
  import { getThinkingModelDefinitions } from '../core/thinking-models.js';
4
4
  import type { OverviewResponse } from './control-ui-query-service.js';
@@ -349,13 +349,13 @@ export class EvolutionQueryService {
349
349
  for (const task of recentTasks) {
350
350
  const [createdDay] = task.createdAt.split('T');
351
351
  if (activityByDay.has(createdDay)) {
352
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: has() check guarantees entry exists
352
+
353
353
  activityByDay.get(createdDay)!.created++;
354
354
  }
355
355
  if (task.completedAt) {
356
356
  const [completedDay] = task.completedAt.split('T');
357
357
  if (activityByDay.has(completedDay)) {
358
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: has() check guarantees entry exists
358
+
359
359
  activityByDay.get(completedDay)!.completed++;
360
360
  }
361
361
  }
@@ -233,8 +233,6 @@ function buildFallbackNocturnalSnapshot(
233
233
  };
234
234
  }
235
235
 
236
- const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * 60 * 1000;
237
-
238
236
  // Queue lock constants and requireQueueLock are imported from queue-io.ts
239
237
 
240
238
  export function extractEvolutionTaskId(task: string): string | null {
@@ -284,13 +282,6 @@ export function purgeStaleFailedTasks(
284
282
  return { purged: purged.length, remaining: queue.length, byReason };
285
283
  }
286
284
 
287
- function normalizePainDedupKey(source: string, preview: string, reason?: string): string {
288
- // Include reason in dedup key to match createEvolutionTaskId() behavior
289
- // Different reasons for the same source/preview should create different tasks
290
- const normalizedReason = (reason || '').trim().toLowerCase();
291
- return `${source.trim().toLowerCase()}::${preview.trim().toLowerCase()}::${normalizedReason}`;
292
- }
293
-
294
285
 
295
286
 
296
287
  export function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number, reason?: string): boolean {
@@ -667,7 +658,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
667
658
  workspaceDir: wctx.workspaceDir,
668
659
  stateDir: wctx.stateDir,
669
660
  logger: api?.logger || logger,
670
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: api is guaranteed non-null in this recovery path where runtimeAdapter is required
661
+
671
662
  runtimeAdapter: new OpenClawTrinityRuntimeAdapter(api!),
672
663
  subagent: api?.runtime?.subagent,
673
664
  });
@@ -1185,7 +1176,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1185
1176
  let snapshotData: NocturnalSessionSnapshot | undefined;
1186
1177
 
1187
1178
  if (isPollingTask) {
1188
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: polling path requires existing resultRef
1179
+
1189
1180
  workflowId = sleepTask.resultRef!;
1190
1181
  } else {
1191
1182
  // Phase 1: Build trajectory snapshot for Nocturnal pipeline
@@ -1556,7 +1547,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1556
1547
  const manager = new CorrectionObserverWorkflowManager({
1557
1548
  workspaceDir: wctx.workspaceDir,
1558
1549
  logger,
1559
- subagent: api?.runtime?.subagent!, /* eslint-disable-line @typescript-eslint/no-non-null-assertion */
1550
+ subagent: api?.runtime?.subagent!,
1560
1551
  agentSession: api?.runtime?.agent?.session,
1561
1552
  });
1562
1553
 
@@ -1570,7 +1561,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1570
1561
  workflowId = handle.workflowId;
1571
1562
  koTask.resultRef = workflowId;
1572
1563
  } else {
1573
- workflowId = koTask.resultRef!; /* eslint-disable-line @typescript-eslint/no-non-null-assertion */
1564
+ workflowId = koTask.resultRef!;
1574
1565
  }
1575
1566
 
1576
1567
  // Poll workflow state
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { readPainFlagData } from '../core/pain.js';
@@ -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
  /**
@@ -390,9 +390,6 @@ export function checkCooldown(
390
390
  } = {}
391
391
  ): CooldownCheckResult {
392
392
  const {
393
-
394
- globalCooldownMs: _globalCooldownMs = DEFAULT_GLOBAL_COOLDOWN_MS,
395
- principleCooldownMs: _principleCooldownMs = DEFAULT_PRINCIPLE_COOLDOWN_MS,
396
393
  maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW,
397
394
  quotaWindowMs = DEFAULT_QUOTA_WINDOW_MS,
398
395
  } = options;
@@ -116,7 +116,7 @@ function incrementGeneratedSampleCount(stateDir: string, principleId: string): v
116
116
  state.generatedSampleCount += 1;
117
117
  setPrincipleState(stateDir, state);
118
118
  } catch (err) {
119
- // eslint-disable-next-line no-console -- Non-critical warning in helper function
119
+
120
120
  console.warn(`[nocturnal-service] Failed to sync generatedSampleCount for ${principleId}:`, err instanceof Error ? err.stack : err);
121
121
  }
122
122
  }
@@ -522,7 +522,7 @@ function persistCodeCandidate(
522
522
  try {
523
523
  refreshPrincipleLifecycle(workspaceDir, stateDir);
524
524
  } catch (err) {
525
- // eslint-disable-next-line no-console -- Non-critical warning in helper function
525
+
526
526
  console.warn('[nocturnal-service] Lifecycle refresh failed after code candidate persistence:', err instanceof Error ? err.stack : err);
527
527
  }
528
528
  return {
@@ -702,7 +702,7 @@ export function executeNocturnalReflection(
702
702
  ): NocturnalRunResult {
703
703
  // Use provided logger or fallback to console
704
704
  const logger = options.logger;
705
- // eslint-disable-next-line no-console -- Intentional console fallback when no logger provided
705
+
706
706
  const warn = logger?.warn?.bind(logger) ?? console.warn.bind(console);
707
707
 
708
708
  const diagnostics: NocturnalRunDiagnostics = {
@@ -882,7 +882,7 @@ export function executeNocturnalReflection(
882
882
  diagnostics,
883
883
  };
884
884
  }
885
- trinityArtifact = trinityResult.artifact!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: artifact is validated by validateTrinityDraft which returns valid: true when artifact exists
885
+ trinityArtifact = trinityResult.artifact!;
886
886
  // Convert Trinity draft to arbiter-compatible artifact
887
887
  const artifactData = draftToArtifact(trinityArtifact);
888
888
  rawJson = JSON.stringify(artifactData);
@@ -933,7 +933,7 @@ export function executeNocturnalReflection(
933
933
  diagnostics,
934
934
  };
935
935
  }
936
- trinityArtifact = trinityResult.artifact!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: artifact is validated by validateTrinityDraft which returns valid: true when artifact exists
936
+ trinityArtifact = trinityResult.artifact!;
937
937
  // Convert Trinity draft to arbiter-compatible artifact
938
938
  const artifactData = draftToArtifact(trinityArtifact);
939
939
  rawJson = JSON.stringify(artifactData);
@@ -1187,7 +1187,7 @@ async function executeNocturnalReflectionWithAdapter(
1187
1187
  ): Promise<NocturnalRunResult> {
1188
1188
  // Use provided logger or fallback to console
1189
1189
  const logger = options.logger;
1190
- // eslint-disable-next-line no-console -- Intentional console fallback when no logger provided
1190
+
1191
1191
  const warn = logger?.warn?.bind(logger) ?? console.warn.bind(console);
1192
1192
 
1193
1193
  const diagnostics: NocturnalRunDiagnostics = {
@@ -1387,7 +1387,7 @@ async function executeNocturnalReflectionWithAdapter(
1387
1387
  adjustThresholdsFromSignals(stateDir, { malformedRate: 1.0, arbiterRejectRate: 0.0, executabilityRejectRate: 0.0, qualityDelta: 0.0 });
1388
1388
  return { success: false, noTargetSelected: false, validationFailed: true, validationFailures: [`Trinity override failed: ${failures.join('; ')}`], snapshot, diagnostics };
1389
1389
  }
1390
- trinityArtifact = trinityResult.artifact!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: artifact is validated by validateTrinityDraft which returns valid: true when artifact exists
1390
+ trinityArtifact = trinityResult.artifact!;
1391
1391
  const artifactData = draftToArtifact(trinityArtifact);
1392
1392
  rawJson = JSON.stringify(artifactData);
1393
1393
  } else {
@@ -1414,7 +1414,7 @@ async function executeNocturnalReflectionWithAdapter(
1414
1414
  adjustThresholdsFromSignals(stateDir, { malformedRate: 1.0, arbiterRejectRate: 0.0, executabilityRejectRate: 0.0, qualityDelta: 0.0 });
1415
1415
  return { success: false, noTargetSelected: false, validationFailed: true, validationFailures: failures, snapshot, diagnostics };
1416
1416
  }
1417
- trinityArtifact = trinityResult.artifact!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Reason: artifact is validated by validateTrinityDraft which returns valid: true when artifact exists
1417
+ trinityArtifact = trinityResult.artifact!;
1418
1418
  const artifactData = draftToArtifact(trinityArtifact);
1419
1419
  rawJson = JSON.stringify(artifactData);
1420
1420
  } else {
@@ -160,9 +160,9 @@ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext
160
160
  }
161
161
  } catch (err) {
162
162
  // Best effort — non-fatal, but surface unexpected errors
163
- /* eslint-disable no-console */
163
+
164
164
  console.warn(`[queue-io] Failed to read pain context (non-fatal): ${String(err)}`);
165
- /* eslint-enable no-console */
165
+
166
166
  }
167
167
 
168
168
  return { mostRecent: null, recentPainCount: 0, recentMaxPainScore: 0 };
@@ -185,7 +185,7 @@ export function shouldSkipForDedup(
185
185
  const recentSimilarReflection = hasRecentSimilarReflection(queue, painSourceKey, now);
186
186
 
187
187
  if (recentSimilarReflection) {
188
- const completedTime = new Date(recentSimilarReflection.completed_at!).getTime(); /* eslint-disable-line @typescript-eslint/no-non-null-assertion */
188
+ const completedTime = new Date(recentSimilarReflection.completed_at!).getTime();
189
189
  logger?.debug?.(`[PD:EvolutionWorker] Skipping sleep_reflection — similar reflection completed ${Math.round((now - completedTime) / 60000)}min ago (same pain pattern: ${painSourceKey})`);
190
190
  return true;
191
191
  }
@@ -358,9 +358,9 @@ export function loadEvolutionQueue(queuePath: string): EvolutionQueueItem[] {
358
358
  rawQueue = [];
359
359
  } else {
360
360
  // Corrupted JSON or other read error — warn and recover with empty queue
361
- /* eslint-disable no-console */
361
+
362
362
  console.warn(`[queue-io] Failed to load evolution queue (recovering with empty): ${String(err)}`);
363
- /* eslint-enable no-console */
363
+
364
364
  rawQueue = [];
365
365
  }
366
366
  }
@@ -71,7 +71,7 @@ export interface CycleOptions {
71
71
  * @param options.heartbeatCounterRef — mutable counter, incremented by runCycle
72
72
  */
73
73
  export async function runCycle(options: CycleOptions): Promise<WorkerStatusReport> {
74
- const { wctx, logger, eventLog: _eventLog, api: _api, heartbeatCounterRef } = options;
74
+ const { wctx, logger, api: _api, heartbeatCounterRef } = options;
75
75
  const cycleStart = Date.now();
76
76
  heartbeatCounterRef.value++;
77
77
 
@@ -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
 
@@ -130,7 +130,7 @@ export const deepReflectWorkflowSpec: SubagentWorkflowSpec<DeepReflectResult> =
130
130
  } else if (Array.isArray(lastMessage?.content)) {
131
131
  insights = (lastMessage.content as { type?: string; text?: string }[])
132
132
  .filter((c) => c?.type === 'text' && typeof c.text === 'string')
133
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures c.text is a string
133
+
134
134
  .map((c) => c.text!)
135
135
  .join('\n');
136
136
  }
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import type { PluginLogger } from '../../openclaw-sdk.js';
3
3
  import type {
4
4
  SubagentWorkflowSpec,
@@ -453,7 +453,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
453
453
 
454
454
  async sweepExpiredWorkflows(
455
455
  maxAgeMs = 30 * 60 * 1000,
456
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: subagentRuntime param is intentionally any for backward compatibility with callers
456
+
457
457
  subagentRuntime?: any,
458
458
 
459
459
  agentSession?: {
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  import Database from 'better-sqlite3';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
@@ -352,7 +352,7 @@ export async function withAsyncLock<T>(
352
352
  try {
353
353
  return await fn();
354
354
  } finally {
355
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: resolveRelease is assigned in Promise constructor before finally runs
355
+
356
356
  resolveRelease!();
357
357
  // 清理已完成的队列
358
358
  if (asyncLockQueues.get(lockPath) === currentQueue) {
package/src/utils/io.ts CHANGED
@@ -126,7 +126,7 @@ export function parseKvLines(text: string): Record<string, string> {
126
126
  return result;
127
127
  }
128
128
 
129
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: serializeKvLines handles arbitrary object shapes for kv line serialization
129
+
130
130
  export function serializeKvLines(data: Record<string, any>): string {
131
131
  const lines: string[] = [];
132
132
  const keys = Object.keys(data).sort();
@@ -424,7 +424,7 @@ export function computeDynamicTimeout(
424
424
  if (history.length < MIN_SAMPLES) {
425
425
  // Not enough data — use the spec's static timeout
426
426
  const fallback = clampTimeout(defaultTimeout);
427
- // eslint-disable-next-line no-console -- Monitoring output for fallback diagnostics
427
+
428
428
  console.info(`[PD:DynamicTimeout] Insufficient samples (${history.length} < ${MIN_SAMPLES}) for '${workflowType}', falling back to static timeout: ${fallback}ms`);
429
429
  return fallback;
430
430
  }
@@ -432,7 +432,7 @@ export function computeDynamicTimeout(
432
432
  const p95 = percentile(history, 95);
433
433
  const adaptive = p95 * SAFETY_MULTIPLIER;
434
434
  const result = clampTimeout(adaptive);
435
- // eslint-disable-next-line no-console -- Monitoring output for adaptive timeout diagnostics
435
+
436
436
  console.info(`[PD:DynamicTimeout] Computed adaptive timeout for '${workflowType}': P95=${p95}ms (from ${history.length} samples) × ${SAFETY_MULTIPLIER} = ${result}ms`);
437
437
  return result;
438
438
  }
@@ -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', () => {