@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 +70 -9
- package/lib/types/constants.d.ts +1 -1
- package/lib/types/gates.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/io.d.ts +12 -0
- package/lib/types/mutations.d.ts +1 -1
- package/lib/types/quality.d.ts +1 -1
- package/package.json +3 -5
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -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 @
|
|
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;
|
package/lib/types/gates.d.ts
CHANGED
|
@@ -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 @
|
|
12
|
+
* @module @lmzhen/dsh-evolution-core
|
|
13
13
|
*/
|
|
14
14
|
export type GateReason = 'excluded' | 'referenced' | 'suppressed' | 'protected-builtin';
|
|
15
15
|
export interface GateSetInputs {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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 @
|
|
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
|
package/lib/types/mutations.d.ts
CHANGED
|
@@ -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 @
|
|
5
|
+
* @module @lmzhen/dsh-evolution-core
|
|
6
6
|
*/
|
|
7
7
|
import { type EvolutionIoLike } from './io.ts';
|
|
8
8
|
export interface MutationRecord {
|
package/lib/types/quality.d.ts
CHANGED
|
@@ -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 @
|
|
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.
|
|
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
|
|
29
|
-
"lib/
|
|
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": {
|