dsh-lite-memory 0.18.30 → 0.18.31

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/anchor.js CHANGED
@@ -10,6 +10,8 @@ export const TASK_LIMITS = { sessions: 8, episodes: 12, sourceMessages: 8, idleM
10
10
 
11
11
  const SWITCH_WORDS = /(改做|改为|改到|改成|换个|换一个|换成|转到|现在做|接下来做|新做|另做|开始做|去做)/;
12
12
  const CONTINUE_WORDS = /(继续|再做一期|下一期|再来一期|再开一期|续一集|再拍一期)/;
13
+ /** 短续句只由通用动作词组成、没点名新对象:仍属于当前任务,不能当换任务。 */
14
+ const ELLIPTICAL_CONTINUE = /^(?:继续|接着|再接着|往下)[,,、\s]*(?:把|将|再)?(?:优化|收敛|调整|完善|打磨|推进|处理|改|修|做|写|整理|检查|排查|测试|发布|跑|看看|看下|弄|试|收尾|统一|对齐|细化|补|过一遍|走一遍|一下|下)+[。.!!??…]*$/u;
13
15
  const REVISE_WORDS = /(再改一下|改一版|再改改|调整一下|这版再|继续改)/;
14
16
  const THANKS_WORDS = /^(谢谢|多谢|谢了|辛苦了|thanks|thank\s*you)[!!。.~~\s]*$/i;
15
17
  const REFERENCE_WORDS = /(这个|这条|这次|上次|之前|刚才|上面|前面|它|这里|继续|接着|还是|同样|再改)/;
@@ -71,6 +73,7 @@ function taskTerms(value) {
71
73
  export function looksLikeNewTask(query, previousText) {
72
74
  const current = text(query, 200);
73
75
  if (!current || isThanks(current)) return false;
76
+ if (ELLIPTICAL_CONTINUE.test(current)) return false;
74
77
  const next = taskTerms(current);
75
78
  const previous = taskTerms(previousText);
76
79
  const cue = isContinuation(current) || REFERENCE_WORDS.test(current);
@@ -0,0 +1,6 @@
1
+ # 0.18.31
2
+
3
+ - 「继续优化收敛」这类短续句不再当成换任务:句首是续说标记、其余只由通用动作词组成时,沿用当前任务与上一句原话;带新对象的「继续优化数据库连接池」仍然换任务。
4
+ - 续句或没有具体动作的短问句,把上一轮助手的话和工具名并进同一份查询,只查一次;通用卡片不再抢先占满名额、挡住真正相关的动作线索。
5
+ - 明确换任务(换做/改用/先别管,或点了一个上一句没有的英文对象)时不并旧动作线索,任务类型认不出也不会把旧任务记忆带进新任务。
6
+ - 回归覆盖:短续句承接下来回、换到数据库后继续说「继续」只承接数据库、未知类型换任务不带旧工具线索、通用卡片存在时动作线索仍能命中。
package/index.js CHANGED
@@ -2714,8 +2714,10 @@ export function apply(ctx, config) {
2714
2714
  /** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
2715
2715
  const said = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
2716
2716
  /** 「继续」这类续说没有检索词:带上同会话上一轮原话,否则这一轮必然召回为空。 */
2717
- const carried = sameSession && previous?.query && view.isContinuationQuery(said) ? previous.query : '';
2718
- const query = (carried ? `${said} ${carried}` : said).trim().slice(0, RECALL_QUERY_CHARS);
2717
+ const carried = sameSession && previous?.query
2718
+ && (view.isContinuationQuery(said) || view.isEllipticalContinuation(said))
2719
+ ? previous.query : '';
2720
+ let query = (carried ? `${said} ${carried}` : said).trim().slice(0, RECALL_QUERY_CHARS);
2719
2721
  if (win && query) win.query = query;
2720
2722
 
2721
2723
  if (!query) {
@@ -2757,37 +2759,43 @@ export function apply(ctx, config) {
2757
2759
  const remembered = runtime && runtime.enabled
2758
2760
  ? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
2759
2761
  : null;
2760
- let snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
2761
- /**
2762
- * 预防型记忆约束的是「下一步动作」,词面常写在上一轮助手的话和工具调用里,用户这句话反而没提。
2763
- * 第一次检索一张卡都没有时,用最近的实词/工具名补一次有界查询,预算仍是同一份。
2764
- */
2765
2762
  const currentTask = {
2766
2763
  kind: (runtime && runtime.held && runtime.held.task && runtime.held.task.kind) || '',
2767
2764
  episodeId: (runtime && runtime.held && runtime.held.episodeId) || '',
2768
2765
  };
2769
2766
  const rememberedTask = lastTask.get(sessionId) || null;
2770
2767
  const previousTask = previous && previous.task ? previous.task : rememberedTask;
2771
- const sameTask = Boolean(sameSession || rememberedTask) && view.sameRecallTask(previousTask, currentTask);
2768
+ const sameTask = Boolean(sameSession || rememberedTask)
2769
+ && !view.isExplicitSwitch(said, previous && previous.query ? previous.query : '')
2770
+ && view.sameRecallTask(previousTask, currentTask);
2772
2771
  lastTask.set(sessionId, currentTask);
2773
2772
  while (lastTask.size > 64) lastTask.delete(lastTask.keys().next().value);
2774
- if (!snapshot.cards.length && !carried && sameTask) {
2775
- const humanTail = (() => {
2776
- const messages = Array.isArray(decision.messages) ? decision.messages : [];
2777
- for (let i = messages.length - 1; i >= 0; i -= 1) {
2778
- if (messages[i]?.source?.kind === 'user') return messages.slice(0, i);
2779
- }
2780
- return messages;
2781
- })();
2782
- const trail = view.actionTerms(humanTail, { limit: 12 }).slice(0, 240);
2783
- if (trail) {
2784
- const actionQuery = `${trail} ${query}`.trim().slice(0, RECALL_QUERY_CHARS);
2785
- const retry = projectRecall(scope, actionQuery, recalledContext, win, runtime ? runtime.held : null);
2786
- /** 补查是为了给出提醒:补不到就保留第一次的「为什么没提醒」,不拿空结果盖掉。 */
2787
- if (retry.cards.length) {
2788
- snapshot = retry;
2789
- if (win) win.query = actionQuery;
2790
- }
2773
+ /**
2774
+ * 预防型记忆约束的是「下一步动作」,词面常写在上一轮助手的话和工具调用里,用户这句话反而没提。
2775
+ * 续句或没有具体动作的短问句,直接把最近的动作线索并进同一份查询:只查一次,不额外占预算。
2776
+ * 明确换任务时不并,旧任务的动作历史不能给新任务找记忆。
2777
+ */
2778
+ const humanTail = (() => {
2779
+ const messages = Array.isArray(decision.messages) ? decision.messages : [];
2780
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
2781
+ if (messages[i]?.source?.kind === 'user') return messages.slice(0, i);
2782
+ }
2783
+ return messages;
2784
+ })();
2785
+ const trail = sameTask ? view.actionTerms(humanTail, { limit: 12 }).slice(0, 240) : '';
2786
+ const trailInQuery = Boolean(trail) && (Boolean(carried) || !view.hasConcreteAction(said));
2787
+ if (trailInQuery) {
2788
+ query = `${query} ${trail}`.trim().slice(0, RECALL_QUERY_CHARS);
2789
+ if (win) win.query = query;
2790
+ }
2791
+ let snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
2792
+ /** 并了动作线索仍一张卡都没有,才补一次有界查询;补到了才替换,不拿空结果盖掉原判断。 */
2793
+ if (!snapshot.cards.length && !carried && sameTask && trail && !trailInQuery) {
2794
+ const actionQuery = `${trail} ${query}`.trim().slice(0, RECALL_QUERY_CHARS);
2795
+ const retry = projectRecall(scope, actionQuery, recalledContext, win, runtime ? runtime.held : null);
2796
+ if (retry.cards.length) {
2797
+ snapshot = retry;
2798
+ if (win) win.query = actionQuery;
2791
2799
  }
2792
2800
  }
2793
2801
  /** 实验规则最后才取预算:普通卡片先占,弱相关实验不能把强相关项目规则挤出去。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.30",
3
+ "version": "0.18.31",
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",
@@ -344,6 +344,35 @@ test('R22 真正的续改保留原任务范围', () => {
344
344
  assert.equal(next.episode.previousEpisodeId, tasks.sessions['sess-switch'].episodeId);
345
345
  });
346
346
 
347
+ test('R22b 短续句「继续优化收敛」不点名新对象,仍算续作', () => {
348
+ const tasks = activeVideoSession();
349
+ const next = anchor.routeTask(tasks, {
350
+ sessionId: 'sess-switch',
351
+ batchKey: 'rb_ellipse',
352
+ messageIds: ['m2'],
353
+ query: '继续优化收敛',
354
+ registeredSeries: ['口播'],
355
+ now: 2000,
356
+ });
357
+ assert.equal(next.decision.kind, 'continuation');
358
+ assert.equal(next.anchor.seriesId, '口播');
359
+ assert.equal(next.episode.previousEpisodeId, tasks.sessions['sess-switch'].episodeId);
360
+ });
361
+
362
+ test('R22c 短续句点名新对象时仍然换任务', () => {
363
+ const tasks = activeVideoSession();
364
+ const next = anchor.routeTask(tasks, {
365
+ sessionId: 'sess-switch',
366
+ batchKey: 'rb_ellipse_obj',
367
+ messageIds: ['m2'],
368
+ query: '继续优化数据库连接池',
369
+ registeredSeries: ['口播'],
370
+ now: 2000,
371
+ });
372
+ assert.equal(next.decision.kind, 'unbound');
373
+ assert.equal(next.decision.reason, 'new_task');
374
+ });
375
+
347
376
  test('R23 只指代旧稿的局部修改不被当成换任务', () => {
348
377
  const tasks = activeVideoSession();
349
378
  const next = anchor.routeTask(tasks, {
@@ -51,5 +51,18 @@ for (const text of ['继续', '继续。', '然后呢?', '接着', 'go on', 'c
51
51
  for (const text of ['继续优化轻量化记忆与前端展示逻辑', '然后发布', '样式', 'next steps for release']) {
52
52
  assert.equal(view.isContinuationQuery(text), false, text);
53
53
  }
54
+ /* 短续句只由通用动作词组成、没点名新对象:沿用上一句,但不把带对象的长句也算进来。 */
55
+ for (const text of ['继续优化收敛', '继续打磨', '接着改', '继续看看']) {
56
+ assert.equal(view.isEllipticalContinuation(text), true, text);
57
+ }
58
+ for (const text of ['继续优化轻量化记忆与前端展示逻辑', '继续优化数据库连接池', '接着改数据库', '然后发布']) {
59
+ assert.equal(view.isEllipticalContinuation(text), false, text);
60
+ }
61
+
62
+ /* 明确换任务:换做/先别管,或点了一个上一句没有的英文对象。 */
63
+ assert.equal(view.isExplicitSwitch('改用 PostgreSQL 先看连接池', '把前端按钮颜色调一下'), true);
64
+ assert.equal(view.isExplicitSwitch('先别管这个了,帮我看看 nginx 502', '把前端按钮颜色调一下'), true);
65
+ assert.equal(view.isExplicitSwitch('继续优化收敛', '把前端按钮颜色调一下'), false);
66
+ assert.equal(view.isExplicitSwitch('继续优化数据库连接池', '把前端按钮颜色调一下'), false);
54
67
 
55
68
  console.log('lexical.test.mjs: all passed');
@@ -588,6 +588,127 @@ console.log('recall.test.mjs: ok');
588
588
  assert.match(runtime.provider.text({ agent, signal: second }), /改完 client\.js 先重载宿主/, '动作侧线索要能召回预防型记忆');
589
589
  }
590
590
 
591
+ // A25b:短续句「继续优化收敛」没有自己的检索词,要沿用上一句,不能断在上一轮。
592
+ {
593
+ const cwdEllipse = mkdir('ellipse');
594
+ const scopeEllipse = store.scopeOf(cwdEllipse);
595
+ save(scopeEllipse, {
596
+ title: '轻量化记忆与前端展示逻辑先定召回顺序',
597
+ tags: ['轻量化记忆', '前端展示逻辑'],
598
+ summary: '先定召回顺序,再谈展示;触发机制跟着任务走',
599
+ });
600
+ const agent = makeAgent('agent-ellipse', cwdEllipse);
601
+ const runtime = boot();
602
+ const first = signal();
603
+ await runtime.preStep({ agent, signal: first, turn: 'e1' }, enter(human('继续优化轻量化记忆与前端展示逻辑 还有触发机制等')));
604
+ const second = signal();
605
+ await runtime.preStep({ agent, signal: second, turn: 'e2' }, enter(human('继续优化收敛')));
606
+ const prepared = runtime.debug.preparedOf(agent);
607
+ assert.ok(prepared, '短续句要准备召回');
608
+ assert.match(prepared.query, /轻量化记忆与前端展示逻辑/, '短续句沿用上一句原话');
609
+ assert.ok(prepared.cards.some((card) => /轻量化记忆/.test(card.title || card.line || '')), '同一话题的记忆要回来');
610
+ }
611
+
612
+ // A25c:上一轮有动作、这句话只有「这个怎么弄」时,通用卡片不能挡住动作线索的补查。
613
+ {
614
+ const cwdGeneric = mkdir('generic');
615
+ const scopeGeneric = store.scopeOf(cwdGeneric);
616
+ save(scopeGeneric, {
617
+ title: '这个怎么弄时先读现有文档',
618
+ tags: ['怎么弄', '文档'],
619
+ summary: '没头绪时先读现有文档再动手',
620
+ });
621
+ save(scopeGeneric, {
622
+ title: '改完 client.js 先重载宿主',
623
+ tags: ['client.js', '重载'],
624
+ summary: 'client.js 一改就要重载宿主,普通刷新覆盖不到',
625
+ });
626
+ const agent = makeAgent('agent-generic', cwdGeneric);
627
+ const runtime = boot();
628
+ const first = signal();
629
+ await runtime.preStep({ agent, signal: first, turn: 'g1' }, enter(human('这个怎么弄')));
630
+ assert.match(runtime.provider.text({ agent, signal: first }), /先读现有文档/, '第一轮命中通用卡片');
631
+ const assistantMessage = {
632
+ role: 'assistant',
633
+ id: 'asst-generic-1',
634
+ content: [
635
+ { type: 'text', text: '我把改动写进文件了。' },
636
+ { type: 'tool-call', name: 'write', input: { file_path: '/tmp/generic/client.js' } },
637
+ ],
638
+ };
639
+ const second = signal();
640
+ await runtime.preStep({ agent, signal: second, turn: 'g2' }, enter(assistantMessage, human('这个怎么弄')));
641
+ assert.match(runtime.provider.text({ agent, signal: second }), /改完 client\.js 先重载宿主/, '通用卡片不能挡住动作线索');
642
+ }
643
+
644
+ // A25d:换到数据库任务后再说「继续」,只承接数据库这一句,不能把前端任务的记忆带回来。
645
+ {
646
+ const cwdSwitch = mkdir('switch-cont');
647
+ const scopeSwitch = store.scopeOf(cwdSwitch);
648
+ save(scopeSwitch, {
649
+ title: '前端按钮颜色先对齐设计稿 token',
650
+ tags: ['前端按钮', '颜色'],
651
+ summary: '按钮颜色只取设计稿 token,不要另写十六进制',
652
+ });
653
+ save(scopeSwitch, {
654
+ title: 'PostgreSQL 连接池先查 max_connections',
655
+ tags: ['PostgreSQL', '连接池'],
656
+ summary: '连接池调大前先看 max_connections 和实际占用',
657
+ });
658
+ const agent = makeAgent('agent-switch-cont', cwdSwitch);
659
+ const runtime = boot();
660
+ const first = signal();
661
+ await runtime.preStep({ agent, signal: first, turn: 'sc1' }, enter(human('前端按钮颜色怎么改')));
662
+ assert.match(runtime.provider.text({ agent, signal: first }), /前端按钮颜色/, '第一轮命中前端任务');
663
+ const second = signal();
664
+ await runtime.preStep({ agent, signal: second, turn: 'sc2' }, enter(human('改用 PostgreSQL,先看连接池')));
665
+ assert.match(runtime.provider.text({ agent, signal: second }), /PostgreSQL 连接池/, '第二轮命中数据库任务');
666
+ const third = signal();
667
+ await runtime.preStep({ agent, signal: third, turn: 'sc3' }, enter(human('继续')));
668
+ const prepared = runtime.debug.preparedOf(agent);
669
+ assert.match(prepared.query, /PostgreSQL/, '续句承接的是数据库那一句');
670
+ assert.ok(prepared.cards.some((card) => /PostgreSQL/.test(card.title || card.line || '')), '续句仍命中数据库记忆');
671
+ assert.ok(!prepared.cards.some((card) => /前端按钮/.test(card.title || card.line || '')), '不把前端任务记忆带回来');
672
+ }
673
+
674
+ // A25e:认不出任务类型的明确换任务,也不能把上一轮的动作线索带过来。
675
+ {
676
+ const cwdUnknown = mkdir('unknown-switch');
677
+ const scopeUnknown = store.scopeOf(cwdUnknown);
678
+ save(scopeUnknown, {
679
+ title: '改完 client.js 先重载宿主',
680
+ tags: ['client.js', '重载'],
681
+ summary: 'client.js 改动后必须重载宿主,普通刷新覆盖不到',
682
+ });
683
+ save(scopeUnknown, {
684
+ title: 'nginx 502 先看 upstream 日志',
685
+ tags: ['nginx', '502', 'upstream'],
686
+ summary: '502 先查 upstream 是否存活,再看超时配置',
687
+ });
688
+ const agent = makeAgent('agent-unknown', cwdUnknown);
689
+ const runtime = boot();
690
+ const first = signal();
691
+ await runtime.preStep({ agent, signal: first, turn: 'u1' }, enter(human('把前端按钮颜色调一下')));
692
+ const assistantMessage = {
693
+ role: 'assistant',
694
+ id: 'asst-unknown-1',
695
+ content: [
696
+ { type: 'text', text: '我把 client.js 的样式改好了。' },
697
+ { type: 'tool-call', name: 'write', input: { file_path: '/tmp/unknown/client.js' } },
698
+ ],
699
+ };
700
+ const second = signal();
701
+ await runtime.preStep({ agent, signal: second, turn: 'u2' }, enter(assistantMessage, human('先别管这个了,帮我看看 nginx 502')));
702
+ const switchedText = runtime.provider.text({ agent, signal: second });
703
+ assert.doesNotMatch(switchedText, /改完 client\.js 先重载宿主/, '明确换任务后不带旧动作线索');
704
+ assert.match(switchedText, /nginx 502/, '新任务本身能命中');
705
+ const third = signal();
706
+ await runtime.preStep({ agent, signal: third, turn: 'u3' }, enter(human('继续')));
707
+ const continuedText = runtime.provider.text({ agent, signal: third });
708
+ assert.match(continuedText, /nginx 502/, '续句承接新任务');
709
+ assert.doesNotMatch(continuedText, /改完 client\.js 先重载宿主/, '续句不回旧任务');
710
+ }
711
+
591
712
  // A26:明确换任务后,不能拿上一轮助手的话去补查、把旧任务记忆塞进新任务。
592
713
  {
593
714
  const cwdSwitch = mkdir('switch');
package/view.js CHANGED
@@ -710,9 +710,44 @@ export function isContinuationQuery(text) {
710
710
  return /^(继续|接着|然后|然后呢|还有|还有呢|往下|说下去|goon|continue|next|more)$/.test(raw);
711
711
  }
712
712
 
713
+ /**
714
+ * 「继续优化收敛」这类短续句:句首是续说标记,剩下的只由通用动作词组成,
715
+ * 没有点名新的对象。它本身检索不出上一轮的记忆,但仍属于当前任务,要沿用上一句原话。
716
+ * 明确带对象(「继续优化数据库连接池」「继续优化轻量化记忆」)不算,交给换任务判断。
717
+ */
718
+ const ELLIPTICAL_BODY = /^(?:把|将|再|接着|继续)?(?:优化|收敛|调整|完善|打磨|推进|处理|改|修|做|写|整理|检查|排查|测试|发布|跑|看看|看下|弄|试|收尾|统一|对齐|细化|补|过一遍|走一遍|一下|下)+[。.!!??…]*$/u;
719
+ export function isEllipticalContinuation(text) {
720
+ const raw = plain(text, 40).trim();
721
+ const cut = raw.replace(/^[\s\u3002\uff01\uff1f?!\uff0c,\u3001\u2026~\uff5e]+/u, '');
722
+ if (!cut || [...cut].length > 12) return false;
723
+ const match = cut.match(/^(?:继续|接着|再接着|往下)[,,、\s]*(.+)$/u);
724
+ if (!match) return false;
725
+ return ELLIPTICAL_BODY.test(match[1].trim());
726
+ }
727
+
728
+ /** 这句话有没有具体动作:只用于判断要不要把上一轮的工具线索并进这次查询。 */
729
+ const CONCRETE_ACTION = /(改|写|修|做|建|删|新增|添加|移除|运行|跑|测试|发布|部署|提交|推送|安装|重载|重启|启动|停止|检查|排查|优化|收敛|处理|调整|配置|设置|替换|更换|上传|下载|生成|保存|读取|打开|关闭|合并|拆分|回滚|迁移|接入|修复|定位|复现)/u;
730
+ export function hasConcreteAction(text) {
731
+ return CONCRETE_ACTION.test(plain(text, 400));
732
+ }
733
+
734
+ /**
735
+ * 明确换任务:句子里有换做/改用/先别管这类词,或点了一个上一句没有的英文对象。
736
+ * 认不出任务类型时,上一轮的动作线索只能靠这条规则隔开,不能因为类型为空就默认同任务。
737
+ */
738
+ const SWITCH_MARK = /(换成|换个|换一个|改用|改做|改为|改到|转到|先不管|别管|先别|不弄这个|不看这个|另一个|新任务|去做|开始做)/u;
739
+ export function isExplicitSwitch(text, previousText = '') {
740
+ const current = plain(text, 400);
741
+ if (!current) return false;
742
+ if (SWITCH_MARK.test(current)) return true;
743
+ const previous = plain(previousText, 400).toLowerCase();
744
+ const objects = current.match(/[A-Za-z][A-Za-z0-9_.-]{1,}|[0-9]{2,}/g) || [];
745
+ return objects.some((token) => !previous.includes(token.toLowerCase()));
746
+ }
747
+
713
748
  /**
714
749
  * 补查只有在「还是同一个任务」时才成立:任务类型或 episode 变了,上一轮的动作历史属于旧任务,
715
- * 不能拿它给新任务找记忆。缺任一侧身份都按不同任务处理,宁可少补一次也不串旧话题。
750
+ * 不能拿它给新任务找记忆。类型缺失时保留补查,由调用方的明确换任务判断兜底。
716
751
  */
717
752
  export function sameRecallTask(previous, current) {
718
753
  if (!previous || !current) return true;