@lmzhen/dsh-evolution-curator 0.3.81 → 0.3.83
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 +2 -2
- package/lib/index.js +62 -6
- package/lib/types/index.d.ts +32 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lmzhen/dsh-evolution-curator
|
|
2
2
|
|
|
3
3
|
Deterministic skill lifecycle and recovery
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ Deterministic skill lifecycle and recovery
|
|
|
8
8
|
|
|
9
9
|
#### What the model sees
|
|
10
10
|
|
|
11
|
-
`@
|
|
11
|
+
`@lmzhen/dsh-evolution-curator` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
12
12
|
|
|
13
13
|
#### Token effect
|
|
14
14
|
|
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
|
|
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 } : {
|
|
@@ -353,6 +355,7 @@ var EvolutionCurator = class extends Service {
|
|
|
353
355
|
async restoreSnapshot() {
|
|
354
356
|
const release = await this.acquireMutex();
|
|
355
357
|
try {
|
|
358
|
+
if (!this.holdsInstance) return this.instanceHeldRefusal("restoreSnapshot");
|
|
356
359
|
return await this.restoreSnapshotCore();
|
|
357
360
|
} finally {
|
|
358
361
|
release();
|
|
@@ -466,6 +469,27 @@ var EvolutionCurator = class extends Service {
|
|
|
466
469
|
};
|
|
467
470
|
return prev.then(() => release);
|
|
468
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* PLAN S4.5 (2026-09-16) (audit P2-16): the manual control-plane entries
|
|
474
|
+
* (restore/consolidate/restoreSnapshot) honor the same per-home instance
|
|
475
|
+
* claim `run()` checks before every pass. The claim is taken at MOUNT and
|
|
476
|
+
* released at DISPOSE (core/instance-scope.ts), so `holdsInstance` is
|
|
477
|
+
* stable for this instance's lifetime: a single-instance deployment always
|
|
478
|
+
* holds it and every manual entry behaves exactly as before. The refusal
|
|
479
|
+
* fires only on a YIELDING second row of one process, whose manual mutation
|
|
480
|
+
* would otherwise race the holder row's in-flight run over the same tree —
|
|
481
|
+
* this instance's control-plane mutex cannot see the holder's chain. The
|
|
482
|
+
* shape is the plain `SkillActionResult` failure the command face already
|
|
483
|
+
* renders through `err(result.message)` — no new error type — and the
|
|
484
|
+
* leading `instance-held` token names the same outcome run() reports via
|
|
485
|
+
* its `skipped` field.
|
|
486
|
+
*/
|
|
487
|
+
instanceHeldRefusal(operation) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
message: `instance-held: this curator row yielded the per-home claim to another curator row of this process — ${operation} is refused alongside run() so the holder row stays the only control-plane writer (run it on the holder row)`
|
|
491
|
+
};
|
|
492
|
+
}
|
|
469
493
|
async runCore(options = {}) {
|
|
470
494
|
const { ignoreGates = false, dryRun = false } = options;
|
|
471
495
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -496,13 +520,13 @@ var EvolutionCurator = class extends Service {
|
|
|
496
520
|
};
|
|
497
521
|
if (!ignoreGates && persisted === null && (stateService !== void 0 || !this.statelessFirstRunDeferred)) {
|
|
498
522
|
if (stateService) try {
|
|
499
|
-
await stateService.
|
|
523
|
+
await stateService.transactCuratorState((current) => current === null ? {
|
|
500
524
|
schemaVersion: 1,
|
|
501
525
|
lastRunAt: Date.now(),
|
|
502
526
|
runCount: 0,
|
|
503
527
|
lastSummary: "first-run-deferred",
|
|
504
528
|
paused: false
|
|
505
|
-
});
|
|
529
|
+
} : null);
|
|
506
530
|
} catch (error) {
|
|
507
531
|
this.ctx.logger.warn(`evolution-curator: failed to persist the first-run baseline: ${error instanceof Error ? error.message : String(error)}`);
|
|
508
532
|
}
|
|
@@ -545,7 +569,7 @@ var EvolutionCurator = class extends Service {
|
|
|
545
569
|
if (text) contents.set(name, text);
|
|
546
570
|
}
|
|
547
571
|
const protectedNames = await this.protectedNameMap();
|
|
548
|
-
const dedupMembers = [...new Set(computeDedupGroups({ contents }).filter((group) => group.length >= 2).flat())].filter((name) => !protectedNames.has(name));
|
|
572
|
+
const dedupMembers = [...new Set(computeDedupGroups({ contents }).groups.filter((group) => group.length >= 2).flat())].filter((name) => !protectedNames.has(name));
|
|
549
573
|
await this.scoreTree(usage, treeNames, contents);
|
|
550
574
|
const result = computeLifecycleTransitions(usage, {
|
|
551
575
|
staleAfterDays: lifecycle.staleAfterDays,
|
|
@@ -934,8 +958,26 @@ var EvolutionCurator = class extends Service {
|
|
|
934
958
|
* (`curator-error-*.json`) are BUDGETED INDEPENDENTLY. Before this they shared
|
|
935
959
|
* one keep-20 window, so after 25 consecutive failures the next successful
|
|
936
960
|
* run's window held only a few real reports alongside the errors.
|
|
961
|
+
*
|
|
962
|
+
* v43 FLOW2-1 (P1): the sweep runs inside the IO backend's cross-process write
|
|
963
|
+
* lock. The inventory used to declare this site as serialized by the per-home
|
|
964
|
+
* instance claim, which is a module-scope Map (core/instance-scope.ts): a
|
|
965
|
+
* second ROW of this process yields, but a second PROCESS over the same home
|
|
966
|
+
* was never excluded — and "list the directory, then delete beyond the window"
|
|
967
|
+
* is exactly the multi-step shape that needs a real lock. A backend without
|
|
968
|
+
* `transact` has no lock at all: the sweep then degrades to the previous
|
|
969
|
+
* best-effort pass, which is safe because every deletion is idempotent and
|
|
970
|
+
* each report name is unique per runId.
|
|
937
971
|
*/
|
|
938
972
|
async retainReports(keep = 20, errorKeep = 10) {
|
|
973
|
+
await transactIo(this.io, reportsSweepLockTarget(), async () => {
|
|
974
|
+
await this.sweepReports(keep, errorKeep);
|
|
975
|
+
return null;
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
/** The sweep `retainReports` holds the per-home lock for. A caller MUST hold
|
|
979
|
+
* that lock: the listing and the deletions are not atomic on their own. */
|
|
980
|
+
async sweepReports(keep, errorKeep) {
|
|
939
981
|
const reportsRoot = join(evolutionHome(), "reports");
|
|
940
982
|
const listed = await probeList(this.io, reportsRoot);
|
|
941
983
|
if (isUnknown(listed)) {
|
|
@@ -1091,6 +1133,7 @@ var EvolutionCurator = class extends Service {
|
|
|
1091
1133
|
async consolidate(target, sources) {
|
|
1092
1134
|
const release = await this.acquireMutex();
|
|
1093
1135
|
try {
|
|
1136
|
+
if (!this.holdsInstance) return this.instanceHeldRefusal("consolidate");
|
|
1094
1137
|
return await this.consolidateMutate(target, sources);
|
|
1095
1138
|
} finally {
|
|
1096
1139
|
release();
|
|
@@ -1135,6 +1178,7 @@ var EvolutionCurator = class extends Service {
|
|
|
1135
1178
|
async restore(name) {
|
|
1136
1179
|
const release = await this.acquireMutex();
|
|
1137
1180
|
try {
|
|
1181
|
+
if (!this.holdsInstance) return this.instanceHeldRefusal("restore");
|
|
1138
1182
|
return await this.restoreMutate(name);
|
|
1139
1183
|
} finally {
|
|
1140
1184
|
release();
|
|
@@ -1167,5 +1211,17 @@ var EvolutionCurator = class extends Service {
|
|
|
1167
1211
|
return result;
|
|
1168
1212
|
}
|
|
1169
1213
|
};
|
|
1214
|
+
/**
|
|
1215
|
+
* v43 FLOW2-1: the per-home lock target for the report retention sweep.
|
|
1216
|
+
* `transactIo` takes the IO write lock on THIS path (minting `<path>.lock` with
|
|
1217
|
+
* the io protocol's `pid:token` body) and holds it for the whole task; nothing
|
|
1218
|
+
* ever writes the target, because the sweep's exclusion is the point. It lives
|
|
1219
|
+
* inside `<home>/reports` next to the directory it protects: the sweep's own
|
|
1220
|
+
* name filter (`curator-*.json`) ignores it, and the lock file is removed on
|
|
1221
|
+
* release.
|
|
1222
|
+
*/
|
|
1223
|
+
function reportsSweepLockTarget() {
|
|
1224
|
+
return join(evolutionHome(), "reports", ".retention");
|
|
1225
|
+
}
|
|
1170
1226
|
//#endregion
|
|
1171
1227
|
export { EvolutionCurator, EvolutionCurator as default, gateConsolidations };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
@@ -241,6 +243,22 @@ export declare class EvolutionCurator extends Service {
|
|
|
241
243
|
private mutexDepth;
|
|
242
244
|
private mutexTail;
|
|
243
245
|
private acquireMutex;
|
|
246
|
+
/**
|
|
247
|
+
* PLAN S4.5 (2026-09-16) (audit P2-16): the manual control-plane entries
|
|
248
|
+
* (restore/consolidate/restoreSnapshot) honor the same per-home instance
|
|
249
|
+
* claim `run()` checks before every pass. The claim is taken at MOUNT and
|
|
250
|
+
* released at DISPOSE (core/instance-scope.ts), so `holdsInstance` is
|
|
251
|
+
* stable for this instance's lifetime: a single-instance deployment always
|
|
252
|
+
* holds it and every manual entry behaves exactly as before. The refusal
|
|
253
|
+
* fires only on a YIELDING second row of one process, whose manual mutation
|
|
254
|
+
* would otherwise race the holder row's in-flight run over the same tree —
|
|
255
|
+
* this instance's control-plane mutex cannot see the holder's chain. The
|
|
256
|
+
* shape is the plain `SkillActionResult` failure the command face already
|
|
257
|
+
* renders through `err(result.message)` — no new error type — and the
|
|
258
|
+
* leading `instance-held` token names the same outcome run() reports via
|
|
259
|
+
* its `skipped` field.
|
|
260
|
+
*/
|
|
261
|
+
private instanceHeldRefusal;
|
|
244
262
|
private runCore;
|
|
245
263
|
/**
|
|
246
264
|
* Seed baseline records for tree skills the sidecar has not seen yet, so
|
|
@@ -283,8 +301,21 @@ export declare class EvolutionCurator extends Service {
|
|
|
283
301
|
* (`curator-error-*.json`) are BUDGETED INDEPENDENTLY. Before this they shared
|
|
284
302
|
* one keep-20 window, so after 25 consecutive failures the next successful
|
|
285
303
|
* run's window held only a few real reports alongside the errors.
|
|
304
|
+
*
|
|
305
|
+
* v43 FLOW2-1 (P1): the sweep runs inside the IO backend's cross-process write
|
|
306
|
+
* lock. The inventory used to declare this site as serialized by the per-home
|
|
307
|
+
* instance claim, which is a module-scope Map (core/instance-scope.ts): a
|
|
308
|
+
* second ROW of this process yields, but a second PROCESS over the same home
|
|
309
|
+
* was never excluded — and "list the directory, then delete beyond the window"
|
|
310
|
+
* is exactly the multi-step shape that needs a real lock. A backend without
|
|
311
|
+
* `transact` has no lock at all: the sweep then degrades to the previous
|
|
312
|
+
* best-effort pass, which is safe because every deletion is idempotent and
|
|
313
|
+
* each report name is unique per runId.
|
|
286
314
|
*/
|
|
287
315
|
private retainReports;
|
|
316
|
+
/** The sweep `retainReports` holds the per-home lock for. A caller MUST hold
|
|
317
|
+
* that lock: the listing and the deletions are not atomic on their own. */
|
|
318
|
+
private sweepReports;
|
|
288
319
|
/** Remove the oldest reports beyond `keep` (each with its `.md` digest, best-effort). */
|
|
289
320
|
private pruneReportList;
|
|
290
321
|
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.
|
|
4
|
+
"version": "0.3.83",
|
|
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.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.3.83"
|
|
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.
|
|
37
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
36
|
+
"@lmzhen/dsh-evolution-io": "^0.3.83",
|
|
37
|
+
"@lmzhen/dsh-evolution-state": "^0.3.83"
|
|
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.
|
|
43
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
44
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
42
|
+
"@lmzhen/dsh-evolution-core": "^0.3.83",
|
|
43
|
+
"@lmzhen/dsh-evolution-io": "^0.3.83",
|
|
44
|
+
"@lmzhen/dsh-evolution-state": "^0.3.83"
|
|
45
45
|
}
|
|
46
46
|
}
|