@lmzhen/dsh-evolution-curator 0.1.0-rc.37 → 0.1.0-rc.38

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 CHANGED
@@ -28,4 +28,5 @@ 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 skill tree first (`pre-consolidate` / `pre-restore`).
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.
package/lib/index.js CHANGED
@@ -54,6 +54,7 @@ var EvolutionCurator = class extends Service {
54
54
  curatorReviewMaxTokens;
55
55
  lastRun = 0;
56
56
  timer;
57
+ running = false;
57
58
  constructor(ctx, config = {}) {
58
59
  super(ctx, "evolutionCurator");
59
60
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
@@ -151,6 +152,47 @@ var EvolutionCurator = class extends Service {
151
152
  return empty;
152
153
  }
153
154
  }
155
+ /** Optional curator-state service (evolution-state-json / storage-domain). */
156
+ curatorStateService() {
157
+ return this.ctx.get("evolutionState");
158
+ }
159
+ /**
160
+ * Full-state snapshot: the skills tree plus the current curator state as an
161
+ * `extras/curator-state.json` side file. Every pre-mutation snapshot in the
162
+ * curator goes through here so a later `restoreSnapshot()` can rewind both
163
+ * the tree and the state (Hermes curator_backup backs up `.curator_state`).
164
+ */
165
+ async snapshotFull(reason = "pre-mutation") {
166
+ const state = await this.curatorStateService()?.loadCuratorState();
167
+ const extras = state === null || state === void 0 ? [] : [{
168
+ name: "curator-state.json",
169
+ content: JSON.stringify(state, null, 2)
170
+ }];
171
+ return await this.skills.snapshotAll(reason, extras);
172
+ }
173
+ /**
174
+ * Full-state rollback: restore the latest snapshot's tree/sidecars/archive
175
+ * AND the curator state it carried. The pre-rollback safety snapshot keeps
176
+ * the current tree plus current state (as extras), so the rollback itself
177
+ * is reversible.
178
+ */
179
+ async restoreSnapshot() {
180
+ const stateService = this.curatorStateService();
181
+ const currentState = await stateService?.loadCuratorState();
182
+ const extras = currentState === null || currentState === void 0 ? [] : [{
183
+ name: "curator-state.json",
184
+ content: JSON.stringify(currentState, null, 2)
185
+ }];
186
+ const result = await this.skills.restoreLatestSnapshot(extras);
187
+ if (!result.ok) return result;
188
+ const stateExtra = result.extras?.find((extra) => extra.name === "curator-state.json");
189
+ if (stateExtra && stateService) try {
190
+ await stateService.saveCuratorState(JSON.parse(stateExtra.content));
191
+ } catch (error) {
192
+ this.ctx.logger.warn(`evolution-curator: failed to restore curator state: ${error instanceof Error ? error.message : String(error)}`);
193
+ }
194
+ return result;
195
+ }
154
196
  skippedReport(runId, startedAt) {
155
197
  return buildCuratorRunReport({
156
198
  runId,
@@ -169,12 +211,29 @@ var EvolutionCurator = class extends Service {
169
211
  * explicit `/evolution curator run` always executes (manual-run semantics):
170
212
  * `dryRun` computes the lifecycle and the LLM nominations but performs no
171
213
  * mutation, reports what WOULD happen, and does not push out the next run.
214
+ * Reentrant calls (autoStart timer + manual command at the same instant) are
215
+ * skipped with an explicit `already-running` outcome.
172
216
  */
173
217
  async run(options = {}) {
218
+ if (this.running) return {
219
+ stale: [],
220
+ archived: [],
221
+ errors: [],
222
+ report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
223
+ skipped: "already-running"
224
+ };
225
+ this.running = true;
226
+ try {
227
+ return await this.runCore(options);
228
+ } finally {
229
+ this.running = false;
230
+ }
231
+ }
232
+ async runCore(options = {}) {
174
233
  const { ignoreGates = false, dryRun = false } = options;
175
234
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
176
235
  const runId = randomUUID();
177
- const stateService = this.ctx.get("evolutionState");
236
+ const stateService = this.curatorStateService();
178
237
  const lifecycle = this.lifecycle();
179
238
  const persisted = await stateService?.loadCuratorState();
180
239
  if (!ignoreGates && persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
@@ -210,7 +269,7 @@ var EvolutionCurator = class extends Service {
210
269
  const root = this.skills.root;
211
270
  const rawUsage = await loadUsage(root, this.io);
212
271
  const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
213
- const snapshotPath = dryRun ? void 0 : await this.skills.snapshotAll("pre-curator-run");
272
+ const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
214
273
  const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
215
274
  const { bundledNames, treeNames } = await this.seedBaseline(usage);
216
275
  const result = computeLifecycleTransitions(usage, {
@@ -501,7 +560,7 @@ var EvolutionCurator = class extends Service {
501
560
  ok: false,
502
561
  message: `Skill(s) excluded from lifecycle management: ${blocked.join(", ")}`
503
562
  };
504
- await this.skills.snapshotAll("pre-consolidate");
563
+ await this.snapshotFull("pre-consolidate");
505
564
  const result = await this.skills.consolidate(target, sources);
506
565
  if (!result.ok) return result;
507
566
  const usage = await loadUsage(this.skills.root, this.io);
@@ -518,7 +577,7 @@ var EvolutionCurator = class extends Service {
518
577
  * and reset its usage state, keeping the recoverable-archive invariant.
519
578
  */
520
579
  async restore(name) {
521
- await this.skills.snapshotAll("pre-restore");
580
+ await this.snapshotFull("pre-restore");
522
581
  const result = await this.skills.restoreFromArchive(name);
523
582
  if (!result.ok) return result;
524
583
  const usage = await loadUsage(this.skills.root, this.io);
@@ -85,6 +85,7 @@ export declare class EvolutionCurator extends Service {
85
85
  private readonly curatorReviewMaxTokens;
86
86
  private lastRun;
87
87
  private timer;
88
+ private running;
88
89
  constructor(ctx: Context, config?: Config);
89
90
  private lifecycle;
90
91
  start(): void;
@@ -98,17 +99,41 @@ export declare class EvolutionCurator extends Service {
98
99
  recommend(candidates: string[], options?: {
99
100
  dryRun?: boolean;
100
101
  }): Promise<CuratorNominations>;
102
+ /** Optional curator-state service (evolution-state-json / storage-domain). */
103
+ private curatorStateService;
104
+ /**
105
+ * Full-state snapshot: the skills tree plus the current curator state as an
106
+ * `extras/curator-state.json` side file. Every pre-mutation snapshot in the
107
+ * curator goes through here so a later `restoreSnapshot()` can rewind both
108
+ * the tree and the state (Hermes curator_backup backs up `.curator_state`).
109
+ */
110
+ snapshotFull(reason?: string): Promise<string>;
111
+ /**
112
+ * Full-state rollback: restore the latest snapshot's tree/sidecars/archive
113
+ * AND the curator state it carried. The pre-rollback safety snapshot keeps
114
+ * the current tree plus current state (as extras), so the rollback itself
115
+ * is reversible.
116
+ */
117
+ restoreSnapshot(): Promise<SkillActionResult & {
118
+ extras?: Array<{
119
+ name: string;
120
+ content: string;
121
+ }>;
122
+ }>;
101
123
  private skippedReport;
102
124
  /**
103
125
  * Run one curator pass. `ignoreGates` skips the interval and idle gates so an
104
126
  * explicit `/evolution curator run` always executes (manual-run semantics):
105
127
  * `dryRun` computes the lifecycle and the LLM nominations but performs no
106
128
  * mutation, reports what WOULD happen, and does not push out the next run.
129
+ * Reentrant calls (autoStart timer + manual command at the same instant) are
130
+ * skipped with an explicit `already-running` outcome.
107
131
  */
108
132
  run(options?: {
109
133
  ignoreGates?: boolean;
110
134
  dryRun?: boolean;
111
135
  }): Promise<CuratorRunOutcome>;
136
+ private runCore;
112
137
  /**
113
138
  * Seed baseline records for tree skills the sidecar has not seen yet, so
114
139
  * 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.37",
4
+ "version": "0.1.0-rc.38",
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.37"
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.38"
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.37",
43
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.37"
42
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.38",
43
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.38"
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.37",
49
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.37",
50
- "@lmzhen/dsh-evolution-state": "^0.1.0-rc.37"
48
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.38",
49
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.38",
50
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.38"
51
51
  }
52
52
  }