@lmzhen/dsh-evolution-core 0.3.25 → 0.3.27

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
@@ -58,6 +58,10 @@ function evolutionIoAdapter(provider) {
58
58
  * us, so a leftover is stale by definition. Module-level by design: it must
59
59
  * survive across `nodeEvolutionIo()` instances for the self-heal to be
60
60
  * effective. (Not a pure function — the cross-call state is the intent.)
61
+ * V4-05: this set — not an mtime heuristic — is the ONLY signal that a
62
+ * same-pid lock is a leftover rather than a live in-process task, so it is the
63
+ * sole gate for the self-pid recycle branch. Exported (read-only in practice)
64
+ * so `io.spec.ts` can drive the self-heal path deterministically.
61
65
  */
62
66
  const pendingSelfCleanup = /* @__PURE__ */ new Set();
63
67
  /**
@@ -127,13 +131,19 @@ function nodeEvolutionIo() {
127
131
  * retry budget (budget >= 2 x threshold), so a dead holder's lock is
128
132
  * actually recoverable within one budget instead of being arithmetically
129
133
  * 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
134
+ * 0.3.21 (F-101), V4-04: takeover re-reads the lock right before acting and
135
+ * only proceeds when the content still names the dead pid — a peer that
132
136
  * 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.
137
+ * LIVE lock is the double-hold (concurrent task) the probe must prevent. The
138
+ * re-read is necessary but not sufficient: two peers can both pass it, so the
139
+ * commit itself is an atomic rename to a unique name (only one peer's rename
140
+ * can succeed; the loser's source is gone). A naive rm lets the later peer
141
+ * delete the winner's freshly re-acquired live lock.
142
+ * 0.3.21 (F-367), V4-05: a self-pid lock is recycled ONLY when the failed
143
+ * release was recorded in pendingSelfCleanup — never on age alone, because an
144
+ * mtime over 1s is indistinguishable from a long task still executing in this
145
+ * process, and recycling that live lock would double-hold it. A failure to
146
+ * release in finally is recorded so the next write self-heals.
137
147
  */
138
148
  const withWriteLock = async (path, task) => {
139
149
  const lock = `${path}.lock`;
@@ -148,7 +158,7 @@ function nodeEvolutionIo() {
148
158
  const holderContent = await readFile(lock, "utf8").catch(() => "");
149
159
  const holder = Number(holderContent);
150
160
  const holderAlive = Number.isInteger(holder) && holder > 0 && isAlive(holder);
151
- if (holder === process.pid && (pendingSelfCleanup.has(lock) || Date.now() - st.mtimeMs > 1e3)) {
161
+ if (holder === process.pid && pendingSelfCleanup.has(lock)) {
152
162
  if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
153
163
  try {
154
164
  await rm(lock, { force: true });
@@ -158,9 +168,13 @@ function nodeEvolutionIo() {
158
168
  continue;
159
169
  }
160
170
  if (Date.now() - st.mtimeMs > 1e3 && !holderAlive) {
161
- if (await readFile(lock, "utf8").catch(() => "") === holderContent) try {
162
- await rm(lock, { force: true });
163
- } catch {}
171
+ if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
172
+ const takeover = `${lock}.takeover-${process.pid}-${randomBytes(6).toString("hex")}`;
173
+ try {
174
+ await rename(lock, takeover);
175
+ await rm(takeover, { force: true }).catch(() => {});
176
+ } catch {}
177
+ }
164
178
  continue;
165
179
  }
166
180
  } catch {
@@ -2937,7 +2951,9 @@ function skillDir(root, name) {
2937
2951
  /** Dot-prefixed on-disk marker name. SINGLE source: `list()` matches directory
2938
2952
  * entries against this name, and path builders must never hardcode a marker
2939
2953
  * literal (N-1: the rc.49 exists()-probe convergence dropped the dot,
2940
- * poisoning every protectedBy/managed report). */
2954
+ * poisoning every protectedBy/managed report). Exported for cross-package
2955
+ * consumers that must probe markers without re-deriving the name (curator's
2956
+ * archive-copy bundled probe, 0.3.26 V4-02). */
2941
2957
  function markerEntryName(marker) {
2942
2958
  return `.${marker}`;
2943
2959
  }
@@ -3323,8 +3339,11 @@ var SkillLibrary = class {
3323
3339
  limits;
3324
3340
  io;
3325
3341
  onMutation;
3326
- /** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
3327
- * When unset each single-file write falls back to read→task→write. */
3342
+ /** 0.3.21 (F-208) + V4-20: cross-process RMW transactor. An explicitly
3343
+ * injected value wins; otherwise the IO backend's own `transact` is bound
3344
+ * when it provides one (cross-process atomicity on by default for any
3345
+ * transact-capable backend), and a backend without `transact` leaves this
3346
+ * undefined (each write falls back to the plain read→task→write path). */
3328
3347
  transact;
3329
3348
  /** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
3330
3349
  * one skill never interleave their read-modify-write (the cross-process layer
@@ -3335,7 +3354,10 @@ var SkillLibrary = class {
3335
3354
  this.io = io;
3336
3355
  this.limits = limits;
3337
3356
  this.onMutation = onMutation;
3338
- this.transact = transact;
3357
+ this.transact = transact ?? (io.transact ? (ioLike, path, task) => {
3358
+ const t = ioLike.transact;
3359
+ return t ? t(path, task) : transactIo(ioLike, path, task);
3360
+ } : void 0);
3339
3361
  this.serial = makeSerialQueue();
3340
3362
  }
3341
3363
  /**
@@ -4695,4 +4717,4 @@ function clampedNumber(value, fallback, opts) {
4695
4717
  return value;
4696
4718
  }
4697
4719
  //#endregion
4698
- 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, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, 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 };
4720
+ 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, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, 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/io.d.ts CHANGED
@@ -53,6 +53,18 @@ 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
+ * F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
58
+ * to the same file proactively recycles our own leftover lock — the holder is
59
+ * us, so a leftover is stale by definition. Module-level by design: it must
60
+ * survive across `nodeEvolutionIo()` instances for the self-heal to be
61
+ * effective. (Not a pure function — the cross-call state is the intent.)
62
+ * V4-05: this set — not an mtime heuristic — is the ONLY signal that a
63
+ * same-pid lock is a leftover rather than a live in-process task, so it is the
64
+ * sole gate for the self-pid recycle branch. Exported (read-only in practice)
65
+ * so `io.spec.ts` can drive the self-heal path deterministically.
66
+ */
67
+ export declare const pendingSelfCleanup: Set<string>;
56
68
  /**
57
69
  * Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
58
70
  * a short 50ms backoff, at most 3 retries (~150ms budget), matching the
@@ -108,6 +108,13 @@ export declare function resolveOrigins(headerOrigin: string | undefined, isRevie
108
108
  approval: 'foreground' | 'background_review';
109
109
  library: WriteOrigin;
110
110
  };
111
+ /** Dot-prefixed on-disk marker name. SINGLE source: `list()` matches directory
112
+ * entries against this name, and path builders must never hardcode a marker
113
+ * literal (N-1: the rc.49 exists()-probe convergence dropped the dot,
114
+ * poisoning every protectedBy/managed report). Exported for cross-package
115
+ * consumers that must probe markers without re-deriving the name (curator's
116
+ * archive-copy bundled probe, 0.3.26 V4-02). */
117
+ export declare function markerEntryName(marker: 'bundled' | 'hub-installed' | 'pinned' | 'hermes-managed'): string;
111
118
  export interface Frontmatter {
112
119
  name?: string;
113
120
  description?: string;
@@ -214,8 +221,11 @@ export declare class SkillLibrary {
214
221
  readonly limits: SkillLimits;
215
222
  private readonly io;
216
223
  private readonly onMutation;
217
- /** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
218
- * When unset each single-file write falls back to read→task→write. */
224
+ /** 0.3.21 (F-208) + V4-20: cross-process RMW transactor. An explicitly
225
+ * injected value wins; otherwise the IO backend's own `transact` is bound
226
+ * when it provides one (cross-process atomicity on by default for any
227
+ * transact-capable backend), and a backend without `transact` leaves this
228
+ * undefined (each write falls back to the plain read→task→write path). */
219
229
  private readonly transact;
220
230
  /** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
221
231
  * one skill never interleave their read-modify-write (the cross-process layer
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.25",
4
+ "version": "0.3.27",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },