dsh-memento 0.2.0 → 0.3.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/ARCHITECTURE.md +140 -132
- package/CHANGELOG.md +67 -46
- package/README.es.md +167 -166
- package/README.hi.md +167 -166
- package/README.md +167 -166
- package/README.pt.md +167 -166
- package/README.zh.md +167 -166
- package/client/client.js +241 -240
- package/index.mjs +169 -35
- package/lib/constants.mjs +64 -58
- package/lib/store.mjs +675 -654
- package/package.json +93 -93
- package/types.d.ts +193 -191
package/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
14
|
import Schema from '@deepseek-ai/schemastery'
|
|
15
15
|
import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
16
17
|
import {
|
|
17
18
|
TRACKS,
|
|
18
19
|
SCOPES,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
DEFAULT_SOURCE,
|
|
21
22
|
SESSION_EVENTS,
|
|
22
23
|
PANEL_AUDIT_CEILING,
|
|
24
|
+
EXPORT_SCHEMA,
|
|
25
|
+
MAX_IMPORT_ENTRIES,
|
|
23
26
|
} from './lib/constants.mjs'
|
|
24
27
|
import {
|
|
25
28
|
MemoryError,
|
|
@@ -49,7 +52,7 @@ import { extractEventText } from './lib/extract.mjs'
|
|
|
49
52
|
* @typedef {object} StoreHandle - ctx.memory 依赖的 Provider 面。
|
|
50
53
|
* @property {(filter?: {track?: string, scope?: string, text?: string, limit?: number}) => MemoryQueryResult} queryEntries
|
|
51
54
|
* @property {() => MemoryEntry[]} listEntries
|
|
52
|
-
* @property {(track: string, scope: string, match: string) => MemoryEntry[]} matchCandidates
|
|
55
|
+
* @property {(track: string, scope: string, match: string, opts?: {agentKey?: string, workspaceKey?: string}) => MemoryEntry[]} matchCandidates
|
|
53
56
|
* @property {(track: string, scope: string) => number} usage
|
|
54
57
|
* @property {(input: object) => MemoryEntry} insertEntry
|
|
55
58
|
* @property {(inputs: object[]) => MemoryEntry[]} seedEntries
|
|
@@ -253,8 +256,10 @@ export class MemoryService {
|
|
|
253
256
|
* 查询条目(无审批;带 sessionId 时记一条 recalled 审计)。
|
|
254
257
|
* 显式合法 limit 生效但被 Provider 硬钳到 MAX_QUERY_LIMIT(1000);缺省/非法 limit 用
|
|
255
258
|
* maxEntriesPerQuery(Config 默认 20,语义是"默认返回上限"而非硬顶)。
|
|
259
|
+
* opts.agentKey 给定时按会话可见集过滤(共享层 + 指定 agent 键);缺省不过滤——
|
|
260
|
+
* 管理面(/memory 命令、面板)与无会话上下文的插件调用保持全量视图。
|
|
256
261
|
* @param {{track?: string, scope?: string, text?: string, limit?: number}} [filter] - {track, scope, text, limit}。
|
|
257
|
-
* @param {{sessionId?: string, session?: MemorySessionLike | null}} [opts] - {sessionId, session};session 用于 memory/recalled
|
|
262
|
+
* @param {{sessionId?: string, session?: MemorySessionLike | null, agentKey?: string}} [opts] - {sessionId, session, agentKey};session 用于 memory/recalled
|
|
258
263
|
* 事件的按已知类型自适应派发(与写事件同一 maybeAppendSessionEvent 门)。
|
|
259
264
|
* @returns {MemoryQueryResult}。
|
|
260
265
|
*/
|
|
@@ -264,6 +269,7 @@ export class MemoryService {
|
|
|
264
269
|
...(filter.scope === undefined ? {} : { scope: filter.scope }),
|
|
265
270
|
...(typeof filter.text === 'string' && filter.text.length > 0 ? { text: filter.text } : {}),
|
|
266
271
|
limit: Number.isInteger(filter.limit) && filter.limit > 0 ? filter.limit : this.maxEntriesPerQuery,
|
|
272
|
+
...(typeof opts.agentKey === 'string' ? { agentKey: opts.agentKey } : {}),
|
|
267
273
|
})
|
|
268
274
|
if (opts.sessionId !== undefined) {
|
|
269
275
|
this.store.auditAppend({
|
|
@@ -291,19 +297,40 @@ export class MemoryService {
|
|
|
291
297
|
* approval/decided 审计对)。write.gate 为可选自定义传输(/memory 命令在
|
|
292
298
|
* turn 外使用:同一 approval/request waterfall + 同一 answerer 链裁决,
|
|
293
299
|
* 会话级 never 策略由调用方预检;审计落在插件审计表 + command/done)。
|
|
300
|
+
* 被拒(rejected/cancelled/unavailable/off)一律落 `<action>-denied` 审计行再抛——
|
|
301
|
+
* turn 外 gate 路径没有审批审计对,这是拒绝的唯一证据链。
|
|
294
302
|
* @param {WritePayload} payload - {action, track, scope, text, count?}。
|
|
295
303
|
* @param {MemoryWriteContext} write - {agent, callId?, signal?, gate?}。
|
|
296
304
|
* @returns {Promise<{outcome: string, source: 'approval'|'gate'}>} 实际裁决结果与传输来源(审计标签用)。
|
|
305
|
+
* 被拒(rejected/cancelled/unavailable/off)一律落 `<action>-denied` 审计行再抛——
|
|
306
|
+
* turn 外 gate 路径没有审批审计对,这是拒绝的唯一证据链。
|
|
297
307
|
*/
|
|
298
308
|
async #ask(payload, write) {
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
try {
|
|
310
|
+
const via = typeof write.gate === 'function'
|
|
311
|
+
? { outcome: await write.gate(payload, write), source: /** @type {'approval'|'gate'} */ ('gate') }
|
|
312
|
+
: { outcome: await askApproval(this.approval, payload, write), source: /** @type {'approval'|'gate'} */ ('approval') }
|
|
313
|
+
this.#assertOutcome(via.outcome)
|
|
314
|
+
return via
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof WriteDeniedError) {
|
|
317
|
+
const outcome = typeof error.details.outcome === 'string' ? error.details.outcome : 'denied'
|
|
318
|
+
const viaLabel = typeof write.gate === 'function'
|
|
319
|
+
? `${outcome} (via write gate)`
|
|
320
|
+
: `${outcome} (via approval, writePolicy ${this.writePolicy})`
|
|
321
|
+
this.store.auditAppend({
|
|
322
|
+
action: `${payload.action}-denied`,
|
|
323
|
+
track: payload.track,
|
|
324
|
+
scope: payload.scope,
|
|
325
|
+
entryId: null,
|
|
326
|
+
text: payload.text,
|
|
327
|
+
outcome: viaLabel,
|
|
328
|
+
source: payload.source ?? this.sourceLabel,
|
|
329
|
+
sessionId: write.agent?.session?.id ?? null,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
throw error
|
|
303
333
|
}
|
|
304
|
-
const outcome = await askApproval(this.approval, payload, write)
|
|
305
|
-
this.#assertOutcome(outcome)
|
|
306
|
-
return { outcome, source: 'approval' }
|
|
307
334
|
}
|
|
308
335
|
|
|
309
336
|
/** 审计 outcome 标签:审批传输标注策略,gate 传输标注 gate(真实裁决来源,不张冠李戴)。 */
|
|
@@ -349,13 +376,18 @@ export class MemoryService {
|
|
|
349
376
|
const { track, scope, text } = this.#validateEntry(input, write)
|
|
350
377
|
this.#assertMatch(input)
|
|
351
378
|
// 审批前先定位:零/多命中在打扰用户之前就响亮失败。
|
|
352
|
-
const initial = this.#resolveMatch(input, track, scope)
|
|
379
|
+
const initial = this.#resolveMatch(input, track, scope, write)
|
|
353
380
|
this.#assertBudget(track, scope, this.store.usage(track, scope), text.length - initial.text.length)
|
|
354
|
-
|
|
381
|
+
// 审批载荷携带将被改写的旧条目全文(approve-what-you-see:人批准的不是抽象动作,是具体变更)。
|
|
382
|
+
const via = await this.#ask({
|
|
383
|
+
action: 'replace', track, scope,
|
|
384
|
+
text: `from:\n${initial.text}\n\nto:\n${text}`,
|
|
385
|
+
source: input.source ?? this.sourceLabel,
|
|
386
|
+
}, write)
|
|
355
387
|
this.#throwIfAborted(write)
|
|
356
388
|
// 审批等待期间目标条目可能已被并发写改动:以此刻重新定位的 previous 为权威重算净变化,
|
|
357
389
|
// 再用此刻用量复审。复审与 replaceEntry 之间无 await,判断与实际写入之间不存在窗口。
|
|
358
|
-
const current = this.#resolveMatch(input, track, scope)
|
|
390
|
+
const current = this.#resolveMatch(input, track, scope, write)
|
|
359
391
|
const net = text.length - current.text.length
|
|
360
392
|
this.#assertBudget(track, scope, this.store.usage(track, scope), net)
|
|
361
393
|
// 事务内重新定位+更新:零/多命中仍会响亮报错(不静默)。
|
|
@@ -383,8 +415,9 @@ export class MemoryService {
|
|
|
383
415
|
this.#assertScope(input.track, input.scope)
|
|
384
416
|
this.#assertMatch(input)
|
|
385
417
|
// 审批前先定位:零/多命中在打扰用户之前就响亮失败。
|
|
386
|
-
const target = this.#resolveMatch(input, input.track, input.scope)
|
|
387
|
-
|
|
418
|
+
const target = this.#resolveMatch(input, input.track, input.scope, write)
|
|
419
|
+
// 审批载荷携带将被删除的条目全文(approve-what-you-see),而非裸子串。
|
|
420
|
+
const via = await this.#ask({ action: 'remove', track: input.track, scope: input.scope, text: target.text, source: target.source }, write)
|
|
388
421
|
this.#throwIfAborted(write)
|
|
389
422
|
const removed = this.store.removeEntry({ track: input.track, scope: input.scope, match: input.match })
|
|
390
423
|
this.#auditWrite('remove', input.track, input.scope, removed, write, via)
|
|
@@ -452,14 +485,21 @@ export class MemoryService {
|
|
|
452
485
|
const { track, scope, text } = this.#validateEntry(input, write)
|
|
453
486
|
this.#assertConsolidateMatches(input)
|
|
454
487
|
// 审批前先定位全部目标:零/多命中在打扰用户之前就响亮失败。
|
|
455
|
-
const initial = input.matches.map((match) => this.#resolveMatch({ match }, track, scope))
|
|
488
|
+
const initial = input.matches.map((match) => this.#resolveMatch({ match }, track, scope, write))
|
|
456
489
|
const removalBefore = initial.reduce((sum, entry) => sum + entry.text.length, 0)
|
|
457
490
|
this.#assertBudget(track, scope, this.store.usage(track, scope), text.length - removalBefore)
|
|
458
|
-
|
|
491
|
+
// 审批载荷携带每个目标的定位条目原文(单条超长只截前 300 字,避免 20×满预算条目撑爆载荷)。
|
|
492
|
+
const plan = [
|
|
493
|
+
...initial.map((entry, index) => {
|
|
494
|
+
const body = entry.text.length > 300 ? `${entry.text.slice(0, 300)}…` : entry.text
|
|
495
|
+
return `remove: ${input.matches[index]}\n${body}`
|
|
496
|
+
}),
|
|
497
|
+
`new text: ${text}`,
|
|
498
|
+
].join('\n')
|
|
459
499
|
const via = await this.#ask({ action: 'consolidate', track, scope, text: plan, source: input.source ?? this.sourceLabel }, write)
|
|
460
500
|
this.#throwIfAborted(write)
|
|
461
501
|
// 审批等待期间目标可能被并发写改动:重新定位并以此刻为权威重算净变化。
|
|
462
|
-
const current = input.matches.map((match) => this.#resolveMatch({ match }, track, scope))
|
|
502
|
+
const current = input.matches.map((match) => this.#resolveMatch({ match }, track, scope, write))
|
|
463
503
|
const removalNow = current.reduce((sum, entry) => sum + entry.text.length, 0)
|
|
464
504
|
this.#assertBudget(track, scope, this.store.usage(track, scope), text.length - removalNow)
|
|
465
505
|
const { removed, entry } = this.store.consolidateEntries({
|
|
@@ -528,9 +568,18 @@ export class MemoryService {
|
|
|
528
568
|
return { track: input.track, scope: input.scope, text: input.text }
|
|
529
569
|
}
|
|
530
570
|
|
|
531
|
-
/**
|
|
532
|
-
|
|
533
|
-
|
|
571
|
+
/**
|
|
572
|
+
* 审批前定位替换/删除目标(唯一子串语义,大小写不敏感,零/多命中结构化报错)。
|
|
573
|
+
* 写定位 = 会话可见集:agentKey 共享 + 写方会话键(显式 input 覆盖);scope='workspace'
|
|
574
|
+
* 时按写方会话 cwd 键过滤——跨 agent/跨工作区条目对本会话不可见、也不可被误改。
|
|
575
|
+
*/
|
|
576
|
+
#resolveMatch(/** @type {{match: string, agentKey?: string, workspaceKey?: string}} */ input, /** @type {string} */ track, /** @type {string} */ scope, /** @type {MemoryWriteContext} */ write) {
|
|
577
|
+
const agentKey = typeof input.agentKey === 'string' && input.agentKey.length > 0 ? input.agentKey : this.#agentKeyOf(write)
|
|
578
|
+
const workspaceKey = typeof input.workspaceKey === 'string' && input.workspaceKey.length > 0 ? input.workspaceKey : this.#workspaceKeyOf(write)
|
|
579
|
+
const hits = /** @type {MemoryEntry[]} */ (this.store.matchCandidates(track, scope, input.match, {
|
|
580
|
+
agentKey,
|
|
581
|
+
workspaceKey: scope === 'workspace' ? workspaceKey : undefined,
|
|
582
|
+
}))
|
|
534
583
|
.filter((entry) => entry.text.toLowerCase().includes(input.match.toLowerCase()))
|
|
535
584
|
if (hits.length === 0) throw new EntryNotFoundError({ track, scope, match: input.match })
|
|
536
585
|
if (hits.length > 1) {
|
|
@@ -830,7 +879,12 @@ export function makeMemoryTool(service, language = 'en') {
|
|
|
830
879
|
...(args.text === undefined ? {} : { text: args.text }),
|
|
831
880
|
...(args.limit === undefined ? {} : { limit: args.limit }),
|
|
832
881
|
},
|
|
833
|
-
{
|
|
882
|
+
{
|
|
883
|
+
sessionId: exec.agent?.session?.id,
|
|
884
|
+
session: exec.agent?.session,
|
|
885
|
+
// 会话内读按可见集过滤(共享 + 本 agent),与冻结快照同一语义。
|
|
886
|
+
agentKey: agentKeyOf(/** @type {string | undefined} */ (exec.agent?.session?.header?.agentPreset)),
|
|
887
|
+
},
|
|
834
888
|
)
|
|
835
889
|
return {
|
|
836
890
|
action: 'query',
|
|
@@ -1268,12 +1322,20 @@ function makeCommandGate(ctx, write) {
|
|
|
1268
1322
|
* @property {string} consolidateNeedsText
|
|
1269
1323
|
* @property {(track: string, scope: string, removed: number, text: string, used: number, limit: number) => string} consolidated
|
|
1270
1324
|
* @property {string} exportUsage
|
|
1325
|
+
* @property {string} importUsage
|
|
1326
|
+
* @property {string} importBadJson
|
|
1327
|
+
* @property {(path: string, message: string) => string} importReadFailed
|
|
1328
|
+
* @property {(schema: string) => string} importBadSchema
|
|
1329
|
+
* @property {string} importNoEntries
|
|
1330
|
+
* @property {(max: number) => string} importTooMany
|
|
1331
|
+
* @property {string} importBadEntry
|
|
1332
|
+
* @property {(n: number) => string} imported
|
|
1271
1333
|
* @property {(verb: string) => string} unknownVerb
|
|
1272
1334
|
* @property {(message: string) => string} commandFailed
|
|
1273
1335
|
*/
|
|
1274
1336
|
const COMMAND_TEXT = /** @type {{en: CommandTextBundle, zh: CommandTextBundle}} */ ({
|
|
1275
1337
|
en: {
|
|
1276
|
-
usage: 'Usage: /memory list | query <word> | add <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1338
|
+
usage: 'Usage: /memory list | query <word> | add <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export | import <path>',
|
|
1277
1339
|
memoryEmpty: 'Memory is empty.',
|
|
1278
1340
|
entries: (total, shown) => `Memory entries (${total} total, showing first ${shown}):`,
|
|
1279
1341
|
entriesFull: (total) => `Memory entries (${total}):`,
|
|
@@ -1299,11 +1361,19 @@ const COMMAND_TEXT = /** @type {{en: CommandTextBundle, zh: CommandTextBundle}}
|
|
|
1299
1361
|
consolidateNeedsText: 'consolidate needs new text (right of =>)',
|
|
1300
1362
|
consolidated: (track, scope, removed, text, used, limit) => `Consolidated (${track}/${scope}): removed ${removed}, added 1.\nNew entry: ${text}\nLayer usage: ${used}/${limit}`,
|
|
1301
1363
|
exportUsage: 'export dumps all entries + budgets as one JSON document (read-only; redirect it to a file for backup/migration): /memory export',
|
|
1302
|
-
|
|
1364
|
+
importUsage: 'import restores entries from an export document (a file path, or inline JSON starting with {): /memory import <path> | /memory import \'{"plugin":"dsh-memento",...}\'',
|
|
1365
|
+
importBadJson: 'import: inline JSON could not be parsed',
|
|
1366
|
+
importReadFailed: (path, message) => `import: cannot read ${JSON.stringify(path)}: ${message}`,
|
|
1367
|
+
importBadSchema: (schema) => `import: not a dsh-memento export document (expected schema "${schema}")`,
|
|
1368
|
+
importNoEntries: 'import: the export document contains no entries',
|
|
1369
|
+
importTooMany: (max) => `import: the export document has more than ${max} entries; split it and import in batches`,
|
|
1370
|
+
importBadEntry: 'import: every entry needs a string track, scope, and non-empty text',
|
|
1371
|
+
imported: (n) => `Imported ${n} entries into memory (single approval; budgets re-checked). Entries get fresh ids and timestamps; proposals, audit rows and recall counts are not migrated.`,
|
|
1372
|
+
unknownVerb: (verb) => `Unknown subcommand "${verb}". Usage: /memory list | query <word> | add <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export | import <path>`,
|
|
1303
1373
|
commandFailed: (message) => `memory command failed: ${message}`,
|
|
1304
1374
|
},
|
|
1305
1375
|
zh: {
|
|
1306
|
-
usage: '用法:/memory list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1376
|
+
usage: '用法:/memory list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export | import <路径>',
|
|
1307
1377
|
memoryEmpty: '记忆为空。',
|
|
1308
1378
|
entries: (total, shown) => `记忆条目(共 ${total} 条,显示前 ${shown} 条):`,
|
|
1309
1379
|
entriesFull: (total) => `记忆条目(${total} 条):`,
|
|
@@ -1329,7 +1399,15 @@ const COMMAND_TEXT = /** @type {{en: CommandTextBundle, zh: CommandTextBundle}}
|
|
|
1329
1399
|
consolidateNeedsText: 'consolidate 需要新文本(=> 右侧)',
|
|
1330
1400
|
consolidated: (track, scope, removed, text, used, limit) => `已整合(${track}/${scope}):删除 ${removed} 条,新增 1 条。\n新条目:${text}\n该层用量:${used}/${limit}`,
|
|
1331
1401
|
exportUsage: 'export 把所有条目 + 预算导出为一份 JSON 文档(只读;可重定向到文件做备份/迁移):/memory export',
|
|
1332
|
-
|
|
1402
|
+
importUsage: 'import 从导出文档恢复条目(文件路径,或以 { 开头的内联 JSON):/memory import <路径> | /memory import \'{"plugin":"dsh-memento",...}\'',
|
|
1403
|
+
importBadJson: 'import:内联 JSON 无法解析',
|
|
1404
|
+
importReadFailed: (path, message) => `import:无法读取 ${JSON.stringify(path)}:${message}`,
|
|
1405
|
+
importBadSchema: (schema) => `import:不是 dsh-memento 导出文档(要求 schema "${schema}")`,
|
|
1406
|
+
importNoEntries: 'import:导出文档没有任何条目',
|
|
1407
|
+
importTooMany: (max) => `import:导出文档超过 ${max} 条;请拆分后分批导入`,
|
|
1408
|
+
importBadEntry: 'import:每条都需要字符串 track、scope 与非空 text',
|
|
1409
|
+
imported: (n) => `已导入 ${n} 条记忆(单次审批;预算已复检)。条目获得新 id 与新时间戳;提案、审计行与召回计数不迁移。`,
|
|
1410
|
+
unknownVerb: (verb) => `未知子命令「${verb}」。用法:/memory list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export | import <路径>`,
|
|
1333
1411
|
commandFailed: (message) => `memory 命令失败:${message}`,
|
|
1334
1412
|
},
|
|
1335
1413
|
})
|
|
@@ -1337,12 +1415,12 @@ const COMMAND_TEXT = /** @type {{en: CommandTextBundle, zh: CommandTextBundle}}
|
|
|
1337
1415
|
/** 命令注册描述与输入提示(双语)。 */
|
|
1338
1416
|
const COMMAND_DESCRIPTION = /** @type {{en: {description: string, hint: string}, zh: {description: string, hint: string}}} */ ({
|
|
1339
1417
|
en: {
|
|
1340
|
-
description: 'View/manage dsh-memento memory: list | query <word> | add [--track=user|agent] [--scope=user-global|workspace] <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1341
|
-
hint: 'list | query <word> | add <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1418
|
+
description: 'View/manage dsh-memento memory: list | query <word> | add [--track=user|agent] [--scope=user-global|workspace] <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export | import <path>',
|
|
1419
|
+
hint: 'list | query <word> | add <text> | remove <substring> | consolidate <substring...> => <new text> | proposals [approve|dismiss <id>] | budgets | audit | export | import <path>',
|
|
1342
1420
|
},
|
|
1343
1421
|
zh: {
|
|
1344
|
-
description: '查看/管理 dsh-memento 记忆:list | query <词> | add [--track=user|agent] [--scope=user-global|workspace] <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1345
|
-
hint: 'list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export',
|
|
1422
|
+
description: '查看/管理 dsh-memento 记忆:list | query <词> | add [--track=user|agent] [--scope=user-global|workspace] <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export | import <路径>',
|
|
1423
|
+
hint: 'list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export | import <路径>',
|
|
1346
1424
|
},
|
|
1347
1425
|
})
|
|
1348
1426
|
|
|
@@ -1441,7 +1519,12 @@ async function runMemoryCommand(ctx, service, invocation) {
|
|
|
1441
1519
|
{ track: proposal.track, scope: proposal.scope, text: proposal.text, source: 'proposal', workspaceKey: proposal.workspaceKey, agentKey: proposal.agentKey },
|
|
1442
1520
|
{ agent: invocation?.agent, gate: makeCommandGate(ctx, invocation) },
|
|
1443
1521
|
)
|
|
1444
|
-
|
|
1522
|
+
try {
|
|
1523
|
+
service.store.proposalDecide(id, 'approved')
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
// 并发裁决(面板/另一命令已 approve/dismiss):写已成功,别用提案状态错误掩盖它。
|
|
1526
|
+
if (!(error instanceof ProposalNotFoundError)) throw error
|
|
1527
|
+
}
|
|
1445
1528
|
return { kind: 'success', text: text.proposalApproved(proposal.track, proposal.scope, result.entry.text, result.usage.used, result.usage.limit) }
|
|
1446
1529
|
}
|
|
1447
1530
|
if (sub === 'dismiss') {
|
|
@@ -1460,7 +1543,7 @@ async function runMemoryCommand(ctx, service, invocation) {
|
|
|
1460
1543
|
const entries = service.store.listEntries()
|
|
1461
1544
|
const payload = {
|
|
1462
1545
|
plugin: 'dsh-memento',
|
|
1463
|
-
schema:
|
|
1546
|
+
schema: EXPORT_SCHEMA,
|
|
1464
1547
|
exportedAt: new Date().toISOString(),
|
|
1465
1548
|
budgets: service.budgets(),
|
|
1466
1549
|
entries: entries.map((/** @type {MemoryEntry} */ entry) => ({
|
|
@@ -1479,6 +1562,50 @@ async function runMemoryCommand(ctx, service, invocation) {
|
|
|
1479
1562
|
}
|
|
1480
1563
|
return { kind: 'success', text: JSON.stringify(payload, null, 2) }
|
|
1481
1564
|
}
|
|
1565
|
+
case 'import': {
|
|
1566
|
+
const arg = rest.join(' ').trim()
|
|
1567
|
+
if (arg.length === 0) return { kind: 'error', text: text.importUsage }
|
|
1568
|
+
let payload
|
|
1569
|
+
if (arg.startsWith('{')) {
|
|
1570
|
+
try {
|
|
1571
|
+
payload = JSON.parse(arg)
|
|
1572
|
+
} catch {
|
|
1573
|
+
return { kind: 'error', text: text.importBadJson }
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
try {
|
|
1577
|
+
payload = JSON.parse(readFileSync(arg, 'utf8'))
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1580
|
+
return { kind: 'error', text: text.importReadFailed(arg, message) }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
const shape = payload !== null && typeof payload === 'object' ? /** @type {{plugin?: unknown, schema?: unknown, entries?: unknown}} */ (payload) : undefined
|
|
1584
|
+
const valid = shape !== undefined && shape.plugin === 'dsh-memento' && shape.schema === EXPORT_SCHEMA && Array.isArray(shape.entries)
|
|
1585
|
+
if (!valid) return { kind: 'error', text: text.importBadSchema(EXPORT_SCHEMA) }
|
|
1586
|
+
const rawEntries = /** @type {unknown[]} */ (shape.entries)
|
|
1587
|
+
if (rawEntries.length === 0) return { kind: 'error', text: text.importNoEntries }
|
|
1588
|
+
if (rawEntries.length > MAX_IMPORT_ENTRIES) return { kind: 'error', text: text.importTooMany(MAX_IMPORT_ENTRIES) }
|
|
1589
|
+
const entries = []
|
|
1590
|
+
for (const raw of rawEntries) {
|
|
1591
|
+
if (raw === null || typeof raw !== 'object') return { kind: 'error', text: text.importBadEntry }
|
|
1592
|
+
const entry = /** @type {{track?: unknown, scope?: unknown, text?: unknown, source?: unknown, workspaceKey?: unknown, agentKey?: unknown}} */ (raw)
|
|
1593
|
+
if (typeof entry.track !== 'string' || typeof entry.scope !== 'string' || typeof entry.text !== 'string' || entry.text.length === 0) {
|
|
1594
|
+
return { kind: 'error', text: text.importBadEntry }
|
|
1595
|
+
}
|
|
1596
|
+
entries.push({
|
|
1597
|
+
track: entry.track,
|
|
1598
|
+
scope: entry.scope,
|
|
1599
|
+
text: entry.text,
|
|
1600
|
+
...(typeof entry.source === 'string' && entry.source.length > 0 ? { source: entry.source } : {}),
|
|
1601
|
+
...(typeof entry.workspaceKey === 'string' && entry.workspaceKey.length > 0 ? { workspaceKey: entry.workspaceKey } : {}),
|
|
1602
|
+
...(typeof entry.agentKey === 'string' && entry.agentKey.length > 0 ? { agentKey: entry.agentKey } : {}),
|
|
1603
|
+
})
|
|
1604
|
+
}
|
|
1605
|
+
// seed 单次审批 + 全量预算预检 + 单事务原子落盘;条目重获新 id 与新时间戳,召回计数归零。
|
|
1606
|
+
const result = await service.seed(entries, { agent: invocation?.agent, gate: makeCommandGate(ctx, invocation) })
|
|
1607
|
+
return { kind: 'success', text: text.imported(result.added) }
|
|
1608
|
+
}
|
|
1482
1609
|
case 'add': {
|
|
1483
1610
|
const parsed = parseCommandWrite(rest, true)
|
|
1484
1611
|
if (parsed.kind === 'error') {
|
|
@@ -1549,9 +1676,11 @@ function parseCommandWrite(/** @type {string[]} */ args, /** @type {boolean} */
|
|
|
1549
1676
|
return { kind: 'ok', track, scope, text }
|
|
1550
1677
|
}
|
|
1551
1678
|
|
|
1552
|
-
/**
|
|
1553
|
-
function renderEntryLine(/** @type {{track: string, scope: string, workspaceKey?: string, text: string}} */ entry) {
|
|
1554
|
-
|
|
1679
|
+
/** 条目渲染行(命令/面板共用格式;workspace 条目带 @工作区键,非共享 agent 条目带 #agent 键)。 */
|
|
1680
|
+
function renderEntryLine(/** @type {{track: string, scope: string, workspaceKey?: string, agentKey?: string, text: string}} */ entry) {
|
|
1681
|
+
const workspaceTag = entry.scope === 'workspace' ? ` @${entry.workspaceKey}` : ''
|
|
1682
|
+
const agentTag = typeof entry.agentKey === 'string' && entry.agentKey.length > 0 ? ` #${entry.agentKey}` : ''
|
|
1683
|
+
return `- [${entry.track}/${entry.scope}${workspaceTag}${agentTag}] ${entry.text}`
|
|
1555
1684
|
}
|
|
1556
1685
|
|
|
1557
1686
|
/**
|
|
@@ -1654,7 +1783,12 @@ export function makeMemoryRecallTool(service, ctx, recall, language = 'en') {
|
|
|
1654
1783
|
exec.signal.throwIfAborted()
|
|
1655
1784
|
const memory = service.query(
|
|
1656
1785
|
{ text: args.query, limit: args.memoryLimit ?? 10 },
|
|
1657
|
-
{
|
|
1786
|
+
{
|
|
1787
|
+
sessionId: exec.agent?.session?.id,
|
|
1788
|
+
session: exec.agent?.session,
|
|
1789
|
+
// 与 memory 工具一致:会话内召回按可见集过滤(共享 + 本 agent)。
|
|
1790
|
+
agentKey: agentKeyOf(/** @type {string | undefined} */ (exec.agent?.session?.header?.agentPreset)),
|
|
1791
|
+
},
|
|
1658
1792
|
)
|
|
1659
1793
|
const history = await recallHistory(
|
|
1660
1794
|
ctx,
|
package/lib/constants.mjs
CHANGED
|
@@ -1,58 +1,64 @@
|
|
|
1
|
-
// lib/constants.mjs — dsh-memento 词汇表与协议常量(零 DSH 依赖)。
|
|
2
|
-
//
|
|
3
|
-
// 本文件只放协议级常量(词汇、错误码、存储格式版本)。部署可调参数一律走
|
|
4
|
-
// index.mjs 的 Schemastery Config,绝不在此写死 tunable。
|
|
5
|
-
|
|
6
|
-
/** 记忆轨道(双轨分家):user=用户画像,agent=环境事实/约定/教训。 */
|
|
7
|
-
export const TRACKS = /** @type {readonly ['user', 'agent']} */ (['user', 'agent'])
|
|
8
|
-
|
|
9
|
-
/** 分层作用域:user-global 跨工作区,workspace 按会话 cwd。 */
|
|
10
|
-
export const SCOPES = /** @type {readonly ['user-global', 'workspace']} */ (['user-global', 'workspace'])
|
|
11
|
-
|
|
12
|
-
/** 写策略(Config.writePolicy):ask=用户审批,auto=放行但记录审批来源,off=拒绝。 */
|
|
13
|
-
export const WRITE_POLICIES = ['ask', 'auto', 'off']
|
|
14
|
-
|
|
15
|
-
/** 审批请求里标识本插件记忆写动作的工具名(审批 answerer 据此认领请求)。 */
|
|
16
|
-
export const TOOL_NAME = 'memory'
|
|
17
|
-
|
|
18
|
-
/** 审批 reason 前缀(含方括号):answerer 只认领带本前缀的 memory 写请求,防止误伤同工具名的其它请求。 */
|
|
19
|
-
export const REQUEST_MARKER = '[dsh-memento]'
|
|
20
|
-
|
|
21
|
-
/** 记忆库 schema 版本(单调递增;旧版逐级迁移,新版响亮拒绝)。v2 = proposals 提案表;v3 = agent_key + 召回排序列。 */
|
|
22
|
-
export const SCHEMA_VERSION = 3
|
|
23
|
-
|
|
24
|
-
/** 数据库文件名(位于 $DSH_HOME/dsh-memento/ 下)。 */
|
|
25
|
-
export const DEFAULT_DB_NAME = 'memory.db'
|
|
26
|
-
|
|
27
|
-
/** 条目默认来源标注(seed 写 source: 'claude' 时除外)。 */
|
|
28
|
-
export const DEFAULT_SOURCE = 'dsh-memento'
|
|
29
|
-
|
|
30
|
-
/** Provider 层查询返回硬上限:显式 limit 的钳制天花板(防模型/面板拉爆上下文)。 */
|
|
31
|
-
export const MAX_QUERY_LIMIT = 1000
|
|
32
|
-
|
|
33
|
-
/** 面板审计路由的返回上限:只读抽屉的审计尾展示天花板(协议常量,非部署 tunable)。 */
|
|
34
|
-
export const PANEL_AUDIT_CEILING = 200
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
export const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
// lib/constants.mjs — dsh-memento 词汇表与协议常量(零 DSH 依赖)。
|
|
2
|
+
//
|
|
3
|
+
// 本文件只放协议级常量(词汇、错误码、存储格式版本)。部署可调参数一律走
|
|
4
|
+
// index.mjs 的 Schemastery Config,绝不在此写死 tunable。
|
|
5
|
+
|
|
6
|
+
/** 记忆轨道(双轨分家):user=用户画像,agent=环境事实/约定/教训。 */
|
|
7
|
+
export const TRACKS = /** @type {readonly ['user', 'agent']} */ (['user', 'agent'])
|
|
8
|
+
|
|
9
|
+
/** 分层作用域:user-global 跨工作区,workspace 按会话 cwd。 */
|
|
10
|
+
export const SCOPES = /** @type {readonly ['user-global', 'workspace']} */ (['user-global', 'workspace'])
|
|
11
|
+
|
|
12
|
+
/** 写策略(Config.writePolicy):ask=用户审批,auto=放行但记录审批来源,off=拒绝。 */
|
|
13
|
+
export const WRITE_POLICIES = ['ask', 'auto', 'off']
|
|
14
|
+
|
|
15
|
+
/** 审批请求里标识本插件记忆写动作的工具名(审批 answerer 据此认领请求)。 */
|
|
16
|
+
export const TOOL_NAME = 'memory'
|
|
17
|
+
|
|
18
|
+
/** 审批 reason 前缀(含方括号):answerer 只认领带本前缀的 memory 写请求,防止误伤同工具名的其它请求。 */
|
|
19
|
+
export const REQUEST_MARKER = '[dsh-memento]'
|
|
20
|
+
|
|
21
|
+
/** 记忆库 schema 版本(单调递增;旧版逐级迁移,新版响亮拒绝)。v2 = proposals 提案表;v3 = agent_key + 召回排序列。 */
|
|
22
|
+
export const SCHEMA_VERSION = 3
|
|
23
|
+
|
|
24
|
+
/** 数据库文件名(位于 $DSH_HOME/dsh-memento/ 下)。 */
|
|
25
|
+
export const DEFAULT_DB_NAME = 'memory.db'
|
|
26
|
+
|
|
27
|
+
/** 条目默认来源标注(seed 写 source: 'claude' 时除外)。 */
|
|
28
|
+
export const DEFAULT_SOURCE = 'dsh-memento'
|
|
29
|
+
|
|
30
|
+
/** Provider 层查询返回硬上限:显式 limit 的钳制天花板(防模型/面板拉爆上下文)。 */
|
|
31
|
+
export const MAX_QUERY_LIMIT = 1000
|
|
32
|
+
|
|
33
|
+
/** 面板审计路由的返回上限:只读抽屉的审计尾展示天花板(协议常量,非部署 tunable)。 */
|
|
34
|
+
export const PANEL_AUDIT_CEILING = 200
|
|
35
|
+
|
|
36
|
+
/** /memory export 文档的 schema 标记(export 与 import 共用;import 只认本标记)。 */
|
|
37
|
+
export const EXPORT_SCHEMA = 'memory-export-v1'
|
|
38
|
+
|
|
39
|
+
/** /memory import 单次导入条目数上限:防一次性灌入海量条目把审批载荷撑爆(协议常量)。 */
|
|
40
|
+
export const MAX_IMPORT_ENTRIES = 1000
|
|
41
|
+
|
|
42
|
+
/** 结构化错误码:工具与面板据此分支,模型据此决定"整合后重试"还是"放弃"。 */
|
|
43
|
+
export const ERROR_CODES = {
|
|
44
|
+
DISABLED: 'MEMORY_DISABLED',
|
|
45
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
46
|
+
NO_AGENT: 'WRITE_REQUIRES_AGENT',
|
|
47
|
+
BUDGET_EXCEEDED: 'BUDGET_EXCEEDED',
|
|
48
|
+
ENTRY_NOT_FOUND: 'ENTRY_NOT_FOUND',
|
|
49
|
+
AMBIGUOUS_MATCH: 'AMBIGUOUS_MATCH',
|
|
50
|
+
WRITE_DENIED: 'WRITE_DENIED',
|
|
51
|
+
PROPOSAL_NOT_FOUND: 'PROPOSAL_NOT_FOUND',
|
|
52
|
+
STORE_CORRUPT: 'STORE_CORRUPT',
|
|
53
|
+
STORE_UNSUPPORTED_VERSION: 'STORE_UNSUPPORTED_VERSION',
|
|
54
|
+
MISSING_DSH_HOME: 'MISSING_DSH_HOME',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 会话事件名(SessionEventMap 声明合并的词汇表;运行时按已知类型自适应派发)。 */
|
|
58
|
+
export const SESSION_EVENTS = {
|
|
59
|
+
added: 'memory/added',
|
|
60
|
+
updated: 'memory/updated',
|
|
61
|
+
removed: 'memory/removed',
|
|
62
|
+
recalled: 'memory/recalled',
|
|
63
|
+
snapshot: 'memory/snapshot',
|
|
64
|
+
}
|