@lmzhen/dsh-evolution-curator 0.3.61 → 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 CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
6
+ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, SKILL_NAME_RE, SkillLibrary, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, resolveSkillsRoot, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
7
7
  //#region lib/types/index.js
8
8
  /**
9
9
  * Deterministic skill lifecycle curator with interval gate and archive.
@@ -39,8 +39,8 @@ var EvolutionCurator = class extends Service {
39
39
  pruneBuiltins: z.boolean().default(false),
40
40
  referencedSkillNames: z.array(z.string()).default([]),
41
41
  autoStart: z.boolean().default(true),
42
- bootGraceSeconds: z.number().min(0).default(10),
43
- curatorReviewMaxTokens: z.number().min(1).default(2048),
42
+ bootGraceSeconds: z.number().min(0).default(DEFAULT_CURATOR_BOOT_GRACE_SECONDS),
43
+ curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
44
44
  healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
45
45
  healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
46
46
  healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
@@ -68,11 +68,12 @@ 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). */
75
74
  statelessFirstRunDeferred = false;
75
+ /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
76
+ statelessStateWarned = false;
76
77
  constructor(ctx, config = {}) {
77
78
  super(ctx, "evolutionCurator");
78
79
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
@@ -81,8 +82,11 @@ var EvolutionCurator = class extends Service {
81
82
  });
82
83
  this.enabled = config.enabled ?? true;
83
84
  const clamped = [];
84
- const field = (name, value, fallback, min) => {
85
- 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
+ });
86
90
  if (value !== void 0 && result !== value) clamped.push(name);
87
91
  return result;
88
92
  };
@@ -98,8 +102,8 @@ var EvolutionCurator = class extends Service {
98
102
  this.manageUnmanaged = config.manageUnmanaged ?? false;
99
103
  this.pruneBuiltins = config.pruneBuiltins ?? false;
100
104
  this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
101
- this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, 10, 0);
102
- this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, 2048, 1);
105
+ this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
106
+ this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
103
107
  this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
104
108
  this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
105
109
  this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
@@ -171,10 +175,21 @@ var EvolutionCurator = class extends Service {
171
175
  * HERE as a cheap pre-check (persisted or in-memory clock) AND again inside
172
176
  * run() as the authoritative gate — the docstring no longer claims the two
173
177
  * never duplicate; a future interval change must update both.
178
+ *
179
+ * P2-5 (v14): without the `evolution-state` service there is no durable
180
+ * `lastRunAt`, so the baseline degrades to this process's lifetime. That is
181
+ * a supported-but-degraded composition (every shipped bundle mounts the
182
+ * state rows), so the schedule is left as-is and the degradation is
183
+ * surfaced once instead of silently meaning "never runs".
174
184
  */
175
185
  async autoCheck() {
176
186
  try {
177
- const last = (await this.curatorStateService()?.loadCuratorState())?.lastRunAt ?? this.lastRun;
187
+ const stateService = this.curatorStateService();
188
+ if (stateService === void 0 && !this.statelessStateWarned) {
189
+ this.statelessStateWarned = true;
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.`);
191
+ }
192
+ const last = (await stateService?.loadCuratorState())?.lastRunAt ?? this.lastRun;
178
193
  if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
179
194
  } catch (error) {
180
195
  const reason = error instanceof Error ? error.message : String(error);
@@ -319,18 +334,18 @@ var EvolutionCurator = class extends Service {
319
334
  * skipped with an explicit `already-running` outcome.
320
335
  */
321
336
  async run(options = {}) {
322
- if (this.running) return {
337
+ if (this.mutexDepth > 0) return {
323
338
  stale: [],
324
339
  archived: [],
325
340
  errors: [],
326
341
  report: this.skippedReport("already-running", (/* @__PURE__ */ new Date()).toISOString()),
327
342
  skipped: "already-running"
328
343
  };
329
- this.running = true;
344
+ const release = await this.acquireMutex();
330
345
  try {
331
346
  return await this.runCore(options);
332
347
  } finally {
333
- this.running = false;
348
+ release();
334
349
  try {
335
350
  await this.retainReports();
336
351
  } catch (error) {
@@ -338,6 +353,36 @@ var EvolutionCurator = class extends Service {
338
353
  }
339
354
  }
340
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
+ }
341
386
  async runCore(options = {}) {
342
387
  const { ignoreGates = false, dryRun = false } = options;
343
388
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -472,16 +517,20 @@ var EvolutionCurator = class extends Service {
472
517
  }
473
518
  const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
474
519
  const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
475
- await stateService?.transactCuratorState((current) => {
476
- const pausedNow = current?.paused ?? false;
477
- return {
478
- schemaVersion: 1,
479
- lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
480
- runCount: dryRun ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
481
- lastSummary: summary,
482
- paused: pausedNow
483
- };
484
- });
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
+ }
485
534
  return {
486
535
  stale: result.markStale,
487
536
  archived: archivedSkills.map((item) => item.name),
@@ -513,6 +562,11 @@ var EvolutionCurator = class extends Service {
513
562
  }
514
563
  /**
515
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`).
516
570
  */
517
571
  async scoreTree(usage, treeNames) {
518
572
  const supportDirs = /* @__PURE__ */ new Map();
@@ -846,6 +900,14 @@ var EvolutionCurator = class extends Service {
846
900
  * records into `archived` state. Snapshot-then-mutate, never a hard delete.
847
901
  */
848
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) {
849
911
  const suppressedNames = new Set(await loadSuppressedNames(this.skills.root, this.io));
850
912
  const gates = new EvolutionGateSet({
851
913
  exclude: this.excludeSkillNames,
@@ -876,6 +938,14 @@ var EvolutionCurator = class extends Service {
876
938
  * and reset its usage state, keeping the recoverable-archive invariant.
877
939
  */
878
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) {
879
949
  await this.snapshotFull("pre-restore");
880
950
  const result = await this.skills.restoreFromArchive(name);
881
951
  if (!result.ok) return result;
@@ -110,11 +110,12 @@ 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). */
117
116
  private statelessFirstRunDeferred;
117
+ /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
118
+ private statelessStateWarned;
118
119
  constructor(ctx: Context, config?: Config);
119
120
  private lifecycle;
120
121
  start(): void;
@@ -139,6 +140,12 @@ export declare class EvolutionCurator extends Service {
139
140
  * HERE as a cheap pre-check (persisted or in-memory clock) AND again inside
140
141
  * run() as the authoritative gate — the docstring no longer claims the two
141
142
  * never duplicate; a future interval change must update both.
143
+ *
144
+ * P2-5 (v14): without the `evolution-state` service there is no durable
145
+ * `lastRunAt`, so the baseline degrades to this process's lifetime. That is
146
+ * a supported-but-degraded composition (every shipped bundle mounts the
147
+ * state rows), so the schedule is left as-is and the degradation is
148
+ * surfaced once instead of silently meaning "never runs".
142
149
  */
143
150
  private autoCheck;
144
151
  /**
@@ -184,6 +191,21 @@ export declare class EvolutionCurator extends Service {
184
191
  ignoreGates?: boolean;
185
192
  dryRun?: boolean;
186
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;
187
209
  private runCore;
188
210
  /**
189
211
  * Seed baseline records for tree skills the sidecar has not seen yet, so
@@ -194,6 +216,11 @@ export declare class EvolutionCurator extends Service {
194
216
  private seedBaseline;
195
217
  /**
196
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`).
197
224
  */
198
225
  private scoreTree;
199
226
  /**
@@ -254,11 +281,13 @@ export declare class EvolutionCurator extends Service {
254
281
  * records into `archived` state. Snapshot-then-mutate, never a hard delete.
255
282
  */
256
283
  consolidate(target: string, sources: string[]): Promise<SkillActionResult>;
284
+ private consolidateMutate;
257
285
  /**
258
286
  * Control-plane restore: bring one archived skill back to the active root
259
287
  * and reset its usage state, keeping the recoverable-archive invariant.
260
288
  */
261
289
  restore(name: string): Promise<SkillActionResult>;
290
+ private restoreMutate;
262
291
  }
263
292
  export default EvolutionCurator;
264
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.61",
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.61"
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.61",
41
- "@lmzhen/dsh-evolution-state": "^0.3.61"
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.61",
47
- "@lmzhen/dsh-evolution-io": "^0.3.61",
48
- "@lmzhen/dsh-evolution-state": "^0.3.61"
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
  }