@lmzhen/dsh-evolution-core 0.1.0-rc.21 → 0.1.0-rc.22

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
@@ -20,7 +20,11 @@ function evolutionIoAdapter(provider) {
20
20
  list: (path) => provider().list(path),
21
21
  exists: (path) => provider().exists(path),
22
22
  rename: (path, destination) => provider().rename(path, destination),
23
- copy: (path, destination) => provider().copy(path, destination)
23
+ copy: (path, destination) => provider().copy(path, destination),
24
+ size: (path) => {
25
+ const io = provider();
26
+ return io.size ? io.size(path) : Promise.resolve(null);
27
+ }
24
28
  };
25
29
  }
26
30
  function nodeEvolutionIo() {
@@ -69,6 +73,13 @@ function nodeEvolutionIo() {
69
73
  recursive: true,
70
74
  force: true
71
75
  });
76
+ },
77
+ async size(path) {
78
+ try {
79
+ return (await stat(path)).size;
80
+ } catch {
81
+ return null;
82
+ }
72
83
  }
73
84
  };
74
85
  }
@@ -599,6 +610,13 @@ function scanContentThreats(text, maxScanChars = 65536, options = NO_SCAN_OPTION
599
610
  * File-backed durable memory with Hermes-compatible semantics.
600
611
  * Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
601
612
  */
613
+ /**
614
+ * Read-guard factor: a memory file larger than this multiple of its target's
615
+ * char limit is treated as externally corrupted and skipped instead of being
616
+ * read whole (aligned with claw `tools/memory.ts` size guard, which uses the
617
+ * same 10× bound around a file that should never exceed the store limit).
618
+ */
619
+ const READ_GUARD_FACTOR = 10;
602
620
  function memoryRoot(env = process.env) {
603
621
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "memories");
604
622
  }
@@ -633,7 +651,24 @@ var MemoryStore = class {
633
651
  limitFor(target) {
634
652
  return target === "memory" ? this.memoryLimit : this.userLimit;
635
653
  }
654
+ /**
655
+ * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
656
+ * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
657
+ * (backend without a size probe), under the bound, or the target has no
658
+ * limit configured.
659
+ */
660
+ async oversizedFile(target) {
661
+ const size = await this.io.size?.(fileFor(this.root, target));
662
+ if (size === null || size === void 0) return null;
663
+ const limit = this.limitFor(target);
664
+ if (limit <= 0) return null;
665
+ return size > limit * READ_GUARD_FACTOR ? {
666
+ size,
667
+ limit
668
+ } : null;
669
+ }
636
670
  async read(target) {
671
+ if (await this.oversizedFile(target)) return [];
637
672
  const raw = await this.io.readText(fileFor(this.root, target));
638
673
  return raw === null ? [] : [...new Set(normalizeEntries(raw))];
639
674
  }
@@ -669,23 +704,45 @@ var MemoryStore = class {
669
704
  return percent >= 80 ? ` ⚠️ Storage at ${percent}% (${chars}/${limit} chars).` : "";
670
705
  }
671
706
  /**
672
- * Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
673
- * refusing the write, so an external edit stays recoverable. Failure to back
674
- * up does not change the refusal semantics.
707
+ * Best-effort raw-copy backup of the on-disk file to `<file>.bak.<stamp>`
708
+ * before a refusal, so an externally modified (or oversized) file stays
709
+ * recoverable. Copies bytes instead of reading them so a pathologically
710
+ * large file is never loaded just to back it up. Failure to back up does
711
+ * not change the refusal semantics.
675
712
  */
676
- async backupDrift(target) {
713
+ async backupFile(target) {
677
714
  const path = fileFor(this.root, target);
678
- const raw = await this.io.readText(path);
679
- if (raw === null) return null;
680
715
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
681
716
  try {
682
- await this.io.writeText(`${path}.bak.${stamp}`, raw);
717
+ await this.io.copy(path, `${path}.bak.${stamp}`);
683
718
  return `${path}.bak.${stamp}`;
684
719
  } catch {
685
720
  return null;
686
721
  }
687
722
  }
723
+ /**
724
+ * Read-guard refusal for write paths. Returns the refusal result when the
725
+ * target file is oversized, `null` otherwise. The file is skipped for
726
+ * reading (never loaded), backed up by raw copy, and the model is told to
727
+ * fix it manually — mirroring the drift refusal so corrupted state is never
728
+ * silently overwritten.
729
+ */
730
+ async oversizedRefusal(target) {
731
+ const oversized = await this.oversizedFile(target);
732
+ if (!oversized) return null;
733
+ const backup = await this.backupFile(target);
734
+ const suffix = backup ? ` A backup was saved to ${basename(backup)}.` : "";
735
+ return {
736
+ ok: false,
737
+ message: `Memory file is ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}) — skipping read.${suffix} Fix the file manually, then retry.`,
738
+ entries: [],
739
+ chars: 0,
740
+ limit: this.limitFor(target)
741
+ };
742
+ }
688
743
  async add(target, facts) {
744
+ const refusal = await this.oversizedRefusal(target);
745
+ if (refusal) return refusal;
689
746
  const content = facts.trim();
690
747
  if (!content) return {
691
748
  ok: false,
@@ -741,6 +798,8 @@ var MemoryStore = class {
741
798
  chars: 0,
742
799
  limit: this.limitFor(target)
743
800
  };
801
+ const refusal = await this.oversizedRefusal(target);
802
+ if (refusal) return refusal;
744
803
  const content = action === "replace" ? (facts ?? "").trim() : "";
745
804
  if (action === "replace" && !content) return {
746
805
  ok: false,
@@ -760,7 +819,7 @@ var MemoryStore = class {
760
819
  };
761
820
  }
762
821
  if (await this.detectDrift(target)) {
763
- const backup = await this.backupDrift(target);
822
+ const backup = await this.backupFile(target);
764
823
  return {
765
824
  ok: false,
766
825
  message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
@@ -806,8 +865,10 @@ var MemoryStore = class {
806
865
  chars: 0,
807
866
  limit: this.limitFor(target)
808
867
  };
868
+ const refusal = await this.oversizedRefusal(target);
869
+ if (refusal) return refusal;
809
870
  if (await this.detectDrift(target)) {
810
- const backup = await this.backupDrift(target);
871
+ const backup = await this.backupFile(target);
811
872
  return {
812
873
  ok: false,
813
874
  message: `External drift detected in memory file.${backup ? ` A backup was saved to ${basename(backup)}.` : ""} Resolve the drift before retrying.`,
@@ -898,12 +959,27 @@ var MemoryStore = class {
898
959
  const memory = await this.read("memory");
899
960
  const user = await this.read("user");
900
961
  const parts = [];
901
- for (const [target, entries] of [["Memory", memory], ["User Profile", user]]) {
962
+ for (const [target, label, entries] of [[
963
+ "memory",
964
+ "Memory",
965
+ memory
966
+ ], [
967
+ "user",
968
+ "User Profile",
969
+ user
970
+ ]]) {
971
+ const oversized = entries.length === 0 ? await this.oversizedFile(target) : null;
972
+ if (oversized) {
973
+ parts.push(`## ${label} — file skipped: ${oversized.size} bytes (limit ${oversized.limit * READ_GUARD_FACTOR}); not read`);
974
+ continue;
975
+ }
902
976
  const safe = entries.filter((entry) => !scanMemoryThreats(entry));
903
977
  if (safe.length > 0) {
904
978
  const body = safe.join(ENTRY_DELIMITER);
979
+ const limit = this.limitFor(target);
980
+ const pct = limit > 0 ? Math.min(100, Math.floor(body.length * 100 / limit)) : 0;
905
981
  const note = safe.length === entries.length ? "" : ` (${entries.length - safe.length} threat-matched entries filtered)`;
906
- parts.push(`## ${target} (${safe.length} entries)${note}\n${body}`);
982
+ parts.push(`## ${label} (${safe.length} entries) [${pct}% — ${body.length}/${limit} chars]${note}\n${body}`);
907
983
  }
908
984
  }
909
985
  return parts.join("\n\n");
@@ -928,6 +1004,7 @@ var MemoryStore = class {
928
1004
  * same serialization and returns false, so a normal write is never flagged.
929
1005
  */
930
1006
  async detectDrift(target) {
1007
+ if (await this.oversizedFile(target)) return true;
931
1008
  const raw = await this.io.readText(fileFor(this.root, target));
932
1009
  if (raw === null) return false;
933
1010
  return render(normalizeEntries(raw)) !== raw;
package/lib/types/io.d.ts CHANGED
@@ -13,6 +13,13 @@ export interface EvolutionIoLike {
13
13
  exists(path: string): Promise<boolean>;
14
14
  rename(path: string, destination: string): Promise<void>;
15
15
  copy(path: string, destination: string): Promise<void>;
16
+ /**
17
+ * Optional byte-size probe for the read guard. Return the file's size in
18
+ * bytes, or `null`/`undefined` when unknown (unsupported backend, missing
19
+ * file, stat failure). An implementation without this probe gets no guard:
20
+ * consumers treat an unknown size as "guard not applicable".
21
+ */
22
+ size?(path: string): Promise<number | null>;
16
23
  }
17
24
  /** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
18
25
  export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
@@ -37,6 +37,13 @@ export declare class MemoryStore {
37
37
  private failureCount;
38
38
  constructor(options?: MemoryStoreOptions);
39
39
  limitFor(target: MemoryTarget): number;
40
+ /**
41
+ * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
42
+ * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
43
+ * (backend without a size probe), under the bound, or the target has no
44
+ * limit configured.
45
+ */
46
+ private oversizedFile;
40
47
  read(target: MemoryTarget): Promise<string[]>;
41
48
  write(target: MemoryTarget, entries: string[]): Promise<void>;
42
49
  resetFailures(): void;
@@ -44,11 +51,21 @@ export declare class MemoryStore {
44
51
  /** Percent-based storage hint appended to success message once the target is ≥80% full. */
45
52
  private storageHint;
46
53
  /**
47
- * Best-effort copy of the drifted on-disk file to `<file>.bak.<stamp>` before
48
- * refusing the write, so an external edit stays recoverable. Failure to back
49
- * up does not change the refusal semantics.
54
+ * Best-effort raw-copy backup of the on-disk file to `<file>.bak.<stamp>`
55
+ * before a refusal, so an externally modified (or oversized) file stays
56
+ * recoverable. Copies bytes instead of reading them so a pathologically
57
+ * large file is never loaded just to back it up. Failure to back up does
58
+ * not change the refusal semantics.
59
+ */
60
+ private backupFile;
61
+ /**
62
+ * Read-guard refusal for write paths. Returns the refusal result when the
63
+ * target file is oversized, `null` otherwise. The file is skipped for
64
+ * reading (never loaded), backed up by raw copy, and the model is told to
65
+ * fix it manually — mirroring the drift refusal so corrupted state is never
66
+ * silently overwritten.
50
67
  */
51
- private backupDrift;
68
+ private oversizedRefusal;
52
69
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
53
70
  replace(target: MemoryTarget, oldText: string, facts: string): Promise<MemoryApplyResult>;
54
71
  remove(target: MemoryTarget, oldText: string): Promise<MemoryApplyResult>;
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.21",
4
+ "version": "0.1.0-rc.22",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },