@lmzhen/dsh-evolution-curator 0.3.63 → 0.3.65
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 +78 -21
- package/lib/types/index.d.ts +12 -0
- 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 = 2147483647;
|
|
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,11 +70,22 @@ 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
90
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
72
91
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
@@ -93,6 +112,10 @@ var EvolutionCurator = class extends Service {
|
|
|
93
112
|
this.intervalHours = field("intervalHours", config.intervalHours, DEFAULT_CURATOR_INTERVAL_HOURS, 1);
|
|
94
113
|
this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
|
|
95
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
|
+
}
|
|
96
119
|
this.llmReview = config.llmReview ?? false;
|
|
97
120
|
this.curatorProvider = config.curatorProvider ?? "deepseek-official";
|
|
98
121
|
this.qualityWarnStaleAfterDays = field("qualityWarnStaleAfterDays", config.qualityWarnStaleAfterDays, DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS, 1);
|
|
@@ -104,6 +127,7 @@ var EvolutionCurator = class extends Service {
|
|
|
104
127
|
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
105
128
|
this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
|
|
106
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);
|
|
107
131
|
this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
|
|
108
132
|
this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
|
|
109
133
|
this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
|
|
@@ -111,6 +135,7 @@ var EvolutionCurator = class extends Service {
|
|
|
111
135
|
this.lastRun = Date.now();
|
|
112
136
|
this.ctx.effect(() => {
|
|
113
137
|
return () => {
|
|
138
|
+
this.disposed = true;
|
|
114
139
|
this.stop();
|
|
115
140
|
};
|
|
116
141
|
}, "evolution-curator.stop");
|
|
@@ -125,7 +150,7 @@ var EvolutionCurator = class extends Service {
|
|
|
125
150
|
};
|
|
126
151
|
}
|
|
127
152
|
start() {
|
|
128
|
-
if (!this.enabled || this.timer) return;
|
|
153
|
+
if (this.isDisposed() || !this.enabled || this.timer) return;
|
|
129
154
|
this.bootCheck = setTimeout(() => {
|
|
130
155
|
this.bootCheck = void 0;
|
|
131
156
|
this.autoCheck();
|
|
@@ -183,13 +208,16 @@ var EvolutionCurator = class extends Service {
|
|
|
183
208
|
* surfaced once instead of silently meaning "never runs".
|
|
184
209
|
*/
|
|
185
210
|
async autoCheck() {
|
|
211
|
+
if (this.isDisposed()) return;
|
|
186
212
|
try {
|
|
187
213
|
const stateService = this.curatorStateService();
|
|
188
214
|
if (stateService === void 0 && !this.statelessStateWarned) {
|
|
189
215
|
this.statelessStateWarned = true;
|
|
190
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.`);
|
|
191
217
|
}
|
|
192
|
-
const
|
|
218
|
+
const persisted = await stateService?.loadCuratorState();
|
|
219
|
+
if (this.isDisposed()) return;
|
|
220
|
+
const last = persisted?.lastRunAt ?? this.lastRun;
|
|
193
221
|
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
194
222
|
} catch (error) {
|
|
195
223
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -258,7 +286,8 @@ var EvolutionCurator = class extends Service {
|
|
|
258
286
|
summary: "curator review"
|
|
259
287
|
}
|
|
260
288
|
})],
|
|
261
|
-
maxTokens: this.curatorReviewMaxTokens
|
|
289
|
+
maxTokens: this.curatorReviewMaxTokens,
|
|
290
|
+
signal: AbortSignal.timeout(this.curatorReviewTimeoutMs)
|
|
262
291
|
})) assembler.push(chunk);
|
|
263
292
|
const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
|
|
264
293
|
return {
|
|
@@ -296,6 +325,14 @@ var EvolutionCurator = class extends Service {
|
|
|
296
325
|
* is reversible.
|
|
297
326
|
*/
|
|
298
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() {
|
|
299
336
|
const stateService = this.curatorStateService();
|
|
300
337
|
const currentState = await stateService?.loadCuratorState();
|
|
301
338
|
const extras = currentState === null || currentState === void 0 ? [] : [{
|
|
@@ -434,6 +471,7 @@ var EvolutionCurator = class extends Service {
|
|
|
434
471
|
const root = this.skills.root;
|
|
435
472
|
const rawUsage = await loadUsage(root, this.io);
|
|
436
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]));
|
|
437
475
|
const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
|
|
438
476
|
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
439
477
|
const gates = new EvolutionGateSet({
|
|
@@ -485,6 +523,7 @@ var EvolutionCurator = class extends Service {
|
|
|
485
523
|
root,
|
|
486
524
|
recommendPool: new Set(recommendPool),
|
|
487
525
|
stateOwned: new Set([...result.transitions.map((t) => t.name), ...archiveCandidates]),
|
|
526
|
+
runStartStates,
|
|
488
527
|
failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
|
|
489
528
|
});
|
|
490
529
|
if (!dryRun) this.lastRun = Date.now();
|
|
@@ -612,6 +651,7 @@ var EvolutionCurator = class extends Service {
|
|
|
612
651
|
consolidated: []
|
|
613
652
|
};
|
|
614
653
|
const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root, failedFrom } = input;
|
|
654
|
+
const runStartStates = input.runStartStates;
|
|
615
655
|
const errors = [];
|
|
616
656
|
const archivedSkills = [];
|
|
617
657
|
const executedConsolidations = [];
|
|
@@ -725,9 +765,13 @@ var EvolutionCurator = class extends Service {
|
|
|
725
765
|
this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
|
|
726
766
|
}
|
|
727
767
|
try {
|
|
768
|
+
let skipped = [];
|
|
728
769
|
await mutateUsage(root, this.io, (disk) => {
|
|
729
|
-
foldCuratorFields(disk, usage, stateOwned);
|
|
730
|
-
})
|
|
770
|
+
skipped = foldCuratorFields(disk, usage, stateOwned, runStartStates);
|
|
771
|
+
}, { onQuarantine: (message) => {
|
|
772
|
+
this.ctx.logger.warn(`evolution-curator: ${message}`);
|
|
773
|
+
} });
|
|
774
|
+
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(", ")}`);
|
|
731
775
|
} catch {
|
|
732
776
|
this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
|
|
733
777
|
}
|
|
@@ -891,7 +935,8 @@ var EvolutionCurator = class extends Service {
|
|
|
891
935
|
/** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
|
|
892
936
|
async protectedNameMap() {
|
|
893
937
|
const map = /* @__PURE__ */ new Map();
|
|
894
|
-
for (const summary of await this.skills.list()) if (summary.
|
|
938
|
+
for (const summary of await this.skills.list()) if (summary.protectionUnknown) map.set(summary.name, "unknown");
|
|
939
|
+
else if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
|
|
895
940
|
return map;
|
|
896
941
|
}
|
|
897
942
|
/**
|
|
@@ -922,15 +967,21 @@ var EvolutionCurator = class extends Service {
|
|
|
922
967
|
await this.snapshotFull("pre-consolidate");
|
|
923
968
|
const result = await this.skills.consolidate(target, sources);
|
|
924
969
|
if (!result.ok) return result;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
record
|
|
930
|
-
|
|
970
|
+
try {
|
|
971
|
+
await mutateUsage(this.skills.root, this.io, (disk) => {
|
|
972
|
+
for (const source of sources) {
|
|
973
|
+
const record = disk.get(source);
|
|
974
|
+
if (record) {
|
|
975
|
+
record.state = "archived";
|
|
976
|
+
record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
977
|
+
}
|
|
931
978
|
}
|
|
932
|
-
}
|
|
933
|
-
|
|
979
|
+
}, { onQuarantine: (message) => {
|
|
980
|
+
this.ctx.logger.warn(`evolution-curator: ${message}`);
|
|
981
|
+
} });
|
|
982
|
+
} catch (error) {
|
|
983
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist consolidate usage state: ${error instanceof Error ? error.message : String(error)}`);
|
|
984
|
+
}
|
|
934
985
|
return result;
|
|
935
986
|
}
|
|
936
987
|
/**
|
|
@@ -949,13 +1000,19 @@ var EvolutionCurator = class extends Service {
|
|
|
949
1000
|
await this.snapshotFull("pre-restore");
|
|
950
1001
|
const result = await this.skills.restoreFromArchive(name);
|
|
951
1002
|
if (!result.ok) return result;
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
record
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1003
|
+
try {
|
|
1004
|
+
await mutateUsage(this.skills.root, this.io, (disk) => {
|
|
1005
|
+
const record = disk.get(name);
|
|
1006
|
+
if (record) {
|
|
1007
|
+
record.state = "active";
|
|
1008
|
+
record.archived_at = null;
|
|
1009
|
+
}
|
|
1010
|
+
}, { onQuarantine: (message) => {
|
|
1011
|
+
this.ctx.logger.warn(`evolution-curator: ${message}`);
|
|
1012
|
+
} });
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
this.ctx.logger.warn(`evolution-curator: failed to persist restore usage state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1015
|
+
}
|
|
959
1016
|
if (new Set(await loadSuppressedNames(this.skills.root, this.io)).has(name)) try {
|
|
960
1017
|
await updateSuppressedNames(this.skills.root, this.io, (current) => {
|
|
961
1018
|
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,11 +106,20 @@ 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
124
|
/** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
|
|
114
125
|
* in-memory clock is seeded and later due runs must proceed, or the
|
|
@@ -178,6 +189,7 @@ export declare class EvolutionCurator extends Service {
|
|
|
178
189
|
content: string;
|
|
179
190
|
}>;
|
|
180
191
|
}>;
|
|
192
|
+
private restoreSnapshotCore;
|
|
181
193
|
private skippedReport;
|
|
182
194
|
/**
|
|
183
195
|
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
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.65",
|
|
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.65"
|
|
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.65",
|
|
41
|
+
"@lmzhen/dsh-evolution-state": "^0.3.65"
|
|
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.65",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.3.65",
|
|
48
|
+
"@lmzhen/dsh-evolution-state": "^0.3.65"
|
|
49
49
|
}
|
|
50
50
|
}
|