@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 +89 -12
- package/lib/types/io.d.ts +7 -0
- package/lib/types/memory-store.d.ts +21 -4
- package/package.json +1 -1
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
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 [[
|
|
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(`## ${
|
|
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
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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.
|
|
4
|
+
"version": "0.1.0-rc.22",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|