dsh-memsys 0.1.0 → 0.2.1
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 +34 -7
- package/client.js +137 -0
- package/index.mjs +152 -11
- package/package.json +9 -2
- package/scripts/build.mjs +2 -1
- package/scripts/import-legacy.mjs +12 -2
- package/scripts/live-check.mjs +1 -1
- package/src/context/compile.mjs +6 -5
- package/src/context/delivery.mjs +1 -1
- package/src/context/snapshots.mjs +10 -2
- package/src/dream/organize.mjs +18 -3
- package/src/import/legacy.mjs +21 -2
- package/src/import/pointers.mjs +62 -20
- package/src/import/rollback.mjs +90 -37
- 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 +176 -0
- package/src/runtime/tools.mjs +21 -4
- package/src/runtime/ui-config.mjs +176 -0
- package/test/fixtures/p3/c1-baseline.json +90 -0
- package/test/p3-c1-dream.test.mjs +1 -1
- package/test/p4-p4-delivery.test.mjs +7 -1
- package/test/p6-b1-merge.test.mjs +60 -0
- package/test/p6-b2-pointers.test.mjs +77 -0
- 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-l6-rollback.test.mjs +38 -0
- package/test/p6-modules.test.mjs +77 -0
- package/test/p6-ui-summary.test.mjs +566 -0
package/README.md
CHANGED
|
@@ -10,9 +10,18 @@ node scripts/live-check.mjs --case tools # 真机:插件能否装进宿
|
|
|
10
10
|
|
|
11
11
|
已实测(隔离 profile,带 key):插件加载 → 10 个 `memsys_*` 工具注册进宿主 → 模型调用 `memsys_search` 返回真实命中 → 模型调用 `memsys_record` 写入候选与记忆,`origin.author_kind=model`、会话与调用 ID 与宿主记录一致。
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 安装
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**已发布到 npm**:`dsh-memsys@0.1.0`(https://www.npmjs.com/package/dsh-memsys)。
|
|
16
|
+
|
|
17
|
+
装进一个**独立 profile**(不要直接装进你日常用的那个):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh --profile memsys headless --from-default-profile --dump-config # 初始化隔离 profile
|
|
21
|
+
dsh plugin --profile memsys add dsh-memsys # 从 npm 装
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或者不依赖 npm,从源码装:
|
|
16
25
|
|
|
17
26
|
```bash
|
|
18
27
|
git clone https://github.com/tok-ryan/dsh-memsys.git dsh-memsys
|
|
@@ -20,10 +29,7 @@ cd dsh-memsys
|
|
|
20
29
|
npm test # 先跑一遍,确认本地环境能过(Node >= 22.13)
|
|
21
30
|
```
|
|
22
31
|
|
|
23
|
-
然后用一个**独立 profile** 装进去(不要直接装进你日常用的那个):
|
|
24
|
-
|
|
25
32
|
```bash
|
|
26
|
-
dsh --profile memsys headless --from-default-profile --dump-config # 初始化一个隔离 profile
|
|
27
33
|
dsh plugin --profile memsys add "file:///全/路径/dsh-memsys" # 按实际路径改
|
|
28
34
|
```
|
|
29
35
|
|
|
@@ -57,9 +63,30 @@ node scripts/live-check.mjs --case tools --profile memsys # 真机:插件
|
|
|
57
63
|
|
|
58
64
|
**设计原则**:事实只追加;当前状态是可重建投影;模型只能提交候选,不能直接落事实;一切写后读回按固化预期比对,不是"行还在就算成功"。
|
|
59
65
|
|
|
60
|
-
##
|
|
66
|
+
## 界面(0.2.0 起)
|
|
67
|
+
|
|
68
|
+
装上之后 Web GUI 里会多三样东西,都来自同一个浏览器半 `client.js`:
|
|
69
|
+
|
|
70
|
+
- **左侧栏「🐳 记忆」**:点开是一块面板——记忆条数 / 候选条数 / 待确认惦记数,加最近 5 条记忆正文(超长截断)
|
|
71
|
+
- **设置 →「记忆系统」**:`auto_inject` 开关 + 当前数据目录;开关改完**立即生效**(重挂/撤下 system prompt 里的记忆段),刷新后仍在
|
|
72
|
+
- 面板与设置页共用数据口 `/dsh-memsys/api/*`(loopback + 同源校验;跨源 403)
|
|
73
|
+
|
|
74
|
+
**设置优先级**:`<data_dir>/ui-config.json`(存在且合法)> profile 的 `config` > 默认全开。
|
|
75
|
+
`/api/summary` 会返回 `modules` / `modules_source`,告诉你每个开关的当前值和来源(`ui-config` / `profile` / `default`)。
|
|
76
|
+
|
|
77
|
+
**注意生效时机**:自动注入、写入工具**改完立即生效**;读取工具 / 做梦 / 项目 / 待确认惦记 / 信息流是**下次启动生效**(工具在启动时一次性注册,宿主没有注销接口)。
|
|
78
|
+
|
|
79
|
+
**headless 不受影响**:没有 `webServer` 时那段注册不激活,浏览器半也不加载。
|
|
80
|
+
|
|
81
|
+
## 状态(0.2.1)
|
|
82
|
+
|
|
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 项全绿
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
**仍不达标**:`L6` 只核到内容 hash 与关联,**没有逐项重放 relations 内容**;真实旧库尚未导入;生产 cutover 未跑;`feed` 生产 provider 未接。见文末"已知边界"。
|
|
63
90
|
|
|
64
91
|
MIT License。
|
|
65
92
|
|
package/client.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-memsys',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
const NS = 'dsh-memsys';
|
|
7
|
+
const API = '/dsh-memsys/api/summary';
|
|
8
|
+
const CONFIG = '/dsh-memsys/api/config';
|
|
9
|
+
|
|
10
|
+
function useSummary() {
|
|
11
|
+
const [state, setState] = React.useState(null);
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
let alive = true;
|
|
14
|
+
const load = () => {
|
|
15
|
+
fetch(API)
|
|
16
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(String(response.status)))))
|
|
17
|
+
.then((data) => alive && setState(data))
|
|
18
|
+
.catch(() => alive && setState({ error: true }));
|
|
19
|
+
};
|
|
20
|
+
load();
|
|
21
|
+
const timer = setInterval(load, 15000);
|
|
22
|
+
return () => {
|
|
23
|
+
alive = false;
|
|
24
|
+
clearInterval(timer);
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
return [state, setState];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function MemoryPanel() {
|
|
31
|
+
const [state] = useSummary();
|
|
32
|
+
if (!state) return h('p', { style: { padding: 16 } }, '读取记忆…');
|
|
33
|
+
if (state.error) return h('p', { style: { padding: 16 } }, '暂时读不到记忆数据。');
|
|
34
|
+
return h(
|
|
35
|
+
'div',
|
|
36
|
+
{ style: { padding: 16, lineHeight: 1.7 } },
|
|
37
|
+
h('h2', { style: { margin: '0 0 8px' } }, '记忆'),
|
|
38
|
+
h('p', null, `记忆 ${state.memories} 条 · 候选 ${state.candidates} 条 · 待确认惦记 ${state.pending_due} 条`),
|
|
39
|
+
h('h3', { style: { margin: '16px 0 6px' } }, '最近 5 条'),
|
|
40
|
+
state.recent.length === 0
|
|
41
|
+
? h('p', null, '还没有记忆。')
|
|
42
|
+
: h('ul', null, state.recent.map((memory) => h('li', { key: memory.id }, memory.text))),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function MemoryIcon() {
|
|
47
|
+
return h('span', { 'aria-hidden': true, role: 'img', title: '记忆' }, '🐳');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SOURCE_LABEL = { 'ui-config': '设置文件', profile: 'profile 配置', default: '默认值' };
|
|
51
|
+
|
|
52
|
+
// key, 名字, 一句人话, 生效时机(别糊成"立即生效",四个模块真的要重启)
|
|
53
|
+
const MODULE_ROWS = [
|
|
54
|
+
['context', '自动注入', '每轮把记忆摘要注入系统提示;关掉后只能手动查', '改完立即生效'],
|
|
55
|
+
['read', '读取工具', '关掉后查询类工具(get / search / review)不可用', '下次启动生效'],
|
|
56
|
+
['write', '写入工具', '关掉后不再记录新记忆,候选也收', '改完立即生效'],
|
|
57
|
+
['dream', '做梦', '关掉后不浮现、也不能记录梦', '下次启动生效'],
|
|
58
|
+
['projects', '项目', '关掉后项目视角不可用', '下次启动生效'],
|
|
59
|
+
['due', '待确认惦记', '关掉后不提醒到期的惦记,也不能确认或搁置', '下次启动生效'],
|
|
60
|
+
['feed', '信息流', '关掉后不产出信息流候选', '下次启动生效'],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function MemorySettings() {
|
|
64
|
+
const [state, setState] = React.useState(null);
|
|
65
|
+
const [note, setNote] = React.useState('');
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
fetch(API)
|
|
68
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(String(response.status)))))
|
|
69
|
+
.then(setState)
|
|
70
|
+
.catch(() => setState({ error: true }));
|
|
71
|
+
}, []);
|
|
72
|
+
const modules = state?.modules ?? {};
|
|
73
|
+
const save = (key, value) => {
|
|
74
|
+
const previous = modules[key] !== false;
|
|
75
|
+
setState((current) => ({ ...(current ?? {}), modules: { ...(current?.modules ?? {}), [key]: value } }));
|
|
76
|
+
setNote('保存中…');
|
|
77
|
+
fetch(CONFIG, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'content-type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ modules: { [key]: value } }),
|
|
81
|
+
})
|
|
82
|
+
.then((response) =>
|
|
83
|
+
response.ok
|
|
84
|
+
? response.json().then((body) => setState((current) => ({ ...(current ?? {}), ...body })))
|
|
85
|
+
: Promise.reject(new Error(String(response.status))),
|
|
86
|
+
)
|
|
87
|
+
.then(() => setNote('已保存'))
|
|
88
|
+
.catch(() => {
|
|
89
|
+
setState((current) => ({ ...(current ?? {}), modules: { ...(current?.modules ?? {}), [key]: previous } }));
|
|
90
|
+
setNote('保存失败');
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const disabled = !state || state.error === true;
|
|
94
|
+
return h(
|
|
95
|
+
'div',
|
|
96
|
+
{ style: { padding: '8px 0', lineHeight: 1.7 } },
|
|
97
|
+
h('p', { style: { margin: '0 0 4px', opacity: 0.75 } }, '按模块开关,默认全开。关掉哪一项,那一项就不再工作。'),
|
|
98
|
+
...MODULE_ROWS.map(([key, label, hint, timing]) =>
|
|
99
|
+
h(
|
|
100
|
+
'label',
|
|
101
|
+
{ key, style: { display: 'block', margin: '6px 0' } },
|
|
102
|
+
h('span', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
103
|
+
h('input', {
|
|
104
|
+
type: 'checkbox',
|
|
105
|
+
checked: modules[key] !== false,
|
|
106
|
+
disabled,
|
|
107
|
+
onChange: (event) => save(key, event.target.checked),
|
|
108
|
+
}),
|
|
109
|
+
label,
|
|
110
|
+
),
|
|
111
|
+
h('span', { style: { display: 'block', marginLeft: 24, opacity: 0.7, fontSize: '0.92em' } },
|
|
112
|
+
`${hint}(${timing})`),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
h('p', { style: { opacity: 0.75, margin: '10px 0 0' } }, `数据目录:${state?.data_dir ?? '未配置'}`),
|
|
116
|
+
h('p', { style: { opacity: 0.75, margin: '4px 0 0' } },
|
|
117
|
+
`当前生效值来源:${SOURCE_LABEL[state?.modules_source] ?? state?.modules_source ?? '未知'}`),
|
|
118
|
+
note ? h('p', { style: { opacity: 0.75, margin: '4px 0 0' } }, note) : null,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
inject: ['slots'],
|
|
124
|
+
apply(ctx) {
|
|
125
|
+
ctx.slots.inject('main', () =>
|
|
126
|
+
ctx.slots.register({ name: 'main', key: NS }, MemoryPanel),
|
|
127
|
+
);
|
|
128
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
129
|
+
ctx.slots.register({ name: 'sidebar.panellist', id: NS, order: 30, label: () => '记忆' }, MemoryIcon),
|
|
130
|
+
);
|
|
131
|
+
ctx.slots.inject('settings.section', () =>
|
|
132
|
+
ctx.slots.register({ name: 'settings.section', id: NS, order: 40, label: () => '记忆系统' }, MemorySettings),
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
package/index.mjs
CHANGED
|
@@ -12,13 +12,81 @@ 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 } 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';
|
|
15
19
|
|
|
16
20
|
export const name = 'dsh-memsys';
|
|
17
21
|
/** tools is a real host service; static inject is the verified registration path. */
|
|
18
22
|
export const inject = ['tools'];
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
/**
|
|
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.
|
|
30
|
+
* The file wins so a toggle survives a restart without rewriting the profile,
|
|
31
|
+
* and the in-memory map is applied immediately (no restart).
|
|
32
|
+
*/
|
|
33
|
+
function createUiState(config = {}, logger = console) {
|
|
21
34
|
const dataDir = config.data_dir ?? process.env.MEMSYS_DATA_DIR ?? null;
|
|
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;
|
|
40
|
+
return {
|
|
41
|
+
dataDir,
|
|
42
|
+
get modules() {
|
|
43
|
+
return modules;
|
|
44
|
+
},
|
|
45
|
+
get source() {
|
|
46
|
+
return source;
|
|
47
|
+
},
|
|
48
|
+
get autoInject() {
|
|
49
|
+
return modules.context !== false;
|
|
50
|
+
},
|
|
51
|
+
apply(written) {
|
|
52
|
+
Object.assign(modules, written.modules);
|
|
53
|
+
source = 'ui-config';
|
|
54
|
+
},
|
|
55
|
+
sync: () => {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createHostHandlers(config = {}, ctx = null, ui = null) {
|
|
60
|
+
const dataDir = config.data_dir ?? process.env.MEMSYS_DATA_DIR ?? null;
|
|
61
|
+
const uiState = ui ?? {
|
|
62
|
+
get modules() {
|
|
63
|
+
return mergeModules(config, null);
|
|
64
|
+
},
|
|
65
|
+
get source() {
|
|
66
|
+
return typeof config.auto_inject === 'boolean' || config.modules ? 'profile' : 'default';
|
|
67
|
+
},
|
|
68
|
+
get autoInject() {
|
|
69
|
+
return mergeModules(config, null).context !== false;
|
|
70
|
+
},
|
|
71
|
+
apply() {},
|
|
72
|
+
sync: () => {},
|
|
73
|
+
};
|
|
74
|
+
const uiSummary = createSummaryHandler({
|
|
75
|
+
main: () => db().main,
|
|
76
|
+
dataDir,
|
|
77
|
+
getConfig: () => ({ modules: uiState.modules, source: uiState.source }),
|
|
78
|
+
onConfig: (patch) => {
|
|
79
|
+
if (!dataDir) {
|
|
80
|
+
const err = new Error('data_dir_missing');
|
|
81
|
+
err.code = 'data_dir_missing';
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
const written = writeUiConfig(dataDir, patch);
|
|
85
|
+
uiState.apply(written);
|
|
86
|
+
uiState.sync();
|
|
87
|
+
return { auto_inject: written.auto_inject, modules: written.modules };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
22
90
|
let handles = null;
|
|
23
91
|
const db = () => {
|
|
24
92
|
if (!dataDir) {
|
|
@@ -67,6 +135,7 @@ function createHostHandlers(config = {}, ctx = null) {
|
|
|
67
135
|
memsys_project_get: (args) => createProjectHandler({ main: db().main, clock: systemClock }).get(args),
|
|
68
136
|
memsys_self_get: (args) => createSelfHandler({ main: db().main, clock: systemClock }).get(args),
|
|
69
137
|
memsys_feed: (args) => createFeedHandler({ clock: systemClock }).feed(args),
|
|
138
|
+
memsys_ui_summary: (req, res) => uiSummary(req, res),
|
|
70
139
|
memsys_record: (args, exec) => {
|
|
71
140
|
const identity = runtimeFromExec({ exec, ctx, tool: 'record', args });
|
|
72
141
|
if (!identity.ok) return { status: 'error', code: identity.code, ...identity };
|
|
@@ -94,8 +163,39 @@ function createHostHandlers(config = {}, ctx = null) {
|
|
|
94
163
|
* section exists only for the isolated probe profile.
|
|
95
164
|
*/
|
|
96
165
|
export async function apply(ctx, config = {}) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
166
|
+
const ui = createUiState(config);
|
|
167
|
+
const handlers = createHostHandlers(config, ctx, ui);
|
|
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();
|
|
99
199
|
const toolProbe = process.env.MEMSYS_TOOL_PROBE ?? null;
|
|
100
200
|
const writeToolProbe = (deferred) => {
|
|
101
201
|
if (!toolProbe) return;
|
|
@@ -235,21 +335,62 @@ export async function apply(ctx, config = {}) {
|
|
|
235
335
|
write();
|
|
236
336
|
}
|
|
237
337
|
|
|
238
|
-
|
|
338
|
+
if (typeof ctx?.inject === 'function') {
|
|
339
|
+
try {
|
|
340
|
+
ctx.inject(['webServer'], (web) => {
|
|
341
|
+
web.effect(
|
|
342
|
+
() =>
|
|
343
|
+
web.webServer.register({
|
|
344
|
+
kind: 'exact',
|
|
345
|
+
path: '/dsh-memsys/api/summary',
|
|
346
|
+
handler: (req, res) => handlers.memsys_ui_summary(req, res),
|
|
347
|
+
}),
|
|
348
|
+
'memsys: ui summary route',
|
|
349
|
+
);
|
|
350
|
+
web.effect(
|
|
351
|
+
() =>
|
|
352
|
+
web.webServer.register({
|
|
353
|
+
kind: 'exact',
|
|
354
|
+
path: '/dsh-memsys/api/config',
|
|
355
|
+
handler: (req, res) => handlers.memsys_ui_summary(req, res),
|
|
356
|
+
}),
|
|
357
|
+
'memsys: ui config route',
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
} catch {
|
|
361
|
+
// profiles without webServer stay headless
|
|
362
|
+
}
|
|
363
|
+
}
|
|
239
364
|
const diagnostic = config.diagnostic_section === true;
|
|
240
365
|
let disposeSection = () => {};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
366
|
+
const syncSection = () => {
|
|
367
|
+
disposeSection();
|
|
368
|
+
disposeSection = () => {};
|
|
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;
|
|
246
372
|
disposeSection = registerSection(ctx, {
|
|
247
|
-
getText
|
|
373
|
+
getText: () =>
|
|
374
|
+
ui.modules.context === false
|
|
375
|
+
? '[memsys] diagnostic section(context=false)'
|
|
376
|
+
: (injector?.current().text ?? ''),
|
|
377
|
+
dynamicText: true,
|
|
248
378
|
diagnosticMarker: process.env.MEMSYS_SECTION_PROBE ?? null,
|
|
249
379
|
});
|
|
250
|
-
}
|
|
380
|
+
};
|
|
381
|
+
// A profile without systemPrompt (headless/embedded) must still boot the tools.
|
|
382
|
+
ui.sync = () => {
|
|
383
|
+
try {
|
|
384
|
+
syncInjector();
|
|
385
|
+
syncSection();
|
|
386
|
+
} catch {
|
|
387
|
+
// keep the host half alive; the section is optional
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
ui.sync();
|
|
251
391
|
return () => {
|
|
252
392
|
disposeSection();
|
|
393
|
+
injector?.dispose?.();
|
|
253
394
|
};
|
|
254
395
|
}
|
|
255
396
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memsys",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
|
@@ -28,15 +28,22 @@
|
|
|
28
28
|
"node": ">=22.13"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
|
-
".": "./index.mjs"
|
|
31
|
+
".": "./index.mjs",
|
|
32
|
+
"./client": "./client.js"
|
|
32
33
|
},
|
|
33
34
|
"dsh": {
|
|
34
35
|
"bundle": {
|
|
35
36
|
"patch": "./cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"client": {
|
|
39
|
+
"platform": "web",
|
|
40
|
+
"immediately": true,
|
|
41
|
+
"inject": []
|
|
36
42
|
}
|
|
37
43
|
},
|
|
38
44
|
"files": [
|
|
39
45
|
"index.mjs",
|
|
46
|
+
"client.js",
|
|
40
47
|
"cordis.patch.yml",
|
|
41
48
|
"src",
|
|
42
49
|
"scripts",
|
package/scripts/build.mjs
CHANGED
|
@@ -12,7 +12,8 @@ function walk(dir, out = []) {
|
|
|
12
12
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
13
13
|
const full = path.join(dir, entry.name);
|
|
14
14
|
if (entry.isDirectory()) walk(full, out);
|
|
15
|
-
else if (entry.name.endsWith('.mjs')) out.push(full);
|
|
15
|
+
else if (entry.name.endsWith('.mjs') || (entry.name.endsWith('.js') && entry.name !== 'client.js')) out.push(full);
|
|
16
|
+
else if (entry.name === 'client.js') out.push(full);
|
|
16
17
|
}
|
|
17
18
|
return out;
|
|
18
19
|
}
|
|
@@ -178,8 +178,10 @@ async function main() {
|
|
|
178
178
|
required(deploymentDir, 'USAGE: activate --plan <plan.json> --data-dir <dir> --restore-report <report.json> --deployment-dir <dir>');
|
|
179
179
|
const activateTarget = openTarget(dataDir);
|
|
180
180
|
let businessRows = null;
|
|
181
|
+
let databaseFreeze = null;
|
|
181
182
|
try {
|
|
182
183
|
businessRows = rollback.snapshotBusinessRows(activateTarget.handles.main);
|
|
184
|
+
databaseFreeze = rollback.snapshotDatabaseFreeze(activateTarget.handles);
|
|
183
185
|
} finally {
|
|
184
186
|
closeAll(activateTarget.handles);
|
|
185
187
|
}
|
|
@@ -193,6 +195,7 @@ async function main() {
|
|
|
193
195
|
restoreBatch: restore.selected_batch,
|
|
194
196
|
business_counts: businessRows?.counts ?? null,
|
|
195
197
|
business_rows: businessRows,
|
|
198
|
+
db_freeze: databaseFreeze,
|
|
196
199
|
activatedAt: systemClock.at().at,
|
|
197
200
|
}, null, 2)}\n`,
|
|
198
201
|
'utf8',
|
|
@@ -225,6 +228,7 @@ async function main() {
|
|
|
225
228
|
}
|
|
226
229
|
const restored = openAll(resolveConfig({ dataDir: restoredDir }));
|
|
227
230
|
let comparison;
|
|
231
|
+
let freezeComparison = { ok: true, missing: [] };
|
|
228
232
|
try {
|
|
229
233
|
comparison = config.business_rows
|
|
230
234
|
? rollback.compareBusinessRows({ db: restored.main, baseline: config.business_rows })
|
|
@@ -232,11 +236,17 @@ async function main() {
|
|
|
232
236
|
db: restored.main,
|
|
233
237
|
expected: config.business_counts ?? {},
|
|
234
238
|
});
|
|
239
|
+
if (config.db_freeze) {
|
|
240
|
+
freezeComparison = rollback.compareDatabaseFreeze(
|
|
241
|
+
{ main: restored.main, control: restored.control, dream: restored.dream },
|
|
242
|
+
config.db_freeze,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
235
245
|
} finally {
|
|
236
246
|
closeAll(restored);
|
|
237
247
|
}
|
|
238
|
-
if (!comparison.ok) {
|
|
239
|
-
console.log(`ROLLBACK_CHECK_FAILED business_missing=${comparison.missing.join(',')}`);
|
|
248
|
+
if (!comparison.ok || !freezeComparison.ok) {
|
|
249
|
+
console.log(`ROLLBACK_CHECK_FAILED business_missing=${comparison.missing.join(',')} freeze_missing=${freezeComparison.missing.join(',')}`);
|
|
240
250
|
process.exitCode = 1;
|
|
241
251
|
return;
|
|
242
252
|
}
|
package/scripts/live-check.mjs
CHANGED
|
@@ -125,7 +125,7 @@ record('profile_prepared', dumpBefore.status === 0, {
|
|
|
125
125
|
// 2. Stage an ASCII copy; the live source tree is never linked into a profile.
|
|
126
126
|
fs.rmSync(STAGE, { recursive: true, force: true });
|
|
127
127
|
fs.mkdirSync(STAGE, { recursive: true });
|
|
128
|
-
for (const name of ['package.json', 'index.mjs', 'cordis.patch.yml']) {
|
|
128
|
+
for (const name of ['package.json', 'index.mjs', 'client.js', 'cordis.patch.yml']) {
|
|
129
129
|
fs.copyFileSync(path.join(ROOT, name), path.join(STAGE, name));
|
|
130
130
|
}
|
|
131
131
|
fs.cpSync(path.join(ROOT, 'src'), path.join(STAGE, 'src'), { recursive: true });
|
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/dream/organize.mjs
CHANGED
|
@@ -37,6 +37,19 @@ function dueDeadline(main, memoryId) {
|
|
|
37
37
|
return queue?.deadline_at ?? null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Merge may not silently erase due semantics; anything due-bearing is refused. */
|
|
41
|
+
function assertMergeAdmissible(main, memory) {
|
|
42
|
+
if (['open', 'pending-due', 'pending-done', 'cooled-due', 'shelved-due', 'done', 'dropped'].includes(memory.due_state)) {
|
|
43
|
+
throw new WriteError('blocked_by_due_state', { memoryId: memory.id, dueState: memory.due_state });
|
|
44
|
+
}
|
|
45
|
+
if (memory.type === 'promise') {
|
|
46
|
+
throw new WriteError('blocked_by_due_state', { memoryId: memory.id, reason: 'promise' });
|
|
47
|
+
}
|
|
48
|
+
if (memory.due_at || dueDeadline(main, memory.id)) {
|
|
49
|
+
throw new WriteError('blocked_by_due_state', { memoryId: memory.id, reason: 'deadline' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Mechanical exact merge only: no similarity, no NFKC, no negation stripping.
|
|
42
55
|
* Endpoints must be active, resolved and accepted; dream-linked rows only yield a
|
|
@@ -58,9 +71,7 @@ export function mergeExact(deps, { runtime, ids }) {
|
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
for (const memory of memories) {
|
|
61
|
-
|
|
62
|
-
throw new WriteError('blocked_by_due_state', { memoryId: memory.id, dueState: memory.due_state });
|
|
63
|
-
}
|
|
74
|
+
assertMergeAdmissible(main, memory);
|
|
64
75
|
if (memory.key_status !== 'resolved') throw new WriteError('unresolved_forbidden', { memoryId: memory.id });
|
|
65
76
|
if (memory.binding_status !== 'accepted') throw new WriteError('binding_not_accepted', { memoryId: memory.id });
|
|
66
77
|
}
|
|
@@ -163,6 +174,10 @@ export function mergeExact(deps, { runtime, ids }) {
|
|
|
163
174
|
};
|
|
164
175
|
let returnStorage = null;
|
|
165
176
|
withImmediate(main, () => {
|
|
177
|
+
for (const id of unique) {
|
|
178
|
+
const fresh = getMemory(main, id);
|
|
179
|
+
assertMergeAdmissible(main, fresh);
|
|
180
|
+
}
|
|
166
181
|
insertObservation(main, {
|
|
167
182
|
id: observationId,
|
|
168
183
|
idempotency_key: `obs:merge:${mergeKey}`,
|
package/src/import/legacy.mjs
CHANGED
|
@@ -481,8 +481,27 @@ export function verifyPlan(deps, plan) {
|
|
|
481
481
|
continue;
|
|
482
482
|
}
|
|
483
483
|
report.items += 1;
|
|
484
|
-
if (item.itemKind === 'graph_node')
|
|
485
|
-
|
|
484
|
+
if (item.itemKind === 'graph_node') {
|
|
485
|
+
report.graph_nodes += 1;
|
|
486
|
+
const node = main.prepare('SELECT * FROM graph_nodes WHERE id = ?').get(item.payload.id);
|
|
487
|
+
if (!node) report.missing.push(`${item.itemId}:graph_node`);
|
|
488
|
+
else if (node.label !== item.payload.label || node.kind !== item.payload.kind || node.source_ref !== `node:${item.payload.id}`) {
|
|
489
|
+
report.missing.push(`${item.itemId}:graph_node_content`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (item.itemKind === 'graph_edge') {
|
|
493
|
+
report.graph_edges += 1;
|
|
494
|
+
const edge = main.prepare('SELECT * FROM graph_edges WHERE id = ?').get(item.payload.id);
|
|
495
|
+
if (!edge) report.missing.push(`${item.itemId}:graph_edge`);
|
|
496
|
+
else if (
|
|
497
|
+
edge.from_id !== item.payload.from ||
|
|
498
|
+
edge.to_id !== item.payload.to ||
|
|
499
|
+
edge.kind !== item.payload.kind ||
|
|
500
|
+
edge.source_ref !== `edge:${item.payload.id}`
|
|
501
|
+
) {
|
|
502
|
+
report.missing.push(`${item.itemId}:graph_edge_content`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
486
505
|
if (item.itemKind === 'memory') {
|
|
487
506
|
const targets = JSON.parse(row.target_ids);
|
|
488
507
|
const observation = targets[0] ? getObservation(main, targets[0]) : null;
|