@lmzhen/dsh-evolution-curator 0.1.0-rc.37 → 0.1.0-rc.39
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 +7 -1
- package/lib/index.js +86 -7
- package/lib/types/index.d.ts +37 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -28,4 +28,10 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
28
28
|
|
|
29
29
|
- `archive` never deletes: skills move to `.archive/` with a `.archive-reason` marker.
|
|
30
30
|
- `restore(name)` (service) / `/evolution skill restore <name>` brings one archived skill back to the active root and resets its usage state.
|
|
31
|
-
- `consolidate(target, sources)` (service) / `/evolution consolidate` merges source bodies into the target, archives the sources with an absorbed-into marker, and folds their usage records into `archived` state. Both operations snapshot the
|
|
31
|
+
- `consolidate(target, sources)` (service) / `/evolution consolidate` merges source bodies into the target, archives the sources with an absorbed-into marker, and folds their usage records into `archived` state. Both operations snapshot the full state first (`pre-consolidate` / `pre-restore`).
|
|
32
|
+
- `restoreSnapshot` (service) / `/evolution restore` rolls the FULL state back to the latest snapshot: active tree, usage/suppression sidecars, `.archive/` and the curator state carried in the snapshot (`curator-state.json`), so the interval gate does not immediately re-fire after a rollback. The restore itself is undoable — the pre-rollback safety snapshot preserves the current tree plus its state.
|
|
33
|
+
|
|
34
|
+
## Automatic scheduling
|
|
35
|
+
|
|
36
|
+
- `autoStart` (default true) arms an hourly interval check 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()`.
|
|
37
|
+
- `autoStart: false` disables both automatic checks; `/evolution curator run` (manual, gate-skipping) still works.
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,7 @@ var EvolutionCurator = class extends Service {
|
|
|
35
35
|
pruneBuiltins: z.boolean().default(false),
|
|
36
36
|
referencedSkillNames: z.array(z.string()).default([]),
|
|
37
37
|
autoStart: z.boolean().default(true),
|
|
38
|
+
bootGraceSeconds: z.number().default(10),
|
|
38
39
|
curatorReviewMaxTokens: z.number().default(2048)
|
|
39
40
|
});
|
|
40
41
|
skills;
|
|
@@ -51,9 +52,12 @@ var EvolutionCurator = class extends Service {
|
|
|
51
52
|
manageUnmanaged;
|
|
52
53
|
pruneBuiltins;
|
|
53
54
|
referencedSkillNames;
|
|
55
|
+
bootGraceSeconds;
|
|
54
56
|
curatorReviewMaxTokens;
|
|
55
57
|
lastRun = 0;
|
|
56
58
|
timer;
|
|
59
|
+
bootCheck;
|
|
60
|
+
running = false;
|
|
57
61
|
constructor(ctx, config = {}) {
|
|
58
62
|
super(ctx, "evolutionCurator");
|
|
59
63
|
this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
@@ -70,6 +74,7 @@ var EvolutionCurator = class extends Service {
|
|
|
70
74
|
this.manageUnmanaged = config.manageUnmanaged ?? false;
|
|
71
75
|
this.pruneBuiltins = config.pruneBuiltins ?? false;
|
|
72
76
|
this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
|
|
77
|
+
this.bootGraceSeconds = config.bootGraceSeconds ?? 10;
|
|
73
78
|
this.curatorReviewMaxTokens = config.curatorReviewMaxTokens ?? 2048;
|
|
74
79
|
this.lastRun = Date.now();
|
|
75
80
|
this.ctx.effect(() => {
|
|
@@ -89,14 +94,30 @@ var EvolutionCurator = class extends Service {
|
|
|
89
94
|
}
|
|
90
95
|
start() {
|
|
91
96
|
if (!this.enabled || this.timer) return;
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
this.bootCheck = setTimeout(() => {
|
|
98
|
+
this.bootCheck = void 0;
|
|
99
|
+
this.autoCheck();
|
|
100
|
+
}, this.bootGraceSeconds * 1e3);
|
|
101
|
+
this.bootCheck.unref();
|
|
102
|
+
this.timer = setInterval(() => void this.autoCheck(), 3600 * 1e3);
|
|
95
103
|
this.timer.unref();
|
|
96
104
|
}
|
|
97
105
|
stop() {
|
|
98
106
|
if (this.timer) clearInterval(this.timer);
|
|
99
107
|
this.timer = void 0;
|
|
108
|
+
if (this.bootCheck) clearTimeout(this.bootCheck);
|
|
109
|
+
this.bootCheck = void 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One automatic schedule check: run a pass when the persisted curator state
|
|
113
|
+
* (falling back to the in-memory clock for state-less compositions) is at
|
|
114
|
+
* least one interval old. All gates — interval, idle, first-run defer,
|
|
115
|
+
* reentrancy — stay inside `run()`, so this method only decides whether to
|
|
116
|
+
* wake it, and never duplicates gate logic.
|
|
117
|
+
*/
|
|
118
|
+
async autoCheck() {
|
|
119
|
+
const last = (await this.curatorStateService()?.loadCuratorState())?.lastRunAt ?? this.lastRun;
|
|
120
|
+
if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
|
|
100
121
|
}
|
|
101
122
|
/**
|
|
102
123
|
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
@@ -151,6 +172,47 @@ var EvolutionCurator = class extends Service {
|
|
|
151
172
|
return empty;
|
|
152
173
|
}
|
|
153
174
|
}
|
|
175
|
+
/** Optional curator-state service (evolution-state-json / storage-domain). */
|
|
176
|
+
curatorStateService() {
|
|
177
|
+
return this.ctx.get("evolutionState");
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Full-state snapshot: the skills tree plus the current curator state as an
|
|
181
|
+
* `extras/curator-state.json` side file. Every pre-mutation snapshot in the
|
|
182
|
+
* curator goes through here so a later `restoreSnapshot()` can rewind both
|
|
183
|
+
* the tree and the state (Hermes curator_backup backs up `.curator_state`).
|
|
184
|
+
*/
|
|
185
|
+
async snapshotFull(reason = "pre-mutation") {
|
|
186
|
+
const state = await this.curatorStateService()?.loadCuratorState();
|
|
187
|
+
const extras = state === null || state === void 0 ? [] : [{
|
|
188
|
+
name: "curator-state.json",
|
|
189
|
+
content: JSON.stringify(state, null, 2)
|
|
190
|
+
}];
|
|
191
|
+
return await this.skills.snapshotAll(reason, extras);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Full-state rollback: restore the latest snapshot's tree/sidecars/archive
|
|
195
|
+
* AND the curator state it carried. The pre-rollback safety snapshot keeps
|
|
196
|
+
* the current tree plus current state (as extras), so the rollback itself
|
|
197
|
+
* is reversible.
|
|
198
|
+
*/
|
|
199
|
+
async restoreSnapshot() {
|
|
200
|
+
const stateService = this.curatorStateService();
|
|
201
|
+
const currentState = await stateService?.loadCuratorState();
|
|
202
|
+
const extras = currentState === null || currentState === void 0 ? [] : [{
|
|
203
|
+
name: "curator-state.json",
|
|
204
|
+
content: JSON.stringify(currentState, null, 2)
|
|
205
|
+
}];
|
|
206
|
+
const result = await this.skills.restoreLatestSnapshot(extras);
|
|
207
|
+
if (!result.ok) return result;
|
|
208
|
+
const stateExtra = result.extras?.find((extra) => extra.name === "curator-state.json");
|
|
209
|
+
if (stateExtra && stateService) try {
|
|
210
|
+
await stateService.saveCuratorState(JSON.parse(stateExtra.content));
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.ctx.logger.warn(`evolution-curator: failed to restore curator state: ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
154
216
|
skippedReport(runId, startedAt) {
|
|
155
217
|
return buildCuratorRunReport({
|
|
156
218
|
runId,
|
|
@@ -169,12 +231,29 @@ var EvolutionCurator = class extends Service {
|
|
|
169
231
|
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
170
232
|
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
171
233
|
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
234
|
+
* Reentrant calls (autoStart timer + manual command at the same instant) are
|
|
235
|
+
* skipped with an explicit `already-running` outcome.
|
|
172
236
|
*/
|
|
173
237
|
async run(options = {}) {
|
|
238
|
+
if (this.running) return {
|
|
239
|
+
stale: [],
|
|
240
|
+
archived: [],
|
|
241
|
+
errors: [],
|
|
242
|
+
report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
|
|
243
|
+
skipped: "already-running"
|
|
244
|
+
};
|
|
245
|
+
this.running = true;
|
|
246
|
+
try {
|
|
247
|
+
return await this.runCore(options);
|
|
248
|
+
} finally {
|
|
249
|
+
this.running = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async runCore(options = {}) {
|
|
174
253
|
const { ignoreGates = false, dryRun = false } = options;
|
|
175
254
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
176
255
|
const runId = randomUUID();
|
|
177
|
-
const stateService = this.
|
|
256
|
+
const stateService = this.curatorStateService();
|
|
178
257
|
const lifecycle = this.lifecycle();
|
|
179
258
|
const persisted = await stateService?.loadCuratorState();
|
|
180
259
|
if (!ignoreGates && persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
|
|
@@ -210,7 +289,7 @@ var EvolutionCurator = class extends Service {
|
|
|
210
289
|
const root = this.skills.root;
|
|
211
290
|
const rawUsage = await loadUsage(root, this.io);
|
|
212
291
|
const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
|
|
213
|
-
const snapshotPath = dryRun ? void 0 : await this.
|
|
292
|
+
const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
|
|
214
293
|
const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
|
|
215
294
|
const { bundledNames, treeNames } = await this.seedBaseline(usage);
|
|
216
295
|
const result = computeLifecycleTransitions(usage, {
|
|
@@ -501,7 +580,7 @@ var EvolutionCurator = class extends Service {
|
|
|
501
580
|
ok: false,
|
|
502
581
|
message: `Skill(s) excluded from lifecycle management: ${blocked.join(", ")}`
|
|
503
582
|
};
|
|
504
|
-
await this.
|
|
583
|
+
await this.snapshotFull("pre-consolidate");
|
|
505
584
|
const result = await this.skills.consolidate(target, sources);
|
|
506
585
|
if (!result.ok) return result;
|
|
507
586
|
const usage = await loadUsage(this.skills.root, this.io);
|
|
@@ -518,7 +597,7 @@ var EvolutionCurator = class extends Service {
|
|
|
518
597
|
* and reset its usage state, keeping the recoverable-archive invariant.
|
|
519
598
|
*/
|
|
520
599
|
async restore(name) {
|
|
521
|
-
await this.
|
|
600
|
+
await this.snapshotFull("pre-restore");
|
|
522
601
|
const result = await this.skills.restoreFromArchive(name);
|
|
523
602
|
if (!result.ok) return result;
|
|
524
603
|
const usage = await loadUsage(this.skills.root, this.io);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface Config {
|
|
|
33
33
|
referencedSkillNames?: string[];
|
|
34
34
|
/** Start the interval timer on context ready (auto-curation). Default true. */
|
|
35
35
|
autoStart?: boolean;
|
|
36
|
+
/** Seconds between host boot and the first automatic schedule check (restart catch-up). */
|
|
37
|
+
bootGraceSeconds?: number;
|
|
36
38
|
/** Max tokens for the optional LLM nomination pass. */
|
|
37
39
|
curatorReviewMaxTokens?: number;
|
|
38
40
|
}
|
|
@@ -82,13 +84,24 @@ export declare class EvolutionCurator extends Service {
|
|
|
82
84
|
private readonly manageUnmanaged;
|
|
83
85
|
private readonly pruneBuiltins;
|
|
84
86
|
private readonly referencedSkillNames;
|
|
87
|
+
private readonly bootGraceSeconds;
|
|
85
88
|
private readonly curatorReviewMaxTokens;
|
|
86
89
|
private lastRun;
|
|
87
90
|
private timer;
|
|
91
|
+
private bootCheck;
|
|
92
|
+
private running;
|
|
88
93
|
constructor(ctx: Context, config?: Config);
|
|
89
94
|
private lifecycle;
|
|
90
95
|
start(): void;
|
|
91
96
|
stop(): void;
|
|
97
|
+
/**
|
|
98
|
+
* One automatic schedule check: run a pass when the persisted curator state
|
|
99
|
+
* (falling back to the in-memory clock for state-less compositions) is at
|
|
100
|
+
* least one interval old. All gates — interval, idle, first-run defer,
|
|
101
|
+
* reentrancy — stay inside `run()`, so this method only decides whether to
|
|
102
|
+
* wake it, and never duplicates gate logic.
|
|
103
|
+
*/
|
|
104
|
+
private autoCheck;
|
|
92
105
|
/**
|
|
93
106
|
* Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
|
|
94
107
|
* consolidation; every move stays a control-plane operation and each
|
|
@@ -98,17 +111,41 @@ export declare class EvolutionCurator extends Service {
|
|
|
98
111
|
recommend(candidates: string[], options?: {
|
|
99
112
|
dryRun?: boolean;
|
|
100
113
|
}): Promise<CuratorNominations>;
|
|
114
|
+
/** Optional curator-state service (evolution-state-json / storage-domain). */
|
|
115
|
+
private curatorStateService;
|
|
116
|
+
/**
|
|
117
|
+
* Full-state snapshot: the skills tree plus the current curator state as an
|
|
118
|
+
* `extras/curator-state.json` side file. Every pre-mutation snapshot in the
|
|
119
|
+
* curator goes through here so a later `restoreSnapshot()` can rewind both
|
|
120
|
+
* the tree and the state (Hermes curator_backup backs up `.curator_state`).
|
|
121
|
+
*/
|
|
122
|
+
snapshotFull(reason?: string): Promise<string>;
|
|
123
|
+
/**
|
|
124
|
+
* Full-state rollback: restore the latest snapshot's tree/sidecars/archive
|
|
125
|
+
* AND the curator state it carried. The pre-rollback safety snapshot keeps
|
|
126
|
+
* the current tree plus current state (as extras), so the rollback itself
|
|
127
|
+
* is reversible.
|
|
128
|
+
*/
|
|
129
|
+
restoreSnapshot(): Promise<SkillActionResult & {
|
|
130
|
+
extras?: Array<{
|
|
131
|
+
name: string;
|
|
132
|
+
content: string;
|
|
133
|
+
}>;
|
|
134
|
+
}>;
|
|
101
135
|
private skippedReport;
|
|
102
136
|
/**
|
|
103
137
|
* Run one curator pass. `ignoreGates` skips the interval and idle gates so an
|
|
104
138
|
* explicit `/evolution curator run` always executes (manual-run semantics):
|
|
105
139
|
* `dryRun` computes the lifecycle and the LLM nominations but performs no
|
|
106
140
|
* mutation, reports what WOULD happen, and does not push out the next run.
|
|
141
|
+
* Reentrant calls (autoStart timer + manual command at the same instant) are
|
|
142
|
+
* skipped with an explicit `already-running` outcome.
|
|
107
143
|
*/
|
|
108
144
|
run(options?: {
|
|
109
145
|
ignoreGates?: boolean;
|
|
110
146
|
dryRun?: boolean;
|
|
111
147
|
}): Promise<CuratorRunOutcome>;
|
|
148
|
+
private runCore;
|
|
112
149
|
/**
|
|
113
150
|
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
114
151
|
* their inactivity clock starts now (first-sight defer) and bundled skills
|
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.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.39",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,20 +33,20 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.39"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
40
|
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
41
41
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
42
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
43
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
42
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.39",
|
|
43
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.39"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
47
47
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
48
|
-
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.
|
|
49
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
50
|
-
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.
|
|
48
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0-rc.39",
|
|
49
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.39",
|
|
50
|
+
"@lmzhen/dsh-evolution-state": "^0.1.0-rc.39"
|
|
51
51
|
}
|
|
52
52
|
}
|