@stardustlc/dsh-dream 0.4.1 → 0.5.0

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/lib/web.js CHANGED
@@ -1,22 +1,30 @@
1
1
  /**
2
2
  * dsh-dream 的网页端后端:给设置页里的梦境面板提供只读数据。
3
3
  *
4
- * 安全模型沿用 dsh-calendar / dsh-email 的设置路由:这个路由能读出用户的
5
- * 全部梦境日记(都是隐私文本),而宿主 webserver 有可能绑定在 0.0.0.0。
4
+ * 安全模型沿用 dsh-calendar / dsh-email 的设置路由:这些路由能读出用户的
5
+ * 全部梦境日记与经验(都是隐私文本),而宿主 webserver 有可能绑定在 0.0.0.0。
6
6
  * 三道门缺一不可 ——
7
7
  * 1. remoteAddress 若是明确的非回环地址则拒绝(局域网不可达);
8
8
  * 2. Host 头必须是 localhost 名(挡 DNS rebinding:恶意域名解析到 127.0.0.1);
9
- * 3. 只接受 GET:本路由没有任何写路径,所以不需要 CSRF 那一套。
9
+ * 3. 只接受 GET:两条路由都没有任何写路径,所以不需要 CSRF 那一套。
10
10
  *
11
- * 这一层不做自己的状态:每次请求都从 journal.ts 现读现算,磁盘上的 JSONL
12
- * 就是唯一真相。
11
+ * 零写盘纪律(M1):知识路由只读 <journalDir>/knowledge/。目录不存在时直接返回
12
+ * 空 lessons/evidence + 全零 stats,绝不为了读去 mkdir 或 rebuildIndex;重建索引
13
+ * 只发生在写路径或显式维护操作里。
13
14
  *
14
15
  * @module dsh-dream/web
15
16
  */
17
+ import { existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
16
19
  import { resolveConfig } from './config.js';
17
20
  import { dreamStats, readDreams, searchDreams } from './journal.js';
21
+ import { KnowledgeStore } from './knowledge-store.js';
22
+ import { maskSecrets } from './mask.js';
23
+ import { buildEvidenceSummary, lessonScopeLabel } from './retrieval.js';
18
24
  /** 面板与浏览器说话的同源路由。 */
19
25
  export const DREAM_ROUTE = '/_dsh/dsh-dream/journal';
26
+ /** M1 经验面板的只读路由。 */
27
+ export const DREAM_KNOWLEDGE_ROUTE = '/_dsh/dsh-dream/knowledge';
20
28
  /** ?limit 的默认值与上下界。 */
21
29
  export const DREAM_LIMIT_DEFAULT = 50;
22
30
  export const DREAM_LIMIT_MIN = 1;
@@ -25,6 +33,8 @@ export const DREAM_LIMIT_MAX = 500;
25
33
  const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
26
34
  /** 回环来源地址白名单。 */
27
35
  const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
36
+ /** 知识的 5 个契约状态(统计归一化时保证键齐全)。 */
37
+ const LESSON_STATES = ['candidate', 'usable', 'disputed', 'stale', 'rejected'];
28
38
  /** Host 头裁决:undefined = 放行,否则给出拒绝原因。 */
29
39
  export function hostVerdict(host) {
30
40
  if (typeof host !== 'string' || host.trim() === '')
@@ -55,14 +65,20 @@ function journalDirOf(options) {
55
65
  return options.journalDir;
56
66
  return resolveConfig((options.config ?? null)).journalDir;
57
67
  }
68
+ /** 解析后的 knowledgeDir:显式路径优先,否则 = <journalDir>/knowledge。 */
69
+ export function knowledgeDirOf(options, journalDir = journalDirOf(options)) {
70
+ if (typeof options.knowledgeDir === 'string' && options.knowledgeDir.trim() !== '')
71
+ return options.knowledgeDir;
72
+ return join(journalDir, 'knowledge');
73
+ }
58
74
  /** 解析请求 URL;非法或缺失时退回路由本身(等价于没有查询参数)。 */
59
- function requestUrl(raw) {
60
- const text = typeof raw === 'string' && raw !== '' ? raw : DREAM_ROUTE;
75
+ function requestUrl(raw, fallback = DREAM_ROUTE) {
76
+ const text = typeof raw === 'string' && raw !== '' ? raw : fallback;
61
77
  try {
62
78
  return new URL(text, 'http://localhost');
63
79
  }
64
80
  catch {
65
- return new URL(DREAM_ROUTE, 'http://localhost');
81
+ return new URL(fallback, 'http://localhost');
66
82
  }
67
83
  }
68
84
  /** 统一回 JSON;403/405/500 都用同一套 { ok:false, error:{ code, message } }。 */
@@ -77,29 +93,91 @@ function responseJson(res, status, payload, headers = {}) {
77
93
  function forbidden(res, message) {
78
94
  responseJson(res, 403, { ok: false, error: { code: 'forbidden', message } });
79
95
  }
80
- /** 组装路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
96
+ function internalError(res, error) {
97
+ responseJson(res, 500, {
98
+ ok: false,
99
+ error: { code: 'internal', message: error instanceof Error ? error.message : String(error) },
100
+ });
101
+ }
102
+ /**
103
+ * 三道门:回环来源 → 本机 Host → 只读 GET。
104
+ * 返回 true 表示已经拒绝并写完响应,调用方应立即返回。
105
+ */
106
+ function requestGate(req, res, readOnlyMessage) {
107
+ const remote = remoteVerdict(req?.socket?.remoteAddress);
108
+ if (remote !== undefined) {
109
+ forbidden(res, remote);
110
+ return true;
111
+ }
112
+ const headers = (req?.headers ?? {});
113
+ const host = hostVerdict(headers.host);
114
+ if (host !== undefined) {
115
+ forbidden(res, host);
116
+ return true;
117
+ }
118
+ // 缺失 method 的请求在宿主内部调用里出现过,按只读的 GET 处理。
119
+ if (String(req?.method ?? 'GET').toUpperCase() !== 'GET') {
120
+ responseJson(res, 405, {
121
+ ok: false,
122
+ error: { code: 'method-not-allowed', message: readOnlyMessage },
123
+ }, { allow: 'GET' });
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+ /** 递归脱敏所有字符串叶子;数字 / 布尔 / null 原样保留。 */
129
+ function maskDeep(value) {
130
+ if (typeof value === 'string')
131
+ return maskSecrets(value);
132
+ if (Array.isArray(value))
133
+ return value.map((item) => maskDeep(item));
134
+ if (value !== null && typeof value === 'object') {
135
+ const out = {};
136
+ for (const [key, item] of Object.entries(value))
137
+ out[key] = maskDeep(item);
138
+ return out;
139
+ }
140
+ return value;
141
+ }
142
+ /** 统计归一化:数字字段兜底为 0,byState 五个键齐全,truncated 只在为 true 时保留。 */
143
+ function normalizeKnowledgeStats(value) {
144
+ const source = value !== null && typeof value === 'object' ? value : {};
145
+ const rawByState = source.byState !== null && typeof source.byState === 'object'
146
+ ? source.byState
147
+ : {};
148
+ const byState = {};
149
+ for (const state of LESSON_STATES) {
150
+ const count = rawByState[state];
151
+ byState[state] = typeof count === 'number' && Number.isFinite(count) && count >= 0 ? count : 0;
152
+ }
153
+ const numberAt = (key) => {
154
+ const raw = source[key];
155
+ return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : 0;
156
+ };
157
+ const stats = {
158
+ lessons: numberAt('lessons'),
159
+ evidence: numberAt('evidence'),
160
+ events: numberAt('events'),
161
+ byState,
162
+ };
163
+ if (source.truncated === true)
164
+ stats.truncated = true;
165
+ return stats;
166
+ }
167
+ /** 知识目录不存在(或尚未建立)时的空载荷:不写盘、不报 404。 */
168
+ export function emptyKnowledgePayload() {
169
+ return {
170
+ lessons: [],
171
+ evidence: [],
172
+ stats: normalizeKnowledgeStats(null),
173
+ };
174
+ }
175
+ /** 组装梦境日记路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
81
176
  export function createDreamWebHandler(options = {}) {
82
177
  const journalDir = journalDirOf(options);
83
178
  return async (req, res) => {
84
- const remote = remoteVerdict(req?.socket?.remoteAddress);
85
- if (remote !== undefined) {
86
- forbidden(res, remote);
87
- return;
88
- }
89
- const headers = (req?.headers ?? {});
90
- const host = hostVerdict(headers.host);
91
- if (host !== undefined) {
92
- forbidden(res, host);
179
+ if (requestGate(req, res, 'dsh-dream journal route is read-only; use GET'))
93
180
  return;
94
- }
95
- // 缺失 method 的请求在宿主内部调用里出现过,按只读的 GET 处理。
96
- if (String(req?.method ?? 'GET').toUpperCase() !== 'GET') {
97
- responseJson(res, 405, {
98
- ok: false,
99
- error: { code: 'method-not-allowed', message: 'dsh-dream journal route is read-only; use GET' },
100
- }, { allow: 'GET' });
101
- return;
102
- }
103
181
  const url = requestUrl(req?.url);
104
182
  const query = (url.searchParams.get('q') ?? '').trim();
105
183
  const limit = limitFromQuery(url.searchParams.get('limit'));
@@ -109,29 +187,74 @@ export function createDreamWebHandler(options = {}) {
109
187
  responseJson(res, 200, { dreams, stats, query, limit });
110
188
  }
111
189
  catch (error) {
112
- responseJson(res, 500, {
113
- ok: false,
114
- error: { code: 'internal', message: error instanceof Error ? error.message : String(error) },
115
- });
190
+ internalError(res, error);
191
+ }
192
+ };
193
+ }
194
+ /** 组装只读经验路由处理器;零写盘:目录不存在就直接返回空结果。 */
195
+ export function createKnowledgeWebHandler(options = {}) {
196
+ const knowledgeDir = knowledgeDirOf(options);
197
+ return async (req, res) => {
198
+ if (requestGate(req, res, 'dsh-dream knowledge route is read-only; use GET'))
199
+ return;
200
+ try {
201
+ const reader = options.knowledgeStoreFactory !== undefined
202
+ ? options.knowledgeStoreFactory(knowledgeDir)
203
+ : (existsSync(knowledgeDir) ? new KnowledgeStore(knowledgeDir) : null);
204
+ if (reader === null) {
205
+ responseJson(res, 200, emptyKnowledgePayload());
206
+ return;
207
+ }
208
+ const lessons = reader.listLessons();
209
+ const evidence = reader.listEvidence();
210
+ const stats = normalizeKnowledgeStats(reader.stats());
211
+ const payload = {
212
+ lessons: lessons.map((lesson) => maskDeep({
213
+ ...lesson,
214
+ scopeLabel: lessonScopeLabel(lesson.scope),
215
+ evidenceSummary: buildEvidenceSummary(lesson, evidence),
216
+ })),
217
+ evidence: evidence.map((item) => maskDeep(item)),
218
+ stats,
219
+ };
220
+ responseJson(res, 200, payload);
221
+ }
222
+ catch (error) {
223
+ internalError(res, error);
116
224
  }
117
225
  };
118
226
  }
119
227
  /**
120
- * 把梦境面板挂到宿主 webserver 上。
121
- * 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由。
228
+ * 把只读路由挂到宿主 webserver 上。
229
+ * 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由;
230
+ * 同一个 effect 里注册日记与知识两条 exact 路由,卸载时一起释放。
122
231
  */
123
232
  export function installDreamWeb(ctx, options = {}) {
124
233
  if (ctx === null || ctx === undefined || typeof ctx.inject !== 'function') {
125
234
  throw new Error('dsh-dream: installDreamWeb 需要带 ctx.inject 的宿主上下文(宿主应提供 webServer 服务)');
126
235
  }
127
- const handler = createDreamWebHandler(options);
236
+ const journalHandler = createDreamWebHandler(options);
237
+ const knowledgeHandler = createKnowledgeWebHandler(options);
128
238
  ctx.inject(['webServer'], (webCtx) => {
129
239
  webCtx.effect(() => {
130
- return webCtx.webServer.register({
131
- kind: 'exact',
132
- path: DREAM_ROUTE,
133
- handler: (req, res) => handler(req, res),
134
- }, 'dsh-dream: dream journal route');
240
+ const disposers = [
241
+ webCtx.webServer.register({
242
+ kind: 'exact',
243
+ path: DREAM_ROUTE,
244
+ handler: (req, res) => journalHandler(req, res),
245
+ }, 'dsh-dream: dream journal route'),
246
+ webCtx.webServer.register({
247
+ kind: 'exact',
248
+ path: DREAM_KNOWLEDGE_ROUTE,
249
+ handler: (req, res) => knowledgeHandler(req, res),
250
+ }, 'dsh-dream: knowledge route'),
251
+ ];
252
+ return () => {
253
+ for (const dispose of disposers) {
254
+ if (typeof dispose === 'function')
255
+ dispose();
256
+ }
257
+ };
135
258
  }, 'dsh-dream: web routes');
136
259
  });
137
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardustlc/dsh-dream",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "DSH 做梦插件:会话间隙回放近期对话、反思写入梦境日记,并把高频教训幂等桥接进 AGENTS.md;默认隐私脱敏,零运行时依赖。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -1,31 +1,66 @@
1
1
  ---
2
2
  name: dream-protocol
3
- description: 做梦协议:会话开场或长任务收尾时,回放近期会话、反思并沉淀梦境日记。触发词:做梦、复盘、回顾、反思。
3
+ description: 做梦协议:任务开始取回适用经验,长任务收尾沉淀第一人称感悟与附证据的技术经验候选。触发词:做梦、复盘、回顾、反思。
4
4
  ---
5
5
 
6
6
  # 做梦协议(Dream Protocol)
7
7
 
8
8
  你是会做梦的 agent。梦不是幻觉,是睡眠中的记忆巩固——你也一样。
9
9
 
10
- ## 何时做梦
11
- 1. 会话开场且用户说"做梦/复盘/回顾/反思"时;
12
- 2. 长任务收尾、用户道别前,可主动提议"要不要做个梦沉淀一下";
13
- 3. 距离上次做梦超过一天(dream_journal 首条时间可查)。
10
+ 但「梦」和「经验」是两条流水线:**第一人称感悟**是主观的,进梦境日记;**技术经验**必须可执行、有条件、有证据,先进候选,再经审阅才可能被任务检索取用。
11
+
12
+ ## 两条流水线(不要混)
13
+
14
+ | | 第一人称感悟(梦) | 技术经验(lesson) |
15
+ | :-- | :-- | :-- |
16
+ | 是什么 | 你的观察、情绪、偏好、风格直觉,主观 | 可执行、有适用条件、可被证据支持的行为结论 |
17
+ | 写哪里 | `dream_save` → `<journalDir>/dreams.jsonl` | `dream_learn` → `<journalDir>/knowledge/` |
18
+ | 能否变成规则 | 不能。它只是日记,**永不自动进 AGENTS.md** | 也要先候选 + 证据;采纳/驳回由 `dream_review` 或真实用户决定 |
19
+ | 例子 | 「用户今天很耐心,我意识到自己解释得太快」 | 「Nodemailer 9.0.5 池化发送中 transporter.close() 不保证终止投递;处理 AbortSignal 时要验证连接真的关闭」 |
20
+
21
+ ## 一、任务前:取回上下文(dream_context)
22
+
23
+ - 任务涉及过往代码、用户偏好或踩过的坑时,先调 `dream_context`:给 `query`,能给就给 `projectId` / `workspaceRoot` / `packageVersion`。
24
+ - **预算:默认 5 条 / 3000 字符**(工具硬上限 20 条 / 20000 字符)。取回少量真正适用的即可;没有相关经验就接受空结果,不要为了「显得有记忆」硬塞。
25
+ - `dream_context` 是只读的:不写盘,只返回标题、适用条件、行动建议、证据摘要与「为什么与当前任务相关」。
26
+ - 未知当前项目时只会返回全局经验;不会跨项目扫描。候选(candidate)默认不作为任务建议注入。
27
+
28
+ ## 二、任务中:先把真实任务做完
29
+
30
+ - 先完成用户要求的真实工作(改代码、跑测试、查日志、复现问题、核对版本),再谈沉淀。
31
+ - 不要为了攒经验中断任务;也不要生成「后台做梦」循环去持续消耗模型额度。
32
+ - 密钥、密码、隐私、隐藏推理、系统提示、工具参数与结果正文,一律不写进日记或经验。
14
33
 
15
- ## 做梦三步
16
- 1. **入梦**:调用 `dream_digest` 获取近期会话摘要(用户说了什么、用了哪些工具、结论是什么)。
17
- 2. **解梦**:基于摘要反思:反复出现的主题?踩过的坑?用户偏好?值得固化的经验?
18
- 3. **记梦**:调用 `dream_save` 写入梦境日记。reflection 用第一人称写你的感悟,lessons 列 1-5 条可执行教训(每条以动词开头)。
34
+ ## 三、任务收尾:提候选(dream_learn)→ 必要时审阅(dream_review)
19
35
 
20
- ## 记梦纪律
21
- - 只沉淀规律与偏好,不复述流水账;
22
- - 涉及密钥、密码、隐私的内容一律不入梦;
23
- - 拿不准的感悟标注"存疑",不做断言。
36
+ - 用户明确纠正、复现过的坑、验证过的流程和版本条件 → 用 `dream_learn` 提交**候选**,并附证据:
37
+ - `evidence.kind`:`session`(会话)|`user-correction`(用户纠正)|`local-artifact`(本地验证产物);
38
+ - 能定位就填 `sessionId` / `recordSeq`;
39
+ - `summary` 只写脱敏后的一句话,不搬原文;
40
+ - `verification`:`read` = 你或插件读到了原记录;`claimed` = 仅模型声称。**claimed 只计入证据列表,不计入独立支持数。**
41
+ - 没有证据的观察只能是 candidate,不能写成确定事实,也不能说成「已验证」。
42
+ - 用户明确采纳/驳回、发现经验互相冲突、依赖升级后旧经验需要复核 → 用 `dream_review`(必须带 `expectedRevision`)。
43
+ - 用户采纳必须来自真实用户指令;模型自己不能冒充 `actor: user`。
44
+ - 同一证据重复提交是幂等的,不会增加独立支持数。**禁止靠反复调用 `dream_save` 或 `dream_learn` 刷次数**——次数不等于正确,也不等于重要。
45
+
46
+ ## 四、记梦纪律(dream_save)
47
+
48
+ - 只沉淀规律与偏好,不复述流水账;`reflection` 用第一人称写你的观察与感受;
49
+ - `lessons` 是 1–5 条可执行教训,每条以动词开头;拿不准就标注「存疑」;
50
+ - **不得把主观反思升级成 AGENTS.md 指令**:不要用 `dream_bridge` 把未经审阅的感悟写成项目规则。桥接只能用于已采纳、当前项目范围、附证据的经验。
51
+
52
+ ## 何时做梦
53
+
54
+ 1. 会话开场且用户说「做梦 / 复盘 / 回顾 / 反思」时;
55
+ 2. 长任务收尾、用户道别前,可主动提议「要不要做个梦沉淀一下」;
56
+ 3. 距离上次做梦超过一天(`dream_journal` 首条时间可查)。
24
57
 
25
58
  ## 醒来之后
26
- 用户问起过往经验时,先 `dream_recall` 搜梦境日记再回答。
27
59
 
28
- 用户想看梦的时候,告诉他:DSH 的「设置 → 梦境日记」有只读面板(心境方块、教训榜、时间线、关键词搜索)。
60
+ - 想在日记里找原话 → `dream_recall`;
61
+ - 想取少量可复用经验 → `dream_context`(带范围与预算,只读);
62
+ - 想查看/审阅候选、处理冲突 → `dream_review`;
63
+ - 面板查看:「设置 → 梦境日记」里有只读的梦境时间线与「经验」区块(状态、适用条件、证据摘要、最近核验时间),面板不提供任何写操作。
29
64
 
30
65
  ---
31
66
 
@@ -33,22 +68,57 @@ description: 做梦协议:会话开场或长任务收尾时,回放近期会
33
68
 
34
69
  You are an agent that dreams. Dreams are not hallucinations — they are memory consolidation during sleep, and the same applies to you.
35
70
 
36
- ## When to dream
37
- 1. At session start, when the user says "dream / recap / review / reflect";
38
- 2. At the end of a long task, before the user leaves — you may propose "shall we dream to consolidate?";
39
- 3. When more than a day has passed since the last dream (check the first entry of `dream_journal`).
71
+ Dreams and **lessons** are two separate pipelines: a **first-person reflection** is subjective and goes to the dream journal; a **technical lesson** must be actionable, conditional and evidence-backed — it starts as a candidate and is only retrieved for tasks after review.
72
+
73
+ ## Two pipelines (do not mix)
74
+
75
+ | | First-person reflection (dream) | Technical lesson |
76
+ | :-- | :-- | :-- |
77
+ | What | your observations, feelings, style instincts — subjective | actionable, conditional conclusions backed by evidence |
78
+ | Where | `dream_save` → `<journalDir>/dreams.jsonl` | `dream_learn` → `<journalDir>/knowledge/` |
79
+ | Can it become a rule? | No. It is a journal entry and **never auto-enters AGENTS.md** | Not either — it starts as a candidate with evidence; acceptance is decided by `dream_review` or a real user |
80
+ | Example | "The user was patient today; I realize I explain too fast" | "In Nodemailer 9.0.5 pooled sending, transporter.close() does not guarantee aborting active delivery; verify the connection actually closed when handling AbortSignal" |
81
+
82
+ ## 1. Before the task: retrieve context (dream_context)
83
+
84
+ - When a task touches past code, user preferences or known pitfalls, call `dream_context` first with `query`, plus `projectId` / `workspaceRoot` / `packageVersion` when available.
85
+ - **Budget: 5 items / 3000 characters by default** (hard caps 20 items / 20000 characters). Take only what is relevant; an empty result is acceptable — never stuff memories in to look useful.
86
+ - `dream_context` is read-only: it writes nothing and returns title, applicability, action, evidence summary and why it is relevant.
87
+ - With an unknown project it returns global lessons only; it never scans across projects. Candidates are not injected as task advice by default.
88
+
89
+ ## 2. During the task: do the real work first
90
+
91
+ - Finish the actual task (code, tests, logs, reproductions, version checks) before reflecting.
92
+ - Do not interrupt the task to accumulate lessons, and do not run a background "dream loop" that keeps spending model budget.
93
+ - Never store secrets, credentials, private data, hidden reasoning, system prompts, tool arguments or result bodies.
40
94
 
41
- ## The three steps
42
- 1. **Enter the dream**: call `dream_digest` for recent session summaries (what the user said, which tools were used, what was concluded).
43
- 2. **Interpret**: reflect on the summaries — recurring themes? pitfalls hit? user preferences? lessons worth keeping?
44
- 3. **Record**: call `dream_save`. Write `reflection` in the first person; list 1–5 actionable `lessons`, each starting with a verb.
95
+ ## 3. Wrap-up: propose candidates (dream_learn), then review if needed (dream_review)
45
96
 
46
- ## Discipline
47
- - Consolidate patterns and preferences only — never a running transcript;
48
- - Secrets, passwords and private data never enter dreams;
49
- - Mark uncertain insights as "tentative"; do not assert.
97
+ - After an explicit user correction, a reproduced pitfall, a verified procedure or a version condition → submit a **candidate** with `dream_learn` and attach evidence:
98
+ - `evidence.kind`: `session` | `user-correction` | `local-artifact`;
99
+ - fill `sessionId` / `recordSeq` when the source can be located;
100
+ - keep `summary` to one masked sentence — never copy the raw record;
101
+ - `verification`: `read` = the record was actually read; `claimed` = the model merely asserts it. **Claimed evidence does not count toward independent support.**
102
+ - An observation without evidence stays a candidate: do not present it as a fact, and never call it "verified".
103
+ - For explicit acceptance/rejection, conflicts between lessons, or re-validation after dependency upgrades → call `dream_review` with an `expectedRevision`.
104
+ - User acceptance must come from a real user instruction; the model must not impersonate `actor: user`.
105
+ - Submitting the same evidence twice is idempotent and does not increase independent support. **Never grind counts by calling `dream_save` or `dream_learn` repeatedly** — frequency is neither correctness nor importance.
106
+
107
+ ## 4. Dreaming discipline (dream_save)
108
+
109
+ - Consolidate patterns and preferences only — never a running transcript. Write `reflection` in the first person.
110
+ - `lessons` are 1–5 actionable items; mark uncertain ones as "tentative".
111
+ - **Never promote a subjective reflection into an AGENTS.md instruction**: do not use `dream_bridge` to write unreviewed reflections into project rules. Bridging is for accepted, project-scoped, evidence-backed lessons only.
112
+
113
+ ## When to dream
114
+
115
+ 1. At session start when the user says "dream / recap / review / reflect";
116
+ 2. At the end of a long task, before the user leaves — you may propose a consolidation;
117
+ 3. When more than a day has passed since the last dream (check the first entry of `dream_journal`).
50
118
 
51
119
  ## After waking
52
- When the user asks about past experience, search the journal with `dream_recall` first, then answer.
53
120
 
54
- When the user wants to browse their dreams, point them at the read-only panel under DSH Settings → Dream journal (mood strip, lessons ledger, timeline, keyword search).
121
+ - Looking for exact words in the journal → `dream_recall`;
122
+ - Retrieving a few reusable lessons → `dream_context` (scoped, budgeted, read-only);
123
+ - Browsing or reviewing candidates and conflicts → `dream_review`;
124
+ - To browse: **Settings → Dream journal** shows the read-only dream timeline and a **Lessons** block (state, applicability, evidence summary, last validated time). The panel has no write actions.