dsh-prime-memory 0.26.5 → 0.26.9

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 (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +62 -1
  3. package/README.md +60 -2
  4. package/dist/client.js +45 -5
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +33 -3
  7. package/dist/contract.d.ts +14 -1
  8. package/dist/embedding-child.cjs +192 -0
  9. package/dist/embedding-worker.cjs +2 -0
  10. package/dist/graph/apply.d.ts +9 -0
  11. package/dist/graph/apply.js +12 -2
  12. package/dist/graph/search.d.ts +11 -2
  13. package/dist/graph/search.js +38 -10
  14. package/dist/hooks/recall.js +28 -1
  15. package/dist/index.js +14 -1
  16. package/dist/llm.d.ts +21 -2
  17. package/dist/llm.js +56 -25
  18. package/dist/pipeline/graph.d.ts +17 -1
  19. package/dist/pipeline/graph.js +45 -2
  20. package/dist/pipeline/l1.d.ts +15 -2
  21. package/dist/pipeline/l1.js +161 -47
  22. package/dist/pipeline/l2.js +72 -50
  23. package/dist/pipeline/md-sync.js +4 -2
  24. package/dist/pipeline/ruminate.js +2 -0
  25. package/dist/pipeline/runner.d.ts +32 -0
  26. package/dist/pipeline/runner.js +109 -11
  27. package/dist/pipeline/slot-migrate.d.ts +3 -2
  28. package/dist/pipeline/slot-migrate.js +3 -2
  29. package/dist/projection/recall.d.ts +10 -3
  30. package/dist/projection/recall.js +14 -0
  31. package/dist/settings.js +6 -0
  32. package/dist/stats.d.ts +1 -1
  33. package/dist/stats.js +35 -14
  34. package/dist/store/embedding-source.d.ts +4 -2
  35. package/dist/store/embedding-source.js +5 -2
  36. package/dist/store/file-versions.d.ts +2 -0
  37. package/dist/store/file-versions.js +2 -0
  38. package/dist/store/graph-store.d.ts +71 -1
  39. package/dist/store/graph-store.js +221 -8
  40. package/dist/store/l1.d.ts +5 -0
  41. package/dist/store/l1.js +18 -2
  42. package/dist/store/local-embedding.d.ts +9 -5
  43. package/dist/store/local-embedding.js +137 -17
  44. package/dist/store/memory-md-disable.d.ts +3 -2
  45. package/dist/store/memory-md.d.ts +3 -3
  46. package/dist/store/memory-md.js +6 -4
  47. package/dist/store/route-health.d.ts +38 -0
  48. package/dist/store/route-health.js +115 -0
  49. package/dist/store/runtime-installer.d.ts +13 -1
  50. package/dist/store/runtime-installer.js +40 -2
  51. package/dist/store/sqlite.d.ts +24 -12
  52. package/dist/store/sqlite.js +97 -31
  53. package/dist/store/supersede.d.ts +10 -2
  54. package/dist/store/supersede.js +10 -2
  55. package/dist/store/validity.d.ts +150 -0
  56. package/dist/store/validity.js +209 -0
  57. package/dist/tools/index.d.ts +15 -1
  58. package/dist/tools/index.js +393 -11
  59. package/dist/util/json-repair.d.ts +48 -0
  60. package/dist/util/json-repair.js +285 -0
  61. package/dist/validity-backfill.d.ts +78 -0
  62. package/dist/validity-backfill.js +160 -0
  63. package/dsh.plugin.json +2 -2
  64. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # 更新日志(Changelog)
2
2
 
3
+ ## [0.26.6] — 2026-10-22
4
+
5
+ ### 新增
6
+
7
+ - **图谱确定性写面(计划 graph-write-plane)**:写面从"add/delete/import"补齐"改、重投、迁"三语义。
8
+ - `memory_update`(高权限):修订一条既有记忆——就地产新版本(version+1,新 id),旧版经退场汇流口
9
+ `superseded` 软删(可恢复),随后对 [旧,新] 强制重投让图谱边跟随;extKey 不随血缘走(幂等索引由旧行占位)。
10
+ - `memory_reproject`(需 `graph.enabled`):抹掉"已按当前版本投影"凭证、按高优先级重新入队——绕开
11
+ `projector_version` 永久跳过;幂等(在途任务不堆重)。手加/导入的记忆长期不出现在图谱里的纠正通道。
12
+ - `memory_migrate_relation`(高权限):源导式关系迁移——硬校验来源全部"现存未退场"(判据单源
13
+ `classifyValidity`)→ 显式边提案经 `applyGraphProjection` 纯函数校验落库(同 (起点,关系) 换目标自动闭旧边
14
+ 出新边)→ 新增承载新关系的 L1 源记录留可回溯根据(metadata `proposal_source='tool'` + 决策凭证)。
15
+ 全程不提供、也不会提供绕过投影直写 `graph_*` 表的入口(零无源事实不变量)。
16
+ - **非蒸馏写入自动入投影队**:`memory_add`/`memory_import` 与蒸馏产出同优先级(`GRAPH_PRIORITY_NEW`)进队——
17
+ 闭合"只有蒸馏才投影"的旁路缺口(手加记录 projected=None 的真根因)。图谱向量路缺陷同步修复:
18
+ 嵌入启用时投影收尾为落库的 active 节点补写 `graph_node_vec`(旧版本该调用生产零落点、全库 0 行)。
19
+ - **记忆时效三档与工具反查(计划 expiry-tiering,ADR-0022)**:退场方式/有效性定性/持续性/现实有效期
20
+ 四轴正交,六态(current/historical/expired/expired_suspect/superseded/retired)判据单源 `classifyValidity`(查询期谓词,
21
+ 不落库翻态);`dsh_validity` metadata 标注落写侧汇流口(证据链 activity_*→evidence、工具直填→llm_inferred、
22
+ 兜底→unknown)。门2 灰度闸 `validity.tieringEnabled`(默认关):开时被动注入剔 `expired`、`expired_suspect`
23
+ 保留带"疑似失效"标注,召回卡显示 `validity`/`validUntil`。门3:`memory_search` 新增 `include_expired` /
24
+ `as_of`(有效期回放,不复活退场)/ `follow_supersede`(顺/逆取代链)三参数,全缺省时输出与改前逐字一致。
25
+ 存量标注由零 LLM 的 `validity-backfill` 低频补齐(启动 3 分钟首轮、6h 续跑;改过的记录顺手重投图谱保鲜)。
26
+
27
+ ### 修复
28
+
29
+ - **门1 解耦:合法历史 valid_to 不再被静默遮蔽**(bugfix,不受灰度门控):退场判据全仓改为只认取代标记
30
+ (`retiredL1Sql`/`isRetired`/md 投射层同口径一对);`retire()` 不再闭合 valid_to,它回归纯现实有效期。
31
+ 升版后首启自动做一次 FTS 全量回灌收敛误遮蔽历史(`gate1.retire-decoupled.v1` 戳幂等,日志报回灌/跳过数;
32
+ 向量面由增量重嵌自动补齐或设置页手动重建加速);面板"已退场"区/删除线/计数随之只认真退场行。
33
+ - **退场→图谱退役传播补腿(R9)**:软删/取代后按"来源是否全部退场态"重算清扫——全退场的 active/disputed
34
+ 节点·边标 archived 墓碑(保留行 expand 可见),来源全部复活(restore)则拉回 active;多源支撑的节点不误杀。
35
+ - **md 同步退场收敛**:`.disable` 回灌的"已退场"判据换 marker 单源,修复同批翻转波及的"第二轮重复计数、
36
+ 永不收敛 same"。
37
+
3
38
  ## 0.26.2 (2026-10-18)
4
39
 
5
40
  - hosts: 追加 0.2.1-rc.2 声明面枚举(34/34 @deepseek-ai/* 宿主 peer 逐包 npm 实测 rc.2 在版;本插件不触 ctx.web provider 与 conversation.input.model.label 槽,代码零改动,纯元数据发版;农场 rc.2 格挂账待命)
@@ -22,6 +57,10 @@
22
57
  - **撤下浮动球 UI(`client/src/longtask-fab.ts`;beta.1 起曾随包发布)**:悬浮建议面板的归属本在 context-compression 侧(「建议启动压缩」是其领域),pm 属误载——2026-10-02 用户裁定撤下。撤下不违座位契约(settings.section 恰一个 + 输入栏 pill,body 单例不占座位),但全应用级 overlay 会与其他插件的悬浮 UI 抢占屏幕角落;长任务开关与手动压缩入口待长任务定版时落记忆设置分节。
23
58
  - **`session-mode-set` 的 `mode` 转为可选**:与 `longTask` 二选一必传——纯切长任务开关不再被迫发送档位(旧客户端恒传 mode,行为不变)。响应 `mode` 改回当前生效值。
24
59
 
60
+ ### 修复
61
+
62
+ - **嵌入推理子进程隔离,根治 libvips DLL 撞名(`fix #11`)**:旧路径把 transformers 推理放在 worker_threads 子线程,与宿主 Electron 同进程——插件 runtime 的 sharp/libvips 与宿主 sharp/libvips 同名不同路径时在 OS LoadLibrary 层冲突(切大模型 bge-small→bge-m3 时崩溃)。现在默认改走 `child_process.fork()` 独立子进程(`resources/embedding-child.cjs`,主线程侧 `RealChildChannel` 协议代理),DLL 表物理隔离;配置经 env `DSH_EMBED_CFG` 传递,向量经 number[][] 序列化回传(512d×32 条 ≈64KB,<5ms,实测 batch32 含序列化 91ms、单条中位数 5ms),零拷贝 transfer 降级为可忽略开销。旧 `RealWorkerChannel` 与 `embedding-worker.cjs` 保留一版作回滚逃生门(`opts.useWorker=true`,下版删);`RuntimeInstaller.isReady()` 兼验 sharp 平台原生件就位(--ignore-scripts 半装状态检出即触发重装)。真机 worker↔child 32 对向量 cosine=1.0(IEEE754 float32 往返无损)。用户无感知,非 breaking。
63
+
25
64
  ## [0.26.1] — 2026-10-15
26
65
 
27
66
  ### 新增
package/README.en.md CHANGED
@@ -474,7 +474,9 @@ adds 12 agent-facing memory tools (high-privilege writes `memory_add` / `memory_
474
474
  `memory_import`, ruminate controls `memory_ruminate`, the memory graph
475
475
  `memory_search_graph` / `memory_expand_graph_node`, decision-receipt backtracking
476
476
  `memory_receipts`, and conflict adjudication `memory_resolve_conflict`), the `skills/memport`
477
- cross-tool memory transfer, and storefront screenshot declarations.
477
+ cross-tool memory transfer, and storefront screenshot declarations; the deterministic
478
+ write-plane adds three more (`memory_update` / `memory_reproject` / `memory_migrate_relation`,
479
+ see the "Deterministic graph write plane" section).
478
480
 
479
481
  Two of those tools exist for **traceability**:
480
482
 
@@ -518,6 +520,65 @@ The restore entry point accepts **directory names only, never paths**: otherwise
518
520
 
519
521
  > **One semantic you need to know**: `cleanup-retired` only removes **already-retired** records, and the snapshot is taken **before** deletion — so every record a cleanup snapshot can bring back carries a retirement marker. By default `snapshot-restore` only writes the row back to the main table (and reports it honestly in `stillRetired`); **"back in the main table" ≠ "back in recall"**. For a genuine one-call rollback add `unretire: true`, or call `records-restore` on that batch of ids afterwards.
520
522
 
523
+ ## Memory expiry tiers and supersede-chain lookup
524
+
525
+ A memory's "temporality" splits into **four non-substitutable axes** ([ADR-0022](./docs/adr/0022-expiry-tiering-and-validity.md)):
526
+ retirement mode (supersede marker), validity state (derived at query time), objective persistence, and the
527
+ real-world validity window (`valid_to`). The old model crammed all four into one `valid_to` column — the cost
528
+ was that perfectly legal historical memories ("a trip that ended in 2020") were **silently shadowed from the
529
+ retrieval surface** although they never retired.
530
+
531
+ | Tier | Meaning | Indexed | Auto-injected (default) | Explicit tool query |
532
+ | --- | --- | --- | --- | --- |
533
+ | `current` | open-ended, not past its window | ✅ | ✅ | ✅ |
534
+ | `historical` | point/closed-span/timeless, not retired | ✅ | ✅ | ✅ |
535
+ | `expired` | window closed **with trustworthy evidence** | ✅ | ❌ (gated, gate 2) | ✅ `include_expired` |
536
+ | `expired_suspect` | same but the window source is untrustworthy (flag, never hide) | ✅ | ✅ annotated | ✅ |
537
+ | `superseded` / `retired` | soft-deleted (restorable) | ❌ | ❌ | via supersede chain only |
538
+
539
+ Three gates:
540
+
541
+ - **Gate 1 (bugfix, ungated)**: retirement is judged by the marker alone, everywhere; the first boot after
542
+ upgrade runs one full FTS back-fill that returns wrongly-shadowed history to the retrieval surface
543
+ (log reports back-filled/skipped counts; vectors catch up incrementally or via the manual re-index button).
544
+ - **Gate 2 (grey flag `validity.tieringEnabled`, off by default)**: when on, passive injection drops
545
+ `expired` and keeps `expired_suspect` with a "suspected expired" annotation; recall bubbles show
546
+ "expired since X". A same-named runtime toggle (OR-ed with the deploy config) lives on the settings overview.
547
+ - **Gate 3 (`memory_search` parameters, all off by default = byte-identical to before)**: `include_expired`
548
+ admits trustworthy-expired rows with `state`/`valid_from`/`valid_to`/`valid_to_source`; `as_of` replays the
549
+ validity axis at a past moment (never reviving retired rows); `follow_supersede` walks the supersede chain
550
+ to answer "what did this entry replace".
551
+
552
+ Legacy rows get their `dsh_validity` annotation from a low-frequency local back-fill (zero LLM; touched
553
+ records are re-projected into the graph to keep it fresh). The classifier is a single source: the six tiers
554
+ are query-time predicates, never wall-clock-driven stored state.
555
+
556
+ ## Deterministic graph write plane and the correction channel
557
+
558
+ The graph is a **derived projection** of L1: nodes/edges are always recomputed from current
559
+ facts, so the single source of truth for every write is L1 itself —
560
+ **correction = edit the source + reproject, never hand-written SQL**. This plugin does not
561
+ offer, and will never offer, an entry that bypasses projection to write `graph_*` tables
562
+ directly ("zero source-less facts" invariant: every edge traces back to live L1 records via
563
+ `sourceRecordIds`).
564
+
565
+ | Action | Tool | Preconditions | Notes |
566
+ | --- | --- | --- | --- |
567
+ | Revise one memory | `memory_update` | high-privilege mode | Read old → create new (`version+1`, new id) → old row retires via the shared soft-delete confluence with a `superseded` marker (restorable) → new version enters retrieval; then both [old, new] ids are force-reprojected so graph edges follow (edges supported only by the old source get tombstoned). Not a physical delete; the JSONL fact source is append-only and never rewritten |
568
+ | Force reprojection | `memory_reproject` | `graph.enabled` | Erases the "already projected at current version" receipts and re-enqueues at top priority, bypassing the permanent `projector_version` skip. Idempotent: in-flight jobs never pile up. **Manually added/imported memories that never showed up in the graph: this is the back-fill projection channel** (non-distilled writes now auto-enqueue; the tool remains the correction path for legacy rows and post-edit freshness) |
569
+ | Migrate one relation | `memory_migrate_relation` | high-privilege mode | Source-derived semantics: hard-validate that every `source_record_ids` is live and un-retired → the new relation lands through the deterministic projection branch (same (from, relation) with a new target closes the old edge) → a new L1 carrier record is written as the traceable basis (tagged `proposal_source=tool` plus a decision receipt). Old edges are not flipped back by later rejections: the reprojection batch pulls only the carrier, not sources whose text still describes the old relation |
570
+ | Non-distilled writes auto-enqueue | `memory_add` / `memory_import` | `graph.enabled` | After a successful write, records join the projection queue at the same priority as distillation output (`GRAPH_PRIORITY_NEW`) — the "only distillation projects" bypass is closed |
571
+
572
+ Two boundaries worth knowing:
573
+
574
+ - **Frequent updates accumulate version rows**: retired old rows stay in the main table (the
575
+ price of restorability); volume is governed by the existing `cleanup-retired` (dry-run +
576
+ snapshot gate).
577
+ - **The graph vector path activates with the embedding source**: `graph_node_vec` is
578
+ *structurally absent* when embeddings are off (not an empty table); once embeddings are on,
579
+ projection wrap-up writes vectors for freshly landed active nodes (legacy nodes catch up
580
+ via reprojection/rebuild).
581
+
521
582
  ## Roadmap
522
583
 
523
584
  Features under planning — feedback and priorities welcome in the
package/README.md CHANGED
@@ -352,7 +352,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
352
352
  | `llm.maxTokens` | `65536` | 未分层调用的兜底输出总闸。各蒸馏层有独立预算(抽取 16k / 去重 8k / L2 32k / L3 16k;思考档 high/xhigh/max 时自动 ×4,防 reasoning 吃光预算),分层预算可在设置页 → 记忆 → 概览 → 蒸馏参数运行时调整(留空/0 = 跟随内置默认) |
353
353
  | `llm.reasoningEffort` | 空 | 蒸馏思考档位:空串 = **自动**(按模型能力解析:模型默认档 → `high`);显式值(`off`/`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`)仅在该模型声明支持时发送——跨供应商 effort 词汇表不同(deepseek 认 `off`,OpenAI 系是 `none`,未声明档位的模型不传),不支持的档位自动降级为不传并告警一次;思考档 high/xhigh/max 时输出预算自动 ×4。运行时可在设置页路由链编辑器里**逐路由**覆盖档位(行内下拉,词表按各模型声明的能力实时显示,缺省跟随本值) |
354
354
  | `llm.temperature` | `0.3` | 蒸馏温度 |
355
- | `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取);运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
355
+ | `llm.maxInputChars` | `200000` | 单次蒸馏调用输入闸门(超限截断);默认 200k 适配本地/免费模型窗口,上限 256k,可再往下调。L1 抽取/L2 另有 24k 分块硬顶,不受此值影响;运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
356
356
  | `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms) |
357
357
  | `tokenCost.retentionDays` | `365` | 蒸馏成本明细(token\_cost 表)保留天数,写入时滚动清理更早行;`0` = 永久保留。成本看板「近 N 天」窗口上限同此值 |
358
358
  | `tools` | `true` | 是否注册模型可调用的记忆工具 |
@@ -415,6 +415,13 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
415
415
 
416
416
  4. **调高超时**:`llm.timeoutMs` 只在路由确实慢但网关不掐时有用;网关 20s 掐断的场景调插件超时无效,请用前两层。
417
417
 
418
+ ### 二线大容量兜底与超时可调(v0.26.7+)
419
+
420
+ - **面板可调超时**:设置 → 记忆 → 蒸馏参数 → **蒸馏超时(ms)**(`distillTimeoutMs`,即时生效、无需重启)。只救"慢但未被网关掐"的路由;供应商网关侧固定掐断(如某些免费路由 60s)抬此值无效,须换路由。
421
+ - **二线模型(大容量兜底)**:`llm.secondary{provider,model,triggerChars}`——单次调用输入 ≥ `triggerChars` 才走二线(小调用走便宜快的一线、肥调用走能吞大输入的二线)。未配则不启用、绝不静默改选任意模型。
422
+ - **分块分载与容错**:L1 抽取 / L2 场景整合按字符预算分块,单块超时/崩只丢该块、留待重试,不再"一块崩整批丢"(待重试不再被整片回滚)。`llm.extractTimeBudgetMs`(默认 0=不限)可给整轮抽取设墙钟预算,防一个慢模型独占后台;`llm.modelMaxInputChars` 声明模型真实输入上限,块大小据此收紧。
423
+ - **显式路由(灰度)**:`llm.requireExplicitRoute=true` 时,未显式配一线/二线/回退链 → 蒸馏顺延并告警,不再蹭宿主默认模型(默认 false 保持旧行为)。
424
+
418
425
  ## 日志与故障排查
419
426
 
420
427
  dsh 宿主把插件日志打到控制台;插件另把 info 级以上镜像到数据目录的 `memory.log`。
@@ -461,6 +468,56 @@ fsync),断电等极端崩溃最多丢最后一小段尾部,检索库可用
461
468
 
462
469
  > **一处必须知道的语义**:`cleanup-retired` 只清理**已退场**记录,而快照拍在删除**之前**——所以清理快照能找回的每一条都带着退场标记。默认情况下 `snapshot-restore` 只把行写回主表(并在 `stillRetired` 里如实报出),**"回到主表" ≠ "回到召回"**;要一步完成真正的回滚,加 `unretire: true`,或随后对该批 id 调 `records-restore`。
463
470
 
471
+ ## 记忆时效三档与失效链反查
472
+
473
+ 一条记忆的"时间性"拆成**四根互不替代的轴**(ADR-0022):退场方式(取代标记)、有效性定性
474
+ (查询期派生六态)、客观持续性(persistence)、现实有效期(valid_to)。旧版把这四件事塞进
475
+ 一根 `valid_to` 列,代价是"2020 年结束的旅行"这类**合法历史记录被检索面静默遮蔽**——
476
+ 它没退场,只是成了历史。
477
+
478
+ | 档 | 含义 | 进索引 | 被动注入(默认) | 工具显式查 |
479
+ | --- | --- | --- | --- | --- |
480
+ | `current` | 仍在持续且未到有效期 | ✅ | ✅ | ✅ |
481
+ | `historical` | 时点/已结束区间/无时间性,未退场 | ✅ | ✅ | ✅ |
482
+ | `expired` | 有效期已闭合**且来源可信** | ✅ | ❌ 需开门2 | ✅ `include_expired` |
483
+ | `expired_suspect` | 同上但有效期来源不可信(宁判疑不静默隐) | ✅ | ✅ 带标注 | ✅ |
484
+ | `superseded` / `retired` | 取代/人工退场(软删,可恢复) | ❌ 撤出 | ❌ | 仅取代链反查 |
485
+
486
+ 三扇门:
487
+
488
+ - **门1(bugfix,不受灰度门控)**:退场判据全仓改为只认取代标记;升版后首启自动做一次
489
+ FTS 全量回灌,把误遮蔽的历史记忆放回检索面(日志报回灌/跳过数;向量面由增量重嵌补齐,
490
+ 或设置页手动「语义检索重建」加速)。
491
+ - **门2(灰度闸 `validity.tieringEnabled`,默认关)**:开时被动注入剔除 `expired`,
492
+ `expired_suspect` 保留并在注入行带"疑似失效"标注;召回卡泡泡同步显示"已于 X 失效"。
493
+ 面板概览有同名运行时开关(与部署配置取或),验证通过后再评估翻默认。
494
+ - **门3(`memory_search` 反查参数,默认全关 = 与改前逐字一致)**:`include_expired` 放行
495
+ 可信过期项并附 `state`/`valid_from`/`valid_to`/`valid_to_source`;`as_of` 按有效期回放
496
+ 历史时刻(不复活退场记忆);`follow_supersede` 顺取代链反查"本条取代了谁"。
497
+
498
+ 存量记录的 `dsh_validity` 标注由低频本地回填补齐(零 LLM;改过的记录顺手重投图谱保鲜)。
499
+ 判据单源 `classifyValidity`:六态是查询期谓词,不会随墙钟落库翻态。
500
+
501
+ ## 图谱确定性写面与纠正通道
502
+
503
+ 图谱是 L1 的**派生投影**:节点/边永远由"当前事实重算"产出,所以写面的唯一真源是 L1,
504
+ **纠正 = 改源 + 重投,不是手写 SQL**。本插件不提供、也永远不会提供绕过投影直写
505
+ `graph_*` 表的入口("零无源事实"不变量:每条边都能沿 `sourceRecordIds` 点查回现存 L1 记录)。
506
+
507
+ | 动作 | 工具 | 前提 | 说明 |
508
+ | --- | --- | --- | --- |
509
+ | 修订一条记忆 | `memory_update` | 高权限模式 | 读旧 → 造新(`version+1`,新 id)→ 旧行经退场汇流口 `superseded` 软删(可恢复)→ 新版进检索面;随后对 [旧,新] 强制重投,图谱边跟随(旧源支撑的边自动标墓碑)。不是物理删,也不覆写 JSONL 事实源 |
510
+ | 强制重投 | `memory_reproject` | 配置 `graph.enabled` | 抹掉"已按当前版本投影"凭证、按高优先级重新入队——绕开 `projector_version` 的永久跳过。幂等:在途任务不堆重。**手加/导入的记忆长期不出现在图谱里,用它补投影**(非蒸馏写入现已自动入队,此工具是给存量与改后保鲜的纠正通道) |
511
+ | 迁移一条关系 | `memory_migrate_relation` | 高权限模式 | 源导语义:先硬校验 `source_record_ids` 全部"现存未退场" → 新关系经确定性投影支路落库(同 (起点,关系) 换目标自动闭旧边出新边)→ 新增一条承载新关系的 L1 源记录留可回溯根据(带 `proposal_source=tool` 审计标记与决策凭证)。旧边不会因后续重投被翻回:重投批只拉载体记录,不拉描述旧关系的旧源 |
512
+ | 非蒸馏写入自动入队 | `memory_add` / `memory_import` | 配置 `graph.enabled` | 落库成功后与蒸馏产出同优先级(`GRAPH_PRIORITY_NEW`)进投影队——"只有蒸馏才投影"的旁路缺口已闭合 |
513
+
514
+ 两条必须知道的边界:
515
+
516
+ - **频繁 update 会累积版本行**:旧行以退场态留在主表(可恢复的设计代价),规模由既有
517
+ `cleanup-retired`(干跑 + 快照门禁)控制;
518
+ - **图谱向量路随嵌入源激活**:`graph_node_vec` 在嵌入关闭时**结构性不存在**(不是空表);
519
+ 启用嵌入后,投影收尾会为落库的 active 节点补写向量(旧版本存量节点经重投/重建逐步补齐)。
520
+
464
521
  ## 路线图
465
522
 
466
523
  以下为规划中的功能,欢迎在 [Issues](https://github.com/drscrewdriver/dsh-prime-memory/issues) 反馈需求与优先级:
@@ -477,7 +534,8 @@ fsync),断电等极端崩溃最多丢最后一小段尾部,检索库可用
477
534
  相对上游,本仓库新增了面向 Agent 的 12 个记忆工具(含高权限写入 `memory_add` / `memory_delete` /
478
535
  `memory_import`、反刍控制 `memory_ruminate` 系列、记忆图谱 `memory_search_graph` /
479
536
  `memory_expand_graph_node`、决策凭证回溯 `memory_receipts`、矛盾裁决 `memory_resolve_conflict`)、
480
- `skills/memport` 跨工具记忆搬运,以及商店截图声明。
537
+ `skills/memport` 跨工具记忆搬运,以及商店截图声明;确定性写面计划又补上三件
538
+ (`memory_update` / `memory_reproject` / `memory_migrate_relation`,见"图谱确定性写面"一节)。
481
539
 
482
540
  其中两个工具服务于**可追溯性**:
483
541
 
package/dist/client.js CHANGED
@@ -37811,9 +37811,12 @@ var __create = Object.create;
37811
37811
  const ib = data.inputBudget || { current: 0, fallback: 0, effective: 0 };
37812
37812
  const curIn = ib.current || 0;
37813
37813
  const effIn = ib.effective || ib.fallback || 0;
37814
+ const tb = data.timeoutBudget || { current: 0, fallback: 0, effective: 0 };
37815
+ const curTo = tb.current || 0;
37816
+ const effTo = tb.effective || tb.fallback || 0;
37814
37817
  const shown = (key) => {
37815
37818
  if (draft && draft[key] !== void 0) return draft[key];
37816
- const c = key === "input" ? curIn : cur[key] || 0;
37819
+ const c = key === "input" ? curIn : key === "timeout" ? curTo : cur[key] || 0;
37817
37820
  return c > 0 ? String(c) : "";
37818
37821
  };
37819
37822
  const commitPart = (keys, buildPayload, applyView) => {
@@ -37823,11 +37826,11 @@ var __create = Object.create;
37823
37826
  const key = keys[i];
37824
37827
  const raw = (draft[key] !== void 0 ? draft[key] : shown(key)).trim();
37825
37828
  const n = raw === "" ? 0 : Number(raw);
37826
- const max = key === "input" ? 1e6 : 1e6;
37827
- const min = key === "input" ? raw === "" ? 0 : 1e3 : 0;
37829
+ const max = key === "timeout" ? 6e5 : key === "input" ? 256e3 : 1e6;
37830
+ const min = key === "input" || key === "timeout" ? raw === "" ? 0 : 1e3 : 0;
37828
37831
  if (!Number.isInteger(n) || n < min || n > max) {
37829
37832
  onError(
37830
- key === "input" ? "输入预算须为 0 或 1000~1000000 的整数(留空或 0 = 跟随配置)" : "输出预算须为 0~1000000 的整数(留空或 0 = 跟随默认)"
37833
+ key === "input" ? "输入预算须为 0 或 1000~256000 的整数(留空或 0 = 跟随配置)" : key === "timeout" ? "蒸馏超时须为 0 或 1000~600000 的整数(留空或 0 = 跟随配置)" : "输出预算须为 0~1000000 的整数(留空或 0 = 跟随默认)"
37831
37834
  );
37832
37835
  setDraft(null);
37833
37836
  return;
@@ -37888,12 +37891,27 @@ var __create = Object.create;
37888
37891
  }
37889
37892
  );
37890
37893
  };
37894
+ const commitTimeout = () => {
37895
+ commitPart(
37896
+ ["timeout"],
37897
+ (v) => ({ distillTimeoutMs: v.timeout }),
37898
+ (prev, v) => ({
37899
+ ...prev,
37900
+ settings: { ...prev.settings, distillTimeoutMs: v.timeout },
37901
+ timeoutBudget: {
37902
+ ...prev.timeoutBudget || { current: 0, fallback: 0, effective: 0 },
37903
+ current: v.timeout,
37904
+ effective: v.timeout > 0 ? v.timeout : tb.fallback || 0
37905
+ }
37906
+ })
37907
+ );
37908
+ };
37891
37909
  const dirty = (keys) => {
37892
37910
  if (!draft) return false;
37893
37911
  for (let i = 0; i < keys.length; i++) {
37894
37912
  const key = keys[i];
37895
37913
  const want = (draft[key] !== void 0 ? draft[key] : "").trim();
37896
- const was = key === "input" ? curIn > 0 ? String(curIn) : "" : (cur[key] || 0) > 0 ? String(cur[key]) : "";
37914
+ const was = key === "input" ? curIn > 0 ? String(curIn) : "" : key === "timeout" ? curTo > 0 ? String(curTo) : "" : (cur[key] || 0) > 0 ? String(cur[key]) : "";
37897
37915
  if (want !== was) return true;
37898
37916
  }
37899
37917
  return false;
@@ -37975,6 +37993,28 @@ var __create = Object.create;
37975
37993
  ),
37976
37994
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "字符" })
37977
37995
  ] }) })
37996
+ ] }) : null,
37997
+ showInput ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginTop: 12 }, children: [
37998
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
37999
+ "div",
38000
+ {
38001
+ style: S.switchLabel,
38002
+ title: '单次蒸馏调用超时(ms):治"慢但未被网关掐"的路由需更大超时窗;留空或 0 = 跟随配置(当前生效 ' + (effTo || "?") + "ms,来自 llm.timeoutMs)。注:供应商网关侧固定掐断(如某些路由 60s)抬此值无效,须换路由",
38003
+ children: "蒸馏超时"
38004
+ }
38005
+ ),
38006
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
38007
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: "超时" }),
38008
+ inputBox(
38009
+ "timeout",
38010
+ "超时",
38011
+ "单次蒸馏调用超时(ms)(留空 = 跟随配置 " + (tb.fallback || "?") + ")",
38012
+ 110,
38013
+ tb.fallback,
38014
+ commitTimeout
38015
+ ),
38016
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "ms" })
38017
+ ] }) })
37978
38018
  ] }) : null
37979
38019
  ] });
37980
38020
  }
package/dist/config.d.ts CHANGED
@@ -61,6 +61,10 @@ export interface MemoryConfig {
61
61
  backgroundMessages: number;
62
62
  /** 去重候选池大小(每条新记忆的相似候选数)。 */
63
63
  candidatePool: number;
64
+ /** L1 去重批连续失败上限:达此值则 fail-loud + park,不再无限重放同肥调用。R1″。 */
65
+ dedupRetryCap?: number;
66
+ /** 抽取结果 checkpoint 落盘:去重/落库失败后下轮从去重续跑、不重抽已成功块。R1″。 */
67
+ extractCheckpoint?: boolean;
64
68
  };
65
69
  l2: {
66
70
  enabled: boolean;
@@ -70,6 +74,8 @@ export interface MemoryConfig {
70
74
  maxScenes: number;
71
75
  /** L2 prompt 里附带的相似场景全文数量上限。 */
72
76
  sceneContextLimit: number;
77
+ /** L2 场景整合连续失败上限:达此值停攒该族 + fail-loud,避免同肥调用每轮重放。R8。 */
78
+ consecutiveFailCap?: number;
73
79
  };
74
80
  l3: {
75
81
  enabled: boolean;
@@ -81,6 +87,15 @@ export interface MemoryConfig {
81
87
  * 同时为真才执行;图谱是 L1 的可重建投影,关闭不影响记忆主链路。 */
82
88
  enabled: boolean;
83
89
  };
90
+ /**
91
+ * 时效分级(expiry-tiering 门2):`tieringEnabled` 灰度闸,**默认关**。
92
+ * 关时被动注入与改前逐字一致(零漂移);开时注入候选按 classifyValidity 现算态
93
+ * 剔除 `expired`(仍在索引可检索,只是不自动浮现),`expired_suspect` 保留并带标注。
94
+ * 与部署 cfg.validity.tieringEnabled 取或的还有面板运行时开关(live.validityTiering)。
95
+ * 门1 解耦本身不受本闸控(它是 bugfix:误遮蔽历史应尽快恢复)。 */
96
+ validity: {
97
+ tieringEnabled: boolean;
98
+ };
84
99
  /** §C 矛盾冻结:去重判定"两边都像是对的、机器判不了"时不自动裁决,
85
100
  * 把冲突对停放到待人工裁决区(conflict_pending)。**默认关**——
86
101
  * 冻结消耗人的注意力,不可默认全开。 */
@@ -381,6 +396,31 @@ export interface MemoryConfig {
381
396
  maxInputChars: number;
382
397
  /** 单次蒸馏调用超时(ms)。 */
383
398
  timeoutMs: number;
399
+ /** R5 整轮抽取墙钟预算(ms):多块抽取累计超此预算即把剩余块留 pending 下轮续,
400
+ * 防一个慢模型独占串行泵十几分钟。0 = 不限(旧行为)。 */
401
+ extractTimeBudgetMs?: number;
402
+ /** 二线蒸馏模型(大容量兜底):仅当单次调用输入字符 ≥ triggerChars 才启用;
403
+ * provider/model 任一为空 = 该档不可用(不隐式改选默认模型)。R3。 */
404
+ secondary?: {
405
+ provider: string;
406
+ model: string;
407
+ reasoningEffort?: string;
408
+ triggerChars: number;
409
+ };
410
+ /** 蒸馏模型真实输入上限(字符):perChunk 初值天花板;0/未声明 = 回退旧口径
411
+ * min(24k, maxInputChars-42k)。R5/R11。 */
412
+ modelMaxInputChars?: number;
413
+ /** 自适应分块:超时/报错后块大小按此因子缩减(默认 0.6=减四成)。R5。 */
414
+ chunkShrinkFactor?: number;
415
+ /** 块缩减最大重试步数(触底=单条或达此步数即止)。R5。 */
416
+ chunkShrinkMaxSteps?: number;
417
+ /** 自适应分块总开关:关=固定 perChunk(零漂移)。R5。 */
418
+ adaptiveChunk?: {
419
+ enabled: boolean;
420
+ };
421
+ /** 蒸馏须显式配路由:true 时一线+二线+链皆空 → 抛 NoDistillRouteError(不蹭默认模型)。
422
+ * 灰度默认 false(不破坏"留空跟默认模型"的存量用户)。R4。 */
423
+ requireExplicitRoute?: boolean;
384
424
  /** 分层输出预算运行时覆盖(设置页 distillBudgets 经 effectiveCfg 注入;
385
425
  * 0/缺省 = 用内置默认。非静态 schema——预算无部署上限语义)。 */
386
426
  budgets?: Partial<{
package/dist/config.js CHANGED
@@ -54,7 +54,9 @@ export function liveSettingsSchema() {
54
54
  // 图谱投影输出预算(投影 job 单批 ≤8 条记录,默认 8000)
55
55
  graph: budget(),
56
56
  }).default({ extract: 0, dedup: 0, l2: 0, l3: 0, graph: 0 }),
57
- distillMaxInputChars: Schema.number().min(0).max(1_000_000).default(0),
57
+ distillMaxInputChars: Schema.number().min(0).max(256_000).default(0),
58
+ // R10 蒸馏超时运行时覆盖(ms):0 = 跟随静态 llm.timeoutMs;上限与部署 schema 同(600s)
59
+ distillTimeoutMs: Schema.number().min(0).max(600_000).default(0),
58
60
  // 蒸馏通道运行时覆盖:'' = 跟随部署 config / 'host' = 复用宿主 / 'direct' = 原生直连
59
61
  distillMode: Schema.union(['', 'host', 'direct']).default(''),
60
62
  directBaseURL: Schema.string().default(''),
@@ -87,6 +89,9 @@ export function liveSettingsSchema() {
87
89
  redistillEnabled: Schema.boolean().default(false),
88
90
  mdSyncEnabled: Schema.boolean().default(false),
89
91
  memportEnabled: Schema.boolean().default(false),
92
+ /** 时效分级门2 面板开关:与部署 cfg.validity.tieringEnabled 取或(任一为真即启用
93
+ * 注入侧 expired 过滤)。默认关;先面板验证再翻部署默认(照 keywordEnabled 先例)。 */
94
+ validityTiering: Schema.boolean().default(false),
90
95
  /**
91
96
  * 存量回填(keyword / redistill)LLM 批调用的**运行时并行度**覆盖:
92
97
  * `0` = 跟随部署 `cfg.backfill.concurrency`;1–8 = 直接覆盖。
@@ -141,12 +146,15 @@ export const memorySchema = Schema.object({
141
146
  idleSeconds: Schema.number().min(0).max(86_400).default(300),
142
147
  backgroundMessages: Schema.number().min(0).max(50).default(10),
143
148
  candidatePool: Schema.number().min(1).max(20).default(5),
149
+ dedupRetryCap: Schema.number().min(1).max(20).default(5),
150
+ extractCheckpoint: Schema.boolean().default(true),
144
151
  }),
145
152
  l2: Schema.object({
146
153
  enabled: Schema.boolean().default(true),
147
154
  minNewMemories: Schema.number().min(1).max(100).default(5),
148
155
  maxScenes: Schema.number().min(1).max(100).default(12),
149
156
  sceneContextLimit: Schema.number().min(0).max(20).default(3),
157
+ consecutiveFailCap: Schema.number().min(1).max(20).default(3),
150
158
  }),
151
159
  l3: Schema.object({
152
160
  enabled: Schema.boolean().default(true),
@@ -156,6 +164,11 @@ export const memorySchema = Schema.object({
156
164
  graph: Schema.object({
157
165
  enabled: Schema.boolean().default(false),
158
166
  }),
167
+ // 时效分级门2 灰度闸:默认关(关=注入行为与改前逐字一致)。验后翻默认为 true 的
168
+ // 发版决策记在计划 findings(task_8.6)。
169
+ validity: Schema.object({
170
+ tieringEnabled: Schema.boolean().default(false),
171
+ }),
159
172
  // §C 矛盾冻结:默认关。开启后去重决策词表多出 conflict 动作,冲突对停放待人工裁决。
160
173
  conflictFreeze: Schema.object({
161
174
  enabled: Schema.boolean().default(false),
@@ -353,9 +366,26 @@ export const memorySchema = Schema.object({
353
366
  // 旧默认 'off' 在非 deepseek 模型上必炸(400/本地拒绝)
354
367
  reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
355
368
  temperature: Schema.number().min(0).max(2).default(0.3),
356
- // 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
357
- maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
369
+ // 单次调用输入闸门默认压到本地模型可参与区间(200k,可再降);上限 256k(<256 KiB)。
370
+ // 旧 700k 为盲值、远超本地/免费路由窗口 → 大调用撞模型硬限。抽取/L2 分块另有 24k 硬顶,不受此影响。
371
+ maxInputChars: Schema.number().min(1000).max(256_000).default(200_000),
358
372
  timeoutMs: Schema.number().min(1000).max(600_000).default(120_000),
373
+ // R5 整轮抽取墙钟预算(ms):0=不限(旧行为);>0 超预算把剩余块留 pending 下轮续
374
+ extractTimeBudgetMs: Schema.number().min(0).max(3_600_000).default(0),
375
+ // 二线蒸馏模型(大容量兜底):输入 ≥ triggerChars 才启用;provider/model 空=该档不可用
376
+ secondary: Schema.object({
377
+ provider: Schema.string().default(''),
378
+ model: Schema.string().default(''),
379
+ reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
380
+ triggerChars: Schema.number().min(1000).max(1_000_000).default(20_000),
381
+ }).default({ provider: '', model: '', reasoningEffort: '', triggerChars: 20_000 }),
382
+ // 蒸馏模型真实输入上限(0=未声明,回退旧口径 min(24k, maxInputChars-42k))
383
+ modelMaxInputChars: Schema.number().min(0).max(1_000_000).default(0),
384
+ chunkShrinkFactor: Schema.number().min(0.1).max(0.95).default(0.6),
385
+ chunkShrinkMaxSteps: Schema.number().min(0).max(8).default(4),
386
+ adaptiveChunk: Schema.object({ enabled: Schema.boolean().default(true) }).default({ enabled: true }),
387
+ // 灰度默认 false:翻 true 后蒸馏须显式配路由(不蹭宿主默认模型),配 NoDistillRouteError
388
+ requireExplicitRoute: Schema.boolean().default(false),
359
389
  }),
360
390
  hall: Schema.object({
361
391
  enabled: Schema.array(Schema.string()).default([...WING_DEFAULT_ENABLED]),
@@ -77,7 +77,10 @@ export interface MemoryLiveSettings {
77
77
  /** 输入预算运行时覆盖(字符,≈token):单次蒸馏调用的输入上限,L1 按此分块、
78
78
  * 超限截断;0 = 跟随静态配置 llm.maxInputChars。 */
79
79
  distillMaxInputChars: number;
80
- /** 运行时按层路由链:层键 l1/l2/l3 各一条完整链;非空即完整接管该层解析
80
+ /** R10 蒸馏调用超时运行时覆盖(ms):0 = 跟随静态配置 llm.timeoutMs;>0 = 覆盖。
81
+ * 治"慢但未被网关掐"的路由需更大超时窗(注:供应商网关侧固定掐断抬此值无效,须换路由)。 */
82
+ distillTimeoutMs: number;
83
+ /** 运行时按层路由链:层键 l1/l2/l3 各一条完整链;非空即完整接管该层解析
81
84
  * (压过静态 layerRoutes,层内第一优先级);空数组 = 该层跟随(静态层链 → 全局解析逐级兜底)。
82
85
  * 写入经 settings-set 逐层校验(头行必须显式)。 */
83
86
  distillLayerChains: Record<LayerRouteKey, DistillChainEntry[]>;
@@ -116,6 +119,8 @@ export interface MemoryLiveSettings {
116
119
  mdSyncEnabled: boolean;
117
120
  /** 外部记忆来源 Connect 启用开关(概览区可拨):true = 放行 memport-* 端点。与部署 cfg.memport.enabled 取或。 */
118
121
  memportEnabled: boolean;
122
+ /** 时效分级门2 启用开关(概览区可拨):true = 被动注入剔除 expired。与部署 cfg.validity.tieringEnabled 取或;关=注入与改前逐字一致。 */
123
+ validityTiering: boolean;
119
124
  /** md 投射/后缀收敛的单次动作护栏运行时覆盖:0 = 跟随部署 `cfg.memExport.maxRecords`(默认 2000)。
120
125
  * 存在的理由:一次点完多少条属于“回填历史时才改一下”的旋钮,不该要求改 YAML + 重启。 */
121
126
  mdExportMaxRecords: number;
@@ -803,6 +808,12 @@ export interface SettingsGetResponse {
803
808
  fallback: number;
804
809
  effective: number;
805
810
  };
811
+ /** R10 蒸馏超时(ms):current 运行时覆盖(0=跟随) / fallback 静态 llm.timeoutMs / effective 生效值。 */
812
+ timeoutBudget: {
813
+ current: number;
814
+ fallback: number;
815
+ effective: number;
816
+ };
806
817
  }
807
818
  /** dsh-memory/settings-set(patch 语义:只带要改的键)。 */
808
819
  export interface SettingsSetRequest {
@@ -824,6 +835,8 @@ export interface SettingsSetRequest {
824
835
  graph: number;
825
836
  };
826
837
  distillMaxInputChars?: number;
838
+ /** R10 蒸馏超时运行时覆盖(ms):0 = 跟随配置。 */
839
+ distillTimeoutMs?: number;
827
840
  /** md 投射/后缀收敛的单次动作护栏覆盖:0 = 跟随部署 `cfg.memExport.maxRecords`。 */
828
841
  mdExportMaxRecords?: number;
829
842
  /** 蒸馏通道运行时覆盖:'' = 跟随部署 config;'host' = 复用宿主;'direct' = 插件原生直连。 */