@lmzhen/dsh-evolution-curator 0.3.80 → 0.3.82

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, 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";
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, transactIo, 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.
@@ -96,7 +96,9 @@ var EvolutionCurator = class extends Service {
96
96
  /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
97
97
  statelessStateWarned = false;
98
98
  /** B3 / G4: the home this instance claimed, plus whether the claim was
99
- * granted. A non-owning instance schedules nothing and runs nothing. */
99
+ * granted. A non-owning instance schedules nothing and runs nothing. The
100
+ * claim is PER PROCESS (v43 FLOW2-1): another process's holder is invisible
101
+ * here, so `holdsInstance` never proves this instance is alone on the home. */
100
102
  instanceHome = "";
101
103
  holdsInstance = false;
102
104
  /** B3 / G4: this instance's identity in the claim — pid + short token, so the
@@ -114,7 +116,7 @@ var EvolutionCurator = class extends Service {
114
116
  this.instanceHome = evolutionHome();
115
117
  const claim = claimInstance(this.instanceHome, INSTANCE_KEYS.curator, this.instanceOwner);
116
118
  this.holdsInstance = claim.granted;
117
- 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".`);
119
+ if (!claim.granted) this.ctx.logger.warn(`evolution-curator: this instance YIELDS — ${claim.key} is already held by ${claim.holder} IN THIS PROCESS (the claim is a module-scope Map, so it does not exclude another process); a second curator ROW over one home would run a second gate set and a second lifecycle pass over the same tree. It schedules nothing and every run() returns skipped "instance-held"; the report sweep itself is cross-process locked (v43 FLOW2-1).`);
118
120
  const clamped = [];
119
121
  const field = (name, value, fallback, min, max) => {
120
122
  const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
@@ -934,8 +936,26 @@ var EvolutionCurator = class extends Service {
934
936
  * (`curator-error-*.json`) are BUDGETED INDEPENDENTLY. Before this they shared
935
937
  * one keep-20 window, so after 25 consecutive failures the next successful
936
938
  * run's window held only a few real reports alongside the errors.
939
+ *
940
+ * v43 FLOW2-1 (P1): the sweep runs inside the IO backend's cross-process write
941
+ * lock. The inventory used to declare this site as serialized by the per-home
942
+ * instance claim, which is a module-scope Map (core/instance-scope.ts): a
943
+ * second ROW of this process yields, but a second PROCESS over the same home
944
+ * was never excluded — and "list the directory, then delete beyond the window"
945
+ * is exactly the multi-step shape that needs a real lock. A backend without
946
+ * `transact` has no lock at all: the sweep then degrades to the previous
947
+ * best-effort pass, which is safe because every deletion is idempotent and
948
+ * each report name is unique per runId.
937
949
  */
938
950
  async retainReports(keep = 20, errorKeep = 10) {
951
+ await transactIo(this.io, reportsSweepLockTarget(), async () => {
952
+ await this.sweepReports(keep, errorKeep);
953
+ return null;
954
+ });
955
+ }
956
+ /** The sweep `retainReports` holds the per-home lock for. A caller MUST hold
957
+ * that lock: the listing and the deletions are not atomic on their own. */
958
+ async sweepReports(keep, errorKeep) {
939
959
  const reportsRoot = join(evolutionHome(), "reports");
940
960
  const listed = await probeList(this.io, reportsRoot);
941
961
  if (isUnknown(listed)) {
@@ -1167,5 +1187,17 @@ var EvolutionCurator = class extends Service {
1167
1187
  return result;
1168
1188
  }
1169
1189
  };
1190
+ /**
1191
+ * v43 FLOW2-1: the per-home lock target for the report retention sweep.
1192
+ * `transactIo` takes the IO write lock on THIS path (minting `<path>.lock` with
1193
+ * the io protocol's `pid:token` body) and holds it for the whole task; nothing
1194
+ * ever writes the target, because the sweep's exclusion is the point. It lives
1195
+ * inside `<home>/reports` next to the directory it protects: the sweep's own
1196
+ * name filter (`curator-*.json`) ignores it, and the lock file is removed on
1197
+ * release.
1198
+ */
1199
+ function reportsSweepLockTarget() {
1200
+ return join(evolutionHome(), "reports", ".retention");
1201
+ }
1170
1202
  //#endregion
1171
1203
  export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
@@ -131,7 +131,9 @@ export declare class EvolutionCurator extends Service {
131
131
  /** P2-5 (v14): one-shot warning that the interval baseline is process-only. */
132
132
  private statelessStateWarned;
133
133
  /** B3 / G4: the home this instance claimed, plus whether the claim was
134
- * granted. A non-owning instance schedules nothing and runs nothing. */
134
+ * granted. A non-owning instance schedules nothing and runs nothing. The
135
+ * claim is PER PROCESS (v43 FLOW2-1): another process's holder is invisible
136
+ * here, so `holdsInstance` never proves this instance is alone on the home. */
135
137
  private instanceHome;
136
138
  private holdsInstance;
137
139
  /** B3 / G4: this instance's identity in the claim — pid + short token, so the
@@ -283,8 +285,21 @@ export declare class EvolutionCurator extends Service {
283
285
  * (`curator-error-*.json`) are BUDGETED INDEPENDENTLY. Before this they shared
284
286
  * one keep-20 window, so after 25 consecutive failures the next successful
285
287
  * run's window held only a few real reports alongside the errors.
288
+ *
289
+ * v43 FLOW2-1 (P1): the sweep runs inside the IO backend's cross-process write
290
+ * lock. The inventory used to declare this site as serialized by the per-home
291
+ * instance claim, which is a module-scope Map (core/instance-scope.ts): a
292
+ * second ROW of this process yields, but a second PROCESS over the same home
293
+ * was never excluded — and "list the directory, then delete beyond the window"
294
+ * is exactly the multi-step shape that needs a real lock. A backend without
295
+ * `transact` has no lock at all: the sweep then degrades to the previous
296
+ * best-effort pass, which is safe because every deletion is idempotent and
297
+ * each report name is unique per runId.
286
298
  */
287
299
  private retainReports;
300
+ /** The sweep `retainReports` holds the per-home lock for. A caller MUST hold
301
+ * that lock: the listing and the deletions are not atomic on their own. */
302
+ private sweepReports;
288
303
  /** Remove the oldest reports beyond `keep` (each with its `.md` digest, best-effort). */
289
304
  private pruneReportList;
290
305
  latestReport(): Promise<CuratorRunReport | null>;
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.80",
4
+ "version": "0.3.82",
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.80"
30
+ "@lmzhen/dsh-evolution-core": "^0.3.82"
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.80",
37
- "@lmzhen/dsh-evolution-state": "^0.3.80"
36
+ "@lmzhen/dsh-evolution-io": "^0.3.82",
37
+ "@lmzhen/dsh-evolution-state": "^0.3.82"
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.80",
43
- "@lmzhen/dsh-evolution-io": "^0.3.80",
44
- "@lmzhen/dsh-evolution-state": "^0.3.80"
42
+ "@lmzhen/dsh-evolution-core": "^0.3.82",
43
+ "@lmzhen/dsh-evolution-io": "^0.3.82",
44
+ "@lmzhen/dsh-evolution-state": "^0.3.82"
45
45
  }
46
46
  }