dsh-prime-memory 0.21.0 → 0.22.2

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.
Files changed (63) hide show
  1. package/dist/client.js +1581 -1150
  2. package/dist/config.d.ts +149 -0
  3. package/dist/config.js +35 -0
  4. package/dist/conflict-service.d.ts +8 -0
  5. package/dist/conflict-service.js +19 -0
  6. package/dist/contract.d.ts +131 -0
  7. package/dist/domain-gate.d.ts +8 -0
  8. package/dist/domain-gate.js +30 -0
  9. package/dist/graph/constraints.d.ts +4 -2
  10. package/dist/graph/constraints.js +4 -2
  11. package/dist/hooks/recall.d.ts +2 -0
  12. package/dist/hooks/recall.js +21 -2
  13. package/dist/hooks/skill-recall.d.ts +25 -0
  14. package/dist/hooks/skill-recall.js +72 -0
  15. package/dist/index.d.ts +98 -0
  16. package/dist/index.js +26 -1
  17. package/dist/memport/format.d.ts +70 -0
  18. package/dist/memport/format.js +305 -0
  19. package/dist/memport/prefilter.d.ts +62 -0
  20. package/dist/memport/prefilter.js +132 -0
  21. package/dist/memport/scan.d.ts +13 -0
  22. package/dist/memport/scan.js +100 -0
  23. package/dist/metadata-validators.d.ts +22 -0
  24. package/dist/metadata-validators.js +45 -0
  25. package/dist/pipeline/l1.js +16 -2
  26. package/dist/pipeline/ruminate.d.ts +3 -0
  27. package/dist/pipeline/ruminate.js +50 -1
  28. package/dist/pipeline/slot-migrate.d.ts +45 -0
  29. package/dist/pipeline/slot-migrate.js +45 -0
  30. package/dist/projection/recall.d.ts +74 -0
  31. package/dist/projection/recall.js +148 -0
  32. package/dist/projection/slots.d.ts +5 -0
  33. package/dist/projection/slots.js +9 -2
  34. package/dist/prompts/graph-projection.js +3 -0
  35. package/dist/prompts/l1-extraction.d.ts +4 -1
  36. package/dist/prompts/l1-extraction.js +27 -4
  37. package/dist/settings.js +106 -11
  38. package/dist/skills/crystallize.d.ts +41 -0
  39. package/dist/skills/crystallize.js +75 -0
  40. package/dist/stats.d.ts +18 -1
  41. package/dist/stats.js +184 -5
  42. package/dist/store/file-versions.d.ts +2 -0
  43. package/dist/store/file-versions.js +2 -0
  44. package/dist/store/l1.d.ts +2 -0
  45. package/dist/store/l1.js +4 -0
  46. package/dist/store/memory-sources.d.ts +59 -0
  47. package/dist/store/memory-sources.js +214 -0
  48. package/dist/store/session-recalled.d.ts +25 -0
  49. package/dist/store/session-recalled.js +163 -0
  50. package/dist/store/skill-store.d.ts +99 -0
  51. package/dist/store/skill-store.js +330 -0
  52. package/dist/store/sqlite.d.ts +5 -0
  53. package/dist/store/sqlite.js +30 -4
  54. package/dist/tools/index.d.ts +0 -13
  55. package/dist/tools/index.js +58 -7
  56. package/dist/tools/skills.d.ts +13 -0
  57. package/dist/tools/skills.js +130 -0
  58. package/dist/types.d.ts +6 -0
  59. package/dist/util/maturity.d.ts +27 -0
  60. package/dist/util/maturity.js +26 -0
  61. package/dist/wing-backfill.js +7 -4
  62. package/dsh.plugin.json +2 -2
  63. package/package.json +14 -14
package/dist/stats.js CHANGED
@@ -19,6 +19,7 @@ import { effectiveCfg } from './pipeline/runner.js';
19
19
  import { emptyRecallStats } from './hooks/recall.js';
20
20
  import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChainOrNull, resolveModelContextWindow, resolveModelEfforts, resolveModelRoute } from './llm.js';
21
21
  import { mergeRoom, renameRoom } from './room-review.js';
22
+ import { scanSources } from './memport/scan.js';
22
23
  import { projectDistillChain, validateDistillChain } from './settings.js';
23
24
  import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
24
25
  import { getTraceStore } from './store/trace.js';
@@ -33,6 +34,7 @@ import { isWingCorner } from './store/session-modes.js';
33
34
  import { startWingBackfill } from './wing-backfill.js';
34
35
  import { errDetail } from './util/filelog.js';
35
36
  import { snapshotTokenCost } from './token-cost.js';
37
+ import { computeMaturity } from './util/maturity.js';
36
38
  const require = createRequire(import.meta.url);
37
39
  export const PLUGIN_VERSION = require('../package.json').version;
38
40
  /**
@@ -93,6 +95,13 @@ export const MEMORY_ENDPOINTS = [
93
95
  'dsh-memory/cleanup-retired',
94
96
  'dsh-memory/snapshots-list',
95
97
  'dsh-memory/snapshot-restore',
98
+ 'dsh-memory/memory-sources-list',
99
+ 'dsh-memory/memory-sources-add',
100
+ 'dsh-memory/memory-sources-retire',
101
+ 'dsh-memory/memport-scan',
102
+ 'dsh-memory/skills-list',
103
+ 'dsh-memory/session-recalled',
104
+ 'dsh-memory/skills-action',
96
105
  ];
97
106
  /** HTTP 路由前缀(客户端 fetch `/dsh-memory/rpc/<短方法名>`)。 */
98
107
  const RPC_ROUTE_PREFIX = '/dsh-memory/rpc';
@@ -173,6 +182,8 @@ export function buildEndpointDeps(base, sources, controller) {
173
182
  sessionInfo: sources.sessionInfo,
174
183
  ruminate: controller,
175
184
  roomRegistry: sources.roomRegistry,
185
+ memorySources: sources.memorySources,
186
+ skillStore: sources.skillStore,
176
187
  };
177
188
  return { ...base, ...injected };
178
189
  }
@@ -180,7 +191,11 @@ export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes,
180
191
  /** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
181
192
  ruminate,
182
193
  /** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
183
- roomRegistry) {
194
+ roomRegistry,
195
+ /** 外部来源注册表(轨3 Connect;经 buildEndpointDeps 落入 deps.memorySources)。 */
196
+ memorySources,
197
+ /** 技能存储(轨2;经 buildEndpointDeps 落入 deps.skillStore)。 */
198
+ skillStore) {
184
199
  /** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
185
200
  let holding = false;
186
201
  /** 当前 handle 绑定的 connection 实例(internal/service 第二参;用于识别实例替换)。 */
@@ -201,7 +216,7 @@ roomRegistry) {
201
216
  const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger },
202
217
  // roomRegistry 必须随 sources 下发——漏带 = 面板侧 room-* 恒"未装配"
203
218
  // (工具面直传不受影响;2026-10-02 beta.9 实测回归)
204
- { status, live, modes, dataDir, rebuild, embedManager, sessionInfo, roomRegistry }, ruminate));
219
+ { status, live, modes, dataDir, rebuild, embedManager, sessionInfo, roomRegistry, memorySources, skillStore }, ruminate));
205
220
  return { ok: true, value };
206
221
  }
207
222
  catch (err) {
@@ -349,7 +364,7 @@ function sanitizeSettings(s) {
349
364
  }
350
365
  /** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
351
366
  export async function handleEndpoint(endpoint, payload, deps) {
352
- const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry } = deps;
367
+ const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry, memorySources, skillStore } = deps;
353
368
  switch (endpoint) {
354
369
  case 'dsh-memory/stats':
355
370
  return buildStats(cfg, stores, status);
@@ -386,6 +401,21 @@ export async function handleEndpoint(endpoint, payload, deps) {
386
401
  };
387
402
  return v;
388
403
  }
404
+ case 'dsh-memory/session-recalled': {
405
+ // G-live:返回本会话(按 ownerId)已注入、尚未被压缩覆盖的召回记忆。
406
+ const p = (payload ?? {});
407
+ const sessionId = expectSessionId(p.sessionId);
408
+ const ids = stores.recalled?.list(sessionId) ?? [];
409
+ if (ids.length === 0)
410
+ return { sessionId, records: [] };
411
+ const halfLife = effectiveCfg(cfg, live).recall?.decayHalfLifeDays ?? 0;
412
+ const byId = new Map(stores.l1.getByIds(ids).map((r) => [r.id, r]));
413
+ const records = ids
414
+ .map((id) => byId.get(id))
415
+ .filter((r) => r !== undefined)
416
+ .map((r) => hitToUiRecord(r, { agingWeight: agingWeightOf(r.updatedAt, halfLife), usage: stores.usage?.get(r.id) ?? null }));
417
+ return { sessionId, records };
418
+ }
389
419
  case 'dsh-memory/session-mode-set': {
390
420
  if (!modes)
391
421
  throw new Error('档位存储未初始化');
@@ -499,6 +529,150 @@ export async function handleEndpoint(endpoint, payload, deps) {
499
529
  };
500
530
  return v;
501
531
  }
532
+ // ── 轨3 外部记忆来源(Connect):登记/列表/退役/只读扫描。整面默认关(cfg.memport.enabled)。
533
+ case 'dsh-memory/memory-sources-list': {
534
+ const enabled = cfg.memport?.enabled === true;
535
+ const v = {
536
+ sources: enabled && memorySources
537
+ ? memorySources.list().map((e) => ({
538
+ id: e.id,
539
+ kind: e.kind,
540
+ path: e.path,
541
+ label: e.label,
542
+ lastScanAt: e.lastScanAt,
543
+ lastImportAt: e.lastImportAt,
544
+ disabled: e.disabled,
545
+ status: e.status,
546
+ }))
547
+ : [],
548
+ memoryMutate: live?.get().memoryMutate === true,
549
+ };
550
+ return v;
551
+ }
552
+ case 'dsh-memory/memory-sources-add': {
553
+ if (cfg.memport?.enabled !== true)
554
+ throw new Error('外部记忆来源未启用(config memport.enabled)。');
555
+ if (!memorySources)
556
+ throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
557
+ if (!live?.get().memoryMutate)
558
+ throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能登记外部来源。');
559
+ const p = (payload ?? {});
560
+ const r = await memorySources.register({
561
+ id: String(p.id ?? '').trim(),
562
+ kind: String(p.kind ?? 'file') ?? 'file',
563
+ path: String(p.path ?? '').trim(),
564
+ label: typeof p.label === 'string' ? p.label : undefined,
565
+ });
566
+ const v = {
567
+ id: r.entry.id,
568
+ created: r.created,
569
+ notice: r.created ? '已登记来源。' : '来源已存在(已按新 path/label 原地更新)。',
570
+ };
571
+ return v;
572
+ }
573
+ case 'dsh-memory/memory-sources-retire': {
574
+ if (cfg.memport?.enabled !== true)
575
+ throw new Error('外部记忆来源未启用(config memport.enabled)。');
576
+ if (!memorySources)
577
+ throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
578
+ if (!live?.get().memoryMutate)
579
+ throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能管理外部来源。');
580
+ const p = (payload ?? {});
581
+ const changed = await memorySources.setStatus(String(p.id ?? '').trim(), p.active === true ? 'active' : 'retired');
582
+ const v = {
583
+ changed,
584
+ notice: !changed
585
+ ? '未找到该来源。'
586
+ : p.active === true
587
+ ? '已恢复来源。'
588
+ : '已退役来源(不删已导入数据;来源路径与 ext_key 仍在,可重放)。',
589
+ };
590
+ return v;
591
+ }
592
+ case 'dsh-memory/memport-scan': {
593
+ if (cfg.memport?.enabled !== true) {
594
+ const v = { enabled: false, sources: [], notice: '外部记忆扫描未启用(config memport.enabled)。' };
595
+ return v;
596
+ }
597
+ if (!memorySources) {
598
+ const v = { enabled: true, sources: [], notice: '外部来源注册表未装配。' };
599
+ return v;
600
+ }
601
+ const p = (payload ?? {});
602
+ const all = memorySources.listEnabled();
603
+ const targets = p.sourceId ? all.filter((e) => e.id === p.sourceId) : all;
604
+ const maxRecords = cfg.memport?.scanMaxRecords ?? 5000;
605
+ // 只读扫描:不写库(写走 memory_import)。逐个标扫描时间(失败不影响预览)。
606
+ const sources = await scanSources({ sources: targets, l1: stores.l1, maxRecords });
607
+ const nowIso = new Date().toISOString();
608
+ for (const s of targets)
609
+ await memorySources.markScanned(s.id, nowIso);
610
+ const v = { enabled: true, sources };
611
+ return v;
612
+ }
613
+ // ── 轨2 技能(SkillsTab):列表 + 人工审阅动作(晋升/归档/pin/改档)。整面默认关(cfg.skills.enabled)。
614
+ case 'dsh-memory/skills-list': {
615
+ const enabled = cfg.skills?.enabled === true || live?.get().skillsEnabled === true;
616
+ const v = {
617
+ enabled,
618
+ allowPromote: cfg.skills?.allowPromote === true,
619
+ memoryMutate: live?.get().memoryMutate === true,
620
+ skills: enabled && skillStore
621
+ ? skillStore.list().map((s) => ({
622
+ id: s.id,
623
+ title: s.title,
624
+ status: s.status,
625
+ tier: s.injectionTier,
626
+ pinned: s.pinned,
627
+ sessionCount: skillStore.sessionCount(s.id),
628
+ updatedAt: s.updatedAt,
629
+ }))
630
+ : [],
631
+ };
632
+ return v;
633
+ }
634
+ case 'dsh-memory/skills-action': {
635
+ if (!(cfg.skills?.enabled === true || live?.get().skillsEnabled === true))
636
+ throw new Error('技能功能未启用(在概览页开启「技能」开关,或 config skills.enabled)。');
637
+ if (!skillStore)
638
+ throw new Error('技能存储未装配。');
639
+ if (!live?.get().memoryMutate)
640
+ throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能管理技能。');
641
+ const p = (payload ?? {});
642
+ const id = String(p.id ?? '').trim();
643
+ if (!id)
644
+ throw new Error('id 不能为空');
645
+ const minSessions = cfg.skills.minSessions;
646
+ const v = { changed: false, notice: '' };
647
+ if (p.action === 'promote') {
648
+ // 人工审阅晋升:不受自动闸 allowPromote 约束,但仍需证据达阈(不凭空的晋升)。
649
+ if (skillStore.sessionCount(id) < minSessions)
650
+ return { changed: false, notice: `证据会话数不足(<${minSessions}),暂不能晋升。` };
651
+ const ok = await skillStore.tryPromote(id, minSessions, { allowPromote: true });
652
+ return { changed: ok, notice: ok ? '已晋升为 active。' : '未找到候选或状态已变。' };
653
+ }
654
+ if (p.action === 'archive') {
655
+ const ok = await skillStore.archive(id, String(p.reason ?? 'manual').slice(0, 120) || 'manual');
656
+ return { changed: ok, notice: ok ? '已归档(不再注入,可恢复)。' : '未找到或已归档。' };
657
+ }
658
+ if (p.action === 'pin') {
659
+ const ok = await skillStore.update(id, { pinned: true });
660
+ return { changed: ok, notice: ok ? '已固定。' : '未找到技能。' };
661
+ }
662
+ if (p.action === 'unpin') {
663
+ const ok = await skillStore.update(id, { pinned: false });
664
+ return { changed: ok, notice: ok ? '已取消固定。' : '未找到技能。' };
665
+ }
666
+ if (p.action === 'set-tier') {
667
+ const tier = p.tier === 'full' || p.tier === 'excerpt' || p.tier === 'hint' ? p.tier : null;
668
+ if (!tier)
669
+ throw new Error('tier 非法(仅 full/excerpt/hint)');
670
+ const ok = await skillStore.update(id, { injectionTier: tier });
671
+ return { changed: ok, notice: ok ? `已设为 ${tier} 档。` : '未找到技能。' };
672
+ }
673
+ void v;
674
+ throw new Error(`未知 action: ${String(p.action)}`);
675
+ }
502
676
  case 'dsh-memory/room-register': {
503
677
  if (!roomRegistry)
504
678
  throw new Error('Room 注册表未装配(需要插件重载以初始化)。');
@@ -812,6 +986,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
812
986
  embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
813
987
  memoryMutate: false,
814
988
  conflictFreeze: live?.get()?.conflictFreeze === true,
989
+ skillsEnabled: live?.get()?.skillsEnabled === true,
815
990
  }),
816
991
  // 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
817
992
  ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
@@ -850,7 +1025,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
850
1025
  const patch = (payload ?? {});
851
1026
  const clean = {};
852
1027
  // 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
853
- for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
1028
+ for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
854
1029
  if (typeof patch[key] === 'boolean')
855
1030
  clean[key] = patch[key];
856
1031
  }
@@ -1107,7 +1282,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
1107
1282
  // (live.conflictFreeze 覆盖静态 cfg.conflictFreeze.enabled)。此前这里直接读
1108
1283
  // cfg.conflictFreeze.enabled —— 面板开关写的是 live,而部署静态值恒 false,
1109
1284
  // 于是开关已开、settings.yaml 已落 true,本页仍报"矛盾冻结未开启"。
1110
- return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
1285
+ return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true, usage: stores.usage, decayHalfLifeDays: effectiveCfg(cfg, live).recall?.decayHalfLifeDays }, { limit: Number(p.limit) || undefined });
1111
1286
  }
1112
1287
  // ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
1113
1288
  // 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
@@ -1714,6 +1889,10 @@ export function hitToUiRecord(r, extra) {
1714
1889
  retiredReason: retired ? (mark?.reason ?? 'unknown') : null,
1715
1890
  agingWeight: extra?.agingWeight ?? null,
1716
1891
  usage: extra?.usage ?? null,
1892
+ // G3 成熟度:读时派生(priority + used + 老化),无额外端点/存储。
1893
+ maturity: computeMaturity({ priority: r.priority ?? 60, usage: extra?.usage ?? null, agingWeight: extra?.agingWeight ?? null }),
1894
+ // G1 使用场景:从命名空间保留键读出(功能关/无键 → undefined)。
1895
+ usageScenario: Array.isArray(r.metadata?.dsh_usage_scenario) ? r.metadata?.dsh_usage_scenario : undefined,
1717
1896
  };
1718
1897
  }
1719
1898
  /**
@@ -21,6 +21,8 @@ export declare const SESSION_MODES_FILE_VERSION = 1;
21
21
  export declare const OCCUPANCY_FILE_VERSION = 1;
22
22
  /** `recall-dedupe.json`。 */
23
23
  export declare const RECALL_DEDUPE_FILE_VERSION = 1;
24
+ /** `session-recalled.json`(G-live 活召回集,持久化以令旧会话重开可读)。 */
25
+ export declare const SESSION_RECALLED_FILE_VERSION = 1;
24
26
  /** `pending.json`。 */
25
27
  export declare const PENDING_FILE_VERSION = 1;
26
28
  /** `reconcile-state.json`。 */
@@ -21,6 +21,8 @@ export const SESSION_MODES_FILE_VERSION = 1;
21
21
  export const OCCUPANCY_FILE_VERSION = 1;
22
22
  /** `recall-dedupe.json`。 */
23
23
  export const RECALL_DEDUPE_FILE_VERSION = 1;
24
+ /** `session-recalled.json`(G-live 活召回集,持久化以令旧会话重开可读)。 */
25
+ export const SESSION_RECALLED_FILE_VERSION = 1;
24
26
  /** `pending.json`。 */
25
27
  export const PENDING_FILE_VERSION = 1;
26
28
  /** `reconcile-state.json`。 */
@@ -142,6 +142,8 @@ export declare class L1Store {
142
142
  };
143
143
  /** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
144
144
  getByIds(ids: string[]): MemoryRecord[];
145
+ /** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
146
+ findIdByExtKey(extKey: string): string | undefined;
145
147
  /**
146
148
  * §B 决策凭证落盘(L1Store 的薄缝)。
147
149
  * 刻意放在 store 上:`runExtraction` 已经持有 L1Store,凭证写入因此无需新增
package/dist/store/l1.js CHANGED
@@ -229,6 +229,10 @@ export class L1Store {
229
229
  getByIds(ids) {
230
230
  return this.db.getL1ByIds(ids);
231
231
  }
232
+ /** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
233
+ findIdByExtKey(extKey) {
234
+ return this.db.findRecordIdByExtKey(extKey);
235
+ }
232
236
  /**
233
237
  * §B 决策凭证落盘(L1Store 的薄缝)。
234
238
  * 刻意放在 store 上:`runExtraction` 已经持有 L1Store,凭证写入因此无需新增
@@ -0,0 +1,59 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ /** sidecar 版本常量(file-versions 同款集中纪律)。 */
3
+ export declare const MEMORY_SOURCES_FILE_VERSION: 1;
4
+ /** 已知外部 agent 来源类别(与 memport foreign-agent.md 对齐);'file' = 任意路径。 */
5
+ export declare const SOURCE_KINDS: readonly ["trae", "qoder", "codex", "claude", "workbuddy", "golutra", "cursor", "file"];
6
+ export type SourceKind = (typeof SOURCE_KINDS)[number];
7
+ export interface MemorySourceEntry {
8
+ /** 稳定 id(小写字母数字连字符);扫描/导入按此归类、支持按来源撤销。 */
9
+ id: string;
10
+ kind: SourceKind;
11
+ /** 路径指针(绝对路径/目录),正文永不复制进来。 */
12
+ path: string;
13
+ label?: string;
14
+ /** 最近一次扫描时间(ISO);未扫为空。 */
15
+ lastScanAt: string;
16
+ /** 最近一次导入时间(ISO);未导入为空。 */
17
+ lastImportAt: string;
18
+ /** 用户临时停用(保留条目但不参与自动扫描;≠ 删除)。 */
19
+ disabled: boolean;
20
+ status: 'active' | 'retired';
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ }
24
+ export type MemorySourceInput = Pick<MemorySourceEntry, 'id' | 'kind' | 'path'> & Partial<Pick<MemorySourceEntry, 'label'>>;
25
+ export declare class MemorySourceStore {
26
+ private readonly logger?;
27
+ private readonly file;
28
+ private entries;
29
+ private rev;
30
+ private loaded;
31
+ private degraded;
32
+ private degradedLogged;
33
+ private writeChain;
34
+ constructor(dataDir: string, logger?: MemoryLogger | undefined);
35
+ /** 载入(启动 await 一次)。缺失 = 合法空表;损坏 = 只读降级(回退"无来源")。 */
36
+ init(): Promise<void>;
37
+ private ensureLoaded;
38
+ /** 全量条目(含 retired;副本返回,活引用纪律)。 */
39
+ list(): MemorySourceEntry[];
40
+ /** active 且未停用的来源(扫描/导入遍历用)。 */
41
+ listEnabled(): MemorySourceEntry[];
42
+ byId(id: string): MemorySourceEntry | undefined;
43
+ /** 登记来源(幂等:同 id 已存在则原地补全 kind/path/label,不新建)。上限防刷。 */
44
+ register(input: MemorySourceInput): Promise<{
45
+ entry: MemorySourceEntry;
46
+ created: boolean;
47
+ }>;
48
+ /** 记一次扫描时间。 */
49
+ markScanned(id: string, at: string): Promise<boolean>;
50
+ /** 记一次导入时间。 */
51
+ markImported(id: string, at: string): Promise<boolean>;
52
+ /** 启用/停用(停用保留条目、只是不参与自动扫描)。 */
53
+ setDisabled(id: string, disabled: boolean): Promise<boolean>;
54
+ /** 退役/恢复(status 迁移)。 */
55
+ setStatus(id: string, status: 'active' | 'retired'): Promise<boolean>;
56
+ private patch;
57
+ /** 写穿持久化(串行链;失败降级内存态并告警一次,绝不抛出阻塞调用方)。 */
58
+ private persist;
59
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * 外部记忆来源注册表(轨3 Connect 路径的一等实体)。
3
+ *
4
+ * 动机:此前"其他 agent → 记忆库"的取内容路径只装配到 `memory_import` 这最后一寸,
5
+ * 前面十一寸(发现来源、记录来源、按来源管理)全外包给 skill 文本 + 用户手填路径。
6
+ * 本注册表把"来源"升为可持久、可枚举、可按来源撤销的一等实体。
7
+ *
8
+ * **路径指针不复制**(Connect 语义,援引 `slots.refs` 先例):每条只存 `path` + 摘要,
9
+ * 正文**永不落进插件目录**——既是"移动房子家具先消毒"的卫生边界,也让带 ext_key 的
10
+ * 外部记忆"来源仍在、可重放"(可重建性显式化)。
11
+ *
12
+ * 存储:rooms-registry 同款 sidecar(`memory-sources.json`),全量载入内存 + 写穿原子持久化,
13
+ * `rev` 乐观计数。文件缺失 = 合法空表;损坏/不可读 → 告警 + 只读降级(内存态照常、停止回写),
14
+ * **绝不阻塞扫描/导入/面板**(降级即回退"无注册来源",非破坏性)。上限防刷。
15
+ */
16
+ import * as path from 'node:path';
17
+ import { readJsonStrict, atomicWriteText, ensureDir } from '../util/io.js';
18
+ /** sidecar 版本常量(file-versions 同款集中纪律)。 */
19
+ export const MEMORY_SOURCES_FILE_VERSION = 1;
20
+ /** 已知外部 agent 来源类别(与 memport foreign-agent.md 对齐);'file' = 任意路径。 */
21
+ export const SOURCE_KINDS = ['trae', 'qoder', 'codex', 'claude', 'workbuddy', 'golutra', 'cursor', 'file'];
22
+ /** 来源上限(防刷;一个工作区通常 <20 个外部工具记忆落点)。 */
23
+ const SOURCES_CAP = 100;
24
+ const PATH_MAX = 500;
25
+ const LABEL_MAX = 80;
26
+ function isSourceId(s) {
27
+ return /^[a-z0-9][a-z0-9-]{0,63}$/.test(s);
28
+ }
29
+ function normKind(k) {
30
+ return SOURCE_KINDS.includes(String(k)) ? k : 'file';
31
+ }
32
+ export class MemorySourceStore {
33
+ logger;
34
+ file;
35
+ entries = [];
36
+ rev = 0;
37
+ loaded = false;
38
+ degraded;
39
+ degradedLogged = false;
40
+ writeChain = Promise.resolve();
41
+ constructor(dataDir, logger) {
42
+ this.logger = logger;
43
+ this.file = path.join(dataDir, 'memory-sources.json');
44
+ }
45
+ /** 载入(启动 await 一次)。缺失 = 合法空表;损坏 = 只读降级(回退"无来源")。 */
46
+ async init() {
47
+ await ensureDir(path.dirname(this.file));
48
+ const read = await readJsonStrict(this.file);
49
+ if (!read.ok) {
50
+ if (read.reason !== 'missing') {
51
+ this.degraded = `${read.reason}${'detail' in read && read.detail ? `: ${read.detail}` : ''}`;
52
+ this.logger?.warn(`[memory] 外部来源注册表只读降级: ${this.degraded}`);
53
+ }
54
+ this.loaded = true;
55
+ return;
56
+ }
57
+ if (read.value.version !== MEMORY_SOURCES_FILE_VERSION) {
58
+ this.logger?.warn(`[memory] 外部来源注册表版本 ${String(read.value.version)} 未知,按当前形状读取`);
59
+ }
60
+ const sources = Array.isArray(read.value.sources) ? read.value.sources : [];
61
+ for (const s of sources) {
62
+ if (!s || typeof s.id !== 'string' || !isSourceId(s.id))
63
+ continue; // 非法 id 丢弃
64
+ if (typeof s.path !== 'string' || !s.path)
65
+ continue; // 无路径指针的来源无意义,丢弃
66
+ this.entries.push({
67
+ id: s.id,
68
+ kind: normKind(s.kind),
69
+ path: s.path.slice(0, PATH_MAX),
70
+ label: typeof s.label === 'string' ? s.label : undefined,
71
+ lastScanAt: typeof s.lastScanAt === 'string' ? s.lastScanAt : '',
72
+ lastImportAt: typeof s.lastImportAt === 'string' ? s.lastImportAt : '',
73
+ disabled: s.disabled === true,
74
+ status: s.status === 'retired' ? 'retired' : 'active',
75
+ createdAt: typeof s.createdAt === 'string' ? s.createdAt : '',
76
+ updatedAt: typeof s.updatedAt === 'string' ? s.updatedAt : '',
77
+ });
78
+ }
79
+ this.rev = typeof read.value.rev === 'number' ? read.value.rev : 0;
80
+ this.loaded = true;
81
+ }
82
+ ensureLoaded() {
83
+ if (!this.loaded)
84
+ throw new Error('外部来源注册表未初始化(先 await init())');
85
+ }
86
+ /** 全量条目(含 retired;副本返回,活引用纪律)。 */
87
+ list() {
88
+ this.ensureLoaded();
89
+ return this.entries.map((e) => ({ ...e }));
90
+ }
91
+ /** active 且未停用的来源(扫描/导入遍历用)。 */
92
+ listEnabled() {
93
+ return this.list().filter((e) => e.status === 'active' && !e.disabled);
94
+ }
95
+ byId(id) {
96
+ this.ensureLoaded();
97
+ const found = this.entries.find((e) => e.id === id);
98
+ return found ? { ...found } : undefined;
99
+ }
100
+ /** 登记来源(幂等:同 id 已存在则原地补全 kind/path/label,不新建)。上限防刷。 */
101
+ async register(input) {
102
+ this.ensureLoaded();
103
+ const id = String(input.id ?? '').trim().toLowerCase();
104
+ if (!isSourceId(id))
105
+ throw new Error(`非法来源 id: ${String(input.id)}(小写字母数字连字符,≤64)`);
106
+ const p = String(input.path ?? '').trim();
107
+ if (!p)
108
+ throw new Error('来源 path 不能为空(路径指针不复制,但必须指向真实落点)');
109
+ if (p.length > PATH_MAX)
110
+ throw new Error(`来源 path 过长(>${PATH_MAX})`);
111
+ const found = this.entries.find((e) => e.id === id);
112
+ if (found) {
113
+ let changed = false;
114
+ const kind = normKind(input.kind);
115
+ if (found.kind !== kind) {
116
+ found.kind = kind;
117
+ changed = true;
118
+ }
119
+ if (found.path !== p) {
120
+ found.path = p;
121
+ changed = true;
122
+ }
123
+ if (typeof input.label === 'string' && input.label.trim() && found.label !== input.label.trim().slice(0, LABEL_MAX)) {
124
+ found.label = input.label.trim().slice(0, LABEL_MAX);
125
+ changed = true;
126
+ }
127
+ if (found.status === 'retired') {
128
+ found.status = 'active';
129
+ changed = true;
130
+ }
131
+ if (changed) {
132
+ found.updatedAt = new Date().toISOString();
133
+ this.rev++;
134
+ await this.persist();
135
+ }
136
+ return { entry: { ...found }, created: false };
137
+ }
138
+ if (this.entries.length >= SOURCES_CAP)
139
+ throw new Error(`外部来源已达上限(${SOURCES_CAP}),请先退役部分来源`);
140
+ const now = new Date().toISOString();
141
+ const entry = {
142
+ id,
143
+ kind: normKind(input.kind),
144
+ path: p.slice(0, PATH_MAX),
145
+ label: typeof input.label === 'string' && input.label.trim() ? input.label.trim().slice(0, LABEL_MAX) : undefined,
146
+ lastScanAt: '',
147
+ lastImportAt: '',
148
+ disabled: false,
149
+ status: 'active',
150
+ createdAt: now,
151
+ updatedAt: now,
152
+ };
153
+ this.entries.push(entry);
154
+ this.rev++;
155
+ await this.persist();
156
+ return { entry: { ...entry }, created: true };
157
+ }
158
+ /** 记一次扫描时间。 */
159
+ async markScanned(id, at) {
160
+ return this.patch(id, (e) => {
161
+ e.lastScanAt = at;
162
+ });
163
+ }
164
+ /** 记一次导入时间。 */
165
+ async markImported(id, at) {
166
+ return this.patch(id, (e) => {
167
+ e.lastImportAt = at;
168
+ });
169
+ }
170
+ /** 启用/停用(停用保留条目、只是不参与自动扫描)。 */
171
+ async setDisabled(id, disabled) {
172
+ return this.patch(id, (e) => {
173
+ e.disabled = disabled;
174
+ });
175
+ }
176
+ /** 退役/恢复(status 迁移)。 */
177
+ async setStatus(id, status) {
178
+ return this.patch(id, (e) => {
179
+ e.status = status;
180
+ });
181
+ }
182
+ async patch(id, mut) {
183
+ this.ensureLoaded();
184
+ const found = this.entries.find((e) => e.id === id);
185
+ if (!found)
186
+ return false;
187
+ mut(found);
188
+ found.updatedAt = new Date().toISOString();
189
+ this.rev++;
190
+ await this.persist();
191
+ return true;
192
+ }
193
+ /** 写穿持久化(串行链;失败降级内存态并告警一次,绝不抛出阻塞调用方)。 */
194
+ async persist() {
195
+ if (this.degraded)
196
+ return;
197
+ this.writeChain = this.writeChain.then(async () => {
198
+ const data = { version: MEMORY_SOURCES_FILE_VERSION, rev: this.rev + 1, sources: this.entries };
199
+ try {
200
+ await ensureDir(path.dirname(this.file));
201
+ await atomicWriteText(this.file, JSON.stringify(data, null, 2));
202
+ this.rev = data.rev;
203
+ }
204
+ catch (err) {
205
+ this.degraded = err instanceof Error ? err.message : String(err);
206
+ if (!this.degradedLogged) {
207
+ this.degradedLogged = true;
208
+ this.logger?.warn(`[memory] 外部来源注册表写入失败,只读降级: ${this.degraded}`);
209
+ }
210
+ }
211
+ });
212
+ await this.writeChain;
213
+ }
214
+ }
@@ -0,0 +1,25 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ export declare class SessionRecalledStore {
3
+ private readonly logger?;
4
+ private readonly file;
5
+ private readonly entries;
6
+ private degraded;
7
+ private degradedLogged;
8
+ private lastPersisted;
9
+ private writeChain;
10
+ /** 脏检序号:每次真实变更 +1,投影 apply 据此 O(1) 判断是否重建。 */
11
+ private rev;
12
+ constructor(dataDir: string, logger?: MemoryLogger | undefined);
13
+ revision(): number;
14
+ private init;
15
+ /** 记入本 sessionKey 本轮实际注入的记录 id(写穿;插入序淘汰最旧)。 */
16
+ add(sessionKey: string, recordIds: readonly string[]): void;
17
+ list(sessionKey: string): string[];
18
+ /** 压缩/清空:注入内容已离开上下文,清该会话活召回集(写穿)。 */
19
+ reset(sessionKey: string): void;
20
+ size(): number;
21
+ /** 等待在途持久化写完成(测试/停机)。 */
22
+ flush(): Promise<void>;
23
+ private persist;
24
+ private serialize;
25
+ }