@lmzhen/dsh-memory-files 0.16.1 → 0.17.0

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
@@ -1,34 +1,53 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_USER_CHAR_LIMIT, MemoryStore, PARAM_NAMESPACES, clampedNumber, evolutionIoAdapter, installParamSection, makeSerialQueue } from "@lmzhen/dsh-evolution-core";
4
+ import { DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_USER_CHAR_LIMIT, MemoryStore, clampedNumber, evolutionIoAdapter, makeSerialQueue, paramRowId } from "@lmzhen/dsh-evolution-core";
5
5
  //#region lib/types/index.js
6
6
  /**
7
7
  * Local layered memory provider for `ctx.memory`.
8
8
  * @module @lmzhen/dsh-memory-files
9
9
  */
10
- const MEMORY_SETTINGS_SCHEMA = z.object({
11
- memoryChars: z.number().min(1).default(DEFAULT_MEMORY_CHAR_LIMIT),
12
- userChars: z.number().min(1).default(DEFAULT_USER_CHAR_LIMIT),
13
- addDatePrefix: z.boolean().default(false),
14
- maxConsolidationFailures: z.number().min(1).default(DEFAULT_CONSOLIDATION_FAILURES)
15
- });
16
10
  const name = "memory-files";
17
11
  const inject = ["memory", "evolutionIo"];
18
12
  const Config = z.object({
19
13
  providerName: z.string().default("files"),
14
+ memoryChars: z.number().min(1).volatile(),
15
+ userChars: z.number().min(1).volatile(),
20
16
  memoryCharLimit: z.number().min(1).default(DEFAULT_MEMORY_CHAR_LIMIT),
21
17
  userCharLimit: z.number().min(1).default(DEFAULT_USER_CHAR_LIMIT),
22
- addDatePrefix: z.boolean().default(false),
18
+ addDatePrefix: z.boolean().default(false).volatile(),
23
19
  root: z.string().default(""),
24
- maxConsolidationFailures: z.number().min(1).default(DEFAULT_CONSOLIDATION_FAILURES),
20
+ maxConsolidationFailures: z.number().min(1).default(DEFAULT_CONSOLIDATION_FAILURES).volatile(),
25
21
  threatExemptLabels: z.array(z.string()).default([])
26
22
  });
27
23
  function apply(ctx, rawConfig = {}) {
28
- const clamped = [];
24
+ ctx.inject(["settings"], (injected) => {
25
+ const settings = injected.settings;
26
+ if (typeof settings?.configure !== "function") return;
27
+ const disposer = settings.configure({ auto: false });
28
+ if (typeof disposer === "function") ctx.effect(() => disposer, "memory-files: settings presentation");
29
+ });
30
+ const clampedKeys = /* @__PURE__ */ new Set();
31
+ /**
32
+ * One live numeric row field as the platform resolved it.
33
+ * @param value - the field, or its volatile reference.
34
+ * @returns the current plain value, or undefined when nothing supplied one.
35
+ */
36
+ const liveNumber = (value) => typeof value === "object" ? value.get() : value;
37
+ /**
38
+ * Clamp one supplied numeric value, warning once per key.
39
+ * @param name - the config key, for the warning.
40
+ * @param value - the value as supplied by the deployment or the user.
41
+ * @param fallback - the package default a corrected value falls back to.
42
+ * @returns the value the store may use.
43
+ */
29
44
  const field = (name, value, fallback) => {
30
- const result = clampedNumber(value, fallback, { min: 1 });
31
- if (value !== void 0 && result !== value) clamped.push(name);
45
+ const current = liveNumber(value);
46
+ const result = clampedNumber(current, fallback, { min: 1 });
47
+ if (current !== void 0 && result !== current && !clampedKeys.has(name)) {
48
+ clampedKeys.add(name);
49
+ ctx.logger.warn(`memory-files: ${name} provided an invalid value; falling back to the default`);
50
+ }
32
51
  return result;
33
52
  };
34
53
  const floorLimit = (value) => Math.floor(value);
@@ -39,58 +58,99 @@ function apply(ctx, rawConfig = {}) {
39
58
  user: snapshot?.userChars
40
59
  };
41
60
  };
42
- const budget = policyBudget();
43
- const schemaDefaults = Config["~standard"].validate({}).value;
44
- const explicitLimit = (name) => rawConfig[name] !== void 0 && rawConfig[name] !== schemaDefaults[name];
45
- const config = Object.assign({}, rawConfig, {
46
- memoryCharLimit: floorLimit(field("memoryCharLimit", explicitLimit("memoryCharLimit") ? rawConfig.memoryCharLimit : budget.memory, DEFAULT_MEMORY_CHAR_LIMIT)),
47
- userCharLimit: floorLimit(field("userCharLimit", explicitLimit("userCharLimit") ? rawConfig.userCharLimit : budget.user, DEFAULT_USER_CHAR_LIMIT)),
48
- maxConsolidationFailures: field("maxConsolidationFailures", rawConfig.maxConsolidationFailures, DEFAULT_CONSOLIDATION_FAILURES)
49
- });
50
- if (clamped.length > 0) ctx.logger.warn(`memory-files: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
51
- if (explicitLimit("memoryCharLimit") && budget.memory !== void 0 && config.memoryCharLimit !== budget.memory) ctx.logger.warn(`memory-files: memoryCharLimit=${config.memoryCharLimit} contradicts evolution-policy memoryChars=${budget.memory} — the store enforces the row value while the review plans against the policy value; align them or leave the row unset`);
52
- if (explicitLimit("userCharLimit") && budget.user !== void 0 && config.userCharLimit !== budget.user) ctx.logger.warn(`memory-files: userCharLimit=${config.userCharLimit} contradicts evolution-policy userChars=${budget.user} — same divergence as memoryCharLimit`);
61
+ const aliasDefaults = Config["~standard"].validate({}).value;
62
+ const ALIAS_OF = {
63
+ memoryChars: "memoryCharLimit",
64
+ userChars: "userCharLimit"
65
+ };
66
+ const DEFAULTS = {
67
+ memoryChars: DEFAULT_MEMORY_CHAR_LIMIT,
68
+ userChars: DEFAULT_USER_CHAR_LIMIT
69
+ };
70
+ /**
71
+ * The live value of one canonical budget key (G1 §8.3).
72
+ * @param id - the canonical registry id.
73
+ * @returns the value the platform resolved (user layer included), or undefined.
74
+ */
75
+ const canonicalBudget = (id) => liveNumber(id === "memoryChars" ? rawConfig.memoryChars : rawConfig.userChars);
76
+ /**
77
+ * Whether the OPERATOR set this budget on THIS ROW (G0/S0.4 + G1).
78
+ *
79
+ * The canonical key answers through its live reference — a user-layer value
80
+ * arrives the same way, which is what keeps a user choice ahead of the policy —
81
+ * and the deprecated alias through the default comparison above. The pair is
82
+ * documented in the Config JSDoc and reported by /evolution doctor (budgetIssues).
83
+ * @param id - the canonical registry id.
84
+ * @returns whether the row or the user supplied a value.
85
+ */
86
+ const explicitBudget = (id) => {
87
+ if (canonicalBudget(id) !== void 0) return true;
88
+ const alias = rawConfig[ALIAS_OF[id]];
89
+ return alias !== void 0 && alias !== aliasDefaults[ALIAS_OF[id]];
90
+ };
91
+ /**
92
+ * The budget one key resolves to (G1 §8.4-A): the row's own value — canonical or
93
+ * alias — over the mounted policy, over the package default.
94
+ * @param id - the canonical registry id.
95
+ * @returns the resolved, unclamped budget.
96
+ */
97
+ const budget = (id) => {
98
+ const explicit = canonicalBudget(id) ?? (explicitBudget(id) ? rawConfig[ALIAS_OF[id]] : void 0);
99
+ if (explicit !== void 0) return explicit;
100
+ const policy = policyBudget();
101
+ return (id === "memoryChars" ? policy.memory : policy.user) ?? DEFAULTS[id];
102
+ };
103
+ const memoryChars = () => floorLimit(field("memoryCharLimit", budget("memoryChars"), DEFAULT_MEMORY_CHAR_LIMIT));
104
+ const userChars = () => floorLimit(field("userCharLimit", budget("userChars"), DEFAULT_USER_CHAR_LIMIT));
105
+ const consolidationFailures = () => field("maxConsolidationFailures", rawConfig.maxConsolidationFailures, DEFAULT_CONSOLIDATION_FAILURES);
106
+ const addDatePrefix = () => rawConfig.addDatePrefix?.get() ?? false;
107
+ const policyAtMount = policyBudget();
108
+ if (explicitBudget("memoryChars") && policyAtMount.memory !== void 0 && memoryChars() !== policyAtMount.memory) ctx.logger.warn(`memory-files: the row's memoryChars=${memoryChars()} contradicts evolution-policy memoryChars=${policyAtMount.memory} — the store enforces the row value while the review plans against the policy value; align them or leave the row unset`);
109
+ if (explicitBudget("userChars") && policyAtMount.user !== void 0 && userChars() !== policyAtMount.user) ctx.logger.warn(`memory-files: the row's userChars=${userChars()} contradicts evolution-policy userChars=${policyAtMount.user} — same divergence as memoryChars`);
53
110
  const io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
54
111
  const serializedWrite = makeSerialQueue();
55
- const trimmedRoot = (config.root || "").trim();
112
+ const trimmedRoot = (rawConfig.root || "").trim();
56
113
  const resolvedRoot = trimmedRoot === "" ? "" : resolve(trimmedRoot);
57
- const section = {};
58
- const buildStore = () => new MemoryStore({
59
- memoryCharLimit: section.overrides?.get("memoryChars") ?? config.memoryCharLimit,
60
- userCharLimit: section.overrides?.get("userChars") ?? config.userCharLimit,
61
- addDatePrefix: section.overrides?.get("addDatePrefix") ?? config.addDatePrefix,
62
- maxConsolidationFailures: section.overrides?.get("maxConsolidationFailures") ?? config.maxConsolidationFailures,
63
- threatExemptLabels: config.threatExemptLabels,
64
- ...resolvedRoot !== "" ? { root: resolvedRoot } : {},
65
- io
66
- });
67
- let store = buildStore();
68
- section.overrides = installParamSection(ctx, PARAM_NAMESPACES["memory-files"] ?? "evolution-memory", MEMORY_SETTINGS_SCHEMA, {
69
- memoryChars: config.memoryCharLimit,
70
- userChars: config.userCharLimit,
71
- addDatePrefix: config.addDatePrefix,
72
- maxConsolidationFailures: config.maxConsolidationFailures
73
- }, {
74
- warn: (message) => {
75
- ctx.logger.warn("memory-files: " + message);
76
- },
77
- onChange: () => {
78
- store = buildStore();
79
- }
80
- });
114
+ let store;
115
+ let storeKey = "";
116
+ /**
117
+ * The store for the configuration as it stands right now.
118
+ * @returns the current store, rebuilt when a resolved limit changed.
119
+ */
120
+ const currentStore = () => {
121
+ const options = {
122
+ memoryCharLimit: memoryChars(),
123
+ userCharLimit: userChars(),
124
+ addDatePrefix: addDatePrefix(),
125
+ maxConsolidationFailures: consolidationFailures()
126
+ };
127
+ const key = `${options.memoryCharLimit}|${options.userCharLimit}|${options.addDatePrefix}|${options.maxConsolidationFailures}`;
128
+ if (store !== void 0 && key === storeKey) return store;
129
+ store = new MemoryStore({
130
+ ...options,
131
+ ...rawConfig.threatExemptLabels !== void 0 ? { threatExemptLabels: rawConfig.threatExemptLabels } : {},
132
+ ...resolvedRoot !== "" ? { root: resolvedRoot } : {},
133
+ io
134
+ });
135
+ storeKey = key;
136
+ return store;
137
+ };
81
138
  const provider = {
82
- name: config.providerName,
83
- read: (target) => store.read(target),
139
+ name: rawConfig.providerName ?? "files",
140
+ read: (target) => currentStore().read(target),
84
141
  applyBatch: async (target, operations) => {
85
142
  const normalized = operations.map((op) => ({
86
143
  action: op.action,
87
144
  facts: op.facts ?? op.content,
88
145
  old_text: op.old_text
89
146
  }));
90
- return await serializedWrite(() => store.applyBatch(target, normalized));
147
+ return await serializedWrite(() => currentStore().applyBatch(target, normalized));
91
148
  },
92
149
  snapshot: async () => {
93
- const [memory, user] = await serializedWrite(async () => [await store.read("memory"), await store.read("user")]);
150
+ const [memory, user] = await serializedWrite(async () => {
151
+ const current = currentStore();
152
+ return [await current.read("memory"), await current.read("user")];
153
+ });
94
154
  const text = JSON.stringify([memory, user]);
95
155
  return {
96
156
  version: 1,
@@ -99,30 +159,51 @@ function apply(ctx, rawConfig = {}) {
99
159
  user
100
160
  };
101
161
  },
102
- renderContext: () => serializedWrite(() => store.renderContext())
162
+ renderContext: () => serializedWrite(() => currentStore().renderContext())
163
+ };
164
+ /**
165
+ * The keys the user set in this row's namespace (G1 §8.4-A). This is the ONLY
166
+ * remaining read of the settings user layer, and it reads KEY NAMES, never
167
+ * values: the platform resolves the user layer into the row's live fields, so the
168
+ * value comes from `budget` — but naming the winning surface needs to know
169
+ * whether the user set the key at all.
170
+ * @returns the keys present in this row's user layer.
171
+ */
172
+ const userSetKeys = () => {
173
+ const entry = ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === paramRowId("memory-files"));
174
+ return new Set(Object.keys(entry?.user ?? {}));
103
175
  };
104
- const limitSource = (name, explicit, policyValue) => {
105
- if (section.overrides?.get(name) !== void 0) return "user";
106
- if (explicit) return "config";
107
- return policyValue === void 0 ? "default" : "policy";
176
+ /**
177
+ * Which surface supplied one budget.
178
+ * @param id - the canonical registry id.
179
+ * @returns 'user', 'config', 'policy' or 'default'.
180
+ */
181
+ const limitSource = (id) => {
182
+ if (userSetKeys().has(id)) return "user";
183
+ if (explicitBudget(id)) return "config";
184
+ return (id === "memoryChars" ? policyBudget().memory : policyBudget().user) === void 0 ? "default" : "policy";
108
185
  };
109
186
  ctx.provide("evolutionMemoryBudget", {
110
187
  get memoryCharLimit() {
111
- return section.overrides?.get("memoryChars") ?? config.memoryCharLimit;
188
+ return memoryChars();
112
189
  },
113
190
  get userCharLimit() {
114
- return section.overrides?.get("userChars") ?? config.userCharLimit;
191
+ return userChars();
115
192
  },
116
193
  get memorySource() {
117
- return limitSource("memoryChars", explicitLimit("memoryCharLimit"), budget.memory);
194
+ return limitSource("memoryChars");
118
195
  },
119
196
  get userSource() {
120
- return limitSource("userChars", explicitLimit("userCharLimit"), budget.user);
197
+ return limitSource("userChars");
121
198
  },
122
- policyMemoryChars: budget.memory,
123
- policyUserChars: budget.user
199
+ get policyMemoryChars() {
200
+ return policyBudget().memory;
201
+ },
202
+ get policyUserChars() {
203
+ return policyBudget().user;
204
+ }
124
205
  });
125
206
  ctx.effect(() => ctx.memory.registerProvider(provider), "memory-files.provider");
126
207
  }
127
208
  //#endregion
128
- export { Config, MEMORY_SETTINGS_SCHEMA, apply, inject, name };
209
+ export { Config, apply, inject, name };
@@ -2,46 +2,59 @@
2
2
  * Local layered memory provider for `ctx.memory`.
3
3
  * @module @lmzhen/dsh-memory-files
4
4
  */
5
- import type { Context } from '@deepseek-ai/cordis';
5
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
6
6
  import z from '@deepseek-ai/schemastery';
7
- /** G3/S3.2: the memory section's schema. Field names are the CANONICAL ids from
8
- * the parameter registry (`memoryChars`/`userChars` are the policy-side names the
9
- * review planner reads too), so the settings document, the params output, the
10
- * doctor report and the cards spell one name. */
11
- export interface MemorySettings {
12
- /** Character budget the store enforces for MEMORY.md. */
13
- memoryChars: number;
14
- /** Character budget the store enforces for USER.md. */
15
- userChars: number;
16
- /** Prefix stored entries with their date heading. */
17
- addDatePrefix: boolean;
18
- /** Consolidation failures one turn tolerates before the tool stops retrying. */
19
- maxConsolidationFailures: number;
20
- }
21
- export declare const MEMORY_SETTINGS_SCHEMA: z<MemorySettings>;
22
7
  export declare const name = "memory-files";
23
8
  export declare const inject: string[];
24
9
  export interface Config {
25
10
  providerName?: string;
26
- /** Store-enforced character budget for MEMORY.md. The policy row's
27
- * `memoryChars` is the same semantic on the review-planner side and is the
28
- * canonical id (G0/S0.2); the two are NOT auto-synchronised —
29
- * `/evolution doctor` reports the divergence (budgetIssues). Deprecated alias
30
- * (G0/S0.3): still readable, refused by writes; removed in 0.7.0. */
11
+ /** Store-enforced character budget for MEMORY.md — the CANONICAL registry id
12
+ * (G1) and a LIVE row field (registry tier E3). The policy row's `memoryChars`
13
+ * is the same semantic on the review-planner side; the two are NOT
14
+ * auto-synchronised — `/evolution doctor` reports the divergence (budgetIssues).
15
+ * A row may spell either this name or the deprecated alias below. No schema
16
+ * default: the alias carries it, and a default on the canonical key would fill
17
+ * it and leave the alias unreachable (the store resolves
18
+ * DEFAULT_MEMORY_CHAR_LIMIT after the alias fallback). */
19
+ memoryChars?: Volatile<number | undefined>;
20
+ /** Store-enforced character budget for USER.md — the CANONICAL registry id (G1),
21
+ * a live row field on the same terms as `memoryChars`. */
22
+ userChars?: Volatile<number | undefined>;
23
+ /** Deprecated alias of `memoryChars` (G0/S0.3): still readable, refused by
24
+ * writes; removed in 0.7.0. */
31
25
  memoryCharLimit?: number;
32
- /** Store-enforced character budget for USER.md; the canonical id is the policy
33
- * row's `userChars`. Divergence is reported by `/evolution doctor`. Deprecated
34
- * alias (G0/S0.3): still readable, refused by writes; removed in 0.7.0. */
26
+ /** Deprecated alias of `userChars` (G0/S0.3): still readable, refused by
27
+ * writes; removed in 0.7.0. */
35
28
  userCharLimit?: number;
36
- addDatePrefix?: boolean;
29
+ addDatePrefix?: Volatile<boolean>;
37
30
  root?: string;
38
31
  /** How many consolidation failures one turn tolerates before the tool tells the model to stop retrying. */
39
- maxConsolidationFailures?: number;
32
+ maxConsolidationFailures?: Volatile<number>;
40
33
  /** F-2 (v18): deployment-declared benign threat labels for the MEMORY store.
41
34
  * The skill/guard channels already read this option; without it the
42
35
  * README's "MemoryStore store option" exemption was unreachable. */
43
36
  threatExemptLabels?: string[];
44
37
  }
45
- export declare const Config: z<Config>;
38
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
39
+ providerName: z<string, string, "defined">;
40
+ memoryChars: z<number, number, "volatile">;
41
+ userChars: z<number, number, "volatile">;
42
+ memoryCharLimit: z<number, number, "defined">;
43
+ userCharLimit: z<number, number, "defined">;
44
+ addDatePrefix: z<boolean, boolean, "volatile-defined">;
45
+ root: z<string, string, "defined">;
46
+ maxConsolidationFailures: z<number, number, "volatile-defined">;
47
+ threatExemptLabels: z<string[], string[], "defined">;
48
+ }>>, Schemastery.ObjectT<NoInfer<{
49
+ providerName: z<string, string, "defined">;
50
+ memoryChars: z<number, number, "volatile">;
51
+ userChars: z<number, number, "volatile">;
52
+ memoryCharLimit: z<number, number, "defined">;
53
+ userCharLimit: z<number, number, "defined">;
54
+ addDatePrefix: z<boolean, boolean, "volatile-defined">;
55
+ root: z<string, string, "defined">;
56
+ maxConsolidationFailures: z<number, number, "volatile-defined">;
57
+ threatExemptLabels: z<string[], string[], "defined">;
58
+ }>>, "plain">;
46
59
  export declare function apply(ctx: Context, rawConfig?: Config): void;
47
60
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-memory-files",
3
3
  "description": "Local layered memory provider (community build)",
4
- "version": "0.16.1",
4
+ "version": "0.17.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,16 +27,16 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.16.1"
30
+ "@lmzhen/dsh-evolution-core": "^0.17.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
- "@lmzhen/dsh-evolution-io": "^0.16.1",
35
- "@lmzhen/dsh-memory": "^0.16.1"
34
+ "@lmzhen/dsh-evolution-io": "^0.17.0",
35
+ "@lmzhen/dsh-memory": "^0.17.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@lmzhen/dsh-evolution-core": "^0.16.1",
39
- "@lmzhen/dsh-evolution-io": "^0.16.1",
40
- "@lmzhen/dsh-memory": "^0.16.1"
38
+ "@lmzhen/dsh-evolution-core": "^0.17.0",
39
+ "@lmzhen/dsh-evolution-io": "^0.17.0",
40
+ "@lmzhen/dsh-memory": "^0.17.0"
41
41
  }
42
42
  }