dsh-lite-memory 0.18.28 → 0.18.29

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/client.js CHANGED
@@ -2200,6 +2200,21 @@ window.__ModuleLoader__.load({
2200
2200
 
2201
2201
  React.useEffect(() => { load(); }, [sessionId]);
2202
2202
 
2203
+ /** 待裁决只做两件事:把草稿另存成一条,或删掉这条待办;不在面板里悄悄合并。 */
2204
+ const resolveQueued = (row, op) => {
2205
+ if (!row || busy) return;
2206
+ setBusy(true);
2207
+ setNote('');
2208
+ post('/lite-memory/conflict', { sessionId, scopeKey: selectedKey, title: row.title, op }).then((result) => {
2209
+ if (!alive.current) return;
2210
+ setBusy(false);
2211
+ if (result.ok && result.body && result.body.ok) {
2212
+ if (op === 'drop') setNote('已忽略这条待裁决');
2213
+ load(selectedKey);
2214
+ } else setNote(failure(result, '待裁决没处理成'));
2215
+ });
2216
+ };
2217
+
2203
2218
  /** 显示设置跟着当前项目走;换项目或换范围时重新读。 */
2204
2219
  React.useEffect(() => {
2205
2220
  setDisplay(readDisplaySettings(selectedKey));
@@ -2249,6 +2264,7 @@ window.__ModuleLoader__.load({
2249
2264
  const selected = data ? data.selected : null;
2250
2265
  const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
2251
2266
  const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
2267
+ const queued = selected && Array.isArray(selected.conflicts) ? selected.conflicts : EMPTY;
2252
2268
  const currentKey = current ? current.key : '';
2253
2269
  const control = selected && selected.control
2254
2270
  ? selected.control
@@ -2710,12 +2726,24 @@ window.__ModuleLoader__.load({
2710
2726
  const memoryViewActive = viewModeOf(display, memoryView);
2711
2727
  const evolutionViewActive = graphViewOf(display.graph, evolutionView);
2712
2728
 
2729
+ const conflictBar = queued.length === 0 ? null : h('div', {
2730
+ style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 9px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
2731
+ },
2732
+ h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, `${queued.length} 条保存没确认`),
2733
+ queued.slice(0, 2).map((row) => h('div', {
2734
+ key: row.id || row.title,
2735
+ style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 },
2736
+ },
2737
+ h('span', { style: { flex: 1, minWidth: 0, fontSize: '12px', lineHeight: '20px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, row.title),
2738
+ h(Action, { disabled: busy, onClick: () => resolveQueued(row, 'keep') }, '另存'),
2739
+ h(Action, { disabled: busy, onClick: () => resolveQueued(row, 'drop') }, '忽略'),
2740
+ )));
2713
2741
  const memoryListPane = h('div', {
2714
2742
  style: {
2715
2743
  ...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
2716
2744
  display: 'flex', flexDirection: 'column', gap: 0,
2717
2745
  },
2718
- }, memoryList);
2746
+ }, conflictBar, memoryList);
2719
2747
  const memoryGraphPane = h('div', {
2720
2748
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
2721
2749
  },
package/index.js CHANGED
@@ -13,6 +13,9 @@ import {
13
13
  runtimeLayers,
14
14
  findAnywhere,
15
15
  renderIndex,
16
+ conflictsOf,
17
+ resolveConflict,
18
+ dropConflict,
16
19
  loadIndex,
17
20
  saveEntry,
18
21
  move,
@@ -214,6 +217,7 @@ function keepHandler(webServer, cwdForSession) {
214
217
  : pathname === REMOTE_PATH + '/entry' ? 'entry'
215
218
  : pathname === REMOTE_PATH + '/move' ? 'move'
216
219
  : pathname === REMOTE_PATH + '/overview' ? 'overview'
220
+ : pathname === REMOTE_PATH + '/conflict' ? 'conflict'
217
221
  : pathname === REMOTE_PATH + '/control' ? 'control'
218
222
  : pathname === REMOTE_PATH + '/evolution' ? 'evolution'
219
223
  : pathname === REMOTE_PATH + '/resume-move' ? 'resume-move' : '';
@@ -530,6 +534,7 @@ function keepHandler(webServer, cwdForSession) {
530
534
  evolution: selectedEvolution,
531
535
  records,
532
536
  archived,
537
+ conflicts: conflictsOf(selectedScope),
533
538
  stats: {
534
539
  total: records.length,
535
540
  experimental: records.filter((record) => record.experimental).length,
@@ -541,6 +546,26 @@ function keepHandler(webServer, cwdForSession) {
541
546
  });
542
547
  }
543
548
 
549
+ /** 待裁决:用队列里留下的草稿另存,或删掉这条待办;不预写、不替用户合并。 */
550
+ if (action === 'conflict') {
551
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
552
+ if (!sessionId) return sendJson(res, 400, { ok: false, reason: '缺少 sessionId' });
553
+ const cwd = typeof cwdForSession === 'function' ? cwdForSession(sessionId) : '';
554
+ if (!cwd) return sendJson(res, 409, { ok: false, reason: '这个会话还没有项目目录' });
555
+ const projectScope = scopeOf(cwd);
556
+ const requested = typeof body.scopeKey === 'string' ? body.scopeKey : '';
557
+ const target = requested === 'global' ? globalScope() : projectScope;
558
+ const title = typeof body.title === 'string' ? body.title.trim() : '';
559
+ if (!title) return sendJson(res, 400, { ok: false, reason: '缺少标题' });
560
+ try {
561
+ const outcome = body.op === 'drop' ? { ok: dropConflict(target, title) } : resolveConflict(target, title);
562
+ if (!outcome.ok) return sendJson(res, 409, { ok: false, reason: outcome.reason || '这条待裁决还没处理成' });
563
+ return sendJson(res, 200, { ok: true, remaining: conflictsOf(target).length });
564
+ } catch (error) {
565
+ return sendJson(res, 409, { ok: false, reason: error instanceof Error ? error.message : '待裁决处理失败' });
566
+ }
567
+ }
568
+
544
569
  /**
545
570
  * 本项目记忆总开关:关闭后这层不再召回、检索、读写、学习,也不继承全局记忆。
546
571
  * 自进化设置原样保留,但有效状态随总开关暂停。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.28",
3
+ "version": "0.18.29",
4
4
  "description": "On-demand memory for the DeepSeek Harness: nothing resident, at most three small cards on a real hit, bodies read on demand.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/store.js CHANGED
@@ -2723,6 +2723,29 @@ function bindSameTopic(projectScope, ref) {
2723
2723
  /** 冲突队列:只记待办,不预写记忆;最多 20 条。 */
2724
2724
  const CONFLICT_LIMIT = 20;
2725
2725
 
2726
+ /** 待办里留一份可重放的草稿:面板能直接另存,不用用户回忆刚才想记什么。 */
2727
+ const CONFLICT_DRAFT_KEYS = ['title', 'summary', 'body', 'when', 'kind', 'tags', 'conditions', 'applicability', 'topicKey', 'source', 'origin'];
2728
+ function conflictDraftOf(draft) {
2729
+ const out = {};
2730
+ if (!draft || typeof draft !== 'object') return out;
2731
+ for (const key of CONFLICT_DRAFT_KEYS) {
2732
+ const value = draft[key];
2733
+ if (value === undefined || value === null) continue;
2734
+ if (key === 'title') out.title = normalizeText(value, 120);
2735
+ else if (key === 'summary') out.summary = normalizeText(value, 400);
2736
+ else if (key === 'body') out.body = typeof value === 'string' ? value.slice(0, 8000) : '';
2737
+ else if (key === 'when') out.when = normalizeText(value, 160);
2738
+ else if (key === 'kind') { if (KINDS.includes(value)) out.kind = value; }
2739
+ else if (key === 'tags') out.tags = Array.isArray(value) ? value.slice(0, 5) : undefined;
2740
+ else if (key === 'conditions') out.conditions = Array.isArray(value) ? value.slice(0, 8) : undefined;
2741
+ else if (key === 'applicability') out.applicability = value;
2742
+ else if (key === 'topicKey') out.topicKey = normalizeText(value, 80);
2743
+ else if (key === 'source') out.source = value;
2744
+ else if (key === 'origin') out.origin = value;
2745
+ }
2746
+ return out;
2747
+ }
2748
+
2726
2749
  function queueConflictLocked(scope, draft, candidates) {
2727
2750
  const index = loadIndex(scope);
2728
2751
  const title = normalizeText(draft && draft.title, 120);
@@ -2734,11 +2757,13 @@ function queueConflictLocked(scope, draft, candidates) {
2734
2757
  id: item.id, title: item.title || '', when: item.when || '', reason: item.reason || 'similar_title',
2735
2758
  }));
2736
2759
  const hit = rows.find((row) => row.title === title && row.scope === scopeKey);
2760
+ const replay = conflictDraftOf(draft);
2737
2761
  if (hit) {
2738
2762
  hit.candidates = view;
2763
+ hit.draft = replay;
2739
2764
  hit.updatedAt = now;
2740
2765
  } else {
2741
- rows.push({ id: `cf_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`, title, scope: scopeKey, candidates: view, createdAt: now, updatedAt: now });
2766
+ rows.push({ id: `cf_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`, title, scope: scopeKey, candidates: view, draft: replay, createdAt: now, updatedAt: now });
2742
2767
  }
2743
2768
  index.conflicts = rows.slice(-CONFLICT_LIMIT);
2744
2769
  saveIndex(scope, index);
@@ -2756,6 +2781,36 @@ function clearConflictLocked(scope, title) {
2756
2781
  return next.length;
2757
2782
  }
2758
2783
 
2784
+ /** 面板待裁决列表:只给可重放草稿与候选,不预写记忆。 */
2785
+ export function conflictsOf(scope) {
2786
+ const rows = loadIndex(scope).conflicts;
2787
+ return Array.isArray(rows) ? JSON.parse(JSON.stringify(rows)) : [];
2788
+ }
2789
+
2790
+ /** 确认另存:用队列里的草稿按 distinct 重放一次,成功后出队。 */
2791
+ export function resolveConflict(scope, title, options = {}) {
2792
+ const rows = Array.isArray(loadIndex(scope).conflicts) ? loadIndex(scope).conflicts : [];
2793
+ const norm = normalizeText(title, 120);
2794
+ const row = rows.find((item) => item.title === norm && item.scope === (scope.key === 'global' ? 'global' : 'project'));
2795
+ if (!row) return { ok: false, reason: '这条待裁决已经不在了,刷新面板看看' };
2796
+ const draft = { ...(row.draft || {}), resolution: 'distinct', writeMode: options.writeMode === 'append' ? 'append' : 'patch' };
2797
+ if (scope.key === 'global') draft.scope = 'global';
2798
+ else draft.scope = 'project';
2799
+ if (!draft.title) draft.title = row.title;
2800
+ const result = saveEntry(scope, draft);
2801
+ if (result.needsResolution) return { ok: false, reason: '还是相近:换一个标题或等确认', result };
2802
+ if (result.needsScope) return { ok: false, reason: '这条记忆的适用范围还没定,先说明当前环境', result };
2803
+ clearConflictLocked(scope, row.title);
2804
+ return { ok: true, result };
2805
+ }
2806
+
2807
+ /** 删掉待裁决:用户决定不再另存。 */
2808
+ export function dropConflict(scope, title) {
2809
+ const before = pendingConflictsOf(scope);
2810
+ clearConflictLocked(scope, title);
2811
+ return pendingConflictsOf(scope) < before;
2812
+ }
2813
+
2759
2814
  /** 当前层还有几条没处理的冲突。 */
2760
2815
  export function pendingConflictsOf(scope) {
2761
2816
  const rows = loadIndex(scope).conflicts;
@@ -1210,4 +1210,26 @@ assert.equal(ovArchivedBody.status, 200);
1210
1210
  assert.equal(ovArchivedBody.body.status, 'archived');
1211
1211
  assert.equal(ovArchivedBody.body.body, '右侧留白。');
1212
1212
 
1213
+ // 待裁决:面板看得到队列,确认后按草稿另存,不预写正文
1214
+ const cfCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-conflict-'));
1215
+ sessionCwds.set('session-conflict-panel', cfCwd);
1216
+ const cfAgent = { id: 'session-conflict-panel', cwd: cfCwd, session: { id: 'session-conflict-panel', header: { cwd: cfCwd } } };
1217
+ await defs.get('memory_save').execute({ title: '发布窗口', summary: '只在周四晚上发版', body: '周四晚上再发版。', scope: 'global' }, { agent: cfAgent });
1218
+ const cfSave = await defs.get('memory_save').execute({ title: '发布窗口', summary: '本项目周五发版', body: '周五发版。' }, { agent: cfAgent });
1219
+ assert.equal(cfSave.needsResolution, true);
1220
+ assert.equal(store.loadIndex(store.scopeOf(cfCwd)).entries.length, 0, '没确认前不落库');
1221
+ const cfBefore = await call('/lite-memory/overview', { sessionId: 'session-conflict-panel' });
1222
+ assert.equal(cfBefore.status, 200);
1223
+ assert.equal(cfBefore.body.selected.conflicts.length, 1);
1224
+ assert.equal(cfBefore.body.selected.conflicts[0].title, '发布窗口');
1225
+ assert.equal(cfBefore.body.selected.conflicts[0].draft.body, '周五发版。');
1226
+ const cfKeep = await call('/lite-memory/conflict', { sessionId: 'session-conflict-panel', scopeKey: cfBefore.body.selected.key, title: '发布窗口', op: 'keep' });
1227
+ assert.equal(cfKeep.status, 200);
1228
+ assert.equal(cfKeep.body.ok, true);
1229
+ const cfAfter = await call('/lite-memory/overview', { sessionId: 'session-conflict-panel' });
1230
+ assert.equal(cfAfter.body.selected.conflicts.length, 0);
1231
+ assert.ok(cfAfter.body.selected.records.some((row) => row.title === '发布窗口' && row.summary === '本项目周五发版'));
1232
+ const cfAgain = await call('/lite-memory/conflict', { sessionId: 'session-conflict-panel', scopeKey: cfBefore.body.selected.key, title: '发布窗口', op: 'keep' });
1233
+ assert.equal(cfAgain.status, 409, '队列没了就明确说,不重复落库');
1234
+
1213
1235
  console.log('contract.test.mjs: all passed');
@@ -11,6 +11,7 @@ const view = await import('../view.js');
11
11
  const {
12
12
  saveEntry, routeScope, visibleRules, move, resumeMove, movePending,
13
13
  upsert, forget, readLinks, loadIndex, scopeOf, globalScope, readEntry, pendingConflictsOf,
14
+ conflictsOf, resolveConflict, dropConflict,
14
15
  } = store;
15
16
 
16
17
  let passed = 0;
@@ -79,6 +80,12 @@ await test('S03 模型给的类别标签不能把「本项目」抬进全局层'
79
80
  });
80
81
  assert.equal(proc.chosenScope, 'project');
81
82
  assert.equal(proc.reasonCode, 'project_bound_procedure');
83
+ /** 人自己确认「就存全局」时,措辞推断不能压过明确指令。 */
84
+ const human = routeScope({
85
+ title: '本项目回复用中文', body: '本项目统一中文、简短', memoryClass: 'personal_preference', scope: 'global',
86
+ }, { humanScope: true, project: { confirmed: true } });
87
+ assert.equal(human.chosenScope, 'global');
88
+ assert.equal(human.reasonCode, 'explicit');
82
89
  });
83
90
 
84
91
  await test('S03 设备事实没给设备条件就返回 needsScope,不落盘', () => {
@@ -412,4 +419,22 @@ await test('S12 相近保存进待办队列,确认 distinct 后出队', () =>
412
419
  assert.equal(pendingConflictsOf(scope), 0);
413
420
  });
414
421
 
422
+ await test('S12 待裁决留可重放草稿,面板确认另存后才落库', () => {
423
+ const scope = fresh('conflict-replay');
424
+ saveEntry(globalScope(), { title: '提交信息格式', summary: '用一句话说清做了什么', body: '全局约定', resolution: 'distinct' });
425
+ const near = saveEntry(scope, { title: '提交信息格式', summary: '本项目用中文写提交信息', body: '项目补充', when: '本仓库' });
426
+ assert.equal(near.needsResolution, true);
427
+ assert.equal(loadIndex(scope).entries.length, 0, '没确认前不能预写正文');
428
+ const [row] = conflictsOf(scope);
429
+ assert.equal(row.title, '提交信息格式');
430
+ assert.equal(row.draft.body, '项目补充');
431
+ assert.equal(row.draft.when, '本仓库');
432
+ const kept = resolveConflict(scope, '提交信息格式');
433
+ assert.equal(kept.ok, true);
434
+ assert.equal(pendingConflictsOf(scope), 0);
435
+ assert.equal(loadIndex(scope).entries.length, 1);
436
+ assert.equal(loadIndex(scope).entries[0].body, '项目补充');
437
+ assert.equal(dropConflict(scope, '不存在'), false);
438
+ });
439
+
415
440
  console.log(`crossing.test.mjs: ${passed} passed`);