@a9i5k4/dsh-auto-memory 2.1.7 → 2.1.9

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/client.js CHANGED
@@ -1223,6 +1223,17 @@ window.__ModuleLoader__.load({
1223
1223
  var pollPair = useState(null)
1224
1224
  var poll = pollPair[0]
1225
1225
  var setPoll = pollPair[1]
1226
+ var gpuPair = useState(function () { try { return localStorage.getItem('dsh-auto-memory.pyGpu') === '1' } catch (eG) { return false } })
1227
+ var gpuPref = gpuPair[0]
1228
+ var setGpuPref = function (v) {
1229
+ gpuPair[1](v)
1230
+ try { localStorage.setItem('dsh-auto-memory.pyGpu', v ? '1' : '0') } catch (eG) {}
1231
+ }
1232
+ var actGpu = function () {
1233
+ if (busy) return
1234
+ setBusy(true)
1235
+ apiPost(API.pyDeps, { gpu: gpuPref }).then(function (d) { setData(d); setBusy(false); setPoll(true); var ph = d && d.phase; if (ph === 'ready' || ph === 'error' || ph === 'idle') setPoll(false) }).catch(function () { setBusy(false); setPoll(false) })
1236
+ }
1226
1237
  var refresh = function () { apiGet(API.pyStatus).then(function (d) { if (d) setData(d) }).catch(function () {}) }
1227
1238
  useEffect(function () {
1228
1239
  var alive = true
@@ -1267,7 +1278,11 @@ window.__ModuleLoader__.load({
1267
1278
  stepRow(t('pyWizStep2'), data.venvOk ? true : 'run',
1268
1279
  (!data.venvOk && (data.pythons || []).some(function (p2) { return p2.status === 'ok' && !p2.isVenv })) ? h('button', Object.assign({}, btnCls, { onClick: function () { act(API.pyVenv, true) } }), t('pyWizCreate')) : null),
1269
1280
  stepRow(t('pyWizStep3'), data.depsOk ? true : 'run',
1270
- (data.venvOk && !data.depsOk) ? h('button', Object.assign({}, btnCls, { onClick: function () { act(API.pyDeps, true) } }), t('pyWizInstall')) : null),
1281
+ (data.venvOk && !data.depsOk) ? h('span', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
1282
+ h('label', { style: { display: 'flex', gap: '4px', alignItems: 'center', fontSize: 'calc(10.5px * var(--dam-scale))' } },
1283
+ h('input', { type: 'checkbox', checked: gpuPref, onChange: function (e) { setGpuPref(e.target.checked) } }),
1284
+ locale === 'zh' ? 'GPU 推理(NVIDIA,需已装 CUDA;否则自动回退 CPU)' : 'GPU inference (NVIDIA + CUDA; auto-falls back to CPU)'),
1285
+ h('button', Object.assign({}, btnCls, { onClick: function () { actGpu() } }), t('pyWizInstall'))) : null),
1271
1286
  stepRow(t('pyWizStep4'), data.modelReady ? true : 'run',
1272
1287
  (!data.modelReady && !dlActive) ? h('button', Object.assign({}, btnCls, { onClick: function () { act(API.pyModel, true) } }), t('pyWizDownload')) : null,
1273
1288
  dlActive ? h('button', { 'data-dam-btn': '', onClick: function () { apiPost(API.pyCancel, {}).then(refresh).catch(function () {}) } }, t('pyWizCancel')) : null),
@@ -3047,6 +3062,7 @@ window.__ModuleLoader__.load({
3047
3062
  : 'The built-in JS semantic model (~135MB, one-time) can be downloaded in the background; the semantic tier enables itself once done:'))
3048
3063
  sdActions.push(h('button', { 'data-dam-btn': '', 'data-primary': 'true', style: close, onClick: function () {
3049
3064
  apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
3065
+ try { localStorage.setItem('dsh-auto-memory.semDetectSnoozeUntil', String(Date.now() + 24 * 3600 * 1000)) } catch (eSn2) {}
3050
3066
  closeDialog()
3051
3067
  } }, sdZh ? '立即下载' : 'Download now'))
3052
3068
  }
@@ -3421,7 +3437,7 @@ window.__ModuleLoader__.load({
3421
3437
  h('option', { value: 'auto' }, t('mAuto')),
3422
3438
  h('option', { value: 'cn' }, t('mCn')),
3423
3439
  h('option', { value: 'intl' }, t('mIntl'))) : null,
3424
- ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python' } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
3440
+ ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python'; try { var gp = localStorage.getItem('dsh-auto-memory.pyGpu'); if (gp === '1') n2.pythonGpu = true } catch (eL) {} } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
3425
3441
  !ready && isJs && !dlActive ? h('button', { 'data-dam-btn': '', onClick: startDl }, (dl.phase === 'error' || dl.phase === 'cancelled') ? t('semDlRetry') : t('semDlStart')) : null,
3426
3442
  !ready && isJs && dlActive ? h('button', { 'data-dam-btn': '', onClick: cancelDl }, t('semDlCancel')) : null,
3427
3443
  h('button', { 'data-dam-btn': '', onClick: function () { setGuide('') } }, t('gotIt'))))
package/lib/index.js CHANGED
@@ -2335,6 +2335,78 @@ class MemoryEngine {
2335
2335
  return true
2336
2336
  }
2337
2337
 
2338
+ /**
2339
+ * F1(M6-CONTRACT 外补丁,2026-08-23):anchor 开启时的记录级压缩。
2340
+ * 按 sidecar 记录序整条保留/移除——今天(标题日期=插件今天)记录无条件保留,其余按字节配额
2341
+ * 从最新往回保留(layer 上限 user 4000/note 3000);被移除记录整条原文写入归档文件
2342
+ * (writeFullRaw,不经 anchor 事务,归档非检索语料)。重组经 store.replace 原子执行:
2343
+ * 候选预检(clean)→被移除 id 自动删除→保留 id/版本不变。永不字符切片。
2344
+ * @returns {Promise<boolean|'not-anchored'>} true=已腾位 false=无法腾位 'not-anchored'=无 sidecar 走旧路径
2345
+ */
2346
+ async compactAnchoredLayer(store, filePath, layer, today, paths) {
2347
+ const cur = await this.readTextSafe(filePath)
2348
+ if (!cur || !cur.trim()) return false
2349
+ let parsed
2350
+ try { parsed = parseAnchors(Buffer.from(cur, 'utf8')) } catch (_) { return 'not-anchored' }
2351
+ if (!parsed || parsed.status !== 'clean') return false // 脏文件 fail closed:不压缩、不切片
2352
+ const anchored = parsed.records.filter((r) => r.kind === 'anchored')
2353
+ if (!anchored.length) return 'not-anchored'
2354
+ const limit = layer === 'user' ? 4000 : 3000
2355
+ const isTodayHeading = (heading) => /^\d{4}-\d{2}-\d{2}/.test(String(heading || '')) && String(heading).slice(0, 10) === today
2356
+ const buf = Buffer.from(cur, 'utf8')
2357
+ // 配额从尾回溯(F1):新的留;今天记录无条件保留
2358
+ const keep = new Array(anchored.length).fill(false)
2359
+ let keptBytes = 0
2360
+ for (let i = anchored.length - 1; i >= 0; i--) {
2361
+ const rec = anchored[i]
2362
+ const spanBytes = rec.byteEnd - rec.markerByteStart
2363
+ if (isTodayHeading(rec.heading) || keptBytes + spanBytes <= limit) { keep[i] = true; keptBytes += spanBytes }
2364
+ }
2365
+ const removedIdx = []
2366
+ anchored.forEach((rec, i) => { if (!keep[i]) removedIdx.push(i) })
2367
+ if (!removedIdx.length) return false // 无可移除 → 无法腾位(预算照常拒绝)
2368
+ // 有序块重组:legacy 块与 preamble/tail 原样保留,gap 字节逐段拷贝,仅丢弃被移除记录的 [marker..contentEnd]
2369
+ const blocks = []
2370
+ for (const r of parsed.records) {
2371
+ if (r.kind === 'anchored') blocks.push({ start: r.markerByteStart, end: r.byteEnd, keep: keep[anchored.indexOf(r)] })
2372
+ else blocks.push({ start: r.byteStart, end: r.byteEnd, keep: true })
2373
+ }
2374
+ blocks.sort((a, b) => a.start - b.start)
2375
+ const parts = []
2376
+ let cursorB = 0
2377
+ for (let i = 0; i < blocks.length; i++) {
2378
+ const b = blocks[i]
2379
+ if (b.start > cursorB) parts.push(buf.slice(cursorB, b.start).toString('utf8'))
2380
+ if (b.keep) parts.push(buf.slice(b.start, b.end).toString('utf8'))
2381
+ cursorB = b.end
2382
+ }
2383
+ if (cursorB < buf.length) parts.push(buf.slice(cursorB).toString('utf8'))
2384
+ const newText = parts.join('')
2385
+ // 归档:整条原文(writeFullRaw 绕开 anchor 事务;归档文件从不参与解析/索引)
2386
+ const archiveFile = layer === 'user'
2387
+ ? path.join(dshHome(), 'memory', 'archived-user.md')
2388
+ : path.join(paths.projectDir, 'archive', 'notes-archived.md')
2389
+ const archiveText = removedIdx.map((i2) => {
2390
+ const r = anchored[i2]
2391
+ return buf.slice(r.markerByteStart, r.byteEnd).toString('utf8')
2392
+ }).join('\n')
2393
+ const existingArchive = await this.readTextSafe(archiveFile)
2394
+ await this.writeFullRaw(archiveFile, (existingArchive ? existingArchive.replace(/\s+$/, '') + '\n' : '') + archiveText + '\n')
2395
+ const rr = await store.replace(filePath, newText)
2396
+ if (!rr.ok) { console.error('[compacted-anchored] replace failed: ' + rr.reason); return false }
2397
+ const freshText = await this.readTextSafe(filePath)
2398
+ if (layer === 'user') this.state.userText = freshText
2399
+ else this.state.notesText = freshText
2400
+ if (this._budgets) {
2401
+ const b = this._budgets.get('today')
2402
+ if (b && b.date === this.memToday()) b[layer] = 0
2403
+ }
2404
+ this._lastCompactAt = Date.now()
2405
+ this.state.loadedAt = Date.now()
2406
+ console.log('[compacted-anchored] ' + layer + ': kept=' + (anchored.length - removedIdx.length) + '/' + anchored.length + ' archived=' + removedIdx.length + ' bytes=' + cur.length + '->' + newText.length)
2407
+ return true
2408
+ }
2409
+
2338
2410
  // ---------- 注入渲染(同步,基于缓存) ----------
2339
2411
  // 动态记忆 → ctx.systemPrompt.context()(user-role 快照):内容变化才追加新快照,内容不变不重复注入(dsh-agent-loop project() 去重),
2340
2412
  // system prompt 不再包含动态内容 → 字节级稳定 → DeepSeek 前缀缓存全程命中(对比 section 方案:动态内容任何变化都从变化点起击穿整个前缀,含全部历史)
@@ -2381,6 +2453,17 @@ class MemoryEngine {
2381
2453
  if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.8) && Date.now() - (s.waterLevelAt || 0) < 600000) {
2382
2454
  lines.push('\n' + L('snapshotWaterTitle') + '\n' + L('snapshotWaterBody', { pct: Math.round((s.waterLevelRatio || 0) * 100) }))
2383
2455
  }
2456
+ // M-CM1 交接白板:PLAN.md 全貌+最新交接账本,动态快照首位(动态层内容,不碰静态字节;敏感段清洗+行边界硬预算截断)
2457
+ if (cfg.handoffEnabled !== false && (s.planText || s.latestHandoffText)) {
2458
+ const planPart = truncateLinesBounded(stripSensitiveSections(sanitizeForInjection(s.planText || '')), Math.max(Number(cfg.handoffPlanChars) || 1200, 200))
2459
+ if (planPart) lines.push('\n' + L('snapshotPlanTitle') + '\n' + planPart)
2460
+ const ledgerPart = truncateLinesBounded(stripSensitiveSections(sanitizeForInjection(s.latestHandoffText || '')), Math.max(Number(cfg.handoffLedgerChars) || 800, 200))
2461
+ if (ledgerPart) lines.push('\n' + L('snapshotHandoffTitle') + '\n' + ledgerPart)
2462
+ }
2463
+ // M-CM4/M-CM5 水位建议:越阈注入交接+派子代理建议(advisory;10 分钟新鲜度;无人值守静默)
2464
+ if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.8) && Date.now() - (s.waterLevelAt || 0) < 600000) {
2465
+ lines.push('\n' + L('snapshotWaterTitle') + '\n' + L('snapshotWaterBody', { pct: Math.round((s.waterLevelRatio || 0) * 100) }))
2466
+ }
2384
2467
  let used = 0
2385
2468
  const part = (title, text, max) => {
2386
2469
  if (!text) return
@@ -4829,7 +4912,10 @@ export function apply(ctx, config) {
4829
4912
  {
4830
4913
  const pluginDir = path.dirname(fileURLToPath(import.meta.url))
4831
4914
  engine._jsSemantic = createJsSemanticEnginePre({ pluginDir })
4832
- engine._jsDownload = createSemanticDownloaderPre({ modelsRoot: path.join(pluginDir, 'models') })
4915
+ // #15 后续/B:JS 模型下载落位用户目录(~/.dsh/models/js-semantic/)——包目录在 npm 更新时被重装,130MB 曾被冲掉
4916
+ const userModelsRoot = path.join((process.env.DSH_HOME && process.env.DSH_HOME.trim()) || path.join(homedir(), '.dsh'), 'models', 'js-semantic')
4917
+ mkdirSync(userModelsRoot, { recursive: true })
4918
+ engine._jsDownload = createSemanticDownloaderPre({ modelsRoot: userModelsRoot })
4833
4919
  // 资产探测(semantic-status / 档位解析 / 引导卡共用):共享实现见 semantic-js-pre 的
4834
4920
  // probeJsSemanticAssets —— peer 探测与引擎加载走同一套 Node 解析(2026-09-02 issue 修正)。
4835
4921
  // _peerExtraDirs = 深度扫描热接入位(semanticDeepDetect 命中后 probe/加载/档位即时生效)。
@@ -5458,7 +5544,8 @@ export function apply(ctx, config) {
5458
5544
  handler: async (req, res) => {
5459
5545
  if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
5460
5546
  if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
5461
- try { writeJson(res, 200, await engine._pythonSetup.ensureDeps()) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
5547
+ const body = await readJsonBody(req).catch(() => ({}))
5548
+ try { writeJson(res, 200, await engine._pythonSetup.ensureDeps({ gpu: !!(body && body.gpu) })) } catch (e) { writeJson(res, 500, { error: String(e && e.message || e) }) }
5462
5549
  },
5463
5550
  },
5464
5551
  {
@@ -35,8 +35,9 @@ const MODEL_SPEC = {
35
35
  ],
36
36
  }
37
37
 
38
- /** venv 内 pip 依赖:与 bench 校准环境同源(transformers AutoTokenizer + onnxruntime;torch 为张量构造所需;池化/精度契约冻结自 R@5 0.925 基线,fastembed 注册表无 BGE-M3 且池化契约不同,不可用)。 */
39
- const PIP_DEPS = ['transformers', 'onnxruntime', 'torch']
38
+ /** venv 内 pip 依赖:#19 实测(int8 档 encode_ids 全程 numpy+onnxruntime,tokenizer 走 Rust 快速路径)——基础集无 torch,venv 体积 ~400MB。GPU 推理开关追加 onnxruntime-gpu(CUDAExecutionProvider)。池化/精度契约冻结自 R@5 0.925 基线,fastembed 注册表无 BGE-M3 且池化契约不同,不可用。 */
39
+ const PIP_DEPS_CPU = ['transformers', 'onnxruntime']
40
+ const PIP_DEPS_GPU_EXTRA = ['onnxruntime-gpu']
40
41
 
41
42
  export function createPythonSetupPre(opts = {}) {
42
43
  const dshHomeOf = typeof opts.dshHome === 'function' ? opts.dshHome : () => opts.dshHome || path.join(homedir(), '.dsh')
@@ -110,7 +111,7 @@ export function createPythonSetupPre(opts = {}) {
110
111
  return {
111
112
  phase: st.phase, error: st.error,
112
113
  pythons: st.pythons, chosenPython: st.chosenPython,
113
- venvOk: st.venvOk, depsOk: st.depsOk, modelReady: st.modelReady,
114
+ venvOk: st.venvOk, depsOk: st.depsOk, modelReady: st.modelReady, wantGpu: !!st.wantGpu,
114
115
  modelPath: modelPath(), venvPython: venvPython(),
115
116
  modelBytes: st.modelReady ? statSync(modelPath()).size : 0, modelExpectedBytes: MODEL_SPEC.bytes,
116
117
  dl: { ...st.dl },
@@ -136,14 +137,17 @@ export function createPythonSetupPre(opts = {}) {
136
137
  return snapshot()
137
138
  }
138
139
 
139
- /** ③deps:venv 内 pip 装 transformers+onnxruntime+torch;官方源失败切清华镜像。 */
140
- async function ensureDeps() {
140
+ /** ③deps:venv 内 pip 装 transformers+onnxruntime(CPU 基础集);opts.gpu=true 追加 onnxruntime-gpu(用户选 GPU 推理时)。官方源失败切清华镜像。 */
141
+ async function ensureDeps(opts2) {
142
+ const wantGpu = !!(opts2 && opts2.gpu)
141
143
  if (!st.venvOk) { st.error = 'venv 未就绪,先执行 venv 步骤'; st.phase = 'error'; return snapshot() }
142
144
  st.phase = 'deps'
145
+ st.wantGpu = wantGpu
146
+ const deps = PIP_DEPS_CPU.concat(wantGpu ? PIP_DEPS_GPU_EXTRA : [])
143
147
  const runPip = async (extra) => {
144
148
  try {
145
- const r = await execFileP(venvPython(), ['-m', 'pip', 'install', '--quiet'].concat(PIP_DEPS).concat(extra || []),
146
- { timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
149
+ const r = await execFileP(venvPython(), ['-m', 'pip', 'install', '--quiet'].concat(deps).concat(extra || []),
150
+ { timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
147
151
  return { ok: true }
148
152
  } catch (e) { return { ok: false, tail: String((e && (e.stderr || e.message)) || e).slice(-300) } }
149
153
  }
@@ -151,7 +155,18 @@ export function createPythonSetupPre(opts = {}) {
151
155
  if (!r.ok) r = await runPip(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
152
156
  st.depsOk = !!r.ok
153
157
  if (!r.ok) { st.error = '依赖安装失败: ' + (r.tail || ''); st.phase = 'error'; return snapshot() }
154
- diagOf('python-setup: deps installed into venv')
158
+ // GPU 偏好写入 embedding-config.json(worker 读 config['gpu'] 选 CUDA/CPU provider;read-modify-write 不覆盖既有键)
159
+ try {
160
+ const cfgDir = path.join(dshHomeOf(), 'memory', 'semantic')
161
+ mkdirSync(cfgDir, { recursive: true })
162
+ const cfgPath = path.join(cfgDir, 'embedding-config.json')
163
+ let cfg = {}
164
+ try { cfg = JSON.parse(await readFile(cfgPath, 'utf8')) } catch (_) {}
165
+ if (!cfg || typeof cfg !== 'object') cfg = {}
166
+ cfg.gpu = wantGpu
167
+ await writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8')
168
+ } catch (eCfg) { diagOf('python-setup: gpu pref write failed: ' + String(eCfg && eCfg.message || eCfg)) }
169
+ diagOf('python-setup: deps installed into venv (gpu=' + wantGpu + ')')
155
170
  return snapshot()
156
171
  }
157
172