principles-disciple 1.44.0 → 1.46.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 +4 -4
- package/package.json +1 -1
- package/src/core/evolution-reducer.ts +45 -1
- package/src/core/pain.ts +7 -3
- package/src/core/principle-tree-ledger.ts +19 -1
- package/src/core/trajectory.ts +16 -8
- package/src/hooks/pain.ts +14 -9
- package/src/hooks/prompt.ts +3 -2
- package/src/service/evolution-worker.ts +15 -2
- package/src/service/keyword-optimization-service.ts +24 -2
- package/src/service/queue-io.ts +5 -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/tests/core/model-deployment-registry.test.ts +9 -2
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.46.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "9ae5cc1407da",
|
|
80
|
+
"bundleMd5": "68bc85f0121a780b83931b7ed5491b97",
|
|
81
|
+
"builtAt": "2026-04-16T02:19:50.219Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
PrincipleSuggestedRule,
|
|
22
22
|
} from './evolution-types.js';
|
|
23
23
|
import { isCompleteDetectorMetadata } from './evolution-types.js';
|
|
24
|
-
import { updateTrainingStore } from './principle-tree-ledger.js';
|
|
24
|
+
import { updateTrainingStore, addPrincipleToLedger, type LedgerPrinciple } from './principle-tree-ledger.js';
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
export interface EvolutionReducer {
|
|
@@ -381,6 +381,50 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
381
381
|
SystemLogger.log(this.workspaceDir, 'PRINCIPLE_SYNC_WARN', `Principle ${principleId} created in memory but failed to sync to PRINCIPLES.md — manual file check required`);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
// Add to ledger tree so the compiler can find this principle.
|
|
385
|
+
// Without this, newly diagnosed principles are invisible to the compiler
|
|
386
|
+
// (which reads tree.principles, not trainingStore or PRINCIPLES.md).
|
|
387
|
+
if (this.stateDir) {
|
|
388
|
+
try {
|
|
389
|
+
// Build a LedgerPrinciple (tree schema) from the evolution-types Principle.
|
|
390
|
+
// Tree schema Principle does NOT have: source, guardrails, contextTags, validation,
|
|
391
|
+
// feedbackScore, usageCount, activatedAt, abstractedPrinciple, valueMetrics.
|
|
392
|
+
// Pain source info is stored via derivedFromPainIds (which the compiler uses).
|
|
393
|
+
const ledgerPrinciple: LedgerPrinciple = {
|
|
394
|
+
id: principle.id,
|
|
395
|
+
version: principle.version,
|
|
396
|
+
text: principle.text,
|
|
397
|
+
triggerPattern: principle.trigger,
|
|
398
|
+
action: principle.action,
|
|
399
|
+
status: principle.status,
|
|
400
|
+
evaluability: principle.evaluability,
|
|
401
|
+
coreAxiomId: principle.coreAxiomId,
|
|
402
|
+
priority: principle.priority ?? 'P1',
|
|
403
|
+
scope: principle.scope ?? 'general',
|
|
404
|
+
domain: principle.domain,
|
|
405
|
+
suggestedRules: principle.suggestedRules?.map((r) => r.name),
|
|
406
|
+
detectorMetadata: principle.detectorMetadata,
|
|
407
|
+
deprecatedAt: principle.deprecatedAt,
|
|
408
|
+
deprecatedReason: undefined,
|
|
409
|
+
createdAt: principle.createdAt,
|
|
410
|
+
updatedAt: now,
|
|
411
|
+
// Ledger-only fields (derived from evolution-types Principle where applicable):
|
|
412
|
+
valueScore: 0,
|
|
413
|
+
adherenceRate: 0,
|
|
414
|
+
painPreventedCount: 0,
|
|
415
|
+
lastPainPreventedAt: undefined,
|
|
416
|
+
derivedFromPainIds: [params.painId],
|
|
417
|
+
ruleIds: [],
|
|
418
|
+
conflictsWithPrincipleIds: [],
|
|
419
|
+
supersedesPrincipleId: undefined,
|
|
420
|
+
};
|
|
421
|
+
addPrincipleToLedger(this.stateDir, ledgerPrinciple);
|
|
422
|
+
SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADDED', `Principle ${principleId} added to ledger tree`);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADD_FAILED', `Failed to add ${principleId} to ledger tree: ${String(err)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
384
428
|
// #204: Write to training store so listEvaluablePrinciples() can find this principle
|
|
385
429
|
if (this.stateDir) {
|
|
386
430
|
try {
|
package/src/core/pain.ts
CHANGED
|
@@ -32,9 +32,11 @@ export interface PainFlagData {
|
|
|
32
32
|
/** Whether this involves risky operation ('true' / 'false') */
|
|
33
33
|
is_risky: string;
|
|
34
34
|
/** Correlation trace ID (for linking events across the pipeline) */
|
|
35
|
-
trace_id
|
|
36
|
-
/**
|
|
37
|
-
trigger_text_preview
|
|
35
|
+
trace_id: string;
|
|
36
|
+
/** Preview of the text that triggered this pain */
|
|
37
|
+
trigger_text_preview: string;
|
|
38
|
+
/** Trajectory pain_events row ID (set by recordPainEvent) */
|
|
39
|
+
pain_event_id?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface PainFlagContractResult {
|
|
@@ -65,6 +67,7 @@ export function buildPainFlag(input: {
|
|
|
65
67
|
is_risky?: boolean;
|
|
66
68
|
trace_id?: string;
|
|
67
69
|
trigger_text_preview?: string;
|
|
70
|
+
pain_event_id?: string;
|
|
68
71
|
}): PainFlagData {
|
|
69
72
|
// Omit optional fields when not provided — prevents writing empty lines to disk
|
|
70
73
|
// which causes agent confusion (SKILL.md vs reality drift)
|
|
@@ -78,6 +81,7 @@ export function buildPainFlag(input: {
|
|
|
78
81
|
is_risky: input.is_risky ? 'true' : 'false',
|
|
79
82
|
trace_id: input.trace_id ?? '',
|
|
80
83
|
trigger_text_preview: input.trigger_text_preview ?? '',
|
|
84
|
+
pain_event_id: input.pain_event_id,
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -368,7 +368,7 @@ export async function saveLedgerAsync(stateDir: string, store: HybridLedgerStore
|
|
|
368
368
|
|
|
369
369
|
export function updateTrainingStore(
|
|
370
370
|
stateDir: string,
|
|
371
|
-
|
|
371
|
+
|
|
372
372
|
mutate: (store: LegacyPrincipleTrainingStore) => void,
|
|
373
373
|
): void {
|
|
374
374
|
mutateLedger(stateDir, (store) => {
|
|
@@ -376,6 +376,24 @@ export function updateTrainingStore(
|
|
|
376
376
|
});
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Add a new principle directly to the ledger tree.
|
|
381
|
+
* This is the companion to updatePrinciple() — use this when creating a NEW
|
|
382
|
+
* principle so the compiler can find it in tree.principles.
|
|
383
|
+
*
|
|
384
|
+
* Idempotent: if the principle already exists, overwrites (update semantics).
|
|
385
|
+
*/
|
|
386
|
+
export function addPrincipleToLedger(
|
|
387
|
+
stateDir: string,
|
|
388
|
+
principle: LedgerPrinciple,
|
|
389
|
+
): LedgerPrinciple {
|
|
390
|
+
return mutateLedger(stateDir, (store) => {
|
|
391
|
+
store.tree.principles[principle.id] = principle;
|
|
392
|
+
store.tree.lastUpdated = new Date().toISOString();
|
|
393
|
+
return principle;
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
379
397
|
export function createRule(stateDir: string, rule: LedgerRule): LedgerRule {
|
|
380
398
|
return mutateLedger(stateDir, (store) => {
|
|
381
399
|
const principle = store.tree.principles[rule.principleId];
|
package/src/core/trajectory.ts
CHANGED
|
@@ -237,10 +237,11 @@ export class TrajectoryDatabase {
|
|
|
237
237
|
return rowId;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
recordPainEvent(input: TrajectoryPainEventInput):
|
|
240
|
+
recordPainEvent(input: TrajectoryPainEventInput): number {
|
|
241
241
|
this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
|
|
242
|
+
let insertedId = -1;
|
|
242
243
|
this.withWrite(() => {
|
|
243
|
-
this.db.prepare(`
|
|
244
|
+
const runResult = this.db.prepare(`
|
|
244
245
|
INSERT INTO pain_events (
|
|
245
246
|
session_id, source, score, reason, severity, origin, confidence, text, created_at
|
|
246
247
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -255,15 +256,22 @@ export class TrajectoryDatabase {
|
|
|
255
256
|
input.text ?? null,
|
|
256
257
|
input.createdAt ?? nowIso(),
|
|
257
258
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
insertedId = runResult.lastInsertRowid as number;
|
|
260
|
+
});
|
|
261
|
+
// FTS indexing is best-effort — run outside the transaction so it cannot
|
|
262
|
+
// roll back the committed pain event row (MEM-03, MEM-04).
|
|
263
|
+
if (input.text && insertedId > 0) {
|
|
264
|
+
try {
|
|
262
265
|
this.db.prepare(`
|
|
263
266
|
INSERT INTO pain_events_fts (text, pain_event_id) VALUES (?, ?)
|
|
264
|
-
`).run(input.text,
|
|
267
|
+
`).run(input.text, insertedId);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
// Non-fatal: FTS index is for search convenience, not correctness.
|
|
270
|
+
// Log but do not re-throw — the pain event itself is already committed.
|
|
271
|
+
console.warn(`[trajectory] FTS index insert failed for pain_event ${insertedId}: ${String(err)}`);
|
|
265
272
|
}
|
|
266
|
-
}
|
|
273
|
+
}
|
|
274
|
+
return insertedId;
|
|
267
275
|
}
|
|
268
276
|
|
|
269
277
|
/**
|
package/src/hooks/pain.ts
CHANGED
|
@@ -295,6 +295,19 @@ export function handleAfterToolCall(
|
|
|
295
295
|
const painScore = computePainScore(1, false, false, isRisk ? 20 : 0, effectiveWorkspaceDir);
|
|
296
296
|
const traceId = createTraceId();
|
|
297
297
|
|
|
298
|
+
// Record to trajectory FIRST so we get the real auto-increment ID.
|
|
299
|
+
// This ID propagates through the pain flag → evolution task → principle,
|
|
300
|
+
// so the compiler can later resolve derivedFromPainIds correctly.
|
|
301
|
+
const trajectoryPainId = wctx.trajectory?.recordPainEvent({
|
|
302
|
+
sessionId,
|
|
303
|
+
source: 'tool_failure',
|
|
304
|
+
score: painScore,
|
|
305
|
+
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
306
|
+
severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
|
|
307
|
+
origin: 'system_infer',
|
|
308
|
+
text: params.text ?? params.content ?? undefined,
|
|
309
|
+
});
|
|
310
|
+
|
|
298
311
|
const painData = buildPainFlag({
|
|
299
312
|
source: 'tool_failure',
|
|
300
313
|
score: String(painScore),
|
|
@@ -303,6 +316,7 @@ export function handleAfterToolCall(
|
|
|
303
316
|
trace_id: traceId,
|
|
304
317
|
session_id: sessionId,
|
|
305
318
|
agent_id: ctx.agentId || '',
|
|
319
|
+
pain_event_id: trajectoryPainId !== undefined && trajectoryPainId >= 0 ? String(trajectoryPainId) : undefined,
|
|
306
320
|
});
|
|
307
321
|
|
|
308
322
|
try {
|
|
@@ -355,15 +369,6 @@ export function handleAfterToolCall(
|
|
|
355
369
|
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
356
370
|
isRisky: isRisk,
|
|
357
371
|
});
|
|
358
|
-
wctx.trajectory?.recordPainEvent?.({
|
|
359
|
-
sessionId,
|
|
360
|
-
source: 'tool_failure',
|
|
361
|
-
score: painScore,
|
|
362
|
-
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
363
|
-
severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
|
|
364
|
-
origin: 'system_infer',
|
|
365
|
-
text: params.text ?? params.content ?? undefined, // Store original text/content that failed
|
|
366
|
-
});
|
|
367
372
|
|
|
368
373
|
// Log to EvolutionLogger
|
|
369
374
|
const evoLogger = getEvolutionLogger(effectiveWorkspaceDir, wctx.trajectory);
|
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
|
|
@@ -141,6 +141,9 @@ export interface EvolutionQueueItem {
|
|
|
141
141
|
// Attaches explicit recent pain signal without merging task kinds.
|
|
142
142
|
// Used by target selector for ranking bias and context enrichment.
|
|
143
143
|
recentPainContext?: RecentPainContext;
|
|
144
|
+
|
|
145
|
+
/** Trajectory pain_events row ID — set when pain flag includes pain_event_id */
|
|
146
|
+
painEventId?: number;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// ── Queue Migration (extracted to queue-migration.ts) ────────────────────────
|
|
@@ -305,6 +308,7 @@ export function hasEquivalentPromotedRule(dictionary: { getAllRules(): Record<st
|
|
|
305
308
|
interface ParsedPainValues {
|
|
306
309
|
score: number; source: string; reason: string; preview: string;
|
|
307
310
|
traceId: string; sessionId: string; agentId: string;
|
|
311
|
+
painEventId?: number;
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
|
|
@@ -352,6 +356,7 @@ async function doEnqueuePainTask(
|
|
|
352
356
|
status: 'pending', session_id: v.sessionId || undefined,
|
|
353
357
|
agent_id: v.agentId || undefined, traceId: effectiveTraceId,
|
|
354
358
|
retryCount: 0, maxRetries: 3,
|
|
359
|
+
painEventId: v.painEventId,
|
|
355
360
|
});
|
|
356
361
|
|
|
357
362
|
saveEvolutionQueue(queuePath, queue);
|
|
@@ -400,6 +405,8 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
400
405
|
const traceId = contract.data.trace_id ?? '';
|
|
401
406
|
const sessionId = contract.data.session_id ?? '';
|
|
402
407
|
const agentId = contract.data.agent_id ?? '';
|
|
408
|
+
const painEventIdRaw = contract.data.pain_event_id;
|
|
409
|
+
const painEventId = painEventIdRaw ? parseInt(painEventIdRaw, 10) : undefined;
|
|
403
410
|
|
|
404
411
|
result.exists = true;
|
|
405
412
|
result.score = score;
|
|
@@ -414,7 +421,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
414
421
|
|
|
415
422
|
if (logger) logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
|
|
416
423
|
return doEnqueuePainTask(wctx, logger, painFlagPath, result, {
|
|
417
|
-
score, source, reason, preview, traceId, sessionId, agentId,
|
|
424
|
+
score, source, reason, preview, traceId, sessionId, agentId, painEventId,
|
|
418
425
|
});
|
|
419
426
|
}
|
|
420
427
|
|
|
@@ -470,6 +477,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
470
477
|
preview: jsonPreview, traceId: '',
|
|
471
478
|
sessionId: jsonPain.session_id || '',
|
|
472
479
|
agentId: jsonPain.agent_id || '',
|
|
480
|
+
painEventId: jsonPain.pain_event_id ? parseInt(jsonPain.pain_event_id, 10) : undefined,
|
|
473
481
|
});
|
|
474
482
|
}
|
|
475
483
|
} catch { /* Not JSON — fall through to KV/Markdown parsing */ }
|
|
@@ -492,6 +500,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
492
500
|
let traceId = '';
|
|
493
501
|
let sessionId = '';
|
|
494
502
|
let agentId = '';
|
|
503
|
+
let painEventId: number | undefined;
|
|
495
504
|
|
|
496
505
|
for (const line of lines) {
|
|
497
506
|
// KV format: "key: value"
|
|
@@ -503,6 +512,10 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
503
512
|
if (line.startsWith('trace_id:')) traceId = line.split(':', 2)[1].trim();
|
|
504
513
|
if (line.startsWith('session_id:')) sessionId = line.slice('session_id:'.length).trim();
|
|
505
514
|
if (line.startsWith('agent_id:')) agentId = line.slice('agent_id:'.length).trim();
|
|
515
|
+
if (line.startsWith('pain_event_id:')) {
|
|
516
|
+
const raw = line.slice('pain_event_id:'.length).trim();
|
|
517
|
+
painEventId = parseInt(raw, 10) || undefined;
|
|
518
|
+
}
|
|
506
519
|
|
|
507
520
|
// Key=Value fallback format: "key=value" (pain skill manual output)
|
|
508
521
|
// Handles both uppercase (Source=X) and lowercase (source=x) variants
|
|
@@ -544,7 +557,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
|
|
|
544
557
|
|
|
545
558
|
return doEnqueuePainTask(wctx, logger, painFlagPath, result, {
|
|
546
559
|
score, source, reason, preview,
|
|
547
|
-
traceId, sessionId, agentId,
|
|
560
|
+
traceId, sessionId, agentId, painEventId,
|
|
548
561
|
});
|
|
549
562
|
|
|
550
563
|
} catch (err) {
|
|
@@ -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
|
/**
|
package/src/service/queue-io.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface RecentPainContext {
|
|
|
68
68
|
reason: string;
|
|
69
69
|
timestamp: string;
|
|
70
70
|
sessionId: string;
|
|
71
|
+
/** Trajectory pain_events row ID — set when pain flag includes pain_event_id */
|
|
72
|
+
painEventId?: number;
|
|
71
73
|
} | null;
|
|
72
74
|
recentPainCount: number;
|
|
73
75
|
recentMaxPainScore: number;
|
|
@@ -150,10 +152,12 @@ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext
|
|
|
150
152
|
const reason = contract.data.reason ?? '';
|
|
151
153
|
const timestamp = contract.data.time ?? '';
|
|
152
154
|
const sessionId = contract.data.session_id ?? '';
|
|
155
|
+
const painEventIdRaw = contract.data.pain_event_id;
|
|
156
|
+
const painEventId = painEventIdRaw ? parseInt(painEventIdRaw, 10) : undefined;
|
|
153
157
|
|
|
154
158
|
if (score > 0) {
|
|
155
159
|
return {
|
|
156
|
-
mostRecent: { score, source, reason, timestamp, sessionId },
|
|
160
|
+
mostRecent: { score, source, reason, timestamp, sessionId, painEventId },
|
|
157
161
|
recentPainCount: 1,
|
|
158
162
|
recentMaxPainScore: score,
|
|
159
163
|
};
|
|
@@ -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
|
-
//
|
|
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', () => {
|