dsh-memsys 0.2.0 → 0.2.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 +11 -5
- package/client.js +115 -24
- package/index.mjs +77 -25
- package/package.json +2 -2
- package/src/context/compile.mjs +6 -5
- package/src/context/delivery.mjs +1 -1
- package/src/context/snapshots.mjs +10 -2
- package/src/read/get.mjs +57 -6
- package/src/runtime/inject-round.mjs +138 -0
- package/src/runtime/inject.mjs +12 -3
- package/src/runtime/modules.mjs +20 -0
- package/src/runtime/summary.mjs +70 -90
- package/src/runtime/tools.mjs +21 -4
- package/src/runtime/ui-config.mjs +176 -0
- package/test/p4-p4-delivery.test.mjs +7 -1
- package/test/p6-c1c4.test.mjs +69 -0
- package/test/p6-c3-append.test.mjs +85 -0
- package/test/p6-inject-round.test.mjs +76 -0
- package/test/p6-modules.test.mjs +77 -0
- package/test/p6-ui-summary.test.mjs +368 -110
package/README.md
CHANGED
|
@@ -71,16 +71,22 @@ node scripts/live-check.mjs --case tools --profile memsys # 真机:插件
|
|
|
71
71
|
- **设置 →「记忆系统」**:`auto_inject` 开关 + 当前数据目录;开关改完**立即生效**(重挂/撤下 system prompt 里的记忆段),刷新后仍在
|
|
72
72
|
- 面板与设置页共用数据口 `/dsh-memsys/api/*`(loopback + 同源校验;跨源 403)
|
|
73
73
|
|
|
74
|
-
**设置优先级**:`<data_dir>/ui-config.json`(存在且合法)> profile 的 `config
|
|
75
|
-
`/api/summary` 会返回 `
|
|
74
|
+
**设置优先级**:`<data_dir>/ui-config.json`(存在且合法)> profile 的 `config` > 默认全开。
|
|
75
|
+
`/api/summary` 会返回 `modules` / `modules_source`,告诉你每个开关的当前值和来源(`ui-config` / `profile` / `default`)。
|
|
76
|
+
|
|
77
|
+
**注意生效时机**:自动注入、写入工具**改完立即生效**;读取工具 / 做梦 / 项目 / 待确认惦记 / 信息流是**下次启动生效**(工具在启动时一次性注册,宿主没有注销接口)。
|
|
76
78
|
|
|
77
79
|
**headless 不受影响**:没有 `webServer` 时那段注册不激活,浏览器半也不加载。
|
|
78
80
|
|
|
79
|
-
##
|
|
81
|
+
## 状态(0.2.1)
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
- **读**:真机 `dispatch=ok`,`direct_hit_count: 2` 返回真实命中
|
|
84
|
+
- **写**:真机 `record=ok`,库内落 candidate + memory,`origin.author_kind=model`、`call_id` 与宿主记录一致
|
|
85
|
+
- **注入**:`compileRound` 已接进运行时,section 文本是**当轮编译出来的记忆目录**(`text` 为函数,每步 `assemble()` 重取);无 tokenizer/H 时退回 tools-only,不假装投递(`delivery=unknown`)
|
|
86
|
+
- **界面**:左侧栏「记忆」入口 + 面板(三数字 + 最近 5 条)+ 设置页七行模块开关
|
|
87
|
+
- **测试**:356 项全绿
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
**仍不达标**:`L6` 只核到内容 hash 与关联,**没有逐项重放 relations 内容**;真实旧库尚未导入;生产 cutover 未跑;`feed` 生产 provider 未接。见文末"已知边界"。
|
|
84
90
|
|
|
85
91
|
MIT License。
|
|
86
92
|
|
package/client.js
CHANGED
|
@@ -27,19 +27,83 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
return [state, setState];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Pure CSS/DOM "memory balls on a shelf". No 3D library, no build step,
|
|
31
|
+
// no external resource: gradient + inset shadow + one float keyframe.
|
|
32
|
+
const BALL_CSS = [
|
|
33
|
+
'@keyframes msFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}',
|
|
34
|
+
'.ms-panel{padding:16px;line-height:1.7}',
|
|
35
|
+
'.ms-counts{margin:0 0 12px;opacity:.85}',
|
|
36
|
+
'.ms-shelf{display:flex;gap:16px;align-items:flex-end;padding:12px 16px 8px;margin:0 0 10px;border-radius:12px;',
|
|
37
|
+
'background:linear-gradient(180deg,rgba(127,127,127,.10),rgba(127,127,127,.03));',
|
|
38
|
+
'box-shadow:inset 0 -1px 0 rgba(127,127,127,.28)}',
|
|
39
|
+
'.ms-ball{width:44px;height:44px;border-radius:50%;border:0;padding:0;cursor:pointer;position:relative;',
|
|
40
|
+
'background:radial-gradient(circle at 32% 27%,rgba(255,255,255,.95),rgba(255,255,255,.28) 16%,var(--ms-c) 60%,rgba(0,0,0,.45) 100%);',
|
|
41
|
+
'box-shadow:inset -6px -8px 14px rgba(0,0,0,.30),inset 6px 6px 12px rgba(255,255,255,.22),0 4px 10px rgba(0,0,0,.18);',
|
|
42
|
+
'animation:msFloat 5s ease-in-out infinite;transition:transform .15s ease,box-shadow .15s ease}',
|
|
43
|
+
'.ms-ball:hover,.ms-ball:focus-visible{transform:scale(1.06);',
|
|
44
|
+
'box-shadow:inset -6px -8px 14px rgba(0,0,0,.30),inset 6px 6px 12px rgba(255,255,255,.22),0 0 18px 4px var(--ms-glow);outline:none}',
|
|
45
|
+
'.ms-ball[aria-pressed="true"]{transform:scale(1.06);box-shadow:0 0 18px 4px var(--ms-glow)}',
|
|
46
|
+
'.ms-empty{width:44px;height:44px;border-radius:50%;border:1px dashed rgba(127,127,127,.35);background:rgba(127,127,127,.05)}',
|
|
47
|
+
'.ms-card{margin-top:10px;padding:12px 14px;border-radius:10px;background:rgba(127,127,127,.08);font-size:.95em}',
|
|
48
|
+
'.ms-hint{opacity:.75;margin:6px 0 0}',
|
|
49
|
+
'@media (prefers-reduced-motion:reduce){.ms-ball{animation:none}}',
|
|
50
|
+
].join('');
|
|
51
|
+
const BALL_COLORS = ['#3fa7ff', '#5b8cff', '#7a6bff', '#8f5bd8', '#8450a8'];
|
|
52
|
+
const BALL_GLOWS = [
|
|
53
|
+
'rgba(63,167,255,.45)',
|
|
54
|
+
'rgba(91,140,255,.45)',
|
|
55
|
+
'rgba(122,107,255,.45)',
|
|
56
|
+
'rgba(143,91,216,.45)',
|
|
57
|
+
'rgba(132,80,168,.45)',
|
|
58
|
+
];
|
|
59
|
+
const EMPTY_SLOTS = 5;
|
|
60
|
+
const CARD_TEXT_LIMIT = 120;
|
|
61
|
+
|
|
30
62
|
function MemoryPanel() {
|
|
31
63
|
const [state] = useSummary();
|
|
32
|
-
|
|
33
|
-
if (state
|
|
64
|
+
const [selectedId, setSelectedId] = React.useState(null);
|
|
65
|
+
if (!state) return h('p', { className: 'ms-panel' }, '读取记忆…');
|
|
66
|
+
if (state.error) return h('p', { className: 'ms-panel' }, '暂时读不到记忆数据。');
|
|
67
|
+
const recent = state.recent ?? [];
|
|
68
|
+
const selected = recent.find((memory) => memory.id === selectedId) ?? null;
|
|
69
|
+
// Pad with ghost slots so an almost-empty shelf keeps its shape.
|
|
70
|
+
const shelf = [];
|
|
71
|
+
for (let index = 0; index < Math.max(recent.length, EMPTY_SLOTS); index += 1) shelf.push(recent[index] ?? null);
|
|
34
72
|
return h(
|
|
35
73
|
'div',
|
|
36
|
-
{
|
|
74
|
+
{ className: 'ms-panel' },
|
|
75
|
+
h('style', { dangerouslySetInnerHTML: { __html: BALL_CSS } }),
|
|
37
76
|
h('h2', { style: { margin: '0 0 8px' } }, '记忆'),
|
|
38
|
-
h('p',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
77
|
+
h('p', { className: 'ms-counts' },
|
|
78
|
+
`记忆 ${state.memories} 条 · 候选 ${state.candidates} 条 · 待确认惦记 ${state.pending_due} 条`),
|
|
79
|
+
h('div', { className: 'ms-shelf', role: 'list', 'aria-label': '最近的记忆' },
|
|
80
|
+
shelf.map((memory, index) => memory === null
|
|
81
|
+
? h('span', { key: `empty-${index}`, className: 'ms-empty', 'aria-hidden': true })
|
|
82
|
+
: h('button', {
|
|
83
|
+
key: memory.id,
|
|
84
|
+
type: 'button',
|
|
85
|
+
role: 'listitem',
|
|
86
|
+
className: 'ms-ball',
|
|
87
|
+
'aria-pressed': selectedId === memory.id,
|
|
88
|
+
'aria-label': memory.text,
|
|
89
|
+
title: memory.text,
|
|
90
|
+
style: {
|
|
91
|
+
'--ms-c': BALL_COLORS[Math.min(index, BALL_COLORS.length - 1)],
|
|
92
|
+
'--ms-glow': BALL_GLOWS[Math.min(index, BALL_GLOWS.length - 1)],
|
|
93
|
+
animationDelay: `${index * 0.35}s`,
|
|
94
|
+
width: `${44 - Math.min(index, 3) * 2}px`,
|
|
95
|
+
height: `${44 - Math.min(index, 3) * 2}px`,
|
|
96
|
+
},
|
|
97
|
+
onClick: () => setSelectedId(selectedId === memory.id ? null : memory.id),
|
|
98
|
+
})),
|
|
99
|
+
),
|
|
100
|
+
recent.length === 0
|
|
101
|
+
? h('p', { className: 'ms-hint' }, '还没有记忆。去跟模型说一句「记住:…」吧')
|
|
102
|
+
: h('p', { className: 'ms-hint' }, '点亮一颗球,看看它装的是什么。'),
|
|
103
|
+
selected
|
|
104
|
+
? h('div', { className: 'ms-card', key: selected.id },
|
|
105
|
+
Array.from(selected.text).slice(0, CARD_TEXT_LIMIT).join(''))
|
|
106
|
+
: null,
|
|
43
107
|
);
|
|
44
108
|
}
|
|
45
109
|
|
|
@@ -49,6 +113,17 @@ window.__ModuleLoader__.load({
|
|
|
49
113
|
|
|
50
114
|
const SOURCE_LABEL = { 'ui-config': '设置文件', profile: 'profile 配置', default: '默认值' };
|
|
51
115
|
|
|
116
|
+
// key, 名字, 一句人话, 生效时机(别糊成"立即生效",四个模块真的要重启)
|
|
117
|
+
const MODULE_ROWS = [
|
|
118
|
+
['context', '自动注入', '每轮把记忆摘要注入系统提示;关掉后只能手动查', '改完立即生效'],
|
|
119
|
+
['read', '读取工具', '关掉后查询类工具(get / search / review)不可用', '下次启动生效'],
|
|
120
|
+
['write', '写入工具', '关掉后不再记录新记忆,候选也收', '改完立即生效'],
|
|
121
|
+
['dream', '做梦', '关掉后不浮现、也不能记录梦', '下次启动生效'],
|
|
122
|
+
['projects', '项目', '关掉后项目视角不可用', '下次启动生效'],
|
|
123
|
+
['due', '待确认惦记', '关掉后不提醒到期的惦记,也不能确认或搁置', '下次启动生效'],
|
|
124
|
+
['feed', '信息流', '关掉后不产出信息流候选', '下次启动生效'],
|
|
125
|
+
];
|
|
126
|
+
|
|
52
127
|
function MemorySettings() {
|
|
53
128
|
const [state, setState] = React.useState(null);
|
|
54
129
|
const [note, setNote] = React.useState('');
|
|
@@ -58,36 +133,52 @@ window.__ModuleLoader__.load({
|
|
|
58
133
|
.then(setState)
|
|
59
134
|
.catch(() => setState({ error: true }));
|
|
60
135
|
}, []);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
136
|
+
const modules = state?.modules ?? {};
|
|
137
|
+
const save = (key, value) => {
|
|
138
|
+
const previous = modules[key] !== false;
|
|
139
|
+
setState((current) => ({ ...(current ?? {}), modules: { ...(current?.modules ?? {}), [key]: value } }));
|
|
64
140
|
setNote('保存中…');
|
|
65
141
|
fetch(CONFIG, {
|
|
66
142
|
method: 'POST',
|
|
67
143
|
headers: { 'content-type': 'application/json' },
|
|
68
|
-
body: JSON.stringify({
|
|
144
|
+
body: JSON.stringify({ modules: { [key]: value } }),
|
|
69
145
|
})
|
|
70
|
-
.then((response) =>
|
|
146
|
+
.then((response) =>
|
|
147
|
+
response.ok
|
|
148
|
+
? response.json().then((body) => setState((current) => ({ ...(current ?? {}), ...body })))
|
|
149
|
+
: Promise.reject(new Error(String(response.status))),
|
|
150
|
+
)
|
|
151
|
+
.then(() => setNote('已保存'))
|
|
71
152
|
.catch(() => {
|
|
72
|
-
setState((current) => ({ ...(current ?? {}),
|
|
153
|
+
setState((current) => ({ ...(current ?? {}), modules: { ...(current?.modules ?? {}), [key]: previous } }));
|
|
73
154
|
setNote('保存失败');
|
|
74
155
|
});
|
|
75
156
|
};
|
|
157
|
+
const disabled = !state || state.error === true;
|
|
76
158
|
return h(
|
|
77
159
|
'div',
|
|
78
160
|
{ style: { padding: '8px 0', lineHeight: 1.7 } },
|
|
79
|
-
h('
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
161
|
+
h('p', { style: { margin: '0 0 4px', opacity: 0.75 } }, '按模块开关,默认全开。关掉哪一项,那一项就不再工作。'),
|
|
162
|
+
...MODULE_ROWS.map(([key, label, hint, timing]) =>
|
|
163
|
+
h(
|
|
164
|
+
'label',
|
|
165
|
+
{ key, style: { display: 'block', margin: '6px 0' } },
|
|
166
|
+
h('span', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
167
|
+
h('input', {
|
|
168
|
+
type: 'checkbox',
|
|
169
|
+
checked: modules[key] !== false,
|
|
170
|
+
disabled,
|
|
171
|
+
onChange: (event) => save(key, event.target.checked),
|
|
172
|
+
}),
|
|
173
|
+
label,
|
|
174
|
+
),
|
|
175
|
+
h('span', { style: { display: 'block', marginLeft: 24, opacity: 0.7, fontSize: '0.92em' } },
|
|
176
|
+
`${hint}(${timing})`),
|
|
177
|
+
),
|
|
87
178
|
),
|
|
88
|
-
h('p', { style: { opacity: 0.75, margin: '
|
|
179
|
+
h('p', { style: { opacity: 0.75, margin: '10px 0 0' } }, `数据目录:${state?.data_dir ?? '未配置'}`),
|
|
89
180
|
h('p', { style: { opacity: 0.75, margin: '4px 0 0' } },
|
|
90
|
-
`当前生效值来源:${SOURCE_LABEL[state?.
|
|
181
|
+
`当前生效值来源:${SOURCE_LABEL[state?.modules_source] ?? state?.modules_source ?? '未知'}`),
|
|
91
182
|
note ? h('p', { style: { opacity: 0.75, margin: '4px 0 0' } }, note) : null,
|
|
92
183
|
);
|
|
93
184
|
}
|
package/index.mjs
CHANGED
|
@@ -12,33 +12,45 @@ import { createFeedHandler } from './src/tools/feed.mjs';
|
|
|
12
12
|
import { createSelfHandler } from './src/tools/self.mjs';
|
|
13
13
|
import { createRecordHandler } from './src/tools/record.mjs';
|
|
14
14
|
import { runtimeFromExec } from './src/runtime/identity.mjs';
|
|
15
|
-
import { createSummaryHandler
|
|
15
|
+
import { createSummaryHandler } from './src/runtime/summary.mjs';
|
|
16
|
+
import { mergeModules, resolveModulesDetail, writeUiConfig } from './src/runtime/ui-config.mjs';
|
|
17
|
+
import { createRoundInjector } from './src/runtime/inject-round.mjs';
|
|
18
|
+
import { compileRound } from './src/context/compile.mjs';
|
|
16
19
|
|
|
17
20
|
export const name = 'dsh-memsys';
|
|
18
21
|
/** tools is a real host service; static inject is the verified registration path. */
|
|
19
22
|
export const inject = ['tools'];
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
23
|
-
* 1. `<data_dir>/ui-config.json` — written by the settings
|
|
24
|
-
* 2. the profile patch config (`
|
|
25
|
-
*
|
|
25
|
+
* Module switches have three ranks, highest first:
|
|
26
|
+
* 1. `<data_dir>/ui-config.json` — written by the settings page;
|
|
27
|
+
* 2. the profile patch config (`auto_inject` / `modules`), which is itself a
|
|
28
|
+
* per-key overlay on the default map;
|
|
29
|
+
* 3. every module on.
|
|
26
30
|
* The file wins so a toggle survives a restart without rewriting the profile,
|
|
27
|
-
* and the in-memory
|
|
31
|
+
* and the in-memory map is applied immediately (no restart).
|
|
28
32
|
*/
|
|
29
33
|
function createUiState(config = {}, logger = console) {
|
|
30
34
|
const dataDir = config.data_dir ?? process.env.MEMSYS_DATA_DIR ?? null;
|
|
31
|
-
|
|
35
|
+
const detail = resolveModulesDetail({ dataDir, profileConfig: config, logger });
|
|
36
|
+
// One stable object: host tools capture it at registration, so a POST has to
|
|
37
|
+
// mutate it in place or the call-time gates would keep reading stale values.
|
|
38
|
+
const modules = { ...detail.modules };
|
|
39
|
+
let source = detail.source;
|
|
32
40
|
return {
|
|
33
41
|
dataDir,
|
|
34
|
-
get
|
|
35
|
-
return
|
|
42
|
+
get modules() {
|
|
43
|
+
return modules;
|
|
36
44
|
},
|
|
37
45
|
get source() {
|
|
38
|
-
return
|
|
46
|
+
return source;
|
|
47
|
+
},
|
|
48
|
+
get autoInject() {
|
|
49
|
+
return modules.context !== false;
|
|
39
50
|
},
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
apply(written) {
|
|
52
|
+
Object.assign(modules, written.modules);
|
|
53
|
+
source = 'ui-config';
|
|
42
54
|
},
|
|
43
55
|
sync: () => {},
|
|
44
56
|
};
|
|
@@ -47,27 +59,32 @@ function createUiState(config = {}, logger = console) {
|
|
|
47
59
|
function createHostHandlers(config = {}, ctx = null, ui = null) {
|
|
48
60
|
const dataDir = config.data_dir ?? process.env.MEMSYS_DATA_DIR ?? null;
|
|
49
61
|
const uiState = ui ?? {
|
|
50
|
-
get
|
|
51
|
-
return config
|
|
62
|
+
get modules() {
|
|
63
|
+
return mergeModules(config, null);
|
|
52
64
|
},
|
|
53
65
|
get source() {
|
|
54
|
-
return typeof config.auto_inject === 'boolean' ? 'profile' : 'default';
|
|
66
|
+
return typeof config.auto_inject === 'boolean' || config.modules ? 'profile' : 'default';
|
|
55
67
|
},
|
|
56
|
-
|
|
68
|
+
get autoInject() {
|
|
69
|
+
return mergeModules(config, null).context !== false;
|
|
70
|
+
},
|
|
71
|
+
apply() {},
|
|
57
72
|
sync: () => {},
|
|
58
73
|
};
|
|
59
74
|
const uiSummary = createSummaryHandler({
|
|
60
75
|
main: () => db().main,
|
|
61
76
|
dataDir,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!
|
|
77
|
+
getConfig: () => ({ modules: uiState.modules, source: uiState.source }),
|
|
78
|
+
onConfig: (patch) => {
|
|
79
|
+
if (!dataDir) {
|
|
65
80
|
const err = new Error('data_dir_missing');
|
|
66
81
|
err.code = 'data_dir_missing';
|
|
67
82
|
throw err;
|
|
68
83
|
}
|
|
69
|
-
|
|
84
|
+
const written = writeUiConfig(dataDir, patch);
|
|
85
|
+
uiState.apply(written);
|
|
70
86
|
uiState.sync();
|
|
87
|
+
return { auto_inject: written.auto_inject, modules: written.modules };
|
|
71
88
|
},
|
|
72
89
|
});
|
|
73
90
|
let handles = null;
|
|
@@ -148,7 +165,37 @@ function createHostHandlers(config = {}, ctx = null, ui = null) {
|
|
|
148
165
|
export async function apply(ctx, config = {}) {
|
|
149
166
|
const ui = createUiState(config);
|
|
150
167
|
const handlers = createHostHandlers(config, ctx, ui);
|
|
151
|
-
const registration = await registerHostTools({ ctx, handlers });
|
|
168
|
+
const registration = await registerHostTools({ ctx, handlers, modules: ui.modules });
|
|
169
|
+
|
|
170
|
+
// The round injector owns the session/turn subscription; the section only
|
|
171
|
+
// reads its current text. Verified empirically: a section's `text` function is
|
|
172
|
+
// re-evaluated on every `systemPrompt.assemble()` (once per model step), so no
|
|
173
|
+
// per-turn re-mount is needed — a toggle only has to change what `current()` says.
|
|
174
|
+
let injector = null;
|
|
175
|
+
const syncInjector = () => {
|
|
176
|
+
if (ui.modules.context === false || !ui.dataDir) {
|
|
177
|
+
injector?.dispose?.();
|
|
178
|
+
injector = null;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!injector) {
|
|
182
|
+
injector = createRoundInjector({
|
|
183
|
+
ctx,
|
|
184
|
+
dataDir: ui.dataDir,
|
|
185
|
+
modules: ui.modules,
|
|
186
|
+
compileRound,
|
|
187
|
+
tokenizer: config.tokenizer ?? null,
|
|
188
|
+
H: config.H ?? null,
|
|
189
|
+
mode: config.mode ?? 'work',
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
injector.refresh({});
|
|
193
|
+
} catch {
|
|
194
|
+
// a failed first compile must not stop the host half from booting
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
syncInjector();
|
|
152
199
|
const toolProbe = process.env.MEMSYS_TOOL_PROBE ?? null;
|
|
153
200
|
const writeToolProbe = (deferred) => {
|
|
154
201
|
if (!toolProbe) return;
|
|
@@ -319,18 +366,22 @@ export async function apply(ctx, config = {}) {
|
|
|
319
366
|
const syncSection = () => {
|
|
320
367
|
disposeSection();
|
|
321
368
|
disposeSection = () => {};
|
|
322
|
-
|
|
369
|
+
// The section is owned by the `context` module; the probe profile may force
|
|
370
|
+
// a diagnostic section even while injection is off.
|
|
371
|
+
if (ui.modules.context === false && !diagnostic) return;
|
|
323
372
|
disposeSection = registerSection(ctx, {
|
|
324
373
|
getText: () =>
|
|
325
|
-
ui.
|
|
326
|
-
? '[memsys]
|
|
327
|
-
:
|
|
374
|
+
ui.modules.context === false
|
|
375
|
+
? '[memsys] diagnostic section(context=false)'
|
|
376
|
+
: (injector?.current().text ?? ''),
|
|
377
|
+
dynamicText: true,
|
|
328
378
|
diagnosticMarker: process.env.MEMSYS_SECTION_PROBE ?? null,
|
|
329
379
|
});
|
|
330
380
|
};
|
|
331
381
|
// A profile without systemPrompt (headless/embedded) must still boot the tools.
|
|
332
382
|
ui.sync = () => {
|
|
333
383
|
try {
|
|
384
|
+
syncInjector();
|
|
334
385
|
syncSection();
|
|
335
386
|
} catch {
|
|
336
387
|
// keep the host half alive; the section is optional
|
|
@@ -339,6 +390,7 @@ export async function apply(ctx, config = {}) {
|
|
|
339
390
|
ui.sync();
|
|
340
391
|
return () => {
|
|
341
392
|
disposeSection();
|
|
393
|
+
injector?.dispose?.();
|
|
342
394
|
};
|
|
343
395
|
}
|
|
344
396
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "dsh-memsys",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A memory system plugin for DeepSeek Harness: append-only facts, candidates and adjudication, dreams, and budget-aware context injection.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/context/compile.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentRevision } from '../storage/open.mjs';
|
|
1
2
|
import { payloadHash } from '../identity.mjs';
|
|
2
3
|
import {
|
|
3
4
|
COMPRESSION_STEPS,
|
|
@@ -188,6 +189,7 @@ function compileRoundInner({
|
|
|
188
189
|
project = null,
|
|
189
190
|
sessionId = null,
|
|
190
191
|
dream = null,
|
|
192
|
+
modules = null,
|
|
191
193
|
}) {
|
|
192
194
|
if (!tokenizer || H === null) {
|
|
193
195
|
return Object.freeze({
|
|
@@ -206,11 +208,11 @@ function compileRoundInner({
|
|
|
206
208
|
feedback: (item) => feedback(item.id),
|
|
207
209
|
});
|
|
208
210
|
const depsDream = dream;
|
|
209
|
-
const dueItems = orderPendingDue(selectPendingDue({ main, mode, now: clock.now() }));
|
|
211
|
+
const dueItems = modules?.due === false ? [] : orderPendingDue(selectPendingDue({ main, mode, now: clock.now() }));
|
|
210
212
|
const dueLines = dueItems.map((item) => dueDirectoryLine(item, 80));
|
|
211
213
|
const dueTypedIds = dueItems.map((item) => item.typed_id);
|
|
212
214
|
const dreamSurface =
|
|
213
|
-
depsDream && dreamSurfaceEnabled(main)
|
|
215
|
+
modules?.dream !== false && depsDream && dreamSurfaceEnabled(main)
|
|
214
216
|
? surfaceDreams({ main, dream: depsDream, clock, mode, sessionId: sessionId ?? 'unknown-session', enabled: true, limit: 1 })
|
|
215
217
|
: { enabled: false, items: [] };
|
|
216
218
|
const dreamItems = dreamSurface.items ?? [];
|
|
@@ -248,7 +250,6 @@ function compileRoundInner({
|
|
|
248
250
|
const typedIds = project && mode === 'work' ? [`project:${project.projectId}`] : [];
|
|
249
251
|
const fixedText = buildFixedText({ clock, mode, project: mode === 'work' ? project : null });
|
|
250
252
|
const snapshotId = `snap:${payloadHash({
|
|
251
|
-
roundId,
|
|
252
253
|
mode,
|
|
253
254
|
revision: selected.revision,
|
|
254
255
|
policyRevision,
|
|
@@ -443,7 +444,7 @@ function compileRoundInner({
|
|
|
443
444
|
roundId,
|
|
444
445
|
mode,
|
|
445
446
|
level,
|
|
446
|
-
sourceRevision:
|
|
447
|
+
sourceRevision: currentRevision(main),
|
|
447
448
|
policyRevision,
|
|
448
449
|
sectionText: chosen.sectionText,
|
|
449
450
|
sectionHash,
|
|
@@ -469,7 +470,7 @@ function compileRoundInner({
|
|
|
469
470
|
mode: 'auto',
|
|
470
471
|
snapshot_revision: snapshotId,
|
|
471
472
|
roundId,
|
|
472
|
-
source_revision:
|
|
473
|
+
source_revision: currentRevision(main),
|
|
473
474
|
policy_revision: policyRevision,
|
|
474
475
|
delivery_level: level,
|
|
475
476
|
delivery: 'unknown',
|
package/src/context/delivery.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function recordDelivery({
|
|
|
50
50
|
missing = expected.filter((id) => !receivedSet.has(id));
|
|
51
51
|
extra = received.filter((id) => !expectedSet.has(id));
|
|
52
52
|
hashMatched = payloadHash !== null && manifest?.hashes?.payload === payloadHash;
|
|
53
|
-
if (missing.length === 0 && extra.length === 0 && hashMatched) {
|
|
53
|
+
if (actualMemories.length > 0 && missing.length === 0 && extra.length === 0 && hashMatched) {
|
|
54
54
|
delivery = 'confirmed';
|
|
55
55
|
} else if (received.length > 0) {
|
|
56
56
|
delivery = 'incomplete';
|
|
@@ -277,8 +277,16 @@ export function getGroupPage({
|
|
|
277
277
|
if (decoded.s !== snapshotRevision || decoded.g !== groupId) {
|
|
278
278
|
return { status: 'error', code: 'invalid_cursor', reason: 'cursor_binding_mismatch', clock: clock.at().at };
|
|
279
279
|
}
|
|
280
|
-
if (main
|
|
281
|
-
|
|
280
|
+
if (main) {
|
|
281
|
+
const manifest = snapshot.manifest_json ? JSON.parse(snapshot.manifest_json) : {};
|
|
282
|
+
const expectedCount = Array.isArray(manifest.eligible_ids) ? manifest.eligible_ids.length : null;
|
|
283
|
+
const currentCount = Number(main.prepare('SELECT COUNT(*) AS n FROM memories').get().n);
|
|
284
|
+
if (
|
|
285
|
+
Number(snapshot.source_revision) !== Number(currentRevision(main)) ||
|
|
286
|
+
(expectedCount !== null && currentCount > expectedCount)
|
|
287
|
+
) {
|
|
288
|
+
return staleCursor(control, clock);
|
|
289
|
+
}
|
|
282
290
|
}
|
|
283
291
|
offset = decoded.o;
|
|
284
292
|
}
|
package/src/read/get.mjs
CHANGED
|
@@ -156,7 +156,7 @@ export function getMemories({
|
|
|
156
156
|
return { status: 'error', code: 'validation_error', message: 'invalid_view', clock: reading };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
let state = { ids, index: 0, offset: 0, versions: null };
|
|
159
|
+
let state = { ids, index: 0, offset: 0, versions: null, evIndex: null, evOffset: 0 };
|
|
160
160
|
if (cursor !== null) {
|
|
161
161
|
const decoded = decodeGetCursor(cursor);
|
|
162
162
|
if (!decoded || decoded.ids.join(',') !== ids.join(',')) {
|
|
@@ -166,7 +166,7 @@ export function getMemories({
|
|
|
166
166
|
if (JSON.stringify(decoded.versions ?? {}) !== JSON.stringify(currentVersions)) {
|
|
167
167
|
return { status: 'error', code: 'stale_cursor', clock: reading };
|
|
168
168
|
}
|
|
169
|
-
state = { ids: decoded.ids, index: decoded.index, offset: decoded.offset, versions: decoded.versions };
|
|
169
|
+
state = { ids: decoded.ids, index: decoded.index, offset: decoded.offset, versions: decoded.versions, evIndex: decoded.evIndex ?? null, evOffset: decoded.evOffset ?? 0 };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
const budget = clampChars(maxChars);
|
|
@@ -252,19 +252,70 @@ export function getMemories({
|
|
|
252
252
|
history: view === 'history',
|
|
253
253
|
});
|
|
254
254
|
const text = formatted.text ?? '';
|
|
255
|
+
if (state.evIndex !== null) {
|
|
256
|
+
let left = budget - used;
|
|
257
|
+
const fragments = [];
|
|
258
|
+
let nextEv = null;
|
|
259
|
+
for (let e = state.evIndex; e < formatted.evidence.length; e += 1) {
|
|
260
|
+
const entry = formatted.evidence[e];
|
|
261
|
+
if (typeof entry.observation_text !== 'string' || entry.observation_text.length === 0) continue;
|
|
262
|
+
const chars = Array.from(entry.observation_text);
|
|
263
|
+
const start = e === state.evIndex ? state.evOffset : 0;
|
|
264
|
+
const take = Math.max(0, Math.min(chars.length - start, left));
|
|
265
|
+
if (take > 0) fragments.push({ ...entry, observation_text: chars.slice(start, start + take).join(''), observation_text_truncated: start + take < chars.length });
|
|
266
|
+
left -= take;
|
|
267
|
+
if (start + take < chars.length) {
|
|
268
|
+
nextEv = { evIndex: e, evOffset: start + take };
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
items.push({ ...formatted, evidence: fragments });
|
|
273
|
+
used = budget - left;
|
|
274
|
+
if (nextEv === null) {
|
|
275
|
+
index += 1;
|
|
276
|
+
offset = 0;
|
|
277
|
+
state.evIndex = null;
|
|
278
|
+
state.evOffset = 0;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const nextCursor = encodeGetCursor({
|
|
282
|
+
ids, index, offset: 0, evIndex: nextEv.evIndex, evOffset: nextEv.evOffset,
|
|
283
|
+
versions: state.versions ?? versionMapOf(main, ids),
|
|
284
|
+
});
|
|
285
|
+
return { status: 'ok', items, missing_ids: missing, snapshot_revision: currentRevision(main), next_cursor: nextCursor, truncated: true, clock: reading };
|
|
286
|
+
}
|
|
255
287
|
if (offset === 0 && used + codePoints(text) <= budget) {
|
|
256
288
|
if (includeEvidence && Array.isArray(formatted.evidence)) {
|
|
257
289
|
let left = budget - used - codePoints(text);
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
let nextEv = null;
|
|
291
|
+
const kept = [];
|
|
292
|
+
for (let e = 0; e < formatted.evidence.length; e += 1) {
|
|
293
|
+
const entry = formatted.evidence[e];
|
|
294
|
+
if (typeof entry.observation_text !== 'string' || entry.observation_text.length === 0) {
|
|
295
|
+
kept.push(entry);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
260
298
|
const chars = Array.from(entry.observation_text);
|
|
261
299
|
if (chars.length <= left) {
|
|
262
300
|
left -= chars.length;
|
|
301
|
+
kept.push(entry);
|
|
263
302
|
continue;
|
|
264
303
|
}
|
|
265
|
-
|
|
266
|
-
entry.observation_text_truncated
|
|
304
|
+
const take = Math.max(0, left);
|
|
305
|
+
kept.push({ ...entry, observation_text: chars.slice(0, take).join(''), observation_text_truncated: true });
|
|
306
|
+
nextEv = { evIndex: e, evOffset: take };
|
|
267
307
|
left = 0;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
formatted.evidence = kept;
|
|
311
|
+
if (nextEv !== null) {
|
|
312
|
+
items.push(formatted);
|
|
313
|
+
used = budget;
|
|
314
|
+
const nextCursor = encodeGetCursor({
|
|
315
|
+
ids, index, offset: 0, evIndex: nextEv.evIndex, evOffset: nextEv.evOffset,
|
|
316
|
+
versions: state.versions ?? versionMapOf(main, ids),
|
|
317
|
+
});
|
|
318
|
+
return { status: 'ok', items, missing_ids: missing, snapshot_revision: currentRevision(main), next_cursor: nextCursor, truncated: true, clock: reading };
|
|
268
319
|
}
|
|
269
320
|
}
|
|
270
321
|
items.push(formatted);
|