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/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
- if (typeof write.gate === 'function') {
300
- const outcome = await write.gate(payload, write)
301
- this.#assertOutcome(outcome)
302
- return { outcome, source: 'gate' }
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
- const via = await this.#ask({ action: 'replace', track, scope, text, source: input.source ?? this.sourceLabel }, write)
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
- const via = await this.#ask({ action: 'remove', track: input.track, scope: input.scope, text: input.match, source: target.source }, write)
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
- const plan = `${input.matches.map((match) => `remove: ${match}`).join('\n')}\nnew text: ${text}`
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
- #resolveMatch(/** @type {{match: string}} */ input, /** @type {string} */ track, /** @type {string} */ scope) {
533
- const hits = /** @type {MemoryEntry[]} */ (this.store.matchCandidates(track, scope, input.match))
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
- { sessionId: exec.agent?.session?.id, session: exec.agent?.session },
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
- 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`,
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
- unknownVerb: (verb) => `未知子命令「${verb}」。用法:/memory list | query <词> | add <文本> | remove <唯一子串> | consolidate <唯一子串...> => <新文本> | proposals [approve|dismiss <id>] | budgets | audit | export`,
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
- service.store.proposalDecide(id, 'approved')
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: 'memory-export-v1',
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
- return `- [${entry.track}/${entry.scope}${entry.scope === 'workspace' ? ` @${entry.workspaceKey}` : ''}] ${entry.text}`
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
- { sessionId: exec.agent?.session?.id, session: exec.agent?.session },
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 ERROR_CODES = {
38
- DISABLED: 'MEMORY_DISABLED',
39
- INVALID_INPUT: 'INVALID_INPUT',
40
- NO_AGENT: 'WRITE_REQUIRES_AGENT',
41
- BUDGET_EXCEEDED: 'BUDGET_EXCEEDED',
42
- ENTRY_NOT_FOUND: 'ENTRY_NOT_FOUND',
43
- AMBIGUOUS_MATCH: 'AMBIGUOUS_MATCH',
44
- WRITE_DENIED: 'WRITE_DENIED',
45
- PROPOSAL_NOT_FOUND: 'PROPOSAL_NOT_FOUND',
46
- STORE_CORRUPT: 'STORE_CORRUPT',
47
- STORE_UNSUPPORTED_VERSION: 'STORE_UNSUPPORTED_VERSION',
48
- MISSING_DSH_HOME: 'MISSING_DSH_HOME',
49
- }
50
-
51
- /** 会话事件名(SessionEventMap 声明合并的词汇表;运行时按已知类型自适应派发)。 */
52
- export const SESSION_EVENTS = {
53
- added: 'memory/added',
54
- updated: 'memory/updated',
55
- removed: 'memory/removed',
56
- recalled: 'memory/recalled',
57
- snapshot: 'memory/snapshot',
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
+ }