@lmzhen/dsh-memory 0.44.0 → 0.46.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.
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Model-facing memory tool and runtime-context memory snapshot.
3
+ * @module @deepseek-ai/dsh-tool-memory
4
+ */
5
+ import { effectiveSessionPolicy } from '@lmzhen/dsh-evolution-approval';
6
+ import z from '@deepseek-ai/schemastery';
7
+ import { defineTool, registerEvolutionTool } from '@lmzhen/dsh-evolution-core/platform';
8
+ import { clampedNumber, unwrapLive, MEMORY_GUIDANCE_SECTION_ORDER, payloadText } from '@lmzhen/dsh-evolution-core';
9
+ import { resolveExecOrigins } from '@lmzhen/dsh-evolution-core/skills';
10
+ export const name = 'tool-memory';
11
+ /** Max characters of each echoed memory entry (single source for the Config default and the runtime slice). */
12
+ const DEFAULT_ENTRY_PREVIEW_CHARS = 200;
13
+ export const inject = ['tools', 'memory'];
14
+ /**
15
+ * System-prompt memory guidance, aligned with Hermes `MEMORY_GUIDANCE`.
16
+ * Reached by the model every turn, so it is the place to steer behavior: what
17
+ * to save durably, what not to save, and how to phrase an entry. The tool
18
+ * schema description (`MEMORY_TOOL_DESCRIPTION`) carries the complementary
19
+ * operation-level guidance (add/replace/remove, batch, targets, when).
20
+ */
21
+ export const MEMORY_GUIDANCE = 'You have durable memory across sessions. Save stable user preferences, '
22
+ + 'environment facts, conventions, and tool quirks with the `memory` tool. '
23
+ + 'The most valuable memory is one that stops the user correcting or reminding '
24
+ + 'you again, so user preferences and recurring corrections outrank procedural '
25
+ + 'task details.\n'
26
+ + 'Write entries as declarative facts, not instructions to yourself: '
27
+ + '"User prefers concise responses" (good); "Always respond concisely" (bad). '
28
+ + 'Imperative phrasing is re-read as a directive in later sessions and can '
29
+ + 'override the user\u2019s current request.\n'
30
+ + 'Do NOT save task progress, session outcomes, completed-work logs, PR/issue '
31
+ + 'numbers, commit SHAs, or anything stale within a week — use the session '
32
+ + 'query tool to recall past sessions instead. Reusable procedures belong in '
33
+ + 'a skill, not memory.';
34
+ /**
35
+ * Tool-schema description for `memory`, aligned with Hermes `MEMORY_SCHEMA`.
36
+ * Carries the operation-level guidance: how to batch, when to save, the
37
+ * priority order, the target semantics, and what to skip. Reached only when
38
+ * the model is choosing/using the tool, so it complements the always-on
39
+ * `MEMORY_GUIDANCE` system prompt.
40
+ */
41
+ export const MEMORY_TOOL_DESCRIPTION = 'Save durable facts to persistent memory that survive across sessions. '
42
+ + 'Memory is injected into future turns, so keep entries compact and high-signal.\n\n'
43
+ + 'HOW: make all changes in ONE call via an `operations` array (each item '
44
+ + '{action, facts?|content?, old_text?}). The batch applies atomically and the char '
45
+ + 'limit is checked on the final result — so one call can remove/replace stale '
46
+ + 'entries to free room AND add new ones. Use bare action/content/old_text '
47
+ + 'only for a single lone change.\n\n'
48
+ + 'WHEN: save proactively when the user states a preference, correction, or '
49
+ + 'personal detail, or you learn a stable fact about their environment, '
50
+ + 'conventions, or workflow. Priority: user preferences & corrections > '
51
+ + 'environment facts > procedures. The best memory stops the user repeating '
52
+ + 'themselves.\n\n'
53
+ + 'TARGETS: "user" = who the user is (name, role, preferences, style). '
54
+ + '"memory" = your notes (environment, conventions, tool quirks, lessons).\n\n'
55
+ + 'SKIP: trivial/obvious info, easily re-discovered facts, task progress, '
56
+ + 'completed-work logs, temporary TODO state. To recall a past session use '
57
+ + 'the session query tool, not memory. Reusable procedures belong in a skill, not '
58
+ + 'memory.';
59
+ export const Config = z.object({
60
+ memoryEnabled: z.boolean().default(true),
61
+ // 0.3.18 (S4.6, T-13): lower bound 1 — `slice(0, negative)` returned the
62
+ // entry TAIL (semantics inversion) instead of an empty/full preview.
63
+ entryPreviewChars: z.number().min(1).default(DEFAULT_ENTRY_PREVIEW_CHARS).volatile(),
64
+ });
65
+ export async function apply(ctx, rawConfig = {}) {
66
+ // G1: suppress the platform's auto-generated settings page for this row (the
67
+ // family renders its own card from the registry). Probed, not assumed: a host
68
+ // without the capability still loads.
69
+ ctx.inject(['settings'], (injected) => {
70
+ const settings = injected.settings;
71
+ if (typeof settings?.configure !== 'function')
72
+ return;
73
+ // v46 S1.12b (finding 6-1): the owner argument is what the platform looks the presentation up by.
74
+ // Omitting it registers the suppression on the settings service's own fiber, so a row that
75
+ // re-mounts keeps a dead key and the auto form can return (platform precedent:
76
+ // settings.configure({ auto: false }, plugin.fiber) — settings/src/index.ts:266).
77
+ const disposer = settings.configure({ auto: false }, ctx.fiber);
78
+ if (typeof disposer === 'function')
79
+ ctx.effect(() => disposer, 'tool-memory: settings presentation');
80
+ });
81
+ // S1-B3: with the tool disabled, prior staged memory writes used to become
82
+ // permanently unreachable — approve fell through to "No replay runner
83
+ // registered" and released the claim back to `pending` with no way forward.
84
+ // The flag gates only the MODEL-FACING surface (tool + prompt injection);
85
+ // the trailing approval-runner inject below runs in BOTH modes, so a staged
86
+ // write from a pre-disable session is either replayed or EXPLICITLY refused
87
+ // — never orphaned in the pending view.
88
+ const memoryDisabled = rawConfig.memoryEnabled === false;
89
+ // V6-06 (0.3.35): assembly-time clamp — a 0/negative/NaN/±Infinity value
90
+ // falls back to the default instead of turning every echoed entry into a
91
+ // tail slice (`slice(0, negative)` inverted the preview) or an empty string
92
+ // (`slice(0, NaN)`). The schema `.min(1)` rejects 0/negative at load; this
93
+ // clamp also covers NaN/±Infinity.
94
+ // G3/S3.2 + G1 §8.3: the preview length is a VOLATILE row field — its reference carries
95
+ // the platform-resolved layers (schema default < bundle row < profile override) and is
96
+ // read at USE time, so a committed change applies to the next tool result with no
97
+ // restart and no watcher. The clamp covers NaN/±Infinity, which schemastery lets
98
+ // through; `.min(1)` rejects 0/negative at load (`slice(0, negative)` used to return
99
+ // the entry TAIL — the 0.3.18 inversion).
100
+ let previewClampWarned = false;
101
+ const previewChars = () => {
102
+ const value = unwrapLive(rawConfig.entryPreviewChars);
103
+ const clamped = clampedNumber(value, DEFAULT_ENTRY_PREVIEW_CHARS, { min: 1 });
104
+ if (!previewClampWarned && value !== undefined && clamped !== value) {
105
+ previewClampWarned = true;
106
+ ctx.logger.warn(`tool-memory: entryPreviewChars=${String(value)} is invalid; falling back to the default ${DEFAULT_ENTRY_PREVIEW_CHARS}`);
107
+ }
108
+ return clamped;
109
+ };
110
+ // 0.3.18 (S4.3, E-67): systemPrompt is an OPTIONAL service (soft probe, the
111
+ // M-7 doctrine — align with tool-skill-manage). A host without it still boots
112
+ // and gets the working memory tool; only guidance/snapshot are skipped.
113
+ // The mount-time renderContext() is also failure-tolerant: without a
114
+ // registered memory provider the snapshot degrades to empty (self-corrects
115
+ // at the first successful write through the applied-event listener).
116
+ // P2-13 (v15): the REAL upstream types (a zero-arg `text` closure satisfies
117
+ // PromptContext's `(ctx) => string`) — upstream signature drift now fails
118
+ // `tsc` at the mirror. (The hand-written shape was { section({name,order,
119
+ // text}): () => void; context({name,order,text}): () => void } | undefined.)
120
+ const systemPrompt = ctx.get('systemPrompt');
121
+ let snapshotText = '';
122
+ if (!memoryDisabled) {
123
+ try {
124
+ snapshotText = await ctx.memory.renderContext();
125
+ }
126
+ catch (error) {
127
+ ctx.logger.warn(`tool-memory: memory provider not ready at mount; snapshot starts empty until the first write: ${error instanceof Error ? error.message : String(error)}`);
128
+ }
129
+ }
130
+ if (systemPrompt && !memoryDisabled) {
131
+ // G6-S2/W8: the guidance and the snapshot are LITERAL text contributions — a memory entry may
132
+ // contain the platform's `{{name}}` prompt syntax, and the platform throws on unknown references.
133
+ // interpolate:false keeps every brace verbatim (no escaping, no substitution) for BOTH the static
134
+ // guidance and the user-authored snapshot.
135
+ ctx.effect(() => systemPrompt.section({
136
+ name: 'evolution:memory-guidance',
137
+ order: MEMORY_GUIDANCE_SECTION_ORDER,
138
+ text: MEMORY_GUIDANCE,
139
+ interpolate: false,
140
+ }), 'tool-memory.memory-guidance');
141
+ ctx.effect(() => systemPrompt.context({
142
+ name: 'evolution:memory-snapshot',
143
+ order: 150,
144
+ text: () => snapshotText,
145
+ interpolate: false,
146
+ }), 'tool-memory.memory-snapshot');
147
+ // P2 fix: the snapshot refresh moved to the write sink (MemoryRegistry
148
+ // emits evolution/memory-applied after ANY successful write). Bypass paths —
149
+ // `/graph edit|delete memory:`, background review direct writes — refresh the
150
+ // model-visible snapshot here, not only the foreground tool's own callback.
151
+ const refreshSnapshot = async () => {
152
+ try {
153
+ snapshotText = await ctx.memory.renderContext();
154
+ }
155
+ catch {
156
+ // Snapshot refresh is best-effort; a stale snapshot self-corrects at the
157
+ // next successful refresh (the write itself already landed).
158
+ }
159
+ };
160
+ ctx.effect(() => ctx.on('evolution/memory-applied', () => { void refreshSnapshot(); }), 'tool-memory.snapshot-refresh');
161
+ // P2-07 (audit): cordis `inject` does not order sibling fibers — if the
162
+ // memory PROVIDER row applies after this row, the mount-time render throws
163
+ // (`memory: no provider registered`). The registry now announces the
164
+ // registration, so the snapshot refreshes on that event instead of a 500 ms
165
+ // retry that held a timer for up to 60 s.
166
+ ctx.effect(() => ctx.on('evolution/memory-provider-registered', () => { void refreshSnapshot(); }), 'tool-memory.snapshot-ready');
167
+ }
168
+ else if (!memoryDisabled) {
169
+ ctx.logger.warn('tool-memory: systemPrompt service not mounted; memory guidance and snapshot are not injected (the write tool still works)');
170
+ }
171
+ async function executeCore(normalized) {
172
+ // V24-20a (v24): element-level shape guard on the DIRECT entry. The
173
+ // approval replay runner calls executeCore with the STORED staged args
174
+ // and no tool schema in front of it, so `operations: [null]` used to
175
+ // reach applyBatch and throw a bare TypeError. (The tool-execute path
176
+ // rejects the same shape earlier, in its conflict pre-check.)
177
+ if (normalized.operations?.some((op) => {
178
+ const raw = op;
179
+ return raw === null || typeof raw !== 'object';
180
+ })) {
181
+ return { ok: false, message: 'operations 的每一项都必须是对象。', entries: [], chars: 0, limit: 0 };
182
+ }
183
+ // v30 TSM-03: field-level guards mirroring tool-skill-manage's scalar
184
+ // guard. The approval replay runner executes STORED args with no schema:
185
+ // a garbage STRING `target` (e.g. "memori") used to retarget the write
186
+ // to USER.md silently (fileFor maps anything ≠ 'memory' to the user
187
+ // profile), and non-string facts/old_text escaped as bare TypeErrors
188
+ // from inside the store.
189
+ // `unknown`-typed alias: the runtime value may violate the declared
190
+ // union (the replay channel executes stored args without a schema), so
191
+ // the comparison below must stay a REAL guard.
192
+ const target = normalized.target;
193
+ if (target !== 'memory' && target !== 'user') {
194
+ return { ok: false, message: `memory:"target" 必须是 "memory" 或 "user"(收到 ${String(target)});拒绝写入。`, entries: [], chars: 0, limit: 0 };
195
+ }
196
+ for (const field of ['facts', 'old_text', 'content']) {
197
+ const value = normalized[field];
198
+ if (value !== undefined && value !== null && typeof value !== 'string') {
199
+ return { ok: false, message: `memory:"${field}" 必须是字符串(收到 ${typeof value});拒绝写入。`, entries: [], chars: 0, limit: 0 };
200
+ }
201
+ }
202
+ // PLAN S3.6 (T2-03/A18): the OPERATION payload gets the same field gate, by INDEX and by FIELD.
203
+ // `content` is the documented alias of `facts` (T3-06) and was the one field the guards above
204
+ // did not name: a staged `operations: [{action:'add', content: 5}]` reached the store, threw, and
205
+ // the approval path answered the generic "Replay runner failed" while the record stayed pending
206
+ // forever. A payload defect can never be approved away, so the refusal says how to dispose of it.
207
+ for (const [index, operation] of (normalized.operations ?? []).entries()) {
208
+ for (const field of ['facts', 'content', 'old_text']) {
209
+ const value = operation[field];
210
+ if (value === undefined || value === null || typeof value === 'string')
211
+ continue;
212
+ return { ok: false, message: `memory:operations[${index}].${field} 必须是字符串(收到 ${typeof value});拒绝写入 —— 重新批准无法修复载荷缺陷,请驳回该记录。`, entries: [], chars: 0, limit: 0 };
213
+ }
214
+ }
215
+ const result = normalized.operations
216
+ ? await ctx.memory.applyBatch(normalized.target, normalized.operations)
217
+ : await ctx.memory.applyBatch(normalized.target, [{ action: normalized.action ?? 'add', facts: normalized.facts, old_text: normalized.old_text }]);
218
+ // 0.3.18 (S4.2, E-20): the snapshot refresh happens ONLY in the
219
+ // memory-applied listener (single sink — MemoryRegistry emits after every
220
+ // successful write). A render here raced the listener's render and could
221
+ // leave a stale snapshot in the prompt.
222
+ return {
223
+ ok: result.ok,
224
+ message: result.message,
225
+ entries: result.entries.map(entry => entry.slice(0, previewChars())),
226
+ chars: result.chars,
227
+ limit: result.limit,
228
+ };
229
+ }
230
+ // PLAN-R2 P2-7 (2026-09-16): the store's per-action required-field contract,
231
+ // mirrored field-for-field so this pre-check and the store's own rejection
232
+ // cannot diverge. The effective judgment (memory-files and this file both
233
+ // resolve the pair through core's `payloadText`, T3-06/A34, then
234
+ // evolution-core's applyBatchCore enforces):
235
+ // add needs non-blank facts; remove/replace need non-blank old_text;
236
+ // replace additionally needs non-blank facts; an out-of-enum action is
237
+ // rejected loud (V6-25). Rejections below reuse the store's exact message
238
+ // text minus the on-disk entry preview (unknowable before the write).
239
+ const requiredFieldRejection = (op, position) => {
240
+ const body = payloadText(op);
241
+ if (op.action === 'add') {
242
+ return body ? null : `第 ${position} 个操作(add):必须提供 facts。未应用任何操作。`;
243
+ }
244
+ if (op.action !== 'remove' && op.action !== 'replace') {
245
+ return `第 ${position} 个操作:未知动作「${String(op.action)}」(应为 add/remove/replace)。未应用任何操作。`;
246
+ }
247
+ if (!(op.old_text ?? '').trim()) {
248
+ return `第 ${position} 个操作(${op.action}):必须提供 old_text。未应用任何操作。`;
249
+ }
250
+ if (op.action === 'replace' && !body) {
251
+ return `第 ${position} 个操作(replace):必须提供 facts。`;
252
+ }
253
+ return null;
254
+ };
255
+ // S1-B3: gated on memoryDisabled — a disabled tool must not appear in the
256
+ // catalog. (The approval runner below registers in both modes.)
257
+ if (!memoryDisabled) {
258
+ registerEvolutionTool(ctx, defineTool({
259
+ name: 'memory',
260
+ description: MEMORY_TOOL_DESCRIPTION,
261
+ parameters: {
262
+ target: { type: 'string', enum: ['memory', 'user'], required: true },
263
+ action: { type: 'string', enum: ['add', 'replace', 'remove'] },
264
+ facts: { type: 'string' },
265
+ content: { type: 'string' },
266
+ old_text: { type: 'string' },
267
+ operations: {
268
+ type: 'array',
269
+ items: {
270
+ type: 'object',
271
+ additionalProperties: false,
272
+ properties: {
273
+ action: { type: 'string', enum: ['add', 'replace', 'remove'], required: true },
274
+ facts: { type: 'string' },
275
+ content: { type: 'string' },
276
+ old_text: { type: 'string' },
277
+ },
278
+ },
279
+ },
280
+ },
281
+ output: {
282
+ schema: {
283
+ type: 'object',
284
+ additionalProperties: false,
285
+ properties: {
286
+ ok: { type: 'boolean', required: true },
287
+ message: { type: 'string', required: true },
288
+ entries: { type: 'array', required: true, items: { type: 'string' } },
289
+ chars: { type: 'integer', required: true },
290
+ limit: { type: 'integer', required: true },
291
+ pending_id: { type: 'string' },
292
+ },
293
+ },
294
+ render: (_args, value) => [{ type: 'text', text: `${value.ok ? 'OK' : 'Error'}: ${value.message} (${value.chars}/${value.limit} chars)` }],
295
+ },
296
+ isConcurrencySafe: () => false,
297
+ // F-06: `session` is optional in the exec contract too — the
298
+ // defensive chaining below is only honest if the type says so.
299
+ async execute(args, exec) {
300
+ // facts and content are the same field under two names; a differing pair
301
+ // is ambiguous input, so fail loud instead of silently dropping one.
302
+ // T3-06/A34: only TWO non-blank bodies can differ. A blank member
303
+ // carries no payload, so `{facts: '', content: 'x'}` is one payload
304
+ // (core's `payloadText`), not an ambiguous pair — refusing it here while
305
+ // every other channel lands 'x' was the half-changed form of the same
306
+ // rule. Read through `payloadText` so a non-string member is skipped
307
+ // rather than trimmed (the replay channel passes stored args with no
308
+ // schema, and the field gate below owns that refusal).
309
+ const conflict = (a) => {
310
+ const facts = payloadText({ facts: a.facts });
311
+ const content = payloadText({ content: a.content });
312
+ return facts !== '' && content !== '' && facts !== content;
313
+ };
314
+ // V24-20a (v24): element-level shape guard — `operations: [null]` used
315
+ // to reach `conflict` and throw a bare TypeError (`a.facts` on null).
316
+ // Same "schema is not a guarantee" posture as V8-09 (array container)
317
+ // and D-1 (scalar fields); the approval staged-args replay path does
318
+ // not pass the schema, so the guard is not redundant with it.
319
+ if (Array.isArray(args.operations)) {
320
+ for (const op of args.operations) {
321
+ const raw = op;
322
+ if (raw === null || typeof raw !== 'object') {
323
+ return { ok: false, message: 'operations 的每一项都必须是对象。', entries: [], chars: 0, limit: 0 };
324
+ }
325
+ if (conflict(op))
326
+ return { ok: false, message: '每个操作只能提供 facts 或 content 之一(同一字段):两者给了不同的值。', entries: [], chars: 0, limit: 0 };
327
+ }
328
+ }
329
+ else if (conflict(args)) {
330
+ return { ok: false, message: '只能提供 facts 或 content 之一(同一字段):两者给了不同的值。', entries: [], chars: 0, limit: 0 };
331
+ }
332
+ // V7-07 (0.3.43): an EMPTY operations array is a no-op input — reject it
333
+ // BEFORE the approval gate (a "memory 0 ops" approval record would be
334
+ // staged and is meaningless to replay; the 0.3.37 V6-25 declaration
335
+ // assumed this check existed).
336
+ if (Array.isArray(args.operations) && args.operations.length === 0) {
337
+ return { ok: false, message: '没有提供任何操作(operations 数组为空)。', entries: [], chars: 0, limit: 0 };
338
+ }
339
+ // F-07: a NON-ARRAY `operations` payload (garbage that slipped
340
+ // past the schema) used to fall into the single-operation branch, so the
341
+ // model received an error about the WRONG shape (e.g. "facts required")
342
+ // instead of the real one. Return a structured shape error before any
343
+ // normalization/approval; an ABSENT operations field keeps the single-op
344
+ // path (the documented bare action/content/old_text form).
345
+ if (args.operations !== undefined && !Array.isArray(args.operations)) {
346
+ return { ok: false, message: '形状不合法:operations 必须是 {action, content?, old_text?} 对象的数组(单个操作时可省略 operations)。', entries: [], chars: 0, limit: 0 };
347
+ }
348
+ const target = args.target === 'user' ? 'user' : 'memory';
349
+ const normalized = Array.isArray(args.operations)
350
+ ? { target, operations: args.operations }
351
+ : { target, action: args.action ?? 'add', facts: payloadText(args), old_text: args.old_text };
352
+ // PLAN-R2 P2-7 (2026-09-16): existence pre-check of the per-action
353
+ // required fields BEFORE the approval gate, on BOTH paths — each
354
+ // operations[] element (store position = index + 1) and the normalized
355
+ // single operation (position 1). The store's required-field rejection
356
+ // (see requiredFieldRejection above) used to run only at EXECUTION, so
357
+ // an approval-enabled deployment staged such a write first and the
358
+ // approved replay then failed hard — V7-07 fixed the empty-operations
359
+ // sibling of this; this closes the per-operation gap the same way.
360
+ if (normalized.operations) {
361
+ for (const [index, op] of normalized.operations.entries()) {
362
+ const rejection = requiredFieldRejection(op, index + 1);
363
+ if (rejection)
364
+ return { ok: false, message: rejection, entries: [], chars: 0, limit: 0 };
365
+ }
366
+ }
367
+ else {
368
+ const rejection = requiredFieldRejection(normalized, 1);
369
+ if (rejection)
370
+ return { ok: false, message: rejection, entries: [], chars: 0, limit: 0 };
371
+ }
372
+ // F-329 parity for single operations (V4-15): only qualify the summary's
373
+ // target when it differs from the default 'memory', so a lone add to the
374
+ // default target reads "memory add" instead of the redundant "memory
375
+ // memory add". The batch form already gets this through normalizeSummary
376
+ // (evolution-approval, only for operations.length > 1) — apply the same
377
+ // single-word rule here for the single-op summary.
378
+ const targetLabel = target === 'memory' ? '' : `${target} `;
379
+ // Single-source origin resolution (S1-B1, on the rc.44 M2-2.3 table):
380
+ // core's resolveExecOrigins reads the session header origin AND the v37
381
+ // S2.2 review-channel session mark. This tool used to skip the mark
382
+ // half, so an inject-mode review's memory writes resolved as
383
+ // `foreground` — mislabeled in the approval queue and, under
384
+ // `stageForeground: false`, executing without staging at all.
385
+ const origin = resolveExecOrigins(exec).approval;
386
+ const sessionPolicy = effectiveSessionPolicy(ctx, exec.agent?.session);
387
+ const approval = ctx.get('evolutionApproval');
388
+ if (approval) {
389
+ const decision = await approval.request({
390
+ kind: 'memory',
391
+ summary: `memory ${targetLabel}${Array.isArray(args.operations) ? `${args.operations.length} ops` : (args.action ?? 'add')}`,
392
+ args: normalized,
393
+ origin,
394
+ // 0.3.20 (N-1): the session id rides along so the approval service can
395
+ // DERIVE the platform override ('never' for unattended sessions); the
396
+ // tool previously sent only the self-reported policy, which the
397
+ // mounted platform approval service discards in favour of its own
398
+ // derivation — leaving CI/cron writes stuck in staging.
399
+ // F-06: full-depth optional chaining (see above).
400
+ ...exec.agent?.session?.id ? { sessionId: exec.agent.session.id } : {},
401
+ // V6-27 (0.3.40): the platform overrideOf resolves the policy from the
402
+ // session's log view — the session OBJECT, not the id, is what it can probe.
403
+ // F-06: full-depth optional chaining (see above).
404
+ ...exec.agent?.session ? { session: exec.agent.session } : {},
405
+ ...sessionPolicy !== undefined ? { sessionPolicy } : {},
406
+ });
407
+ if (decision.action === 'staged') {
408
+ // 0.3.20 (N-1-followup): no `pending_id ?? ''` — absent stays absent
409
+ // (mirrors the tool-skill-manage E-70 shape).
410
+ return {
411
+ ok: true,
412
+ message: decision.message,
413
+ entries: [],
414
+ chars: 0,
415
+ limit: 0,
416
+ ...decision.pendingId !== undefined ? { pending_id: decision.pendingId } : {},
417
+ };
418
+ }
419
+ }
420
+ return await executeCore(normalized);
421
+ },
422
+ }));
423
+ }
424
+ ctx.inject(['evolutionApproval'], (approvalCtx) => {
425
+ const approval = approvalCtx.evolutionApproval;
426
+ // S1-B3: the runner exists in BOTH modes — replay when enabled, an explicit
427
+ // refusal when disabled — so a staged write can never become an unresolvable
428
+ // pending record ("No replay runner registered" bounce).
429
+ const dispose = memoryDisabled
430
+ ? approval.registerRunner('memory', () => Promise.resolve({
431
+ ok: false,
432
+ message: '拒绝:记忆工具已禁用(tool-memory memoryEnabled:false)—— 它的回放运行器无法执行这条暂存写入。请驳回该记录;若仍需要该写入,请在重新启用该工具后再暂存。',
433
+ }))
434
+ : approval.registerRunner('memory', args => executeCore(args));
435
+ approvalCtx.effect(() => dispose, memoryDisabled ? 'tool-memory.approval-runner-disabled' : 'tool-memory.approval-runner');
436
+ });
437
+ }
438
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Lokales Gedächtnis",
4
+ "description": "Merkt sich nützliche Fakten über Sitzungen hinweg und behält sie auf diesem Rechner."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Local memory",
4
+ "description": "Remembers useful facts across sessions and keeps them on this computer."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Memoria local",
4
+ "description": "Recuerda datos útiles entre sesiones y los guarda en este equipo."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Mémoire locale",
4
+ "description": "Retient les faits utiles d’une session à l’autre et les garde sur cet ordinateur."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "このパソコンのメモリ",
4
+ "description": "役に立つ知識をセッションをまたいで覚え、このパソコンに置いておく。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Memória local",
4
+ "description": "Lembra fatos úteis entre sessões e os mantém neste computador."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Память на компьютере",
4
+ "description": "Запоминает полезные факты от сессии к сессии и хранит их на этом компьютере."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "本地记忆",
4
+ "description": "记住有用的信息,换个会话也不会丢,都存在这台电脑上。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Agenten-Gedächtnis",
4
+ "description": "Lässt den Agenten speichern, was er gelernt hat, und später wieder abrufen."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Agent memory",
4
+ "description": "Lets the agent save and recall what it learned."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Memoria del agente",
4
+ "description": "El agente guarda y recupera lo que aprende."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Mémoire de l’agent",
4
+ "description": "Permet à l’agent d’enregistrer ce qu’il a appris et de le retrouver."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "エージェントのメモリ",
4
+ "description": "エージェントが学んだことを保存し、あとから思い出せる。"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Memória do agente",
4
+ "description": "Deixa o agente salvar e lembrar o que aprendeu."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Память агента",
4
+ "description": "Агент может сохранять и вспоминать то, что узнал."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "智能体记忆",
4
+ "description": "让智能体把学到的东西存下来,需要时再取。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-memory",
3
3
  "description": "Memory provider registry for self-evolution (community build)",
4
- "version": "0.44.0",
4
+ "version": "0.46.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -19,16 +19,30 @@
19
19
  "default": "./lib/index.js"
20
20
  },
21
21
  "./package.json": "./package.json",
22
- "./locale/*.json": "./locale/*.json"
22
+ "./locale/*.json": "./locale/*.json",
23
+ "./files": {
24
+ "types": "./lib/types/files/index.d.ts",
25
+ "default": "./lib/files/index.js"
26
+ },
27
+ "./files/locale/*.json": "./locale/files/*.json",
28
+ "./tools": {
29
+ "types": "./lib/types/tools/index.d.ts",
30
+ "default": "./lib/tools/index.js"
31
+ },
32
+ "./tools/locale/*.json": "./locale/tools/*.json"
23
33
  },
24
34
  "files": [
25
- "locale/*.json",
35
+ "lib/**/*.js",
26
36
  "lib/*.js",
27
- "lib/types/**/*.d.ts"
37
+ "lib/types/**/*.d.ts",
38
+ "locale/**/*.json",
39
+ "locale/*.json"
28
40
  ],
29
41
  "license": "MIT",
30
42
  "dependencies": {
31
- "@deepseek-ai/schemastery": "^3.18.5"
43
+ "@deepseek-ai/schemastery": "^3.18.5",
44
+ "@lmzhen/dsh-evolution-core": "^0.46.0",
45
+ "@lmzhen/dsh-evolution-approval": "^0.46.0"
32
46
  },
33
47
  "peerDependencies": {
34
48
  "@deepseek-ai/cordis": "^4.0.5"