sage-mem 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -430,14 +430,16 @@ export class MemoryGateway extends TypertRemoteService {
430
430
  const norm = (list) => JSON.stringify([...list].map((s) => String(s).toLowerCase()).sort())
431
431
  const restartRequired =
432
432
  JSON.stringify(limits) !== JSON.stringify(pending.limits) || norm(activeExtra) !== norm(reservedExtra)
433
+ // 2.7.0:entry 是 canonical;sidebarEntry 只为 2.6.0 的客户端保留(派生值,不落盘)。
434
+ const ui = pendingUi(fileRead.data)
433
435
  return {
434
436
  limits,
435
437
  pendingLimits: pending.limits,
436
438
  restartRequired,
437
439
  // Q04 的诊断出口:面板据此说明「你保存的值没生效,原因是 X」;宿主侧先如实给出,别让问题只能靠猜。
438
440
  limitDiagnostics,
439
- // 1.8.0:客户端行为(侧栏入口开关)。**不参与** restartRequired —— 改完立刻生效。
440
- ui: pendingUi(fileRead.data),
441
+ // 1.8.0 / 2.7.0:客户端行为(界面入口三选一)。**不参与** restartRequired —— 改完立刻生效。
442
+ ui: { ...ui, sidebarEntry: ui.entry !== 'settings' },
441
443
  // 生效的保护名单(含两个内置名)+ 用户声明的那部分原文(面板编辑的是后者)
442
444
  reserved: [...new Set([INDEX_FILE_KEY, SESSION_LOG_KEY, ...reservedExtra.map((s) => s.toLowerCase())])],
443
445
  reservedExtra,
@@ -498,7 +500,8 @@ export class MemoryGateway extends TypertRemoteService {
498
500
  }
499
501
  const current = currentRead.data
500
502
  const mergedLimits = { ...(current?.limits ?? {}), ...nextLimits }
501
- // ui 只覆盖这次给出的键;没给就沿用文件里的有效值(坏值 / 缺失一律按默认「开」)
503
+ // ui 只覆盖这次给出的键;没给就沿用文件里的有效值(坏值 / 缺失一律按默认「两个都显示」)。
504
+ // 2.7.0:落盘只写 canonical 的 entry,旧 sidebarEntry 由 pendingUi 迁移进来、不再写出。
502
505
  const mergedUi = { ...pendingUi(current), ...(uiPatch ?? {}) }
503
506
  await writeFileAtomic(SETTINGS_FILE, JSON.stringify({ limits: mergedLimits, ui: mergedUi, updatedAt: Date.now() }, null, 2))
504
507
  }
package/lib/settings.js CHANGED
@@ -21,22 +21,34 @@ export const RESERVED_FILE = join(STATE_ROOT, 'reserved.json')
21
21
  export const clampInt = (v, spec) => (Number.isFinite(v) ? Math.max(spec.lo, Math.min(spec.hi, Math.round(v))) : spec.dflt)
22
22
 
23
23
  /**
24
- * 面板 UI 设置(1.8.0)。
24
+ * 面板 UI 设置(1.8.0;2.7.0 改成三选一)。
25
25
  *
26
26
  * ⚠️ 与 `limits` **不是一类**:上限在模块加载时就固化进常量,改完必须重启;
27
- * `ui.sidebarEntry` 是**客户端行为** —— 界面读到值就决定注不注册侧栏槽位,
27
+ * `ui.entry` 是**客户端行为** —— 界面读到值就决定注不注册侧栏 / 设置页槽位,
28
28
  * 改完立刻生效。所以它不进 `restartRequired` 的计算。
29
+ *
30
+ * 2.7.0:`entry` 取代 1.8.0 的布尔 `sidebarEntry`。三个值从类型上就保证
31
+ * 「至少保留一个入口」,不需要额外的互斥校验;旧字段只读不写。
29
32
  */
30
- export const DEFAULT_UI = Object.freeze({ sidebarEntry: true })
33
+ export const ENTRY_MODES = Object.freeze(['both', 'sidebar', 'settings'])
34
+ export const DEFAULT_UI = Object.freeze({ entry: 'both' })
31
35
 
32
- /** 读出 `ui` 的**有效值**:文件里没有 / 坏对象 / 不是布尔 → 一律回落默认(开)。 */
36
+ /**
37
+ * 读出 `ui` 的**有效值**:新字段优先,旧 `sidebarEntry` 按
38
+ * `true → both` / `false → settings` 迁移;缺失或坏值回落默认(两个都显示)。
39
+ */
33
40
  export function pendingUi(fileObj) {
34
41
  const saved = fileObj?.ui && typeof fileObj.ui === 'object' && !Array.isArray(fileObj.ui) ? fileObj.ui : {}
35
- return { sidebarEntry: typeof saved.sidebarEntry === 'boolean' ? saved.sidebarEntry : DEFAULT_UI.sidebarEntry }
42
+ if (ENTRY_MODES.includes(saved.entry)) return { entry: saved.entry }
43
+ if (typeof saved.sidebarEntry === 'boolean') return { entry: saved.sidebarEntry ? 'both' : 'settings' }
44
+ return { entry: DEFAULT_UI.entry }
36
45
  }
37
46
 
38
47
  /**
39
- * 校验 `setSettings({ ui })` 的那一段:**只认 `sidebarEntry` 一个键**,值必须是布尔。
48
+ * 校验 `setSettings({ ui })` 的那一段:认 `entry`(2.7.0)与旧 `sidebarEntry`(1.8.0)。
49
+ *
50
+ * 两个键同时给时要求语义一致(`sidebarEntry === (entry !== 'settings')`),否则报冲突 ——
51
+ * 旧客户端写 true 的语义是「侧栏显示、设置页也保留」,和 `entry:'sidebar'` 不是一回事。
40
52
  *
41
53
  * 返回 `{ ok:true, patch }`(patch 为 null = 调用方没给 ui,或给的是空对象);`{ ok:false, error }`。
42
54
  * 与 limits 同一套严格语义:绝不静默丢掉不认识的键。
@@ -45,10 +57,30 @@ export function parseUiPatch(ui) {
45
57
  if (ui === undefined) return { ok: true, patch: null }
46
58
  if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return { ok: false, error: 'sage-mem: ui must be an object' }
47
59
  const patch = {}
60
+ let hasLegacy = false
61
+ let legacy = null
48
62
  for (const [key, value] of Object.entries(ui)) {
49
- if (key !== 'sidebarEntry') return { ok: false, error: `sage-mem: unknown ui setting: ${key}` }
50
- if (typeof value !== 'boolean') return { ok: false, error: `sage-mem: ui.sidebarEntry must be a boolean, got ${typeof value}` }
51
- patch.sidebarEntry = value
63
+ if (key === 'entry') {
64
+ if (typeof value !== 'string' || !ENTRY_MODES.includes(value)) {
65
+ return { ok: false, error: `sage-mem: ui.entry must be one of ${ENTRY_MODES.join(' | ')}, got ${JSON.stringify(value)}` }
66
+ }
67
+ patch.entry = value
68
+ continue
69
+ }
70
+ if (key === 'sidebarEntry') {
71
+ if (typeof value !== 'boolean') return { ok: false, error: `sage-mem: ui.sidebarEntry must be a boolean, got ${typeof value}` }
72
+ hasLegacy = true
73
+ legacy = value
74
+ continue
75
+ }
76
+ return { ok: false, error: `sage-mem: unknown ui setting: ${key}` }
77
+ }
78
+ if (hasLegacy) {
79
+ const derived = legacy ? 'both' : 'settings'
80
+ if (patch.entry !== undefined && patch.entry !== derived) {
81
+ return { ok: false, error: `sage-mem: ui.entry=${patch.entry} conflicts with ui.sidebarEntry=${legacy}` }
82
+ }
83
+ patch.entry = derived
52
84
  }
53
85
  return { ok: true, patch: Object.keys(patch).length ? patch : null }
54
86
  }
@@ -239,10 +239,9 @@ const settingsLimitsSchema = z.object({
239
239
  'maxSessionBytes': z.number().readonly(),
240
240
  'staleDays': z.number().readonly(),
241
241
  }).readonly()
242
- // 1.8.0:面板 UI 设置。`sidebarEntry` 是**客户端行为**(改完立刻生效),不进 restartRequired。
243
- const settingsUiSchema = z.object({
244
- 'sidebarEntry': z.boolean().readonly(),
245
- }).readonly()
242
+ // 1.8.0 / 2.7.0:面板 UI 设置。`entry` 是**客户端行为**(改完立刻生效),不进 restartRequired。
243
+ // `sidebarEntry` 是给 2.6.0 客户端的派生值(= entry !== 'settings'),永远随 `entry` 一起返回。
244
+ const settingsUiSchema = z.object({ 'entry': z.enum(['both', 'sidebar', 'settings']).readonly(), 'sidebarEntry': z.boolean().readonly() }).readonly()
246
245
  const settingsSchema = z.object({
247
246
  'limits': settingsLimitsSchema,
248
247
  'pendingLimits': settingsLimitsSchema,
@@ -260,6 +259,7 @@ const settingsPatchSchema = z.object({
260
259
  'staleDays': z.number().optional(),
261
260
  }).readonly().optional(),
262
261
  'ui': z.object({
262
+ 'entry': z.enum(['both', 'sidebar', 'settings']).optional(),
263
263
  'sidebarEntry': z.boolean().optional(),
264
264
  }).readonly().optional(),
265
265
  'reserved': z.array(z.string()).optional(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sage-mem",
3
3
  "description": "Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.",
4
- "version": "2.5.1",
4
+ "version": "2.7.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",