dsh-lite-memory 0.15.0 → 0.16.2
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/README.md +4 -4
- package/anchor.js +55 -13
- package/client.js +8 -6
- package/docs/RELEASE-0.16.0.md +10 -0
- package/index.js +138 -37
- package/package.json +1 -1
- package/state.js +7 -5
- package/store.js +60 -34
- package/test/anchor.test.mjs +90 -0
- package/test/control.test.mjs +19 -0
- package/test/evolution-read.test.mjs +133 -3
- package/test/recall.test.mjs +14 -0
- package/test/store.test.mjs +87 -19
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Lightweight memory for the DeepSeek Harness. No standing system prompt, nothing
|
|
|
23
23
|
|
|
24
24
|
写入时分两层:默认只在这个项目生效;用户说「以后所有项目都这样」才进全局层(200 条起提示,硬上限 1000 条),检索时一起查。同一主题在不同前提下用 `when` 区分,各存一条。
|
|
25
25
|
|
|
26
|
-
写入前先蒸馏:标题是结论,正文只留原则、判断规则和触发条件,不把具体尺寸、字号、层级、颜色或一次性数字记进来;需要这些值时按记忆里的指针查当前代码或文档。单条正文超过
|
|
26
|
+
写入前先蒸馏:标题是结论,正文只留原则、判断规则和触发条件,不把具体尺寸、字号、层级、颜色或一次性数字记进来;需要这些值时按记忆里的指针查当前代码或文档。单条正文超过 4096 字节会被拒绝,先删细节再存;摘要超长自动截到 60 字。
|
|
27
27
|
|
|
28
28
|
触发词:`记住`、`记一下`、`上次`、`规范`、`约定`、`路径`、`凭据怎么取`。
|
|
29
29
|
|
|
@@ -39,7 +39,7 @@ Those lines belong to the turn that made them: each turn is rebuilt from its own
|
|
|
39
39
|
|
|
40
40
|
Writes land in one of two layers: the current project by default, or the global layer (warns past 200, hard ceiling 1000) when the user says the rule holds for every project. Both layers are searched together, and a `when` field separates the same subject under different preconditions.
|
|
41
41
|
|
|
42
|
-
Before writing, distill: the title is the conclusion and the body keeps only the principle, decision rule and trigger condition. Concrete dimensions, font sizes, z-index, colors and one-off numbers stay out; the memory points at where to read the current value instead. A body over
|
|
42
|
+
Before writing, distill: the title is the conclusion and the body keeps only the principle, decision rule and trigger condition. Concrete dimensions, font sizes, z-index, colors and one-off numbers stay out; the memory points at where to read the current value instead. A body over 4096 bytes is refused, so drop detail before saving; an over-long summary is truncated to 60 characters.
|
|
43
43
|
|
|
44
44
|
## 点一下就存 / One click, no turn
|
|
45
45
|
|
|
@@ -80,13 +80,13 @@ On the Memories tab, the search row also switches between list and graph; either
|
|
|
80
80
|
- **0 依赖**,存储层无定时器、无监听、无网络,只有同步单文件写;写入用一把有超时的锁,别的进程正在写就明确报「忙」,不抢别人的锁。
|
|
81
81
|
- 搜索最多 4 条、400 字;正文只在 `memory_read` 里出现,全文永不进上下文。
|
|
82
82
|
- 普通检索只列当前有效的记录;已经归档或被更新替换的旧说法不混进来,要查历史得在 `memory_search` 里显式要 `history`。
|
|
83
|
-
- 上限:单条正文
|
|
83
|
+
- 上限:单条正文 4096 字节、整库正文 1 MB、每层 1000 条、标题与摘要各 60 字;每层 200 条只当提示线。90 天未用只留 `id + 标题`。到了硬上限不拒绝新记忆:把同层最久没被读的一条移进归档,用户固定过的规则不动,保存结果里写明是哪条(正文与完整记录都留着,可人工取回),单条本身超预算才报错;用户明确说「别记」也进归档。文件从不删除。
|
|
84
84
|
- 一个项目一个目录,项目之间互相看不见。
|
|
85
85
|
|
|
86
86
|
- **Zero dependencies.** The store has no timers, no listeners, no network — just synchronous single-file writes, guarded by a lock that gives up after 500 ms instead of stealing it.
|
|
87
87
|
- Search returns at most 4 hits and 400 characters. Full text only appears in `memory_read`.
|
|
88
88
|
- Normal search lists current entries only; archived notes and superseded wording stay out unless `memory_search` asks for `history`.
|
|
89
|
-
- Limits:
|
|
89
|
+
- Limits: 4096 bytes per body, 1 MB of bodies per store, 1000 entries per layer, 60 characters per title or summary; 200 entries per layer is just a warning line. At the hard ceiling a save is never refused: the least recently read entry in the same layer moves to archive (pinned rules are never evicted) and the result names it (body and full record kept, restorable by hand); only an entry that alone exceeds the budget fails. Saying 「别记」 also archives. Files are never deleted.
|
|
90
90
|
- One directory per project, so projects never see each other's notes.
|
|
91
91
|
|
|
92
92
|
## 项目自进化〔实验〕 / Project self-evolution (experimental)
|
package/anchor.js
CHANGED
|
@@ -16,12 +16,22 @@ const REFERENCE_WORDS = /(这个|这条|这次|上次|之前|刚才|上面|前
|
|
|
16
16
|
const TASK_STOP = new Set(['一个','一下','这个','那个','我们','你们','他们','什么','怎么','可以','需要','帮我','帮忙','请问','然后','现在','还是','就是','没有','不是','以及','关于','进行','使用','问题','任务','内容','时候','已经','如果','因为','所以','但是','而且','或者','这些','那些','这样','那样','一下','一点','一些']);
|
|
17
17
|
/** 判断任务是否换了对象时,这些词只表示动作或指代,不算对象证据。 */
|
|
18
18
|
const TASK_NOISE = /(继续|再做一期|下一期|再来一期|再开一期|续一集|再拍一期|接着|然后|接下来|现在|帮我|帮忙|这个|这条|这次|上次|之前|刚才|上面|前面|下面|后面|这里|还是|同样|再改|排查|处理|检查|看看|看一下|看下|修复|解决|优化|调整|修改|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|定位|复现|查一下|找一下|问题|任务|内容|一下)/g;
|
|
19
|
-
const NEW_OBJECT_VERB = /(排查|检查|看看|看一下|看下|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|修复|解决|处理|定位|复现|查一下|找一下)/;
|
|
20
|
-
const NEW_TASK_MARKER = /(现在|接下来|然后|下面|新任务|另一个|重新|改做|换个|换一个)/;
|
|
21
19
|
/** 续接词后面还得跟一个动作,才算真正在改旧稿;单说「继续 / 这个 / 之前」不构成复用依据。 */
|
|
22
20
|
const CONTINUATION_ACTION = /(继续|接着|然后|再|还是|同样)\s*(做|改|调|弄|看|写|处理|优化|调整|检查|完成)/;
|
|
21
|
+
/** 明确指向刚才那一版作品的说法,是沿用旧范围的正面证据;「这版/那版」只是指示,不算旧稿。 */
|
|
22
|
+
const OLD_WORK_REFERENCE = /(刚才那版|之前那版|上一版|原稿|旧稿)/;
|
|
23
|
+
/** 旧稿被放下、否定或只作为话题提起时,不能因为它出现就恢复旧范围。 */
|
|
24
|
+
const OLD_WORK_PAUSE = /(先放着|放着|放一边|先不|先别|别管|不用管|搁置|暂停|不做了|先不做|不用了)/;
|
|
25
|
+
const OLD_WORK_ACTION = /(继续|接着|再改|改成|改一下|调整|优化|改改|开头|结尾|结构|节奏|语气|口吻|文案|措辞|细节|版本)/;
|
|
26
|
+
/** 创作类与工程类任务的默认判断不同;同一主题从写口播切到排查代码,不能沿用旧栏目。 */
|
|
27
|
+
const CONTENT_INTENT = /(口播|拍摄|录制|剪辑|文案|封面|字幕|直播|视频|选题|脚本|一期|栏目)/;
|
|
28
|
+
const ENGINEERING_INTENT = /(代码|排查|故障|部署|调试|重构|接口|数据库|查询|报错|日志|服务|环境|依赖|测试|配置|修复)/;
|
|
29
|
+
/** 这些词描述的是旧稿的某个方面,不是新对象;不该因为它们出现就撤销旧范围。 */
|
|
30
|
+
const ASPECT_NOISE = /(节奏|速度|风格|语气|口吻|结构|长度|时长|篇幅|配色|字号|字体|标题|文案|措辞|逻辑|细节|开头|结尾|过渡|画面|镜头|音乐|音效|字幕|参数|格式|排版)/g;
|
|
31
|
+
/** 程度、指代和虚词组成的二字片段不是对象证据。 */
|
|
32
|
+
const GENERIC_CHARS = /[再更稍很太挺点些快慢多少好坏大小高低长短早晚新旧一二三四五六七八九十百千万个把的得地了着过吗呢吧啊]/;
|
|
23
33
|
function taskTerms(value) {
|
|
24
|
-
const plain = text(value, 200).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(TASK_NOISE, ' ').replace(/\s+/g, ' ').trim();
|
|
34
|
+
const plain = text(value, 200).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(ASPECT_NOISE, ' ').replace(TASK_NOISE, ' ').replace(/\s+/g, ' ').trim();
|
|
25
35
|
const terms = new Set();
|
|
26
36
|
for (const word of plain.match(/[a-z][a-z0-9_-]{2,}/g) || []) terms.add(word);
|
|
27
37
|
const runs = plain.match(/[\u4e00-\u9fff]+/g) || [];
|
|
@@ -29,7 +39,8 @@ function taskTerms(value) {
|
|
|
29
39
|
if (TASK_STOP.has(run)) continue;
|
|
30
40
|
for (let i = 0; i + 2 <= run.length; i += 1) {
|
|
31
41
|
const gram = run.slice(i, i + 2);
|
|
32
|
-
if (
|
|
42
|
+
if (TASK_STOP.has(gram) || GENERIC_CHARS.test(gram)) continue;
|
|
43
|
+
terms.add(gram);
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
return terms;
|
|
@@ -37,19 +48,28 @@ function taskTerms(value) {
|
|
|
37
48
|
/** 继承旧任务要有正面证据:新要求与本轮任务没有实质对象交集时,先按新任务处理。 */
|
|
38
49
|
export function looksLikeNewTask(query, previousText) {
|
|
39
50
|
const current = text(query, 200);
|
|
40
|
-
if (!current || isThanks(current)
|
|
51
|
+
if (!current || isThanks(current)) return false;
|
|
41
52
|
const next = taskTerms(current);
|
|
42
53
|
const previous = taskTerms(previousText);
|
|
43
54
|
const cue = isContinuation(current) || REFERENCE_WORDS.test(current);
|
|
44
55
|
/** 只剩指代或续接词时,必须另有动作才算续接;否则宁可换任务也不沿用旧范围。 */
|
|
45
56
|
if (!next.size) return cue ? !CONTINUATION_ACTION.test(current) : false;
|
|
46
57
|
if (!previous.size) return false;
|
|
58
|
+
/** 同一主题换了工作类型(口播 → 排查代码)是新任务,不靠对象词重叠判续作。 */
|
|
59
|
+
const nextKind = CONTENT_INTENT.test(current) ? (ENGINEERING_INTENT.test(current) ? '' : 'content')
|
|
60
|
+
: (ENGINEERING_INTENT.test(current) ? 'engineering' : '');
|
|
61
|
+
const previousKind = CONTENT_INTENT.test(previousText) ? (ENGINEERING_INTENT.test(previousText) ? '' : 'content')
|
|
62
|
+
: (ENGINEERING_INTENT.test(previousText) ? 'engineering' : '');
|
|
63
|
+
if (nextKind && previousKind && nextKind !== previousKind) return true;
|
|
47
64
|
for (const term of next) if (previous.has(term)) return false;
|
|
48
|
-
/**
|
|
49
|
-
if (!
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
/** 指向旧稿还要有继续改它的动作;「旧稿先放着」「这版数据库配置怎么改」都算新任务。 */
|
|
66
|
+
if (OLD_WORK_REFERENCE.test(current) && !OLD_WORK_PAUSE.test(current) && OLD_WORK_ACTION.test(current)) return false;
|
|
67
|
+
/**
|
|
68
|
+
* 有实质对象词却跟旧任务没有任何交集:这就是新任务,不因为句首带了
|
|
69
|
+
* 「继续」「这个」就把旧范围连同正文一起搬过来。继承只认对象交集、
|
|
70
|
+
* 明确的旧稿指代,或者除动作/指代外没有新对象(上面两条)。
|
|
71
|
+
*/
|
|
72
|
+
return true;
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
export function emptyTasks() {
|
|
@@ -76,6 +96,27 @@ export function isRevision(query) {
|
|
|
76
96
|
return REVISE_WORDS.test(text(query, 160));
|
|
77
97
|
}
|
|
78
98
|
|
|
99
|
+
const NEGATED_SERIES = /(不要|不用|别|不按|不照|不是|并非|先不|不做|不搞|忽略|跳过|不管|别提)[^,。;!?\n]{0,10}$/;
|
|
100
|
+
const MENTION_SERIES = /(日志|报错|错误|信息|提到|说到|包含|关键词|这个词|源码|代码|插件|文档|规则名|栏目名|分析)[^,。;!?\n]{0,10}$/;
|
|
101
|
+
const PAUSED_SERIES = /^(先放着|先放|放一边|先不做|不做了|不用了|暂停)/;
|
|
102
|
+
const PAUSED_BEFORE = /(先放着|先放|放一边|先不做|不做了|不用了|暂停)[^,。;!?\n]{0,4}$/;
|
|
103
|
+
/** 提到栏目名不等于选中它:否定、引用、看源码或「先放着」都只是话题,不能建绑定。 */
|
|
104
|
+
export function seriesChosenBy(query, series) {
|
|
105
|
+
const value = text(query, 400);
|
|
106
|
+
const name = text(series, 40);
|
|
107
|
+
if (!value || !name) return false;
|
|
108
|
+
let from = 0;
|
|
109
|
+
while (from <= value.length) {
|
|
110
|
+
const at = value.indexOf(name, from);
|
|
111
|
+
if (at < 0) return false;
|
|
112
|
+
const before = value.slice(Math.max(0, at - 14), at);
|
|
113
|
+
const after = value.slice(at + name.length);
|
|
114
|
+
if (!NEGATED_SERIES.test(before) && !MENTION_SERIES.test(before) && !PAUSED_BEFORE.test(before) && !PAUSED_SERIES.test(after)) return true;
|
|
115
|
+
from = at + name.length;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
export function requestBatchKey(messageIds) {
|
|
80
121
|
const ids = [...new Set((Array.isArray(messageIds) ? messageIds : []).map((id) => String(id || '')).filter(Boolean))].sort();
|
|
81
122
|
if (!ids.length) return '';
|
|
@@ -271,7 +312,7 @@ export function routeTask(tasksInput, input = {}) {
|
|
|
271
312
|
}
|
|
272
313
|
const registered = (Array.isArray(input.registeredSeries) ? input.registeredSeries : []).map((item) => text(item, 40)).filter(Boolean);
|
|
273
314
|
/** 用户在这句话里点名的栏目优先于旧栏目和项目默认值。 */
|
|
274
|
-
const namedSeries = registered.find((series) =>
|
|
315
|
+
const namedSeries = registered.find((series) => seriesChosenBy(query, series)) || '';
|
|
275
316
|
const changedSeries = current && namedSeries && namedSeries !== (current.seriesId || '') ? namedSeries : '';
|
|
276
317
|
const fallbackKind = (currentEpisode && currentEpisode.taskKind)
|
|
277
318
|
|| (input.defaults && input.defaults.taskKind) || '';
|
|
@@ -311,9 +352,10 @@ export function routeTask(tasksInput, input = {}) {
|
|
|
311
352
|
return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'none', reason: 'thanks', pointer: true }, changed: false };
|
|
312
353
|
}
|
|
313
354
|
|
|
314
|
-
/** 先认新对象,再认续作:带「继续 / 这个」但换了对象,也必须撤销旧范围。
|
|
355
|
+
/** 先认新对象,再认续作:带「继续 / 这个」但换了对象,也必须撤销旧范围。
|
|
356
|
+
* 本句点名了已登记栏目(「再做一期口播」)就是明确的对象证据,不算换任务。 */
|
|
315
357
|
if (current && currentEpisode && currentEpisode.status !== 'unbound'
|
|
316
|
-
&& !SWITCH_WORDS.test(query) && !changedSeries
|
|
358
|
+
&& !SWITCH_WORDS.test(query) && !changedSeries && !namedSeries
|
|
317
359
|
&& looksLikeNewTask(query, currentEpisode.taskText || '')) {
|
|
318
360
|
const transitionId = transitionIdFor(batchKey, tasks.revision);
|
|
319
361
|
const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
|
package/client.js
CHANGED
|
@@ -852,7 +852,7 @@ window.__ModuleLoader__.load({
|
|
|
852
852
|
const unresolved = rule.applicability === 'unresolved'
|
|
853
853
|
|| (rule.applicability === 'conditional' && (!Array.isArray(rule.conditions) || rule.conditions.length === 0));
|
|
854
854
|
if (unresolved) return '适用范围待确认,暂不参与回答。';
|
|
855
|
-
if (rule.state === 'pending') return '
|
|
855
|
+
if (rule.state === 'pending') return '还没生效,采纳后才参与回答。';
|
|
856
856
|
if (rule.state === 'trial') return '在条件匹配的任务中参与参考,到期后暂停。';
|
|
857
857
|
if (rule.state === 'active') return '在适用任务中作为默认参考,当前明确要求优先。';
|
|
858
858
|
return '';
|
|
@@ -865,7 +865,7 @@ window.__ModuleLoader__.load({
|
|
|
865
865
|
if (conditions.length === 0) return '待确认';
|
|
866
866
|
return conditions.map((item) => `${CONDITION_LABEL[item.key] || item.key} ${item.value}`).join('、');
|
|
867
867
|
}
|
|
868
|
-
return '
|
|
868
|
+
return '本项目内';
|
|
869
869
|
};
|
|
870
870
|
|
|
871
871
|
const evidenceTextOf = (row) => {
|
|
@@ -1267,7 +1267,7 @@ window.__ModuleLoader__.load({
|
|
|
1267
1267
|
h(Toggle, { ariaLabel: '自进化', on: Boolean(memoryOn && evolution && evolution.enabled === true), busy: busy || !evolution || !canToggle || !memoryOn, onClick: canToggle && memoryOn ? toggleEvolution : undefined }),
|
|
1268
1268
|
),
|
|
1269
1269
|
h('div', { style: PANEL_MUTED }, '从反馈中整理经验,按需使用'), // ui-copy-ignore 用户指定文案
|
|
1270
|
-
h('div', { style: PANEL_MUTED }, '
|
|
1270
|
+
h('div', { style: PANEL_MUTED }, '实验功能,会增加 token 消耗'), // ui-copy-ignore 用户指定文案
|
|
1271
1271
|
);
|
|
1272
1272
|
const switchBar = h('div', { style: { display: 'flex', alignItems: 'stretch', gap: '10px', flexWrap: 'wrap', padding: '12px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' } },
|
|
1273
1273
|
globalView ? h('div', { style: { ...PANEL_MUTED, padding: '2px 0' } }, '全局记忆只读') : memorySwitch,
|
|
@@ -1528,14 +1528,16 @@ window.__ModuleLoader__.load({
|
|
|
1528
1528
|
const groups = React.useMemo(() => {
|
|
1529
1529
|
const list = Array.isArray(refs) ? refs : EMPTY;
|
|
1530
1530
|
return {
|
|
1531
|
-
|
|
1531
|
+
found: list.filter((item) => item.exposure === 'reference' && item.kind === 'memory-search'),
|
|
1532
|
+
provided: list.filter((item) => item.exposure === 'reference' && item.kind === 'memory-read'),
|
|
1532
1533
|
update: list.filter((item) => item.exposure === 'update'),
|
|
1533
1534
|
};
|
|
1534
1535
|
}, [refs]);
|
|
1535
1536
|
if (!sessionId && refs.length === 0) return null;
|
|
1536
|
-
if (groups.
|
|
1537
|
+
if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) return null;
|
|
1537
1538
|
return h('div', { style: TAIL },
|
|
1538
|
-
h(RefSection, { label: '
|
|
1539
|
+
h(RefSection, { label: '检索发现', items: groups.found, sessionId, turnKey: turn }),
|
|
1540
|
+
h(RefSection, { label: '已提供正文', items: groups.provided, sessionId, turnKey: turn }),
|
|
1539
1541
|
h(RefSection, { label: '本轮更新', items: groups.update, sessionId, turnKey: turn }),
|
|
1540
1542
|
);
|
|
1541
1543
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 0.16.0
|
|
2
|
+
|
|
3
|
+
- 任务切换不再把上一条任务的规则带进新任务:带明确对象的新要求按新任务处理,只有「继续改」这类没有对象的短句才继承。
|
|
4
|
+
- 自进化关掉或换代后,已经准备好的实验正文和 `contextId` 立即失效;重新打开也不复活旧正文。
|
|
5
|
+
- 试用到期的学习结果不再自动注入,检索、读取、自动注入共用同一档可用性判断。
|
|
6
|
+
- 检索先过滤不可用条目再截断,不会被待确认的候选挤掉真正的命中;只有待确认候选时明确返回「适用条件还没确认」。
|
|
7
|
+
- 写入口径放宽:正文上限 4096 字节,整库 1 MB;摘要超长自动截断;不再因为正文提到像素、字号或写成指令句而拒绝保存。
|
|
8
|
+
- 只改标题的学习规则保持 `active` 和已有证据,不再降成试用。
|
|
9
|
+
- 容量腾挪不归档用户固定过的规则;实验规则与待确认候选的额度不再重复计数。
|
|
10
|
+
- 面板:作用范围改成「本项目内」,候选状态与引用分组的文案对齐实际行为,自进化开关标明会增加消耗。
|
package/index.js
CHANGED
|
@@ -635,8 +635,8 @@ const SAVE_PARAMS = {
|
|
|
635
635
|
properties: {
|
|
636
636
|
id: { type: 'string', description: '要更新的记忆 id(来自搜索结果或相近提示)。同一件事更新时用它,别另建一条' },
|
|
637
637
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
638
|
-
summary: { type: 'string', description:
|
|
639
|
-
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes}
|
|
638
|
+
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语最好,超长会自动截到这个长度` },
|
|
639
|
+
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes} 字节。只写原则、判断规则、触发条件和边界;不写一次性数字或代码片段,这些查当前代码或文档。` },
|
|
640
640
|
when: { type: 'string', description: `前置条件或适用场景(「在哪台机器 / 哪个环境 / 什么情况下」),不超过 ${LIMITS.whenChars} 字。同一标题但前置条件不同,会存成两条,不合并;更新时不给就保留原值` },
|
|
641
641
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新时不给就保留原值' },
|
|
642
642
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
@@ -1051,6 +1051,14 @@ export function apply(ctx, config) {
|
|
|
1051
1051
|
return text;
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
+
/** 实验正文按条目记有效期:一条到期只撤一条,不把同请求里其他规则一起清掉。 */
|
|
1055
|
+
function experimentTextFrom(parts, now = Date.now()) {
|
|
1056
|
+
if (!parts || !Array.isArray(parts.items)) return '';
|
|
1057
|
+
const items = parts.items.filter((item) => !item.validUntil || now <= Date.parse(String(item.validUntil)));
|
|
1058
|
+
if (!items.length) return '';
|
|
1059
|
+
return `${parts.header}\n${items.map((item) => item.line).join('\n')}${parts.tail || ''}`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1054
1062
|
/** 本轮问题与栏目名的字面相关度:只展示有交集的栏目,交集为空就整段跳过。 */
|
|
1055
1063
|
function relevantRanges(names, query) {
|
|
1056
1064
|
const text = String(query || '').toLowerCase();
|
|
@@ -1070,8 +1078,19 @@ export function apply(ctx, config) {
|
|
|
1070
1078
|
});
|
|
1071
1079
|
}
|
|
1072
1080
|
|
|
1081
|
+
/** 实验正文只在「自进化开着,且还是同一代」时可用;关掉再开不能复活旧正文。 */
|
|
1082
|
+
function experimentStillCurrent(scope, held) {
|
|
1083
|
+
let evolution;
|
|
1084
|
+
try { evolution = evolutionStatus(scope); } catch { return false; }
|
|
1085
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false) return false;
|
|
1086
|
+
if (held && held.epoch && String(evolution.epoch || '') !== String(held.epoch)) return false;
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1073
1090
|
/** 读决策 → 一段短提示:未绑定只给栏目索引,已绑定才给规则正文。 */
|
|
1074
1091
|
function renderExperiment(scope, decision, context, held, win) {
|
|
1092
|
+
if (held && typeof held === 'object') { held.validUntil = ''; held.experimentParts = null; held.injectedIds = []; }
|
|
1093
|
+
if (!experimentStillCurrent(scope, held)) return '';
|
|
1075
1094
|
if (!decision || decision.kind === 'none') return '';
|
|
1076
1095
|
if (decision.kind === 'conflict') {
|
|
1077
1096
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
@@ -1095,6 +1114,8 @@ export function apply(ctx, config) {
|
|
|
1095
1114
|
if (record.reviewState === 'legacy_unreviewed') continue;
|
|
1096
1115
|
const learning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1097
1116
|
if (learning.state !== 'active' && learning.state !== 'trial') continue;
|
|
1117
|
+
/** 试用到期、暂停或身份失效:自动注入与检索、读取用同一档可用性,不能各判各的。 */
|
|
1118
|
+
if (!view.experimentalUsable(record, Date.now())) continue;
|
|
1098
1119
|
const status = view.statusOf(record, context);
|
|
1099
1120
|
if (status === 'needs_context') { unknown.push(record); continue; }
|
|
1100
1121
|
if (status !== 'ok') continue;
|
|
@@ -1106,8 +1127,7 @@ export function apply(ctx, config) {
|
|
|
1106
1127
|
if (conflict) {
|
|
1107
1128
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
1108
1129
|
}
|
|
1109
|
-
const
|
|
1110
|
-
const shown = [];
|
|
1130
|
+
const parts = { header: EXPERIMENT_RULES_HEADER, items: [], tail: held && held.contextId ? `\ncontextId=${held.contextId}` : '' };
|
|
1111
1131
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
1112
1132
|
for (const record of matched.slice(0, 3)) {
|
|
1113
1133
|
const body = String(record.body || '');
|
|
@@ -1123,17 +1143,24 @@ export function apply(ctx, config) {
|
|
|
1123
1143
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1124
1144
|
if (used + size > EXPERIMENT_BYTES) break;
|
|
1125
1145
|
used += size;
|
|
1126
|
-
|
|
1127
|
-
|
|
1146
|
+
const trialLearning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1147
|
+
const trialUntil = trialLearning.state === 'trial' && Number.isFinite(Date.parse(String(trialLearning.trialUntil || '')))
|
|
1148
|
+
? String(trialLearning.trialUntil) : '';
|
|
1149
|
+
parts.items.push({ id: record.id, line, validUntil: trialUntil });
|
|
1150
|
+
}
|
|
1151
|
+
if (held) held.injectedIds = parts.items.map((item) => item.id);
|
|
1152
|
+
if (!parts.items.length && unknown.length) {
|
|
1153
|
+
/** 条件没确认的条目只报数量,绝不在模型侧暴露标题。 */
|
|
1154
|
+
return chargeExperiment(win, '有些实验规则的适用条件还没确认,先按用户这次说的做,别把它们当本轮依据。');
|
|
1128
1155
|
}
|
|
1129
|
-
if (
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1156
|
+
if (!parts.items.length) return '';
|
|
1157
|
+
if (held) held.experimentParts = parts;
|
|
1158
|
+
/** 兼容旧读取点:整体最早到期只取本轮真正注入的条目,不再被无关试验拖下水。 */
|
|
1159
|
+
if (held) {
|
|
1160
|
+
const deadlines = parts.items.map((item) => Date.parse(String(item.validUntil))).filter(Number.isFinite);
|
|
1161
|
+
if (deadlines.length) held.validUntil = new Date(Math.min(...deadlines)).toISOString();
|
|
1133
1162
|
}
|
|
1134
|
-
|
|
1135
|
-
const tail = held && held.contextId ? `\ncontextId=${held.contextId}` : '';
|
|
1136
|
-
return chargeExperiment(win, `${EXPERIMENT_RULES_HEADER}\n${lines.join('\n')}${tail}`);
|
|
1163
|
+
return chargeExperiment(win, experimentTextFrom(parts));
|
|
1137
1164
|
}
|
|
1138
1165
|
|
|
1139
1166
|
/** 一次真人请求只路由一次;同一批次重试复用同一 transitionId。 */
|
|
@@ -1240,7 +1267,7 @@ export function apply(ctx, config) {
|
|
|
1240
1267
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
1241
1268
|
const created = {
|
|
1242
1269
|
contextId, projectKey: scope.key, sessionId: win.sessionId, batchKey,
|
|
1243
|
-
messageIds: win.messageIds.slice(), context: view.trustedContext(scope, {}),
|
|
1270
|
+
messageIds: win.messageIds.slice(), query: trustedQueryOf(win), context: view.trustedContext(scope, {}),
|
|
1244
1271
|
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, epoch: status.epoch, at: Date.now(),
|
|
1245
1272
|
};
|
|
1246
1273
|
win.experiment = created;
|
|
@@ -1248,13 +1275,27 @@ export function apply(ctx, config) {
|
|
|
1248
1275
|
return contextId;
|
|
1249
1276
|
}
|
|
1250
1277
|
|
|
1278
|
+
/** 本轮真人消息是唯一可信的任务依据;模型自己写的 query 只用于检索词,不能拿来建绑定。 */
|
|
1279
|
+
function trustedQueryOf(win) {
|
|
1280
|
+
return win && typeof win.query === 'string' ? win.query : '';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1251
1283
|
/** 前台绑定只升到 proposed:真正执行链还没发生,不冒充 active。 */
|
|
1252
|
-
function bindSelection(execution, resolved
|
|
1284
|
+
function bindSelection(execution, resolved) {
|
|
1253
1285
|
const held = resolved.held;
|
|
1286
|
+
if (!held || typeof held !== 'object') return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
1287
|
+
const query = held.query || trustedQueryOf(resolved.win);
|
|
1288
|
+
if (!query) return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
1254
1289
|
const ranges = held.ranges || { series: [], taskKinds: [] };
|
|
1290
|
+
/** 自动路由已经选了范围就沿用同一个结果,工具绑定不重新猜一遍,避免两处给出不同任务。 */
|
|
1291
|
+
const routedAnchor = held.anchor && typeof held.anchor === 'object' ? held.anchor : null;
|
|
1255
1292
|
const hits = [];
|
|
1256
|
-
|
|
1257
|
-
|
|
1293
|
+
if (routedAnchor && routedAnchor.seriesId) hits.push({ seriesId: routedAnchor.seriesId });
|
|
1294
|
+
else if (routedAnchor && routedAnchor.taskKind) hits.push({ taskKind: routedAnchor.taskKind });
|
|
1295
|
+
else {
|
|
1296
|
+
for (const value of ranges.series || []) if (anchor.seriesChosenBy(query, value)) hits.push({ seriesId: value });
|
|
1297
|
+
for (const value of ranges.taskKinds || []) if (anchor.seriesChosenBy(query, value)) hits.push({ taskKind: value });
|
|
1298
|
+
}
|
|
1258
1299
|
if (hits.length !== 1) {
|
|
1259
1300
|
const names = [...(ranges.series || []), ...(ranges.taskKinds || [])].slice(0, 3);
|
|
1260
1301
|
return { ok: false, status: 'needs_context', reason: names.length ? '本轮没有匹配的记忆范围,按当前任务继续' : '本轮没有适用的记忆范围,按当前任务继续' };
|
|
@@ -1276,7 +1317,11 @@ export function apply(ctx, config) {
|
|
|
1276
1317
|
resolved.context = held.context;
|
|
1277
1318
|
const text = renderExperiment(resolved.scope, held.decision, held.context, held, resolved.win);
|
|
1278
1319
|
const preparedState = prepared.get(execution && execution.agent);
|
|
1279
|
-
if (preparedState)
|
|
1320
|
+
if (preparedState) {
|
|
1321
|
+
preparedState.experimentText = text;
|
|
1322
|
+
preparedState.experimentParts = held.experimentParts || null;
|
|
1323
|
+
preparedState.validUntil = held.validUntil || '';
|
|
1324
|
+
}
|
|
1280
1325
|
return { ok: true };
|
|
1281
1326
|
}
|
|
1282
1327
|
|
|
@@ -1297,7 +1342,17 @@ export function apply(ctx, config) {
|
|
|
1297
1342
|
if (!memoryEnabled(scope)) return '';
|
|
1298
1343
|
const epoch = controlStatus(scope).epoch || '';
|
|
1299
1344
|
if (state.controlEpoch && epoch && state.controlEpoch !== epoch) return '';
|
|
1300
|
-
|
|
1345
|
+
/** 自进化关掉或换过代:关掉前的实验正文不再输出,重开也不复活。 */
|
|
1346
|
+
let evolutionText = state.experimentParts ? experimentTextFrom(state.experimentParts) : state.experimentText;
|
|
1347
|
+
if (evolutionText) {
|
|
1348
|
+
let evolution;
|
|
1349
|
+
try { evolution = evolutionStatus(scope); } catch { evolution = null; }
|
|
1350
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false
|
|
1351
|
+
|| (state.evolutionEpoch && String(evolution.epoch || '') !== state.evolutionEpoch)) evolutionText = '';
|
|
1352
|
+
}
|
|
1353
|
+
/** 没有逐条快照时才退回整段到期判断;有快照就只撤到期的那一条。 */
|
|
1354
|
+
if (evolutionText && !state.experimentParts && state.validUntil && Date.now() > Date.parse(String(state.validUntil))) evolutionText = '';
|
|
1355
|
+
return [evolutionText, state.text].filter(Boolean).join('\n\n');
|
|
1301
1356
|
} catch {
|
|
1302
1357
|
return '';
|
|
1303
1358
|
}
|
|
@@ -1328,6 +1383,8 @@ export function apply(ctx, config) {
|
|
|
1328
1383
|
title: { type: 'string' },
|
|
1329
1384
|
summary: { type: 'string' },
|
|
1330
1385
|
when: { type: 'string' },
|
|
1386
|
+
summaryShortened: { type: 'boolean' },
|
|
1387
|
+
fullTextInBody: { type: 'boolean' },
|
|
1331
1388
|
revision: { type: 'integer' },
|
|
1332
1389
|
merged: { type: 'boolean' },
|
|
1333
1390
|
changed: { type: 'boolean' },
|
|
@@ -1361,7 +1418,7 @@ export function apply(ctx, config) {
|
|
|
1361
1418
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
1362
1419
|
: value.changed === false
|
|
1363
1420
|
? `memory · unchanged ${withTag(value, `"${value.title}"`)} · ${value.id}`
|
|
1364
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}`,
|
|
1421
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextInBody ? ' · 摘要超过 60 字,完整原句已存进正文' : ''}`,
|
|
1365
1422
|
}],
|
|
1366
1423
|
},
|
|
1367
1424
|
execute: async (args, execution) => {
|
|
@@ -1439,6 +1496,8 @@ export function apply(ctx, config) {
|
|
|
1439
1496
|
title: saved.record.title,
|
|
1440
1497
|
summary: saved.record.summary,
|
|
1441
1498
|
when: saved.record.when || '',
|
|
1499
|
+
summaryShortened: Boolean(saved.draft?.summaryShortened),
|
|
1500
|
+
fullTextInBody: Boolean(saved.draft?.fullTextInBody),
|
|
1442
1501
|
revision: saved.record.revision || 1,
|
|
1443
1502
|
merged: saved.merged,
|
|
1444
1503
|
changed: !saved.noop,
|
|
@@ -1507,6 +1566,7 @@ export function apply(ctx, config) {
|
|
|
1507
1566
|
chars: { type: 'integer' },
|
|
1508
1567
|
text: { type: 'string' },
|
|
1509
1568
|
status: { type: 'string', enum: ['ok', 'budget_exhausted', 'memory_disabled', 'needs_context'] },
|
|
1569
|
+
reasonCode: { type: 'string' },
|
|
1510
1570
|
refs: {
|
|
1511
1571
|
type: 'array',
|
|
1512
1572
|
items: {
|
|
@@ -1553,19 +1613,20 @@ export function apply(ctx, config) {
|
|
|
1553
1613
|
const resolved = resolveRequestContext(execution, contextId);
|
|
1554
1614
|
if (!resolved.ok) return { count: 0, chars: 0, text: resolved.reason, status: resolved.status || 'needs_context', refs: [] };
|
|
1555
1615
|
if (wantsBind) {
|
|
1556
|
-
const bound = bindSelection(execution, resolved
|
|
1616
|
+
const bound = bindSelection(execution, resolved);
|
|
1557
1617
|
if (!bound.ok) return { count: 0, chars: 0, text: bound.reason, status: bound.status || 'needs_context', refs: [] };
|
|
1558
1618
|
}
|
|
1559
1619
|
const scope = resolved.scope;
|
|
1560
1620
|
const context = resolved.context;
|
|
1561
|
-
/**
|
|
1562
|
-
const
|
|
1621
|
+
/** 检索只列当前任务真能用的条目;先取全量候选,过滤后再截断,避免不可用条目挤掉后面的命中。 */
|
|
1622
|
+
const limit = args.limit ?? 3;
|
|
1623
|
+
const rawHits = search(scope, args.query, 50, {
|
|
1563
1624
|
context,
|
|
1564
1625
|
history: args.history === true,
|
|
1565
1626
|
allowExperimental: resolved.experimental === true,
|
|
1566
|
-
}).filter((item) =>
|
|
1567
|
-
|
|
1568
|
-
|
|
1627
|
+
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key);
|
|
1628
|
+
const hits = rawHits.filter((item) => item.archived || item.status === 'ok').slice(0, limit);
|
|
1629
|
+
const withheld = rawHits.filter((item) => !item.archived && item.status !== 'ok');
|
|
1569
1630
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1570
1631
|
const shown = hits.map((item) => ({
|
|
1571
1632
|
...view.toModelView(item, 'search', { status: item.status || (item.archived ? 'archived' : 'ok') }),
|
|
@@ -1575,6 +1636,27 @@ export function apply(ctx, config) {
|
|
|
1575
1636
|
const rendered = renderIndex(shown.slice(0, kept));
|
|
1576
1637
|
return { count: kept, chars: rendered.chars, text: rendered.text, status: 'ok', refs: refsOf(hits.slice(0, kept)) };
|
|
1577
1638
|
};
|
|
1639
|
+
/** 有匹配但当前用不上:只说状态,不把标题、编号或旧规则递给模型,避免它被这些字面带偏。 */
|
|
1640
|
+
if (!hits.length && withheld.length) {
|
|
1641
|
+
const unresolved = withheld.some((item) => item.status === 'needs_context');
|
|
1642
|
+
const value = {
|
|
1643
|
+
count: 0,
|
|
1644
|
+
chars: 0,
|
|
1645
|
+
text: unresolved
|
|
1646
|
+
? '有相关的项目记忆,但适用条件还没确认。先说明当前任务,或确认这条记忆的适用范围。'
|
|
1647
|
+
: '有相关的项目记忆,但当前任务明确不适用。',
|
|
1648
|
+
status: unresolved ? 'needs_context' : 'ok',
|
|
1649
|
+
reasonCode: unresolved ? 'applicability_unresolved' : 'not_applicable',
|
|
1650
|
+
refs: [],
|
|
1651
|
+
};
|
|
1652
|
+
if (win) {
|
|
1653
|
+
const bytes = jsonBytes(value);
|
|
1654
|
+
if (!affordable(win, bytes)) return { ...value, status: 'budget_exhausted', text: '' };
|
|
1655
|
+
win.bytes += bytes;
|
|
1656
|
+
win.reads += 1;
|
|
1657
|
+
}
|
|
1658
|
+
return value;
|
|
1659
|
+
}
|
|
1578
1660
|
/** 没命中是正常结果,不是额度问题;空响应同样记一次本请求开销。 */
|
|
1579
1661
|
if (!hits.length) {
|
|
1580
1662
|
const empty = { count: 0, chars: 0, text: '', status: 'ok', refs: [] };
|
|
@@ -1632,10 +1714,20 @@ export function apply(ctx, config) {
|
|
|
1632
1714
|
: value.status === 'memory_disabled'
|
|
1633
1715
|
? 'memory · 记忆检索已关闭'
|
|
1634
1716
|
: value.repeat
|
|
1635
|
-
?
|
|
1717
|
+
? 'memory · 本请求已读过这条记忆'
|
|
1636
1718
|
: value.body
|
|
1637
1719
|
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''} · ${value.id}${statusLabel(value.status)}\n${value.body}`
|
|
1638
|
-
:
|
|
1720
|
+
: value.links !== undefined && value.status === 'ok'
|
|
1721
|
+
? `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`
|
|
1722
|
+
: value.status === 'needs_context'
|
|
1723
|
+
? 'memory · 这条记忆的适用条件还没确认'
|
|
1724
|
+
: value.status === 'not_applicable'
|
|
1725
|
+
? 'memory · 这条记忆不适用于当前任务'
|
|
1726
|
+
: value.status === 'archived'
|
|
1727
|
+
? 'memory · 这条记忆已归档'
|
|
1728
|
+
: value.status === 'gone'
|
|
1729
|
+
? 'memory · 这条记忆已不存在'
|
|
1730
|
+
: 'memory · 当前不能读这条记忆',
|
|
1639
1731
|
}],
|
|
1640
1732
|
presentationMeta: (_args, value) => ({
|
|
1641
1733
|
kind: 'memory-read',
|
|
@@ -1661,7 +1753,7 @@ export function apply(ctx, config) {
|
|
|
1661
1753
|
const found = readLinks(scope, args.id, { context, allowExperimental });
|
|
1662
1754
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1663
1755
|
if (found.status !== 'ok') {
|
|
1664
|
-
const withheld = { id:
|
|
1756
|
+
const withheld = { id: '', scope: '', status: found.status, links: '' };
|
|
1665
1757
|
if (win) {
|
|
1666
1758
|
const bytes = jsonBytes(withheld);
|
|
1667
1759
|
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
@@ -1687,22 +1779,22 @@ export function apply(ctx, config) {
|
|
|
1687
1779
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1688
1780
|
if (!record) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1689
1781
|
if (record.origin === 'project_evolution' && record.ownerProjectId && record.ownerProjectId !== scope.key) {
|
|
1690
|
-
return { id:
|
|
1782
|
+
return { id: '', scope: record.scope, status: 'not_applicable', body: '' };
|
|
1691
1783
|
}
|
|
1692
1784
|
if (record.origin === 'project_evolution' && !allowExperimental) {
|
|
1693
|
-
return { id:
|
|
1785
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1694
1786
|
}
|
|
1695
|
-
/**
|
|
1787
|
+
/** 暂停、过期或条件不匹配的实验条目只给状态,不给标题和正文。 */
|
|
1696
1788
|
if (record.origin === 'project_evolution' && !view.experimentalUsable(record)) {
|
|
1697
|
-
return { id:
|
|
1789
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1698
1790
|
}
|
|
1699
1791
|
const learningState = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1700
1792
|
if (record.origin === 'project_evolution' && (learningState.state === 'paused' || learningState.blockedReason)) {
|
|
1701
|
-
return { id:
|
|
1793
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1702
1794
|
}
|
|
1703
|
-
/**
|
|
1795
|
+
/** 条件未知或不适用的条目只报状态:正文和标题都不能绕过自动召回的门槛。 */
|
|
1704
1796
|
if (record.status !== 'ok') {
|
|
1705
|
-
return { id:
|
|
1797
|
+
return { id: '', scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
1706
1798
|
}
|
|
1707
1799
|
const revision = record.revision || 1;
|
|
1708
1800
|
const body = String(record.body ?? '');
|
|
@@ -1936,6 +2028,8 @@ export function apply(ctx, config) {
|
|
|
1936
2028
|
previous.text = snapshot ? snapshot.text : '';
|
|
1937
2029
|
previous.cards = snapshot ? snapshot.cards : [];
|
|
1938
2030
|
previous.experimentText = experimentText;
|
|
2031
|
+
previous.experimentParts = win && win.experiment ? (win.experiment.experimentParts || null) : null;
|
|
2032
|
+
previous.validUntil = win && win.experiment ? (win.experiment.validUntil || '') : '';
|
|
1939
2033
|
previous.fingerprint = fingerprint;
|
|
1940
2034
|
} else {
|
|
1941
2035
|
clearPrepared(agent);
|
|
@@ -1948,13 +2042,15 @@ export function apply(ctx, config) {
|
|
|
1948
2042
|
/** 工具模式也要开额度窗口:只是不自动注入,工具调用同样受每请求预算约束。 */
|
|
1949
2043
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
1950
2044
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2045
|
+
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2046
|
+
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2047
|
+
if (win && query) win.query = query;
|
|
1951
2048
|
|
|
1952
2049
|
if (recallMode() !== 'adaptive') {
|
|
1953
2050
|
clearPrepared(agent);
|
|
1954
2051
|
return decision;
|
|
1955
2052
|
}
|
|
1956
2053
|
|
|
1957
|
-
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
1958
2054
|
if (!query) {
|
|
1959
2055
|
clearPrepared(agent);
|
|
1960
2056
|
return decision;
|
|
@@ -1995,9 +2091,13 @@ export function apply(ctx, config) {
|
|
|
1995
2091
|
clearPrepared(agent);
|
|
1996
2092
|
return decision;
|
|
1997
2093
|
}
|
|
2094
|
+
let evolutionEpoch = '';
|
|
2095
|
+
try { evolutionEpoch = String(evolutionStatus(scope).epoch || ''); } catch { evolutionEpoch = ''; }
|
|
1998
2096
|
const state = {
|
|
1999
2097
|
text: snapshot ? snapshot.text : '',
|
|
2000
2098
|
experimentText: experiment ? experiment.text : '',
|
|
2099
|
+
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2100
|
+
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
2001
2101
|
cards: snapshot ? snapshot.cards : [],
|
|
2002
2102
|
query,
|
|
2003
2103
|
sessionId,
|
|
@@ -2005,6 +2105,7 @@ export function apply(ctx, config) {
|
|
|
2005
2105
|
signal,
|
|
2006
2106
|
fingerprint,
|
|
2007
2107
|
controlEpoch,
|
|
2108
|
+
evolutionEpoch,
|
|
2008
2109
|
at: Date.now(),
|
|
2009
2110
|
};
|
|
2010
2111
|
prepared.set(agent, state);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.2",
|
|
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/state.js
CHANGED
|
@@ -182,9 +182,9 @@ export const LIMITS = {
|
|
|
182
182
|
entries: 200,
|
|
183
183
|
globalEntries: 200,
|
|
184
184
|
hardEntries: 1000,
|
|
185
|
-
entryBytes:
|
|
185
|
+
entryBytes: 4096,
|
|
186
186
|
recordBytes: 10 * 1024,
|
|
187
|
-
totalBytes:
|
|
187
|
+
totalBytes: 1024 * 1024,
|
|
188
188
|
historyBytes: 512 * 1024,
|
|
189
189
|
stateBytes: 2 * 1024 * 1024,
|
|
190
190
|
titleChars: 60,
|
|
@@ -747,10 +747,12 @@ export function enforceBudget(state, scope) {
|
|
|
747
747
|
const active = state.entries.filter((entry) => entry.status === 'active');
|
|
748
748
|
const bytes = active.reduce((sum, entry) => sum + entryBytesOf(entry), 0);
|
|
749
749
|
if (active.length <= LIMITS.hardEntries && bytes <= LIMITS.totalBytes) break;
|
|
750
|
-
|
|
751
|
-
const
|
|
750
|
+
/** 用户固定过的规则不参与容量腾挪;只从没固定的条目里选最久没读的。 */
|
|
751
|
+
const movable = active.filter((entry) => entry.maintenance?.learning?.pinned !== true);
|
|
752
|
+
if (!movable.length) throw new StateError('budget_exhausted', '记忆库已满,固定住的规则不参与腾挪:先取消固定或精简正文');
|
|
753
|
+
const idle = movable.filter((entry) => now - entryLastTouch(entry) >= IDLE_MS);
|
|
754
|
+
const pool = idle.length ? idle : movable;
|
|
752
755
|
const victim = pool.reduce((oldest, entry) => (entryLastTouch(entry) < entryLastTouch(oldest) ? entry : oldest));
|
|
753
|
-
if (!victim) throw new StateError('budget_exhausted', `记忆库已满:这一条自己就超过 ${LIMITS.totalBytes} 字节正文预算,先精简正文`);
|
|
754
756
|
victim.status = 'archived';
|
|
755
757
|
victim.updatedAt = new Date().toISOString();
|
|
756
758
|
evicted.push({ id: victim.id, title: victim.current.title });
|