@lmzhen/dsh-evolution-curator 0.3.77 → 0.3.78

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_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, relatedSkillNames, renderCuratorReportMarkdown, 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_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, parseCuratorNominations, parseFrontmatter, probeList, probeMtime, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, 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.
@@ -92,6 +92,13 @@ var EvolutionCurator = class extends Service {
92
92
  statelessFirstRunDeferred = false;
93
93
  /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
94
94
  statelessStateWarned = false;
95
+ /** B3 / G4: the home this instance claimed, plus whether the claim was
96
+ * granted. A non-owning instance schedules nothing and runs nothing. */
97
+ instanceHome = "";
98
+ holdsInstance = false;
99
+ /** B3 / G4: this instance's identity in the claim — pid + short token, so the
100
+ * yielding instance's log line names a concrete holder. */
101
+ instanceOwner = `evolution-curator[${process.pid}:${randomUUID().slice(0, 8)}]`;
95
102
  constructor(ctx, config = {}) {
96
103
  super(ctx, "evolutionCurator");
97
104
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
@@ -101,6 +108,10 @@ var EvolutionCurator = class extends Service {
101
108
  ctx: this.ctx
102
109
  });
103
110
  this.enabled = config.enabled ?? true;
111
+ this.instanceHome = evolutionHome();
112
+ const claim = claimInstance(this.instanceHome, INSTANCE_KEYS.curator, this.instanceOwner);
113
+ this.holdsInstance = claim.granted;
114
+ if (!claim.granted) this.ctx.logger.warn(`evolution-curator: this instance YIELDS — ${claim.key} is already held by ${claim.holder}; a second curator over one home would race the report retention sweep. It schedules nothing and every run() returns skipped "instance-held".`);
104
115
  const clamped = [];
105
116
  const field = (name, value, fallback, min, max) => {
106
117
  const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
@@ -138,6 +149,7 @@ var EvolutionCurator = class extends Service {
138
149
  return () => {
139
150
  this.disposed = true;
140
151
  this.stop();
152
+ releaseInstance(this.instanceHome, INSTANCE_KEYS.curator, this.instanceOwner);
141
153
  };
142
154
  }, "evolution-curator.stop");
143
155
  if (config.autoStart ?? true) this.start();
@@ -151,6 +163,7 @@ var EvolutionCurator = class extends Service {
151
163
  };
152
164
  }
153
165
  start() {
166
+ if (!this.holdsInstance) return;
154
167
  if (this.isDisposed() || !this.enabled || this.timer) return;
155
168
  this.bootCheck = setTimeout(() => {
156
169
  this.bootCheck = void 0;
@@ -372,6 +385,13 @@ var EvolutionCurator = class extends Service {
372
385
  * skipped with an explicit `already-running` outcome.
373
386
  */
374
387
  async run(options = {}) {
388
+ if (!this.holdsInstance) return {
389
+ stale: [],
390
+ archived: [],
391
+ errors: [],
392
+ report: this.skippedReport("instance-held", (/* @__PURE__ */ new Date()).toISOString()),
393
+ skipped: "instance-held"
394
+ };
375
395
  if (this.mutexDepth > 0) return {
376
396
  stale: [],
377
397
  archived: [],
@@ -905,12 +925,12 @@ var EvolutionCurator = class extends Service {
905
925
  */
906
926
  async retainReports(keep = 20, errorKeep = 10) {
907
927
  const reportsRoot = join(evolutionHome(), "reports");
908
- let entries;
909
- try {
910
- entries = await this.io.list(reportsRoot);
911
- } catch {
928
+ const listed = await probeList(this.io, reportsRoot);
929
+ if (isUnknown(listed)) {
930
+ this.ctx.logger.warn(`evolution-curator: report retention skipped — the reports directory could not be listed (${listed.reason})`);
912
931
  return;
913
932
  }
933
+ const entries = isPresent(listed) ? listed.value : [];
914
934
  const real = [];
915
935
  const errors = [];
916
936
  let unorderableWarned = false;
@@ -920,8 +940,8 @@ var EvolutionCurator = class extends Service {
920
940
  const parsed = JSON.parse(raw);
921
941
  let startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : NaN;
922
942
  if (!Number.isFinite(startedAt)) {
923
- const mtime = await this.io.mtime?.(join(reportsRoot, name)) ?? null;
924
- if (mtime !== null) startedAt = mtime;
943
+ const probe = await probeMtime(this.io, join(reportsRoot, name));
944
+ if (isPresent(probe)) startedAt = probe.value;
925
945
  }
926
946
  if (!Number.isFinite(startedAt) && typeof parsed.at === "string") startedAt = Date.parse(parsed.at);
927
947
  if (Number.isFinite(startedAt)) (name.startsWith("curator-error-") ? errors : real).push({
@@ -952,27 +972,25 @@ var EvolutionCurator = class extends Service {
952
972
  }
953
973
  async latestReport() {
954
974
  const reportsRoot = join(evolutionHome(), "reports");
955
- let names;
956
- try {
957
- names = (await this.io.list(reportsRoot)).filter((name) => name.startsWith("curator-") && name.endsWith(".json") && !name.startsWith("curator-error-"));
958
- } catch (error) {
959
- this.ctx.logger.warn(`evolution-curator: latestReport could not list the reports directory (${error instanceof Error ? error.message : String(error)}) - answering "no report"`);
975
+ const listed = await probeList(this.io, reportsRoot);
976
+ if (isUnknown(listed)) {
977
+ this.ctx.logger.warn(`evolution-curator: latestReport could not list the reports directory (${listed.reason}) - answering "no report"`);
960
978
  return null;
961
979
  }
980
+ const names = (isPresent(listed) ? listed.value : []).filter((name) => name.startsWith("curator-") && name.endsWith(".json") && !name.startsWith("curator-error-"));
962
981
  let latest = null;
963
982
  let probeWarned = false;
964
983
  for (const name of names) {
965
- let mtime;
966
- try {
967
- mtime = await this.io.mtime?.(join(reportsRoot, name)) ?? null;
968
- } catch (error) {
984
+ const probe = await probeMtime(this.io, join(reportsRoot, name));
985
+ if (isUnknown(probe)) {
969
986
  if (!probeWarned) {
970
987
  probeWarned = true;
971
- this.ctx.logger.warn(`evolution-curator: latestReport could not read a report mtime (${error instanceof Error ? error.message : String(error)}) - the affected report(s) are not eligible as the latest`);
988
+ this.ctx.logger.warn(`evolution-curator: latestReport could not read a report mtime (${probe.reason}) - the affected report(s) are not eligible as the latest`);
972
989
  }
973
990
  continue;
974
991
  }
975
- if (mtime === null) continue;
992
+ if (!isPresent(probe)) continue;
993
+ const mtime = probe.value;
976
994
  if (latest === null || mtime > latest.mtime) latest = {
977
995
  name,
978
996
  mtime
@@ -127,6 +127,13 @@ export declare class EvolutionCurator extends Service {
127
127
  private statelessFirstRunDeferred;
128
128
  /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
129
129
  private statelessStateWarned;
130
+ /** B3 / G4: the home this instance claimed, plus whether the claim was
131
+ * granted. A non-owning instance schedules nothing and runs nothing. */
132
+ private instanceHome;
133
+ private holdsInstance;
134
+ /** B3 / G4: this instance's identity in the claim — pid + short token, so the
135
+ * yielding instance's log line names a concrete holder. */
136
+ private readonly instanceOwner;
130
137
  constructor(ctx: Context, config?: Config);
131
138
  private lifecycle;
132
139
  start(): void;
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.77",
4
+ "version": "0.3.78",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,20 +27,20 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.3.77"
30
+ "@lmzhen/dsh-evolution-core": "^0.3.78"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
35
35
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
36
- "@lmzhen/dsh-evolution-io": "^0.3.77",
37
- "@lmzhen/dsh-evolution-state": "^0.3.77"
36
+ "@lmzhen/dsh-evolution-io": "^0.3.78",
37
+ "@lmzhen/dsh-evolution-state": "^0.3.78"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
41
41
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
42
- "@lmzhen/dsh-evolution-core": "^0.3.77",
43
- "@lmzhen/dsh-evolution-io": "^0.3.77",
44
- "@lmzhen/dsh-evolution-state": "^0.3.77"
42
+ "@lmzhen/dsh-evolution-core": "^0.3.78",
43
+ "@lmzhen/dsh-evolution-io": "^0.3.78",
44
+ "@lmzhen/dsh-evolution-state": "^0.3.78"
45
45
  }
46
46
  }