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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/export.ts +6 -6
- package/src/commands/nocturnal-train.ts +1 -1
- package/src/commands/thinking-os.ts +1 -1
- package/src/core/adaptive-thresholds.ts +3 -3
- package/src/core/config.ts +2 -2
- package/src/core/dictionary.ts +1 -1
- package/src/core/evolution-engine.ts +1 -1
- package/src/core/nocturnal-compliance.ts +7 -7
- package/src/core/path-resolver.ts +1 -1
- package/src/core/pd-task-reconciler.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/profile.ts +1 -1
- package/src/core/promotion-gate.ts +0 -2
- package/src/core/replay-engine.ts +1 -1
- package/src/core/session-tracker.ts +1 -1
- package/src/core/trajectory.ts +0 -4
- package/src/core/workspace-context.ts +1 -1
- package/src/hooks/edit-verification.ts +1 -1
- package/src/hooks/prompt.ts +4 -7
- package/src/index.ts +8 -8
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +1 -1
- package/src/service/evolution-query-service.ts +2 -2
- package/src/service/evolution-worker.ts +4 -13
- package/src/service/health-query-service.ts +1 -1
- package/src/service/keyword-optimization-service.ts +24 -2
- package/src/service/nocturnal-runtime.ts +0 -3
- package/src/service/nocturnal-service.ts +8 -8
- package/src/service/queue-io.ts +5 -5
- package/src/service/sleep-cycle.ts +1 -1
- package/src/service/subagent-workflow/correction-observer-types.ts +13 -0
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +5 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/workflow-store.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +1 -1
- package/src/utils/retry.ts +2 -2
- package/tests/core/model-deployment-registry.test.ts +9 -2
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ const TOOLS_TO_SCAN = [
|
|
|
14
14
|
{ name: 'shellcheck', cmd: ['shellcheck', '--version'] },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
function scanEnvironment(wctx: WorkspaceContext): any {
|
|
19
19
|
const tools: Record<string, { available: boolean; version?: string }> = {};
|
|
20
20
|
|
package/src/commands/context.ts
CHANGED
package/src/commands/export.ts
CHANGED
|
@@ -46,12 +46,12 @@ export function handleExportCommand(ctx: PluginCommandContext): PluginCommandRes
|
|
|
46
46
|
|
|
47
47
|
return {
|
|
48
48
|
text: zh
|
|
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)}...`,
|
|
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
|
-
|
|
541
|
+
|
|
542
542
|
let result: any;
|
|
543
543
|
try {
|
|
544
544
|
result = JSON.parse(resultJson);
|
|
@@ -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))) {
|
|
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))) {
|
|
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))) {
|
|
472
|
+
if (result.changed && (!bestResult.changed || ((result.oldValue! - result.newValue!) > 0))) {
|
|
473
473
|
bestResult = result;
|
|
474
474
|
}
|
|
475
475
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 };
|
package/src/core/dictionary.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
+
|
|
1090
1090
|
const session = sessionMap.get(sessionId)!;
|
|
1091
1091
|
|
|
1092
1092
|
switch (event.type) {
|
package/src/core/profile.ts
CHANGED
|
@@ -57,7 +57,7 @@ export const PROFILE_DEFAULTS = {
|
|
|
57
57
|
custom_guards: [] as { pattern: string; message: string; severity: string }[],
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
|
|
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));
|
package/src/core/trajectory.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
134
|
+
|
|
135
135
|
ctx: { logger?: any; sessionId?: string },
|
|
136
136
|
config: EditVerificationConfig = {}
|
|
137
137
|
): PluginHookBeforeToolCallResult | void {
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -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!,
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
|
|
384
384
|
registerCommandWithAlias('pd-init', 'pdi', getCommandDescription('pd-init', language), (ctx: any) => handleInitStrategy(ctx));
|
|
385
|
-
|
|
385
|
+
|
|
386
386
|
registerCommandWithAlias('pd-okr', 'pdk', getCommandDescription('pd-okr', language), (ctx: any) => handleManageOkr(ctx));
|
|
387
|
-
|
|
387
|
+
|
|
388
388
|
registerCommandWithAlias('pd-bootstrap', 'pdb', getCommandDescription('pd-bootstrap', language), (ctx: any) => handleBootstrapTools(ctx));
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
registerCommandWithAlias('pd-research', 'pdr', getCommandDescription('pd-research', language), (ctx: any) => handleResearchTools(ctx));
|
|
391
|
-
|
|
391
|
+
|
|
392
392
|
registerCommandWithAlias('pd-thinking', 'pdt', getCommandDescription('pd-thinking', language), (ctx: any) => handleThinkingOs(ctx), { acceptsArgs: true });
|
|
393
|
-
|
|
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'
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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!,
|
|
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!;
|
|
1564
|
+
workflowId = koTask.resultRef!;
|
|
1574
1565
|
}
|
|
1575
1566
|
|
|
1576
1567
|
// Poll workflow state
|
|
@@ -31,12 +31,13 @@ export class KeywordOptimizationService {
|
|
|
31
31
|
applyResult(result: CorrectionObserverResult): void {
|
|
32
32
|
const learner = CorrectionCueLearner.get(this.stateDir);
|
|
33
33
|
|
|
34
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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!;
|
|
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!;
|
|
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
|
-
|
|
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!;
|
|
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!;
|
|
1417
|
+
trinityArtifact = trinityResult.artifact!;
|
|
1418
1418
|
const artifactData = draftToArtifact(trinityArtifact);
|
|
1419
1419
|
rawJson = JSON.stringify(artifactData);
|
|
1420
1420
|
} else {
|
package/src/service/queue-io.ts
CHANGED
|
@@ -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
|
-
|
|
163
|
+
|
|
164
164
|
console.warn(`[queue-io] Failed to read pain context (non-fatal): ${String(err)}`);
|
|
165
|
-
|
|
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();
|
|
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
|
-
|
|
361
|
+
|
|
362
362
|
console.warn(`[queue-io] Failed to load evolution queue (recovering with empty): ${String(err)}`);
|
|
363
|
-
|
|
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,
|
|
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
|
-
|
|
133
|
+
|
|
134
134
|
.map((c) => c.text!)
|
|
135
135
|
.join('\n');
|
|
136
136
|
}
|
|
@@ -453,7 +453,7 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
453
453
|
|
|
454
454
|
async sweepExpiredWorkflows(
|
|
455
455
|
maxAgeMs = 30 * 60 * 1000,
|
|
456
|
-
|
|
456
|
+
|
|
457
457
|
subagentRuntime?: any,
|
|
458
458
|
|
|
459
459
|
agentSession?: {
|
package/src/utils/file-lock.ts
CHANGED
|
@@ -352,7 +352,7 @@ export async function withAsyncLock<T>(
|
|
|
352
352
|
try {
|
|
353
353
|
return await fn();
|
|
354
354
|
} finally {
|
|
355
|
-
|
|
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
|
-
|
|
129
|
+
|
|
130
130
|
export function serializeKvLines(data: Record<string, any>): string {
|
|
131
131
|
const lines: string[] = [];
|
|
132
132
|
const keys = Object.keys(data).sort();
|
package/src/utils/retry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
382
|
-
|
|
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', () => {
|