@lmzhen/dsh-memory 0.43.2-rc.1 → 0.45.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/files/index.js +187 -0
- package/lib/tools/index.js +341 -0
- package/lib/types/files/index.d.ts +60 -0
- package/lib/types/files/index.js +261 -0
- package/lib/types/index.js +117 -0
- package/lib/types/tools/index.d.ts +42 -0
- package/lib/types/tools/index.js +438 -0
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/files/de.json +6 -0
- package/locale/files/en.json +6 -0
- package/locale/files/es.json +6 -0
- package/locale/files/fr.json +6 -0
- package/locale/files/ja.json +6 -0
- package/locale/files/pt.json +6 -0
- package/locale/files/ru.json +6 -0
- package/locale/files/zh.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/pt.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/tools/de.json +6 -0
- package/locale/tools/en.json +6 -0
- package/locale/tools/es.json +6 -0
- package/locale/tools/fr.json +6 -0
- package/locale/tools/ja.json +6 -0
- package/locale/tools/pt.json +6 -0
- package/locale/tools/ru.json +6 -0
- package/locale/tools/zh.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +20 -4
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local layered memory provider for `ctx.memory`.
|
|
3
|
+
* @module @lmzhen/dsh-memory-files
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import { DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_USER_CHAR_LIMIT, paramRowId, payloadText, userSetKeys, evolutionIoAdapter, makeSerialQueue, MemoryStore, clampOnce, liveBool, unwrapLive } from '@lmzhen/dsh-evolution-core';
|
|
9
|
+
export const name = 'memory-files';
|
|
10
|
+
export const inject = ['memory', 'evolutionIo'];
|
|
11
|
+
// G1: the four E3 keys of this row carry `.volatile()`, so the platform hands the
|
|
12
|
+
// plugin a stable reference it updates in place and the settings surface can edit
|
|
13
|
+
// them live. The schema keeps NO annotation: a volatile field's output is a
|
|
14
|
+
// reference, which the annotated `z<Config>` reading cannot express (TS2375) — the
|
|
15
|
+
// platform's own volatile rows are unannotated for the same reason.
|
|
16
|
+
export const Config = z.object({
|
|
17
|
+
providerName: z.string().default('files'),
|
|
18
|
+
// The canonical registry ids. No .default() on either one: a schema default would
|
|
19
|
+
// fill the canonical key and make the deprecated alias below unreachable through
|
|
20
|
+
// the store's alias fallback (DEFAULT_MEMORY_CHAR_LIMIT /
|
|
21
|
+
// DEFAULT_USER_CHAR_LIMIT are resolved there).
|
|
22
|
+
memoryChars: z.number().min(1).volatile(),
|
|
23
|
+
userChars: z.number().min(1).volatile(),
|
|
24
|
+
memoryCharLimit: z.number().min(1).default(DEFAULT_MEMORY_CHAR_LIMIT),
|
|
25
|
+
userCharLimit: z.number().min(1).default(DEFAULT_USER_CHAR_LIMIT),
|
|
26
|
+
addDatePrefix: z.boolean().default(false).volatile(),
|
|
27
|
+
root: z.string().default(''),
|
|
28
|
+
maxConsolidationFailures: z.number().min(1).default(DEFAULT_CONSOLIDATION_FAILURES).volatile(),
|
|
29
|
+
threatExemptLabels: z.array(z.string()).default([]),
|
|
30
|
+
});
|
|
31
|
+
export function apply(ctx, rawConfig = {}) {
|
|
32
|
+
// G1: suppress the platform's auto-generated settings page for this row (the
|
|
33
|
+
// family renders its own card from the registry). Probed, not assumed: a host
|
|
34
|
+
// without the capability still loads.
|
|
35
|
+
ctx.inject(['settings'], (injected) => {
|
|
36
|
+
const settings = injected.settings;
|
|
37
|
+
if (typeof settings?.configure !== 'function')
|
|
38
|
+
return;
|
|
39
|
+
// v46 S1.12b (finding 6-1): the owner argument is what the platform looks the presentation up by.
|
|
40
|
+
// Omitting it registers the suppression on the settings service's own fiber, so a row that
|
|
41
|
+
// re-mounts keeps a dead key and the auto form can return (platform precedent:
|
|
42
|
+
// settings.configure({ auto: false }, plugin.fiber) — settings/src/index.ts:266).
|
|
43
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
44
|
+
if (typeof disposer === 'function')
|
|
45
|
+
ctx.effect(() => disposer, 'memory-files: settings presentation');
|
|
46
|
+
});
|
|
47
|
+
// G3.1 (0.3.23): clamp the numeric memory limit so a 0/negative/NaN/±Infinity
|
|
48
|
+
// value falls back to the package default. A 0 limit is never an "unbounded"
|
|
49
|
+
// meaning (MemoryStore keeps its own internal defense); the schema `.min(1)`
|
|
50
|
+
// rejects 0/negative at load, this clamp also covers NaN/±Infinity (which
|
|
51
|
+
// schemastery lets a bare number schema through) and direct construction.
|
|
52
|
+
// G1 §8.3: every read happens at USE time, so the correction warns once per KEY
|
|
53
|
+
// per mount instead of once at assembly.
|
|
54
|
+
const clampedKeys = new Set();
|
|
55
|
+
/**
|
|
56
|
+
* Clamp one supplied numeric value, warning once per key.
|
|
57
|
+
* @param name - the config key, for the warning.
|
|
58
|
+
* @param value - the value as supplied by the deployment or the user.
|
|
59
|
+
* @param fallback - the package default a corrected value falls back to.
|
|
60
|
+
* @returns the value the store may use.
|
|
61
|
+
*/
|
|
62
|
+
const field = (name, value, fallback) => clampOnce(clampedKeys, (message) => { ctx.logger.warn(message); }, `memory-files: ${name}`, value, fallback, 1);
|
|
63
|
+
// P2-23 (v37): a fractional char limit reached the store unchanged, and the
|
|
64
|
+
// tool surface declares `limit` as an integer — every memory call then failed
|
|
65
|
+
// platform output validation AFTER the write had landed. The char limit is a
|
|
66
|
+
// counted quantity, so it is floored at the one point where it enters the store
|
|
67
|
+
// (floor, not round: a limit must never grow past what the operator configured).
|
|
68
|
+
const floorLimit = (value) => Math.floor(value);
|
|
69
|
+
// S2-12③ (FLOW5-4): the memory budget has TWO configuration surfaces — this
|
|
70
|
+
// package's `memoryCharLimit`/`userCharLimit` (what the STORE enforces) and
|
|
71
|
+
// `evolution-policy`'s `memoryChars`/`userChars` (what the review pipeline
|
|
72
|
+
// PLANS against). They used to default independently, so moving one left the
|
|
73
|
+
// other behind: the reviewer planned ops for a budget the store then refused.
|
|
74
|
+
// Explicit config still wins (the operator said so); an UNSET limit follows the
|
|
75
|
+
// policy when that service is already mounted, and otherwise keeps the default
|
|
76
|
+
// (row order is not something this plugin can force — doctor compares the two
|
|
77
|
+
// surfaces again at run time and flags a disagreement, including the
|
|
78
|
+
// order-induced one).
|
|
79
|
+
const policyBudget = () => {
|
|
80
|
+
const policy = ctx.get('evolutionPolicy');
|
|
81
|
+
const snapshot = policy?.get?.();
|
|
82
|
+
return { memory: snapshot?.memoryChars, user: snapshot?.userChars };
|
|
83
|
+
};
|
|
84
|
+
// The row fills SCHEMA DEFAULTS into the deprecated alias, so "the operator set
|
|
85
|
+
// the alias" is read as "differs from that default" — the same test the review
|
|
86
|
+
// row uses for shadowed fields. A row that pins the alias default is therefore
|
|
87
|
+
// indistinguishable from one that omits it, and follows the policy like the
|
|
88
|
+
// latter.
|
|
89
|
+
const aliasDefaults = Config['~standard'].validate({}).value;
|
|
90
|
+
const ALIAS_OF = { memoryChars: 'memoryCharLimit', userChars: 'userCharLimit' };
|
|
91
|
+
const DEFAULTS = { memoryChars: DEFAULT_MEMORY_CHAR_LIMIT, userChars: DEFAULT_USER_CHAR_LIMIT };
|
|
92
|
+
/**
|
|
93
|
+
* The live value of one canonical budget key (G1 §8.3).
|
|
94
|
+
* @param id - the canonical registry id.
|
|
95
|
+
* @returns the value the platform resolved (user layer included), or undefined.
|
|
96
|
+
*/
|
|
97
|
+
const canonicalBudget = (id) => unwrapLive(id === 'memoryChars' ? rawConfig.memoryChars : rawConfig.userChars);
|
|
98
|
+
/**
|
|
99
|
+
* Whether the OPERATOR set this budget on THIS ROW (G0/S0.4 + G1).
|
|
100
|
+
*
|
|
101
|
+
* The canonical key answers through its live reference — a user-layer value
|
|
102
|
+
* arrives the same way, which is what keeps a user choice ahead of the policy —
|
|
103
|
+
* and the deprecated alias through the default comparison above. The pair is
|
|
104
|
+
* documented in the Config JSDoc and reported by /evolution doctor (budgetIssues).
|
|
105
|
+
* @param id - the canonical registry id.
|
|
106
|
+
* @returns whether the row or the user supplied a value.
|
|
107
|
+
*/
|
|
108
|
+
const explicitBudget = (id) => {
|
|
109
|
+
if (canonicalBudget(id) !== undefined)
|
|
110
|
+
return true;
|
|
111
|
+
const alias = rawConfig[ALIAS_OF[id]];
|
|
112
|
+
return alias !== undefined && alias !== aliasDefaults[ALIAS_OF[id]];
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The budget one key resolves to (G1 §8.4-A): the row's own value — canonical or
|
|
116
|
+
* alias — over the mounted policy, over the package default.
|
|
117
|
+
* @param id - the canonical registry id.
|
|
118
|
+
* @returns the resolved, unclamped budget.
|
|
119
|
+
*/
|
|
120
|
+
const budget = (id) => {
|
|
121
|
+
const explicit = canonicalBudget(id) ?? (explicitBudget(id) ? rawConfig[ALIAS_OF[id]] : undefined);
|
|
122
|
+
if (explicit !== undefined)
|
|
123
|
+
return explicit;
|
|
124
|
+
const policy = policyBudget();
|
|
125
|
+
return (id === 'memoryChars' ? policy.memory : policy.user) ?? DEFAULTS[id];
|
|
126
|
+
};
|
|
127
|
+
// BOTH spellings carry the RESOLVED value downstream, so the store, the budget
|
|
128
|
+
// view and the doctor read one number whichever name the deployment wrote.
|
|
129
|
+
const memoryChars = () => floorLimit(field('memoryCharLimit', budget('memoryChars'), DEFAULT_MEMORY_CHAR_LIMIT));
|
|
130
|
+
const userChars = () => floorLimit(field('userCharLimit', budget('userChars'), DEFAULT_USER_CHAR_LIMIT));
|
|
131
|
+
const consolidationFailures = () => field('maxConsolidationFailures', rawConfig.maxConsolidationFailures, DEFAULT_CONSOLIDATION_FAILURES);
|
|
132
|
+
const addDatePrefix = () => liveBool(rawConfig.addDatePrefix);
|
|
133
|
+
// The one disagreement this plugin can see at load: an EXPLICIT row value that
|
|
134
|
+
// contradicts the mounted policy. Doctor re-checks the same pair at run time (it
|
|
135
|
+
// can see the policy mount that happened after this row).
|
|
136
|
+
const policyAtMount = policyBudget();
|
|
137
|
+
if (explicitBudget('memoryChars') && policyAtMount.memory !== undefined && memoryChars() !== policyAtMount.memory) {
|
|
138
|
+
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`);
|
|
139
|
+
}
|
|
140
|
+
if (explicitBudget('userChars') && policyAtMount.user !== undefined && userChars() !== policyAtMount.user) {
|
|
141
|
+
ctx.logger.warn(`memory-files: the row's userChars=${userChars()} contradicts evolution-policy userChars=${policyAtMount.user} — same divergence as memoryChars`);
|
|
142
|
+
}
|
|
143
|
+
// The IO provider is resolved lazily so `memory-files` does not depend on
|
|
144
|
+
// row order: the first write happens only after the preset has fully mounted.
|
|
145
|
+
const io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
146
|
+
const authority = ctx.get('evolutionMutationAuthority');
|
|
147
|
+
// In-process write serialization: applyBatch is read-modify-write, so two
|
|
148
|
+
// concurrent callers (multi-session host) can otherwise compute on the same
|
|
149
|
+
// old entries and the last rename wins, silently dropping the other's ops.
|
|
150
|
+
// 0.3.17 (S2.8, T-1): the queue factory is shared with state-json now.
|
|
151
|
+
const serializedWrite = makeSerialQueue();
|
|
152
|
+
// V5-11 (0.3.32): a whitespace-only `root` was truthy and resolved to a
|
|
153
|
+
// CWD-relative path — trim like resolveSkillsRoot/state-json (V4-09 third
|
|
154
|
+
// occurrence); empty/whitespace both fall through to the default root.
|
|
155
|
+
// PLAN S2.2-adjacent P2-31 (2026-09-16): an EXPLICIT non-empty root is now
|
|
156
|
+
// run through resolve() — the same standard as core's evolutionRoot
|
|
157
|
+
// (state-store.ts) — so the store root is pinned to an absolute path at
|
|
158
|
+
// mount instead of staying CWD-relative and letting every io call land
|
|
159
|
+
// wherever the process CWD points at that moment. Empty/whitespace keeps
|
|
160
|
+
// the memoryRoot() default unchanged.
|
|
161
|
+
const trimmedRoot = (rawConfig.root || '').trim();
|
|
162
|
+
const resolvedRoot = trimmedRoot === '' ? '' : resolve(trimmedRoot);
|
|
163
|
+
// G3/S3.2 + G1: the user layer and the deployment carriers both land in the four
|
|
164
|
+
// live values above, so the store is DERIVED from them rather than kept in step
|
|
165
|
+
// by a settings hook. `MemoryStore` reads its limits in its CONSTRUCTOR, so an
|
|
166
|
+
// option set that changed since the last operation rebuilds it — registration-
|
|
167
|
+
// level facts, the one thing the platform's `onChange` hook existed for. A
|
|
168
|
+
// rebuild resets the store's consolidation-failure counter; the previous hook
|
|
169
|
+
// rebuilt on ANY committed change, so this fires strictly less often. One
|
|
170
|
+
// operation takes one store (the read/write pair inside `snapshot` must not
|
|
171
|
+
// straddle a rebuild), and the write queue lives outside the store either way.
|
|
172
|
+
let store;
|
|
173
|
+
let storeKey = '';
|
|
174
|
+
/**
|
|
175
|
+
* The store for the configuration as it stands right now.
|
|
176
|
+
* @returns the current store, rebuilt when a resolved limit changed.
|
|
177
|
+
*/
|
|
178
|
+
const currentStore = () => {
|
|
179
|
+
const options = {
|
|
180
|
+
memoryCharLimit: memoryChars(),
|
|
181
|
+
userCharLimit: userChars(),
|
|
182
|
+
addDatePrefix: addDatePrefix(),
|
|
183
|
+
maxConsolidationFailures: consolidationFailures(),
|
|
184
|
+
};
|
|
185
|
+
const key = `${options.memoryCharLimit}|${options.userCharLimit}|${options.addDatePrefix}|${options.maxConsolidationFailures}`;
|
|
186
|
+
if (store !== undefined && key === storeKey)
|
|
187
|
+
return store;
|
|
188
|
+
store = new MemoryStore({
|
|
189
|
+
...options,
|
|
190
|
+
// F-2 (v18): the memory store's own exemption list, now configurable.
|
|
191
|
+
...rawConfig.threatExemptLabels !== undefined ? { threatExemptLabels: rawConfig.threatExemptLabels } : {},
|
|
192
|
+
...resolvedRoot !== '' ? { root: resolvedRoot } : {},
|
|
193
|
+
io,
|
|
194
|
+
...(authority?.homeFence === undefined ? {} : { fence: authority.homeFence }),
|
|
195
|
+
});
|
|
196
|
+
storeKey = key;
|
|
197
|
+
return store;
|
|
198
|
+
};
|
|
199
|
+
const provider = {
|
|
200
|
+
name: rawConfig.providerName ?? 'files',
|
|
201
|
+
read: (target) => currentStore().read(target),
|
|
202
|
+
applyBatch: async (target, operations) => {
|
|
203
|
+
// T3-06/A34: the payload alias is resolved by core's ONE rule, so this
|
|
204
|
+
// provider cannot disagree with the validator or the store about a blank
|
|
205
|
+
// `facts` sitting next to a real `content`.
|
|
206
|
+
const normalized = operations.map(op => ({ action: op.action, facts: payloadText(op), old_text: op.old_text }));
|
|
207
|
+
return await serializedWrite(() => currentStore().applyBatch(target, normalized));
|
|
208
|
+
},
|
|
209
|
+
snapshot: async () => {
|
|
210
|
+
// 0.3.17 (E-73): serial reads — a concurrent write between the two
|
|
211
|
+
// Promise.all reads used to produce a mixed-generation snapshot. V4-12:
|
|
212
|
+
// reading memory and user as two separate awaited reads still lets a
|
|
213
|
+
// serializedWrite (an applyBatch) yield BETWEEN them, so the two reads
|
|
214
|
+
// now run inside one serializedWrite task: no write can interleave, the
|
|
215
|
+
// snapshot is a single generation, and the sha256 pins a state that truly
|
|
216
|
+
// existed. (The cross-process window is documented as accepted — the
|
|
217
|
+
// single-process chain is this family's second layer.)
|
|
218
|
+
const [memory, user] = await serializedWrite(async () => {
|
|
219
|
+
const current = currentStore();
|
|
220
|
+
return [await current.read('memory'), await current.read('user')];
|
|
221
|
+
});
|
|
222
|
+
const text = JSON.stringify([memory, user]);
|
|
223
|
+
return { version: 1, sha256: createHash('sha256').update(text).digest('hex'), memory, user };
|
|
224
|
+
},
|
|
225
|
+
// P2-3 (v14): the SAME single-generation rule as `snapshot` — this is the
|
|
226
|
+
// path the model actually reads (tool-memory injects `renderContext`), and
|
|
227
|
+
// it read memory and user as two independent awaits, so a concurrent
|
|
228
|
+
// applyBatch could land between them and produce a mixed-generation
|
|
229
|
+
// context. Queue both reads as one serialized step.
|
|
230
|
+
renderContext: () => serializedWrite(() => currentStore().renderContext()),
|
|
231
|
+
};
|
|
232
|
+
// S2.9 (T3-10): the settings user-layer KEY read is ONE core function (`userSetKeys`); this row
|
|
233
|
+
// only names its own namespace. The review, curator and skill rows call the same reader.
|
|
234
|
+
/**
|
|
235
|
+
* Which surface supplied one budget.
|
|
236
|
+
* @param id - the canonical registry id.
|
|
237
|
+
* @returns 'user', 'config', 'policy' or 'default'.
|
|
238
|
+
*/
|
|
239
|
+
const limitSource = (id) => {
|
|
240
|
+
if (userSetKeys(ctx, paramRowId('memory-files')).has(id))
|
|
241
|
+
return 'user';
|
|
242
|
+
if (explicitBudget(id))
|
|
243
|
+
return 'config';
|
|
244
|
+
return (id === 'memoryChars' ? policyBudget().memory : policyBudget().user) === undefined ? 'default' : 'policy';
|
|
245
|
+
};
|
|
246
|
+
// S2-12③: publish the EFFECTIVE budget so doctor can compare the two surfaces
|
|
247
|
+
// at run time (the same one-writer/one-reader contract as evolutionFeedback /
|
|
248
|
+
// evolutionReplay). Every member is a LIVE VIEW (G1 §8.3): the user layer can
|
|
249
|
+
// change at run time, and a policy mounted after this row is visible too.
|
|
250
|
+
// `source` names the winning surface; 'user' is the highest-priority one.
|
|
251
|
+
ctx.provide('evolutionMemoryBudget', {
|
|
252
|
+
get memoryCharLimit() { return memoryChars(); },
|
|
253
|
+
get userCharLimit() { return userChars(); },
|
|
254
|
+
get memorySource() { return limitSource('memoryChars'); },
|
|
255
|
+
get userSource() { return limitSource('userChars'); },
|
|
256
|
+
get policyMemoryChars() { return policyBudget().memory; },
|
|
257
|
+
get policyUserChars() { return policyBudget().user; },
|
|
258
|
+
});
|
|
259
|
+
ctx.effect(() => ctx.memory.registerProvider(provider), 'memory-files.provider');
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory provider registry for the evolution family.
|
|
3
|
+
* Service Definition role; concrete providers register here.
|
|
4
|
+
* @module @lmzhen/dsh-memory
|
|
5
|
+
*/
|
|
6
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
export class MemoryRegistry extends Service {
|
|
9
|
+
/**
|
|
10
|
+
* V27 G6.3: the same provider-pin contract the io/state seams use. Before
|
|
11
|
+
* this, every delegation called `provider()` with no name, so with two
|
|
12
|
+
* providers mounted the session-visible store was decided by ROW ORDER while
|
|
13
|
+
* `memory-files.providerName` renamed a registration nothing selected — a
|
|
14
|
+
* config no consumer read. Empty keeps the old behavior (first registered).
|
|
15
|
+
*/
|
|
16
|
+
static Config = z.object({
|
|
17
|
+
provider: z.string().default(''),
|
|
18
|
+
});
|
|
19
|
+
providers = new Map();
|
|
20
|
+
/** Dispose handles by provider name, so a stale handle cannot remove a newer registration. */
|
|
21
|
+
disposals = new Map();
|
|
22
|
+
providerName;
|
|
23
|
+
pinWarned = false;
|
|
24
|
+
constructor(ctx, config = {}) {
|
|
25
|
+
super(ctx, 'memory');
|
|
26
|
+
this.providerName = config.provider ?? '';
|
|
27
|
+
// A mount-time check like evolution-state's S-07 is not available here: the
|
|
28
|
+
// registry and the provider-registration API are the SAME service, so a
|
|
29
|
+
// provider can only register after this constructor and the registry is
|
|
30
|
+
// always empty at mount. The pin is therefore verified at the two points
|
|
31
|
+
// that exist — a warning when a differently-named provider registers (the
|
|
32
|
+
// earliest visible signal), and the hard `provider(name)` throw on the first
|
|
33
|
+
// read or write, which names the pin.
|
|
34
|
+
}
|
|
35
|
+
registerProvider(provider) {
|
|
36
|
+
// 1-8/A11: parity with the io and state-storage registries. The three registries serve ONE role
|
|
37
|
+
// (a provider table on the Service Definition side), and both siblings treat re-registering the
|
|
38
|
+
// IDENTICAL object as idempotent — they return the original dispose and do not throw, which is
|
|
39
|
+
// what lets a backend re-apply on HMR (a re-mounted row) without a crash. Memory threw
|
|
40
|
+
// `already registered` for the same sequence. A DIFFERENT object under a registered name still
|
|
41
|
+
// throws in all three.
|
|
42
|
+
const idempotent = this.providers.get(provider.name) === provider;
|
|
43
|
+
if (!idempotent && this.providers.has(provider.name))
|
|
44
|
+
throw new Error(`memory provider "${provider.name}" already registered`);
|
|
45
|
+
if (idempotent) {
|
|
46
|
+
const existing = this.disposals.get(provider.name);
|
|
47
|
+
if (existing !== undefined)
|
|
48
|
+
return existing;
|
|
49
|
+
}
|
|
50
|
+
this.providers.set(provider.name, provider);
|
|
51
|
+
this.ctx.emit('evolution/memory-provider-registered', { name: provider.name });
|
|
52
|
+
if (this.providerName && provider.name !== this.providerName && !this.providers.has(this.providerName) && !this.pinWarned) {
|
|
53
|
+
this.pinWarned = true;
|
|
54
|
+
this.ctx.logger.warn(`memory: config.provider="${this.providerName}" is not registered (mounted: "${provider.name}"); memory reads and writes fail until that provider mounts`);
|
|
55
|
+
}
|
|
56
|
+
const dispose = () => {
|
|
57
|
+
// P3 (v16) parity with the io registry: a handle acts only while it is
|
|
58
|
+
// still the CURRENT one for this name. An object-identity check cannot
|
|
59
|
+
// tell generations apart, so after dispose → re-register(same object) a
|
|
60
|
+
// stale handle used to delete the LIVE registration.
|
|
61
|
+
if (this.disposals.get(provider.name) !== dispose)
|
|
62
|
+
return;
|
|
63
|
+
this.providers.delete(provider.name);
|
|
64
|
+
this.disposals.delete(provider.name);
|
|
65
|
+
};
|
|
66
|
+
this.disposals.set(provider.name, dispose);
|
|
67
|
+
return dispose;
|
|
68
|
+
}
|
|
69
|
+
/** Configured provider: the pinned name, or the first registered one. */
|
|
70
|
+
selected() {
|
|
71
|
+
return this.provider(this.providerName || undefined);
|
|
72
|
+
}
|
|
73
|
+
/** 0.3.17 (E-73): named lookup like the io/state-storage registries; no
|
|
74
|
+
* name = first registered (backward compatible). A named miss throws (F-333,
|
|
75
|
+
* 0.3.23) instead of silently falling back to the first provider, so a wrong
|
|
76
|
+
* name surfaces rather than writing to the wrong memory store. */
|
|
77
|
+
provider(name) {
|
|
78
|
+
if (name) {
|
|
79
|
+
const byName = this.providers.get(name);
|
|
80
|
+
if (!byName)
|
|
81
|
+
throw new Error(`memory provider "${name}" is not registered`);
|
|
82
|
+
return byName;
|
|
83
|
+
}
|
|
84
|
+
const first = this.providers.values().next().value;
|
|
85
|
+
if (!first)
|
|
86
|
+
throw new Error('memory: no provider registered');
|
|
87
|
+
return first;
|
|
88
|
+
}
|
|
89
|
+
read(target) {
|
|
90
|
+
return this.selected().read(target);
|
|
91
|
+
}
|
|
92
|
+
async applyBatch(target, operations) {
|
|
93
|
+
const result = await this.selected().applyBatch(target, operations);
|
|
94
|
+
// P2 fix: every successful write refreshes whatever listens — the snapshot
|
|
95
|
+
// subscriber (tool-memory) re-renders the model-visible context. This is
|
|
96
|
+
// the single write sink, so bypass paths are covered without per-path fixes.
|
|
97
|
+
if (result.ok)
|
|
98
|
+
this.ctx.emit('evolution/memory-applied', {
|
|
99
|
+
target,
|
|
100
|
+
chars: result.chars,
|
|
101
|
+
entries: result.entries.length,
|
|
102
|
+
});
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/** V6-44 (0.3.37): test-support API — production consumers read the
|
|
106
|
+
* model-visible context via `renderContext()`; snapshot() has no production
|
|
107
|
+
* consumer (tests inspect the raw snapshot, kept by declaration).
|
|
108
|
+
* @internal Exported for this package's own tests only. */
|
|
109
|
+
snapshot() {
|
|
110
|
+
return this.selected().snapshot();
|
|
111
|
+
}
|
|
112
|
+
renderContext() {
|
|
113
|
+
return this.selected().renderContext();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export default MemoryRegistry;
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing memory tool and runtime-context memory snapshot.
|
|
3
|
+
* @module @deepseek-ai/dsh-tool-memory
|
|
4
|
+
*/
|
|
5
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
export declare const name = "tool-memory";
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
/**
|
|
10
|
+
* System-prompt memory guidance, aligned with Hermes `MEMORY_GUIDANCE`.
|
|
11
|
+
* Reached by the model every turn, so it is the place to steer behavior: what
|
|
12
|
+
* to save durably, what not to save, and how to phrase an entry. The tool
|
|
13
|
+
* schema description (`MEMORY_TOOL_DESCRIPTION`) carries the complementary
|
|
14
|
+
* operation-level guidance (add/replace/remove, batch, targets, when).
|
|
15
|
+
*/
|
|
16
|
+
export declare const MEMORY_GUIDANCE: string;
|
|
17
|
+
/**
|
|
18
|
+
* Tool-schema description for `memory`, aligned with Hermes `MEMORY_SCHEMA`.
|
|
19
|
+
* Carries the operation-level guidance: how to batch, when to save, the
|
|
20
|
+
* priority order, the target semantics, and what to skip. Reached only when
|
|
21
|
+
* the model is choosing/using the tool, so it complements the always-on
|
|
22
|
+
* `MEMORY_GUIDANCE` system prompt.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MEMORY_TOOL_DESCRIPTION: string;
|
|
25
|
+
/** G3/S3.2: `memoryEnabled` stays a deployment switch on purpose — it decides whether the
|
|
26
|
+
* tool and its prompt section are REGISTERED at all, so making it live would mean dynamic
|
|
27
|
+
* registration with catalog-visible effects (registry tier E2). The display knob the user
|
|
28
|
+
* may tune (`entryPreviewChars`) is a volatile row field instead (G1 §8.1). */
|
|
29
|
+
export interface Config {
|
|
30
|
+
memoryEnabled?: boolean;
|
|
31
|
+
/** Maximum characters of each memory entry echoed back in tool results (live reference). */
|
|
32
|
+
entryPreviewChars?: Volatile<number>;
|
|
33
|
+
}
|
|
34
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
35
|
+
memoryEnabled: z<boolean, boolean, "defined">;
|
|
36
|
+
entryPreviewChars: z<number, number, "volatile-defined">;
|
|
37
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
38
|
+
memoryEnabled: z<boolean, boolean, "defined">;
|
|
39
|
+
entryPreviewChars: z<number, number, "volatile-defined">;
|
|
40
|
+
}>>, "plain">;
|
|
41
|
+
export declare function apply(ctx: Context, rawConfig?: Config): Promise<void>;
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|