@stardustlc/dsh-dream 0.3.1 → 0.3.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.en.md +6 -2
- package/README.md +5 -3
- package/lib/config.d.ts +1 -1
- package/lib/config.js +4 -4
- package/lib/sessions.js +80 -29
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
[简体中文](README.md)
|
|
2
2
|
|
|
3
|
+
   
|
|
4
|
+
|
|
3
5
|
# dsh-dream
|
|
4
6
|
|
|
5
7
|
> **An agent that dreams**: session replay (dream material) → reflection (interpretation) → dream journal (memory consolidation).
|
|
@@ -8,7 +10,9 @@ Humans consolidate memories by replaying the day during sleep — dsh-dream give
|
|
|
8
10
|
|
|
9
11
|
## Compatibility
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Adapted to the session contracts in `@deepseek-ai/dsh@0.1.3-alpha.1`. Follows the cordis patch-bundle model (`cordis.patch.yml` + `dsh.bundle.patch`), with no runtime imports of `@deepseek-ai/*` internals. Reads v0/v1/v2 plaintext JSONL and multi-frame zstd, legacy packed chunks, and v2 embedded streams. When migrations retain older generations, only the newest canonical file is selected per session.
|
|
14
|
+
|
|
15
|
+
Default data directories follow `DSH_HOME`: `sessions` for session logs and `.dsh-dream` for the journal. Without `DSH_HOME`, the base is `~/.dsh`. Explicit `sessionsRoot` and `journalDir` settings take precedence.
|
|
12
16
|
|
|
13
17
|
## Install / Uninstall
|
|
14
18
|
|
|
@@ -48,7 +52,7 @@ Read-only access to the sessions directory; appends JSONL to the journal directo
|
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
pnpm install
|
|
51
|
-
pnpm test # build +
|
|
55
|
+
pnpm test # build + offline tests (session generations, zstd, streams, masking, journal, bridge, registration)
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
## License
|
package/README.md
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
> **会做梦的 agent**:会话回放(梦原料)→ 反思(解梦)→ 梦境日记(记忆巩固)。
|
|
6
6
|
|
|
7
|
-
 
|
|
7
|
+
   
|
|
8
8
|
|
|
9
9
|
人睡觉时大脑回放白天的经历、巩固记忆——dsh-dream 让 DeepSeek Harness 的 agent 也拥有这个能力:读取你的历史会话(官方多帧 zstd 会话日志,零依赖解析),提炼梦原料,反思后写入永久梦境日记,下次醒来可以忆梦。
|
|
10
10
|
|
|
11
11
|
## 兼容性
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
按 `@deepseek-ai/dsh@0.1.3-alpha.1` 的会话接口适配。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。支持 v0/v1/v2 的普通 JSONL 与多帧 zstd、旧 packed-chunk 行和 v2 内嵌 stream。升级留下多个代际文件时,每个会话只读取最新规范文件。
|
|
14
|
+
|
|
15
|
+
默认从 `DSH_HOME/sessions` 读取会话、在 `DSH_HOME/.dsh-dream` 保存日记;未设置 `DSH_HOME` 时使用 `~/.dsh`。配置中的 `sessionsRoot`、`journalDir` 始终优先。
|
|
14
16
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
@@ -95,7 +97,7 @@ dream_bridge { path: "AGENTS.md", maxLessons: 10 }
|
|
|
95
97
|
|
|
96
98
|
```bash
|
|
97
99
|
pnpm install
|
|
98
|
-
pnpm test # 构建 +
|
|
100
|
+
pnpm test # 构建 + 离线测试(会话代际、多帧 zstd、流式记录、隐私脱敏、日记、桥接与注册生命周期)
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
## License
|
package/lib/config.d.ts
CHANGED
|
@@ -14,4 +14,4 @@ export interface ResolvedDreamConfig {
|
|
|
14
14
|
maskSecrets: boolean;
|
|
15
15
|
}
|
|
16
16
|
/** 解析并校验插件配置。 */
|
|
17
|
-
export declare function resolveConfig(raw?: Record<string, unknown> | null): ResolvedDreamConfig;
|
|
17
|
+
export declare function resolveConfig(raw?: Record<string, unknown> | null, env?: NodeJS.ProcessEnv): ResolvedDreamConfig;
|
package/lib/config.js
CHANGED
|
@@ -14,12 +14,12 @@ function clamp(value, fallback, min, max) {
|
|
|
14
14
|
return Math.min(max, Math.max(min, Math.round(value)));
|
|
15
15
|
}
|
|
16
16
|
/** 解析并校验插件配置。 */
|
|
17
|
-
export function resolveConfig(raw) {
|
|
17
|
+
export function resolveConfig(raw, env = process.env) {
|
|
18
18
|
const cfg = raw ?? {};
|
|
19
|
-
const
|
|
19
|
+
const dshHome = str(env.DSH_HOME, join(homedir(), '.dsh'));
|
|
20
20
|
return {
|
|
21
|
-
sessionsRoot: str(cfg.sessionsRoot, join(
|
|
22
|
-
journalDir: str(cfg.journalDir, join(
|
|
21
|
+
sessionsRoot: str(cfg.sessionsRoot, join(dshHome, 'sessions')),
|
|
22
|
+
journalDir: str(cfg.journalDir, join(dshHome, '.dsh-dream')),
|
|
23
23
|
maxSessions: clamp(cfg.maxSessions, 10, 1, 50),
|
|
24
24
|
maxCharsPerSession: clamp(cfg.maxCharsPerSession, 6000, 500, 50000),
|
|
25
25
|
maxUserMessages: clamp(cfg.maxUserMessages, 5, 1, 20),
|
package/lib/sessions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DSH
|
|
2
|
+
* DSH 会话日志读取:兼容 v0/v1/v2 的 JSONL 与多帧 zstd。
|
|
3
3
|
* 首个逻辑行是会话头({ type: 'session', ... }),其后每行一条存储记录({ type, seq, time, data })。
|
|
4
4
|
* 本模块只读不写;损坏帧/行一律容错跳过。
|
|
5
5
|
*
|
|
@@ -78,7 +78,8 @@ export function digestSessionFile(filePath, maxUserMessages) {
|
|
|
78
78
|
return null;
|
|
79
79
|
let text;
|
|
80
80
|
try {
|
|
81
|
-
|
|
81
|
+
const bytes = readFileSync(filePath);
|
|
82
|
+
text = filePath.endsWith('.zstd') ? decompressAll(bytes) : bytes.toString('utf8');
|
|
82
83
|
}
|
|
83
84
|
catch {
|
|
84
85
|
return null;
|
|
@@ -91,6 +92,8 @@ export function digestSessionFile(filePath, maxUserMessages) {
|
|
|
91
92
|
const first = JSON.parse(lines[0]);
|
|
92
93
|
if (first.type !== 'session')
|
|
93
94
|
return null;
|
|
95
|
+
if (first.version !== undefined && ![0, 1, 2].includes(first.version))
|
|
96
|
+
return null;
|
|
94
97
|
header = {
|
|
95
98
|
id: String(first.id ?? ''),
|
|
96
99
|
createdAt: typeof first.createdAt === 'number' ? first.createdAt : 0,
|
|
@@ -113,6 +116,31 @@ export function digestSessionFile(filePath, maxUserMessages) {
|
|
|
113
116
|
streamTail: '',
|
|
114
117
|
endedAt: null,
|
|
115
118
|
};
|
|
119
|
+
const appendStreamText = (text) => {
|
|
120
|
+
digest.streamTail = (digest.streamTail + text).slice(-4000);
|
|
121
|
+
};
|
|
122
|
+
const appendToolName = (name) => {
|
|
123
|
+
if (typeof name === 'string' && name !== '' && digest.toolCalls.length < 200)
|
|
124
|
+
digest.toolCalls.push(name);
|
|
125
|
+
};
|
|
126
|
+
const readStreamRecord = (value) => {
|
|
127
|
+
if (typeof value !== 'object' || value === null)
|
|
128
|
+
return;
|
|
129
|
+
const record = value;
|
|
130
|
+
if (record.type === 'text-chunks' && Array.isArray(record.texts)) {
|
|
131
|
+
appendStreamText(record.texts.filter((part) => typeof part === 'string').join(''));
|
|
132
|
+
}
|
|
133
|
+
else if (record.type === 'tool-call-chunks') {
|
|
134
|
+
appendToolName(record.name);
|
|
135
|
+
}
|
|
136
|
+
else if (record.type === 'chunk' && typeof record.chunk === 'object' && record.chunk !== null) {
|
|
137
|
+
const chunk = record.chunk;
|
|
138
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string')
|
|
139
|
+
appendStreamText(chunk.text);
|
|
140
|
+
if (chunk.type === 'tool-call-start')
|
|
141
|
+
appendToolName(chunk.name);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
116
144
|
for (const line of lines.slice(1)) {
|
|
117
145
|
let rec;
|
|
118
146
|
try {
|
|
@@ -121,11 +149,20 @@ export function digestSessionFile(filePath, maxUserMessages) {
|
|
|
121
149
|
catch {
|
|
122
150
|
continue;
|
|
123
151
|
}
|
|
152
|
+
if (typeof rec !== 'object' || rec === null || Array.isArray(rec))
|
|
153
|
+
continue;
|
|
124
154
|
const type = String(rec.type ?? '');
|
|
125
155
|
const data = rec.data;
|
|
126
|
-
const time = typeof rec.time === 'number' ? rec.time : null;
|
|
156
|
+
const time = typeof rec.time === 'number' ? rec.time : typeof rec.time0 === 'number' ? rec.time0 : null;
|
|
127
157
|
if (time !== null && (digest.endedAt === null || time > digest.endedAt))
|
|
128
158
|
digest.endedAt = time;
|
|
159
|
+
// v2 把流式片段内嵌进 message/attempt;v0 的独立打包行继续兼容。
|
|
160
|
+
if ((type === 'assistant/message' || type === 'assistant/attempt') && typeof data === 'object' && data !== null) {
|
|
161
|
+
const stream = data.stream;
|
|
162
|
+
if (Array.isArray(stream))
|
|
163
|
+
for (const record of stream)
|
|
164
|
+
readStreamRecord(record);
|
|
165
|
+
}
|
|
129
166
|
if (type === 'session/title') {
|
|
130
167
|
digest.title = textOf(data);
|
|
131
168
|
}
|
|
@@ -146,24 +183,15 @@ export function digestSessionFile(filePath, maxUserMessages) {
|
|
|
146
183
|
}
|
|
147
184
|
}
|
|
148
185
|
else if (type === 'tool/call') {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
digest.toolCalls.push(name);
|
|
152
|
-
}
|
|
153
|
-
else if (type === 'text-chunks') {
|
|
154
|
-
// 官方打包行:{ data: { texts: string[], ... } },还原流式正文
|
|
155
|
-
const texts = typeof data === 'object' && data !== null ? data.texts : undefined;
|
|
156
|
-
if (Array.isArray(texts)) {
|
|
157
|
-
digest.streamTail += texts.filter((t) => typeof t === 'string').join('');
|
|
158
|
-
if (digest.streamTail.length > 4000)
|
|
159
|
-
digest.streamTail = digest.streamTail.slice(-4000);
|
|
160
|
-
}
|
|
186
|
+
if (typeof data === 'object' && data !== null)
|
|
187
|
+
appendToolName(data.name);
|
|
161
188
|
}
|
|
162
|
-
else if (type === 'tool-call-chunks') {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
else if (type === 'text-chunks' || type === 'tool-call-chunks') {
|
|
190
|
+
if (typeof data === 'object' && data !== null)
|
|
191
|
+
readStreamRecord({ ...data, type });
|
|
192
|
+
}
|
|
193
|
+
else if (type === 'assistant/chunk' && typeof data === 'object' && data !== null) {
|
|
194
|
+
readStreamRecord({ type: 'chunk', chunk: data.chunk });
|
|
167
195
|
}
|
|
168
196
|
}
|
|
169
197
|
digest.userMessages = digest.userMessages.slice(0, maxUserMessages);
|
|
@@ -174,7 +202,15 @@ export function listSessionFiles(sessionsRoot, limit) {
|
|
|
174
202
|
if (!existsSync(sessionsRoot))
|
|
175
203
|
return [];
|
|
176
204
|
const out = [];
|
|
177
|
-
|
|
205
|
+
const listDirectory = (path) => {
|
|
206
|
+
try {
|
|
207
|
+
return readdirSync(path);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
for (const project of listDirectory(sessionsRoot)) {
|
|
178
214
|
const projectDir = join(sessionsRoot, project);
|
|
179
215
|
try {
|
|
180
216
|
if (!statSync(projectDir).isDirectory())
|
|
@@ -183,15 +219,30 @@ export function listSessionFiles(sessionsRoot, limit) {
|
|
|
183
219
|
catch {
|
|
184
220
|
continue;
|
|
185
221
|
}
|
|
186
|
-
for (const session of
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
for (const session of listDirectory(projectDir)) {
|
|
223
|
+
const sessionDir = join(projectDir, session);
|
|
224
|
+
// 迁移会保留旧代文件;每个会话只读最新已提交的规范文件,避免重复或读旧副本。
|
|
225
|
+
const candidates = listDirectory(sessionDir).flatMap((name) => {
|
|
226
|
+
const match = /^session(?:\.v([1-9][0-9]*))?\.jsonl(?:\.zstd)?$/.exec(name);
|
|
227
|
+
if (match === null)
|
|
228
|
+
return [];
|
|
229
|
+
const version = match[1] === undefined ? 0 : Number(match[1]);
|
|
230
|
+
if (!Number.isSafeInteger(version))
|
|
231
|
+
return [];
|
|
232
|
+
const file = join(sessionDir, name);
|
|
233
|
+
try {
|
|
234
|
+
const stat = statSync(file);
|
|
235
|
+
return stat.isFile() ? [{ file, version, mtime: stat.mtimeMs }] : [];
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}).sort((a, b) => b.version - a.version || Number(b.file.endsWith('.zstd')) - Number(a.file.endsWith('.zstd')));
|
|
241
|
+
if (candidates[0] !== undefined)
|
|
242
|
+
out.push(candidates[0]);
|
|
193
243
|
}
|
|
194
244
|
}
|
|
195
245
|
out.sort((a, b) => b.mtime - a.mtime);
|
|
196
|
-
|
|
246
|
+
const capped = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
|
|
247
|
+
return out.slice(0, capped).map((item) => item.file);
|
|
197
248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardustlc/dsh-dream",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "DSH 做梦插件:agent 在会话间隙回放近期对话(dream_digest)、反思并写入梦境日记(dream_save/journal/recall),高频教训幂等桥接进 AGENTS.md(dream_bridge),六工具含健康自检。多帧 zstd 会话读取,默认隐私脱敏,零运行时依赖。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|