@lmzhen/dsh-evolution-core 0.3.19 → 0.3.21

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
@@ -52,6 +52,49 @@ function evolutionIoAdapter(provider) {
52
52
  }
53
53
  };
54
54
  }
55
+ /**
56
+ * F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
57
+ * to the same file proactively recycles our own leftover lock — the holder is
58
+ * us, so a leftover is stale by definition. Module-level by design: it must
59
+ * survive across `nodeEvolutionIo()` instances for the self-heal to be
60
+ * effective. (Not a pure function — the cross-call state is the intent.)
61
+ */
62
+ const pendingSelfCleanup = /* @__PURE__ */ new Set();
63
+ /**
64
+ * Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
65
+ * a short 50ms backoff, at most 3 retries (~150ms budget), matching the
66
+ * write-lock cadence. A non-transient code surfaces immediately. `fn` is the
67
+ * rename primitive, injectable for deterministic tests.
68
+ *
69
+ * @param tmp - the source path to rename.
70
+ * @param target - the destination path.
71
+ * @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
72
+ * @returns a promise that resolves once the rename succeeds.
73
+ */
74
+ async function renameWithRetry(tmp, target, fn = rename) {
75
+ for (let retry = 0;; retry += 1) try {
76
+ await fn(tmp, target);
77
+ return;
78
+ } catch (error) {
79
+ const code = error?.code;
80
+ if (code !== "EPERM" && code !== "EBUSY") throw error;
81
+ if (retry >= 3) throw error;
82
+ await new Promise((resolve) => setTimeout(resolve, 50));
83
+ }
84
+ }
85
+ /**
86
+ * F-366: commit a freshly-written tmp to its target inside the write lock. On a
87
+ * still-failing rename the tmp is deleted immediately rather than left for the
88
+ * (1h + dead-pid) sweep, so a live writer never leaks a tmp it abandoned.
89
+ */
90
+ async function commitTmp(tmp, target) {
91
+ try {
92
+ await renameWithRetry(tmp, target);
93
+ } catch (error) {
94
+ await rm(tmp, { force: true }).catch(() => {});
95
+ throw error;
96
+ }
97
+ }
55
98
  function nodeEvolutionIo() {
56
99
  const isMissing = (error) => {
57
100
  const code = error?.code;
@@ -84,6 +127,13 @@ function nodeEvolutionIo() {
84
127
  * retry budget (budget >= 2 x threshold), so a dead holder's lock is
85
128
  * actually recoverable within one budget instead of being arithmetically
86
129
  * unreachable.
130
+ * 0.3.21 (F-101): takeover re-reads the lock right before removing it and
131
+ * only removes it when the content still names the dead pid — a peer that
132
+ * acquired the lock after our stale probe wrote its own pid, and deleting a
133
+ * LIVE lock is the double-hold (concurrent task) the probe must prevent.
134
+ * 0.3.21 (F-367): a self-pid lock is this process's own leftover (a failed
135
+ * release or a crash) and is recycled immediately regardless of age; a
136
+ * failure to release in finally is recorded so the next write self-heals.
87
137
  */
88
138
  const withWriteLock = async (path, task) => {
89
139
  const lock = `${path}.lock`;
@@ -95,14 +145,23 @@ function nodeEvolutionIo() {
95
145
  if (code !== "EEXIST" && code !== "EPERM") throw error;
96
146
  try {
97
147
  const st = await stat(lock);
98
- if (Date.now() - st.mtimeMs > 1e3) {
99
- const holder = Number(await readFile(lock, "utf8").catch(() => ""));
100
- if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
148
+ const holderContent = await readFile(lock, "utf8").catch(() => "");
149
+ const holder = Number(holderContent);
150
+ const holderAlive = Number.isInteger(holder) && holder > 0 && isAlive(holder);
151
+ if (holder === process.pid && (pendingSelfCleanup.has(lock) || Date.now() - st.mtimeMs > 1e3)) {
152
+ if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
101
153
  try {
102
154
  await rm(lock, { force: true });
103
155
  } catch {}
104
- continue;
156
+ pendingSelfCleanup.delete(lock);
105
157
  }
158
+ continue;
159
+ }
160
+ if (Date.now() - st.mtimeMs > 1e3 && !holderAlive) {
161
+ if (await readFile(lock, "utf8").catch(() => "") === holderContent) try {
162
+ await rm(lock, { force: true });
163
+ } catch {}
164
+ continue;
106
165
  }
107
166
  } catch {
108
167
  continue;
@@ -113,7 +172,9 @@ function nodeEvolutionIo() {
113
172
  try {
114
173
  return await task();
115
174
  } finally {
116
- await rm(lock, { force: true }).catch(() => {});
175
+ await rm(lock, { force: true }).catch(() => {
176
+ pendingSelfCleanup.add(lock);
177
+ });
117
178
  }
118
179
  }
119
180
  throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
@@ -139,7 +200,7 @@ function nodeEvolutionIo() {
139
200
  try {
140
201
  const st = await stat(tmpPath);
141
202
  const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
142
- if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
203
+ if (holder === process.pid || Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
143
204
  } catch {}
144
205
  }
145
206
  };
@@ -158,7 +219,7 @@ function nodeEvolutionIo() {
158
219
  await sweepStaleTmps(path);
159
220
  const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
160
221
  await writeFile(tmp, content, "utf8");
161
- await rename(tmp, path);
222
+ await commitTmp(tmp, path);
162
223
  });
163
224
  },
164
225
  async transact(path, task) {
@@ -179,7 +240,7 @@ function nodeEvolutionIo() {
179
240
  }
180
241
  const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
181
242
  await writeFile(tmp, next, "utf8");
182
- await rename(tmp, path);
243
+ await commitTmp(tmp, path);
183
244
  });
184
245
  },
185
246
  async remove(path) {
@@ -4400,4 +4461,4 @@ function evolutionHome(env = process.env) {
4400
4461
  return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
4401
4462
  }
4402
4463
  //#endregion
4403
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
4464
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
@@ -17,7 +17,7 @@
17
17
  * Package-private tunables (used by exactly one package) stay in that package,
18
18
  * not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
19
19
  * threshold, which are intentionally left where they are used.
20
- * @module @deepseek-ai/dsh-evolution-core
20
+ * @module @lmzhen/dsh-evolution-core
21
21
  */
22
22
  /** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
23
23
  export declare const SKILL_NAME_RE: RegExp;
@@ -9,7 +9,7 @@
9
9
  * protections (pinned / bundled / hub-installed) are file markers resolved by
10
10
  * `SkillLibrary.writeProtection` / `deleteProtection` — they depend on the
11
11
  * filesystem and the write origin, not on a name list.
12
- * @module @deepseek-ai/dsh-evolution-core
12
+ * @module @lmzhen/dsh-evolution-core
13
13
  */
14
14
  export type GateReason = 'excluded' | 'referenced' | 'suppressed' | 'protected-builtin';
15
15
  export interface GateSetInputs {
@@ -5,7 +5,7 @@
5
5
  * types, and session-event augmentations. This package owns no Cordis plugin
6
6
  * entry of its own; consumers import named exports from the package root so
7
7
  * published npm bundles never depend on source subpaths.
8
- * @module @deepseek-ai/dsh-evolution-core
8
+ * @module @lmzhen/dsh-evolution-core
9
9
  */
10
10
  export * from './curator.ts';
11
11
  export * from './evolution-events.ts';
package/lib/types/io.d.ts CHANGED
@@ -53,5 +53,17 @@ export interface EvolutionIoLike {
53
53
  export declare function transactIo(io: EvolutionIoLike, path: string, task: (current: string | null) => string | null | Promise<string | null>): Promise<void>;
54
54
  /** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
55
55
  export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
56
+ /**
57
+ * Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
58
+ * a short 50ms backoff, at most 3 retries (~150ms budget), matching the
59
+ * write-lock cadence. A non-transient code surfaces immediately. `fn` is the
60
+ * rename primitive, injectable for deterministic tests.
61
+ *
62
+ * @param tmp - the source path to rename.
63
+ * @param target - the destination path.
64
+ * @param fn - the rename primitive (defaults to `node:fs/promises.rename`).
65
+ * @returns a promise that resolves once the rename succeeds.
66
+ */
67
+ export declare function renameWithRetry(tmp: string, target: string, fn?: (from: string, to: string) => Promise<void>): Promise<void>;
56
68
  export declare function nodeEvolutionIo(): EvolutionIoLike;
57
69
  //# sourceMappingURL=io.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * Curator/author audit trail: `.mutations.json` records every skill mutation
3
3
  * with before/after content hashes so any automated edit is reviewable and
4
4
  * replayable. Best-effort persistence, mirroring the usage sidecar posture.
5
- * @module @deepseek-ai/dsh-evolution-core
5
+ * @module @lmzhen/dsh-evolution-core
6
6
  */
7
7
  import { type EvolutionIoLike } from './io.ts';
8
8
  export interface MutationRecord {
@@ -7,7 +7,7 @@
7
7
  * mutation maturity is a documented DSH approximation (single per-month patch
8
8
  * trend ratio replaces the claw timestamp-trend formula, since DSH usage
9
9
  * records only carry the last patched timestamp).
10
- * @module @deepseek-ai/dsh-evolution-core
10
+ * @module @lmzhen/dsh-evolution-core
11
11
  */
12
12
  import type { UsageMap } from './usage.ts';
13
13
  export interface QualityFactors {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.19",
4
+ "version": "0.3.21",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -25,10 +25,8 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "files": [
28
- "lib/index.js",
29
- "lib/invariant.js",
30
- "lib/types/**/*.d.ts",
31
- "lib/types/invariant.d.ts"
28
+ "lib/*.js",
29
+ "lib/types/**/*.d.ts"
32
30
  ],
33
31
  "license": "MIT",
34
32
  "dependencies": {