@lmzhen/dsh-evolution-curator 0.3.61 → 0.3.63
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/lib/index.js +92 -22
- package/lib/types/index.d.ts +30 -1
- package/package.json +7 -7
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_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, 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_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, 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.
|
|
@@ -39,8 +39,8 @@ var EvolutionCurator = class extends Service {
|
|
|
39
39
|
pruneBuiltins: z.boolean().default(false),
|
|
40
40
|
referencedSkillNames: z.array(z.string()).default([]),
|
|
41
41
|
autoStart: z.boolean().default(true),
|
|
42
|
-
bootGraceSeconds: z.number().min(0).default(
|
|
43
|
-
curatorReviewMaxTokens: z.number().min(1).default(
|
|
42
|
+
bootGraceSeconds: z.number().min(0).default(DEFAULT_CURATOR_BOOT_GRACE_SECONDS),
|
|
43
|
+
curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
|
|
44
44
|
healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
|
|
45
45
|
healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
|
|
46
46
|
healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
|
|
@@ -68,11 +68,12 @@ var EvolutionCurator = class extends Service {
|
|
|
68
68
|
lastRun = 0;
|
|
69
69
|
timer;
|
|
70
70
|
bootCheck;
|
|
71
|
-
running = false;
|
|
72
71
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
73
72
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
74
73
|
* persisted===null defer repeats forever (no state service to persist). */
|
|
75
74
|
statelessFirstRunDeferred = false;
|
|
75
|
+
/** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
|
|
76
|
+
statelessStateWarned = false;
|
|
76
77
|
constructor(ctx, config = {}) {
|
|
77
78
|
super(ctx, "evolutionCurator");
|
|
78
79
|
this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
@@ -81,8 +82,11 @@ var EvolutionCurator = class extends Service {
|
|
|
81
82
|
});
|
|
82
83
|
this.enabled = config.enabled ?? true;
|
|
83
84
|
const clamped = [];
|
|
84
|
-
const field = (name, value, fallback, min) => {
|
|
85
|
-
const result = clampedNumber(value, fallback, { min }
|
|
85
|
+
const field = (name, value, fallback, min, max) => {
|
|
86
|
+
const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
|
|
87
|
+
min,
|
|
88
|
+
max
|
|
89
|
+
});
|
|
86
90
|
if (value !== void 0 && result !== value) clamped.push(name);
|
|
87
91
|
return result;
|
|
88
92
|
};
|
|
@@ -98,8 +102,8 @@ var EvolutionCurator = class extends Service {
|
|
|
98
102
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
99
103
|
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
100
104
|
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
101
|
-
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds,
|
|
102
|
-
this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens,
|
|
105
|
+
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
|
|
106
|
+
this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
|
|
103
107
|
this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
|
|
104
108
|
this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
|
|
105
109
|
this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
|
|
@@ -171,10 +175,21 @@ var EvolutionCurator = class extends Service {
|
|
|
171
175
|
* HERE as a cheap pre-check (persisted or in-memory clock) AND again inside
|
|
172
176
|
* run() as the authoritative gate — the docstring no longer claims the two
|
|
173
177
|
* never duplicate; a future interval change must update both.
|
|
178
|
+
*
|
|
179
|
+
* P2-5 (v14): without the `evolution-state` service there is no durable
|
|
180
|
+
* `lastRunAt`, so the baseline degrades to this process's lifetime. That is
|
|
181
|
+
* a supported-but-degraded composition (every shipped bundle mounts the
|
|
182
|
+
* state rows), so the schedule is left as-is and the degradation is
|
|
183
|
+
* surfaced once instead of silently meaning "never runs".
|
|
174
184
|
*/
|
|
175
185
|
async autoCheck() {
|
|
176
186
|
try {
|
|
177
|
-
const
|
|
187
|
+
const stateService = this.curatorStateService();
|
|
188
|
+
if (stateService === void 0 && !this.statelessStateWarned) {
|
|
189
|
+
this.statelessStateWarned = true;
|
|
190
|
+
this.ctx.logger.warn(`evolution-curator: evolution-state is not mounted — the curation interval baseline is this process's lifetime only (default interval ${DEFAULT_CURATOR_INTERVAL_HOURS}h), so automatic curation will not fire again until the process has been alive that long. Mount evolution-state (evolution-host/all bundle) for a durable schedule.`);
|
|
191
|
+
}
|
|
192
|
+
const last = (await stateService?.loadCuratorState())?.lastRunAt ?? this.lastRun;
|
|
178
193
|
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
179
194
|
} catch (error) {
|
|
180
195
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -319,18 +334,18 @@ var EvolutionCurator = class extends Service {
|
|
|
319
334
|
* skipped with an explicit `already-running` outcome.
|
|
320
335
|
*/
|
|
321
336
|
async run(options = {}) {
|
|
322
|
-
if (this.
|
|
337
|
+
if (this.mutexDepth > 0) return {
|
|
323
338
|
stale: [],
|
|
324
339
|
archived: [],
|
|
325
340
|
errors: [],
|
|
326
341
|
report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
|
|
327
342
|
skipped: "already-running"
|
|
328
343
|
};
|
|
329
|
-
|
|
344
|
+
const release = await this.acquireMutex();
|
|
330
345
|
try {
|
|
331
346
|
return await this.runCore(options);
|
|
332
347
|
} finally {
|
|
333
|
-
|
|
348
|
+
release();
|
|
334
349
|
try {
|
|
335
350
|
await this.retainReports();
|
|
336
351
|
} catch (error) {
|
|
@@ -338,6 +353,36 @@ var EvolutionCurator = class extends Service {
|
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* P1 (v16): the control-plane mutex — ONE promise chain serializing run(),
|
|
358
|
+
* restore() and consolidate(). Replaces the v15 draft (`running` flag +
|
|
359
|
+
* `runSettled` polling), which could (a) spin forever on an
|
|
360
|
+
* already-resolved promise while a control-plane mutator held the flag
|
|
361
|
+
* (micro-task starvation: the flag's reset lives behind IO the spun loop
|
|
362
|
+
* never lets run) and (b) let two queued waiters wake into the same idle
|
|
363
|
+
* window and mutate concurrently. Here the chain IS the mutex: an entrant
|
|
364
|
+
* increments `mutexDepth` synchronously (so run()'s skip check sees queued
|
|
365
|
+
* work), awaits the previous tail, and the returned release resolves the
|
|
366
|
+
* tail for the next entrant. Double-release is a no-op.
|
|
367
|
+
*/
|
|
368
|
+
mutexDepth = 0;
|
|
369
|
+
mutexTail = Promise.resolve();
|
|
370
|
+
acquireMutex() {
|
|
371
|
+
this.mutexDepth += 1;
|
|
372
|
+
const prev = this.mutexTail;
|
|
373
|
+
let releaseMutex;
|
|
374
|
+
this.mutexTail = new Promise((resolve) => {
|
|
375
|
+
releaseMutex = resolve;
|
|
376
|
+
});
|
|
377
|
+
let released = false;
|
|
378
|
+
const release = () => {
|
|
379
|
+
if (released) return;
|
|
380
|
+
released = true;
|
|
381
|
+
this.mutexDepth -= 1;
|
|
382
|
+
releaseMutex();
|
|
383
|
+
};
|
|
384
|
+
return prev.then(() => release);
|
|
385
|
+
}
|
|
341
386
|
async runCore(options = {}) {
|
|
342
387
|
const { ignoreGates = false, dryRun = false } = options;
|
|
343
388
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -472,16 +517,20 @@ var EvolutionCurator = class extends Service {
|
|
|
472
517
|
}
|
|
473
518
|
const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
474
519
|
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
try {
|
|
521
|
+
await stateService?.transactCuratorState((current) => {
|
|
522
|
+
const pausedNow = current?.paused ?? false;
|
|
523
|
+
return {
|
|
524
|
+
schemaVersion: 1,
|
|
525
|
+
lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
|
|
526
|
+
runCount: dryRun ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
|
|
527
|
+
lastSummary: summary,
|
|
528
|
+
paused: pausedNow
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist run bookkeeping: ${error instanceof Error ? error.message : String(error)}`);
|
|
533
|
+
}
|
|
485
534
|
return {
|
|
486
535
|
stale: result.markStale,
|
|
487
536
|
archived: archivedSkills.map((item) => item.name),
|
|
@@ -513,6 +562,11 @@ var EvolutionCurator = class extends Service {
|
|
|
513
562
|
}
|
|
514
563
|
/**
|
|
515
564
|
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
565
|
+
* P1-1 (v15): these are the CURATOR-owned fields (`quality_score`/
|
|
566
|
+
* `quality_warn`) — this method must never touch the feedback-owned
|
|
567
|
+
* `feedback_*` pair (the lifecycle engine reads the union of both warn
|
|
568
|
+
* flags, so overwriting feedback here is what used to make negative
|
|
569
|
+
* feedback decision-irrelevant; field ownership on `UsageRecord`).
|
|
516
570
|
*/
|
|
517
571
|
async scoreTree(usage, treeNames) {
|
|
518
572
|
const supportDirs = /* @__PURE__ */ new Map();
|
|
@@ -846,6 +900,14 @@ var EvolutionCurator = class extends Service {
|
|
|
846
900
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
847
901
|
*/
|
|
848
902
|
async consolidate(target, sources) {
|
|
903
|
+
const release = await this.acquireMutex();
|
|
904
|
+
try {
|
|
905
|
+
return await this.consolidateMutate(target, sources);
|
|
906
|
+
} finally {
|
|
907
|
+
release();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async consolidateMutate(target, sources) {
|
|
849
911
|
const suppressedNames = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
850
912
|
const gates = new EvolutionGateSet({
|
|
851
913
|
exclude: this.excludeSkillNames,
|
|
@@ -876,6 +938,14 @@ var EvolutionCurator = class extends Service {
|
|
|
876
938
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
877
939
|
*/
|
|
878
940
|
async restore(name) {
|
|
941
|
+
const release = await this.acquireMutex();
|
|
942
|
+
try {
|
|
943
|
+
return await this.restoreMutate(name);
|
|
944
|
+
} finally {
|
|
945
|
+
release();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async restoreMutate(name) {
|
|
879
949
|
await this.snapshotFull("pre-restore");
|
|
880
950
|
const result = await this.skills.restoreFromArchive(name);
|
|
881
951
|
if (!result.ok) return result;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -110,11 +110,12 @@ export declare class EvolutionCurator extends Service {
|
|
|
110
110
|
private lastRun;
|
|
111
111
|
private timer;
|
|
112
112
|
private bootCheck;
|
|
113
|
-
private running;
|
|
114
113
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
115
114
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
116
115
|
* persisted===null defer repeats forever (no state service to persist). */
|
|
117
116
|
private statelessFirstRunDeferred;
|
|
117
|
+
/** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
|
|
118
|
+
private statelessStateWarned;
|
|
118
119
|
constructor(ctx: Context, config?: Config);
|
|
119
120
|
private lifecycle;
|
|
120
121
|
start(): void;
|
|
@@ -139,6 +140,12 @@ export declare class EvolutionCurator extends Service {
|
|
|
139
140
|
* HERE as a cheap pre-check (persisted or in-memory clock) AND again inside
|
|
140
141
|
* run() as the authoritative gate — the docstring no longer claims the two
|
|
141
142
|
* never duplicate; a future interval change must update both.
|
|
143
|
+
*
|
|
144
|
+
* P2-5 (v14): without the `evolution-state` service there is no durable
|
|
145
|
+
* `lastRunAt`, so the baseline degrades to this process's lifetime. That is
|
|
146
|
+
* a supported-but-degraded composition (every shipped bundle mounts the
|
|
147
|
+
* state rows), so the schedule is left as-is and the degradation is
|
|
148
|
+
* surfaced once instead of silently meaning "never runs".
|
|
142
149
|
*/
|
|
143
150
|
private autoCheck;
|
|
144
151
|
/**
|
|
@@ -184,6 +191,21 @@ export declare class EvolutionCurator extends Service {
|
|
|
184
191
|
ignoreGates?: boolean;
|
|
185
192
|
dryRun?: boolean;
|
|
186
193
|
}): Promise<CuratorRunOutcome>;
|
|
194
|
+
/**
|
|
195
|
+
* P1 (v16): the control-plane mutex — ONE promise chain serializing run(),
|
|
196
|
+
* restore() and consolidate(). Replaces the v15 draft (`running` flag +
|
|
197
|
+
* `runSettled` polling), which could (a) spin forever on an
|
|
198
|
+
* already-resolved promise while a control-plane mutator held the flag
|
|
199
|
+
* (micro-task starvation: the flag's reset lives behind IO the spun loop
|
|
200
|
+
* never lets run) and (b) let two queued waiters wake into the same idle
|
|
201
|
+
* window and mutate concurrently. Here the chain IS the mutex: an entrant
|
|
202
|
+
* increments `mutexDepth` synchronously (so run()'s skip check sees queued
|
|
203
|
+
* work), awaits the previous tail, and the returned release resolves the
|
|
204
|
+
* tail for the next entrant. Double-release is a no-op.
|
|
205
|
+
*/
|
|
206
|
+
private mutexDepth;
|
|
207
|
+
private mutexTail;
|
|
208
|
+
private acquireMutex;
|
|
187
209
|
private runCore;
|
|
188
210
|
/**
|
|
189
211
|
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
@@ -194,6 +216,11 @@ export declare class EvolutionCurator extends Service {
|
|
|
194
216
|
private seedBaseline;
|
|
195
217
|
/**
|
|
196
218
|
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
219
|
+
* P1-1 (v15): these are the CURATOR-owned fields (`quality_score`/
|
|
220
|
+
* `quality_warn`) — this method must never touch the feedback-owned
|
|
221
|
+
* `feedback_*` pair (the lifecycle engine reads the union of both warn
|
|
222
|
+
* flags, so overwriting feedback here is what used to make negative
|
|
223
|
+
* feedback decision-irrelevant; field ownership on `UsageRecord`).
|
|
197
224
|
*/
|
|
198
225
|
private scoreTree;
|
|
199
226
|
/**
|
|
@@ -254,11 +281,13 @@ export declare class EvolutionCurator extends Service {
|
|
|
254
281
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
255
282
|
*/
|
|
256
283
|
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
284
|
+
private consolidateMutate;
|
|
257
285
|
/**
|
|
258
286
|
* Control-plane restore: bring one archived skill back to the active root
|
|
259
287
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
260
288
|
*/
|
|
261
289
|
restore(name: string): Promise<SkillActionResult>;
|
|
290
|
+
private restoreMutate;
|
|
262
291
|
}
|
|
263
292
|
export default EvolutionCurator;
|
|
264
293
|
//# sourceMappingURL=index.d.ts.map
|
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.3.
|
|
4
|
+
"version": "0.3.63",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,20 +31,20 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.3.63"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
38
38
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
39
39
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
40
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
41
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
40
|
+
"@lmzhen/dsh-evolution-io": "^0.3.63",
|
|
41
|
+
"@lmzhen/dsh-evolution-state": "^0.3.63"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
45
45
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
46
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
47
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
48
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
46
|
+
"@lmzhen/dsh-evolution-core": "^0.3.63",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.3.63",
|
|
48
|
+
"@lmzhen/dsh-evolution-state": "^0.3.63"
|
|
49
49
|
}
|
|
50
50
|
}
|