@lmzhen/dsh-evolution-curator 0.4.1 → 0.6.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/README.md +3 -0
- package/lib/index.js +135 -29
- package/lib/types/index.d.ts +64 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -26,6 +26,9 @@ Deterministic skill lifecycle and recovery
|
|
|
26
26
|
|
|
27
27
|
- `autoStart` (default true) arms an HOURLY tick that only asks whether the due-ness interval (`intervalHours`, default 168 h) has elapsed — the tick is not the interval — plus a deferred catch-up check `bootGraceSeconds` (default 10) after host boot. Both decide due-ness from the **persisted** `lastRunAt`, so a restart with an overdue schedule runs the first pass within the boot grace instead of waiting a full interval. `bootGraceSeconds: 0` disables the deferral (not recommended: the check may run against a half-mounted host). All scheduling gates — interval, idle, first-run deferral, and the reentrancy guard — remain inside `run()`.
|
|
28
28
|
- `autoStart: false` disables both automatic checks; `/evolution curator run` (manual, gate-skipping) still works.
|
|
29
|
+
- Deprecated name (G0/S0.3): `intervalHours` is the legacy spelling of the policy
|
|
30
|
+
row's `curatorIntervalHours`, whose value the snapshot shadows. Reading it still
|
|
31
|
+
works; writing it is refused, and it is removed in 0.7.0.
|
|
29
32
|
|
|
30
33
|
**Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
|
|
31
34
|
## Notes and history
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
-
import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, probeList, probeMtime, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
|
|
6
|
+
import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, PARAM_NAMESPACES, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, installParamSection, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
|
|
7
7
|
//#region lib/types/index.js
|
|
8
8
|
/**
|
|
9
9
|
* Deterministic skill lifecycle curator with interval gate and archive.
|
|
@@ -17,6 +17,36 @@ const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
|
|
|
17
17
|
* 120s matches the review subagent default; the 32-bit ceiling is Node's
|
|
18
18
|
* timer-delay limit (`AbortSignal.timeout` throws above it). */
|
|
19
19
|
const DEFAULT_CURATOR_REVIEW_TIMEOUT_MS = 12e4;
|
|
20
|
+
/** Namespace the curator group's user-writable knobs live in (core's PARAM_NAMESPACES). */
|
|
21
|
+
const CURATOR_SETTINGS_NAMESPACE = "evolution-curator";
|
|
22
|
+
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
23
|
+
* constants and the bounds mirror the row schema, so an empty document resolves to
|
|
24
|
+
* today's behaviour and an out-of-range value is refused instead of clamped. */
|
|
25
|
+
const CURATOR_SETTINGS_SCHEMA = z.object({
|
|
26
|
+
curatorIntervalHours: z.number().min(1).default(DEFAULT_CURATOR_INTERVAL_HOURS),
|
|
27
|
+
staleAfterDays: z.number().min(1).default(DEFAULT_STALE_AFTER_DAYS),
|
|
28
|
+
archiveAfterDays: z.number().min(1).default(DEFAULT_ARCHIVE_AFTER_DAYS),
|
|
29
|
+
qualityWarnStaleAfterDays: z.number().min(1).default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
|
|
30
|
+
minIdleHours: z.number().min(0).default(DEFAULT_MIN_IDLE_HOURS),
|
|
31
|
+
minIdleFailOpen: z.boolean().default(true),
|
|
32
|
+
llmReview: z.boolean().default(false),
|
|
33
|
+
curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
|
|
34
|
+
curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
|
|
35
|
+
healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
|
|
36
|
+
healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
|
|
37
|
+
healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* The cross-field rule the schema cannot express (A2-17): an archive threshold
|
|
41
|
+
* below the stale threshold makes the engine reactivate stale records instead of
|
|
42
|
+
* archiving them, so a resolved pair that violates it is refused at WRITE time.
|
|
43
|
+
* The runtime keeps its clamp for the carriers this hook never sees (the policy
|
|
44
|
+
* snapshot and the plugin row).
|
|
45
|
+
* @param value - the resolved section the platform hands the owner.
|
|
46
|
+
*/
|
|
47
|
+
function validateCuratorSettings(value) {
|
|
48
|
+
if (value.archiveAfterDays < value.staleAfterDays) throw new Error(`archiveAfterDays (${value.archiveAfterDays}) must be >= staleAfterDays (${value.staleAfterDays})`);
|
|
49
|
+
}
|
|
20
50
|
/**
|
|
21
51
|
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
22
52
|
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
@@ -76,6 +106,10 @@ var EvolutionCurator = class extends Service {
|
|
|
76
106
|
healthSoftBodyChars;
|
|
77
107
|
healthStampDensityPerKb;
|
|
78
108
|
healthChurnMinPatches;
|
|
109
|
+
/** G3/S3.3: this row's resolved values — the base layer of the settings section. */
|
|
110
|
+
settingsBase;
|
|
111
|
+
/** The user layer for this row's namespace (absent provider = deployment values). */
|
|
112
|
+
overrides;
|
|
79
113
|
lastRun = 0;
|
|
80
114
|
timer;
|
|
81
115
|
/** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
|
|
@@ -110,7 +144,11 @@ var EvolutionCurator = class extends Service {
|
|
|
110
144
|
this.skills = newSkillLibrary({
|
|
111
145
|
config,
|
|
112
146
|
io: this.io,
|
|
113
|
-
ctx: this.ctx
|
|
147
|
+
ctx: this.ctx,
|
|
148
|
+
limits: {
|
|
149
|
+
...DEFAULT_SKILL_LIMITS,
|
|
150
|
+
...policyStageLimits(this.ctx.get("evolutionPolicy")?.get?.())
|
|
151
|
+
}
|
|
114
152
|
});
|
|
115
153
|
this.enabled = config.enabled ?? true;
|
|
116
154
|
this.instanceHome = evolutionHome();
|
|
@@ -126,7 +164,7 @@ var EvolutionCurator = class extends Service {
|
|
|
126
164
|
if (value !== void 0 && result !== value) clamped.push(name);
|
|
127
165
|
return result;
|
|
128
166
|
};
|
|
129
|
-
this.intervalHours = field("intervalHours", config
|
|
167
|
+
this.intervalHours = field("intervalHours", readNumberParam(config, "curatorIntervalHours"), DEFAULT_CURATOR_INTERVAL_HOURS, 1);
|
|
130
168
|
this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
|
|
131
169
|
this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
|
|
132
170
|
if (this.archiveAfterDays < this.staleAfterDays) {
|
|
@@ -149,6 +187,26 @@ var EvolutionCurator = class extends Service {
|
|
|
149
187
|
this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
|
|
150
188
|
this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
|
|
151
189
|
if (clamped.length > 0) this.ctx.logger.warn(`evolution-curator: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
|
|
190
|
+
this.settingsBase = {
|
|
191
|
+
curatorIntervalHours: this.intervalHours,
|
|
192
|
+
staleAfterDays: this.staleAfterDays,
|
|
193
|
+
archiveAfterDays: this.archiveAfterDays,
|
|
194
|
+
qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
|
|
195
|
+
minIdleHours: this.minIdleHours,
|
|
196
|
+
minIdleFailOpen: this.minIdleFailOpen,
|
|
197
|
+
llmReview: this.llmReview,
|
|
198
|
+
curatorReviewMaxTokens: this.curatorReviewMaxTokens,
|
|
199
|
+
curatorReviewTimeoutMs: this.curatorReviewTimeoutMs,
|
|
200
|
+
healthSoftBodyChars: this.healthSoftBodyChars,
|
|
201
|
+
healthStampDensityPerKb: this.healthStampDensityPerKb,
|
|
202
|
+
healthChurnMinPatches: this.healthChurnMinPatches
|
|
203
|
+
};
|
|
204
|
+
this.overrides = installParamSection(ctx, PARAM_NAMESPACES["evolution-curator"] ?? "evolution-curator", CURATOR_SETTINGS_SCHEMA, this.settingsBase, {
|
|
205
|
+
warn: (message) => {
|
|
206
|
+
this.ctx.logger.warn("dsh-evolution-curator: " + message);
|
|
207
|
+
},
|
|
208
|
+
validate: validateCuratorSettings
|
|
209
|
+
});
|
|
152
210
|
this.lastRun = Date.now();
|
|
153
211
|
this.ctx.effect(() => {
|
|
154
212
|
return () => {
|
|
@@ -159,17 +217,58 @@ var EvolutionCurator = class extends Service {
|
|
|
159
217
|
}, "evolution-curator.stop");
|
|
160
218
|
if (config.autoStart ?? true) this.start();
|
|
161
219
|
}
|
|
220
|
+
/** The policy-snapshot carrier for this group (unmounted service = no policy). */
|
|
221
|
+
policyFields() {
|
|
222
|
+
return this.ctx.get("evolutionPolicy")?.get() ?? {};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
|
|
226
|
+
* user > policy snapshot > this row, and {@link ParamOverrides.get} returns
|
|
227
|
+
* undefined for a key the user never set, so the deployment carriers keep
|
|
228
|
+
* winning exactly as before. Read at USE time: a committed settings change is
|
|
229
|
+
* live without a restart and without a watcher.
|
|
230
|
+
* @returns the resolved section, every numeric field sanitized.
|
|
231
|
+
*/
|
|
232
|
+
settings() {
|
|
233
|
+
const snapshot = this.policyFields();
|
|
234
|
+
const row = this.settingsBase;
|
|
235
|
+
const pick = (key) => {
|
|
236
|
+
const user = this.overrides.get(key);
|
|
237
|
+
if (user !== void 0) return user;
|
|
238
|
+
return snapshot[key] ?? row[key];
|
|
239
|
+
};
|
|
240
|
+
const number = (key, min, max) => {
|
|
241
|
+
const lower = row[key];
|
|
242
|
+
return clampedNumber(pick(key), lower, max === void 0 ? { min } : {
|
|
243
|
+
min,
|
|
244
|
+
max
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
curatorIntervalHours: number("curatorIntervalHours", 1),
|
|
249
|
+
staleAfterDays: number("staleAfterDays", 1),
|
|
250
|
+
archiveAfterDays: number("archiveAfterDays", 1),
|
|
251
|
+
qualityWarnStaleAfterDays: number("qualityWarnStaleAfterDays", 1),
|
|
252
|
+
minIdleHours: number("minIdleHours", 0),
|
|
253
|
+
minIdleFailOpen: pick("minIdleFailOpen"),
|
|
254
|
+
llmReview: pick("llmReview"),
|
|
255
|
+
curatorReviewMaxTokens: number("curatorReviewMaxTokens", 1),
|
|
256
|
+
curatorReviewTimeoutMs: number("curatorReviewTimeoutMs", 1, MAX_TIMER_DELAY_MS),
|
|
257
|
+
healthSoftBodyChars: number("healthSoftBodyChars", 1),
|
|
258
|
+
healthStampDensityPerKb: number("healthStampDensityPerKb", 1),
|
|
259
|
+
healthChurnMinPatches: number("healthChurnMinPatches", 1)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
162
262
|
lifecycle() {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
archiveAfterDays = staleAfterDays;
|
|
263
|
+
const settings = this.settings();
|
|
264
|
+
let archiveAfterDays = settings.archiveAfterDays;
|
|
265
|
+
if (archiveAfterDays < settings.staleAfterDays) {
|
|
266
|
+
this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${archiveAfterDays}) < staleAfterDays (${settings.staleAfterDays}); using the stale threshold as the archive threshold`);
|
|
267
|
+
archiveAfterDays = settings.staleAfterDays;
|
|
169
268
|
}
|
|
170
269
|
return {
|
|
171
|
-
intervalHours:
|
|
172
|
-
staleAfterDays,
|
|
270
|
+
intervalHours: settings.curatorIntervalHours,
|
|
271
|
+
staleAfterDays: settings.staleAfterDays,
|
|
173
272
|
archiveAfterDays
|
|
174
273
|
};
|
|
175
274
|
}
|
|
@@ -303,6 +402,7 @@ var EvolutionCurator = class extends Service {
|
|
|
303
402
|
"",
|
|
304
403
|
"Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
|
|
305
404
|
].join("\n");
|
|
405
|
+
const settings = this.settings();
|
|
306
406
|
try {
|
|
307
407
|
const assembler = new BlockAssembler();
|
|
308
408
|
for await (const chunk of llm.stream({
|
|
@@ -320,8 +420,8 @@ var EvolutionCurator = class extends Service {
|
|
|
320
420
|
summary: "curator review"
|
|
321
421
|
}
|
|
322
422
|
})],
|
|
323
|
-
maxTokens:
|
|
324
|
-
signal: AbortSignal.timeout(
|
|
423
|
+
maxTokens: settings.curatorReviewMaxTokens,
|
|
424
|
+
signal: AbortSignal.timeout(settings.curatorReviewTimeoutMs)
|
|
325
425
|
})) assembler.push(chunk);
|
|
326
426
|
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
327
427
|
return {
|
|
@@ -394,7 +494,7 @@ var EvolutionCurator = class extends Service {
|
|
|
394
494
|
archiveCandidates: [],
|
|
395
495
|
archived: [],
|
|
396
496
|
failed: [],
|
|
397
|
-
llmReviewEnabled: this.llmReview
|
|
497
|
+
llmReviewEnabled: this.settings().llmReview
|
|
398
498
|
});
|
|
399
499
|
}
|
|
400
500
|
/**
|
|
@@ -502,6 +602,7 @@ var EvolutionCurator = class extends Service {
|
|
|
502
602
|
const runId = randomUUID();
|
|
503
603
|
const stateService = this.curatorStateService();
|
|
504
604
|
const lifecycle = this.lifecycle();
|
|
605
|
+
const settings = this.settings();
|
|
505
606
|
const persisted = await stateService?.loadCuratorState() ?? null;
|
|
506
607
|
if (!ignoreGates && persisted?.paused === true) return {
|
|
507
608
|
stale: [],
|
|
@@ -517,7 +618,7 @@ var EvolutionCurator = class extends Service {
|
|
|
517
618
|
report: this.skippedReport(runId, startedAt),
|
|
518
619
|
skipped: "interval"
|
|
519
620
|
};
|
|
520
|
-
if (!ignoreGates &&
|
|
621
|
+
if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) return {
|
|
521
622
|
stale: [],
|
|
522
623
|
archived: [],
|
|
523
624
|
errors: [],
|
|
@@ -580,7 +681,7 @@ var EvolutionCurator = class extends Service {
|
|
|
580
681
|
const result = computeLifecycleTransitions(usage, {
|
|
581
682
|
staleAfterDays: lifecycle.staleAfterDays,
|
|
582
683
|
archiveAfterDays: lifecycle.archiveAfterDays,
|
|
583
|
-
qualityWarnStaleAfterDays:
|
|
684
|
+
qualityWarnStaleAfterDays: settings.qualityWarnStaleAfterDays,
|
|
584
685
|
excludeSkillNames: this.excludeSkillNames,
|
|
585
686
|
manageUnmanaged: this.manageUnmanaged,
|
|
586
687
|
pruneBuiltins: this.pruneBuiltins,
|
|
@@ -589,7 +690,7 @@ var EvolutionCurator = class extends Service {
|
|
|
589
690
|
referencedSkillNames: this.referencedSkillNames
|
|
590
691
|
}, /* @__PURE__ */ new Date(), gates, protectedNames);
|
|
591
692
|
const recommendPool = [...new Set([...result.markStale, ...dedupMembers])].filter((name) => SKILL_NAME_RE.test(name));
|
|
592
|
-
const nominations =
|
|
693
|
+
const nominations = settings.llmReview ? await this.recommend(recommendPool, { dryRun }) : {
|
|
593
694
|
prunings: [],
|
|
594
695
|
consolidations: [],
|
|
595
696
|
warnings: []
|
|
@@ -601,7 +702,7 @@ var EvolutionCurator = class extends Service {
|
|
|
601
702
|
};
|
|
602
703
|
const llmNominations = gatedNominations.prunings;
|
|
603
704
|
const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
|
|
604
|
-
if (!ignoreGates &&
|
|
705
|
+
if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
|
|
605
706
|
if (!dryRun) try {
|
|
606
707
|
await mutateUsage(root, this.io, (map) => {
|
|
607
708
|
for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
|
|
@@ -618,7 +719,7 @@ var EvolutionCurator = class extends Service {
|
|
|
618
719
|
};
|
|
619
720
|
}
|
|
620
721
|
const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
|
|
621
|
-
if (!ignoreGates &&
|
|
722
|
+
if (!ignoreGates && settings.minIdleHours > 0 && this.recentSessionActive()) {
|
|
622
723
|
if (!dryRun) try {
|
|
623
724
|
await mutateUsage(root, this.io, (map) => {
|
|
624
725
|
for (const [name, record] of usage) if (!map.has(name)) map.set(name, { ...record });
|
|
@@ -654,10 +755,13 @@ var EvolutionCurator = class extends Service {
|
|
|
654
755
|
});
|
|
655
756
|
const runAborted = errors.some((error) => error.startsWith("run aborted"));
|
|
656
757
|
if (!dryRun && !runAborted) this.lastRun = Date.now();
|
|
758
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
759
|
+
const wouldPrune = this.skills.archiveRetentionPolicy() === "report" ? await this.skills.expiredArchives() : void 0;
|
|
657
760
|
const report = buildCuratorRunReport({
|
|
658
761
|
runId,
|
|
659
762
|
startedAt,
|
|
660
|
-
finishedAt
|
|
763
|
+
finishedAt,
|
|
764
|
+
...wouldPrune === void 0 ? {} : { wouldPrune },
|
|
661
765
|
staleCandidates: result.markStale,
|
|
662
766
|
llmNominations,
|
|
663
767
|
archiveCandidates,
|
|
@@ -679,7 +783,7 @@ var EvolutionCurator = class extends Service {
|
|
|
679
783
|
})(),
|
|
680
784
|
consolidated,
|
|
681
785
|
...snapshotPath === void 0 ? {} : { snapshotPath },
|
|
682
|
-
llmReviewEnabled:
|
|
786
|
+
llmReviewEnabled: settings.llmReview,
|
|
683
787
|
...gatedNominations.warnings.length > 0 ? { nominationsWarnings: gatedNominations.warnings } : {}
|
|
684
788
|
});
|
|
685
789
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
@@ -690,7 +794,7 @@ var EvolutionCurator = class extends Service {
|
|
|
690
794
|
this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
|
|
691
795
|
this.ctx.logger.warn(error);
|
|
692
796
|
}
|
|
693
|
-
const llmHint = !
|
|
797
|
+
const llmHint = !settings.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
694
798
|
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${consolidated.length}${llmHint}`;
|
|
695
799
|
try {
|
|
696
800
|
await stateService?.transactCuratorState((current) => {
|
|
@@ -711,7 +815,7 @@ var EvolutionCurator = class extends Service {
|
|
|
711
815
|
archived: archivedSkills.map((item) => item.name),
|
|
712
816
|
errors,
|
|
713
817
|
report,
|
|
714
|
-
...
|
|
818
|
+
...settings.llmReview ? { nominations: gatedNominations } : {}
|
|
715
819
|
};
|
|
716
820
|
}
|
|
717
821
|
/**
|
|
@@ -943,15 +1047,16 @@ var EvolutionCurator = class extends Service {
|
|
|
943
1047
|
};
|
|
944
1048
|
}
|
|
945
1049
|
recentSessionActive() {
|
|
1050
|
+
const settings = this.settings();
|
|
946
1051
|
const agents = this.ctx.get("agents");
|
|
947
|
-
if (!agents) return !
|
|
1052
|
+
if (!agents) return !settings.minIdleFailOpen;
|
|
948
1053
|
let latest = 0;
|
|
949
1054
|
for (const agent of agents.list()) {
|
|
950
1055
|
const events = agent.session.snapshotEvents();
|
|
951
1056
|
const last = events.length === 0 ? 0 : events[events.length - 1]?.time ?? 0;
|
|
952
1057
|
latest = Math.max(latest, last);
|
|
953
1058
|
}
|
|
954
|
-
return latest > 0 && Date.now() - latest <
|
|
1059
|
+
return latest > 0 && Date.now() - latest < settings.minIdleHours * 36e5;
|
|
955
1060
|
}
|
|
956
1061
|
/**
|
|
957
1062
|
* Keep only the newest N curator run reports plus at most `errorKeep` error
|
|
@@ -1098,10 +1203,11 @@ var EvolutionCurator = class extends Service {
|
|
|
1098
1203
|
* layer.
|
|
1099
1204
|
*/
|
|
1100
1205
|
async healthView() {
|
|
1206
|
+
const settings = this.settings();
|
|
1101
1207
|
const thresholds = {
|
|
1102
|
-
softBodyChars:
|
|
1103
|
-
stampDensityPerKb:
|
|
1104
|
-
churnMinPatches:
|
|
1208
|
+
softBodyChars: settings.healthSoftBodyChars,
|
|
1209
|
+
stampDensityPerKb: settings.healthStampDensityPerKb,
|
|
1210
|
+
churnMinPatches: settings.healthChurnMinPatches
|
|
1105
1211
|
};
|
|
1106
1212
|
const usage = await loadUsage(this.skills.root, this.io);
|
|
1107
1213
|
const observed = usageObserved(usage);
|
|
@@ -1230,4 +1336,4 @@ function reportsSweepLockTarget() {
|
|
|
1230
1336
|
return join(evolutionHome(), "reports", ".retention");
|
|
1231
1337
|
}
|
|
1232
1338
|
//#endregion
|
|
1233
|
-
export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
|
|
1339
|
+
export { CURATOR_SETTINGS_NAMESPACE, CURATOR_SETTINGS_SCHEMA, EvolutionCurator, EvolutionCurator as default, gateConsolidations, validateCuratorSettings };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module @lmzhen/dsh-evolution-curator
|
|
4
4
|
*/
|
|
5
5
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
6
7
|
import type Schema from '@deepseek-ai/schemastery';
|
|
7
8
|
import { EvolutionGateSet, type SkillLibrary } from '@lmzhen/dsh-evolution-core';
|
|
8
9
|
import { type CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, type SkillActionResult, type SkillHealthVerdict } from '@lmzhen/dsh-evolution-core';
|
|
@@ -14,6 +15,10 @@ declare module '@deepseek-ai/cordis' {
|
|
|
14
15
|
}
|
|
15
16
|
export interface Config {
|
|
16
17
|
enabled?: boolean;
|
|
18
|
+
/** The due-ness interval. The policy snapshot shadows it in every shipped
|
|
19
|
+
* composition — configure `curatorIntervalHours` there; that name is the
|
|
20
|
+
* canonical id. Deprecated alias (G0/S0.3): still readable, refused by
|
|
21
|
+
* writes; removed 0.7.0. */
|
|
17
22
|
intervalHours?: number;
|
|
18
23
|
staleAfterDays?: number;
|
|
19
24
|
archiveAfterDays?: number;
|
|
@@ -56,6 +61,50 @@ export interface Config {
|
|
|
56
61
|
/** Structure-health write-ghost floor: patches at/above with zero reads (A2). */
|
|
57
62
|
healthChurnMinPatches?: number;
|
|
58
63
|
}
|
|
64
|
+
/** Namespace the curator group's user-writable knobs live in (core's PARAM_NAMESPACES). */
|
|
65
|
+
export declare const CURATOR_SETTINGS_NAMESPACE = "evolution-curator";
|
|
66
|
+
/** Curator behaviour a user may change (G3/S3.3). Field names are the CANONICAL
|
|
67
|
+
* parameter ids from the registry, so the settings document, the params output,
|
|
68
|
+
* the doctor report and the cards all spell one name. */
|
|
69
|
+
export interface CuratorSettings {
|
|
70
|
+
/** Minimum hours between deterministic curation passes. */
|
|
71
|
+
curatorIntervalHours: number;
|
|
72
|
+
/** Inactive days before a skill counts as stale. */
|
|
73
|
+
staleAfterDays: number;
|
|
74
|
+
/** Inactive days before a stale skill is archived (>= staleAfterDays). */
|
|
75
|
+
archiveAfterDays: number;
|
|
76
|
+
/** Age at which a low quality score starts warning. */
|
|
77
|
+
qualityWarnStaleAfterDays: number;
|
|
78
|
+
/** Idle hours required before an automatic pass runs (0 disables the gate). */
|
|
79
|
+
minIdleHours: number;
|
|
80
|
+
/** Let the idle gate open when the activity probe is unavailable. */
|
|
81
|
+
minIdleFailOpen: boolean;
|
|
82
|
+
/** Enable the LLM nomination pass on top of the deterministic lifecycle. */
|
|
83
|
+
llmReview: boolean;
|
|
84
|
+
/** Token budget of the curator LLM review. */
|
|
85
|
+
curatorReviewMaxTokens: number;
|
|
86
|
+
/** Wall-clock bound of the curator LLM review. */
|
|
87
|
+
curatorReviewTimeoutMs: number;
|
|
88
|
+
/** Body character line the health view judges against. */
|
|
89
|
+
healthSoftBodyChars: number;
|
|
90
|
+
/** Stamp density per KB that flags log-like content in a body. */
|
|
91
|
+
healthStampDensityPerKb: number;
|
|
92
|
+
/** Patches without a read that flag a write-ghost skill. */
|
|
93
|
+
healthChurnMinPatches: number;
|
|
94
|
+
}
|
|
95
|
+
/** Schema the platform validates the user layer against; defaults mirror the core
|
|
96
|
+
* constants and the bounds mirror the row schema, so an empty document resolves to
|
|
97
|
+
* today's behaviour and an out-of-range value is refused instead of clamped. */
|
|
98
|
+
export declare const CURATOR_SETTINGS_SCHEMA: z<CuratorSettings>;
|
|
99
|
+
/**
|
|
100
|
+
* The cross-field rule the schema cannot express (A2-17): an archive threshold
|
|
101
|
+
* below the stale threshold makes the engine reactivate stale records instead of
|
|
102
|
+
* archiving them, so a resolved pair that violates it is refused at WRITE time.
|
|
103
|
+
* The runtime keeps its clamp for the carriers this hook never sees (the policy
|
|
104
|
+
* snapshot and the plugin row).
|
|
105
|
+
* @param value - the resolved section the platform hands the owner.
|
|
106
|
+
*/
|
|
107
|
+
export declare function validateCuratorSettings(value: CuratorSettings): void;
|
|
59
108
|
/** Outcome of one curator run pass. */
|
|
60
109
|
export interface CuratorRunOutcome {
|
|
61
110
|
stale: string[];
|
|
@@ -113,6 +162,10 @@ export declare class EvolutionCurator extends Service {
|
|
|
113
162
|
private readonly healthSoftBodyChars;
|
|
114
163
|
private readonly healthStampDensityPerKb;
|
|
115
164
|
private readonly healthChurnMinPatches;
|
|
165
|
+
/** G3/S3.3: this row's resolved values — the base layer of the settings section. */
|
|
166
|
+
private readonly settingsBase;
|
|
167
|
+
/** The user layer for this row's namespace (absent provider = deployment values). */
|
|
168
|
+
private readonly overrides;
|
|
116
169
|
private lastRun;
|
|
117
170
|
private timer;
|
|
118
171
|
/** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
|
|
@@ -140,6 +193,17 @@ export declare class EvolutionCurator extends Service {
|
|
|
140
193
|
* yielding instance's log line names a concrete holder. */
|
|
141
194
|
private readonly instanceOwner;
|
|
142
195
|
constructor(ctx: Context, config?: Config);
|
|
196
|
+
/** The policy-snapshot carrier for this group (unmounted service = no policy). */
|
|
197
|
+
private policyFields;
|
|
198
|
+
/**
|
|
199
|
+
* G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
|
|
200
|
+
* user > policy snapshot > this row, and {@link ParamOverrides.get} returns
|
|
201
|
+
* undefined for a key the user never set, so the deployment carriers keep
|
|
202
|
+
* winning exactly as before. Read at USE time: a committed settings change is
|
|
203
|
+
* live without a restart and without a watcher.
|
|
204
|
+
* @returns the resolved section, every numeric field sanitized.
|
|
205
|
+
*/
|
|
206
|
+
private settings;
|
|
143
207
|
private lifecycle;
|
|
144
208
|
start(): void;
|
|
145
209
|
stop(): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-curator",
|
|
3
3
|
"description": "Deterministic skill lifecycle and recovery (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.6.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
34
34
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
35
35
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
36
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
37
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
36
|
+
"@lmzhen/dsh-evolution-io": "^0.6.0",
|
|
37
|
+
"@lmzhen/dsh-evolution-state": "^0.6.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
41
41
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
42
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
43
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
44
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
42
|
+
"@lmzhen/dsh-evolution-core": "^0.6.0",
|
|
43
|
+
"@lmzhen/dsh-evolution-io": "^0.6.0",
|
|
44
|
+
"@lmzhen/dsh-evolution-state": "^0.6.0"
|
|
45
45
|
}
|
|
46
46
|
}
|