@lmzhen/dsh-evolution-core 0.3.16 → 0.3.17

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
@@ -64,45 +64,81 @@ function nodeEvolutionIo() {
64
64
  };
65
65
  /**
66
66
  * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
67
- * guards the atomic write. A >5s-old lock is taken over ONLY after probing
67
+ * guards the atomic write. A >1s-old lock is taken over ONLY after probing
68
68
  * the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
69
- * slow writer no longer loses its lock to a peer at the 5s mark (the
69
+ * slow writer no longer loses its lock to a peer at the threshold (the
70
70
  * takeover is the only best-effort surface; the retry budget fails loud —
71
71
  * rc.65 — instead of ever proceeding unlocked). Budget = 40 * 50ms (~2s,
72
72
  * rc.69): 8-writer contention bursts on a loaded CI runner exceed 10
73
73
  * attempts (500ms), and a fail-loud throw was observed instead of a clean
74
74
  * serialization.
75
+ * 0.3.17 (E-8): acquisition and the task are now SEPARATE try blocks — a
76
+ * task error (win32 rename/EBUSY surfaces as EPERM) used to be mistaken for
77
+ * lock contention, retried up to 40x and finally reported as
78
+ * "could not acquire write lock" while the real cause was hidden.
79
+ * 0.3.17 (E-8a): the takeover threshold (1000ms) now fits INSIDE the ~2s
80
+ * retry budget (budget >= 2 x threshold), so a dead holder's lock is
81
+ * actually recoverable within one budget instead of being arithmetically
82
+ * unreachable.
75
83
  */
76
84
  const withWriteLock = async (path, task) => {
77
85
  const lock = `${path}.lock`;
78
- for (let attempt = 0; attempt < 40; attempt += 1) try {
79
- await writeFile(lock, String(process.pid), { flag: "wx" });
86
+ for (let attempt = 0; attempt < 40; attempt += 1) {
80
87
  try {
81
- return await task();
82
- } finally {
83
- await rm(lock, { force: true }).catch(() => {});
84
- }
85
- } catch (error) {
86
- const code = error?.code;
87
- if (code !== "EEXIST" && code !== "EPERM") throw error;
88
- try {
89
- const st = await stat(lock);
90
- if (Date.now() - st.mtimeMs > 5e3) {
91
- const holder = Number(await readFile(lock, "utf8").catch(() => ""));
92
- if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
93
- try {
94
- await rm(lock, { force: true });
95
- } catch {}
96
- continue;
88
+ await writeFile(lock, String(process.pid), { flag: "wx" });
89
+ } catch (error) {
90
+ const code = error?.code;
91
+ if (code !== "EEXIST" && code !== "EPERM") throw error;
92
+ try {
93
+ const st = await stat(lock);
94
+ if (Date.now() - st.mtimeMs > 1e3) {
95
+ const holder = Number(await readFile(lock, "utf8").catch(() => ""));
96
+ if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
97
+ try {
98
+ await rm(lock, { force: true });
99
+ } catch {}
100
+ continue;
101
+ }
97
102
  }
103
+ } catch {
104
+ continue;
98
105
  }
99
- } catch {
106
+ await new Promise((resolve) => setTimeout(resolve, 50));
100
107
  continue;
101
108
  }
102
- await new Promise((resolve) => setTimeout(resolve, 50));
109
+ try {
110
+ return await task();
111
+ } finally {
112
+ await rm(lock, { force: true }).catch(() => {});
113
+ }
103
114
  }
104
115
  throw new Error(`could not acquire write lock for ${path} after 40 attempts`);
105
116
  };
117
+ /** 0.3.17 (E-8b): sweep tmp files a crashed writer left behind — same
118
+ * `<target>.<pid>.<rand>.tmp` shape, older than 1h AND held by a dead pid.
119
+ * Lazy: only the directory a write is about to touch gets swept, once per
120
+ * write, inside the write lock. */
121
+ const sweepStaleTmps = async (path) => {
122
+ const dir = dirname(path);
123
+ const base = basename(path);
124
+ let entries;
125
+ try {
126
+ entries = await readdir(dir);
127
+ } catch {
128
+ return;
129
+ }
130
+ const prefix = `${base}.`;
131
+ for (const name of entries) {
132
+ if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
133
+ const tmpPath = join(dir, name);
134
+ const holder = Number(name.slice(prefix.length, name.length - 4).split(".")[0] ?? "");
135
+ try {
136
+ const st = await stat(tmpPath);
137
+ const deadHolder = !Number.isInteger(holder) || holder <= 0 || !isAlive(holder);
138
+ if (Date.now() - st.mtimeMs > 36e5 && deadHolder) await rm(tmpPath, { force: true });
139
+ } catch {}
140
+ }
141
+ };
106
142
  return {
107
143
  async readText(path) {
108
144
  try {
@@ -115,6 +151,7 @@ function nodeEvolutionIo() {
115
151
  async writeText(path, content) {
116
152
  await mkdir(dirname(path), { recursive: true });
117
153
  await withWriteLock(path, async () => {
154
+ await sweepStaleTmps(path);
118
155
  const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
119
156
  await writeFile(tmp, content, "utf8");
120
157
  await rename(tmp, path);
@@ -123,6 +160,7 @@ function nodeEvolutionIo() {
123
160
  async transact(path, task) {
124
161
  await mkdir(dirname(path), { recursive: true });
125
162
  await withWriteLock(path, async () => {
163
+ await sweepStaleTmps(path);
126
164
  let current;
127
165
  try {
128
166
  current = await readFile(path, "utf8");
@@ -494,6 +532,19 @@ const DEFAULT_USER_CHAR_LIMIT = 1375;
494
532
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
495
533
  const DEFAULT_CONSOLIDATION_FAILURES = 3;
496
534
  const DEFAULT_SKILL_CONTENT_CHARS = 1e5;
535
+ /** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
536
+ * never carry — single source for plan-validator, evolution-policy and the
537
+ * threat scanner (they used to each hardcode the list). */
538
+ const FORBIDDEN_CONTROL_KEYS = [
539
+ "policy",
540
+ "threshold",
541
+ "prompt_hash",
542
+ "model_route",
543
+ "evolution_config"
544
+ ];
545
+ /** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
546
+ * scanner cover. */
547
+ const EVOLUTION_WRITE_TOOLS = ["memory", "skill_manage"];
497
548
  /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
498
549
  * platform's own index limit stays in validateFrontmatter; this bar is the
499
550
  * target the authoring standard names, enforced as ADVISORY feedback.
@@ -2362,6 +2413,24 @@ function redactSecrets(text) {
2362
2413
  return out;
2363
2414
  }
2364
2415
  //#endregion
2416
+ //#region lib/types/serial.js
2417
+ /**
2418
+ * A process-local serial task queue: each task starts only after the previous
2419
+ * one settles (success or failure), so read-modify-write sequences that share
2420
+ * one file never interleave inside this process. The durable cross-process
2421
+ * serialization layer is the IO backend's transact lock; this chain is the
2422
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
2423
+ * memory-files — one factory now).
2424
+ */
2425
+ function makeSerialQueue() {
2426
+ let chain = Promise.resolve();
2427
+ return (task) => {
2428
+ const run = chain.then(task, task);
2429
+ chain = run.then(() => void 0, () => void 0);
2430
+ return run;
2431
+ };
2432
+ }
2433
+ //#endregion
2365
2434
  //#region lib/types/skill-health.js
2366
2435
  /**
2367
2436
  * Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
@@ -4301,4 +4370,4 @@ function evolutionHome(env = process.env) {
4301
4370
  return join(env.DSH_HOME || join(homedir(), ".dsh"), "evolution");
4302
4371
  }
4303
4372
  //#endregion
4304
- 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, EvolutionGateSet, 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, 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, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
4373
+ 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, 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, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
@@ -50,6 +50,13 @@ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
50
50
  /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
51
51
  export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
52
52
  export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
53
+ /** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
54
+ * never carry — single source for plan-validator, evolution-policy and the
55
+ * threat scanner (they used to each hardcode the list). */
56
+ export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config"];
57
+ /** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
58
+ * scanner cover. */
59
+ export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
53
60
  /** Hermes authoring quality bar for descriptions (the 60-char Rule). The
54
61
  * platform's own index limit stays in validateFrontmatter; this bar is the
55
62
  * target the authoring standard names, enforced as ADVISORY feedback.
@@ -19,6 +19,7 @@ export * from './preset-composition.ts';
19
19
  export * from './prompts.ts';
20
20
  export * from './quality.ts';
21
21
  export * from './redact.ts';
22
+ export * from './serial.ts';
22
23
  export * from './skill-health.ts';
23
24
  export * from './signals.ts';
24
25
  export * from './drift-signals.ts';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A process-local serial task queue: each task starts only after the previous
3
+ * one settles (success or failure), so read-modify-write sequences that share
4
+ * one file never interleave inside this process. The durable cross-process
5
+ * serialization layer is the IO backend's transact lock; this chain is the
6
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
7
+ * memory-files — one factory now).
8
+ */
9
+ export declare function makeSerialQueue(): <T>(task: () => Promise<T>) => Promise<T>;
10
+ //# sourceMappingURL=serial.d.ts.map
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.16",
4
+ "version": "0.3.17",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },