dsh-memu-memory 1.1.2 → 1.1.3

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 (49) hide show
  1. package/AGENTS.md +8 -6
  2. package/README.md +21 -5
  3. package/README.zh.md +21 -5
  4. package/docs/contributing.md +5 -3
  5. package/docs/env-and-commands.md +13 -0
  6. package/docs/host-api.md +35 -3
  7. package/docs/memory-semantics.md +32 -5
  8. package/docs/troubleshooting.md +36 -7
  9. package/lib/activity-log.js +190 -0
  10. package/lib/backup.js +1103 -0
  11. package/lib/binding-diagnostics.js +630 -0
  12. package/lib/client.js +697 -49
  13. package/lib/curator.js +81 -37
  14. package/lib/cycle.js +3 -0
  15. package/lib/embedding-catalog.js +763 -0
  16. package/lib/embedding-download.js +407 -0
  17. package/lib/embedding-runtime.js +710 -0
  18. package/lib/engine.js +2 -2
  19. package/lib/index.js +36 -0
  20. package/lib/inject.js +131 -30
  21. package/lib/local-embedding-entry.js +145 -0
  22. package/lib/local-embedding-manager.js +249 -0
  23. package/lib/local-embedding-protocol.js +173 -0
  24. package/lib/local-embedding-worker.js +652 -0
  25. package/lib/local-vector-service.js +283 -0
  26. package/lib/memory.js +385 -45
  27. package/lib/memu-config.js +10 -22
  28. package/lib/memu.js +53 -19
  29. package/lib/paths.js +24 -0
  30. package/lib/private.js +139 -13
  31. package/lib/rpc.js +45 -4
  32. package/lib/state.js +109 -161
  33. package/lib/tools.js +9 -5
  34. package/lib/vector-migration.js +878 -0
  35. package/lib/vector-profile.js +941 -0
  36. package/package.json +3 -2
  37. package/scripts/acceptance-local.mjs +5 -4
  38. package/scripts/chain-matrix.mjs +527 -0
  39. package/scripts/check-all.mjs +19 -0
  40. package/scripts/check-doc-drift.mjs +353 -0
  41. package/scripts/diagnose-bindings.mjs +176 -0
  42. package/scripts/local-runtime-install.mjs +142 -0
  43. package/scripts/local-runtime-verify.mjs +56 -0
  44. package/scripts/local-vector-verify.mjs +93 -0
  45. package/scripts/recall-bench.mjs +813 -0
  46. package/scripts/repetition-ab-scenarios.mjs +93 -0
  47. package/scripts/repetition-detector-snapshot.mjs +314 -0
  48. package/scripts/repetition-eval-live.mjs +360 -0
  49. package/scripts/repetition-eval.mjs +702 -0
package/lib/curator.js CHANGED
@@ -1,15 +1,47 @@
1
1
  import {
2
- globalDbPath, loadGlobalIndex, updateGlobalIndex, newMemoryId, getSettings, getMemoryBackend,
2
+ globalDbPath, loadGlobalIndex, updateGlobalIndex, newMemoryId, getSettings, effectiveVectorRoute,
3
3
  } from './state.js'
4
4
  import { commit } from './memu.js'
5
5
  import { loadPrompt } from './prompts.js'
6
- import { retryStoreWrite, redactSecrets } from './paths.js'
6
+ import { retryStoreWrite, redactSecrets, warn } from './paths.js'
7
+ import { record } from './activity-log.js'
8
+ import { assertVectorWriteAllowed, hasActiveVectorGeneration, readVectorState, withVectorWrite } from './vector-profile.js'
7
9
 
8
10
  const MAX_SESSIONS = 10
9
11
  const MAX_TOTAL_CHARS = 90_000
10
12
  const MAX_SESSION_CHARS = 14_000
11
13
  const jobs = new Map()
12
14
 
15
+ function managedVectorIntent() {
16
+ if (effectiveVectorRoute() !== 'managed-local') return false
17
+ try {
18
+ const state = readVectorState({ tolerateMissing: true }).state
19
+ return Boolean(state && (state.active || state.requested || state.pending))
20
+ } catch {
21
+ return true
22
+ }
23
+ }
24
+
25
+ // 与 memory.js 同一条口径:受管路由还没有代际时,引擎这一步按词法处理(索引照写),
26
+ // 否则 MemU 会以 "managed local vector profile is not active" 拒绝整次提炼。
27
+ function engineRoute() {
28
+ const route = effectiveVectorRoute()
29
+ return route === 'managed-local' && !hasActiveVectorGeneration() ? 'lexical' : route
30
+ }
31
+
32
+ async function withManagedCuratorWrite(action) {
33
+ if (!managedVectorIntent()) return action()
34
+ // Same rule as the private write path: requested-but-never-migrated has no
35
+ // generation to stay consistent with, so the global index + engine still get
36
+ // the memories. Failing here would throw away a whole curation run.
37
+ if (!hasActiveVectorGeneration()) {
38
+ warn('managed vector write degraded to index+engine: no active vector generation; run the profile switch to enable vectors')
39
+ record({ kind: 'vector', level: 'warn', scope: 'global', message: '向量未激活,全局提炼已降级为索引+引擎', detail: { reason: 'no active vector generation' } })
40
+ return action()
41
+ }
42
+ return withVectorWrite(async () => { assertVectorWriteAllowed(); return action() })
43
+ }
44
+
13
45
  function textOf(blocks) {
14
46
  if (!Array.isArray(blocks)) return ''
15
47
  return blocks
@@ -259,46 +291,49 @@ export async function commitCuratorOutput(output, { job }) {
259
291
  }
260
292
 
261
293
  if (files.length === 0) {
262
- if (pending.length) await retryStoreWrite(() => updateGlobalIndex(fresh => {
294
+ if (pending.length) await withManagedCuratorWrite(() => retryStoreWrite(() => updateGlobalIndex(fresh => {
263
295
  for (const { id, entry } of pending) fresh.entries[id] = entry
264
- }))
296
+ })))
265
297
  return { files: [], created: 0, merged: 0, superseded: 0, pending: pending.length, skipped }
266
298
  }
267
299
 
268
300
  job.step = `committing ${files.length} global memories`
269
- // In agent mode the JSON index is the store. Only the explicit remote backend
270
- // needs MemU's database, so local curation stays fast and key-free.
271
- if (getMemoryBackend() === 'api') {
272
- await commit({
273
- payload: { recall_files: files, resource: [], user: {} },
274
- db: globalDbPath(),
275
- })
276
- }
277
-
278
- if (job?.abort?.signal?.aborted) throw new Error('cancelled by user')
279
- await retryStoreWrite(() => updateGlobalIndex(fresh => {
280
- for (const { id, entry } of pending) fresh.entries[id] = entry
281
- for (const { id, entry, op, targetId, previous } of updates) {
282
- if (previous && JSON.stringify(fresh.entries[targetId]) !== JSON.stringify(previous)) {
283
- const heldId = newMemoryId('gmem')
284
- fresh.entries[heldId] = { ...entry, status: 'pending_review', conflictingWith: targetId, reviewReason: 'target changed while curator was running' }
285
- continue
286
- }
287
- if (op === 'merge' && previous) {
288
- entry.content = entry.content || previous.content
289
- entry.title = entry.title || previous.title
290
- entry.evidence = [...(previous.evidence || []), ...(entry.evidence || [])].slice(-40)
291
- }
292
- fresh.entries[id] = entry
293
- if ((op === 'supersede' || op === 'merge') && targetId && fresh.entries[targetId]) {
294
- fresh.entries[targetId].status = 'superseded'
295
- fresh.entries[targetId].supersededBy = id
296
- fresh.entries[targetId].validTo = now
297
- fresh.entries[targetId].updatedAt = now
298
- }
301
+ await withManagedCuratorWrite(async () => {
302
+ // Managed agent mode still commits the exact same files to the active local
303
+ // vector database; only pending_review entries remain index-only.
304
+ const route = engineRoute()
305
+ if (route !== 'lexical') {
306
+ await commit({
307
+ payload: { recall_files: files, resource: [], user: {} },
308
+ route, db: globalDbPath(),
309
+ })
299
310
  }
300
- }))
301
- return {
311
+
312
+ if (job?.abort?.signal?.aborted) throw new Error('cancelled by user')
313
+ await retryStoreWrite(() => updateGlobalIndex(fresh => {
314
+ for (const { id, entry } of pending) fresh.entries[id] = entry
315
+ for (const { id, entry, op, targetId, previous } of updates) {
316
+ if (previous && JSON.stringify(fresh.entries[targetId]) !== JSON.stringify(previous)) {
317
+ const heldId = newMemoryId('gmem')
318
+ fresh.entries[heldId] = { ...entry, status: 'pending_review', conflictingWith: targetId, reviewReason: 'target changed while curator was running' }
319
+ continue
320
+ }
321
+ if (op === 'merge' && previous) {
322
+ entry.content = entry.content || previous.content
323
+ entry.title = entry.title || previous.title
324
+ entry.evidence = [...(previous.evidence || []), ...(entry.evidence || [])].slice(-40)
325
+ }
326
+ fresh.entries[id] = entry
327
+ if ((op === 'supersede' || op === 'merge') && targetId && fresh.entries[targetId]) {
328
+ fresh.entries[targetId].status = 'superseded'
329
+ fresh.entries[targetId].supersededBy = id
330
+ fresh.entries[targetId].validTo = now
331
+ fresh.entries[targetId].updatedAt = now
332
+ }
333
+ }
334
+ }))
335
+ })
336
+ const summary = {
302
337
  files,
303
338
  created: updates.filter((u) => u.op === 'create').length,
304
339
  merged: updates.filter((u) => u.op === 'merge').length,
@@ -306,8 +341,17 @@ export async function commitCuratorOutput(output, { job }) {
306
341
  pending: pending.length,
307
342
  skipped,
308
343
  }
344
+ record({
345
+ kind: 'curate',
346
+ scope: 'global',
347
+ ok: true,
348
+ message: `全局提炼:新增 ${summary.created} / 合并 ${summary.merged} / 待复核 ${summary.pending}`
349
+ + (summary.skipped.length ? ` / 跳过 ${summary.skipped.length}` : ''),
350
+ detail: { ...summary, skipped: summary.skipped.slice(0, 20) },
351
+ refs: files.map((file) => file.name).slice(0, 20),
352
+ })
353
+ return summary
309
354
  }
310
-
311
355
  export function startGlobalCuration(ctx, { sessionIds, parentSessionId, model } = {}) {
312
356
  const ids = [...new Set((Array.isArray(sessionIds) ? sessionIds : []).filter((s) => typeof s === 'string' && s.trim()))]
313
357
  const jobId = `curate-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
package/lib/cycle.js CHANGED
@@ -7,6 +7,7 @@ import { expireShortTerm } from './memory.js'
7
7
  import { requestPrivateConsolidation } from './private.js'
8
8
  import { startGlobalCuration } from './curator.js'
9
9
  import { runVisibilityMigrationPass } from './migrate.js'
10
+ import { record } from './activity-log.js'
10
11
 
11
12
  const SWEEP_MS = 5 * 60 * 1000
12
13
  // Long enough that the host has restored its session list and the agent objects
@@ -152,9 +153,11 @@ export async function runCurationPass(ctx, { now = Date.now(), log = () => {} }
152
153
  try {
153
154
  const job = await startGlobalCuration(ctx, { sessionIds: due, parentSessionId: parent, model: settings.curatorModel })
154
155
  setCycleState({ lastCurateAt: new Date(now).toISOString(), lastCurateJob: job?.id || null })
156
+ record({ kind: 'curate', scope: 'global', ok: true, message: `自动提炼启动:${due.length} 个会话`, detail: { sessions: due.length, jobId: job?.id || null, parent } })
155
157
  return { ran: due.length, job }
156
158
  } catch (error) {
157
159
  log(`auto curation failed: ${error?.message || error}`)
160
+ record({ kind: 'curate', scope: 'global', ok: false, level: 'error', message: '自动提炼失败:' + String(error?.message || error), detail: { sessions: due.length } })
158
161
  return { ran: 0, error: String(error?.message || error) }
159
162
  }
160
163
  }