@lmzhen/dsh-evolution-state-json 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.
Files changed (2) hide show
  1. package/lib/index.js +43 -17
  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
@@ -301,6 +302,24 @@ createPromptBundle({
301
302
  const FILLER = String.raw`(?:\w+\s+){0,8}`;
302
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");
303
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
304
323
  //#region ../../../node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml/dist/js-yaml.mjs
305
324
  /*! js-yaml 4.2.0 https://github.com/nodeca/js-yaml @license MIT */
306
325
  var __create = Object.create;
@@ -2648,13 +2667,24 @@ function apply(ctx, rawConfig) {
2648
2667
  const root = rawConfig.root || defaultRoot();
2649
2668
  const io = () => ctx.evolutionIo.provider();
2650
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
+ }
2651
2681
  async function readJson(file) {
2652
2682
  const raw = await io().readText(pathOf(file));
2653
2683
  if (raw === null) return null;
2654
2684
  try {
2655
2685
  return JSON.parse(raw);
2656
- } catch {
2657
- return null;
2686
+ } catch (error) {
2687
+ return await quarantine(file, raw, error instanceof Error ? error.message : String(error));
2658
2688
  }
2659
2689
  }
2660
2690
  /**
@@ -2669,19 +2699,14 @@ function apply(ctx, rawConfig) {
2669
2699
  let parsed = null;
2670
2700
  if (current !== null) try {
2671
2701
  parsed = JSON.parse(current);
2672
- } catch {
2673
- parsed = null;
2702
+ } catch (error) {
2703
+ return await quarantine(file, current, error instanceof Error ? error.message : String(error));
2674
2704
  }
2675
2705
  const next = await task(parsed);
2676
2706
  return next === null ? null : JSON.stringify(next, null, 2);
2677
2707
  });
2678
2708
  }
2679
- let chain = Promise.resolve();
2680
- function mutate(task) {
2681
- const run = chain.then(task, task);
2682
- chain = run.then(() => void 0, () => void 0);
2683
- return run;
2684
- }
2709
+ const mutate = makeSerialQueue();
2685
2710
  async function loadPendingMap() {
2686
2711
  const [current, legacy] = await Promise.all([readJson("pending-state.json"), readJson("pending.json")]);
2687
2712
  return {
@@ -2743,12 +2768,13 @@ function apply(ctx, rawConfig) {
2743
2768
  ...current ?? {}
2744
2769
  };
2745
2770
  const record = map[id] ?? null;
2746
- if (record === null || record.status !== "pending") return map;
2771
+ if (record === null || !canClaimPending(record.status)) return map;
2747
2772
  const now = Date.now();
2748
2773
  const claimedAt = typeof record.claimedAt === "string" ? Date.parse(record.claimedAt) : 0;
2749
- 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;
2750
2775
  slot.claimed = {
2751
2776
  ...record,
2777
+ status: "executing",
2752
2778
  claimedBy: claimId,
2753
2779
  claimedAt: new Date(now).toISOString()
2754
2780
  };
@@ -2766,10 +2792,10 @@ function apply(ctx, rawConfig) {
2766
2792
  ...current ?? {}
2767
2793
  };
2768
2794
  const record = map[id];
2769
- if (record && record.status === "pending" && record.claimedBy === claimId) {
2770
- delete record.claimedBy;
2771
- delete record.claimedAt;
2772
- }
2795
+ if (!record || record.claimedBy !== claimId) return map;
2796
+ record.status = releasedStatus(record.status);
2797
+ delete record.claimedBy;
2798
+ delete record.claimedAt;
2773
2799
  return map;
2774
2800
  });
2775
2801
  });
@@ -2786,7 +2812,7 @@ function apply(ctx, rawConfig) {
2786
2812
  ...current ?? {}
2787
2813
  };
2788
2814
  const record = map[id] ?? null;
2789
- if (record === null || record.status !== "pending") {
2815
+ if (record === null || !canResolvePending(record.status)) {
2790
2816
  result = {
2791
2817
  record,
2792
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.16",
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.16",
41
- "@lmzhen/dsh-evolution-state-storage": "^0.3.16"
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.16",
46
- "@lmzhen/dsh-evolution-state-storage": "^0.3.16"
45
+ "@lmzhen/dsh-evolution-io": "^0.3.17",
46
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.17"
47
47
  }
48
48
  }