@modusensus/dsh-mneme 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +54 -21
  2. package/docs/CONFIGURATION.md +45 -4
  3. package/docs/DIRECTORIES.md +114 -0
  4. package/docs/DISTILL.md +64 -0
  5. package/docs/DSH-GO.md +97 -0
  6. package/docs/ENTITIES.md +5 -3
  7. package/docs/FORGET.md +77 -0
  8. package/docs/KB.md +110 -0
  9. package/docs/SLEEP.md +28 -3
  10. package/docs/STORAGE.md +23 -0
  11. package/lib/api-standalone.js +4 -1
  12. package/lib/api.js +744 -26
  13. package/lib/attachment-text.js +122 -0
  14. package/lib/choro-command.js +9 -2
  15. package/lib/client.js +3752 -819
  16. package/lib/config.js +171 -2
  17. package/lib/continuity.js +7 -6
  18. package/lib/diary/auto.js +190 -0
  19. package/lib/diary/compose.js +440 -0
  20. package/lib/diary/dir.js +59 -0
  21. package/lib/diary/export.js +97 -0
  22. package/lib/diary/index.js +26 -0
  23. package/lib/diary/library.js +189 -0
  24. package/lib/diary/qq-inbox.js +142 -0
  25. package/lib/diary/rollover.js +61 -0
  26. package/lib/diary/session-provider.js +122 -0
  27. package/lib/document.js +9 -3
  28. package/lib/dream/clustering.js +6 -1
  29. package/lib/dream/decisions.js +56 -5
  30. package/lib/dream/sleep.js +616 -15
  31. package/lib/dream.js +45 -13
  32. package/lib/dsh-go/host.js +162 -0
  33. package/lib/dsh-go/panel.js +2091 -0
  34. package/lib/dsh-go/text.js +342 -0
  35. package/lib/dsh-go/views/common.js +68 -0
  36. package/lib/dsh-go/views/entity.js +48 -0
  37. package/lib/dsh-go/views/memory.js +198 -0
  38. package/lib/dsh-go/views/root.js +30 -0
  39. package/lib/dsh-go/views/settings.js +398 -0
  40. package/lib/dsh-go/views/status.js +51 -0
  41. package/lib/embedding.js +42 -8
  42. package/lib/entities/extractor.js +36 -7
  43. package/lib/hot-memory.js +8 -9
  44. package/lib/index.js +131 -5
  45. package/lib/inject.js +192 -19
  46. package/lib/kb/index.js +309 -0
  47. package/lib/kb/routes.js +220 -0
  48. package/lib/kb/tools.js +148 -0
  49. package/lib/lang.js +115 -4
  50. package/lib/peak-hours.js +57 -2
  51. package/lib/profile-facts.js +356 -0
  52. package/lib/semantic.js +347 -60
  53. package/lib/service.js +302 -70
  54. package/lib/settings.js +117 -14
  55. package/lib/store.js +830 -55
  56. package/lib/summarize.js +38 -11
  57. package/lib/tools.js +173 -25
  58. package/lib/user-settings.js +175 -23
  59. package/lib/vector-index.js +76 -6
  60. package/lib/wiki/cli-hint.js +90 -0
  61. package/lib/wiki/queries.js +264 -10
  62. package/lib/wiki/t1-jobs.js +232 -0
  63. package/lib/wiki/t1.js +7 -1
  64. package/package.json +25 -1
  65. package/scripts/sync-test-badge.mjs +1 -1
  66. package/scripts/test-count-sync.mjs +9 -5
  67. package/src/api-standalone.js +4 -1
  68. package/src/api.js +744 -26
  69. package/src/attachment-text.js +122 -0
  70. package/src/choro-command.js +9 -2
  71. package/src/config.js +171 -2
  72. package/src/continuity.js +7 -6
  73. package/src/diary/auto.js +190 -0
  74. package/src/diary/compose.js +440 -0
  75. package/src/diary/dir.js +59 -0
  76. package/src/diary/export.js +97 -0
  77. package/src/diary/index.js +26 -0
  78. package/src/diary/library.js +189 -0
  79. package/src/diary/qq-inbox.js +142 -0
  80. package/src/diary/rollover.js +61 -0
  81. package/src/diary/session-provider.js +122 -0
  82. package/src/document.js +9 -3
  83. package/src/dream/clustering.js +6 -1
  84. package/src/dream/decisions.js +56 -5
  85. package/src/dream/sleep.js +616 -15
  86. package/src/dream.js +45 -13
  87. package/src/dsh-go/host.js +162 -0
  88. package/src/dsh-go/panel.js +2091 -0
  89. package/src/dsh-go/text.js +342 -0
  90. package/src/dsh-go/views/common.js +68 -0
  91. package/src/dsh-go/views/entity.js +48 -0
  92. package/src/dsh-go/views/memory.js +198 -0
  93. package/src/dsh-go/views/root.js +30 -0
  94. package/src/dsh-go/views/settings.js +398 -0
  95. package/src/dsh-go/views/status.js +51 -0
  96. package/src/embedding.js +42 -8
  97. package/src/entities/extractor.js +36 -7
  98. package/src/hot-memory.js +8 -9
  99. package/src/index.js +131 -5
  100. package/src/inject.js +192 -19
  101. package/src/kb/index.js +309 -0
  102. package/src/kb/routes.js +220 -0
  103. package/src/kb/tools.js +148 -0
  104. package/src/lang.js +115 -4
  105. package/src/peak-hours.js +57 -2
  106. package/src/profile-facts.js +356 -0
  107. package/src/semantic.js +347 -60
  108. package/src/service.js +302 -70
  109. package/src/settings.js +117 -14
  110. package/src/store.js +830 -55
  111. package/src/summarize.js +38 -11
  112. package/src/tools.js +173 -25
  113. package/src/user-settings.js +175 -23
  114. package/src/vector-index.js +76 -6
  115. package/src/wiki/cli-hint.js +90 -0
  116. package/src/wiki/queries.js +264 -10
  117. package/src/wiki/t1-jobs.js +232 -0
  118. package/src/wiki/t1.js +7 -1
  119. package/test/api-routes-gaps.test.js +63 -0
  120. package/test/api.test.js +377 -4
  121. package/test/attachment-text.test.js +140 -0
  122. package/test/bm25-corpus-cache.test.js +145 -0
  123. package/test/capacity-warning.test.js +262 -0
  124. package/test/client.test.js +908 -138
  125. package/test/config.test.js +79 -0
  126. package/test/conflict-freeze.test.js +45 -1
  127. package/test/continuity.test.js +8 -1
  128. package/test/diary-library.test.js +187 -0
  129. package/test/diary.test.js +576 -0
  130. package/test/document.test.js +32 -0
  131. package/test/dream-maxtokens-fallback.test.js +3 -3
  132. package/test/dream.test.js +62 -1
  133. package/test/dsh-go-host.test.js +134 -0
  134. package/test/dsh-go-panel-actions.test.js +854 -0
  135. package/test/dsh-go-panel.test.js +251 -0
  136. package/test/embedding-cache-f32.test.js +276 -0
  137. package/test/embedding-ready.test.js +33 -3
  138. package/test/entities.test.js +73 -0
  139. package/test/forget-corrections.test.js +70 -0
  140. package/test/image-memory.test.js +171 -0
  141. package/test/inject-pin-explicit.test.js +300 -0
  142. package/test/inject-pin-vector-crowd.test.js +225 -0
  143. package/test/issue-617-step1-query-and-fusion.test.js +361 -0
  144. package/test/kb-api-wiring.test.js +128 -0
  145. package/test/kb-collections.test.js +272 -0
  146. package/test/kb-routes.test.js +379 -0
  147. package/test/mint-source.test.js +26 -0
  148. package/test/peak-hours-observability.test.js +70 -0
  149. package/test/persona-api.test.js +3 -3
  150. package/test/persona-by-session.test.js +568 -0
  151. package/test/persona-settings.test.js +38 -7
  152. package/test/profile-facts.test.js +492 -0
  153. package/test/reflection-update-gate.test.js +129 -0
  154. package/test/reindex-backfill.test.js +334 -0
  155. package/test/reindex-scheduler.test.js +369 -0
  156. package/test/scope-candidates.test.js +249 -0
  157. package/test/session-gate.test.js +1 -1
  158. package/test/sleep-action-set.test.js +14 -0
  159. package/test/sleep-heat.test.js +49 -0
  160. package/test/sleep.test.js +598 -0
  161. package/test/standalone-api.test.js +24 -0
  162. package/test/store.test.js +20 -0
  163. package/test/summarize.test.js +26 -0
  164. package/test/test-count-sync.test.js +14 -3
  165. package/test/tool-state-ignore.test.js +65 -0
  166. package/test/tools-dto-schema-matrix.test.js +23 -2
  167. package/test/tools.test.js +144 -7
  168. package/test/user-settings-segments.test.js +528 -0
  169. package/test/vector-index.test.js +120 -6
  170. package/test/wiki-api.test.js +476 -1
  171. package/test/wiki-cli-hint.test.js +118 -0
  172. package/test/wiki-queries.test.js +91 -1
  173. package/test/wiki-t1-jobs.test.js +125 -0
  174. package/test/wiki-tools.test.js +131 -76
package/lib/settings.js CHANGED
@@ -34,7 +34,9 @@ function parseList(raw) {
34
34
  // persona 对象的字段白名单:写入时只挑这些键,未知字段丢弃(设计稿①:字段从简,
35
35
  // beta 收到真实反馈再加;provenance 是 v1.5 协作式人格的预留位,v1 不写管道)。
36
36
  // 读侧损坏即回空对象——同一容错口径照 parseList(坏值不抛错、面板显示为空可重写)。
37
- const PERSONA_FIELDS = ["name", "personality", "replyStyle", "enabled", "provenance"];
37
+ // scenario / mesExample(#606):对标 Character Card V2 的同名字段,是「人格卡能装
38
+ // 下完整角色」所需的最小补充。两者都可选、都不写时渲染零输出(老用户逐字节不变)。
39
+ const PERSONA_FIELDS = ["name", "personality", "replyStyle", "scenario", "mesExample", "enabled", "provenance"];
38
40
 
39
41
  function parsePersona(raw) {
40
42
  try {
@@ -54,14 +56,20 @@ function parsePersona(raw) {
54
56
  // 兼容层:v1 存的是单对象(有 PERSONA_FIELDS 键、无 default/byAgent 键),读侧
55
57
  // 判形状——单对象原样落 default 位,旧数据零迁移。byAgent 的键是 agent 身份
56
58
  // id(注入时按会话身份寻址,取不到落 default)。default 卡不可删(兜底位)。
59
+ //
60
+ // 绑会话(v1.3,#606):加第三维 bySession: { <sessionId>: <卡> }。为什么需要它——
61
+ // byAgent 的键来自 session.header.agentPreset,而 DS Bot 这类宿主把它写成常量
62
+ // (所有 bot 会话都是 "standard",实测 43/50),按 bot 名字建卡永远不命中。会话 id
63
+ // 才是那个「一个 bot 一张卡」真正能分开的键。bySession 同样是纯加法:没有这个键
64
+ // 的老数据读成 {},注入文本逐字节不变。
57
65
  function parsePersonaStore(raw) {
58
66
  try {
59
67
  const value = JSON.parse(raw);
60
68
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
61
- return { default: {}, byAgent: {} };
69
+ return { default: {}, byAgent: {}, bySession: {} };
62
70
  }
63
- // 新形:含 default 或 byAgent 键 → 按多卡解析
64
- if ("default" in value || "byAgent" in value) {
71
+ // 新形:含 default / byAgent / bySession 任一键 → 按多卡解析
72
+ if ("default" in value || "byAgent" in value || "bySession" in value) {
65
73
  const byAgent = {};
66
74
  if (value.byAgent && typeof value.byAgent === "object" && !Array.isArray(value.byAgent)) {
67
75
  for (const [agentId, card] of Object.entries(value.byAgent)) {
@@ -69,19 +77,27 @@ function parsePersonaStore(raw) {
69
77
  if (Object.keys(parsed).length) byAgent[String(agentId)] = parsed;
70
78
  }
71
79
  }
72
- return { default: parsePersona(JSON.stringify(value.default ?? {})), byAgent };
80
+ const bySession = {};
81
+ if (value.bySession && typeof value.bySession === "object" && !Array.isArray(value.bySession)) {
82
+ for (const [sessionId, card] of Object.entries(value.bySession)) {
83
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
84
+ if (Object.keys(parsed).length) bySession[String(sessionId)] = parsed;
85
+ }
86
+ }
87
+ return { default: parsePersona(JSON.stringify(value.default ?? {})), byAgent, bySession };
73
88
  }
74
89
  // 旧形:v1 单对象 → 整体作 default(字段白名单在 parsePersona 内裁)
75
- return { default: parsePersona(raw), byAgent: {} };
90
+ return { default: parsePersona(raw), byAgent: {}, bySession: {} };
76
91
  } catch {
77
- return { default: {}, byAgent: {} };
92
+ return { default: {}, byAgent: {}, bySession: {} };
78
93
  }
79
94
  }
80
95
 
81
96
  function serializePersonaStore(store) {
82
97
  const out = {
83
98
  default: parsePersona(JSON.stringify(store?.default ?? {})),
84
- byAgent: {}
99
+ byAgent: {},
100
+ bySession: {}
85
101
  };
86
102
  if (store?.byAgent && typeof store.byAgent === "object") {
87
103
  for (const [agentId, card] of Object.entries(store.byAgent)) {
@@ -89,6 +105,12 @@ function serializePersonaStore(store) {
89
105
  if (Object.keys(parsed).length) out.byAgent[String(agentId)] = parsed;
90
106
  }
91
107
  }
108
+ if (store?.bySession && typeof store.bySession === "object") {
109
+ for (const [sessionId, card] of Object.entries(store.bySession)) {
110
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
111
+ if (Object.keys(parsed).length) out.bySession[String(sessionId)] = parsed;
112
+ }
113
+ }
92
114
  return JSON.stringify(out);
93
115
  }
94
116
 
@@ -137,15 +159,27 @@ const FEATURE_FLAG_BOOLEANS = [
137
159
  // Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
138
160
  // doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
139
161
  "documentMemoryEnabled",
162
+ // 日记本:跨天惰性判定 + 启动补写,正文落 <memoryDir>/diaries/<项目键>/。
163
+ // 默认关,进 LIGHT_MODE_OFF(要调模型,与低资源档互斥)。与 documentMemoryEnabled
164
+ // 成对但不相依:关掉文档记忆只影响检索,正文照样写。
165
+ "diaryEnabled",
140
166
  "codingRetrospect",
141
167
  "autoDream",
142
168
  // Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes,
143
169
  // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。
144
170
  "autoDreamFailureBackoff",
171
+ // Issue #557:dream 审计行输入快照瘦身(默认关)。开启后 auto 轮只落骨架
172
+ // (不含 content),organize 轮的重放载荷不受影响;面板可启停。
173
+ "dreamInputSummaryEnabled",
145
174
  "sleepModeEnabled",
146
175
  // Issue #529:sleep 连续失败退避(默认关)。基数是 sleepMinIntervalHours,
147
176
  // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 max(基数, 24h);面板可启停。
148
177
  "sleepFailureBackoff",
178
+ // Issue #619:情感归因相位(默认关)。把「某人某场合的情绪」写成 occasion-bound
179
+ // 的 affective_context 关系,只有跨够 sleepAffectiveTraitMinSessions 个独立会话
180
+ // 才允许升成稳定特质。要调模型且依赖实体抽取,所以必须显式打开、可随时关掉;
181
+ // 它是 sleepModeEnabled 的子相:父开关关着时整个 sleep 不跑。
182
+ "sleepAffectiveEnabled",
149
183
  "heatEnabled",
150
184
  // issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
151
185
  "injectHeatEnabled",
@@ -159,6 +193,11 @@ const FEATURE_FLAG_BOOLEANS = [
159
193
  "reflectionFailureTracking",
160
194
  "bm25SearchEnabled",
161
195
  "conflictFreezeEnabled",
196
+ // Issue #603 第 4 层:自动裁决档(默认关)。相似度极高的同 scope 重复对直接
197
+ // 保留新条、归档旧条,不进人工队列;模糊地带照旧留给人。会归档记忆,所以必须
198
+ // 显式打开,面板可启停。阈值见 FEATURE_FLAG_NUMBER_RANGES 的
199
+ // conflictAutoResolveMinSimilarity。
200
+ "conflictAutoResolveEnabled",
162
201
  "trustEpistemicWeighting",
163
202
  // Plan #2: attach per-source {keyword, vector, bm25, final} signals to each
164
203
  // search result for transparency/debugging. Default off, purely decorative.
@@ -247,10 +286,22 @@ const FEATURE_FLAG_BOOLEANS = [
247
286
  // Issue #497:画像提取草稿(POST /profile/extract)。默认关——每次调用一次 LLM 且
248
287
  // 读上百行记忆;产物只回草稿,绝不写 user_settings。进 LIGHT_MODE_OFF(预设关)。
249
288
  "profileExtractEnabled",
289
+ // 「Agent 眼中的你」自动事实(2026-11-06):dream 后逐卡维护 + 手动轮 + 注入段,
290
+ // 事实住 profile_facts 独立三表,绝不写 user_settings(#497 口径不变)。默认关,
291
+ // 进 LIGHT_MODE_OFF(自动维护轮与低资源档互斥)。顶层扁平键,与 config.js schema
292
+ // 成对出现(计数锁 test/api.test.js 钉住)。
293
+ "profileFactsEnabled",
250
294
  // #539:逐会话无痕默认关闭;子代理开关受总开关约束。手动 session id 名单留在配置,
251
295
  // 不进入只支持标量的 feature_flags 白名单。
252
296
  "sessionGateEnabled",
253
- "sessionGateSubagents"
297
+ "sessionGateSubagents",
298
+ // 2026-11-08 kb-qoder parity(契约 §3.1):chorography 面板闭环的两个授权闸 +
299
+ // kb 子系统总闸。三条都与 config.js schema 成对出现,计数锁 test/api.test.js 钉住。
300
+ // 不进 LIGHT_MODE_OFF —— 它们是「谁按下了按钮」而不是「每轮付多少」,理由写在
301
+ // config.js 那三个键的注释里。kbEnabled 本车道只登记、不接线(B 线做子系统)。
302
+ "chorographyAutoRefresh",
303
+ "chorographyT1FromPanel",
304
+ "kbEnabled"
254
305
  ];
255
306
  // 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
256
307
  const FEATURE_FLAG_INT_RANGES = {
@@ -275,6 +326,9 @@ const FEATURE_FLAG_INT_RANGES = {
275
326
  summarizePeakMaxDeferMinutes: [0, 1440],
276
327
  // Issue #239 第 4 项镜像到巩固:同一口径(分钟,0 = 不设上限)。
277
328
  dreamPeakMaxDeferMinutes: [0, 1440],
329
+ // v0.13 日记定时写:每隔 N 分钟唤醒一次跨天判定(0 = 关,只在 turn/end 与
330
+ // 启动时判)。上限一天——判定单位是天,更长的间隔没有意义。
331
+ diaryWriteEveryMinutes: [0, 1440],
278
332
  // Issue #125:hybrid 候选量上限(0 = 复用 dreamMaxSnapshotSize)。
279
333
  dreamCandidateMax: [0, 5000],
280
334
  // #135 成本审计派生:单轮 merge 决策上限(merge 是唯一回吐正文的动作,默认 40
@@ -290,6 +344,11 @@ const FEATURE_FLAG_INT_RANGES = {
290
344
  pinnedInjectBudget: [0, 5],
291
345
  // Issue #257:sleep 冲突/模式阶段的 LLM 输出预算(原硬编码 2048,实测不足)。
292
346
  sleepMaxTokens: [256, 131072],
347
+ // Issue #619:情感归因的两个闸——升特质所需的最小独立会话数(下界 2:N=1 就是
348
+ // 单次场合,正是必须禁止升特质的那个情形),以及单轮可写的 affective_context
349
+ // 关系条数上限(0 = 关闭关系通道)。闭区间与 config.js 的 z.natural() 对齐。
350
+ sleepAffectiveTraitMinSessions: [2, 10],
351
+ sleepAffectiveMaxRelationsPerRun: [0, 50],
293
352
  // Issue #529:sleep 冲突阶段的输入侧预算(此前完全没有——全部候选对一次进一个
294
353
  // prompt,实测 163,054 token 撞服务端 163,840 上限)。两个键都是 0 = 不设上限,
295
354
  // 与 config.js 的 z.natural().min(0).max() 成对。
@@ -304,7 +363,12 @@ const FEATURE_FLAG_INT_RANGES = {
304
363
  graphInjectBudget: [0, 5],
305
364
  // Issue #399:蒸馏原文记录的保留天数(与 config.js 的 z.natural().min(1).max(3650)
306
365
  // 对齐;同 llmAudit.retentionDays,是 int 而非开关)。
307
- "distillRecords.retentionDays": [1, 3650]
366
+ "distillRecords.retentionDays": [1, 3650],
367
+ // #275 容量预警阈值(嵌套点号键,必须同时进 api.js 的 NESTED_FLAG_PATHS)。
368
+ // 0 = 显式关掉那一条检查(阈值判级的启用条件写在 service.getCapacityHealth)。
369
+ // 上限与 config.js 的 z.natural().min(0).max() 成对。
370
+ "capacityWarning.dbSizeWarnMb": [0, 1048576],
371
+ "capacityWarning.archivedRowsWarn": [0, 10000000]
308
372
  };
309
373
  // 浮点开关的闭区间(与 config.js 的 z.number().min().max() 对齐)。与整数开关
310
374
  // 分开:面板的整数控件要求 Number.isInteger,而余弦相似度阈值必须允许小数。
@@ -314,7 +378,14 @@ const FEATURE_FLAG_NUMBER_RANGES = {
314
378
  // Issue #125:hybrid 判"高相似"的阈值。
315
379
  dreamCandidateMinSim: [0.5, 0.99],
316
380
  // Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。
317
- graphWeightDelta: [0, 1]
381
+ graphWeightDelta: [0, 1],
382
+ // Issue #603 第 4 层:自动裁决的相似度门槛。下界 0.85 与冲突检测阈值同源——
383
+ // 低于它就不是「重复」而是「相似」,不该自动归档任何一条。
384
+ conflictAutoResolveMinSimilarity: [0.85, 1],
385
+ // #275 容量预警的归档占比阈值(0 = 关):健康形态本身就落在 0.8~0.9,默认
386
+ // 0.95 起才算「归档区吞掉活跃区」——0.9 会对着健康库天天报警(依据见 config.js
387
+ // capacityWarning 的注释)。浮点键,同 conflictAutoResolveMinSimilarity 的缘由。
388
+ "capacityWarning.archivedRatioWarn": [0, 1]
318
389
  };
319
390
  // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
320
391
  // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
@@ -529,10 +600,10 @@ export function createSettings(db) {
529
600
  return parsePersonaStore(getSetting("persona") ?? "{}");
530
601
  },
531
602
  setPersona(input) {
532
- // 双形兼容:单对象形 = 只更新 default、byAgent 保留(旧面板/旧调用方合并语义);
533
- // 多卡形 = 全量替换。坏体(非对象)在此不落库不抛错,保值由调用方守卫。
603
+ // 双形兼容:单对象形 = 只更新 default、byAgent/bySession 保留(旧面板/旧调用方
604
+ // 合并语义);多卡形 = 全量替换。坏体(非对象)在此不落库不抛错,保值由调用方守卫。
534
605
  if (typeof input !== "object" || input === null || Array.isArray(input)) return;
535
- if ("default" in input || "byAgent" in input) {
606
+ if ("default" in input || "byAgent" in input || "bySession" in input) {
536
607
  setSetting("persona", serializePersonaStore(input));
537
608
  return;
538
609
  }
@@ -568,6 +639,38 @@ export function createSettings(db) {
568
639
  return true;
569
640
  },
570
641
 
642
+ /**
643
+ * 绑会话卡(#606):按 sessionId 存/删/列。形状与 setPersonaFor 一族逐一对应,
644
+ * 只是换了一个维度——为什么需要第二维见 parsePersonaStore 的注释。
645
+ * sessionId 为空或 "default" 拒绝(与 byAgent 同一条纪律:兜底位不可被顶替)。
646
+ */
647
+ setPersonaForSession(sessionId, card) {
648
+ const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
649
+ if (!id || id === "default") return false;
650
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
651
+ const clean = parsePersona(JSON.stringify(
652
+ typeof card === "object" && card !== null && !Array.isArray(card) ? card : {}
653
+ ));
654
+ if (Object.keys(clean).length) store.bySession[id] = clean;
655
+ else delete store.bySession[id];
656
+ setSetting("persona", serializePersonaStore(store));
657
+ return true;
658
+ },
659
+ /** bySession 键列表(面板「按会话建的卡」用,default 不在其中)。 */
660
+ listPersonaSessions() {
661
+ return Object.keys(parsePersonaStore(getSetting("persona") ?? "{}").bySession);
662
+ },
663
+ /** 删一张 bySession 卡;不存在返回 false。 */
664
+ removePersonaForSession(sessionId) {
665
+ const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
666
+ if (!id || id === "default") return false;
667
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
668
+ if (!(id in store.bySession)) return false;
669
+ delete store.bySession[id];
670
+ setSetting("persona", serializePersonaStore(store));
671
+ return true;
672
+ },
673
+
571
674
  /** All custom commands, sorted by name. */
572
675
  listCommands() {
573
676
  const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();