dsh-lite-memory 0.16.2 → 0.16.4

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
@@ -315,6 +315,7 @@ window.__ModuleLoader__.load({
315
315
  return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
316
316
  };
317
317
 
318
+ const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
318
319
  const versionLine = (shown) => {
319
320
  const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
320
321
  const archived = shown.status === 'archived' ? ' · 已归档' : '';
@@ -389,7 +390,7 @@ window.__ModuleLoader__.load({
389
390
  'aria-label': `查看记忆 ${item.title || item.id}`,
390
391
  title: item.title || item.id,
391
392
  style: ITEM, // ui-copy-ignore
392
- }, `${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
393
+ }, `${scopeTag(item.scope)} ${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
393
394
  const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
394
395
  ? h('div', { style: BODY },
395
396
  shown.loading ? '读取中' : null,
@@ -686,8 +687,10 @@ window.__ModuleLoader__.load({
686
687
  const primaryKind = props.primaryKind || 'memory';
687
688
  const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records), [props.records, props.graphOf]);
688
689
  const [zoom, setZoom] = React.useState(1);
690
+ const [hoveredId, setHoveredId] = React.useState('');
689
691
  const selectedId = props.selectedId;
690
- const labelSet = graph.people <= 10;
692
+ const labelSet = graph.people <= 40;
693
+ const pickHub = props.onPickHub;
691
694
  const stepZoom = (delta) => setZoom((value) => Math.min(2.5, Math.max(0.75, Math.round((value + delta) * 100) / 100)));
692
695
  const frame = React.useMemo(() => {
693
696
  const list = graph.nodes;
@@ -765,26 +768,48 @@ window.__ModuleLoader__.load({
765
768
  h('g', null, graph.nodes.map((node) => {
766
769
  const isPrimary = node.kind === primaryKind;
767
770
  const active = node.id === selectedId;
771
+ const hovered = node.id === hoveredId;
772
+ const pick = isPrimary ? props.onPick : (pickHub || undefined);
768
773
  const fill = !isPrimary ? 'var(--dsw-alias-label-secondary)'
769
774
  : node.state ? (STAGE_TONE[node.state] || 'var(--dsw-alias-brand-primary)')
770
775
  : node.experimental ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-brand-primary)';
771
- const showLabel = active || (isPrimary && labelSet) || !isPrimary;
776
+ const showLabel = active || hovered || (isPrimary && labelSet) || !isPrimary;
777
+ const nameChars = active || hovered ? 14 : isPrimary ? 7 : 10;
772
778
  return h('g', {
773
779
  key: node.id,
774
780
  transform: `translate(${node.x} ${node.y})`,
775
- onClick: isPrimary ? () => props.onPick(node) : undefined,
776
- style: { cursor: isPrimary ? 'pointer' : 'default' },
781
+ onClick: pick ? () => pick(node) : undefined,
782
+ onMouseEnter: () => setHoveredId(node.id),
783
+ onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
784
+ style: { cursor: pick ? 'pointer' : 'default' },
777
785
  },
786
+ h('circle', { r: Math.max(12, node.radius + 6), fill: 'transparent' }),
778
787
  active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 1.4, opacity: 0.5 }) : null,
779
- h('circle', { r: node.radius, fill, opacity: isPrimary ? 0.9 : 0.5 }),
788
+ h('circle', {
789
+ r: node.radius, fill,
790
+ opacity: active ? 1 : isPrimary ? 0.9 : 0.5,
791
+ stroke: hovered ? 'var(--dsw-alias-brand-primary)' : 'none',
792
+ strokeWidth: hovered ? 1.2 : 0,
793
+ }),
780
794
  showLabel ? h('text', {
781
795
  y: node.radius + 12, textAnchor: 'middle',
782
796
  fill: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
783
- fontSize: isPrimary ? 10 : 8.5, opacity: active ? 0.95 : 0.68,
784
- }, SHORT(node.label, isPrimary ? 9 : 10)) : null,
797
+ fontSize: isPrimary ? 9.5 : 8.5, opacity: active ? 0.95 : 0.72,
798
+ paintOrder: 'stroke', stroke: 'var(--dsw-alias-bg-layer-1)', strokeWidth: 3,
799
+ strokeLinejoin: 'round',
800
+ }, SHORT(node.label, nameChars)) : null,
785
801
  h('title', null, node.title || node.label),
786
802
  );
787
803
  }))),
804
+ h('div', {
805
+ style: {
806
+ position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
807
+ maxWidth: 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
808
+ background: 'var(--dsw-alias-bg-layer-1)',
809
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
810
+ pointerEvents: 'none',
811
+ },
812
+ }, props.legend || '圆点 记忆 · 小点 标签/主题 · 虚线 关联'),
788
813
  );
789
814
  };
790
815
 
@@ -1126,6 +1151,11 @@ window.__ModuleLoader__.load({
1126
1151
  const changedCount = activity && typeof activity === 'object' && Number.isInteger(activity.changed)
1127
1152
  ? activity.changed
1128
1153
  : (Number.isInteger(activity && activity.count) ? activity.count : null);
1154
+ const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
1155
+ ? activity.reason
1156
+ : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
1157
+ const staleLearning = /没采用|作废|已更新|换到别的作品/.test(activityReason);
1158
+ const activityReasonText = staleLearning ? '整理过程中反馈或作品变了,这轮结果没采用。' : activityReason;
1129
1159
  let activityText = '';
1130
1160
  if (!evolution && memoryOn) {
1131
1161
  activityText = '读不到自进化状态';
@@ -1142,7 +1172,7 @@ window.__ModuleLoader__.load({
1142
1172
  } else if (activityCode === 'quota' || activityCode === 'exhausted') {
1143
1173
  activityText = '本轮整理额度已用完';
1144
1174
  } else if (activityCode === 'failed' || activityCode === 'error') {
1145
- activityText = '最近整理失败,可查看原因';
1175
+ activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
1146
1176
  } else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
1147
1177
  activityText = '等待可学习的反馈';
1148
1178
  }
@@ -1155,16 +1185,12 @@ window.__ModuleLoader__.load({
1155
1185
  else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
1156
1186
  else activityText = '暂无整理记录';
1157
1187
  }
1158
- const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
1159
- ? activity.reason
1160
- : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
1161
-
1162
1188
  const filtered = React.useMemo(() => {
1163
1189
  const needle = query.trim().toLowerCase();
1164
1190
  if (!needle) return records;
1165
1191
  return records.filter((record) => {
1166
1192
  const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
1167
- return `${record.title} ${record.summary} ${tags} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1193
+ return `${record.title} ${record.summary} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1168
1194
  });
1169
1195
  }, [records, query]);
1170
1196
  const filteredRules = React.useMemo(() => {
@@ -1355,7 +1381,15 @@ window.__ModuleLoader__.load({
1355
1381
  const memoryGraphPane = h('div', {
1356
1382
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1357
1383
  },
1358
- h(MemoryGraph, { records: filtered, selectedId: selectedMemory, onPick: onPickNode, emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图' }));
1384
+ h(MemoryGraph, {
1385
+ records: filtered, selectedId: selectedMemory, onPick: onPickNode,
1386
+ onPickHub: (node) => {
1387
+ setQuery(node.kind === 'tag' ? node.label.replace(/^#/, '') : node.label);
1388
+ setMemoryView('list');
1389
+ },
1390
+ emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
1391
+ legend: '圆点 记忆 · 小点 标签/主题 · 虚线 关联 · 点任意点筛选或查看',
1392
+ }));
1359
1393
  const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1360
1394
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
1361
1395
  h('input', {
@@ -1452,6 +1486,7 @@ window.__ModuleLoader__.load({
1452
1486
  records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
1453
1487
  selectedId: selectedRule, onPick: onPickRuleNode,
1454
1488
  emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
1489
+ legend: '圆点 经验 · 小点 条件 · 虚线 共用条件 · 点圆点查看',
1455
1490
  }));
1456
1491
  const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1457
1492
  h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
@@ -1459,9 +1494,9 @@ window.__ModuleLoader__.load({
1459
1494
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
1460
1495
  h('div', { style: { fontSize: '12px', lineHeight: '1.55' } }, activityText || (rules.length === 0 ? '还没有整理出经验' : '暂无整理记录')),
1461
1496
  countsUnknown ? h('div', { style: PANEL_MUTED }, '数量未知') : null,
1462
- activityReason
1497
+ activityReasonText
1463
1498
  ? (showReason
1464
- ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReason)
1499
+ ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReasonText)
1465
1500
  : h(Action, { onClick: () => setShowReason(true) }, '查看原因'))
1466
1501
  : null,
1467
1502
  ),
@@ -1536,8 +1571,8 @@ window.__ModuleLoader__.load({
1536
1571
  if (!sessionId && refs.length === 0) return null;
1537
1572
  if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) return null;
1538
1573
  return h('div', { style: TAIL },
1539
- h(RefSection, { label: '检索发现', items: groups.found, sessionId, turnKey: turn }),
1540
- h(RefSection, { label: '已提供正文', items: groups.provided, sessionId, turnKey: turn }),
1574
+ h(RefSection, { label: '搜索命中', items: groups.found, sessionId, turnKey: turn }),
1575
+ h(RefSection, { label: '读过正文', items: groups.provided, sessionId, turnKey: turn }),
1541
1576
  h(RefSection, { label: '本轮更新', items: groups.update, sessionId, turnKey: turn }),
1542
1577
  );
1543
1578
  };
package/index.js CHANGED
@@ -1361,7 +1361,7 @@ export function apply(ctx, config) {
1361
1361
 
1362
1362
  ctx.effect(() => tools.register({
1363
1363
  name: 'memory_save',
1364
- description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,不抄原始输出里的具体数值。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时带 id:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就用那个 id 更新,确实是另一条才带 resolution="distinct" 再存。带 id 更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
1364
+ description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时带 id:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就用那个 id 更新,确实是另一条才带 resolution="distinct" 再存。带 id 更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
1365
1365
  parameters: SAVE_PARAMS,
1366
1366
  output: {
1367
1367
  presentationMeta: (_args, value) => ({
@@ -2250,7 +2250,7 @@ export function apply(ctx, config) {
2250
2250
  if (sawTool) throw new Error('整理返回了工具调用');
2251
2251
  /** 运行期间用户又给了新反馈:旧结果不能覆盖新纠正,直接作废,等排队的下一次整理。 */
2252
2252
  if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
2253
- throw new Error('反馈已更新,这次整理作废');
2253
+ throw new Error('反馈已更新,这轮结果没采用');
2254
2254
  }
2255
2255
  const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
2256
2256
  if (!parsed.ok) throw new Error(parsed.reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "On-demand memory for the DeepSeek Harness: nothing in the system prompt, one muted line only when something is saved, hit or suggested.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/store.js CHANGED
@@ -2546,7 +2546,7 @@ export function commitLearning(scope, batch = {}) {
2546
2546
  if (!episode) throw new Error('作品已经失效,这次整理作废');
2547
2547
  if (batch.sessionId) {
2548
2548
  const focus = evolution.tasks?.sessions?.[batch.sessionId];
2549
- if (!focus || focus.episodeId !== episodeId) throw new Error('这期已经换到别的作品,这次整理作废');
2549
+ if (!focus || focus.episodeId !== episodeId) throw new Error('这期已经换到别的作品,这轮结果没采用');
2550
2550
  }
2551
2551
  if (batch.episodeUpdatedAt && episode.updatedAt !== batch.episodeUpdatedAt) throw new Error('用户又改过这期,这次整理作废');
2552
2552
  const prepared = new Map((Array.isArray(batch.preparedTargets) ? batch.preparedTargets : []).map((row) => [row.id, row]));
@@ -433,7 +433,7 @@ test('P06 作品版本变了旧整理整批作废', () => {
433
433
  store.saveTaskState(scope, later.tasks, { operationId: 'tasks_later' });
434
434
  const stale = commitNew(scope, grown, started, NEW_CHANGE, { now: Date.now() + 2000 });
435
435
  assert.equal(stale.ok, false);
436
- assert.match(stale.reason, /换到别的作品|改过这期|失效/);
436
+ assert.match(stale.reason, /换到别的作品|改过这期|失效|没采用/);
437
437
  assert.equal(ruleRows(scope).length, 0);
438
438
  });
439
439