@lmzhen/dsh-evolution-core 0.1.0-rc.35 → 0.1.0-rc.37

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
@@ -32,6 +32,39 @@ function nodeEvolutionIo() {
32
32
  const code = error?.code;
33
33
  return code === "ENOENT" || code === "ENOTDIR";
34
34
  };
35
+ /**
36
+ * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
37
+ * guards the atomic write; a >5s-old lock is treated as stale and taken
38
+ * over. After the retry budget the write proceeds unlocked — the lock is a
39
+ * best-effort accommodation for multi-process deployments, never a read of
40
+ * availability.
41
+ */
42
+ const withWriteLock = async (path, task) => {
43
+ const lock = `${path}.lock`;
44
+ for (let attempt = 0; attempt < 10; attempt += 1) try {
45
+ await writeFile(lock, String(process.pid), { flag: "wx" });
46
+ try {
47
+ return await task();
48
+ } finally {
49
+ await rm(lock, { force: true }).catch(() => {});
50
+ }
51
+ } catch (error) {
52
+ if (error?.code !== "EEXIST") throw error;
53
+ try {
54
+ const st = await stat(lock);
55
+ if (Date.now() - st.mtimeMs > 5e3) {
56
+ try {
57
+ await rm(lock, { force: true });
58
+ } catch {}
59
+ continue;
60
+ }
61
+ } catch {
62
+ continue;
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, 50));
65
+ }
66
+ return await task();
67
+ };
35
68
  return {
36
69
  async readText(path) {
37
70
  try {
@@ -43,9 +76,11 @@ function nodeEvolutionIo() {
43
76
  },
44
77
  async writeText(path, content) {
45
78
  await mkdir(dirname(path), { recursive: true });
46
- const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
47
- await writeFile(tmp, content, "utf8");
48
- await rename(tmp, path);
79
+ await withWriteLock(path, async () => {
80
+ const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
81
+ await writeFile(tmp, content, "utf8");
82
+ await rename(tmp, path);
83
+ });
49
84
  },
50
85
  async remove(path) {
51
86
  await rm(path, {
@@ -1837,7 +1872,7 @@ var SkillLibrary = class {
1837
1872
  path: dir
1838
1873
  };
1839
1874
  }
1840
- async create(name, content, origin) {
1875
+ async create(name, content, origin = "foreground") {
1841
1876
  const normalized = name.trim();
1842
1877
  if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return {
1843
1878
  ok: false,
@@ -1859,7 +1894,7 @@ var SkillLibrary = class {
1859
1894
  message: `Skill "${normalized}" already exists.`
1860
1895
  };
1861
1896
  await this.io.writeText(join(dir, "SKILL.md"), content.trimEnd() + "\n");
1862
- if (origin === "background_review") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
1897
+ if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
1863
1898
  await this.audit(normalized, "create", null, content, "created");
1864
1899
  return {
1865
1900
  ok: true,
@@ -2222,8 +2257,16 @@ var SkillLibrary = class {
2222
2257
  skills: names,
2223
2258
  sidecars
2224
2259
  }, null, 2));
2260
+ await this.retainSnapshots(5);
2225
2261
  return dest;
2226
2262
  }
2263
+ /** Keep only the newest N snapshots (Hermes keep=5 parity); oldest folded into .backups history. */
2264
+ async retainSnapshots(keep) {
2265
+ const snapshots = await this.listSnapshots();
2266
+ for (const snapshot of snapshots.slice(keep)) try {
2267
+ await this.io.remove(snapshot.path);
2268
+ } catch {}
2269
+ }
2227
2270
  async listSnapshots() {
2228
2271
  const backupRoot = join(this.root, ".backups");
2229
2272
  let entries;
@@ -29,7 +29,7 @@ export interface SkillActionResult {
29
29
  path?: string;
30
30
  }
31
31
  /** Who is writing: a foreground user-directed tool call, or the autonomous review/curator pipeline. */
32
- export type WriteOrigin = 'foreground' | 'background_review';
32
+ export type WriteOrigin = 'foreground' | 'subagent' | 'background_review';
33
33
  /** Options for `SkillLibrary.archive`. The absorbed-into name and the archival reason are distinct fields. */
34
34
  export interface ArchiveOptions {
35
35
  /** Umbrella skill this one was consolidated into; when set it must exist (consolidate semantics). */
@@ -81,7 +81,7 @@ export declare class SkillLibrary {
81
81
  * marker write is the only state change; content is untouched.
82
82
  */
83
83
  setPinned(name: string, pinned: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
84
- create(name: string, content: string, origin: 'foreground' | 'background_review'): Promise<SkillActionResult>;
84
+ create(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
85
85
  update(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
86
86
  patch(name: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
87
87
  archive(name: string, options?: ArchiveOptions): Promise<SkillActionResult>;
@@ -100,6 +100,8 @@ export declare class SkillLibrary {
100
100
  writeSupportFile(name: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
101
101
  removeSupportFile(name: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
102
102
  snapshotAll(reason?: string): Promise<string>;
103
+ /** Keep only the newest N snapshots (Hermes keep=5 parity); oldest folded into .backups history. */
104
+ private retainSnapshots;
103
105
  listSnapshots(): Promise<Array<{
104
106
  path: string;
105
107
  createdAt: string;
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.1.0-rc.35",
4
+ "version": "0.1.0-rc.37",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },