@lmzhen/dsh-evolution-state-json 0.3.15 → 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.
Files changed (2) hide show
  1. package/lib/index.js +44 -42
  2. package/package.json +5 -5
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
+ import { CLAIM_EXPIRY_MS, canClaimPending, canResolvePending, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
5
6
  //#region ../evolution-core/src/io.ts
6
7
  /**
7
8
  * Run `task` inside `io.transact` when the backend provides it; otherwise fall
@@ -17,31 +18,7 @@ async function transactIo(io, path, task) {
17
18
  if (next === null) await io.remove(path);
18
19
  else await io.writeText(path, next);
19
20
  }
20
- //#endregion
21
- //#region ../evolution-core/src/prompts.ts
22
- /**
23
- * Review and curation prompts adapted from Hermes Agent
24
- * `agent/background_review.py`, `agent/curator.py`, and
25
- * `agent/learn_prompt.py`, with tool names translated to the DSH-native
26
- * catalog (`memory`, `skill_manage`, `skill`, `bash`, `str_replace_editor`).
27
- *
28
- * Alignment policy (2026-08-29): the OPERATIONAL steps and instructions the
29
- * model follows mirror the Hermes originals structurally (signal list,
30
- * preference order, support-file taxonomy, curator package integrity,
31
- * consolidated/pruned reporting block). Tool and platform differences are
32
- * DSH-adapted (native tool names, pinned-within-review semantics, this
33
- * platform's index cap), and DSH-only additions are marked as such.
34
- *
35
- * Every prompt is pinned in a versioned bundle. Review workers verify the
36
- * bundle digest before spending a model call, so a partially-patched
37
- * deployment fails closed instead of silently running a truncated prompt.
38
- */
39
- /**
40
- * Prompt bundle identity. Bump both id and version whenever a prompt's text
41
- * changes semantically: the bundle digest is the fail-closed signal for
42
- * review workers, so a stale id across deployments must be distinguishable.
43
- */
44
- const PROMPT_BUNDLE_ID = "dsh-evolution@13";
21
+ const PROMPT_BUNDLE_ID = `dsh-evolution@13`;
45
22
  const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
46
23
  Review the conversation above and consider saving to memory if appropriate.
47
24
 
@@ -325,6 +302,24 @@ createPromptBundle({
325
302
  const FILLER = String.raw`(?:\w+\s+){0,8}`;
326
303
  new RegExp(String.raw`ignore\s+${FILLER}(?:previous|above|prior|all)\s+${FILLER}instructions`, "i"), new RegExp(String.raw`new\s+${FILLER}system\s+${FILLER}prompt`, "i"), new RegExp(String.raw`forget\s+${FILLER}(?:everything|all)\s+${FILLER}(?:discussed|you\s+know)`, "i"), new RegExp(String.raw`you\s+have\s+been\s+${FILLER}(?:updated|upgraded|patched)\s+to`, "i"), new RegExp(String.raw`do\s+not\s+${FILLER}tell\s+${FILLER}the\s+user`, "i"), new RegExp(String.raw`output\s+${FILLER}(?:system|initial)\s+prompt`, "i");
327
304
  //#endregion
305
+ //#region ../evolution-core/src/serial.ts
306
+ /**
307
+ * A process-local serial task queue: each task starts only after the previous
308
+ * one settles (success or failure), so read-modify-write sequences that share
309
+ * one file never interleave inside this process. The durable cross-process
310
+ * serialization layer is the IO backend's transact lock; this chain is the
311
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
312
+ * memory-files — one factory now).
313
+ */
314
+ function makeSerialQueue() {
315
+ let chain = Promise.resolve();
316
+ return (task) => {
317
+ const run = chain.then(task, task);
318
+ chain = run.then(() => void 0, () => void 0);
319
+ return run;
320
+ };
321
+ }
322
+ //#endregion
328
323
  //#region ../../../node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml/dist/js-yaml.mjs
329
324
  /*! js-yaml 4.2.0 https://github.com/nodeca/js-yaml @license MIT */
330
325
  var __create = Object.create;
@@ -2672,13 +2667,24 @@ function apply(ctx, rawConfig) {
2672
2667
  const root = rawConfig.root || defaultRoot();
2673
2668
  const io = () => ctx.evolutionIo.provider();
2674
2669
  const pathOf = (file) => join(root, file);
2670
+ /** 0.3.17 (E-9): a malformed state file used to parse to `null` and was then
2671
+ * OVERWRITTEN by the next save — every other session's review state / the
2672
+ * whole pending table vanished silently. Fail loud instead: preserve the
2673
+ * original bytes beside it and throw, so the operator can rescue and the
2674
+ * corruption is never accepted as "empty". */
2675
+ async function quarantine(file, raw, reason) {
2676
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2677
+ const dest = `${pathOf(file)}.corrupt-${stamp}-${Math.random().toString(36).slice(2, 6)}`;
2678
+ await io().writeText(dest, raw).catch(() => {});
2679
+ throw new Error(`evolution state file "${file}" is not valid JSON (${reason}); original preserved at ${dest} — inspect and fix it, then retry.`);
2680
+ }
2675
2681
  async function readJson(file) {
2676
2682
  const raw = await io().readText(pathOf(file));
2677
2683
  if (raw === null) return null;
2678
2684
  try {
2679
2685
  return JSON.parse(raw);
2680
- } catch {
2681
- return null;
2686
+ } catch (error) {
2687
+ return await quarantine(file, raw, error instanceof Error ? error.message : String(error));
2682
2688
  }
2683
2689
  }
2684
2690
  /**
@@ -2693,19 +2699,14 @@ function apply(ctx, rawConfig) {
2693
2699
  let parsed = null;
2694
2700
  if (current !== null) try {
2695
2701
  parsed = JSON.parse(current);
2696
- } catch {
2697
- parsed = null;
2702
+ } catch (error) {
2703
+ return await quarantine(file, current, error instanceof Error ? error.message : String(error));
2698
2704
  }
2699
2705
  const next = await task(parsed);
2700
2706
  return next === null ? null : JSON.stringify(next, null, 2);
2701
2707
  });
2702
2708
  }
2703
- let chain = Promise.resolve();
2704
- function mutate(task) {
2705
- const run = chain.then(task, task);
2706
- chain = run.then(() => void 0, () => void 0);
2707
- return run;
2708
- }
2709
+ const mutate = makeSerialQueue();
2709
2710
  async function loadPendingMap() {
2710
2711
  const [current, legacy] = await Promise.all([readJson("pending-state.json"), readJson("pending.json")]);
2711
2712
  return {
@@ -2767,12 +2768,13 @@ function apply(ctx, rawConfig) {
2767
2768
  ...current ?? {}
2768
2769
  };
2769
2770
  const record = map[id] ?? null;
2770
- if (record === null || record.status !== "pending") return map;
2771
+ if (record === null || !canClaimPending(record.status)) return map;
2771
2772
  const now = Date.now();
2772
2773
  const claimedAt = typeof record.claimedAt === "string" ? Date.parse(record.claimedAt) : 0;
2773
- if (record.claimedBy !== void 0 && Number.isFinite(claimedAt) && now - claimedAt < 10 * 6e4) return map;
2774
+ if (record.claimedBy !== void 0 && Number.isFinite(claimedAt) && now - claimedAt < CLAIM_EXPIRY_MS) return map;
2774
2775
  slot.claimed = {
2775
2776
  ...record,
2777
+ status: "executing",
2776
2778
  claimedBy: claimId,
2777
2779
  claimedAt: new Date(now).toISOString()
2778
2780
  };
@@ -2790,10 +2792,10 @@ function apply(ctx, rawConfig) {
2790
2792
  ...current ?? {}
2791
2793
  };
2792
2794
  const record = map[id];
2793
- if (record && record.status === "pending" && record.claimedBy === claimId) {
2794
- delete record.claimedBy;
2795
- delete record.claimedAt;
2796
- }
2795
+ if (!record || record.claimedBy !== claimId) return map;
2796
+ record.status = releasedStatus(record.status);
2797
+ delete record.claimedBy;
2798
+ delete record.claimedAt;
2797
2799
  return map;
2798
2800
  });
2799
2801
  });
@@ -2810,7 +2812,7 @@ function apply(ctx, rawConfig) {
2810
2812
  ...current ?? {}
2811
2813
  };
2812
2814
  const record = map[id] ?? null;
2813
- if (record === null || record.status !== "pending") {
2815
+ if (record === null || !canResolvePending(record.status)) {
2814
2816
  result = {
2815
2817
  record,
2816
2818
  applied: false
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-state-json",
3
3
  "description": "JSON-file evolution state provider over the IO seam (community build)",
4
- "version": "0.3.15",
4
+ "version": "0.3.17",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,12 +37,12 @@
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
- "@lmzhen/dsh-evolution-io": "^0.3.15",
41
- "@lmzhen/dsh-evolution-state-storage": "^0.3.15"
40
+ "@lmzhen/dsh-evolution-io": "^0.3.17",
41
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.17"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
- "@lmzhen/dsh-evolution-io": "^0.3.15",
46
- "@lmzhen/dsh-evolution-state-storage": "^0.3.15"
45
+ "@lmzhen/dsh-evolution-io": "^0.3.17",
46
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.17"
47
47
  }
48
48
  }