@lmzhen/dsh-evolution-curator 0.3.62 → 0.3.64
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 +146 -38
- package/lib/types/index.d.ts +34 -1
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -11,6 +11,13 @@ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEF
|
|
|
11
11
|
*/
|
|
12
12
|
/** Quality-warned skills may turn stale after this many idle days (package-private tunable, P2-8). */
|
|
13
13
|
const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
|
|
14
|
+
/** B-10 (v18): the optional LLM nomination pass gets its own timeout so a
|
|
15
|
+
* hung/unresponsive provider cannot hold the control-plane mutex forever
|
|
16
|
+
* (`run()` never returns; `restore()`/`consolidate()` queue behind it).
|
|
17
|
+
* 120s matches the review subagent default; the 32-bit ceiling is Node's
|
|
18
|
+
* timer-delay limit (`AbortSignal.timeout` throws above it). */
|
|
19
|
+
const DEFAULT_CURATOR_REVIEW_TIMEOUT_MS = 12e4;
|
|
20
|
+
const MAX_TIMER_DELAY_MS = 4294967295;
|
|
14
21
|
/**
|
|
15
22
|
* Block LLM-nominated consolidations that would touch a gate-protected name:
|
|
16
23
|
* exclude / referenced / suppressed skills must never merge (neither as the
|
|
@@ -41,6 +48,7 @@ var EvolutionCurator = class extends Service {
|
|
|
41
48
|
autoStart: z.boolean().default(true),
|
|
42
49
|
bootGraceSeconds: z.number().min(0).default(DEFAULT_CURATOR_BOOT_GRACE_SECONDS),
|
|
43
50
|
curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
|
|
51
|
+
curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
|
|
44
52
|
healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
|
|
45
53
|
healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
|
|
46
54
|
healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
|
|
@@ -62,13 +70,23 @@ var EvolutionCurator = class extends Service {
|
|
|
62
70
|
referencedSkillNames;
|
|
63
71
|
bootGraceSeconds;
|
|
64
72
|
curatorReviewMaxTokens;
|
|
73
|
+
curatorReviewTimeoutMs;
|
|
65
74
|
healthSoftBodyChars;
|
|
66
75
|
healthStampDensityPerKb;
|
|
67
76
|
healthChurnMinPatches;
|
|
68
77
|
lastRun = 0;
|
|
69
78
|
timer;
|
|
79
|
+
/** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
|
|
80
|
+
* keep mutating the tree after the plugin was disposed. */
|
|
81
|
+
disposed = false;
|
|
82
|
+
/** B-8 (v18): read the disposal flag through a method. The only assignment
|
|
83
|
+
* lives in the disposer closure, which TypeScript's flow analysis cannot
|
|
84
|
+
* see, so a direct `this.disposed` read narrows to the literal `false` and
|
|
85
|
+
* the runtime check would be reported as dead code by `no-unnecessary-condition`. */
|
|
86
|
+
isDisposed() {
|
|
87
|
+
return this.disposed;
|
|
88
|
+
}
|
|
70
89
|
bootCheck;
|
|
71
|
-
running = false;
|
|
72
90
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
73
91
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
74
92
|
* persisted===null defer repeats forever (no state service to persist). */
|
|
@@ -83,14 +101,21 @@ var EvolutionCurator = class extends Service {
|
|
|
83
101
|
});
|
|
84
102
|
this.enabled = config.enabled ?? true;
|
|
85
103
|
const clamped = [];
|
|
86
|
-
const field = (name, value, fallback, min) => {
|
|
87
|
-
const result = clampedNumber(value, fallback, { min }
|
|
104
|
+
const field = (name, value, fallback, min, max) => {
|
|
105
|
+
const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
|
|
106
|
+
min,
|
|
107
|
+
max
|
|
108
|
+
});
|
|
88
109
|
if (value !== void 0 && result !== value) clamped.push(name);
|
|
89
110
|
return result;
|
|
90
111
|
};
|
|
91
112
|
this.intervalHours = field("intervalHours", config.intervalHours, DEFAULT_CURATOR_INTERVAL_HOURS, 1);
|
|
92
113
|
this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
|
|
93
114
|
this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
|
|
115
|
+
if (this.archiveAfterDays < this.staleAfterDays) {
|
|
116
|
+
this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${this.archiveAfterDays}) < staleAfterDays (${this.staleAfterDays}); using staleAfterDays as the archive threshold`);
|
|
117
|
+
this.archiveAfterDays = this.staleAfterDays;
|
|
118
|
+
}
|
|
94
119
|
this.llmReview = config.llmReview ?? false;
|
|
95
120
|
this.curatorProvider = config.curatorProvider ?? "deepseek-official";
|
|
96
121
|
this.qualityWarnStaleAfterDays = field("qualityWarnStaleAfterDays", config.qualityWarnStaleAfterDays, DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS, 1);
|
|
@@ -100,8 +125,9 @@ var EvolutionCurator = class extends Service {
|
|
|
100
125
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
101
126
|
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
102
127
|
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
103
|
-
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0);
|
|
128
|
+
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
|
|
104
129
|
this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
|
|
130
|
+
this.curatorReviewTimeoutMs = field("curatorReviewTimeoutMs", config.curatorReviewTimeoutMs, DEFAULT_CURATOR_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS);
|
|
105
131
|
this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
|
|
106
132
|
this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
|
|
107
133
|
this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
|
|
@@ -109,6 +135,7 @@ var EvolutionCurator = class extends Service {
|
|
|
109
135
|
this.lastRun = Date.now();
|
|
110
136
|
this.ctx.effect(() => {
|
|
111
137
|
return () => {
|
|
138
|
+
this.disposed = true;
|
|
112
139
|
this.stop();
|
|
113
140
|
};
|
|
114
141
|
}, "evolution-curator.stop");
|
|
@@ -123,7 +150,7 @@ var EvolutionCurator = class extends Service {
|
|
|
123
150
|
};
|
|
124
151
|
}
|
|
125
152
|
start() {
|
|
126
|
-
if (!this.enabled || this.timer) return;
|
|
153
|
+
if (this.isDisposed() || !this.enabled || this.timer) return;
|
|
127
154
|
this.bootCheck = setTimeout(() => {
|
|
128
155
|
this.bootCheck = void 0;
|
|
129
156
|
this.autoCheck();
|
|
@@ -181,13 +208,16 @@ var EvolutionCurator = class extends Service {
|
|
|
181
208
|
* surfaced once instead of silently meaning "never runs".
|
|
182
209
|
*/
|
|
183
210
|
async autoCheck() {
|
|
211
|
+
if (this.isDisposed()) return;
|
|
184
212
|
try {
|
|
185
213
|
const stateService = this.curatorStateService();
|
|
186
214
|
if (stateService === void 0 && !this.statelessStateWarned) {
|
|
187
215
|
this.statelessStateWarned = true;
|
|
188
|
-
this.ctx.logger.warn(
|
|
216
|
+
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
217
|
}
|
|
190
|
-
const
|
|
218
|
+
const persisted = await stateService?.loadCuratorState();
|
|
219
|
+
if (this.isDisposed()) return;
|
|
220
|
+
const last = persisted?.lastRunAt ?? this.lastRun;
|
|
191
221
|
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
192
222
|
} catch (error) {
|
|
193
223
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -256,7 +286,8 @@ var EvolutionCurator = class extends Service {
|
|
|
256
286
|
summary: "curator review"
|
|
257
287
|
}
|
|
258
288
|
})],
|
|
259
|
-
maxTokens: this.curatorReviewMaxTokens
|
|
289
|
+
maxTokens: this.curatorReviewMaxTokens,
|
|
290
|
+
signal: AbortSignal.timeout(this.curatorReviewTimeoutMs)
|
|
260
291
|
})) assembler.push(chunk);
|
|
261
292
|
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
262
293
|
return {
|
|
@@ -294,6 +325,14 @@ var EvolutionCurator = class extends Service {
|
|
|
294
325
|
* is reversible.
|
|
295
326
|
*/
|
|
296
327
|
async restoreSnapshot() {
|
|
328
|
+
const release = await this.acquireMutex();
|
|
329
|
+
try {
|
|
330
|
+
return await this.restoreSnapshotCore();
|
|
331
|
+
} finally {
|
|
332
|
+
release();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async restoreSnapshotCore() {
|
|
297
336
|
const stateService = this.curatorStateService();
|
|
298
337
|
const currentState = await stateService?.loadCuratorState();
|
|
299
338
|
const extras = currentState === null || currentState === void 0 ? [] : [{
|
|
@@ -332,18 +371,18 @@ var EvolutionCurator = class extends Service {
|
|
|
332
371
|
* skipped with an explicit `already-running` outcome.
|
|
333
372
|
*/
|
|
334
373
|
async run(options = {}) {
|
|
335
|
-
if (this.
|
|
374
|
+
if (this.mutexDepth > 0) return {
|
|
336
375
|
stale: [],
|
|
337
376
|
archived: [],
|
|
338
377
|
errors: [],
|
|
339
378
|
report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
|
|
340
379
|
skipped: "already-running"
|
|
341
380
|
};
|
|
342
|
-
|
|
381
|
+
const release = await this.acquireMutex();
|
|
343
382
|
try {
|
|
344
383
|
return await this.runCore(options);
|
|
345
384
|
} finally {
|
|
346
|
-
|
|
385
|
+
release();
|
|
347
386
|
try {
|
|
348
387
|
await this.retainReports();
|
|
349
388
|
} catch (error) {
|
|
@@ -351,6 +390,36 @@ var EvolutionCurator = class extends Service {
|
|
|
351
390
|
}
|
|
352
391
|
}
|
|
353
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* P1 (v16): the control-plane mutex — ONE promise chain serializing run(),
|
|
395
|
+
* restore() and consolidate(). Replaces the v15 draft (`running` flag +
|
|
396
|
+
* `runSettled` polling), which could (a) spin forever on an
|
|
397
|
+
* already-resolved promise while a control-plane mutator held the flag
|
|
398
|
+
* (micro-task starvation: the flag's reset lives behind IO the spun loop
|
|
399
|
+
* never lets run) and (b) let two queued waiters wake into the same idle
|
|
400
|
+
* window and mutate concurrently. Here the chain IS the mutex: an entrant
|
|
401
|
+
* increments `mutexDepth` synchronously (so run()'s skip check sees queued
|
|
402
|
+
* work), awaits the previous tail, and the returned release resolves the
|
|
403
|
+
* tail for the next entrant. Double-release is a no-op.
|
|
404
|
+
*/
|
|
405
|
+
mutexDepth = 0;
|
|
406
|
+
mutexTail = Promise.resolve();
|
|
407
|
+
acquireMutex() {
|
|
408
|
+
this.mutexDepth += 1;
|
|
409
|
+
const prev = this.mutexTail;
|
|
410
|
+
let releaseMutex;
|
|
411
|
+
this.mutexTail = new Promise((resolve) => {
|
|
412
|
+
releaseMutex = resolve;
|
|
413
|
+
});
|
|
414
|
+
let released = false;
|
|
415
|
+
const release = () => {
|
|
416
|
+
if (released) return;
|
|
417
|
+
released = true;
|
|
418
|
+
this.mutexDepth -= 1;
|
|
419
|
+
releaseMutex();
|
|
420
|
+
};
|
|
421
|
+
return prev.then(() => release);
|
|
422
|
+
}
|
|
354
423
|
async runCore(options = {}) {
|
|
355
424
|
const { ignoreGates = false, dryRun = false } = options;
|
|
356
425
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -402,6 +471,7 @@ var EvolutionCurator = class extends Service {
|
|
|
402
471
|
const root = this.skills.root;
|
|
403
472
|
const rawUsage = await loadUsage(root, this.io);
|
|
404
473
|
const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
|
|
474
|
+
const runStartStates = new Map([...usage].map(([name, record]) => [name, record.state]));
|
|
405
475
|
const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
|
|
406
476
|
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
407
477
|
const gates = new EvolutionGateSet({
|
|
@@ -453,6 +523,7 @@ var EvolutionCurator = class extends Service {
|
|
|
453
523
|
root,
|
|
454
524
|
recommendPool: new Set(recommendPool),
|
|
455
525
|
stateOwned: new Set([...result.transitions.map((t) => t.name), ...archiveCandidates]),
|
|
526
|
+
runStartStates,
|
|
456
527
|
failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
|
|
457
528
|
});
|
|
458
529
|
if (!dryRun) this.lastRun = Date.now();
|
|
@@ -485,16 +556,20 @@ var EvolutionCurator = class extends Service {
|
|
|
485
556
|
}
|
|
486
557
|
const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
|
|
487
558
|
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
|
-
|
|
559
|
+
try {
|
|
560
|
+
await stateService?.transactCuratorState((current) => {
|
|
561
|
+
const pausedNow = current?.paused ?? false;
|
|
562
|
+
return {
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
|
|
565
|
+
runCount: dryRun ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
|
|
566
|
+
lastSummary: summary,
|
|
567
|
+
paused: pausedNow
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
571
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist run bookkeeping: ${error instanceof Error ? error.message : String(error)}`);
|
|
572
|
+
}
|
|
498
573
|
return {
|
|
499
574
|
stale: result.markStale,
|
|
500
575
|
archived: archivedSkills.map((item) => item.name),
|
|
@@ -526,6 +601,11 @@ var EvolutionCurator = class extends Service {
|
|
|
526
601
|
}
|
|
527
602
|
/**
|
|
528
603
|
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
604
|
+
* P1-1 (v15): these are the CURATOR-owned fields (`quality_score`/
|
|
605
|
+
* `quality_warn`) — this method must never touch the feedback-owned
|
|
606
|
+
* `feedback_*` pair (the lifecycle engine reads the union of both warn
|
|
607
|
+
* flags, so overwriting feedback here is what used to make negative
|
|
608
|
+
* feedback decision-irrelevant; field ownership on `UsageRecord`).
|
|
529
609
|
*/
|
|
530
610
|
async scoreTree(usage, treeNames) {
|
|
531
611
|
const supportDirs = /* @__PURE__ */ new Map();
|
|
@@ -571,6 +651,7 @@ var EvolutionCurator = class extends Service {
|
|
|
571
651
|
consolidated: []
|
|
572
652
|
};
|
|
573
653
|
const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root, failedFrom } = input;
|
|
654
|
+
const runStartStates = input.runStartStates;
|
|
574
655
|
const errors = [];
|
|
575
656
|
const archivedSkills = [];
|
|
576
657
|
const executedConsolidations = [];
|
|
@@ -684,9 +765,11 @@ var EvolutionCurator = class extends Service {
|
|
|
684
765
|
this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
|
|
685
766
|
}
|
|
686
767
|
try {
|
|
768
|
+
let skipped = [];
|
|
687
769
|
await mutateUsage(root, this.io, (disk) => {
|
|
688
|
-
foldCuratorFields(disk, usage, stateOwned);
|
|
770
|
+
skipped = foldCuratorFields(disk, usage, stateOwned, runStartStates);
|
|
689
771
|
});
|
|
772
|
+
if (skipped.length > 0) this.ctx.logger.warn(`evolution-curator: lifecycle fold skipped ${skipped.length} name(s) whose on-disk state moved during the run (a concurrent curator/tool won): ${skipped.join(", ")}`);
|
|
690
773
|
} catch {
|
|
691
774
|
this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
|
|
692
775
|
}
|
|
@@ -850,7 +933,8 @@ var EvolutionCurator = class extends Service {
|
|
|
850
933
|
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
851
934
|
async protectedNameMap() {
|
|
852
935
|
const map = /* @__PURE__ */ new Map();
|
|
853
|
-
for (const summary of await this.skills.list()) if (summary.
|
|
936
|
+
for (const summary of await this.skills.list()) if (summary.protectionUnknown) map.set(summary.name, "unknown");
|
|
937
|
+
else if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
|
|
854
938
|
return map;
|
|
855
939
|
}
|
|
856
940
|
/**
|
|
@@ -859,6 +943,14 @@ var EvolutionCurator = class extends Service {
|
|
|
859
943
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
860
944
|
*/
|
|
861
945
|
async consolidate(target, sources) {
|
|
946
|
+
const release = await this.acquireMutex();
|
|
947
|
+
try {
|
|
948
|
+
return await this.consolidateMutate(target, sources);
|
|
949
|
+
} finally {
|
|
950
|
+
release();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async consolidateMutate(target, sources) {
|
|
862
954
|
const suppressedNames = new Set(await loadSuppressedNames(this.skills.root, this.io));
|
|
863
955
|
const gates = new EvolutionGateSet({
|
|
864
956
|
exclude: this.excludeSkillNames,
|
|
@@ -873,15 +965,19 @@ var EvolutionCurator = class extends Service {
|
|
|
873
965
|
await this.snapshotFull("pre-consolidate");
|
|
874
966
|
const result = await this.skills.consolidate(target, sources);
|
|
875
967
|
if (!result.ok) return result;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
record
|
|
881
|
-
|
|
968
|
+
try {
|
|
969
|
+
await mutateUsage(this.skills.root, this.io, (disk) => {
|
|
970
|
+
for (const source of sources) {
|
|
971
|
+
const record = disk.get(source);
|
|
972
|
+
if (record) {
|
|
973
|
+
record.state = "archived";
|
|
974
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
975
|
+
}
|
|
882
976
|
}
|
|
883
|
-
}
|
|
884
|
-
})
|
|
977
|
+
});
|
|
978
|
+
} catch (error) {
|
|
979
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist consolidate usage state: ${error instanceof Error ? error.message : String(error)}`);
|
|
980
|
+
}
|
|
885
981
|
return result;
|
|
886
982
|
}
|
|
887
983
|
/**
|
|
@@ -889,16 +985,28 @@ var EvolutionCurator = class extends Service {
|
|
|
889
985
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
890
986
|
*/
|
|
891
987
|
async restore(name) {
|
|
988
|
+
const release = await this.acquireMutex();
|
|
989
|
+
try {
|
|
990
|
+
return await this.restoreMutate(name);
|
|
991
|
+
} finally {
|
|
992
|
+
release();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async restoreMutate(name) {
|
|
892
996
|
await this.snapshotFull("pre-restore");
|
|
893
997
|
const result = await this.skills.restoreFromArchive(name);
|
|
894
998
|
if (!result.ok) return result;
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
record
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
999
|
+
try {
|
|
1000
|
+
await mutateUsage(this.skills.root, this.io, (disk) => {
|
|
1001
|
+
const record = disk.get(name);
|
|
1002
|
+
if (record) {
|
|
1003
|
+
record.state = "active";
|
|
1004
|
+
record.archived_at = null;
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist restore usage state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1009
|
+
}
|
|
902
1010
|
if (new Set(await loadSuppressedNames(this.skills.root, this.io)).has(name)) try {
|
|
903
1011
|
await updateSuppressedNames(this.skills.root, this.io, (current) => {
|
|
904
1012
|
current.delete(name);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface Config {
|
|
|
47
47
|
bootGraceSeconds?: number;
|
|
48
48
|
/** Max tokens for the optional LLM nomination pass. */
|
|
49
49
|
curatorReviewMaxTokens?: number;
|
|
50
|
+
/** Timeout (ms) for the optional LLM nomination pass (B-10, v18). */
|
|
51
|
+
curatorReviewTimeoutMs?: number;
|
|
50
52
|
/** Structure-health soft body limit (chars) — see DEFAULT_HEALTH_THRESHOLDS (rc.73 A1). */
|
|
51
53
|
healthSoftBodyChars?: number;
|
|
52
54
|
/** Structure-health stamp-density ceiling per KB — see DEFAULT_HEALTH_THRESHOLDS. */
|
|
@@ -104,13 +106,21 @@ export declare class EvolutionCurator extends Service {
|
|
|
104
106
|
private readonly referencedSkillNames;
|
|
105
107
|
private readonly bootGraceSeconds;
|
|
106
108
|
private readonly curatorReviewMaxTokens;
|
|
109
|
+
private readonly curatorReviewTimeoutMs;
|
|
107
110
|
private readonly healthSoftBodyChars;
|
|
108
111
|
private readonly healthStampDensityPerKb;
|
|
109
112
|
private readonly healthChurnMinPatches;
|
|
110
113
|
private lastRun;
|
|
111
114
|
private timer;
|
|
115
|
+
/** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
|
|
116
|
+
* keep mutating the tree after the plugin was disposed. */
|
|
117
|
+
private disposed;
|
|
118
|
+
/** B-8 (v18): read the disposal flag through a method. The only assignment
|
|
119
|
+
* lives in the disposer closure, which TypeScript's flow analysis cannot
|
|
120
|
+
* see, so a direct `this.disposed` read narrows to the literal `false` and
|
|
121
|
+
* the runtime check would be reported as dead code by `no-unnecessary-condition`. */
|
|
122
|
+
private isDisposed;
|
|
112
123
|
private bootCheck;
|
|
113
|
-
private running;
|
|
114
124
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
115
125
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
116
126
|
* persisted===null defer repeats forever (no state service to persist). */
|
|
@@ -179,6 +189,7 @@ export declare class EvolutionCurator extends Service {
|
|
|
179
189
|
content: string;
|
|
180
190
|
}>;
|
|
181
191
|
}>;
|
|
192
|
+
private restoreSnapshotCore;
|
|
182
193
|
private skippedReport;
|
|
183
194
|
/**
|
|
184
195
|
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
@@ -192,6 +203,21 @@ export declare class EvolutionCurator extends Service {
|
|
|
192
203
|
ignoreGates?: boolean;
|
|
193
204
|
dryRun?: boolean;
|
|
194
205
|
}): Promise<CuratorRunOutcome>;
|
|
206
|
+
/**
|
|
207
|
+
* P1 (v16): the control-plane mutex — ONE promise chain serializing run(),
|
|
208
|
+
* restore() and consolidate(). Replaces the v15 draft (`running` flag +
|
|
209
|
+
* `runSettled` polling), which could (a) spin forever on an
|
|
210
|
+
* already-resolved promise while a control-plane mutator held the flag
|
|
211
|
+
* (micro-task starvation: the flag's reset lives behind IO the spun loop
|
|
212
|
+
* never lets run) and (b) let two queued waiters wake into the same idle
|
|
213
|
+
* window and mutate concurrently. Here the chain IS the mutex: an entrant
|
|
214
|
+
* increments `mutexDepth` synchronously (so run()'s skip check sees queued
|
|
215
|
+
* work), awaits the previous tail, and the returned release resolves the
|
|
216
|
+
* tail for the next entrant. Double-release is a no-op.
|
|
217
|
+
*/
|
|
218
|
+
private mutexDepth;
|
|
219
|
+
private mutexTail;
|
|
220
|
+
private acquireMutex;
|
|
195
221
|
private runCore;
|
|
196
222
|
/**
|
|
197
223
|
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
@@ -202,6 +228,11 @@ export declare class EvolutionCurator extends Service {
|
|
|
202
228
|
private seedBaseline;
|
|
203
229
|
/**
|
|
204
230
|
* F13 six-factor quality scoring, persisted onto the usage records.
|
|
231
|
+
* P1-1 (v15): these are the CURATOR-owned fields (`quality_score`/
|
|
232
|
+
* `quality_warn`) — this method must never touch the feedback-owned
|
|
233
|
+
* `feedback_*` pair (the lifecycle engine reads the union of both warn
|
|
234
|
+
* flags, so overwriting feedback here is what used to make negative
|
|
235
|
+
* feedback decision-irrelevant; field ownership on `UsageRecord`).
|
|
205
236
|
*/
|
|
206
237
|
private scoreTree;
|
|
207
238
|
/**
|
|
@@ -262,11 +293,13 @@ export declare class EvolutionCurator extends Service {
|
|
|
262
293
|
* records into `archived` state. Snapshot-then-mutate, never a hard delete.
|
|
263
294
|
*/
|
|
264
295
|
consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
|
|
296
|
+
private consolidateMutate;
|
|
265
297
|
/**
|
|
266
298
|
* Control-plane restore: bring one archived skill back to the active root
|
|
267
299
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
268
300
|
*/
|
|
269
301
|
restore(name: string): Promise<SkillActionResult>;
|
|
302
|
+
private restoreMutate;
|
|
270
303
|
}
|
|
271
304
|
export default EvolutionCurator;
|
|
272
305
|
//# 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.64",
|
|
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.64"
|
|
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.64",
|
|
41
|
+
"@lmzhen/dsh-evolution-state": "^0.3.64"
|
|
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.64",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.3.64",
|
|
48
|
+
"@lmzhen/dsh-evolution-state": "^0.3.64"
|
|
49
49
|
}
|
|
50
50
|
}
|