memory-eternal 0.9.14 → 0.9.16

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/index.js CHANGED
@@ -179,7 +179,7 @@ function schemaDefaults(schema) {
179
179
  return {}
180
180
  }
181
181
 
182
- function bindSettings(ctx, schema, config) {
182
+ export function bindSettings(ctx, schema, config) {
183
183
  const service = typeof ctx.get === 'function' ? ctx.get('settings') : ctx.settings
184
184
  if (service && typeof service.register === 'function') {
185
185
  return service.register('memory-eternal', schema, { base: config ?? {} })
@@ -194,16 +194,57 @@ function bindSettings(ctx, schema, config) {
194
194
  try { return service.configure({ auto: false }, ctx.fiber) } catch { return () => {} }
195
195
  }, 'memory-eternal: settings presentation')
196
196
  }
197
+ // 宿主「volatile 回流」可能滞后甚至不回流(issue #12:改了保存不上)。两步兜底:
198
+ // ① 写成功后把 patch 叠加在本地视图上 → 面板重新加载立刻是新值;
199
+ // ② 等宿主快照追上(值相等)后自动摘除,避免长期掩盖宿主的真实状态。
200
+ const overlay = {}
201
+ const snapshotWithoutOverlay = () => (config === undefined || config === null ? fallback : plainConfig(config))
202
+ const pruneOverlay = (snapshot) => {
203
+ for (const key of Object.keys(overlay)) {
204
+ if (snapshot && snapshot[key] !== undefined && JSON.stringify(snapshot[key]) === JSON.stringify(overlay[key])) delete overlay[key]
205
+ }
206
+ }
207
+ const readMerged = () => {
208
+ const base = snapshotWithoutOverlay()
209
+ pruneOverlay(base)
210
+ return Object.keys(overlay).length ? { ...base, ...overlay } : base
211
+ }
197
212
  return {
198
- get: read,
213
+ get: readMerged,
199
214
  watch(listener) {
200
- const handler = () => { try { listener(read()) } catch { /* 监听器异常不影响宿主 */ } }
215
+ const handler = () => { try { listener(readMerged()) } catch { /* 监听器异常不影响宿主 */ } }
201
216
  ctx.on('loader/volatile-update', handler)
202
217
  return () => { try { ctx.off('loader/volatile-update', handler) } catch { /* 已卸载 */ } }
203
218
  },
219
+ /**
220
+ * 写配置。
221
+ * 宿主 write() 用「describe 返回的 revision 必须完全一致」做乐观并发(dsh-settings
222
+ * SettingsConflictError),而面板持有的 revision 可能已经过期(上一次写入、宿主重建配置等)。
223
+ * 这里遇到冲突就取一次最新 revision 重试一次,而不是把 409 直接抛给用户。
224
+ * @returns {Promise<{revision:number, retried:boolean}>}
225
+ */
204
226
  async update(patch, expectedRevision) {
205
227
  if (!service || typeof service.update !== 'function') throw new Error('当前 DSH 版本不支持写配置')
206
- await service.update(entryId, patch, expectedRevision)
228
+ const call = (rev) => service.update(entryId, patch, rev)
229
+ let retried = false
230
+ let used = expectedRevision
231
+ try {
232
+ await call(expectedRevision)
233
+ } catch (error) {
234
+ const msg = String((error && error.message) || error)
235
+ if (!/revision|changed since|conflict/i.test(msg)) throw error
236
+ let fresh
237
+ try {
238
+ const list = typeof service.describe === 'function' ? service.describe() : []
239
+ fresh = (list.find((d) => d && d.ns === entryId) || {}).revision
240
+ } catch { fresh = undefined }
241
+ if (fresh === undefined) throw error
242
+ used = fresh
243
+ retried = true
244
+ await call(fresh)
245
+ }
246
+ Object.assign(overlay, patch)
247
+ return { revision: used, retried }
207
248
  },
208
249
  }
209
250
  }
@@ -766,6 +807,13 @@ export function apply(ctx, config) {
766
807
  // 多库(#10):配置页可直接编辑
767
808
  vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [],
768
809
  activeVault: cfg.activeVault || '',
810
+ // 配置覆盖补齐(用户要求「配置按钮全覆盖」):这些以前只能手改配置文件
811
+ enabled: cfg.enabled !== false,
812
+ vaultDir: cfg.vaultDir || '',
813
+ captureProvider: cfg.captureProvider || '',
814
+ captureModel: cfg.captureModel || '',
815
+ recallEmbedding: cfg.recallEmbedding || '',
816
+ sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
769
817
  }
770
818
  const descriptor = (ctx.get('settings') ?? {}).describe?.({ redactSecrets: true }) ?? []
771
819
  const me = descriptor.find((d) => d.ns === 'memory-eternal')
@@ -799,10 +847,21 @@ export function apply(ctx, config) {
799
847
  if (Object.keys(clean).length === 0) return json(res, 400, { ok: false, error: '无可写入字段' })
800
848
  if (typeof settings.update === 'function') {
801
849
  try {
802
- await settings.update(clean, expectedRevision)
850
+ const writeResult = await settings.update(clean, expectedRevision)
803
851
  // 把完整配置写入共享文件,让独立 web / MCP hook 与 DSH 设置同步(不同步修复)
804
852
  syncConfigFile()
805
- return json(res, 200, { ok: true, applied: Object.keys(clean), note: '已保存。autoWebMode/watchdogAutoSpawn 等需重启 DSH 生效' })
853
+ // 回读校验:宿主 volatile 回流可能滞后(issue #12),此时明确告知而不是假装成功
854
+ const now = settings.get() ?? {}
855
+ const pending = Object.keys(clean).filter((k) => JSON.stringify(now[k]) !== JSON.stringify(clean[k]))
856
+ return json(res, 200, {
857
+ ok: true,
858
+ applied: Object.keys(clean),
859
+ pending,
860
+ retried: !!(writeResult && writeResult.retried),
861
+ note: pending.length
862
+ ? '已写入;宿主尚未回流这些值(面板已本地生效,重启 DSH 后以配置文件为准)'
863
+ : '已保存。autoWebMode/watchdogAutoSpawn 等需重启 DSH 生效',
864
+ })
806
865
  } catch (e) {
807
866
  if (e && e.code === 'SETTINGS_CONFLICT') return json(res, 409, { ok: false, error: '配置已被外部修改,请刷新后重试(revision conflict)' })
808
867
  return json(res, 500, { ok: false, error: String(e?.message || e) })
package/lib/api.js CHANGED
@@ -376,6 +376,9 @@ export function createApi(deps = {}) {
376
376
  autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0', autoWebMode: cfg.autoWebMode || 'init', webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999, webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000, webMaxRestart: Number(cfg.webMaxRestart) || 10, watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1', autoMcpSetup: cfg.autoMcpSetup ?? false,
377
377
  auditMode: cfg.auditMode ?? 'all', auditExemptAgents: cfg.auditExemptAgents ?? [], auditExemptKinds: cfg.auditExemptKinds ?? [], recycleRetentionDays: cfg.recycleRetentionDays ?? 30,
378
378
  vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [], activeVault: cfg.activeVault || '',
379
+ enabled: cfg.enabled !== false, vaultDir: cfg.vaultDir || '', captureProvider: cfg.captureProvider || '',
380
+ captureModel: cfg.captureModel || '', recallEmbedding: cfg.recallEmbedding || '', sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
381
+ webPort: cfg.webPort ?? 7999, webCheckIntervalMs: cfg.webCheckIntervalMs ?? 5000, webMaxRestart: cfg.webMaxRestart ?? 10,
379
382
  }
380
383
  json(res, 200, { ok: true, config: safe, revision: 0, writable: false, readonly: true, version: env.MEMORY_ETERNAL_VERSION || '' })
381
384
  return