@modusensus/dsh-mneme 0.9.3 → 0.9.4

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-2257%20passed-success)](https://github.com/slow-stack/mneme)
8
+ [![tests](https://img.shields.io/badge/tests-2302%20passed-success)](https://github.com/slow-stack/mneme)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
10
10
  [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
11
11
  [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
@@ -275,6 +275,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
275
275
  | **v0.9.1**(2026-10-18) | M.O. 从「看得见」跑到「跑得出账」:执行窗口由 `/mo-run start` / `/mo-run stop` 显式划开、运行账落 `<项目>/.mneme/pipelines/<id>.runs.jsonl`(账本坏了与从没跑过严格分开)、归属只出候选、观察旁挂与脱节检出、步骤 `assert` / `optional`、R1/R2 提议;沉淀链从真实会话日志挖 draft 工作流(判据恒 `null`,答案只能由人补);人格定义页 v1 → v1.2(按 agent 寻址多卡,默认关);面板、向量与检查更新的一圈修复(#448 / #450 / #460);Release 正文改分层渲染 |
276
276
  | **v0.9.2**(2026-10-19) | 让「看不见的失败」显形:功能开关的第三态「待重启」把「面板显示已改 ≠ 运行时已改」摆到界面上(#481);作用域级可逆闸门让一路记忆能被单独按下与松开、不必改配置重启(#471,默认关);巩固用的 `dreamMaxTokens` 默认值撞服务商上限、八天 180 次全失败,面板现在能改、超上限还会自动降级重试(#478);CLI 把 `--` 开头的值当新旗标的真实缺陷(一次 1/4096 概率的 CI 红揪出来的)、人格卡删除 × 永远不可见、自动蒸馏写入的行 100% 拿不到向量(#477) |
277
277
  | **v0.9.3**(2026-10-20) | M.O. 从「挖得出来」到「会被叫去挖」:沉淀链算法自 v0.9.1 起就完整实现(多帧 zstd → 重复序列 → 缺席对照 → draft 草稿),运行时却没有任何调用点——机器挖得出来、却永远不会被叫去挖。这一版把归属与触发接上:按会话头 `cwd` 反查项目桶(桶名是路径的有损变形,查不到整跑跳过而不是降级成全局普查)、挂 `turn/end`、冷却 10 分钟、单次预算 4 秒、延到宏任务、一次一个项目、抛错只 `warn`,**只产提议不产草稿**(`actionNudgeEnabled`,beta,默认关);面板加「现在沉淀一次」,与自动跑同一条入口,区别只有它不改仓库;另修作用域反查自 v0.8.0 起每次都抛错走降级(#487)、`/nudge` 三处「没看全」被报成「确实没有」(#489 评审)、多帧 zstd 反证断言绑了 Node 整块解语义(#490),并把整个 M.O. 的「手法」改回「工作流」 |
278
+ | **v0.9.4**(2026-10-21) | 三条链各自补上「最后一环」——东西早就造好了,只差没人能叫它:**agent 主动整理**(`memory_organize`,先 dryRun 出比对报告 → agent 判断 → apply 落库,筛除只归档不删、全程留审计回执;`organizeEnabled` 默认关,关时工具根本不注册);**画像提取草稿**(面板按钮读活跃记忆 + 一次模型调用,产一段「助手眼中的用户」草稿,`profileExtractEnabled` 默认关)——**只出草稿**,采用是用户再点一次,手写的画像原文不会被模型改写,空库与解析失败都直接报错、绝不回空草稿冒充「没提炼出东西」;**人格寻址收口**(`default` 卡以前无条件落给所有没建卡的身份,宿主里自带身份的 bot 每帧收到两句「你是谁」;`personaStrictAddressing` 默认关,把「没建卡」与「卡被显式关掉」都收回为不注入)。同版补蒸馏的**转录围栏 + 解析失败护栏**(#494:一条会话被同一窗口卡死、重试 20 次越试越贵的根因;失败行现在恒带原始输出形状),把面板自称「注入生效文本」的只读区改成后端单源渲染(并修它从不显示人格行),评审另收掉两处「坏了却不报」:整理报告侧的存在性探测口(`strictScope` 下按调用方可见性过滤落点并重算计数)、画像保存不判 `res.ok` 却提示已保存 |
279
+
278
280
  | **v0.8.x** | 作用域隔离(agent / workspace 双维 + opt-in 硬过滤)、冲突队列人工裁决、生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断) |
279
281
 
280
282
  ## 📦 安装
@@ -371,6 +373,7 @@ dsh plugin --profile web add "C:/path/to/modusensus-dsh-mneme-0.9.0.tgz"
371
373
  | `summarizeMaxEntriesPerRun` | `0` | 单次蒸馏产出记忆条数上限(0-50,0=不限):节流防单会话大量重复条目(#127) |
372
374
  | `summarizeMinWindowChars` | `0` | 蒸馏前零 LLM 预判:窗口可蒸馏文本不足此字符数直接跳过调用(0-100000,0=关);skip 原因写入审计(#239) |
373
375
  | `summarizeMaxRunsPerSession` | `0` | 每会话最多发起多少次蒸馏 LLM 调用(0-1000,0=不限);只计真实调用,被预判拦下的不占额度(#239) |
376
+ | `summarizeParseFailureMaxRetries` | `0` | 连续几次解析失败后放弃当前窗口(0-10,0=关):确定性失败(模型把转录当成该自己回话,`finish_reason=stop`)重试收益为 0,而窗口只会越攒越大;设 2-3 可止血,达到阈值强制消费游标跳过,失败行 metadata 带 `raw_head`/`raw_has_bracket`/`streak`(#493) |
374
377
  | `summarizePeakHours` | 空 | 高峰时段(本地时间,逗号分隔、可带星期前缀,支持跨零点):空=关;命中时蒸馏顺延到非高峰、窗口累积后一次蒸、skip 原因入审计。例:按高峰计费的供应商可写 `mon-fri 08:00-12:00,14:00-18:00`(如 DeepSeek,以其官方定价页为准)(#239) |
375
378
  | `summarizePeakMaxDeferMinutes` | `120` | 高峰顺延上限(0-1440 分钟,0=不设上限):到点仍处高峰就照常跑,避免长高峰把蒸馏饿死(#239) |
376
379
  | `summarizeDedupeMode` | `off` | 落库前去重档位:`off`(默认=现状)/ `title`(零成本,仅拦完全同名)/ `vector`(复用 embedding 列做同会话语义近邻,无 LLM 调用,#127) |
@@ -676,7 +679,7 @@ src/
676
679
  ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
677
680
  └── index.js # 插件接线
678
681
  lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
679
- test/ # 2257 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
682
+ test/ # 2302 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
680
683
  scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
681
684
  ```
682
685
 
@@ -685,7 +688,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
685
688
  ```bash
686
689
  cd dsh-mneme
687
690
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
688
- npm test # 运行 2257 个测试
691
+ npm test # 运行 2302 个测试
689
692
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
690
693
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
691
694
  ```
@@ -92,6 +92,7 @@
92
92
  | `summarizeMaxEntriesPerRun` | `0` | 单次蒸馏最多落库条数(≤50) | 0 = 不限 |
93
93
  | `summarizeMinWindowChars` | `0` | 窗口可蒸馏文本下限(字符),不足直接跳过 LLM(#239) | 被挡窗口照常消费游标并留 skip 审计 |
94
94
  | `summarizeMaxRunsPerSession` | `0` | 每会话蒸馏次数预算,只计真实发起的调用(≤1000) | 0 = 不限;进程内计数,重启清零 |
95
+ | `summarizeParseFailureMaxRetries` | `0` | 连续解析失败几次后放弃当前窗口(0–10,0 = 关,#493) | 达到阈值强制消费游标跳过该窗口;失败行 metadata 恒带 `raw_head`/`raw_has_bracket`/`streak` |
95
96
  | `summarizePeakHours` | `""` | 高峰时段串(`"09:00-18:00"` / `"mon-fri 08:00-12:00,14:00-18:00"`,支持跨零点) | 命中高峰不调 LLM、顺延补跑;任一写法非法整串按未配置处理 |
96
97
  | `summarizePeakMaxDeferMinutes` | `120` | 高峰顺延上限(分钟,≤1440) | 0 = 不设上限;到点仍处高峰照跑(bypassPeak) |
97
98
  | `summarizeDedupeMode` | `off` | 落库前去重档位:`off` / `title`(拦完全同名)/ `vector`(同会话语义近邻) | `vector` 复用已有 embedding 列,无 LLM 调用 |
@@ -140,6 +141,8 @@
140
141
  | `escapePromptVariables` | `true` | 注入内容连续花括号转义(#162) | 关 = 原样透传(DSH interpolate 可能 throw) |
141
142
  | `injectTimePrefix` | `false` | 对话开始注入一次当前时间(#34,v0.7.11 误删后随 #333 恢复) | opt-in;格式与键名与 v0.7.2 一致,存量 feature_flags 恢复即生效 |
142
143
  | `personaEnabled` | `false` | 人格段:用户自写的一小段常驻注入文本 | opt-in;只是轻量常驻文本、不引入重型后台路径,故不进 `LIGHT_MODE_OFF` |
144
+ | `personaStrictAddressing` | `false` | 人格寻址口径。关(历史行为):会话身份没建卡、或该卡被显式关掉时,落 `default` 卡 | 开:这两种情况都不注入人格,`default` 只兜「解析不到会话身份」的无名会话。用途是宿主里跑着自带身份的外部 bot 时,别让某个人格无条件进它每一帧。默认关 = 升级零变化 |
145
+ | `profileExtractEnabled` **light** | `false` | 画像提取草稿:`POST /api/dsh-mneme/profile/extract` 从活跃记忆反向写一段「助手眼中的用户」草稿(面板「用户画像」卡上的按钮) | opt-in;一次调用一轮 LLM + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它。**只出草稿**:它不写 `user_settings`,采用草稿是面板里另一次显式保存——画像正文必须是用户手写的原文。空库直接报 `no-input`、模型没按格式回报 `unparseable`,两者都不回空草稿 |
143
146
 
144
147
  ## 巩固(autoDream)
145
148
 
@@ -283,6 +286,7 @@
283
286
  | `externalApiHost` | `127.0.0.1` | 绑定地址 | 改成非回环 = 把整个记忆库暴露给网络,自负其责 |
284
287
  | `disableMemorySearch` | `false` | 对模型隐藏 `memory_search` 工具(v0.8.5) | 慢/轻量模型的工具往返节流;隐藏即不可调 |
285
288
  | `disableMemoryArchive` | `false` | 对模型隐藏 `memory_archive` 工具 | 同上 |
289
+ | `organizeEnabled` | `false` | 对模型开放 `memory_organize` 工具(#231:先 dryRun 出比对报告,agent 判断后 apply 落库) | 与上面两个**反向**:那两个藏默认在场的工具,本键开默认不在场的能力。关 = 工具根本不注册(模型看不到就不会调)。dryRun→apply 是两步协议,弱模型容易漏 `run_id` 或跳过报告,所以 `LIGHT_MODE_OFF` 里有它;apply 只能引用一次真实报告、且一份报告只许落地一次 |
286
290
  | `actionPipelineEnabled` | `false` | M.O. 只读视图的门控:做法图谱 + 对话行结算卡 | opt-in beta;只是纯 UI 入口、不引入重型后台路径,故不进 `LIGHT_MODE_OFF`。写账另有独立的执行记账键(`/mo-run`),看图 ≠ 写账 |
287
291
  | `actionRunEnabled` | `false` | M.O. 执行记账:`/mo-run` 开一次执行窗口,窗口内命令执行才可能被记成一条 run | opt-in beta;挂在热路径(每个会话事件都过一次监听),故 `LIGHT_MODE_OFF` 里有它 |
288
292
  | `actionNudgeEnabled` | `false` | M.O. 自动沉淀:每轮结束(`turn/end`)后挖一次「这个项目重复走的动作」,结果落成 `memoryDir/proposals/<项目键>/` 里的提议,等人确认 | opt-in beta;三个 M.O. 键里最重的一条——挂 `session/event` 且要**同步解压 zstd 会话日志**(本机 118 份实测 16.2s,单次预算 `budgetMs`=4s 兜底),故 `LIGHT_MODE_OFF` 里有它。**永不自动落草稿**:`materialize` 恒 false,进仓库必须是人的显式动作 |
@@ -319,6 +323,8 @@
319
323
  `graphAnchoringEnabled` · `graphWeightEnabled` · `graphInjectHint` · `graphPassiveConfirm` ·
320
324
  `actionRunEnabled` · `actionNudgeEnabled` ·
321
325
  `dreamNarrativeEnabled` · `documentMemoryEnabled` · `heatEnabled` · `continuityRescueEnabled` ·
326
+ `organizeEnabled`(#231) ·
327
+ `profileExtractEnabled`(#497) ·
322
328
  `preInjectGate.enabled`(#380) · `distillRecords.enabled`(#399)
323
329
 
324
330
  预设只是默认值而非强制:用户显式写进 feature_flags 的值在装配顺序上后展开、仍然生效
package/lib/api.js CHANGED
@@ -9,6 +9,7 @@ import { langOf } from "./lang.js";
9
9
  import { computeHeat } from "./heat.js";
10
10
  import { EFFORT_REJECT_RE, describeStreamFailure, resolveRoute, withEffortFallback } from "./dream.js";
11
11
  import { getInjectionSnapshot } from "./inject.js";
12
+ import { renderUserSettingsBlock } from "./user-settings.js";
12
13
  import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
13
14
  import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
14
15
  import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
@@ -182,7 +183,7 @@ function parseBody(text) {
182
183
  // maintenance 是第 9 个位置参数,默认 null:既有调用点(测试里的 14 处)全是位置传参、
183
184
  // 不传它,必须继续工作——不传时 /maintenance/reclaim 回 503 maintenance-unavailable,
184
185
  // 而不是把「没接线」伪装成崩溃(#386 第 4 条)。
185
- export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "", config = null, maintenance = null) {
186
+ export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "", config = null, maintenance = null, profileExtract = null) {
186
187
  const disposers = [];
187
188
 
188
189
  // #182 注入状态探测:面板状态页的「极简模式下注入按宿主设计关闭」提示需要
@@ -488,6 +489,49 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
488
489
  }
489
490
  });
490
491
 
492
+ // --- 画像提取(issue #497):一次性从活跃记忆出「助手眼中的用户」**草稿** ------
493
+ // 语义上它是「读 + 一次模型调用」,不是写端点:草稿回给面板,采用与否是用户在那里
494
+ // 另一次显式 PUT /profile。所以这个路由永远不碰 user_settings——画像正文是用户
495
+ // 手写的原文,让模型合并等于给它改写权,而改写后的文本每帧都注入。
496
+ register({
497
+ kind: "exact",
498
+ path: "/api/dsh-mneme/profile/extract",
499
+ // policy 是 `token` 而不是 `token-write`:这条路由**没有读分支**(非 POST 一律 405),
500
+ // 声明成 token-write 会让 SECURITY.md 的开放读清单按「GET 开放」把它算进去——
501
+ // 那张表是代码的投影,不是愿望清单(test/security-doc.test.js 就是钉这件事的)。
502
+ policy: "token",
503
+ handler(req, res) {
504
+ // 方法白名单在碰任何副作用之前:#489 的教训是「只拦 POST/PUT,其余直落
505
+ // run(null)」——那等于给带 token 的 PATCH/DELETE 开了一条静默花一次 LLM 的口子。
506
+ // 顺序也要紧:405 若挪到 readBody 之后,一个不带 body 的方法会把请求永远挂在
507
+ // 读体上(把这道判断改成只拦 GET 时实测卡住整条用例),等于一个免费的挂起面。
508
+ if (req.method !== "POST") {
509
+ res.writeHead(405, { Allow: "POST" });
510
+ res.end("");
511
+ return undefined;
512
+ }
513
+ if (config?.profileExtractEnabled !== true) return sendJson(res, 409, { error: "disabled" });
514
+ if (typeof profileExtract !== "function") return sendJson(res, 503, { error: "unavailable" });
515
+ return readBody(req, res).then((text) => {
516
+ if (text === null) return undefined; // 超限已在 readBody 里回过 413
517
+ return profileExtract().then(
518
+ (out) => sendJson(res, 200, out),
519
+ (error) => {
520
+ // 「没东西可提」与「模型没配合」必须能被调用方区分:把它们都压成 200 +
521
+ // 空草稿,面板就会把故障显示成「这次没提炼出东西」(#493 同一条教训)。
522
+ const code = typeof error?.code === "string" ? error.code : "";
523
+ if (code === "no-input") return sendJson(res, 409, { error: code });
524
+ // 没配模型是「环境缺东西」,与「模型不配合」两码:前者重跑也一样,后者可重试。
525
+ if (code === "no-route") return sendJson(res, 503, { error: code });
526
+ if (code === "unparseable" || code === "stream-failed") return sendJson(res, 502, { error: code });
527
+ ctx.logger?.warn?.(`dsh-mneme: profile extract failed: ${String(error?.message ?? error)}`);
528
+ return sendJson(res, 500, { error: "internal" });
529
+ }
530
+ );
531
+ });
532
+ }
533
+ });
534
+
491
535
  // --- agent persona(人格定义页,beta) -------------------------------------
492
536
  // 形状照抄 /profile 路由(设计稿②):写方法走 requireAuth + readBody + parseBody。
493
537
  // v1.2 多卡:GET 回全量 { default, byAgent };PUT 双形——单对象形(旧面板)只更新
@@ -519,7 +563,20 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
519
563
  sendJson(res, 200, { persona: settings.getPersona() });
520
564
  });
521
565
  }
522
- sendJson(res, 200, { persona: settings.getPersona() });
566
+ const url = new URL(req.url, "http://localhost");
567
+ // 预览(面板「Agent 眼中的画像」用):由注入器同一个渲染函数产出,前端只显示
568
+ // 返回的文本——以前端自己拼列表冒充「生效文本」是这个面板最容易漂的地方。
569
+ // ?agent=<身份> 让面板能逐张卡预览:传了就按那个身份寻址,没传按「解析不到身份」
570
+ // 走(也就是 default 卡的受众)。
571
+ // 这里不加鉴权是有意的:画像、规则、人格三处在同一条开放读面上本来就能各读一次
572
+ // (GET /profile 与 GET /persona 都无 token 可读),本字段只是把同样的内容拼成
573
+ // 模型实际看到的形状,没有新增可达面;真要锁,配 apiToken(写面与它同源)。
574
+ const preview = renderUserSettingsBlock({
575
+ settings,
576
+ config,
577
+ agentId: url.searchParams.get("agent")
578
+ });
579
+ sendJson(res, 200, { persona: settings.getPersona(), preview });
523
580
  } catch {
524
581
  sendJson(res, 500, { error: "internal" });
525
582
  }
package/lib/client.js CHANGED
@@ -281,6 +281,19 @@ window.__ModuleLoader__.load({
281
281
  "memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
282
282
  "memory.settings.profileSave": "保存画像",
283
283
  "memory.settings.profileSaved": "画像已保存",
284
+ "memory.settings.profileExtract": "从记忆提取草稿",
285
+ "memory.settings.profileExtracting": "正在提取…",
286
+ "memory.settings.profileExtractHint": "会花一次模型调用,只在你点这个按钮时发生;提取出来的是草稿,不采用就不进库,你手写的画像也不会被覆盖。",
287
+ "memory.settings.profileExtractDisabled": "没有提取:功能开关里的「画像提取草稿」还没打开。",
288
+ "memory.settings.profileExtractEmpty": "库里没有可用的记忆条目,这次没有能提炼的内容(空输入下硬写一段画像只会是编的)。",
289
+ "memory.settings.profileExtractNoRoute": "没有可用的模型:先在宿主里选一个默认模型(或在配置里填 dreamProvider / dreamModel),再点一次。",
290
+ "memory.settings.profileSaveFailed": "画像没存进去",
291
+ "memory.settings.profileExtractFailed": "提取没成:模型没按格式返回,或这次调用失败了。你现有的画像一个字都没被改动。",
292
+ "memory.settings.profileDraftTitle": "草稿(还没保存)",
293
+ "memory.settings.profileDraftRows": "依据记忆条数",
294
+ "memory.settings.profileDraftTruncated": "条目较多,这次只读了前面一部分",
295
+ "memory.settings.profileDraftApply": "采用为画像",
296
+ "memory.settings.profileDraftDiscard": "丢弃草稿",
284
297
  "memory.settings.persona": "人格定义(beta)",
285
298
  "memory.settings.personaHint": "给这位 Agent 一张角色卡:名字、性格、回复要求。开关打开且下方「人格定义」总闸开启时,每轮对话都会注入。",
286
299
  "memory.settings.personaName": "名字",
@@ -294,7 +307,7 @@ window.__ModuleLoader__.load({
294
307
  "memory.settings.personaSaveFailed": "保存失败",
295
308
  "memory.settings.personaEnabledToggle": "启用这张卡",
296
309
  "memory.settings.personaEnabledOn": "已启用——身份命中的会话会注入这张卡",
297
- "memory.settings.personaEnabledOff": "未启用——这个身份的会话将使用默认卡",
310
+ "memory.settings.personaEnabledOff": "未启用——默认口径下这个身份的会话会改用默认卡;开着「严格人格寻址」时则完全不注入人格",
298
311
  "memory.settings.personaDefaultCard": "默认卡(兜底)",
299
312
  "memory.settings.personaAdd": "添加卡",
300
313
  "memory.settings.personaAddPlaceholder": "输入 agent 身份名(如 nyx),按它寻址注入",
@@ -302,10 +315,21 @@ window.__ModuleLoader__.load({
302
315
  "memory.settings.personaEditingFor": "正在编辑身份卡:",
303
316
  "memory.settings.personaDeleteCard": "删除这张卡",
304
317
  "memory.settings.personaView": "Agent 眼中的你(注入生效文本)",
305
- "memory.settings.personaViewHint": "以下是每一轮实际注入的用户画像与规则,只读。",
306
- "memory.settings.personaViewEmpty": "画像与规则均为空——Agent 每轮只会看到记忆库。",
318
+ "memory.settings.personaViewHint": "下面是这一轮真正会送进模型的整段文本:人格 → 画像 → 规则(后写的覆盖前写的,规则最硬)。文本由插件渲染,前端只显示。",
319
+ "memory.settings.personaViewEmpty": "三项都是空的——Agent 每轮只会看到记忆库。",
320
+ "memory.settings.personaViewUnknown": "读不到注入文本(预览请求失败或后端版本过旧)。这里不显示猜测的内容。",
321
+ "memory.settings.personaViewSourceDefault": "当前按「解析不到会话身份」的会话预览,取默认卡。",
322
+ "memory.settings.personaViewSourceFallback": "这个身份解析到了,但它没有可用的卡(没建或已关掉),所以落回默认卡——默认口径下这正是「关掉这张卡」不生效的原因;打开「严格人格寻址」后这种情况改为不注入人格。",
323
+ "memory.settings.personaViewSourceCard": "当前按你正在编辑的这张身份卡预览。",
324
+ "memory.settings.personaViewSourceNone": "按当前寻址口径,这个身份不会注入任何人格——默认卡只兜「解析不到会话身份」的会话。",
325
+ "memory.settings.personaViewGateOff": "总闸「人格定义(beta)」现在是关的:下面这段里不会出现人格,只有画像与规则。",
326
+ "memory.settings.personaViewRefresh": "刷新预览",
307
327
  "memory.features.personaEnabled": "人格定义(beta)",
308
328
  "memory.features.personaEnabled.hint": "把「设置 → 个性化」里的人格卡注入到每轮对话。默认关(beta);总闸与人格卡内的开关同时打开才生效。",
329
+ "memory.features.personaStrictAddressing": "严格人格寻址",
330
+ "memory.features.personaStrictAddressing.hint": "开:会话身份没有对应人格卡、或那张卡被关掉时,就不注入人格——默认卡只兜「解析不到会话身份」的会话。宿主里跑着自带身份的 bot(DS Bot 那类)时关掉这种兜底,免得两句「你是谁」撞在同一轮。默认关 = 未命中就落默认卡(历史行为)。",
331
+ "memory.features.profileExtractEnabled": "画像提取草稿",
332
+ "memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 → 用户画像」里的「从记忆提取草稿」按钮:读一批活跃记忆、花一次模型调用,产一段「助手眼中的用户」草稿。草稿要你点「采用」才会写进画像,绝不自动覆盖你手写的原文。轻量档预设关(用户显式勾选仍然赢)。",
309
333
  "memory.settings.rules": "规则",
310
334
  "memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
311
335
  "memory.settings.ruleAdd": "添加规则",
@@ -975,6 +999,19 @@ window.__ModuleLoader__.load({
975
999
  "memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
976
1000
  "memory.settings.profileSave": "Save Profile",
977
1001
  "memory.settings.profileSaved": "Profile saved",
1002
+ "memory.settings.profileExtract": "Draft from memory",
1003
+ "memory.settings.profileExtracting": "Extracting…",
1004
+ "memory.settings.profileExtractHint": "Costs one model call, and only when you press this button. The result is a draft: nothing is stored until you apply it, and your hand-written profile is never overwritten.",
1005
+ "memory.settings.profileExtractDisabled": "Nothing extracted: the “Profile draft from memory” feature switch is still off.",
1006
+ "memory.settings.profileExtractEmpty": "No usable memory entries in the store, so there was nothing to draft from (writing a profile out of an empty input would only be invention).",
1007
+ "memory.settings.profileExtractNoRoute": "No model available: pick a default model in the host (or set dreamProvider / dreamModel), then try again.",
1008
+ "memory.settings.profileSaveFailed": "Profile was not saved",
1009
+ "memory.settings.profileExtractFailed": "Extraction failed: the model did not return the required shape, or the call errored. Your existing profile is untouched.",
1010
+ "memory.settings.profileDraftTitle": "Draft (not saved)",
1011
+ "memory.settings.profileDraftRows": "Based on entries",
1012
+ "memory.settings.profileDraftTruncated": "many entries — only the first batch was read",
1013
+ "memory.settings.profileDraftApply": "Apply as profile",
1014
+ "memory.settings.profileDraftDiscard": "Discard draft",
978
1015
  "memory.settings.persona": "Persona (beta)",
979
1016
  "memory.settings.personaHint": "Give this agent a character card: name, personality, reply style. Injected every turn when the toggle below and the \"Persona\" master switch are both on.",
980
1017
  "memory.settings.personaName": "Name",
@@ -988,7 +1025,7 @@ window.__ModuleLoader__.load({
988
1025
  "memory.settings.personaSaveFailed": "Save failed",
989
1026
  "memory.settings.personaEnabledToggle": "Enable this card",
990
1027
  "memory.settings.personaEnabledOn": "On — sessions matching this identity get this card",
991
- "memory.settings.personaEnabledOff": "Off — sessions with this identity fall back to the default card",
1028
+ "memory.settings.personaEnabledOff": "Off — by default such sessions fall back to the default card; with strict persona addressing they get no persona at all",
992
1029
  "memory.settings.personaDefaultCard": "Default (fallback)",
993
1030
  "memory.settings.personaAdd": "Add card",
994
1031
  "memory.settings.personaAddPlaceholder": "Agent identity (e.g. nyx) used to address injection",
@@ -996,10 +1033,21 @@ window.__ModuleLoader__.load({
996
1033
  "memory.settings.personaEditingFor": "Editing identity card:",
997
1034
  "memory.settings.personaDeleteCard": "Delete this card",
998
1035
  "memory.settings.personaView": "You, as the agent sees you (injected text)",
999
- "memory.settings.personaViewHint": "The profile and rules actually injected every turn. Read-only.",
1000
- "memory.settings.personaViewEmpty": "Profile and rules are both empty — the agent only sees the memory store.",
1036
+ "memory.settings.personaViewHint": "This is the exact block handed to the model this turn: persona → profile → rules (later lines override earlier ones; rules are hardest). The plugin renders it; the panel only displays it.",
1037
+ "memory.settings.personaViewEmpty": "All three are empty — the agent only sees the memory store.",
1038
+ "memory.settings.personaViewUnknown": "Could not read the injected text (preview request failed or backend too old). Nothing is guessed here.",
1039
+ "memory.settings.personaViewSourceDefault": "Preview for sessions whose identity cannot be resolved — the default card applies.",
1040
+ "memory.settings.personaViewSourceFallback": "This identity resolved, but it has no usable card (never created, or switched off), so the default card applies — which is exactly why turning this card off changes nothing under the default mode. With strict persona addressing on, such a session gets no persona at all.",
1041
+ "memory.settings.personaViewSourceCard": "Preview for the identity card you are editing.",
1042
+ "memory.settings.personaViewSourceNone": "Under the current addressing mode this identity injects no persona at all — the default card only covers sessions with no resolvable identity.",
1043
+ "memory.settings.personaViewGateOff": "The master switch “Persona (beta)” is off: the block below contains profile and rules only.",
1044
+ "memory.settings.personaViewRefresh": "Refresh preview",
1001
1045
  "memory.features.personaEnabled": "Persona (beta)",
1002
1046
  "memory.features.personaEnabled.hint": "Inject the persona card (Settings → Personalization) into every turn. Off by default (beta); both this switch and the card's own toggle must be on.",
1047
+ "memory.features.personaStrictAddressing": "Strict persona addressing",
1048
+ "memory.features.personaStrictAddressing.hint": "On: when a session's identity has no persona card — or that card is switched off — no persona is injected, so the default card only covers sessions with no resolvable identity. Turn this on when the host runs a bot that brings its own identity (DS Bot and friends), otherwise two \"you are ...\" claims land in the same turn. Off (default): unresolved identities fall back to the default card, as before.",
1049
+ "memory.features.profileExtractEnabled": "Profile draft from memory",
1050
+ "memory.features.profileExtractEnabled.hint": "Adds the \"Draft from memory\" button to Settings → Personalization → profile: reads a batch of active memories, spends one model call, and returns an \"as the assistant sees them\" draft. Nothing is written until you press Apply, so your hand-written profile is never overwritten automatically. Off in the lightweight preset (an explicit choice still wins).",
1003
1051
  "memory.settings.rules": "Rules",
1004
1052
  "memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
1005
1053
  "memory.settings.ruleAdd": "Add Rule",
@@ -2963,7 +3011,8 @@ window.__ModuleLoader__.load({
2963
3011
  { key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled",
2964
3012
  // 人格定义页(beta):注入总闸。分组随 enhance(行为开关,非座位闸门);
2965
3013
  // 人格内容在设置页「个性化」区的人格卡。
2966
- "personaEnabled"] },
3014
+ // 严格寻址是同一条链上的第二个口径开关(只在总闸打开时起作用)。
3015
+ "personaEnabled", "personaStrictAddressing", "profileExtractEnabled"] },
2967
3016
  { key: "group.dream", items: ["sleepModeEnabled"] },
2968
3017
  // v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
2969
3018
  { key: "group.scope", items: ["scopeEnabled", "strictScope"] }
@@ -3504,6 +3553,12 @@ window.__ModuleLoader__.load({
3504
3553
  const [newRule, setNewRule] = react.useState("");
3505
3554
  const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
3506
3555
  const [saved, setSaved] = react.useState(false);
3556
+ // 画像提取草稿(#497):null = 没有结果;否则带 kind 的一次性结果,四种结局各一句
3557
+ // 话——把「没启用」「库里没东西」「模型没配合」压成同一句「没有草稿」,用户就只能瞎猜。
3558
+ const [profileDraft, setProfileDraft] = react.useState(null);
3559
+ const [profileExtracting, setProfileExtracting] = react.useState(false);
3560
+ // 保存失败要可见(persona 那条一直有,画像这条以前是静默 catch)。
3561
+ const [profileError, setProfileError] = react.useState("");
3507
3562
  const [cmdError, setCmdError] = react.useState("");
3508
3563
  // 人格卡(beta,v1.2 多卡):default 卡草稿 + byAgent 卡列表 + 当前编辑位;
3509
3564
  // provenance 是 v1.5 协作式人格的预留位,v1 只透传不编辑。
@@ -3513,6 +3568,11 @@ window.__ModuleLoader__.load({
3513
3568
  const [personaNewName, setPersonaNewName] = react.useState("");
3514
3569
  const [personaSaved, setPersonaSaved] = react.useState(false);
3515
3570
  const [personaError, setPersonaError] = react.useState("");
3571
+ // 注入预览:文本由后端渲染(GET /persona 的 preview 字段),前端只显示、不自己拼。
3572
+ // 原先这里是一段前端手拼的 profile + rules 列表,注释却写着「注入生效文本」——
3573
+ // 它既没有表头、没有「- 用户画像:」前缀,也完全没有人格三行,两份渲染代码必然漂。
3574
+ // null = 还没读到或读失败(fail-closed:显示「未知」而不是「空」,也不显示旧值)。
3575
+ const [injectedPreview, setInjectedPreview] = react.useState(null);
3516
3576
  const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
3517
3577
  const [vectorSaved, setVectorSaved] = react.useState(false);
3518
3578
  const [reindexing, setReindexing] = react.useState(false);
@@ -3603,11 +3663,34 @@ window.__ModuleLoader__.load({
3603
3663
  setPersonaDefault(norm(dRaw));
3604
3664
  setPersonaByAgent(Object.fromEntries(Object.entries(bRaw).map(([k, c]) => [k, norm(c)])));
3605
3665
  setPersonaEditing("default");
3666
+ // 预览不在这里写:它只有一个所有者(loadPreview)。两处写同一个 state 就会
3667
+ // 互相覆盖,而这张卡的标题是「注入生效文本」——显示别张卡的文本比不显示更糟。
3606
3668
  } catch { /* ignore */ }
3607
3669
  }, []);
3608
3670
 
3609
3671
  react.useEffect(() => { load(); }, [load]);
3610
3672
 
3673
+ // 预览请求:切卡与点「刷新预览」时各一次。刻意不挂在卡的数据上——卡内改动是
3674
+ // 逐字自动保存的,那样等于每敲一个字发一次 GET。
3675
+ // 序号守卫:先切 A 再切 B 时,A 的 GET 可能后回来;没有这道判断,界面上就是
3676
+ // 「编辑位显示 B、预览显示 A」。
3677
+ const previewSeq = react.useRef(0);
3678
+ const loadPreview = react.useCallback(async (agentId) => {
3679
+ const seq = ++previewSeq.current;
3680
+ const q = agentId && agentId !== "default" ? `?agent=${encodeURIComponent(agentId)}` : "";
3681
+ try {
3682
+ const res = await apiFetch(`/api/dsh-mneme/persona${q}`);
3683
+ const body = await res.json();
3684
+ if (seq !== previewSeq.current) return;
3685
+ setInjectedPreview(body && body.preview && typeof body.preview.text === "string" ? body.preview : null);
3686
+ } catch {
3687
+ if (seq !== previewSeq.current) return;
3688
+ setInjectedPreview(null);
3689
+ }
3690
+ }, []);
3691
+
3692
+ react.useEffect(() => { loadPreview(personaEditing); }, [personaEditing, loadPreview]);
3693
+
3611
3694
  // 帮助与反馈卡片的版本号:独立小请求,失败不影响其它卡片。
3612
3695
  react.useEffect(() => {
3613
3696
  let cancelled = false;
@@ -3665,17 +3748,76 @@ window.__ModuleLoader__.load({
3665
3748
  return () => { cancelled = true; };
3666
3749
  }, []);
3667
3750
 
3668
- async function saveProfile() {
3751
+ // 返回「是否真的存下了」。apiFetch 只是 fetch 的薄壳,非 2xx 不会抛——同页的
3752
+ // savePersonaCards 一直判 `res.ok`,画像这条一直没判,于是服务端 403/413 时界面
3753
+ // 照样闪「画像已保存」(评审实测的同类洞,这次一起收)。
3754
+ async function saveProfile(next = profile) {
3669
3755
  try {
3670
- await apiFetch("/api/dsh-mneme/profile", {
3756
+ const res = await apiFetch("/api/dsh-mneme/profile", {
3671
3757
  method: "PUT",
3672
3758
  headers: { "Content-Type": "application/json" },
3673
- body: JSON.stringify({ profile })
3759
+ body: JSON.stringify({ profile: next })
3674
3760
  });
3761
+ if (!res.ok) throw new Error("HTTP " + res.status);
3675
3762
  setSaved(true);
3763
+ setProfileError("");
3676
3764
  announce(t("memory.settings.profileSaved"));
3677
3765
  setTimeout(() => setSaved(false), 1500);
3678
- } catch { /* ignore */ }
3766
+ return true;
3767
+ } catch (err) {
3768
+ setProfileError((err && err.message) || "failed");
3769
+ setTimeout(() => setProfileError(""), 4000);
3770
+ return false;
3771
+ }
3772
+ }
3773
+
3774
+ // 画像提取:一次宿主 LLM 调用,只在用户点按钮时发生。开关没开时后端直接回
3775
+ // 409,这里显示「未启用」而不是转圈后什么也没有——那会被读成按钮坏了。
3776
+ async function extractProfileDraft() {
3777
+ setProfileExtracting(true);
3778
+ setProfileDraft(null);
3779
+ try {
3780
+ const res = await apiFetch("/api/dsh-mneme/profile/extract", {
3781
+ method: "POST",
3782
+ headers: { "Content-Type": "application/json" },
3783
+ body: "{}"
3784
+ });
3785
+ const body = await res.json().catch(() => null);
3786
+ if (res.ok && typeof body?.draft === "string" && body.draft.trim()) {
3787
+ setProfileDraft({
3788
+ kind: "draft",
3789
+ text: body.draft,
3790
+ rowsUsed: Number.isInteger(body.rowsUsed) ? body.rowsUsed : 0,
3791
+ truncated: body.truncated === true
3792
+ });
3793
+ return;
3794
+ }
3795
+ const error = body && typeof body.error === "string" ? body.error : "";
3796
+ // 五种结局各一句话:没启用 / 库里没内容 / 没配模型 / 模型不配合或调用失败 /
3797
+ // 成功草稿。压在一起就是把「环境缺东西」报成「这次没提炼出东西」。
3798
+ const kind = error === "disabled" ? "disabled"
3799
+ : error === "no-input" ? "empty"
3800
+ : error === "no-route" ? "noroute"
3801
+ : "failed";
3802
+ setProfileDraft({ kind });
3803
+ } catch {
3804
+ setProfileDraft({ kind: "failed" });
3805
+ } finally {
3806
+ setProfileExtracting(false);
3807
+ }
3808
+ }
3809
+
3810
+ // 采用草稿 = 显式写画像。两件事都要对:把草稿文本直接交给保存(不能 setProfile
3811
+ // 之后再调无参保存——那读的是这次渲染里的旧闭包值,会存回旧文本),以及**只有
3812
+ // 真的存下了才提交界面状态**(否则一次 403 就留下「框里是草稿、库里是旧文、
3813
+ // 草稿已被清掉」的三不像,关掉面板后彻底找不回来)。
3814
+ async function applyProfileDraft() {
3815
+ if (profileDraft?.kind !== "draft") return;
3816
+ const text = profileDraft.text;
3817
+ if (await saveProfile(text)) {
3818
+ setProfile(text);
3819
+ setProfileDraft(null);
3820
+ }
3679
3821
  }
3680
3822
 
3681
3823
  // 人格卡保存(v1.2 多卡):全量多卡形 PUT({default, byAgent},后端全量
@@ -3931,9 +4073,39 @@ window.__ModuleLoader__.load({
3931
4073
  onChange: (e) => setProfile(e.target.value)
3932
4074
  }),
3933
4075
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
3934
- h("button", { className: "mneme-btn", onClick: saveProfile }, t("memory.settings.profileSave")),
3935
- saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
3936
- )
4076
+ h("button", { className: "mneme-btn", onClick: () => saveProfile() }, t("memory.settings.profileSave")),
4077
+ saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved")),
4078
+ profileError ? h("span", { className: "mneme-saved", style: { color: "var(--dsw-danger, #d33)" } },
4079
+ t("memory.settings.profileSaveFailed") + " (" + profileError + ")") : null
4080
+ ),
4081
+ // 从记忆提取草稿(#497)。两种结局都要显眼:草稿(要用户点「采用」才落库)
4082
+ // 与失败原因(开关没开 / 没内容可提 / 模型没配合)。手写的原文在这之上一直
4083
+ // 可见——采用前它不会被任何东西覆盖。
4084
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
4085
+ h("button", {
4086
+ type: "button",
4087
+ className: "mneme-btn",
4088
+ disabled: profileExtracting,
4089
+ onClick: extractProfileDraft
4090
+ }, t("memory.settings.profileExtract")),
4091
+ profileExtracting ? h("span", { className: "mneme-saved" }, t("memory.settings.profileExtracting")) : null
4092
+ ),
4093
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractHint")),
4094
+ profileDraft?.kind === "disabled" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractDisabled")) : null,
4095
+ profileDraft?.kind === "empty" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractEmpty")) : null,
4096
+ profileDraft?.kind === "noroute" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractNoRoute")) : null,
4097
+ profileDraft?.kind === "failed" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractFailed")) : null,
4098
+ profileDraft?.kind === "draft" ? h("div", null,
4099
+ h("div", { className: "mneme-featnumlabel" }, t("memory.settings.profileDraftTitle")),
4100
+ h("div", { className: "mneme-set-desc" },
4101
+ t("memory.settings.profileDraftRows") + ": " + profileDraft.rowsUsed
4102
+ + (profileDraft.truncated ? " · " + t("memory.settings.profileDraftTruncated") : "")),
4103
+ h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } }, profileDraft.text),
4104
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 6 } },
4105
+ h("button", { type: "button", className: "mneme-btn", onClick: applyProfileDraft }, t("memory.settings.profileDraftApply")),
4106
+ h("button", { type: "button", className: "mneme-btn", onClick: () => setProfileDraft(null) }, t("memory.settings.profileDraftDiscard"))
4107
+ )
4108
+ ) : null
3937
4109
  ),
3938
4110
  // 人格卡(beta,v1.2 多卡)— who the agent is:多张角色卡按会话身份注入。
3939
4111
  // 卡内开关只是该卡 enabled 半边;总闸 personaEnabled 在功能开关组,双闸同真。
@@ -4024,15 +4196,40 @@ window.__ModuleLoader__.load({
4024
4196
  t("memory.settings.personaSaveFailed") + " (" + personaError + ")")
4025
4197
  )
4026
4198
  ),
4027
- // Agent 眼中的画像(只读)— 注入生效文本:profile + rules 原样展示。
4028
- // 纯前端拼接(GET 已在 load 里拿到 profile/rules),零后端新请求。
4199
+ // Agent 眼中的你(只读)— 注入生效文本由后端渲染(GET /persona 的 preview 字段),
4200
+ // 前端只显示。以前这一段是前端自己拼的 profile + rules,注释却写着「注入生效文本」:
4201
+ // 没有人格三行、没有表头、没有「- 用户画像:」前缀,两份实现必然漂。
4029
4202
  h("section", { className: "mneme-set-card" },
4030
4203
  h("div", { className: "mneme-set-title" }, t("memory.settings.personaView")),
4031
4204
  h("div", { className: "mneme-set-desc" }, t("memory.settings.personaViewHint")),
4032
- (profile.trim() || rules.length)
4033
- ? h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } },
4034
- [profile.trim() ? "- " + profile.trim() : "", ...rules.map((r) => "- " + r)].filter(Boolean).join("\n"))
4035
- : h("div", { className: "mneme-xempty" }, t("memory.settings.personaViewEmpty"))
4205
+ injectedPreview === null
4206
+ ? h("div", { className: "mneme-xempty" }, t("memory.settings.personaViewUnknown"))
4207
+ : h("div", null,
4208
+ h("div", { className: "mneme-set-desc" },
4209
+ injectedPreview.source === "byAgent"
4210
+ ? t("memory.settings.personaViewSourceCard")
4211
+ : injectedPreview.source === "none"
4212
+ ? t("memory.settings.personaViewSourceNone")
4213
+ // source=default 有两种来路,必须分开说:请求带了 ?agent=<身份>
4214
+ // 却仍落默认卡(身份解析到了,只是没建卡或卡被关掉——关卡的人最想
4215
+ // 确认的就是这一格),与真的解析不到会话身份。
4216
+ : injectedPreview.agent
4217
+ ? t("memory.settings.personaViewSourceFallback")
4218
+ : t("memory.settings.personaViewSourceDefault")),
4219
+ injectedPreview.configGate === true
4220
+ ? null
4221
+ : h("div", { className: "mneme-set-desc" }, t("memory.settings.personaViewGateOff")),
4222
+ injectedPreview.text
4223
+ ? h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } }, injectedPreview.text)
4224
+ : h("div", { className: "mneme-xempty" }, t("memory.settings.personaViewEmpty"))
4225
+ ),
4226
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 6 } },
4227
+ h("button", {
4228
+ type: "button",
4229
+ className: "mneme-btn",
4230
+ onClick: () => loadPreview(personaEditing)
4231
+ }, t("memory.settings.personaViewRefresh"))
4232
+ )
4036
4233
  ),
4037
4234
  // 规则 — numbered rows, hover reveals the delete affordance
4038
4235
  h("section", { className: "mneme-set-card" },
package/lib/config.js CHANGED
@@ -35,6 +35,14 @@ export const Config = z.object({
35
35
  // 额度浪费在零成本窗口上。计数是进程内 per-session(与最小间隔闸门同生命
36
36
  // 周期),宿主重启即清零,与 #229 的游标持久化是两件事。
37
37
  summarizeMaxRunsPerSession: z.natural().min(0).max(1000).default(0),
38
+ // Issue #493:连续解析失败几次后放弃当前窗口(0 = 关闭,行为与历史版本逐字节
39
+ // 一致)。失败默认不推进游标——语义是「这段还没提炼完,下次接着来」,这对
40
+ // 偶发失败(429/5xx/超时)是对的。但确定性失败(模型把转录当成该自己回话,
41
+ // finish_reason=stop,同文同温)重试收益为 0,而窗口只会随新事件继续变大,
42
+ // 于是一条会话能被同一窗口卡死几十轮、每轮都白烧一次完整调用。设成 2-3 即
43
+ // 可止血:达到阈值后强制消费游标跳过该窗口,并留一行带 streak/raw_head 的
44
+ // error 审计。默认关是因为放弃内容本身有代价——短窗口偶尔失败仍值得重试。
45
+ summarizeParseFailureMaxRetries: z.natural().min(0).max(10).default(0),
38
46
  // Issue #239(第 4 项,错峰队列):高峰期蒸馏顺延。空串 = 关闭,行为与现状逐
39
47
  // 字节一致。取值是本地时间的时段列表(逗号分隔,支持跨零点),可带**星期前缀**
40
48
  // (可省;ISO 1=周一…7=周日,也认 mon..sun;省略 = 每天):
@@ -841,6 +849,19 @@ export const Config = z.object({
841
849
  disableMemorySearch: z.boolean().default(false),
842
850
  disableMemoryArchive: z.boolean().default(false),
843
851
 
852
+ // Issue #231 第二批:agent 主动整理接口(src/organize.js)的**暴露**闸。引擎在
853
+ // PR #267 就造好了,当时刻意不进工具列表、不加开关(模块头注释:「与 #249 同批
854
+ // 暴露时只差『注册工具 + 注入指引』一步」),配置面留到这一批再定——就是这里。
855
+ // 与上面两个 disable* 是**反向**语义:那两个把默认在场的工具藏起来,本键把默认
856
+ // 不在场的能力开出来。因为 dryRun→apply 是一套两步协议(apply 必须引用真实报告
857
+ // 的 run_id),让 agent 未经用户同意就能整理记忆,不是「少一次往返」的成本问题,
858
+ // 而是「谁在改库」的授权问题。
859
+ // 进 LIGHT_MODE_OFF:两步协议在弱模型上最容易走歪(漏 run_id、或跳过报告直接
860
+ // apply——那一拒绝就是一次失败的工具调用),而一次 dryRun 的代价是一回嵌入调用
861
+ // 加按 type 有界扫库。这是预设给的默认值、不是强制:用户显式勾选仍然赢(合并顺序
862
+ // 「用户开关 > 轻量预设 > bundle」,同 continuityRescueEnabled / distillRecords.enabled)。
863
+ organizeEnabled: z.boolean().default(false),
864
+
844
865
  // --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
845
866
  // 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
846
867
  // 逐字节不变。开启后**两条写入路径都自动盖章**(*_scope_source='auto'):
@@ -925,7 +946,27 @@ export const Config = z.object({
925
946
  // 人格段是用户自写的一小段轻量常驻文本,不引入任何重型后台路径,轻量档用户
926
947
  // 显式勾选仍然赢(同 actionPipelineEnabled 的定性)。
927
948
  // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
928
- personaEnabled: z.boolean().default(false)
949
+ personaEnabled: z.boolean().default(false),
950
+
951
+ // 人格寻址的兜底口径(只在上一个键打开时才有意义)。关(默认,= 历史行为):
952
+ // 会话身份没有卡、或有卡但卡被关掉时,一律落 default 卡——于是 default 成了
953
+ // 「所有没建卡的 agent 都能拿到某个人格」,宿主里跑的外部 bot(自带身份与设定)
954
+ // 会在同一帧里收到两句「你是谁」。开:这两种情况都不注入人格,default 退回它的
955
+ // 本意——只兜「这一帧解析不到会话身份」的无名会话(无身份时行为不变)。
956
+ // 默认关是因为这是一次口径变更:老用户升级后注入文本不许静默变化。
957
+ // 不进 LIGHT_MODE_OFF:它只是取卡时的一个分支,不引入常驻文本、也不引入后台路径,
958
+ // 且它本身只在 personaEnabled 打开时才起作用(同 scopeState 的定性)。
959
+ personaStrictAddressing: z.boolean().default(false),
960
+
961
+ // Issue #497:从活跃记忆反向提取「助手眼中的用户」画像**草稿**(POST
962
+ // /api/dsh-mneme/profile/extract)。默认关有三层理由:① 它每次点一次就是一次 LLM
963
+ // 调用并读上百行记忆,成本与 dream/summarize 同级,不该在没人要求时挂在面上;
964
+ // ② 产物会被用户当成「关于我的第一版画像」读,编造的代价不对称——所以空库直接报错、
965
+ // 解析失败也报错,绝不回空草稿;③ 它**不写** user_settings,采用草稿是面板里另一次
966
+ // 显式保存,画像正文必须是用户手写的原文,不给模型改写权。
967
+ // 进 LIGHT_MODE_OFF:一次调用要读上百行记忆 + 一轮模型生成,与低资源档互斥(同
968
+ // preInjectGate.enabled 的定性);是预设不是强制,用户显式勾选仍然赢。
969
+ profileExtractEnabled: z.boolean().default(false)
929
970
  });
930
971
 
931
972
  // Fields forced to false by the light-mode preset. Everything not listed here
@@ -958,6 +999,10 @@ const LIGHT_MODE_OFF = [
958
999
  "dreamNarrativeEnabled",
959
1000
  // 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
960
1001
  "documentMemoryEnabled",
1002
+ // #231 第二批:轻量档不开 agent 主动整理。理由不是成本而是协议——dryRun→apply
1003
+ // 要求 agent 拿住上一轮的 run_id 再回来,弱模型漏这一环就是一次被拒的调用;
1004
+ // 预设关、不剥夺显式开启的权利(合并顺序同 continuityRescueEnabled)。
1005
+ "organizeEnabled",
961
1006
  // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
962
1007
  "heatEnabled",
963
1008
  // 轻量模式不开注入前判定(#380:LLM-per-turn 附加路径,每帧注入多一次池级
@@ -977,6 +1022,10 @@ const LIGHT_MODE_OFF = [
977
1022
  // 是「小模型 / 小上下文 / 少常驻路径」,这与一个每事件都过一遍的监听器相冲突。
978
1023
  // 默认关,这里只是轻量预设再确认一次;用户显式勾选仍然赢(合并顺序同上面两条)。
979
1024
  "actionRunEnabled",
1025
+ // #497:轻量档不开画像提取——一次调用要读上百行记忆再生成一段画像,与低资源档
1026
+ // 互斥(同 preInjectGate.enabled 的定性)。默认关,这里只是预设再确认一次;
1027
+ // 用户显式勾选仍然赢(合并顺序同 continuityRescueEnabled)。
1028
+ "profileExtractEnabled",
980
1029
  // M.O. 自动沉淀:同样挂在 session/event 热路径上,而且解压会话日志是同步 I/O。
981
1030
  // 轻量档的取向(小模型 / 小上下文 / 少常驻路径)与它直接冲突,预设里关掉;
982
1031
  // 用户显式勾选仍然赢(合并顺序同上面几条)。