@lmzhen/dsh-evolution-core 0.3.26 → 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 {
@@ -3325,8 +3339,11 @@ var SkillLibrary = class {
3325
3339
  limits;
3326
3340
  io;
3327
3341
  onMutation;
3328
- /** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
3329
- * 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). */
3330
3347
  transact;
3331
3348
  /** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
3332
3349
  * one skill never interleave their read-modify-write (the cross-process layer
@@ -3337,7 +3354,10 @@ var SkillLibrary = class {
3337
3354
  this.io = io;
3338
3355
  this.limits = limits;
3339
3356
  this.onMutation = onMutation;
3340
- 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);
3341
3361
  this.serial = makeSerialQueue();
3342
3362
  }
3343
3363
  /**
@@ -4697,4 +4717,4 @@ function clampedNumber(value, fallback, opts) {
4697
4717
  return value;
4698
4718
  }
4699
4719
  //#endregion
4700
- 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, 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
@@ -221,8 +221,11 @@ export declare class SkillLibrary {
221
221
  readonly limits: SkillLimits;
222
222
  private readonly io;
223
223
  private readonly onMutation;
224
- /** 0.3.21 (F-208): optional cross-process RMW transactor injected by callers.
225
- * 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). */
226
229
  private readonly transact;
227
230
  /** 0.3.21 (F-208): in-process serialize queue so two concurrent mutators on
228
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.26",
4
+ "version": "0.3.27",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },