@lmzhen/dsh-evolution-curator 0.3.62 → 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 +75 -18
- package/lib/types/index.d.ts +22 -1
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -68,7 +68,6 @@ 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). */
|
|
@@ -83,8 +82,11 @@ var EvolutionCurator = class extends Service {
|
|
|
83
82
|
});
|
|
84
83
|
this.enabled = config.enabled ?? true;
|
|
85
84
|
const clamped = [];
|
|
86
|
-
const field = (name, value, fallback, min) => {
|
|
87
|
-
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
|
+
});
|
|
88
90
|
if (value !== void 0 && result !== value) clamped.push(name);
|
|
89
91
|
return result;
|
|
90
92
|
};
|
|
@@ -100,7 +102,7 @@ var EvolutionCurator = class extends Service {
|
|
|
100
102
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
101
103
|
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
102
104
|
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
103
|
-
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0);
|
|
105
|
+
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
|
|
104
106
|
this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
|
|
105
107
|
this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
|
|
106
108
|
this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
|
|
@@ -185,7 +187,7 @@ var EvolutionCurator = class extends Service {
|
|
|
185
187
|
const stateService = this.curatorStateService();
|
|
186
188
|
if (stateService === void 0 && !this.statelessStateWarned) {
|
|
187
189
|
this.statelessStateWarned = true;
|
|
188
|
-
this.ctx.logger.warn(
|
|
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.`);
|
|
189
191
|
}
|
|
190
192
|
const last = (await stateService?.loadCuratorState())?.lastRunAt ?? this.lastRun;
|
|
191
193
|
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
@@ -332,18 +334,18 @@ var EvolutionCurator = class extends Service {
|
|
|
332
334
|
* skipped with an explicit `already-running` outcome.
|
|
333
335
|
*/
|
|
334
336
|
async run(options = {}) {
|
|
335
|
-
if (this.
|
|
337
|
+
if (this.mutexDepth > 0) return {
|
|
336
338
|
stale: [],
|
|
337
339
|
archived: [],
|
|
338
340
|
errors: [],
|
|
339
341
|
report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
|
|
340
342
|
skipped: "already-running"
|
|
341
343
|
};
|
|
342
|
-
|
|
344
|
+
const release = await this.acquireMutex();
|
|
343
345
|
try {
|
|
344
346
|
return await this.runCore(options);
|
|
345
347
|
} finally {
|
|
346
|
-
|
|
348
|
+
release();
|
|
347
349
|
try {
|
|
348
350
|
await this.retainReports();
|
|
349
351
|
} catch (error) {
|
|
@@ -351,6 +353,36 @@ var EvolutionCurator = class extends Service {
|
|
|
351
353
|
}
|
|
352
354
|
}
|
|
353
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
|
+
}
|
|
354
386
|
async runCore(options = {}) {
|
|
355
387
|
const { ignoreGates = false, dryRun = false } = options;
|
|
356
388
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -485,16 +517,20 @@ var EvolutionCurator = class extends Service {
|
|
|
485
517
|
}
|
|
486
518
|
const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
487
519
|
const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
534
|
return {
|
|
499
535
|
stale: result.markStale,
|
|
500
536
|
archived: archivedSkills.map((item) => item.name),
|
|
@@ -526,6 +562,11 @@ var EvolutionCurator = class extends Service {
|
|
|
526
562
|
}
|
|
527
563
|
/**
|
|
528
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`).
|
|
529
570
|
*/
|
|
530
571
|
async scoreTree(usage, treeNames) {
|
|
531
572
|
const supportDirs = /* @__PURE__ */ new Map();
|
|
@@ -859,6 +900,14 @@ var EvolutionCurator = class extends Service {
|
|
|
859
900
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
860
901
|
*/
|
|
861
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) {
|
|
862
911
|
const suppressedNames = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
863
912
|
const gates = new EvolutionGateSet({
|
|
864
913
|
exclude: this.excludeSkillNames,
|
|
@@ -889,6 +938,14 @@ var EvolutionCurator = class extends Service {
|
|
|
889
938
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
890
939
|
*/
|
|
891
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) {
|
|
892
949
|
await this.snapshotFull("pre-restore");
|
|
893
950
|
const result = await this.skills.restoreFromArchive(name);
|
|
894
951
|
if (!result.ok) return result;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -110,7 +110,6 @@ 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). */
|
|
@@ -192,6 +191,21 @@ export declare class EvolutionCurator extends Service {
|
|
|
192
191
|
ignoreGates?: boolean;
|
|
193
192
|
dryRun?: boolean;
|
|
194
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;
|
|
195
209
|
private runCore;
|
|
196
210
|
/**
|
|
197
211
|
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
@@ -202,6 +216,11 @@ export declare class EvolutionCurator extends Service {
|
|
|
202
216
|
private seedBaseline;
|
|
203
217
|
/**
|
|
204
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`).
|
|
205
224
|
*/
|
|
206
225
|
private scoreTree;
|
|
207
226
|
/**
|
|
@@ -262,11 +281,13 @@ export declare class EvolutionCurator extends Service {
|
|
|
262
281
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
263
282
|
*/
|
|
264
283
|
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
284
|
+
private consolidateMutate;
|
|
265
285
|
/**
|
|
266
286
|
* Control-plane restore: bring one archived skill back to the active root
|
|
267
287
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
268
288
|
*/
|
|
269
289
|
restore(name: string): Promise<SkillActionResult>;
|
|
290
|
+
private restoreMutate;
|
|
270
291
|
}
|
|
271
292
|
export default EvolutionCurator;
|
|
272
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
|
}
|