@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.
- 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/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/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/package.json +19 -5
|
@@ -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
|
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.
|
|
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
|
-
"
|
|
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"
|