@stardustlc/dsh-dream 0.3.1 → 0.3.3

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  [简体中文](README.md)
2
2
 
3
+ ![npm](https://img.shields.io/npm/v/@stardustlc/dsh-dream) ![downloads](https://img.shields.io/npm/dm/@stardustlc/dsh-dream) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-dream) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-dream?style=social)
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,11 @@ Humans consolidate memories by replaying the day during sleep — dsh-dream give
8
10
 
9
11
  ## Compatibility
10
12
 
11
- Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` (2026-08-31). Follows the cordis patch-bundle model (`cordis.patch.yml` + `dsh.bundle.patch`); no runtime imports of `@deepseek-ai/*` internals. Session reading is layout-blind with respect to the official JSONL-zstd multi-frame format and packed-chunk rows.
13
+ Verified with official `@deepseek-ai/dsh@0.1.5-rc.1` and Node `24.16.0` on 2026-09-11: all 18 components load alongside Modlens, with passing tool-schema, skill-registration and offline read-only invocation checks. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements match this Harness release: 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
14
+
15
+ Reads v0/v1/v2/v3 plaintext JSONL and multi-frame zstd, legacy packed chunks and v2/v3 embedded streams. Only the newest canonical file is selected per session. PTC child calls retain tool names without double counting; system prompts, reasoning, tool arguments and result bodies are excluded.
16
+
17
+ 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
18
 
13
19
  ## Install / Uninstall
14
20
 
@@ -48,7 +54,7 @@ Read-only access to the sessions directory; appends JSONL to the journal directo
48
54
 
49
55
  ```bash
50
56
  pnpm install
51
- pnpm test # build + 48 tests (multi-frame zstd fixtures, packed-chunk decoding, privacy masking, journal round-trip, bridge idempotency, stats, six tools, registration lifecycle)
57
+ pnpm test # build + offline tests (session generations, zstd, streams, masking, journal, bridge, registration)
52
58
  ```
53
59
 
54
60
  ## License
package/README.md CHANGED
@@ -4,13 +4,17 @@
4
4
 
5
5
  > **会做梦的 agent**:会话回放(梦原料)→ 反思(解梦)→ 梦境日记(记忆巩固)。
6
6
 
7
- ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-dream) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-dream?style=social)
7
+ ![npm](https://img.shields.io/npm/v/@stardustlc/dsh-dream) ![downloads](https://img.shields.io/npm/dm/@stardustlc/dsh-dream) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-dream) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-dream?style=social)
8
8
 
9
9
  人睡觉时大脑回放白天的经历、巩固记忆——dsh-dream 让 DeepSeek Harness 的 agent 也拥有这个能力:读取你的历史会话(官方多帧 zstd 会话日志,零依赖解析),提炼梦原料,反思后写入永久梦境日记,下次醒来可以忆梦。
10
10
 
11
11
  ## 兼容性
12
12
 
13
- 在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。会话读取兼容官方 JSONL-zstd 多帧格式与 packed-chunk 行(只取所需事件类型,布局无关)。
13
+ 已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
14
+
15
+ 支持 v0/v1/v2/v3 的普通 JSONL 与多帧 zstd、旧 packed-chunk 行及 v2/v3 内嵌 stream;升级留下多代文件时只读取每个会话的最新规范文件。PTC 子工具调用保留工具名且不重复计数,不提取系统提示、推理、工具参数或结果正文。
16
+
17
+ 默认从 `DSH_HOME/sessions` 读取会话、在 `DSH_HOME/.dsh-dream` 保存日记;未设置 `DSH_HOME` 时使用 `~/.dsh`。配置中的 `sessionsRoot`、`journalDir` 始终优先。
14
18
 
15
19
  ## 安装
16
20
 
@@ -95,7 +99,7 @@ dream_bridge { path: "AGENTS.md", maxLessons: 10 }
95
99
 
96
100
  ```bash
97
101
  pnpm install
98
- pnpm test # 构建 + 48 个测试(多帧 zstd 夹具、打包行解码、隐私脱敏、梦境日记往返、记忆桥接幂等、统计、六工具、注册生命周期)
102
+ pnpm test # 构建 + 离线测试(会话代际、多帧 zstd、流式记录、隐私脱敏、日记、桥接与注册生命周期)
99
103
  ```
100
104
 
101
105
  ## 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 home = homedir();
19
+ const dshHome = str(env.DSH_HOME, join(homedir(), '.dsh'));
20
20
  return {
21
- sessionsRoot: str(cfg.sessionsRoot, join(home, '.dsh', 'sessions')),
22
- journalDir: str(cfg.journalDir, join(home, '.dsh', '.dsh-dream')),
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 会话日志读取:官方格式为“多帧 zstd 拼接的 JSONL”。
2
+ * DSH 会话日志读取:兼容 v0/v1/v2/v3 的 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
- text = decompressAll(readFileSync(filePath));
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, 3].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/v3 把流式片段内嵌进 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
  }
@@ -145,25 +182,17 @@ export function digestSessionFile(filePath, maxUserMessages) {
145
182
  digest.assistantTail.shift();
146
183
  }
147
184
  }
148
- else if (type === 'tool/call') {
149
- const name = typeof data === 'object' && data !== null ? String(data.name ?? '') : '';
150
- if (name !== '' && digest.toolCalls.length < 200)
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
- }
185
+ else if (type === 'tool/call' || type === 'tool/code-dispatch-start' || type === 'tool/ptc-dispatch-start') {
186
+ // PTC 子调用只记录开始事件,避免与完成事件重复计算;不提取参数或结果正文。
187
+ if (typeof data === 'object' && data !== null)
188
+ appendToolName(data.name);
161
189
  }
162
- else if (type === 'tool-call-chunks') {
163
- // 官方打包行:还原工具名足迹
164
- const name = typeof data === 'object' && data !== null ? String(data.name ?? '') : '';
165
- if (name !== '' && digest.toolCalls.length < 200)
166
- digest.toolCalls.push(name);
190
+ else if (type === 'text-chunks' || type === 'tool-call-chunks') {
191
+ if (typeof data === 'object' && data !== null)
192
+ readStreamRecord({ ...data, type });
193
+ }
194
+ else if (type === 'assistant/chunk' && typeof data === 'object' && data !== null) {
195
+ readStreamRecord({ type: 'chunk', chunk: data.chunk });
167
196
  }
168
197
  }
169
198
  digest.userMessages = digest.userMessages.slice(0, maxUserMessages);
@@ -174,7 +203,15 @@ export function listSessionFiles(sessionsRoot, limit) {
174
203
  if (!existsSync(sessionsRoot))
175
204
  return [];
176
205
  const out = [];
177
- for (const project of readdirSync(sessionsRoot)) {
206
+ const listDirectory = (path) => {
207
+ try {
208
+ return readdirSync(path);
209
+ }
210
+ catch {
211
+ return [];
212
+ }
213
+ };
214
+ for (const project of listDirectory(sessionsRoot)) {
178
215
  const projectDir = join(sessionsRoot, project);
179
216
  try {
180
217
  if (!statSync(projectDir).isDirectory())
@@ -183,15 +220,30 @@ export function listSessionFiles(sessionsRoot, limit) {
183
220
  catch {
184
221
  continue;
185
222
  }
186
- for (const session of readdirSync(projectDir)) {
187
- const file = join(projectDir, session, 'session.jsonl.zstd');
188
- try {
189
- if (existsSync(file))
190
- out.push({ file, mtime: statSync(file).mtimeMs });
191
- }
192
- catch { /* 并发删除等竞态,跳过 */ }
223
+ for (const session of listDirectory(projectDir)) {
224
+ const sessionDir = join(projectDir, session);
225
+ // 迁移会保留旧代文件;每个会话只读最新已提交的规范文件,避免重复或读旧副本。
226
+ const candidates = listDirectory(sessionDir).flatMap((name) => {
227
+ const match = /^session(?:\.v([1-9][0-9]*))?\.jsonl(?:\.zstd)?$/.exec(name);
228
+ if (match === null)
229
+ return [];
230
+ const version = match[1] === undefined ? 0 : Number(match[1]);
231
+ if (!Number.isSafeInteger(version))
232
+ return [];
233
+ const file = join(sessionDir, name);
234
+ try {
235
+ const stat = statSync(file);
236
+ return stat.isFile() ? [{ file, version, mtime: stat.mtimeMs }] : [];
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ }).sort((a, b) => b.version - a.version || Number(b.file.endsWith('.zstd')) - Number(a.file.endsWith('.zstd')));
242
+ if (candidates[0] !== undefined)
243
+ out.push(candidates[0]);
193
244
  }
194
245
  }
195
246
  out.sort((a, b) => b.mtime - a.mtime);
196
- return out.slice(0, limit).map((item) => item.file);
247
+ const capped = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
248
+ return out.slice(0, capped).map((item) => item.file);
197
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardustlc/dsh-dream",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "license": "MIT",
45
45
  "engines": {
46
- "node": ">=22.13"
46
+ "node": "^22.19.0 || >=24.0.0"
47
47
  },
48
48
  "dependencies": {},
49
49
  "devDependencies": {