@a9i5k4/dsh-auto-memory 0.1.28 → 0.1.30
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 +352 -266
- package/README.zh-CN.md +370 -264
- package/cordis.patch.yml +9 -9
- package/lib/activation-host.js +455 -0
- package/lib/activation-inbox-state.js +261 -0
- package/lib/activation-inbox.js +426 -0
- package/lib/client.js +1395 -50
- package/lib/context-bridge.js +619 -0
- package/lib/context-host.js +712 -0
- package/lib/context-sink-python.js +90 -0
- package/lib/episodic-store.js +316 -0
- package/lib/evidence-store.js +272 -0
- package/lib/fact-store.js +418 -0
- package/lib/index-sync.js +160 -0
- package/lib/index.js +2386 -105
- package/lib/intent-clean.js +74 -0
- package/lib/m4-corpus.js +169 -0
- package/lib/m7-index-sync-host.js +194 -0
- package/lib/m7-wire.js +268 -0
- package/lib/memory-anchor.js +451 -0
- package/lib/memory-hub.js +259 -0
- package/lib/memory-index.js +145 -0
- package/lib/memory-writer.js +391 -0
- package/lib/policies/activation_policy_v2.json +88 -0
- package/lib/policies/recall_intent_lr_v1.json +1 -0
- package/lib/procedure-store.js +406 -0
- package/lib/python-sidecar-client.js +326 -0
- package/lib/semantic-decide.js +265 -0
- package/lib/semantic-js.js +381 -0
- package/lib/shadow-host.js +361 -0
- package/lib/shadow-retrieval.js +673 -0
- package/lib/storage-manage.js +203 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -18,19 +18,43 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readFile, writeFile, mkdir, readdir, stat, rm, copyFile, appendFile } from 'node:fs/promises'
|
|
21
|
-
import { createReadStream, existsSync } from 'node:fs'
|
|
21
|
+
import { createReadStream, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
22
22
|
import { exec as cpExec } from 'node:child_process'
|
|
23
23
|
import { promisify } from 'node:util'
|
|
24
24
|
const execP = promisify(cpExec)
|
|
25
25
|
import { homedir } from 'node:os'
|
|
26
26
|
import path from 'node:path'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
|
+
import { MemoryDocumentStore } from './memory-writer.js'
|
|
29
|
+
import { parseAnchors } from './memory-anchor.js'
|
|
30
|
+
import { createShadowHost } from './shadow-host.js'
|
|
31
|
+
import { createContextHost } from './context-host.js'
|
|
32
|
+
import { createSuccessEvidencePre } from './context-bridge.js'
|
|
33
|
+
import { createIndexSyncHostPre } from './m7-index-sync-host.js'
|
|
34
|
+
import { createActivationHost } from './activation-host.js'
|
|
35
|
+
import { createJsSemanticEnginePre, createSemanticDownloaderPre, fuseD6Pre, E5_SMALL_Q8_MANIFEST_V1 } from './semantic-js.js'
|
|
36
|
+
import { loadAndVerifyPolicy, decideActivationV2, lexicalContainment } from './semantic-decide.js'
|
|
37
|
+
import { createPythonSidecarClientPre, defaultWorkerScriptPathPre } from './python-sidecar-client.js'
|
|
38
|
+
import { createEpisodicStorePre } from './episodic-store.js'
|
|
39
|
+
import { createFactStorePre } from './fact-store.js'
|
|
40
|
+
import { createProcedureStorePre } from './procedure-store.js'
|
|
41
|
+
import { createMemoryHubPre } from './memory-hub.js'
|
|
42
|
+
import { pickConsolidationTextPre } from './intent-clean.js'
|
|
43
|
+
import { createStorageManagerPre } from './storage-manage.js'
|
|
44
|
+
import { buildSourceCatalog, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
45
|
+
import * as nodeZlib from 'node:zlib'
|
|
46
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
47
|
+
import { createHash } from 'node:crypto'
|
|
48
|
+
import { buildIndex as buildMemoryIndexFile, verifyRecord as verifyMemoryRecord, coverage as memoryCoverage, INDEX_MAX_FILE_BYTES } from './memory-index.js'
|
|
49
|
+
|
|
50
|
+
/** zstd 解压(DSH 新版会话持久化 session.jsonl.zstd);Node <22 无此能力时为空,自动回退明文读。 */
|
|
51
|
+
const zstdDec = typeof nodeZlib.zstdDecompressSync === 'function' ? nodeZlib.zstdDecompressSync : null
|
|
28
52
|
|
|
29
53
|
/** Stable cordis plugin name. */
|
|
30
54
|
export const name = 'auto-memory'
|
|
31
55
|
|
|
32
56
|
/** Services required before the memory surfaces can mount. */
|
|
33
|
-
export const inject = ['webServer', 'tools', 'systemPrompt', 'subagents']
|
|
57
|
+
export const inject = ['webServer', 'tools', 'systemPrompt', 'subagents', 'llm']
|
|
34
58
|
|
|
35
59
|
/** Prompt order of the memory section. 10000 = 末尾注入(紧跟用户消息,recency 最高,保证记忆纪律/自动沉淀说明被模型最后读到,遵循度更高)。 */
|
|
36
60
|
const SECTION_ORDER = 10000
|
|
@@ -39,7 +63,7 @@ const SECTION_ORDER = 10000
|
|
|
39
63
|
const NOTICES_URL = 'https://raw.githubusercontent.com/Aik358/dsh-auto-memory/main/notices.json'
|
|
40
64
|
|
|
41
65
|
/** Model-facing announcement (tools + engine). */
|
|
42
|
-
export const GUIDANCE = '本机已安装 dsh-auto-memory
|
|
66
|
+
export const GUIDANCE = '本机已安装 dsh-auto-memory 插件(集中式自动记忆 + 外部记忆继承):三层本地记忆(用户级 ~/.dsh/memory/MEMORY.md、项目笔记与每日日志 .dsh-memory/)+ 会话自动注入 + 每日反思 + 其他 AI 工具记忆接入。能力:memory_log 追加今日日志(append-only,完成实质性工作后必须调用);memory_note 更新项目笔记;memory_user 更新用户级规则;memory_recall 检索本地记忆 + 外部记忆(WorkBuddy/CodeBuddy/Claude Code/Codex 历史会话与画像)+ 历史 DSH 会话;memory_external 查看/接入外部记忆源;memory_maintain 归档 30 天前日志;memory_reflect 保存每日反思;memory_status 查看状态;memory_consolidate 让 AI 读日志发散提炼长期要点固化进笔记。自动沉淀:每轮对话结束插件自动评估本轮内容并写今日日志/升格长期记忆(寒暄轮跳过,间隔与每日额度可在设置页「自动化」分组调整),无需你手动调 memory_log。主动性纪律:任务开始遇到不熟悉的代码/领域/历史决策时,先 memory_recall 检索本机全部 AI 工具历史,不凭空猜测;新工作区主动探索历史。限制:记忆文件为明文 Markdown;不存密钥除非用户明确要求;外部会话检索为关键词级(非语义);GUI 侧边栏「记忆」面板(含「接续」页签,可查看来源内容、从记忆 prompt 移除已导入段落)与设置页可查看/配置/接入。用户提到「记忆 / 昨天做了什么 / 之前怎么做的 / 每日反思 / 接续 / 其他 AI 的记忆」时即指本插件,请据此协作。'
|
|
43
67
|
|
|
44
68
|
/** Route family. */
|
|
45
69
|
export const API = {
|
|
@@ -67,6 +91,15 @@ export const API = {
|
|
|
67
91
|
summarize: '/api/dsh-auto-memory/summarize',
|
|
68
92
|
greet: '/api/dsh-auto-memory/greet',
|
|
69
93
|
notices: '/api/dsh-auto-memory/notices',
|
|
94
|
+
'activation-inbox': '/api/dsh-auto-memory/activation-inbox',
|
|
95
|
+
'semantic-status': '/api/dsh-auto-memory/semantic-status',
|
|
96
|
+
'semantic-download': '/api/dsh-auto-memory/semantic-download',
|
|
97
|
+
'semantic-emit': '/api/dsh-auto-memory/semantic-emit',
|
|
98
|
+
'shadow-recent': '/api/dsh-auto-memory/shadow-recent',
|
|
99
|
+
'review-feedback': '/api/dsh-auto-memory/review-feedback',
|
|
100
|
+
'memory-hub': '/api/dsh-auto-memory/memory-hub',
|
|
101
|
+
'storage-manage': '/api/dsh-auto-memory/storage-manage',
|
|
102
|
+
models: '/api/dsh-auto-memory/models',
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
const DEFAULT_CONFIG = {
|
|
@@ -78,10 +111,19 @@ const DEFAULT_CONFIG = {
|
|
|
78
111
|
memoryRoot: '~/.dsh/memory/workspaces',
|
|
79
112
|
/** 是否注入记忆上下文。 */
|
|
80
113
|
injectEnabled: true,
|
|
81
|
-
/** 注入总预算(字符)
|
|
82
|
-
|
|
114
|
+
/** 注入总预算(字符)。动态记忆快照(最近日志+反思+用户级/项目笔记摘要)每轮最多注入的字符数。
|
|
115
|
+
* 2026-08-26 调低:该快照每轮都会追加到历史尾部(尾部追加不击穿前缀缓存,但有稳态 token 成本,
|
|
116
|
+
* 活跃会话中每 ~15s 刷新一次)。默认 2400≈600-800 token/轮 已是平衡点;建议 1600-2400 之间,
|
|
117
|
+
* 设置页「记忆窗口→注入预算」可调。调太低会截断记忆内容,太高会拉长每轮 token 基数。 */
|
|
118
|
+
injectBudgetChars: 1600,
|
|
83
119
|
/** 注入的最近日志天数。 */
|
|
84
120
|
recentDaysInjected: 1,
|
|
121
|
+
/** subagent 类功能(时段总结/问候语/自动沉淀)使用的模型;留空=跟随系统路由默认。 */
|
|
122
|
+
subagentModel: '',
|
|
123
|
+
/** M3a 只读记忆索引开关(默认关闭;开启后仅构建只读索引与调试快照,不修改任何 Markdown)。 */
|
|
124
|
+
memoryFileIndexEnabled: false,
|
|
125
|
+
/** M3b 稳定 Anchor 写入开关(默认关闭=全部旧 Markdown 写法逐字节不变;开启后记忆写路径经 anchor-aware 事务,CALENDAR.md 始终除外)。 */
|
|
126
|
+
memoryAnchorEnabled: false,
|
|
85
127
|
/** 每轮对话结束自动沉淀记忆(subagent 判断+提炼,有 API 成本;默认开)。 */
|
|
86
128
|
autoConsolidate: true,
|
|
87
129
|
/** 自动沉淀内容门槛:本轮 user+assistant 文本总字符数低于此值视为寒暄,跳过。 */
|
|
@@ -92,6 +134,36 @@ const DEFAULT_CONFIG = {
|
|
|
92
134
|
autoConsolidateDailyMax: 8,
|
|
93
135
|
/** 暂离阈值(分钟):距上次活动超过该值视为暂离,回归时自动弹出记忆窗口并欢迎。默认 60。 */
|
|
94
136
|
awayMinutes: 60,
|
|
137
|
+
/** 无人值守/托管模式(默认关):面向无人值守批量任务。开启后剥离所有"会话性/行为性"注入——
|
|
138
|
+
* 不注入欢迎回来指令、不注入行为指令(如"以X开头")、不注入暂离/回归提示、不注入日历提醒;
|
|
139
|
+
* 只保留纯事实记忆(最近日志/反思/笔记摘要)。避免无人值守时模型浪费 token 在寒暄上。
|
|
140
|
+
* 与模型侧的"托管模式"判断联动(如晚上自动判断托管)。设置页「自动化→无人值守模式」可调。
|
|
141
|
+
* roadmap 2026-08-26:独立开关,不只阈值/弹窗。 */
|
|
142
|
+
unattendedMode: false,
|
|
143
|
+
/** 无人值守自动检测(默认关):开启后,当本地时间为深夜/凌晨/非工作时间(默认 22:00-08:00,
|
|
144
|
+
* 可配 unattendedAutoHours),或检测到自动托管任务运行时,自动进入无人值守模式(等同
|
|
145
|
+
* unattendedMode=true),不弹出欢迎窗、不注入寒暄,批量场景零配置免打扰。
|
|
146
|
+
* 手动开关 unattendedMode 优先;自动检测只在手动未开启时生效。设置页可调。 */
|
|
147
|
+
unattendedAuto: false,
|
|
148
|
+
/** 无人值守自动检测的非工作时间窗(24h 字符串数组,如 ["22:00-08:00"];跨午夜支持)。
|
|
149
|
+
* 空数组=不按时间自动(仅托管任务触发)。 */
|
|
150
|
+
unattendedAutoHours: ['22:00-08:00'],
|
|
151
|
+
/** 动态记忆快照注入频率控制(2026-08-27):
|
|
152
|
+
* snapshotMinGapRounds = 最小注入间隔轮数(同内容在 N 轮内不重复注入,默认 5;
|
|
153
|
+
* 0=每轮都尝试(受变化检测约束))。
|
|
154
|
+
* snapshotReinjectOnCompact = 上下文压缩/截断后是否强制重注入一次(默认 true)。 */
|
|
155
|
+
snapshotMinGapRounds: 5,
|
|
156
|
+
snapshotReinjectOnCompact: true,
|
|
157
|
+
/** 自定义 prompt 层(2026-08-27,小众功能):把记忆注入 prompt 拆成可自定义的层,
|
|
158
|
+
* 用户可改任意层的文案(JSON 对象,key=层名,value=覆盖文本;空字符串=使用默认)。
|
|
159
|
+
* 支持占位符:{date}=今天日期,{ws}=工作区,{budget}=注入预算。
|
|
160
|
+
* DEFAULT_PROMPT_LAYERS 定义默认文案;promptLayerOverrides 覆盖其中任意层;
|
|
161
|
+
* 一键恢复默认 = 清空 promptLayerOverrides。层清单见 renderMemoryDynamic/renderMemoryStatic。 */
|
|
162
|
+
promptLayerOverrides: {},
|
|
163
|
+
/** 暂离回来自动弹出记忆窗口(corner/问候栏)开关:false=关闭,只能手动打开。默认 true。 */
|
|
164
|
+
autoPopupEnabled: true,
|
|
165
|
+
/** 首启欢迎向导(分步功能介绍+语义引擎检测/下载):true=首次启动后自动播放(设置页可重看);false=不自动弹,仍可手动触发。 */
|
|
166
|
+
welcomeTourEnabled: true,
|
|
95
167
|
/** 自动总结时间点(24h "HH:MM" 数组,如 ["12:00","18:00","22:00"]):到点自动生成本时段总结并弹窗展示。空数组=关闭。 */
|
|
96
168
|
autoSummaryTimes: [],
|
|
97
169
|
/** 日界(分钟,从 0 点起算):凌晨在此之前的活儿归前一天日志;默认 450=早上 7:30 后才进入新一天。 */
|
|
@@ -115,15 +187,118 @@ const DEFAULT_CONFIG = {
|
|
|
115
187
|
'claude-sessions': true,
|
|
116
188
|
'codex-sessions': true,
|
|
117
189
|
},
|
|
190
|
+
// ---------- 主动联想记忆实验基线(M0-R 恢复;全部默认关闭,关闭时现有行为零变化) ----------
|
|
191
|
+
/** 主动联想记忆总开关(观察账本之外的检索/注入行为门)。 */
|
|
192
|
+
associativeMemoryEnabled: false,
|
|
193
|
+
/** Shadow Retrieval(只记录候选不注入)。 */
|
|
194
|
+
shadowRetrievalEnabled: false,
|
|
195
|
+
/** M5 Context/Evidence Bridge(实时上下文组装+Access Evidence;需 associativeMemoryEnabled 同时开启;默认关闭零 IO)。 */
|
|
196
|
+
contextBridgeEnabled: false,
|
|
197
|
+
/** M5 sink 类型:'null'=关闭态语义(零 IO)/'fake'=内存 fixtures 记账;'python' 属 M7,当前非法值回退 null。 */
|
|
198
|
+
contextSinkMode: 'null',
|
|
199
|
+
/** M6 Activation Inbox(fake activation → Reference Tail;需 associativeMemoryEnabled 同时开启;默认关闭零注入)。 */
|
|
200
|
+
activationInboxEnabled: false,
|
|
201
|
+
/** M6 激活来源:'fake'=确定性 fixtures(路由注入);'js'=JS 判定核(C2 检索+JS 决策,默认闭环);
|
|
202
|
+
* 'python' 仅在 assoc∧inbox∧pythonBackend 三重门下解锁(M7-1)。2026-08-27 默认改 js。 */
|
|
203
|
+
activationSource: 'js',
|
|
204
|
+
/** JS 判定冷却(分钟):JS 判定 emit 注入后,N 分钟内不再判定,防止连续唤起浪费 token。
|
|
205
|
+
* 默认 1(working memory 实时定位:procedural 举一反三需频繁浮现;token 靠精简注入控制)。
|
|
206
|
+
* 0=不冷却。与 M6 投递冷却(TTL/2 步)叠加。设置页可调。 */
|
|
207
|
+
jsDecideCooldownRounds: 1,
|
|
208
|
+
/** JS 档 margin 阈值覆盖(2026-08-28 e5 校准增量):fv2 冻结策略的 deltaExp=0.03 是在
|
|
209
|
+
* bge-m3 余弦分布上校准的(held-out emit margin 0.033-0.223);e5-small 聚类更紧、
|
|
210
|
+
* margin 系统性压缩 3-5 倍(实测 0-0.028),同语义阈值 ≈0.01。
|
|
211
|
+
* 仅覆盖 JS(e5) 判定档;Python(bge-m3) 档继续用冻结策略工件。null=用冻结值。
|
|
212
|
+
* 决策核 decideActivationV2 一字不动——覆盖在调用侧克隆 policy 实现。 */
|
|
213
|
+
jsDecideDeltaExp: 0.01,
|
|
214
|
+
/** 唤起注入的 excerpt 长度(字符):Reference Tail 的 Reference 行内容上限。
|
|
215
|
+
* 默认 40=几个字/关键词级(省 token,agent 需要细节时用 memory_read 取全文);
|
|
216
|
+
* 可调 20-480(M6 excerpt 上限)。设置页可调。 */
|
|
217
|
+
jsDecideExcerptChars: 40,
|
|
218
|
+
/** 唤起候选方案(2026-08-27 优化③):'balanced'=3条×40字符(默认,信息量/token 平衡);
|
|
219
|
+
* 'dense'=6条×20字符(更多候选更广联想,每条更短);'custom'=用 jsDecideCandidatesN/jsDecideExcerptChars
|
|
220
|
+
* 自定义。设置页档位切换 + 自定义数字。 */
|
|
221
|
+
jsDecideCandidateScheme: 'balanced',
|
|
222
|
+
/** custom 档的候选条数(1-8)。 */
|
|
223
|
+
jsDecideCandidatesN: 4,
|
|
224
|
+
/** M7 Python sidecar worker 脚本路径;留空=捆绑的 python/worker_v1.py(fake 确定性实现)。 */
|
|
225
|
+
pythonBackendWorkerPath: '',
|
|
226
|
+
/** M7 Python 可执行文件;留空='python'(PATH 解析,no-shell spawn)。 */
|
|
227
|
+
pythonBackendExecutable: '',
|
|
228
|
+
/** M7.5 语义引擎档位:'auto'=C1 词法保底+C2 就绪即用(默认)/'lexical'=仅词法/'js'=内置语义(e5-small q8)/'python'=Python sidecar 高级档(bge-m3 int8)。 */
|
|
229
|
+
semanticEngineMode: 'auto',
|
|
230
|
+
/** pre-step 软注入。 */
|
|
231
|
+
softInjectionEnabled: false,
|
|
232
|
+
/** M7.5:分支/子代理会话是否也纳入上下文观测(shadow 观测零注入风险;2026-08-26 裁定默认开:开源模型为主,思维链/分支是主要观测面)。 */
|
|
233
|
+
contextBridgeObserveChildSessions: true,
|
|
234
|
+
/** Python sidecar(embedding/graph)。 */
|
|
235
|
+
pythonBackendEnabled: false,
|
|
236
|
+
/** 推理 trace 观察器(2026-08-26 裁定默认开:监听目标是模型思维链,闭源概括式 CoT 同样纳入)。 */
|
|
237
|
+
reasoningObserverEnabled: true,
|
|
238
|
+
/** Procedure 自动晋升。 */
|
|
239
|
+
procedurePromotionEnabled: false,
|
|
240
|
+
/** ── M8 记忆中枢(Memory Hub)── 三层记忆(episodic/semantic/procedural)编排参数。
|
|
241
|
+
* 所有参数都在设置页「记忆中枢」分组可调;默认值对应 M-02/M-03/M-04 元代码门槛。 */
|
|
242
|
+
/** 记忆中枢总开关(默认关;开启后消费 judgement-shadow + 三层 store 运行)。 */
|
|
243
|
+
memoryHubEnabled: false,
|
|
244
|
+
/** episodic: 一个 episode 至少多少段才巩固(少于=噪声丢弃)。默认 2。 */
|
|
245
|
+
episodicMinSegments: 2,
|
|
246
|
+
/** episodic: 保留的最多 episode 数(超出按时间淘汰)。默认 256。 */
|
|
247
|
+
episodicRetention: 256,
|
|
248
|
+
/** procedure: 晋升所需跨会话多样性(≥N 个独立 session)。默认 3(M-04 元代码)。 */
|
|
249
|
+
procedureMinSessions: 3,
|
|
250
|
+
/** procedure: 晋升所需成功次数(≥N)。默认 2(一次成功不足以证明可靠)。 */
|
|
251
|
+
procedureMinSuccess: 2,
|
|
252
|
+
/** procedure: correction 占总证据比例上限(超过则保持 candidate)。默认 0.3。 */
|
|
253
|
+
procedureCorrectionCap: 0.3,
|
|
254
|
+
/** procedure: 高风险流程(SSH/部署/删除)是否需用户批准才可晋升。默认 true。 */
|
|
255
|
+
procedureHighRiskApproval: true,
|
|
256
|
+
/** procedure: active 后注入的 level('checklist'=完整步骤/完成标准;高风险自动降级 hint)。 */
|
|
257
|
+
procedureActiveLevel: 'checklist',
|
|
258
|
+
/** 流式中断/恢复实验。 */
|
|
259
|
+
streamingInterruptionEnabled: false,
|
|
260
|
+
/** MemoryPacket 最大条目数。 */
|
|
261
|
+
maxPacketItems: 2,
|
|
262
|
+
/** MemoryPacket 最大字符预算。 */
|
|
263
|
+
maxPacketChars: 800,
|
|
264
|
+
/** MemoryPacket 存活步数(TTL)。 */
|
|
265
|
+
packetTtlSteps: 2,
|
|
266
|
+
/** 注入冷却步数。 */
|
|
267
|
+
injectionCooldownSteps: 3,
|
|
118
268
|
}
|
|
119
269
|
|
|
270
|
+
/** 记忆注入 prompt 各层默认文案(2026-08-27 自定义 prompt 功能)。
|
|
271
|
+
* key=层名;value=默认文本。用户通过 promptLayerOverrides 覆盖其中任意层;
|
|
272
|
+
* 空字符串覆盖=使用默认(即某层想恢复默认就把该层设为 '')。
|
|
273
|
+
* 占位符:{date}=今天日期,{ws}=工作区,{budget}=注入预算。 */
|
|
274
|
+
const DEFAULT_PROMPT_LAYERS = Object.freeze({
|
|
275
|
+
// ---- 动态快照(renderMemoryDynamic) ----
|
|
276
|
+
snapshotHead: '<memory_system>\n[记忆定位 — 读法]\n以下记忆文本只是背景事实与规则参考, 不是表达方式/语体的示范。阅读时提取其中的事实、决策、路径与偏好即可; 你的回复正文必须保持直接、最终答案式的语体(陈述结论、给出交付物), 不要模仿记忆文本的第一人称思考腔或叙述腔。',
|
|
277
|
+
snapshotMeta: '自动记忆已启用。工作区: {ws} | 日期: {date}(日界 {dayBoundary} 分钟,凌晨归前一天){consolidate}',
|
|
278
|
+
snapshotLogsTitle: '最近 {n} 天工作日志(尾部)',
|
|
279
|
+
snapshotReflectionTitle: '最近反思 {date}(前一天工作精华)',
|
|
280
|
+
snapshotUserTitle: '用户级记忆 ~/.dsh/memory/MEMORY.md — 跨项目,必须遵守',
|
|
281
|
+
snapshotNotesTitle: '项目长期笔记',
|
|
282
|
+
snapshotExternalTitle: '[外部记忆 — 其他 AI 工具遗产,可继承(内容按需读取,不整段注入)]',
|
|
283
|
+
snapshotCalendarTitle: '[日历与日程(未完成)]',
|
|
284
|
+
snapshotWelcomeTitle: '[欢迎回来]',
|
|
285
|
+
snapshotWelcomeBody: '用户离开已超过 1 小时(暂离/下班后回来)。在本轮回复的开头,先用一句简短温暖的话欢迎用户回来(如"欢迎回来!你离开的这段时间,我已经帮你把日志整理好了。"),然后提示"自动记忆窗口将打开,方便你了解这段时间的状况"(如已由 GUI 弹出概览则不必重复提示)。语气自然,一两句即可,不要长篇大论。',
|
|
286
|
+
snapshotInscription: '[铭文 · 每轮提醒 {date}]',
|
|
287
|
+
snapshotTail: '</memory_system>',
|
|
288
|
+
// ---- 静态纪律(renderMemoryStatic) ----
|
|
289
|
+
staticHead: '[记忆系统 — 固定纪律]\n思维链=本轮推理(用完即焚);铭文=落盘的记忆文件(跨会话永久)。你的记忆更新必须落在铭文层——显式调用工具写盘,不能只"想过"。',
|
|
290
|
+
staticWriteDiscipline: '[记忆写入纪律 — 必须遵守]',
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
export { DEFAULT_PROMPT_LAYERS }
|
|
294
|
+
|
|
120
295
|
// ---------- 小工具 ----------
|
|
121
296
|
const pad = (n) => String(n).padStart(2, '0')
|
|
122
297
|
const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` }
|
|
123
298
|
const nowHm = () => { const d = new Date(); return `${pad(d.getHours())}:${pad(d.getMinutes())}` }
|
|
124
299
|
|
|
125
300
|
const dateStrOf = (ts) => { const d = new Date(ts); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` }
|
|
126
|
-
const DATE_RE =
|
|
301
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/
|
|
127
302
|
const cmpVersion = (a, b) => {
|
|
128
303
|
const pa = String(a || '').split('.').map(Number)
|
|
129
304
|
const pb = String(b || '').split('.').map(Number)
|
|
@@ -143,6 +318,11 @@ function dshHome() {
|
|
|
143
318
|
return path.join(homedir(), '.dsh')
|
|
144
319
|
}
|
|
145
320
|
|
|
321
|
+
/** 本插件 lib/ 的上级目录(开发树=仓库根;发行包=包根)。 */
|
|
322
|
+
function pluginRootDir() {
|
|
323
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
324
|
+
}
|
|
325
|
+
|
|
146
326
|
/** 诊断输出:写 ~/.dsh/dsh-auto-memory-diagnose.log(append)+console.log 双保险。验证完移除。 */
|
|
147
327
|
let _diagChain = Promise.resolve()
|
|
148
328
|
function diag(msg) {
|
|
@@ -153,37 +333,878 @@ function diag(msg) {
|
|
|
153
333
|
} catch (e) {}
|
|
154
334
|
}
|
|
155
335
|
|
|
336
|
+
// ---------- 会话隔离层(M0/M1: SessionRuntimeStore,消灭进程级全局状态) ----------
|
|
337
|
+
// ---------- M2 ContextObserver 常量与有界结构(只观察,不检索,不注入) ----------
|
|
338
|
+
/** EventEnvelope 结构版本(系统地图 N-01/C-01 契约)。 */
|
|
339
|
+
const OBSERVER_SCHEMA_VERSION = 1
|
|
340
|
+
/** 每 runtime envelope audit ring 容量(条)。超出淘汰最旧;本轮不暴露为设置项。 */
|
|
341
|
+
const ENVELOPE_RING_LIMIT = 128
|
|
342
|
+
/** 每 runtime Segment ring 容量(条)。 */
|
|
343
|
+
const SEGMENT_RING_LIMIT = 64
|
|
344
|
+
/** 每 runtime Segment ring 总字符预算(所有 segment.text 之和)。 */
|
|
345
|
+
const SEGMENT_RING_CHAR_BUDGET = 32768
|
|
346
|
+
/** 单个 Segment 文本上限(语义切片,不是"最后 N token")。 */
|
|
347
|
+
const SEGMENT_TEXT_MAX = 1200
|
|
348
|
+
/** payload 内预览字段截断上限(有界、可序列化最小投影)。 */
|
|
349
|
+
const OBSERVER_PREVIEW_MAX = 240
|
|
350
|
+
/** seed replay 单次补放事件上限(取尾部窗口):长会话恢复不做无界同步遍历。 */
|
|
351
|
+
const SEED_REPLAY_MAX_EVENTS = 512
|
|
352
|
+
// 【默认关闭语义契约(2026-08-22 审查修复轮,采用方案 B)】
|
|
353
|
+
// associativeMemoryEnabled=false(默认)时:观察器不建立 envelope/segment ring(ring 惰性分配,
|
|
354
|
+
// 关闭期间为 null)、不保存任何 payload/segment 文本,仅保留最小计数(eventSeq 序号、标量元数据、
|
|
355
|
+
// dropped/disabled 计数);配置 true→false 切换时立即清零已采集数据(purgeObserverStorage)。
|
|
356
|
+
// 因此工具参数/结果、用户文本等潜在敏感内容在默认配置下零留存;"关闭时零行为变化"覆盖
|
|
357
|
+
// model-visible prompt、14 个 _pre 工具、24 条路由、持久 Markdown 与观察账本存储。
|
|
358
|
+
// associativeMemoryEnabled=true 时才启用完整账本(ring+payload+segment)。
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 有界环形缓冲:M2 envelope/segment 共用。容量按条数(+可选总字符预算)限制,淘汰最旧。
|
|
362
|
+
*/
|
|
363
|
+
class ObserverRing {
|
|
364
|
+
constructor(limit, charBudget) {
|
|
365
|
+
this.limit = Math.max(Number(limit) || 1, 1)
|
|
366
|
+
this.charBudget = Number(charBudget) > 0 ? Number(charBudget) : Infinity
|
|
367
|
+
this.items = []
|
|
368
|
+
this.totalChars = 0
|
|
369
|
+
this.evicted = 0
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 文本长度按字符计(segment 用);envelope 无文本传 undefined。 */
|
|
373
|
+
push(item, chars) {
|
|
374
|
+
this.items.push(item)
|
|
375
|
+
if (Number.isFinite(chars)) this.totalChars += chars
|
|
376
|
+
while (this.items.length > this.limit || this.totalChars > this.charBudget) {
|
|
377
|
+
const dropped = this.items.shift()
|
|
378
|
+
if (!dropped) break
|
|
379
|
+
this.evicted += 1
|
|
380
|
+
if (Number.isFinite(dropped._chars)) this.totalChars -= dropped._chars
|
|
381
|
+
}
|
|
382
|
+
return item
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
get length() { return this.items.length }
|
|
386
|
+
|
|
387
|
+
clear() { this.items.length = 0; this.totalChars = 0; /* evicted 保留累计值供调试 */ }
|
|
388
|
+
|
|
389
|
+
snapshot() { return this.items.slice() }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 规范化 payload 的确定性 digest:覆盖实际最小 payload 全文,而非 sourceKind/turn/callId 摘要。 */
|
|
393
|
+
function observerPayloadDigest(payload) {
|
|
394
|
+
return createHash('sha256').update(JSON.stringify(payload === undefined ? null : payload)).digest('hex')
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 确定性 Segment id:基于稳定原生坐标(sessionId+nativeSeq+eventType)而非 runtime 内观察序号,
|
|
399
|
+
* 保证 live 运行与 dispose 后 resume 重放对同一持久事件生成相同 id(审查修复轮)。
|
|
400
|
+
* nativeCoord 由调用方给出:session 通道用 'seq:'+nativeSeq;无原生 seq 的流退化为 'ord:'+eventSeq(仅限不可重放流,文档明示)。
|
|
401
|
+
*/
|
|
402
|
+
function stableSegmentId(sessionId, nativeCoord, eventType, payloadDigest) {
|
|
403
|
+
return 'seg-' + createHash('sha256').update(String(sessionId) + '|' + String(nativeCoord) + '|' + String(eventType) + '|' + String(payloadDigest)).digest('hex').slice(0, 20)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 从消息 content 块中提取纯文本(有界);返回 {text, chars},text 已截断,chars 为原始全长。 */
|
|
407
|
+
function extractBoundedText(content, maxChars) {
|
|
408
|
+
const cap = Math.max(Number(maxChars) || SEGMENT_TEXT_MAX, 1)
|
|
409
|
+
let full = ''
|
|
410
|
+
try {
|
|
411
|
+
if (typeof content === 'string') full = content
|
|
412
|
+
else if (Array.isArray(content)) {
|
|
413
|
+
const parts = []
|
|
414
|
+
for (const block of content) {
|
|
415
|
+
if (block && typeof block === 'object' && typeof block.text === 'string') parts.push(block.text)
|
|
416
|
+
else if (typeof block === 'string') parts.push(block)
|
|
417
|
+
}
|
|
418
|
+
full = parts.join('\n')
|
|
419
|
+
}
|
|
420
|
+
} catch (e) { full = '' }
|
|
421
|
+
const trimmed = full.trim()
|
|
422
|
+
return { text: trimmed.length > cap ? trimmed.slice(0, cap) : trimmed, chars: trimmed.length }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** 字符串安全截断(undefined→''),用于 payload 预览字段。 */
|
|
426
|
+
function boundedStr(v, max) {
|
|
427
|
+
const s = v === undefined || v === null ? '' : String(v)
|
|
428
|
+
const cap = Math.max(Number(max) || OBSERVER_PREVIEW_MAX, 1)
|
|
429
|
+
return s.length > cap ? s.slice(0, cap) : s
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @typedef {Object} EventEnvelope
|
|
434
|
+
* @property {1} schemaVersion
|
|
435
|
+
* @property {string} sessionId
|
|
436
|
+
* @property {string} agentId
|
|
437
|
+
* @property {number} eventSeq
|
|
438
|
+
* @property {'session'|'tools'|'agent'} channel
|
|
439
|
+
* @property {string} eventType
|
|
440
|
+
* @property {number} timestamp
|
|
441
|
+
* @property {number|undefined} nativeSeq
|
|
442
|
+
* @property {number|undefined} turn
|
|
443
|
+
* @property {number|undefined} step
|
|
444
|
+
* @property {'user'|'tool'|'agent'|'lifecycle'} sourceKind
|
|
445
|
+
* @property {string|undefined} messageId
|
|
446
|
+
* @property {string|undefined} callId
|
|
447
|
+
* @property {string|undefined} rootCallId
|
|
448
|
+
* @property {string} payloadDigest
|
|
449
|
+
* @property {Object} payload 有界规范化最小投影(绝不持有 Agent/AbortSignal/DSH 内部对象)
|
|
450
|
+
*/
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @typedef {Object} ContextSegment
|
|
454
|
+
* @property {string} id 确定性 segment id(sessionId+eventSeq+payloadDigest 派生)
|
|
455
|
+
* @property {string} sessionId
|
|
456
|
+
* @property {'user'|'tool_call'|'tool_result'|'assistant'} kind
|
|
457
|
+
* @property {string} eventType
|
|
458
|
+
* @property {number} eventSeq
|
|
459
|
+
* @property {number} contextVersion
|
|
460
|
+
* @property {string} text
|
|
461
|
+
* @property {string} digest
|
|
462
|
+
* @property {number} ts
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @typedef {Object} MemoryPacket
|
|
467
|
+
* @property {string} packetId
|
|
468
|
+
* @property {number} contextVersion
|
|
469
|
+
* @property {Array<ContextSegment>} items
|
|
470
|
+
* @property {number} expiresAtStep
|
|
471
|
+
*/
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @typedef {Object} SessionRuntime
|
|
475
|
+
* @property {string} key
|
|
476
|
+
* @property {string} sessionId
|
|
477
|
+
* @property {string} agentId
|
|
478
|
+
* @property {Object} state
|
|
479
|
+
* @property {number} contextVersion 仅在 observer 接受有效 Segment 时递增
|
|
480
|
+
* @property {number} eventCursor 每个 accepted envelope 单调递增
|
|
481
|
+
* @property {ObserverRing} envelopes M2 envelope audit ring(可回放最小观察账本)
|
|
482
|
+
* @property {ObserverRing} segments M2 语义 Segment ring(有界)
|
|
483
|
+
* @property {number} nativeCursor 已消费的最大原生 session seq(seed/live 去重游标)
|
|
484
|
+
* @property {Map<string,Object>} callLinks callId→{rootCallId,name,frozen,persisted}(root/nested 关联,有界)
|
|
485
|
+
* @property {{noOwner:number,duplicate:number,ignored:number}} observerDropped 每 runtime 观察丢弃计数
|
|
486
|
+
* @property {MemoryPacket|undefined} pendingPacket
|
|
487
|
+
* @property {number|undefined} cooldownUntilStep
|
|
488
|
+
* @property {number|undefined} lastInjectionAt
|
|
489
|
+
*/
|
|
490
|
+
|
|
491
|
+
function createRuntimeState() {
|
|
492
|
+
return {
|
|
493
|
+
home: undefined, ws: undefined,
|
|
494
|
+
userDir: undefined, notesPath: undefined, logPath: undefined, reflectDir: undefined, projectDir: undefined,
|
|
495
|
+
userText: '', notesText: '', logText: '',
|
|
496
|
+
recentLogs: [], // {date, text}
|
|
497
|
+
latestReflection: '', latestReflectionDate: '',
|
|
498
|
+
pendingReflection: undefined, // {date, text}
|
|
499
|
+
reflectionShownSession: undefined,
|
|
500
|
+
todayGreeting: '', greetingShownSession: undefined,
|
|
501
|
+
calendarText: '', calendarPath: undefined,
|
|
502
|
+
away: false, pendingSummary: undefined, // 时间检测:暂离标记 / 待展示的自动时段总结
|
|
503
|
+
workspaceMap: [],
|
|
504
|
+
workspaceCache: undefined,
|
|
505
|
+
loadedAt: 0, loading: undefined, configLoaded: false,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function createSessionRuntime(key) {
|
|
510
|
+
return {
|
|
511
|
+
key,
|
|
512
|
+
sessionId: '',
|
|
513
|
+
agentId: '',
|
|
514
|
+
agent: undefined,
|
|
515
|
+
state: createRuntimeState(),
|
|
516
|
+
contextVersion: 0,
|
|
517
|
+
eventCursor: 0,
|
|
518
|
+
pendingPacket: undefined,
|
|
519
|
+
cooldownUntilStep: undefined,
|
|
520
|
+
lastInjectionAt: undefined,
|
|
521
|
+
lastTurn: undefined,
|
|
522
|
+
consolidating: undefined,
|
|
523
|
+
pendingConsolidations: [],
|
|
524
|
+
autoStats: { count: 0, lastAt: 0, lastText: '', lastDate: '' },
|
|
525
|
+
lastActiveAt: 0,
|
|
526
|
+
lastCompactAt: 0,
|
|
527
|
+
lastConsolidateAt: 0, // 自动沉淀冷却起点(per-session 隔离)
|
|
528
|
+
debug: { observedEvents: 0, lastEventKind: '', lastEventAt: 0, lastEventSeq: 0, lastEnvelope: undefined },
|
|
529
|
+
// M2: 观察账本与语义上下文环(per-runtime 隔离;dispose 时清空)。
|
|
530
|
+
// 审查修复轮2:惰性分配——默认关闭(方案 B)时不构造任何 ring 对象,
|
|
531
|
+
// 仅在 associativeMemoryEnabled=true 且首条观察写入时才创建。
|
|
532
|
+
envelopes: null,
|
|
533
|
+
segments: null,
|
|
534
|
+
// M7.5 CoT 监听(reasoning-delta 聚合缓冲):惰性创建;
|
|
535
|
+
// { text, chars, lastSeq, lastFlushAt } —— 有界 4096,防高频流刷爆 ring。
|
|
536
|
+
reasoningBuf: null,
|
|
537
|
+
nativeCursor: 0,
|
|
538
|
+
callLinks: null,
|
|
539
|
+
observerDropped: { noOwner: 0, duplicate: 0, ignored: 0 },
|
|
540
|
+
abortController: new AbortController(),
|
|
541
|
+
disposed: false,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function identityOfAgent(agent) {
|
|
546
|
+
try {
|
|
547
|
+
const session = agent && agent.session
|
|
548
|
+
const sessionId = session && (session.id || (session.header && session.header.id))
|
|
549
|
+
if (sessionId) return 'session:' + String(sessionId)
|
|
550
|
+
if (agent && agent.id) return 'agent:' + String(agent.id)
|
|
551
|
+
} catch (e) {}
|
|
552
|
+
return ''
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
class SessionRuntimeStore {
|
|
556
|
+
constructor() {
|
|
557
|
+
this._byAgent = new WeakMap()
|
|
558
|
+
this._byIdentity = new Map()
|
|
559
|
+
this._sequence = 0
|
|
560
|
+
this._default = createSessionRuntime('default')
|
|
561
|
+
this._all = new Set([this._default])
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
get(agent) {
|
|
565
|
+
if (!agent || typeof agent !== 'object') return this._default
|
|
566
|
+
let runtime = this._byAgent.get(agent)
|
|
567
|
+
const identity = identityOfAgent(agent)
|
|
568
|
+
if (!runtime && identity) runtime = this._byIdentity.get(identity)
|
|
569
|
+
if (!runtime) {
|
|
570
|
+
runtime = createSessionRuntime(identity || 'agent-object:' + (++this._sequence))
|
|
571
|
+
this._all.add(runtime)
|
|
572
|
+
if (identity) this._byIdentity.set(identity, runtime)
|
|
573
|
+
}
|
|
574
|
+
runtime.agent = agent
|
|
575
|
+
try {
|
|
576
|
+
runtime.agentId = agent.id ? String(agent.id) : runtime.agentId
|
|
577
|
+
const session = agent.session
|
|
578
|
+
runtime.sessionId = session && (session.id || (session.header && session.header.id))
|
|
579
|
+
? String(session.id || session.header.id)
|
|
580
|
+
: runtime.sessionId
|
|
581
|
+
} catch (e) {}
|
|
582
|
+
this._byAgent.set(agent, runtime)
|
|
583
|
+
return runtime
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
dispose(agent) {
|
|
587
|
+
if (!agent || typeof agent !== 'object') return false
|
|
588
|
+
const runtime = this._byAgent.get(agent)
|
|
589
|
+
if (!runtime || runtime === this._default) return false
|
|
590
|
+
runtime.disposed = true
|
|
591
|
+
try { runtime.abortController.abort('agent disposed') } catch (e) {}
|
|
592
|
+
if (runtime.key && this._byIdentity.get(runtime.key) === runtime) this._byIdentity.delete(runtime.key)
|
|
593
|
+
this._byAgent.delete(agent)
|
|
594
|
+
this._all.delete(runtime)
|
|
595
|
+
runtime.agent = undefined
|
|
596
|
+
runtime.pendingConsolidations.length = 0
|
|
597
|
+
runtime.pendingPacket = undefined
|
|
598
|
+
// M4-3:Shadow per-runtime 状态与 inFlight abort
|
|
599
|
+
try { if (this._shadowHost) this._shadowHost.disposeRuntime(runtime) } catch (e) {}
|
|
600
|
+
// M2: 清空观察账本与语义环,断开 call 关联(abort 已由 abortController 完成);ring 为惰性分配,可能为 null
|
|
601
|
+
if (runtime.envelopes) runtime.envelopes.clear()
|
|
602
|
+
if (runtime.segments) runtime.segments.clear()
|
|
603
|
+
if (runtime.callLinks) runtime.callLinks.clear()
|
|
604
|
+
runtime.envelopes = null
|
|
605
|
+
runtime.segments = null
|
|
606
|
+
runtime.callLinks = null
|
|
607
|
+
return true
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
disposeAll() {
|
|
611
|
+
for (const runtime of this._all) {
|
|
612
|
+
runtime.disposed = true
|
|
613
|
+
try { runtime.abortController.abort('plugin disposed') } catch (e) {}
|
|
614
|
+
runtime.pendingConsolidations.length = 0
|
|
615
|
+
runtime.pendingPacket = undefined
|
|
616
|
+
runtime.agent = undefined
|
|
617
|
+
// M2: 插件级 dispose 同步清空观察账本与语义环(惰性分配可能为 null)
|
|
618
|
+
if (runtime.envelopes) runtime.envelopes.clear()
|
|
619
|
+
if (runtime.segments) runtime.segments.clear()
|
|
620
|
+
if (runtime.callLinks) runtime.callLinks.clear()
|
|
621
|
+
runtime.envelopes = null
|
|
622
|
+
runtime.segments = null
|
|
623
|
+
runtime.callLinks = null
|
|
624
|
+
}
|
|
625
|
+
this._byIdentity.clear()
|
|
626
|
+
this._byAgent = new WeakMap()
|
|
627
|
+
this._all.clear()
|
|
628
|
+
this._default.disposed = true
|
|
629
|
+
try { this._default.abortController.abort('plugin disposed') } catch (e) {}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
findBySessionId(sessionId) {
|
|
633
|
+
if (!sessionId) return undefined
|
|
634
|
+
return this._byIdentity.get('session:' + String(sessionId))
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
disposeSession(session) {
|
|
638
|
+
try {
|
|
639
|
+
const sessionId = session && (session.id || (session.header && session.header.id))
|
|
640
|
+
const runtime = this.findBySessionId(sessionId)
|
|
641
|
+
if (!runtime || !runtime.agent) return false
|
|
642
|
+
return this.dispose(runtime.agent)
|
|
643
|
+
} catch (e) { return false }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
values() {
|
|
647
|
+
return Array.from(this._all)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// M0/M1 的粗粒度 recordRuntimeEvent 已由 M2 ContextObserver 取代:
|
|
652
|
+
// 观察入口收敛到 MemoryEngine.observeSessionEvent / observeToolResult / ingestAgentLifecycle / seedRuntimeFromSession。
|
|
653
|
+
// 语义严格分工:eventSeq(=eventCursor)对每个 accepted envelope 递增;
|
|
654
|
+
// contextVersion 仅在产生有效 Segment、实际改变检索上下文时递增,二者绝不机械同步。
|
|
655
|
+
|
|
156
656
|
/** 记忆引擎:路径解析、缓存、文件读写、检索、反思状态。 */
|
|
157
657
|
class MemoryEngine {
|
|
158
658
|
constructor() {
|
|
159
659
|
this.config = { ...DEFAULT_CONFIG }
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
userText: '', notesText: '', logText: '',
|
|
164
|
-
recentLogs: [], // {date, text}
|
|
165
|
-
latestReflection: '', latestReflectionDate: '',
|
|
166
|
-
pendingReflection: undefined, // {date, text}
|
|
167
|
-
reflectionShownSession: undefined,
|
|
168
|
-
todayGreeting: '', greetingShownSession: undefined,
|
|
169
|
-
calendarText: '', calendarPath: undefined,
|
|
170
|
-
away: false, pendingSummary: undefined, // 时间检测:暂离标记 / 待展示的自动时段总结
|
|
171
|
-
loadedAt: 0, loading: undefined, configLoaded: false,
|
|
172
|
-
}
|
|
660
|
+
this.runtimes = new SessionRuntimeStore() // M0/M1: per-agent/session 运行态(消灭全局状态)
|
|
661
|
+
this._runtimeContext = new AsyncLocalStorage() // 执行 token: 关联嵌套观测
|
|
662
|
+
this._routeAgents = new Map() // sessionId -> agent(路由请求定位)
|
|
173
663
|
this._configPath = path.join(dshHome(), 'dsh-auto-memory.json')
|
|
174
664
|
this._readError = undefined
|
|
175
|
-
this._lastAgent = undefined // 最近一次 agent 引用(subagent parent 需要完整 agent 对象)
|
|
176
|
-
this._lastTurnByAgent = undefined // Map<agentId, turn>:自动沉淀去重(每轮只写一次)
|
|
177
|
-
this._consolidating = undefined // 自动沉淀进行中标记(防重入)
|
|
665
|
+
this._lastAgent = undefined // 最近一次 agent 引用(subagent parent 需要完整 agent 对象);M1 主状态已按 session 隔离,runtime 优先
|
|
666
|
+
this._lastTurnByAgent = undefined // Map<agentId, turn>:自动沉淀去重(每轮只写一次);runtime.lastTurn 优先
|
|
667
|
+
this._consolidating = undefined // 自动沉淀进行中标记(防重入);runtime.consolidating 优先
|
|
178
668
|
this._autoCallDate = ''
|
|
179
669
|
this._autoCallCount = 0
|
|
180
670
|
this._lastConsolidateStartedAt = 0
|
|
181
671
|
this._smartRecallFlight = undefined
|
|
182
672
|
this._budgets = undefined // 每日写入预算:用户级4000/项目级3000字/天(所有会话共享,跨天重置)
|
|
183
|
-
|
|
673
|
+
// M2: 进程级观察统计(无 owner 而无法归属 runtime 的事件只在这里留痕,绝不落入 default runtime)
|
|
674
|
+
this._observerStats = { ingestedEnvelopes: 0, segmentsCreated: 0, droppedNoOwner: 0, disabledObservations: 0, seedTruncatedEvents: 0 }
|
|
184
675
|
this.external = new ExternalMemory(this)
|
|
185
676
|
}
|
|
186
677
|
|
|
678
|
+
get state() {
|
|
679
|
+
return this.currentRuntime().state
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
currentRuntime() {
|
|
683
|
+
return this._runtimeContext.getStore() || this.runtimes.get(undefined)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
runtimeFor(agent) {
|
|
687
|
+
return this.runtimes.get(agent)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
stateFor(agent) {
|
|
691
|
+
return this.runtimeFor(agent || this.currentRuntime().agent).state
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
get autoStats() {
|
|
695
|
+
return this.currentRuntime().autoStats
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
aggregateAutoStats() {
|
|
699
|
+
const stats = { count: 0, lastAt: 0, lastText: '', lastDate: '' }
|
|
700
|
+
for (const runtime of this.runtimes.values()) {
|
|
701
|
+
const current = runtime.autoStats
|
|
702
|
+
stats.count += Number(current.count) || 0
|
|
703
|
+
if ((Number(current.lastAt) || 0) > stats.lastAt) {
|
|
704
|
+
stats.lastAt = current.lastAt
|
|
705
|
+
stats.lastText = current.lastText || ''
|
|
706
|
+
stats.lastDate = current.lastDate || ''
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return stats
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
disposeAgent(agent) {
|
|
713
|
+
return this.runtimes.dispose(agent)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
withAgent(agent, callback) {
|
|
717
|
+
return this._runtimeContext.run(this.runtimeFor(agent), callback)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ---------- M2 ContextObserver(C-01):结构化事件 → EventEnvelope → 语义 Segment ----------
|
|
721
|
+
// 只观察:不检索、不注入、不改写 Markdown、不写主 Session。检索中间态绝不进入事实源。
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* 可靠 session 身份判定(审查修复轮2):生命周期入口必须先通过本检查,
|
|
725
|
+
* 才允许把 agent 送进 runtimeFor/stateFor/refresh 等会创建 runtime 的路径。
|
|
726
|
+
*/
|
|
727
|
+
hasReliableSessionIdentity(agent) {
|
|
728
|
+
try {
|
|
729
|
+
const s = agent && agent.session
|
|
730
|
+
return !!(s && (s.id || (s.header && s.header.id)))
|
|
731
|
+
} catch (e) { return false }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* M3a 只读记忆索引快照:memoryFileIndexEnabled=false(默认)时零 IO、零行为变化;
|
|
736
|
+
* 开启时对用户级/项目笔记/今日日志构建只读索引(不修改 Markdown)。
|
|
737
|
+
* sourceVersion 进程内递增:按绝对路径缓存 {fileDigest, sourceVersion},
|
|
738
|
+
* digest 不变复用版本、变化则 +1(跨重启持久化版本留待 M3b sidecar)。
|
|
739
|
+
* 归属:优先当前 runtime 的 agent(AsyncLocalStorage),路由等无 ALS 上下文时回退 _lastAgent。
|
|
740
|
+
* 读取前 stat 预检 >5MB 跳过(不读取内容);模块层 buildIndex 亦自带超限保护(双保险)。
|
|
741
|
+
*/
|
|
742
|
+
async memoryIndexSnapshot() {
|
|
743
|
+
const out = { enabled: this.config.memoryFileIndexEnabled === true }
|
|
744
|
+
if (!out.enabled) return out
|
|
745
|
+
if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
|
|
746
|
+
if (!this._memoryIndexState) this._memoryIndexState = new Map()
|
|
747
|
+
try {
|
|
748
|
+
const owner = this.currentRuntime().agent || this._lastAgent
|
|
749
|
+
const p = await this.resolvePaths(owner)
|
|
750
|
+
const files = [p.userFile, p.notesPath, p.logPath].filter(Boolean)
|
|
751
|
+
out.files = []
|
|
752
|
+
for (const f of files) {
|
|
753
|
+
try {
|
|
754
|
+
// 先 stat:超限文件不读取内容(零大文件 IO)
|
|
755
|
+
const st = await stat(f)
|
|
756
|
+
if (st.size > INDEX_MAX_FILE_BYTES) { out.files.push({ sourceFile: f, skipped: true, bytes: st.size, ownerWs: p.ws }); continue }
|
|
757
|
+
const b = await readFile(f)
|
|
758
|
+
if (b.length > INDEX_MAX_FILE_BYTES) { out.files.push({ sourceFile: f, skipped: true, bytes: b.length, ownerWs: p.ws }); continue }
|
|
759
|
+
const prev = this._memoryIndexState.get(f)
|
|
760
|
+
const idx = buildMemoryIndexFile(f, b, prev)
|
|
761
|
+
this._memoryIndexState.set(f, { fileDigest: idx.fileDigest, sourceVersion: idx.sourceVersion })
|
|
762
|
+
out.files.push({
|
|
763
|
+
sourceFile: f,
|
|
764
|
+
sourceVersion: idx.sourceVersion,
|
|
765
|
+
fileDigest: idx.fileDigest,
|
|
766
|
+
records: idx.records.length,
|
|
767
|
+
charTotal: idx.records.reduce((sum, r) => sum + r.chars, 0),
|
|
768
|
+
ownerWs: p.ws,
|
|
769
|
+
})
|
|
770
|
+
} catch (e) { /* ENOENT 等跳过 */ }
|
|
771
|
+
}
|
|
772
|
+
} catch (e) { out.error = String(e && e.message ? e.message : e) }
|
|
773
|
+
return out
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 观察存储清零(审查修复轮2):associativeMemoryEnabled true→false 切换时调用,
|
|
778
|
+
* 清空全部 runtime 的 envelope/segment ring、callLinks 与 lastEnvelope 引用,contextVersion 归零
|
|
779
|
+
* ——方案 B 的"关闭时零留存"覆盖配置切换前已采集的数据。
|
|
780
|
+
* false→true:不从当前 Session 追溯回放(观察自切换点重新开始);因 Segment id 由原生坐标派生,
|
|
781
|
+
* 未来若实现显式恢复回放,补写的 Segment id 与 live 一致、不会冲突。
|
|
782
|
+
*/
|
|
783
|
+
purgeObserverStorage(reason) {
|
|
784
|
+
let purged = 0
|
|
785
|
+
for (const runtime of this.runtimes.values()) {
|
|
786
|
+
if (runtime.envelopes && runtime.envelopes.length) purged += runtime.envelopes.length
|
|
787
|
+
if (runtime.envelopes) runtime.envelopes.clear()
|
|
788
|
+
if (runtime.segments) runtime.segments.clear()
|
|
789
|
+
if (runtime.callLinks) runtime.callLinks.clear()
|
|
790
|
+
runtime.envelopes = null
|
|
791
|
+
runtime.segments = null
|
|
792
|
+
runtime.callLinks = null
|
|
793
|
+
runtime.reasoningBuf = null // M7.5:CoT 聚合缓冲随观察存储一并清零(审查修复轮2 方案 B 语义)
|
|
794
|
+
runtime.debug.lastEnvelope = undefined
|
|
795
|
+
runtime.contextVersion = 0
|
|
796
|
+
}
|
|
797
|
+
this._observerStats.lastPurgeReason = String(reason || '')
|
|
798
|
+
try { diag('observer storage purged (' + String(reason || '') + '): envelopes=' + purged) } catch (e) {}
|
|
799
|
+
return purged
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* 严格身份解析(审查修复轮):观察路径必须拥有可靠 session.id/session.header.id 才允许归属 runtime;
|
|
804
|
+
* 无身份对象直接计入 droppedNoOwner,绝不调用会伪造 agent-object:* 匿名 runtime 的 get()。
|
|
805
|
+
* 身份存在时才允许 identity-backed 创建(key 必为 session:<id>);仅 agent.id 不足以成立。
|
|
806
|
+
*/
|
|
807
|
+
resolveObserverRuntime(agent, sessionId) {
|
|
808
|
+
let sid = ''
|
|
809
|
+
if (sessionId) sid = String(sessionId)
|
|
810
|
+
if (!sid && agent && typeof agent === 'object') {
|
|
811
|
+
try {
|
|
812
|
+
const s = agent.session
|
|
813
|
+
const candidate = s && (s.id || (s.header && s.header.id))
|
|
814
|
+
if (candidate) sid = String(candidate)
|
|
815
|
+
} catch (e) {}
|
|
816
|
+
}
|
|
817
|
+
if (!sid) { this._observerStats.droppedNoOwner += 1; return undefined }
|
|
818
|
+
try {
|
|
819
|
+
if (agent && typeof agent === 'object') {
|
|
820
|
+
// sessionId 已验证:此调用只会产生/命中 session:<id> 键,不会伪造匿名 runtime
|
|
821
|
+
const rt = this.runtimes.get(agent)
|
|
822
|
+
if (rt && !rt.disposed && rt.key === 'session:' + sid) return rt
|
|
823
|
+
}
|
|
824
|
+
const bySession = this.runtimes.findBySessionId(sid)
|
|
825
|
+
if (bySession && !bySession.disposed) return bySession
|
|
826
|
+
} catch (e) {}
|
|
827
|
+
this._observerStats.droppedNoOwner += 1
|
|
828
|
+
return undefined
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* 核心 ingest:校验并接受一条观察事件。
|
|
833
|
+
* 关闭模式(associativeMemoryEnabled=false,默认):只保留最小计数——递增序号与标量元数据,
|
|
834
|
+
* 不建立 ring、不保存任何 payload/segment(默认关闭语义契约,见文件头常量区注释)。
|
|
835
|
+
* 启用模式:分配 eventSeq → 写 envelope audit ring;有效 Segment 推进 contextVersion 并写 Segment ring。
|
|
836
|
+
* spec.time 为原生事实时间(session/event 的 event.time);tools/lifecycle 无原生时间时才用采集时间。
|
|
837
|
+
* @returns {{envelope:Object,segment:(Object|undefined)}|null}
|
|
838
|
+
*/
|
|
839
|
+
ingestEnvelope(runtime, spec) {
|
|
840
|
+
if (!runtime || runtime.disposed || !spec || typeof spec !== 'object') return null
|
|
841
|
+
const channel = spec.channel === 'session' || spec.channel === 'tools' || spec.channel === 'agent' ? spec.channel : 'agent'
|
|
842
|
+
const eventType = String(spec.eventType || '')
|
|
843
|
+
// ---- 默认关闭:最小计数路径,零留存 ----
|
|
844
|
+
if (this.config.associativeMemoryEnabled !== true) {
|
|
845
|
+
const seq = runtime.eventCursor + 1
|
|
846
|
+
runtime.eventCursor = seq
|
|
847
|
+
runtime.debug.observedEvents += 1
|
|
848
|
+
runtime.debug.lastEventKind = channel + '/' + eventType
|
|
849
|
+
runtime.debug.lastEventAt = Number.isFinite(spec.time) ? Math.trunc(spec.time) : Date.now()
|
|
850
|
+
runtime.debug.lastEventSeq = seq
|
|
851
|
+
this._observerStats.disabledObservations += 1
|
|
852
|
+
return null
|
|
853
|
+
}
|
|
854
|
+
// ---- 启用:完整账本(惰性建环:首次写入才分配,关闭期间零对象) ----
|
|
855
|
+
if (!runtime.envelopes) runtime.envelopes = new ObserverRing(ENVELOPE_RING_LIMIT)
|
|
856
|
+
const sourceKind = spec.sourceKind === 'user' || spec.sourceKind === 'tool' || spec.sourceKind === 'lifecycle'
|
|
857
|
+
? spec.sourceKind
|
|
858
|
+
: (spec.sourceKind === 'agent' ? 'agent' : 'lifecycle')
|
|
859
|
+
const payload = (spec.payload && typeof spec.payload === 'object' && !Array.isArray(spec.payload)) ? spec.payload : {}
|
|
860
|
+
const observedAt = Date.now()
|
|
861
|
+
const factTime = Number.isFinite(spec.time) ? Math.trunc(spec.time) : observedAt
|
|
862
|
+
const nativeSeq = Number.isFinite(spec.nativeSeq) ? Math.trunc(spec.nativeSeq) : undefined
|
|
863
|
+
const eventSeq = runtime.eventCursor + 1
|
|
864
|
+
const envelope = {
|
|
865
|
+
schemaVersion: OBSERVER_SCHEMA_VERSION,
|
|
866
|
+
sessionId: runtime.sessionId || '',
|
|
867
|
+
agentId: runtime.agentId || '',
|
|
868
|
+
eventSeq,
|
|
869
|
+
channel,
|
|
870
|
+
eventType,
|
|
871
|
+
timestamp: factTime,
|
|
872
|
+
observedAt,
|
|
873
|
+
nativeSeq,
|
|
874
|
+
turn: Number.isFinite(spec.turn) ? Math.trunc(spec.turn) : undefined,
|
|
875
|
+
step: Number.isFinite(spec.step) ? Math.trunc(spec.step) : undefined,
|
|
876
|
+
sourceKind,
|
|
877
|
+
messageId: spec.messageId ? String(spec.messageId) : undefined,
|
|
878
|
+
callId: spec.callId ? String(spec.callId) : undefined,
|
|
879
|
+
rootCallId: spec.rootCallId ? String(spec.rootCallId) : undefined,
|
|
880
|
+
payloadDigest: observerPayloadDigest(payload),
|
|
881
|
+
payload,
|
|
882
|
+
}
|
|
883
|
+
runtime.eventCursor = eventSeq
|
|
884
|
+
runtime.envelopes.push(envelope)
|
|
885
|
+
runtime.debug.observedEvents += 1
|
|
886
|
+
runtime.debug.lastEventKind = channel + '/' + eventType
|
|
887
|
+
runtime.debug.lastEventAt = factTime
|
|
888
|
+
runtime.debug.lastEventSeq = eventSeq
|
|
889
|
+
runtime.debug.lastEnvelope = envelope
|
|
890
|
+
this._observerStats.ingestedEnvelopes += 1
|
|
891
|
+
|
|
892
|
+
let segment
|
|
893
|
+
if (spec.segment && typeof spec.segment.text === 'string' && spec.segment.text.length > 0) {
|
|
894
|
+
const segText = spec.segment.text.length > SEGMENT_TEXT_MAX ? spec.segment.text.slice(0, SEGMENT_TEXT_MAX) : spec.segment.text
|
|
895
|
+
// contextVersion 仅在有效 Segment 实际改变检索上下文时递增
|
|
896
|
+
// Segment 身份使用稳定原生坐标(nativeSeq),live 与 resume 重放对同一持久事件同 id
|
|
897
|
+
const nativeCoord = nativeSeq !== undefined ? 'seq:' + nativeSeq : 'ord:' + eventSeq
|
|
898
|
+
const seg = {
|
|
899
|
+
id: stableSegmentId(envelope.sessionId || runtime.key, nativeCoord, eventType, envelope.payloadDigest),
|
|
900
|
+
sessionId: envelope.sessionId,
|
|
901
|
+
kind: String(spec.segment.kind),
|
|
902
|
+
eventType,
|
|
903
|
+
eventSeq,
|
|
904
|
+
nativeSeq,
|
|
905
|
+
contextVersion: runtime.contextVersion + 1,
|
|
906
|
+
text: segText,
|
|
907
|
+
digest: observerPayloadDigest({ kind: String(spec.segment.kind), text: segText }),
|
|
908
|
+
ts: factTime,
|
|
909
|
+
}
|
|
910
|
+
runtime.contextVersion = seg.contextVersion
|
|
911
|
+
seg._chars = seg.text.length
|
|
912
|
+
if (!runtime.segments) runtime.segments = new ObserverRing(SEGMENT_RING_LIMIT, SEGMENT_RING_CHAR_BUDGET)
|
|
913
|
+
runtime.segments.push(seg, seg._chars)
|
|
914
|
+
this._observerStats.segmentsCreated += 1
|
|
915
|
+
segment = seg
|
|
916
|
+
}
|
|
917
|
+
// M4-3:accepted Segment → Shadow 异步调度(fire-and-forget;三开关全开时才构造状态/IO)
|
|
918
|
+
if (segment && this._shadowHost) {
|
|
919
|
+
try { this._shadowHost.onSegmentAccepted(runtime, segment, envelope) } catch (_) {}
|
|
920
|
+
}
|
|
921
|
+
// M5-3:accepted Segment → Context Bridge envelope 组装/cite/correction 扫描(assoc+contextBridge 双门)
|
|
922
|
+
if (segment && this._contextHost) {
|
|
923
|
+
try { this._contextHost.onSegmentAccepted(runtime, segment, envelope) } catch (_) {}
|
|
924
|
+
}
|
|
925
|
+
return { envelope, segment }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** callId→rootCallId 关联账本(root/nested tool call;tools/result 与持久 tool/result 双通道合并,有界)。关闭模式零留存。 */
|
|
929
|
+
linkObserverCall(runtime, callId, rootCallId, name, via) {
|
|
930
|
+
if (!runtime || !callId) return
|
|
931
|
+
if (this.config.associativeMemoryEnabled !== true) return // 方案 B:关闭时不保留任何关联数据
|
|
932
|
+
if (!runtime.callLinks) runtime.callLinks = new Map()
|
|
933
|
+
const key = String(callId)
|
|
934
|
+
let link = runtime.callLinks.get(key)
|
|
935
|
+
if (!link) {
|
|
936
|
+
link = { rootCallId: undefined, name: '', frozen: false, persisted: false }
|
|
937
|
+
runtime.callLinks.set(key, link)
|
|
938
|
+
}
|
|
939
|
+
if (rootCallId !== undefined && rootCallId !== null && rootCallId !== '') link.rootCallId = String(rootCallId)
|
|
940
|
+
if (name) link.name = boundedStr(name)
|
|
941
|
+
if (via === 'frozen') link.frozen = true
|
|
942
|
+
if (via === 'persisted') link.persisted = true
|
|
943
|
+
while (runtime.callLinks.size > 64) {
|
|
944
|
+
const oldest = runtime.callLinks.keys().next().value
|
|
945
|
+
runtime.callLinks.delete(oldest)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** M2.1 session/event 入口(post-commit append feed)。无 owner 或无法定位 runtime 的事件被丢弃留痕。 */
|
|
950
|
+
observeSessionEvent(session, event) {
|
|
951
|
+
try {
|
|
952
|
+
if (!event || typeof event !== 'object' || !event.type) { this._observerStats.droppedNoOwner += 1; return null }
|
|
953
|
+
let sessionId = ''
|
|
954
|
+
try {
|
|
955
|
+
sessionId = session && (session.id || (session.header && session.header.id))
|
|
956
|
+
? String(session.id || session.header.id)
|
|
957
|
+
: ''
|
|
958
|
+
} catch (e) {}
|
|
959
|
+
if (!sessionId) { this._observerStats.droppedNoOwner += 1; return null }
|
|
960
|
+
const runtime = this.runtimes.findBySessionId(sessionId)
|
|
961
|
+
if (!runtime) { this._observerStats.droppedNoOwner += 1; return null }
|
|
962
|
+
return this.ingestSessionEventRecord(runtime, event)
|
|
963
|
+
} catch (e) { return null }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** M2.4 seed/live 共用的持久事件消费路径:nativeSeq 游标去重,禁止重复计数。 */
|
|
967
|
+
ingestSessionEventRecord(runtime, event) {
|
|
968
|
+
try {
|
|
969
|
+
if (!runtime || runtime.disposed || !event || typeof event !== 'object') return null
|
|
970
|
+
const type = typeof event.type === 'string' ? event.type : ''
|
|
971
|
+
if (!type) { runtime.observerDropped.ignored += 1; return null }
|
|
972
|
+
const nativeSeq = Number.isFinite(event.seq) ? Math.trunc(event.seq) : undefined
|
|
973
|
+
if (nativeSeq !== undefined) {
|
|
974
|
+
if (nativeSeq <= runtime.nativeCursor) { runtime.observerDropped.duplicate += 1; return null }
|
|
975
|
+
runtime.nativeCursor = nativeSeq
|
|
976
|
+
}
|
|
977
|
+
const data = (event.data && typeof event.data === 'object') ? event.data : {}
|
|
978
|
+
const turn = Number.isFinite(data.turn) ? Math.trunc(data.turn) : undefined
|
|
979
|
+
const step = Number.isFinite(data.step) ? Math.trunc(data.step) : undefined
|
|
980
|
+
// 原生事实时间(审查修复轮):envelope.timestamp 优先取 session/event 的 event.time,不用采集时间覆盖
|
|
981
|
+
const nativeTime = Number.isFinite(event.time) ? Math.trunc(event.time) : undefined
|
|
982
|
+
const baseSpec = { channel: 'session', eventType: type, nativeSeq, turn, step, time: nativeTime }
|
|
983
|
+
|
|
984
|
+
if (type === 'user/message') {
|
|
985
|
+
const text = extractBoundedText(data.content !== undefined ? data.content : (data.message && data.message.content))
|
|
986
|
+
const messageId = (data.id || (data.message && data.message.id)) ? String(data.id || data.message.id) : undefined
|
|
987
|
+
// 来源溯源(审查修复轮):user/message 统一承载用户输入/插件注入/续接等多种来源,
|
|
988
|
+
// 记录 source.kind 与 plugin 名,后续 M4/M5 才能区分用户事实与插件生成文本。
|
|
989
|
+
const srcObj = (data.source && typeof data.source === 'object') ? data.source : null
|
|
990
|
+
return this.ingestEnvelope(runtime, {
|
|
991
|
+
...baseSpec,
|
|
992
|
+
sourceKind: 'user',
|
|
993
|
+
messageId,
|
|
994
|
+
payload: {
|
|
995
|
+
role: 'user',
|
|
996
|
+
inputSource: boundedStr(srcObj ? (srcObj.kind !== undefined ? srcObj.kind : '') : data.source) || null,
|
|
997
|
+
sourcePlugin: srcObj ? (boundedStr(srcObj.plugin) || null) : null,
|
|
998
|
+
text: text.text,
|
|
999
|
+
textChars: text.chars,
|
|
1000
|
+
},
|
|
1001
|
+
segment: text.text ? { kind: 'user', text: text.text } : undefined,
|
|
1002
|
+
})
|
|
1003
|
+
}
|
|
1004
|
+
if (type === 'assistant/message') {
|
|
1005
|
+
const msg = (data.message && typeof data.message === 'object') ? data.message : data
|
|
1006
|
+
const text = extractBoundedText(msg.content)
|
|
1007
|
+
return this.ingestEnvelope(runtime, {
|
|
1008
|
+
...baseSpec,
|
|
1009
|
+
sourceKind: 'agent',
|
|
1010
|
+
payload: { turn, step, interrupted: data.interrupted === true, text: text.text, textChars: text.chars },
|
|
1011
|
+
segment: text.text ? { kind: 'assistant', text: text.text } : undefined,
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
if (type === 'tool/call') {
|
|
1015
|
+
const argsRaw = typeof data.arguments === 'string' ? data.arguments : JSON.stringify(data.arguments ?? null)
|
|
1016
|
+
const callId = data.callId ? String(data.callId) : undefined
|
|
1017
|
+
this.linkObserverCall(runtime, callId, undefined, data.name)
|
|
1018
|
+
return this.ingestEnvelope(runtime, {
|
|
1019
|
+
...baseSpec,
|
|
1020
|
+
sourceKind: 'tool',
|
|
1021
|
+
callId,
|
|
1022
|
+
payload: {
|
|
1023
|
+
turn, step,
|
|
1024
|
+
name: boundedStr(data.name),
|
|
1025
|
+
argsPreview: boundedStr(argsRaw),
|
|
1026
|
+
argsChars: String(argsRaw == null ? '' : argsRaw).length,
|
|
1027
|
+
},
|
|
1028
|
+
segment: { kind: 'tool_call', text: boundedStr(data.name) + '(' + boundedStr(argsRaw) + ')' },
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
if (type === 'tool/result') {
|
|
1032
|
+
const blocks = data.message && Array.isArray(data.message.content) ? data.message.content : []
|
|
1033
|
+
const block = blocks.length && blocks[0] && typeof blocks[0] === 'object' ? blocks[0] : {}
|
|
1034
|
+
const callId = block.toolCallId ? String(block.toolCallId) : (data.callId ? String(data.callId) : undefined)
|
|
1035
|
+
const errInfo = (data.error && typeof data.error === 'object') ? data.error : null
|
|
1036
|
+
const isError = !!errInfo || block.isError === true
|
|
1037
|
+
const result = extractBoundedText(block.content)
|
|
1038
|
+
this.linkObserverCall(runtime, callId, undefined, undefined, 'persisted')
|
|
1039
|
+
return this.ingestEnvelope(runtime, {
|
|
1040
|
+
...baseSpec,
|
|
1041
|
+
sourceKind: 'tool',
|
|
1042
|
+
callId,
|
|
1043
|
+
payload: {
|
|
1044
|
+
turn, step,
|
|
1045
|
+
isError,
|
|
1046
|
+
errorName: errInfo ? boundedStr(errInfo.name) : null,
|
|
1047
|
+
errorCode: errInfo ? boundedStr(errInfo.code) : null,
|
|
1048
|
+
resultPreview: result.text,
|
|
1049
|
+
resultChars: result.chars,
|
|
1050
|
+
},
|
|
1051
|
+
segment: { kind: 'tool_result', text: (isError ? '[error] ' : '') + result.text },
|
|
1052
|
+
})
|
|
1053
|
+
}
|
|
1054
|
+
if (type === 'turn/start' || type === 'turn/end' || type === 'step/start' || type === 'step/end') {
|
|
1055
|
+
// 生命周期边界:进入 envelope audit,但无语义内容时不产生 Segment、不推进 contextVersion
|
|
1056
|
+
const payload = { turn: data.turn, step: data.step }
|
|
1057
|
+
if (type === 'turn/end' && data.reason !== undefined) payload.reason = boundedStr(data.reason)
|
|
1058
|
+
return this.ingestEnvelope(runtime, { ...baseSpec, sourceKind: 'lifecycle', payload })
|
|
1059
|
+
}
|
|
1060
|
+
if (type === 'assistant/chunk') {
|
|
1061
|
+
// M7.5 CoT 监听(RFC docs/COT-WATCH-RFC.md):chunk.type==='reasoning-delta'
|
|
1062
|
+
// 是模型思维链的唯一流式载体;text-delta 由 assistant/message 完整承载,不重复观察。
|
|
1063
|
+
// 断言式单播:仅本 runtime 的 ring;幂等由 session.append 的 nativeSeq 天然保证。
|
|
1064
|
+
const chunk = (data.chunk && typeof data.chunk === 'object') ? data.chunk : {}
|
|
1065
|
+
if (!this.config.reasoningObserverEnabled || chunk.type !== 'reasoning-delta') {
|
|
1066
|
+
runtime.observerDropped.ignored += 1
|
|
1067
|
+
return this.ingestEnvelope(runtime, { ...baseSpec, sourceKind: 'agent', payload: { chunkType: boundedStr(chunk.type) } })
|
|
1068
|
+
}
|
|
1069
|
+
const rt = extractBoundedText(chunk.text !== undefined ? chunk.text : '')
|
|
1070
|
+
if (!rt.text) { runtime.observerDropped.ignored += 1; return null }
|
|
1071
|
+
return this.ingestReasoningDelta(runtime, baseSpec, rt.text)
|
|
1072
|
+
}
|
|
1073
|
+
// assistant/message 到达 = 本轮可见输出开始,强制冲刷残余 CoT 进 window
|
|
1074
|
+
if (type === 'assistant/message') this.flushReasoningBuffer(runtime)
|
|
1075
|
+
// assistant/chunk、reasoning、todo/write、request/* 等默认不形成持久观察(计数留痕)
|
|
1076
|
+
runtime.observerDropped.ignored += 1
|
|
1077
|
+
return null
|
|
1078
|
+
} catch (e) { return null }
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** M7.5:reasoning-delta 聚合缓冲(有界 4096;≥512 字符或 ≥1500ms 冲刷为 reasoning Segment)。 */
|
|
1082
|
+
ingestReasoningDelta(runtime, baseSpec, deltaText) {
|
|
1083
|
+
try {
|
|
1084
|
+
if (!runtime.reasoningBuf) runtime.reasoningBuf = { text: '', chars: 0, lastSeq: 0, lastFlushAt: 0 }
|
|
1085
|
+
const buf = runtime.reasoningBuf
|
|
1086
|
+
const room = 4096 - buf.chars
|
|
1087
|
+
if (room <= 0) return null // 缓冲满:丢弃增量直到下次冲刷,绝不无限缓冲
|
|
1088
|
+
buf.text += deltaText.slice(0, room)
|
|
1089
|
+
buf.chars = buf.text.length
|
|
1090
|
+
buf.lastSeq = baseSpec.nativeSeq
|
|
1091
|
+
const now = Date.now()
|
|
1092
|
+
if (buf.chars >= 512 || now - (buf.lastFlushAt || 0) >= 1500) {
|
|
1093
|
+
return this.flushReasoningBuffer(runtime)
|
|
1094
|
+
}
|
|
1095
|
+
return this.ingestEnvelope(runtime, { ...baseSpec, sourceKind: 'reasoning', payload: { bufferedChars: buf.chars } })
|
|
1096
|
+
} catch (e) { return null }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** M7.5:把聚合中的 CoT 冲刷为一个 kind='reasoning' Segment(进 ring/window,参与检索)。 */
|
|
1100
|
+
flushReasoningBuffer(runtime) {
|
|
1101
|
+
try {
|
|
1102
|
+
const buf = runtime && runtime.reasoningBuf
|
|
1103
|
+
if (!buf || !buf.text) return null
|
|
1104
|
+
const text = buf.text
|
|
1105
|
+
const seq = buf.lastSeq
|
|
1106
|
+
runtime.reasoningBuf = { text: '', chars: 0, lastSeq: seq, lastFlushAt: Date.now() }
|
|
1107
|
+
return this.ingestEnvelope(runtime, {
|
|
1108
|
+
channel: 'session', eventType: 'assistant/reasoning', nativeSeq: seq,
|
|
1109
|
+
time: Date.now(), sourceKind: 'reasoning',
|
|
1110
|
+
payload: { text, textChars: text.length },
|
|
1111
|
+
segment: { kind: 'reasoning', text },
|
|
1112
|
+
})
|
|
1113
|
+
} catch (e) { return null }
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/** M2.1 frozen tools/result 入口(执行级最终结果/失败/nested call)。emit-only,只观察不修改。 */
|
|
1117
|
+
observeToolResult(exec, result) {
|
|
1118
|
+
try {
|
|
1119
|
+
if (!exec || typeof exec !== 'object') { this._observerStats.droppedNoOwner += 1; return null }
|
|
1120
|
+
// 严格身份解析(审查修复轮):无 session 身份的 Agent 不创建任何 runtime(含匿名 agent-object:*)
|
|
1121
|
+
const runtime = this.resolveObserverRuntime(exec.agent || undefined)
|
|
1122
|
+
if (!runtime) return null
|
|
1123
|
+
const ok = !(result && typeof result === 'object' && result.isError === true)
|
|
1124
|
+
// 真实 DSH ToolFailure 形状:{ message, info?: { name, code } }(审查修复轮修正字段来源)
|
|
1125
|
+
const errInfo = (!ok && result.error && typeof result.error === 'object') ? result.error : null
|
|
1126
|
+
const errNested = (errInfo && errInfo.info && typeof errInfo.info === 'object') ? errInfo.info : null
|
|
1127
|
+
const errName = errNested ? boundedStr(errNested.name) : ''
|
|
1128
|
+
const errCode = errNested ? boundedStr(errNested.code) : ''
|
|
1129
|
+
let preview = ''
|
|
1130
|
+
let previewChars = 0
|
|
1131
|
+
if (ok) {
|
|
1132
|
+
let raw = ''
|
|
1133
|
+
try { raw = JSON.stringify(result.value === undefined ? null : result.value) ?? '' } catch (e) { raw = '"<unserializable>"' }
|
|
1134
|
+
previewChars = raw.length
|
|
1135
|
+
preview = raw.slice(0, OBSERVER_PREVIEW_MAX)
|
|
1136
|
+
} else {
|
|
1137
|
+
preview = boundedStr(errInfo && errInfo.message)
|
|
1138
|
+
previewChars = (errInfo && errInfo.message ? String(errInfo.message) : '').length
|
|
1139
|
+
}
|
|
1140
|
+
this.linkObserverCall(runtime, exec.callId, exec.rootCallId, exec.name, 'frozen')
|
|
1141
|
+
const envelopeInfo = this.ingestEnvelope(runtime, {
|
|
1142
|
+
channel: 'tools',
|
|
1143
|
+
eventType: 'tools/result',
|
|
1144
|
+
sourceKind: 'tool',
|
|
1145
|
+
callId: exec.callId,
|
|
1146
|
+
rootCallId: exec.rootCallId,
|
|
1147
|
+
// 无原生时间:使用采集时间(envelope.timestamp=observedAt)
|
|
1148
|
+
payload: {
|
|
1149
|
+
name: boundedStr(exec.name),
|
|
1150
|
+
ok,
|
|
1151
|
+
errorName: errName || null,
|
|
1152
|
+
errorCode: errCode || null,
|
|
1153
|
+
errorMessage: ok ? null : boundedStr(errInfo && errInfo.message) || null,
|
|
1154
|
+
resultPreview: preview,
|
|
1155
|
+
resultChars: previewChars,
|
|
1156
|
+
},
|
|
1157
|
+
// 不生成 Segment:上下文语义以可回放的持久 tool/result(session 通道)为准,保证确定性回放
|
|
1158
|
+
})
|
|
1159
|
+
// M5-3:read coverage 观察(ok=true 且 preview 含 memoryId token 时 precision-first 建 read evidence)
|
|
1160
|
+
if (envelopeInfo && this._contextHost) {
|
|
1161
|
+
try { this._contextHost.onToolResult(runtime, envelopeInfo.envelope) } catch (_) {}
|
|
1162
|
+
}
|
|
1163
|
+
return envelopeInfo
|
|
1164
|
+
} catch (e) { return null }
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** Agent 生命周期事件(session-start/pre-step/turn-stopping)→ audit envelope;无语义内容不推进 contextVersion。 */
|
|
1168
|
+
ingestAgentLifecycle(agent, eventType, data) {
|
|
1169
|
+
try {
|
|
1170
|
+
const d = data && typeof data === 'object' ? data : {}
|
|
1171
|
+
const runtime = this.resolveObserverRuntime(agent)
|
|
1172
|
+
if (!runtime) return null
|
|
1173
|
+
if (runtime.disposed) return null
|
|
1174
|
+
return this.ingestEnvelope(runtime, {
|
|
1175
|
+
channel: 'agent',
|
|
1176
|
+
eventType,
|
|
1177
|
+
sourceKind: 'lifecycle',
|
|
1178
|
+
turn: d.turn,
|
|
1179
|
+
step: d.step,
|
|
1180
|
+
payload: (d.payload && typeof d.payload === 'object') ? d.payload : {},
|
|
1181
|
+
})
|
|
1182
|
+
} catch (e) { return null }
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* M2.4 session-start 对现有 session.events 按原生 seq 补放;live feed 从已消费 nativeSeq 续接去重。
|
|
1187
|
+
* 有界策略(审查修复轮):只回放尾部 SEED_REPLAY_MAX_EVENTS 窗口,超长会话不做无界同步遍历;
|
|
1188
|
+
* 被截断的头部事件计数留痕(seedTruncatedEvents),其旧 seq 之后若出现会被游标视为重复跳过。
|
|
1189
|
+
*/
|
|
1190
|
+
seedRuntimeFromSession(agent) {
|
|
1191
|
+
try {
|
|
1192
|
+
if (!agent || typeof agent !== 'object') return 0
|
|
1193
|
+
const runtime = this.resolveObserverRuntime(agent)
|
|
1194
|
+
if (!runtime || runtime.disposed) return 0
|
|
1195
|
+
const events = agent.session && Array.isArray(agent.session.events) ? agent.session.events : []
|
|
1196
|
+
const capped = events.length > SEED_REPLAY_MAX_EVENTS ? events.slice(-SEED_REPLAY_MAX_EVENTS) : events
|
|
1197
|
+
this._observerStats.seedTruncatedEvents += events.length - capped.length
|
|
1198
|
+
let seeded = 0
|
|
1199
|
+
for (const ev of capped) {
|
|
1200
|
+
const r = this.ingestSessionEventRecord(runtime, ev)
|
|
1201
|
+
if (r) seeded += 1
|
|
1202
|
+
}
|
|
1203
|
+
return seeded
|
|
1204
|
+
} catch (e) { return 0 }
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
|
|
187
1208
|
// ---------- 配置 ----------
|
|
188
1209
|
async loadConfig() {
|
|
189
1210
|
try {
|
|
@@ -202,7 +1223,27 @@ class MemoryEngine {
|
|
|
202
1223
|
await this.loadConfig()
|
|
203
1224
|
const oldRoot = this.expandUserPath(this.config.memoryRoot)
|
|
204
1225
|
const oldUser = this.expandUserPath(this.config.userMemoryDir)
|
|
1226
|
+
const oldObserverEnabled = this.config.associativeMemoryEnabled === true
|
|
205
1227
|
this.config = { ...this.config, ...patch }
|
|
1228
|
+
// 2026-08-27 模式联动(修基础 bug):semanticEngineMode 变更时自动对齐底层引擎,
|
|
1229
|
+
// 防止"设置选 JS 实际跑 Python"。js/auto/lexical → JS 判定闭环(不依赖 Python sink);
|
|
1230
|
+
// python → Python sidecar。用户可后续手动覆盖。
|
|
1231
|
+
if (patch.semanticEngineMode !== undefined) {
|
|
1232
|
+
const mode = String(patch.semanticEngineMode || 'auto')
|
|
1233
|
+
if (mode === 'python') {
|
|
1234
|
+
this.config.activationSource = 'python'
|
|
1235
|
+
this.config.contextSinkMode = 'python'
|
|
1236
|
+
this.config.pythonBackendEnabled = true
|
|
1237
|
+
} else {
|
|
1238
|
+
this.config.activationSource = 'js'
|
|
1239
|
+
this.config.contextSinkMode = 'null'
|
|
1240
|
+
this.config.pythonBackendEnabled = false // 非 python 模式不 spawn Python(避免空跑)
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// 审查修复轮2:观察开关 true→false 时立即清零全部已采集观察数据(方案 B 关闭时零留存)
|
|
1244
|
+
if (oldObserverEnabled && this.config.associativeMemoryEnabled !== true) {
|
|
1245
|
+
try { this.purgeObserverStorage('associative-memory-disabled') } catch (e) {}
|
|
1246
|
+
}
|
|
206
1247
|
const newRoot = this.expandUserPath(this.config.memoryRoot)
|
|
207
1248
|
const newUser = this.expandUserPath(this.config.userMemoryDir)
|
|
208
1249
|
// 换存放位置时自动迁移旧文件(旧文件保留不删,新位置缺啥补啥),所有路径变量在下方 refresh 后全部跟随新配置
|
|
@@ -260,6 +1301,12 @@ class MemoryEngine {
|
|
|
260
1301
|
|
|
261
1302
|
/** 集中式记忆根目录(集中式):所有工作区记忆统一存放,每工作区一个子目录。 */
|
|
262
1303
|
projectDirOf(ws) {
|
|
1304
|
+
if (process.env.DAM_PROBE_STACK === '1') {
|
|
1305
|
+
const _root = this.expandUserPath(this.config.memoryRoot)
|
|
1306
|
+
if (_root && String(_root).includes('.dsh') && !String(_root).includes('Temp')) {
|
|
1307
|
+
console.log('[PROBE] real-root projectDirOf ws=' + ws + ' cfgRoot=' + String(this.config.memoryRoot) + ' cfgLoaded=' + this.configLoaded + ' cfgPath=' + this._configPath + ' stack=' + new Error().stack.split('\n').slice(2, 5).join(' <= '))
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
263
1310
|
const name = this.config.projectMemoryDir || '.dsh-memory'
|
|
264
1311
|
if (path.isAbsolute(name)) return name // 旧用法:绝对路径兼容
|
|
265
1312
|
const root = this.expandUserPath(this.config.memoryRoot) || path.join(dshHome(), 'memory', 'workspaces')
|
|
@@ -299,9 +1346,18 @@ class MemoryEngine {
|
|
|
299
1346
|
}
|
|
300
1347
|
|
|
301
1348
|
async resolvePaths(agent) {
|
|
1349
|
+
// 审查修复轮2:路径解析前必须保证配置已加载 —— 否则首个工具调用会按默认 '~'(真实 homedir)
|
|
1350
|
+
// 解析集中记忆根,造成测试/早期调用写穿真实用户记忆(本次 smoke 污染的根因)。
|
|
1351
|
+
if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
|
|
302
1352
|
let ws
|
|
303
1353
|
try { ws = agent && agent.session && agent.session.header && agent.session.header.cwd } catch (e) {}
|
|
304
1354
|
if (!ws) ws = this.state.ws || process.cwd()
|
|
1355
|
+
// 无人值守模式(2026-08-26,issue 修复):锁定工作区——一旦本次会话已确定工作区,后续 refresh
|
|
1356
|
+
// 若发现 agent cwd 漂移(如 subagent/工具改了 cwd),忽略新 cwd 沿用已锁定工作区,保持上下文路径稳定。
|
|
1357
|
+
// 无人值守批量任务中,路径漂移会导致工具调用失败/重读文件,必须杜绝。手动或自动(夜间/托管)均生效。
|
|
1358
|
+
if (this.isUnattendedNow() && this.state.ws && String(ws) !== String(this.state.ws)) {
|
|
1359
|
+
ws = this.state.ws
|
|
1360
|
+
}
|
|
305
1361
|
const userDir = this.userDirOf()
|
|
306
1362
|
const projectDir = this.projectDirOf(ws)
|
|
307
1363
|
return {
|
|
@@ -599,6 +1655,36 @@ class MemoryEngine {
|
|
|
599
1655
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
600
1656
|
}
|
|
601
1657
|
|
|
1658
|
+
/**
|
|
1659
|
+
* 是否应处于无人值守模式(2026-08-26,roadmap「自动检测」)。
|
|
1660
|
+
* 优先级:手动 unattendedMode=true → 恒 true;否则 unattendedAuto=true 且(命中非工作时间窗
|
|
1661
|
+
* 或检测到自动托管任务) → true;否则 false。
|
|
1662
|
+
* 非工作时间窗:unattendedAutoHours 数组,每项 "HH:MM-HH:MM"(支持跨午夜,如 22:00-08:00)。
|
|
1663
|
+
* 自动托管任务检测:未来接入(如 DSH 会话 source=automation/batch);当前留钩子返回 false。
|
|
1664
|
+
*/
|
|
1665
|
+
isUnattendedNow() {
|
|
1666
|
+
try {
|
|
1667
|
+
if (this.config.unattendedMode === true) return true
|
|
1668
|
+
if (this.config.unattendedAuto !== true) return false
|
|
1669
|
+
// 自动托管任务检测钩子(未来:DSH source=automation/batch 时 true)
|
|
1670
|
+
if (this._hostedTaskActive) return true
|
|
1671
|
+
const windows = Array.isArray(this.config.unattendedAutoHours) ? this.config.unattendedAutoHours : []
|
|
1672
|
+
if (!windows.length) return false
|
|
1673
|
+
const now = new Date()
|
|
1674
|
+
const cur = now.getHours() * 60 + now.getMinutes()
|
|
1675
|
+
for (const w of windows) {
|
|
1676
|
+
const m = String(w || '').match(/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/)
|
|
1677
|
+
if (!m) continue
|
|
1678
|
+
const s = Number(m[1]) * 60 + Number(m[2])
|
|
1679
|
+
const e = Number(m[3]) * 60 + Number(m[4])
|
|
1680
|
+
if (s === e) continue
|
|
1681
|
+
if (s < e) { if (cur >= s && cur < e) return true }
|
|
1682
|
+
else { if (cur >= s || cur < e) return true } // 跨午夜
|
|
1683
|
+
}
|
|
1684
|
+
return false
|
|
1685
|
+
} catch (_) { return false }
|
|
1686
|
+
}
|
|
1687
|
+
|
|
602
1688
|
// ---------- 每日写入预算 + 超限自动压缩(硬约束,但不拒绝新内容) ----------
|
|
603
1689
|
/**
|
|
604
1690
|
* 用户级 ≤4000 字/天、项目级 ≤3000 字/天(所有会话共享一天预算,跨天重置)。
|
|
@@ -633,6 +1719,19 @@ class MemoryEngine {
|
|
|
633
1719
|
const now = Date.now()
|
|
634
1720
|
if (this._lastCompactAt && now - this._lastCompactAt < 10 * 60 * 1000) return false
|
|
635
1721
|
const p = await this.resolvePaths(agent)
|
|
1722
|
+
// F1:anchor 开启 → 记录级压缩(整条保留/移除,经 store.replace;永不字符切片)。
|
|
1723
|
+
// 目标文件无 sidecar('not-anchored')才回退旧文本路径。
|
|
1724
|
+
const store0 = this.docStore
|
|
1725
|
+
if (store0) {
|
|
1726
|
+
try {
|
|
1727
|
+
const r = await this.compactAnchoredLayer(store0, layer === 'user' ? p.userFile : p.notesPath, layer, this.memToday(), p)
|
|
1728
|
+
if (process.env.DSH_F1_DEBUG) console.error('[f1-diag] compactAnchoredLayer -> ' + (layer === 'user' ? p.userFile : p.notesPath) + ' => ' + r)
|
|
1729
|
+
if (r !== 'not-anchored') return r === true
|
|
1730
|
+
} catch (e) {
|
|
1731
|
+
console.error('[compacted-anchored] failed: ' + (e && e.message ? e.message : e))
|
|
1732
|
+
return false
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
636
1735
|
const cur = (layer === 'user' ? this.state.userText : this.state.notesText) || ''
|
|
637
1736
|
if (!cur.trim()) return true
|
|
638
1737
|
const limit = layer === 'user' ? 4000 : 3000
|
|
@@ -709,18 +1808,98 @@ class MemoryEngine {
|
|
|
709
1808
|
return true
|
|
710
1809
|
}
|
|
711
1810
|
|
|
1811
|
+
/**
|
|
1812
|
+
* F1(M6-CONTRACT 外补丁,2026-08-23):anchor 开启时的记录级压缩。
|
|
1813
|
+
* 按 sidecar 记录序整条保留/移除——今天(标题日期=插件今天)记录无条件保留,其余按字节配额
|
|
1814
|
+
* 从最新往回保留(layer 上限 user 4000/note 3000);被移除记录整条原文写入归档文件
|
|
1815
|
+
* (writeFullRaw,不经 anchor 事务,归档非检索语料)。重组经 store.replace 原子执行:
|
|
1816
|
+
* 候选预检(clean)→被移除 id 自动删除→保留 id/版本不变。永不字符切片。
|
|
1817
|
+
* @returns {Promise<boolean|'not-anchored'>} true=已腾位 false=无法腾位 'not-anchored'=无 sidecar 走旧路径
|
|
1818
|
+
*/
|
|
1819
|
+
async compactAnchoredLayer(store, filePath, layer, today, paths) {
|
|
1820
|
+
const cur = await this.readTextSafe(filePath)
|
|
1821
|
+
if (!cur || !cur.trim()) return false
|
|
1822
|
+
let parsed
|
|
1823
|
+
try { parsed = parseAnchors(Buffer.from(cur, 'utf8')) } catch (_) { return 'not-anchored' }
|
|
1824
|
+
if (!parsed || parsed.status !== 'clean') return false // 脏文件 fail closed:不压缩、不切片
|
|
1825
|
+
const anchored = parsed.records.filter((r) => r.kind === 'anchored')
|
|
1826
|
+
if (!anchored.length) return 'not-anchored'
|
|
1827
|
+
const limit = layer === 'user' ? 4000 : 3000
|
|
1828
|
+
const isTodayHeading = (heading) => /^\d{4}-\d{2}-\d{2}/.test(String(heading || '')) && String(heading).slice(0, 10) === today
|
|
1829
|
+
const buf = Buffer.from(cur, 'utf8')
|
|
1830
|
+
// 配额从尾回溯(F1):新的留;今天记录无条件保留
|
|
1831
|
+
const keep = new Array(anchored.length).fill(false)
|
|
1832
|
+
let keptBytes = 0
|
|
1833
|
+
for (let i = anchored.length - 1; i >= 0; i--) {
|
|
1834
|
+
const rec = anchored[i]
|
|
1835
|
+
const spanBytes = rec.byteEnd - rec.markerByteStart
|
|
1836
|
+
if (isTodayHeading(rec.heading) || keptBytes + spanBytes <= limit) { keep[i] = true; keptBytes += spanBytes }
|
|
1837
|
+
}
|
|
1838
|
+
const removedIdx = []
|
|
1839
|
+
anchored.forEach((rec, i) => { if (!keep[i]) removedIdx.push(i) })
|
|
1840
|
+
if (!removedIdx.length) return false // 无可移除 → 无法腾位(预算照常拒绝)
|
|
1841
|
+
// 有序块重组:legacy 块与 preamble/tail 原样保留,gap 字节逐段拷贝,仅丢弃被移除记录的 [marker..contentEnd]
|
|
1842
|
+
const blocks = []
|
|
1843
|
+
for (const r of parsed.records) {
|
|
1844
|
+
if (r.kind === 'anchored') blocks.push({ start: r.markerByteStart, end: r.byteEnd, keep: keep[anchored.indexOf(r)] })
|
|
1845
|
+
else blocks.push({ start: r.byteStart, end: r.byteEnd, keep: true })
|
|
1846
|
+
}
|
|
1847
|
+
blocks.sort((a, b) => a.start - b.start)
|
|
1848
|
+
const parts = []
|
|
1849
|
+
let cursorB = 0
|
|
1850
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1851
|
+
const b = blocks[i]
|
|
1852
|
+
if (b.start > cursorB) parts.push(buf.slice(cursorB, b.start).toString('utf8'))
|
|
1853
|
+
if (b.keep) parts.push(buf.slice(b.start, b.end).toString('utf8'))
|
|
1854
|
+
cursorB = b.end
|
|
1855
|
+
}
|
|
1856
|
+
if (cursorB < buf.length) parts.push(buf.slice(cursorB).toString('utf8'))
|
|
1857
|
+
const newText = parts.join('')
|
|
1858
|
+
// 归档:整条原文(writeFullRaw 绕开 anchor 事务;归档文件从不参与解析/索引)
|
|
1859
|
+
const archiveFile = layer === 'user'
|
|
1860
|
+
? path.join(dshHome(), 'memory', 'archived-user.md')
|
|
1861
|
+
: path.join(paths.projectDir, 'archive', 'notes-archived.md')
|
|
1862
|
+
const archiveText = removedIdx.map((i2) => {
|
|
1863
|
+
const r = anchored[i2]
|
|
1864
|
+
return buf.slice(r.markerByteStart, r.byteEnd).toString('utf8')
|
|
1865
|
+
}).join('\n')
|
|
1866
|
+
const existingArchive = await this.readTextSafe(archiveFile)
|
|
1867
|
+
await this.writeFullRaw(archiveFile, (existingArchive ? existingArchive.replace(/\s+$/, '') + '\n' : '') + archiveText + '\n')
|
|
1868
|
+
const rr = await store.replace(filePath, newText)
|
|
1869
|
+
if (!rr.ok) { console.error('[compacted-anchored] replace failed: ' + rr.reason); return false }
|
|
1870
|
+
const freshText = await this.readTextSafe(filePath)
|
|
1871
|
+
if (layer === 'user') this.state.userText = freshText
|
|
1872
|
+
else this.state.notesText = freshText
|
|
1873
|
+
if (this._budgets) {
|
|
1874
|
+
const b = this._budgets.get('today')
|
|
1875
|
+
if (b && b.date === this.memToday()) b[layer] = 0
|
|
1876
|
+
}
|
|
1877
|
+
this._lastCompactAt = Date.now()
|
|
1878
|
+
this.state.loadedAt = Date.now()
|
|
1879
|
+
console.log('[compacted-anchored] ' + layer + ': kept=' + (anchored.length - removedIdx.length) + '/' + anchored.length + ' archived=' + removedIdx.length + ' bytes=' + cur.length + '->' + newText.length)
|
|
1880
|
+
return true
|
|
1881
|
+
}
|
|
1882
|
+
|
|
712
1883
|
// ---------- 注入渲染(同步,基于缓存) ----------
|
|
713
1884
|
// 动态记忆 → ctx.systemPrompt.context()(user-role 快照):内容变化才追加新快照,内容不变不重复注入(dsh-agent-loop project() 去重),
|
|
714
1885
|
// system prompt 不再包含动态内容 → 字节级稳定 → DeepSeek 前缀缓存全程命中(对比 section 方案:动态内容任何变化都从变化点起击穿整个前缀,含全部历史)
|
|
715
1886
|
renderMemoryDynamic(context) {
|
|
716
1887
|
const s = this.state
|
|
717
1888
|
const cfg = this.config
|
|
718
|
-
|
|
1889
|
+
// 2026-08-27 频率控制:同内容在 snapshotMinGapRounds 轮内不重复注入(减少历史膨胀)。
|
|
1890
|
+
// 用"内容指纹"判断是否变化:剔除铭文日期行(无意义变化源)后,仅日志/反思/笔记等实际内容变化才触发。
|
|
1891
|
+
const budget = Math.max(Number(cfg.injectBudgetChars) || 1600, 400)
|
|
719
1892
|
const lines = []
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1893
|
+
// 自定义 prompt 层(2026-08-27):用户可覆盖各层文案;空覆盖=默认。占位符替换。
|
|
1894
|
+
const L = (key, vars) => {
|
|
1895
|
+
const ov = cfg.promptLayerOverrides && cfg.promptLayerOverrides[key]
|
|
1896
|
+
let txt = (ov && ov.trim() !== '' ? ov : (DEFAULT_PROMPT_LAYERS[key] || ''))
|
|
1897
|
+
if (txt) for (const k of Object.keys(vars || {})) txt = txt.split('{' + k + '}').join(String(vars[k]))
|
|
1898
|
+
return txt
|
|
1899
|
+
}
|
|
1900
|
+
const d = this.memToday()
|
|
1901
|
+
lines.push(L('snapshotHead') || '<memory_system>')
|
|
1902
|
+
lines.push(L('snapshotMeta', { date: d, ws: s.ws || '(未知)', dayBoundary: Number(cfg.dayBoundaryMinutes) || 450, consolidate: cfg.autoConsolidate === false ? ' | 自动沉淀: 已关闭' : ' | 自动沉淀: 每轮对话结束自动评估' }))
|
|
724
1903
|
// 记忆地图:告诉模型其他工作区记忆存在,需要时用 memory_recall 跨区检索
|
|
725
1904
|
if (s.workspaceMap && s.workspaceMap.length) {
|
|
726
1905
|
lines.push('其他工作区记忆(开发/排查时可调用 memory_recall 检索其日志/笔记): ' + s.workspaceMap.join('、'))
|
|
@@ -736,14 +1915,14 @@ class MemoryEngine {
|
|
|
736
1915
|
// 读取顺序:progress(工作日志/反思)先行,再读 memory(用户级/项目笔记)
|
|
737
1916
|
if (s.recentLogs.length) {
|
|
738
1917
|
const recent = s.recentLogs.map((r) => '[' + r.date + '] ' + r.text.replace(/\n+/g, ' | ')).join('\n')
|
|
739
|
-
part('
|
|
1918
|
+
part(L('snapshotLogsTitle', { n: s.recentLogs.length }), scrubJunkLines(recent, { dedup: false }).clean, sub)
|
|
740
1919
|
}
|
|
741
1920
|
if (s.latestReflection) {
|
|
742
|
-
part('
|
|
1921
|
+
part(L('snapshotReflectionTitle', { date: s.latestReflectionDate }), reflectionDigest(s.latestReflection), sub)
|
|
743
1922
|
}
|
|
744
1923
|
// 敏感段落(凭据/token/密钥等)不注入 prompt,避免密钥暴露给模型;脏内容(乱码/重复/外部文档)清洗后再注入
|
|
745
|
-
part('
|
|
746
|
-
part('
|
|
1924
|
+
part(L('snapshotUserTitle'), stripSensitiveSections(sanitizeForInjection(s.userText)), sub)
|
|
1925
|
+
part(L('snapshotNotesTitle') + ' ' + (s.notesPath || (cfg.projectMemoryDir + '/MEMORY.md')), stripSensitiveSections(sanitizeForInjection(s.notesText)), sub)
|
|
747
1926
|
// 外部记忆摘要(其他 AI 工具遗产)
|
|
748
1927
|
if (this.external.cache && this.external.cache.length) {
|
|
749
1928
|
const extBudget = Math.max(Number(cfg.externalInjectionChars) || 1400, 200)
|
|
@@ -754,27 +1933,28 @@ class MemoryEngine {
|
|
|
754
1933
|
return '· ' + x.name + '(' + x.tool + '): 绝对路径 ' + (paths || '(未知)')
|
|
755
1934
|
})
|
|
756
1935
|
.slice(0, 3)
|
|
757
|
-
if (ext.length) lines.push('\n
|
|
1936
|
+
if (ext.length) lines.push('\n' + L('snapshotExternalTitle') + '\n' + ext.join('\n') + '\n需要这些记忆时:直接读取上述绝对路径文件(你有文件读取能力),或用 memory_recall 按需检索;不要凭空猜测其内容。')
|
|
758
1937
|
const sess = this.external.cache.filter((x) => x.kind === 'sessions')
|
|
759
1938
|
if (sess.length) {
|
|
760
1939
|
lines.push('· 历史会话索引: ' + sess.map((x) => x.name + ' ' + x.files.length + ' 个').join(', ') + ' —— 需要时用 memory_recall 检索。')
|
|
761
1940
|
}
|
|
762
1941
|
}
|
|
763
|
-
// 日历/日程注入(让 AI 主动感知 deadline/约定)
|
|
764
|
-
if (this.state.calendarText && this.state.calendarText.trim()) {
|
|
1942
|
+
// 日历/日程注入(让 AI 主动感知 deadline/约定)——无人值守模式下剥离(不注入提醒类内容)
|
|
1943
|
+
if (this.state.calendarText && this.state.calendarText.trim() && !this.isUnattendedNow()) {
|
|
765
1944
|
const calEntries = this.parseCalendar(this.state.calendarText).filter((en) => !en.done && en.date >= todayStr()).slice(0, 10)
|
|
766
1945
|
if (calEntries.length) {
|
|
767
1946
|
const calLines = calEntries.map((en) => '· ' + en.date + ' ' + en.time + ' | ' + en.quadrant + ' | ' + en.title).join('\n')
|
|
768
|
-
lines.push('\n
|
|
1947
|
+
lines.push('\n' + L('snapshotCalendarTitle') + '\n' + calLines + '\n主动关注这些安排:对话中若提及相关时间点,主动用 calendar_add 补充新事项、calendar_done 标记完成、calendar_remove 删除过期事项;回复正文中向用户转述日历变更。')
|
|
769
1948
|
}
|
|
770
1949
|
}
|
|
771
1950
|
// 暂离回来提示:距上次活动>1小时,要求 agent 在回复开头写欢迎语并提示打开记忆窗口
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
lines.push('
|
|
1951
|
+
// ——无人值守模式下剥离(不注入社交性/行为性指令,避免无人值守任务浪费 token 在寒暄上)
|
|
1952
|
+
if (!this.isUnattendedNow() && this._lastActiveAt && Date.now() - this._lastActiveAt > 3600000) {
|
|
1953
|
+
lines.push('\n' + L('snapshotWelcomeTitle'))
|
|
1954
|
+
lines.push(L('snapshotWelcomeBody'))
|
|
775
1955
|
}
|
|
776
|
-
lines.push('\n
|
|
777
|
-
lines.push('</memory_system>')
|
|
1956
|
+
lines.push('\n' + L('snapshotInscription', { date: this.memToday() })) // 动态快照追加在历史尾部,变化只 miss 快照本身;秒级时间戳也不再击穿 system prompt 前缀
|
|
1957
|
+
lines.push(L('snapshotTail') || '</memory_system>')
|
|
778
1958
|
return lines.join('\n')
|
|
779
1959
|
}
|
|
780
1960
|
|
|
@@ -854,7 +2034,26 @@ class MemoryEngine {
|
|
|
854
2034
|
}
|
|
855
2035
|
|
|
856
2036
|
// ---------- 写操作 ----------
|
|
2037
|
+
/** M3b-3:anchor 写入事务层(仅 memoryAnchorEnabled=true 时创建;sidecar 落盘 DSH_HOME/memory/index/files)。 */
|
|
2038
|
+
get docStore() {
|
|
2039
|
+
if (this.config.memoryAnchorEnabled !== true) return null
|
|
2040
|
+
if (!this._docStore) {
|
|
2041
|
+
this._docStore = new MemoryDocumentStore({
|
|
2042
|
+
sidecarDir: path.join(dshHome(), 'memory', 'index', 'files'),
|
|
2043
|
+
})
|
|
2044
|
+
}
|
|
2045
|
+
return this._docStore
|
|
2046
|
+
}
|
|
2047
|
+
|
|
857
2048
|
async appendText(p, text) {
|
|
2049
|
+
if (process.env.DSH_F1_DEBUG) console.error('[f1-diag] appendText -> ' + p + ' len=' + String(text || '').length)
|
|
2050
|
+
// M3b-3 分流:anchor 开启 → 记忆文档走原子写入事务(稳定 marker/ID);关闭 → 原逻辑逐字节不变。
|
|
2051
|
+
const store = this.docStore
|
|
2052
|
+
if (store) {
|
|
2053
|
+
const r = await store.append(p, String(text == null ? '' : String(text)))
|
|
2054
|
+
if (!r.ok) throw new Error('memory-anchor-append-failed:' + r.reason)
|
|
2055
|
+
return await this.readTextSafe(p)
|
|
2056
|
+
}
|
|
858
2057
|
const existing = await this.readTextSafe(p)
|
|
859
2058
|
const body = existing ? existing.replace(/\s+$/, '') + '\n' + text : text
|
|
860
2059
|
await mkdir(path.dirname(p), { recursive: true })
|
|
@@ -862,7 +2061,31 @@ class MemoryEngine {
|
|
|
862
2061
|
return body
|
|
863
2062
|
}
|
|
864
2063
|
|
|
2064
|
+
/** 原始整篇写(不经 anchor 分流):CALENDAR.md 与非记忆文件专用(契约 §2.11 排除项)。 */
|
|
2065
|
+
async writeFullRaw(p, text) {
|
|
2066
|
+
await mkdir(path.dirname(p), { recursive: true })
|
|
2067
|
+
await writeFile(p, text, 'utf8')
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/** 单记录整篇写(anchor 开启=单 marker 包裹全文档;关闭=原始写):reflection 等单记录文档专用。 */
|
|
2071
|
+
async writeFullSingle(p, text) {
|
|
2072
|
+
const store = this.docStore
|
|
2073
|
+
if (store) {
|
|
2074
|
+
const r = await store.replaceSingle(p, String(text == null ? '' : String(text)))
|
|
2075
|
+
if (!r.ok) throw new Error('memory-anchor-replace-single-failed:' + r.reason)
|
|
2076
|
+
return
|
|
2077
|
+
}
|
|
2078
|
+
await this.writeFullRaw(p, text)
|
|
2079
|
+
}
|
|
2080
|
+
|
|
865
2081
|
async writeFull(p, text) {
|
|
2082
|
+
// M3b-3 分流:anchor 开启 → §9 整篇替换语义(保留已有 ID/新块分配 ID/省略删除);关闭 → 原逻辑。
|
|
2083
|
+
const store = this.docStore
|
|
2084
|
+
if (store) {
|
|
2085
|
+
const r = await store.replace(p, String(text == null ? '' : String(text)))
|
|
2086
|
+
if (!r.ok) throw new Error('memory-anchor-replace-failed:' + r.reason)
|
|
2087
|
+
return
|
|
2088
|
+
}
|
|
866
2089
|
await mkdir(path.dirname(p), { recursive: true })
|
|
867
2090
|
await writeFile(p, text, 'utf8')
|
|
868
2091
|
}
|
|
@@ -1046,8 +2269,18 @@ class MemoryEngine {
|
|
|
1046
2269
|
}
|
|
1047
2270
|
|
|
1048
2271
|
// ---------- 工作区总览(跨工作区全局总结) ----------
|
|
1049
|
-
/** 读 jsonl 首行(session header)。 */
|
|
2272
|
+
/** 读 jsonl 首行(session header);.jsonl 明文流式读首行,.jsonl.zstd 为 zstd 压缩帧(整块解压后取首行,与 dsh 核心 dsh-session-persistence-jsonl 一致)。 */
|
|
1050
2273
|
readFirstLine(p) {
|
|
2274
|
+
if (p.endsWith('.zstd')) {
|
|
2275
|
+
return readFile(p).then((buf) => {
|
|
2276
|
+
try {
|
|
2277
|
+
const dec = zstdDec ? zstdDec(buf) : null
|
|
2278
|
+
const text = (dec || buf).toString('utf8')
|
|
2279
|
+
const i = text.indexOf('\n')
|
|
2280
|
+
return i >= 0 ? text.slice(0, i) : text
|
|
2281
|
+
} catch (e) { return '' }
|
|
2282
|
+
}).catch(() => '')
|
|
2283
|
+
}
|
|
1051
2284
|
return new Promise((resolve) => {
|
|
1052
2285
|
const rs = createReadStream(p, { encoding: 'utf8' })
|
|
1053
2286
|
let buf = ''
|
|
@@ -1072,7 +2305,7 @@ class MemoryEngine {
|
|
|
1072
2305
|
for (const en of entries) {
|
|
1073
2306
|
if (out.size >= 30) return
|
|
1074
2307
|
if (en.isDirectory()) await walk(path.join(dir, en.name), depth + 1)
|
|
1075
|
-
else if (en.isFile() && en.name.endsWith('.jsonl')) {
|
|
2308
|
+
else if (en.isFile() && (en.name.endsWith('.jsonl.zstd') || en.name.endsWith('.jsonl'))) {
|
|
1076
2309
|
try {
|
|
1077
2310
|
const first = await this.readFirstLine(path.join(dir, en.name))
|
|
1078
2311
|
if (first) {
|
|
@@ -1112,7 +2345,11 @@ class MemoryEngine {
|
|
|
1112
2345
|
const raw = await this.readTextSafe(cacheFile)
|
|
1113
2346
|
if (raw) {
|
|
1114
2347
|
const j = JSON.parse(raw)
|
|
1115
|
-
|
|
2348
|
+
// 空结果只短时信任(≤30 分钟),防止旧版 bug 产出的"空缓存"被永久复用导致永远显示"未发现带记忆的工作区"
|
|
2349
|
+
const ageMs = Date.now() - (Number(j && j.generatedAt) || 0)
|
|
2350
|
+
const list = j && Array.isArray(j.workspaces) ? j.workspaces : null
|
|
2351
|
+
const valid = !!list && (list.length > 0 || (ageMs >= 0 && ageMs <= 30 * 60 * 1000))
|
|
2352
|
+
if (valid && j.graph && Array.isArray(j.graph.topics) && Array.isArray(j.graph.links)) return { workspaces: list, graph: j.graph, cached: true, generatedAt: j.generatedAt }
|
|
1116
2353
|
}
|
|
1117
2354
|
} catch (e) {}
|
|
1118
2355
|
}
|
|
@@ -1125,6 +2362,11 @@ class MemoryEngine {
|
|
|
1125
2362
|
const fallbackItems = mem.logs.flatMap((l) => l.text.split('\n').filter((x) => x.trim().startsWith('- ')).map((x) => x.trim().replace(/^- /, '').slice(0, 120))).slice(-5)
|
|
1126
2363
|
return { path: cwd, name, input: { name, logs: logText.slice(0, 5200) || '(无)', notes: mem.notes || '(无)' }, fallbackItems, logCount: fallbackItems.length, dateRange: mem.logs.length ? (mem.logs[mem.logs.length - 1].date + ' ~ ' + mem.logs[0].date) : '' }
|
|
1127
2364
|
}))).filter(Boolean)
|
|
2365
|
+
if (!records.length) {
|
|
2366
|
+
const empty = { workspaces: [], graph: { topics: [], links: [] }, generatedAt: Date.now() }
|
|
2367
|
+
try { await mkdir(path.dirname(cacheFile), { recursive: true }); await writeFile(cacheFile, JSON.stringify(empty, null, 2), 'utf8') } catch (e) {}
|
|
2368
|
+
return { workspaces: [], graph: empty.graph, cached: false, generatedAt: empty.generatedAt }
|
|
2369
|
+
}
|
|
1128
2370
|
const fallback = records.map((r) => ({ path: r.path, name: r.name, summary: '', items: r.fallbackItems, graphTopics: r.fallbackItems.slice(0, 4).map((label) => ({ label, detail: '' })), logCount: r.logCount, dateRange: r.dateRange }))
|
|
1129
2371
|
let workspaces = fallback
|
|
1130
2372
|
let graph = { topics: [], links: [] }
|
|
@@ -1205,17 +2447,92 @@ class MemoryEngine {
|
|
|
1205
2447
|
indexPath,
|
|
1206
2448
|
indexMtime,
|
|
1207
2449
|
needsRestart: !!(indexMtime && indexMtime > startTime + 5000),
|
|
2450
|
+
// 2026-08-28 诊断:进程内 jsEmitMode() 真实返回(读 embedding-config.json 的
|
|
2451
|
+
// activationEmitMode + 5s 缓存)。用于核对设置面板与判定链读数是否一致。
|
|
2452
|
+
jsEmitModeLive: (() => { try { return typeof this.jsEmitMode === 'function' ? this.jsEmitMode() : 'n/a' } catch (_) { return 'err' } })(),
|
|
1208
2453
|
},
|
|
1209
2454
|
config: this.config,
|
|
1210
2455
|
heartbeat,
|
|
2456
|
+
// M0/M1: 会话隔离调试状态(每个 runtime 的观察计数与游标,供隔离测试/调试中心检查)
|
|
2457
|
+
associativeMemory: {
|
|
2458
|
+
enabled: this.config.associativeMemoryEnabled === true,
|
|
2459
|
+
// M2: 观察账本只读调试视图(ring 容量与进程级统计;不改变任何既有字段语义)
|
|
2460
|
+
observer: {
|
|
2461
|
+
schemaVersion: OBSERVER_SCHEMA_VERSION,
|
|
2462
|
+
// 默认关闭语义契约(方案 B):false 时不建 ring、不存 payload,仅最小计数
|
|
2463
|
+
observationStorage: this.config.associativeMemoryEnabled === true ? 'full' : 'counters-only',
|
|
2464
|
+
envelopeRingLimit: ENVELOPE_RING_LIMIT,
|
|
2465
|
+
segmentRingLimit: SEGMENT_RING_LIMIT,
|
|
2466
|
+
segmentCharBudget: SEGMENT_RING_CHAR_BUDGET,
|
|
2467
|
+
seedReplayMaxEvents: SEED_REPLAY_MAX_EVENTS,
|
|
2468
|
+
ingestedEnvelopes: this._observerStats.ingestedEnvelopes,
|
|
2469
|
+
segmentsCreated: this._observerStats.segmentsCreated,
|
|
2470
|
+
droppedNoOwner: this._observerStats.droppedNoOwner,
|
|
2471
|
+
disabledObservations: this._observerStats.disabledObservations,
|
|
2472
|
+
seedTruncatedEvents: this._observerStats.seedTruncatedEvents,
|
|
2473
|
+
},
|
|
2474
|
+
// M3a: 只读记忆索引快照(默认关闭时仅 {enabled:false},零 IO)
|
|
2475
|
+
memoryIndex: await this.memoryIndexSnapshot(),
|
|
2476
|
+
// M4-3: Shadow Retrieval 最小投影(§17;关闭时严格 {enabled:false},无 query/excerpt/path)
|
|
2477
|
+
shadowRetrieval: this._shadowHost ? this._shadowHost.debugView() : { enabled: false },
|
|
2478
|
+
// M5-3: Context Bridge 最小投影(M5-CONTRACT §17 式;关闭时严格 {enabled:false})
|
|
2479
|
+
contextBridge: this._contextHost ? this._contextHost.debugView() : { enabled: false },
|
|
2480
|
+
// M6-3: Activation Inbox 最小投影(关闭时严格 {enabled:false})
|
|
2481
|
+
activationInbox: this._activationHost ? this._activationHost.debugView() : { enabled: false },
|
|
2482
|
+
// M7-0: Python sidecar 最小投影(enabled=配置门;started=false 即零进程零 IO)
|
|
2483
|
+
pythonBackend: (() => {
|
|
2484
|
+
const c = this._pythonSidecar
|
|
2485
|
+
return Object.assign({ enabled: this.config.pythonBackendEnabled === true }, c ? c.debugView() : {})
|
|
2486
|
+
})(),
|
|
2487
|
+
indexSyncHost: this._indexSyncHost ? this._indexSyncHost.debugView() : { enabled: false },
|
|
2488
|
+
runtimes: this.runtimes.values().map((rt) => ({
|
|
2489
|
+
key: rt.key,
|
|
2490
|
+
sessionId: rt.sessionId,
|
|
2491
|
+
agentId: rt.agentId,
|
|
2492
|
+
ws: rt.state.ws,
|
|
2493
|
+
contextVersion: rt.contextVersion,
|
|
2494
|
+
eventCursor: rt.eventCursor,
|
|
2495
|
+
// 惰性分配(审查修复轮2):关闭模式下 ring 为 null,视图按零处理
|
|
2496
|
+
envelopes: rt.envelopes ? rt.envelopes.length : 0,
|
|
2497
|
+
segments: rt.segments ? rt.segments.length : 0,
|
|
2498
|
+
nativeCursor: rt.nativeCursor,
|
|
2499
|
+
// M2 调试尾窗: 最近 8 条 envelope/segment 的身份字段(无文本 payload),供回放一致性检查。
|
|
2500
|
+
// 审查修复轮:envelope 尾窗增补 timestamp(原生事实时间)与 ok/errorName/errorCode 标量(工具失败诊断),
|
|
2501
|
+
// 不暴露 payload 本体。
|
|
2502
|
+
envelopeTail: (rt.envelopes ? rt.envelopes.items : []).slice(-8).map((env) => ({
|
|
2503
|
+
eventSeq: env.eventSeq, channel: env.channel, eventType: env.eventType,
|
|
2504
|
+
nativeSeq: env.nativeSeq === undefined ? null : env.nativeSeq,
|
|
2505
|
+
callId: env.callId || null, rootCallId: env.rootCallId || null,
|
|
2506
|
+
sourceKind: env.sourceKind, payloadDigest: env.payloadDigest,
|
|
2507
|
+
timestamp: env.timestamp,
|
|
2508
|
+
ok: env.payload && typeof env.payload.ok === 'boolean' ? env.payload.ok : null,
|
|
2509
|
+
errorName: env.payload && env.payload.errorName != null ? env.payload.errorName : null,
|
|
2510
|
+
errorCode: env.payload && env.payload.errorCode != null ? env.payload.errorCode : null,
|
|
2511
|
+
})),
|
|
2512
|
+
segmentTail: (rt.segments ? rt.segments.items : []).slice(-8).map((seg) => ({
|
|
2513
|
+
id: seg.id, kind: seg.kind, eventType: seg.eventType, eventSeq: seg.eventSeq,
|
|
2514
|
+
nativeSeq: seg.nativeSeq === undefined ? null : seg.nativeSeq,
|
|
2515
|
+
contextVersion: seg.contextVersion, digest: seg.digest,
|
|
2516
|
+
})),
|
|
2517
|
+
lastEventKind: rt.debug.lastEventKind,
|
|
2518
|
+
lastEventAt: rt.debug.lastEventAt,
|
|
2519
|
+
// 审查修复轮:lastEventSeq 改为标量(关闭模式下不保存 envelope 对象,仅保留最小计数)
|
|
2520
|
+
lastEventSeq: rt.debug.lastEventSeq || 0,
|
|
2521
|
+
consolidating: !!rt.consolidating,
|
|
2522
|
+
pendingConsolidations: rt.pendingConsolidations.length,
|
|
2523
|
+
lastTurn: rt.lastTurn,
|
|
2524
|
+
lastActiveAt: rt.lastActiveAt,
|
|
2525
|
+
disposed: rt.disposed,
|
|
2526
|
+
})),
|
|
2527
|
+
},
|
|
1211
2528
|
autoConsolidate: {
|
|
1212
2529
|
enabled: this.config.autoConsolidate !== false,
|
|
1213
2530
|
minChars: Math.max(Number(this.config.autoConsolidateMinChars) || 240, 80),
|
|
1214
2531
|
cooldownMinutes: Math.max(Number(this.config.autoConsolidateCooldownMinutes) || 30, 1),
|
|
1215
2532
|
dailyMax: Math.max(Number(this.config.autoConsolidateDailyMax) || 8, 1),
|
|
1216
2533
|
callCountToday: this._autoCallCount || 0,
|
|
1217
|
-
consolidating:
|
|
1218
|
-
pendingQueue:
|
|
2534
|
+
consolidating: this.runtimes.values().some((rt) => !!rt.consolidating),
|
|
2535
|
+
pendingQueue: this.runtimes.values().reduce((sum, rt) => sum + rt.pendingConsolidations.length, 0),
|
|
1219
2536
|
stats: this.autoStats,
|
|
1220
2537
|
},
|
|
1221
2538
|
subagents: { available: !!this._subagents, providers },
|
|
@@ -1258,7 +2575,7 @@ class MemoryEngine {
|
|
|
1258
2575
|
if (!content) return 'memory_reflect: text 为空,未保存。'
|
|
1259
2576
|
const p = await this.resolvePaths(agent)
|
|
1260
2577
|
const file = path.join(p.reflectDir, date + '.md')
|
|
1261
|
-
await this.
|
|
2578
|
+
await this.writeFullSingle(file, '# 反思 ' + date + '\n\n' + content)
|
|
1262
2579
|
this.state.latestReflection = content
|
|
1263
2580
|
this.state.latestReflectionDate = date
|
|
1264
2581
|
if (this.state.pendingReflection && this.state.pendingReflection.date === date) {
|
|
@@ -1316,7 +2633,7 @@ class MemoryEngine {
|
|
|
1316
2633
|
quadrant: item.quadrant || '未分类', title: String(item.title || '').trim(), note: String(item.note || '').trim(),
|
|
1317
2634
|
})
|
|
1318
2635
|
const body = this.renderCalendar(entries)
|
|
1319
|
-
await this.
|
|
2636
|
+
await this.writeFullRaw(p.calendarPath, body)
|
|
1320
2637
|
this.state.calendarText = body; this.state.loadedAt = Date.now()
|
|
1321
2638
|
return '已加入日历: ' + item.date + ' ' + (item.time || '') + ' ' + item.title + ' (' + (item.quadrant || '未分类') + ')'
|
|
1322
2639
|
}
|
|
@@ -1329,7 +2646,7 @@ class MemoryEngine {
|
|
|
1329
2646
|
if (!hit) return '未找到该日历条目: ' + date + ' ' + time + ' ' + title
|
|
1330
2647
|
hit.done = true
|
|
1331
2648
|
const body = this.renderCalendar(entries)
|
|
1332
|
-
await this.
|
|
2649
|
+
await this.writeFullRaw(p.calendarPath, body)
|
|
1333
2650
|
this.state.calendarText = body; this.state.loadedAt = Date.now()
|
|
1334
2651
|
return '已标记完成: ' + date + ' ' + title
|
|
1335
2652
|
}
|
|
@@ -1342,7 +2659,7 @@ class MemoryEngine {
|
|
|
1342
2659
|
const kept = entries.filter((en) => !(en.date === date && en.time === time && en.title === title))
|
|
1343
2660
|
if (kept.length === before) return '未找到该日历条目: ' + date + ' ' + time + ' ' + title
|
|
1344
2661
|
const body = this.renderCalendar(kept)
|
|
1345
|
-
await this.
|
|
2662
|
+
await this.writeFullRaw(p.calendarPath, body)
|
|
1346
2663
|
this.state.calendarText = body; this.state.loadedAt = Date.now()
|
|
1347
2664
|
return '已删除日历条目: ' + date + ' ' + title
|
|
1348
2665
|
}
|
|
@@ -1547,13 +2864,46 @@ class MemoryEngine {
|
|
|
1547
2864
|
let run
|
|
1548
2865
|
try {
|
|
1549
2866
|
// prompt 必须是 block 数组(createUserMessage 校验 content.some)
|
|
2867
|
+
const subModel = String(this.config.subagentModel || '').trim()
|
|
1550
2868
|
run = await subagents.start(providerName, {
|
|
1551
2869
|
label,
|
|
1552
2870
|
prompt: [{ type: 'text', text }],
|
|
1553
2871
|
signal: controller.signal,
|
|
1554
2872
|
...(parent ? { parent } : {}),
|
|
2873
|
+
// 设置页「总结/问候默认模型」:非空时覆盖子代理的模型选择(空=跟随路由默认)
|
|
2874
|
+
...(subModel ? { agentOptions: { model: subModel } } : {}),
|
|
1555
2875
|
})
|
|
1556
|
-
|
|
2876
|
+
// UNKNOWN_MODEL 自保:子代理路由为两源拼接(provider 继承父会话 + 模型落宿主全局默认),
|
|
2877
|
+
// 父 provider 目录可能不含该模型(如 opencode-go-free × deepseek-v4-flash);
|
|
2878
|
+
// 此时回退用父会话抽屉当前模型(settings.yaml agent-default-model)重试一次。
|
|
2879
|
+
let result
|
|
2880
|
+
try {
|
|
2881
|
+
result = await run.result
|
|
2882
|
+
} catch (eRoute) {
|
|
2883
|
+
const em0 = eRoute && eRoute.message ? String(eRoute.message) : ''
|
|
2884
|
+
if (!/UNKNOWN_MODEL|has no configured model/.test(em0)) throw eRoute
|
|
2885
|
+
diag('subagent ' + label + ' UNKNOWN_MODEL fallback -> agent-default-model')
|
|
2886
|
+
let fbModel = ''
|
|
2887
|
+
let fbProvider = ''
|
|
2888
|
+
try {
|
|
2889
|
+
const rawCfg = await readFile(path.join(dshHome(), 'settings.yaml'), 'utf8')
|
|
2890
|
+
// agent-default-model 是 provider+model 一对;仅取 model 而不换 provider 会继续 UNKNOWN_MODEL
|
|
2891
|
+
const mFb = String(rawCfg).match(/agent-default-model:[\s\S]{0,200}?\n\s*provider:\s*([^\s#]+)/)
|
|
2892
|
+
const mFm = String(rawCfg).match(/agent-default-model:[\s\S]{0,200}?\n\s*model:\s*([^\s#]+)/)
|
|
2893
|
+
if (mFm) fbModel = mFm[1].trim()
|
|
2894
|
+
if (mFb) fbProvider = mFb[1].trim()
|
|
2895
|
+
} catch (eFs) {}
|
|
2896
|
+
if (!fbModel) throw eRoute
|
|
2897
|
+
diag('subagent ' + label + ' fallback model: ' + fbModel + ' @ ' + (fbProvider || providerName))
|
|
2898
|
+
const run2 = await subagents.start(fbProvider || providerName, {
|
|
2899
|
+
label,
|
|
2900
|
+
prompt: [{ type: 'text', text }],
|
|
2901
|
+
signal: controller.signal,
|
|
2902
|
+
...(parent ? { parent } : {}),
|
|
2903
|
+
agentOptions: { model: fbModel },
|
|
2904
|
+
})
|
|
2905
|
+
result = await run2.result
|
|
2906
|
+
}
|
|
1557
2907
|
const blocks = result && result.output ? result.output : []
|
|
1558
2908
|
return blocks.filter((b) => b && b.type === 'text').map((b) => b.text).join('').trim()
|
|
1559
2909
|
} catch (e) {
|
|
@@ -1584,11 +2934,12 @@ class MemoryEngine {
|
|
|
1584
2934
|
/** 每轮对话结束自动沉淀:取本轮 user+assistant 消息 → subagent 判断/提炼 → 写今日日志+升格。 */
|
|
1585
2935
|
async consolidateTurn(turn, agent) {
|
|
1586
2936
|
const why = (reason) => diag('consolidate skip: ' + reason + ' (turn=' + JSON.stringify(turn) + ' agentId=' + ((agent && (agent.id || (agent.session && agent.session.id))) || '?') + ')')
|
|
2937
|
+
const runtime = this.runtimeFor(agent || this.currentRuntime().agent)
|
|
1587
2938
|
// 声明提到函数级:异步 IIFE 与 try 块各自作用域,块内 let 在外面不可见(曾导致 userText is not defined)
|
|
1588
2939
|
let userText = ''
|
|
1589
2940
|
let assistantText = ''
|
|
1590
2941
|
try {
|
|
1591
|
-
if (
|
|
2942
|
+
if (runtime.consolidating) { why('runtime consolidating busy'); return }
|
|
1592
2943
|
if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
|
|
1593
2944
|
if (this.config.autoConsolidate === false) { why('config.autoConsolidate=false'); return }
|
|
1594
2945
|
if (!agent || !agent.session) { why('no agent/session'); return }
|
|
@@ -1603,28 +2954,92 @@ class MemoryEngine {
|
|
|
1603
2954
|
const cooldownMs = baseCooldown * 60000 * ((hourNow >= 22 || hourNow < 8) ? 2 : 1)
|
|
1604
2955
|
const dailyMax = Math.max(Number(this.config.autoConsolidateDailyMax) || 8, 1)
|
|
1605
2956
|
if (this._autoCallCount >= dailyMax) { why('daily subagent cap=' + dailyMax); return }
|
|
1606
|
-
if (
|
|
1607
|
-
// 按 turn 去重:同一 agent 的同一轮只处理一次
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
const lastTurn = this._lastTurnByAgent.get(agentId)
|
|
1611
|
-
if (lastTurn === turn) { why('dup turn'); return }
|
|
1612
|
-
this._lastTurnByAgent.set(agentId, turn)
|
|
2957
|
+
if (runtime.lastConsolidateAt && Date.now() - runtime.lastConsolidateAt < cooldownMs) { why('cooldown'); return }
|
|
2958
|
+
// 按 turn 去重:同一 agent 的同一轮只处理一次(runtime 隔离)
|
|
2959
|
+
if (runtime.lastTurn === turn) { why('dup turn'); return }
|
|
2960
|
+
runtime.lastTurn = turn
|
|
1613
2961
|
// 取本轮最后一条 user + 最后一条 assistant(模型可见消息序列)
|
|
1614
2962
|
const messages = extractSessionMessages(agent)
|
|
1615
2963
|
if (messages.length < 2) { why('messages<2 got=' + messages.length + ' seqs=' + ((agent.session.surface && agent.session.surface.nodes && (Array.isArray(agent.session.surface.nodes) ? agent.session.surface.nodes.length : 'set')) || 'none') + ' events=' + ((agent.session.events && agent.session.events.length) || 'none')); return }
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
2964
|
+
// 2026-08-30 intent 提纯(三层):①只认真人消息事件 'user/message'(tool/result 在协议里
|
|
2965
|
+
// 也是 user 角色,正文常是 JSON/行号文本)②跳过 harness 合成的上下文注入消息
|
|
2966
|
+
// ("Current runtime context." 开头 / 含 <memory_system>)③剥离注入快照块,取其后真人问题。
|
|
2967
|
+
// 逻辑已抽到 lib/intent-clean.js(纯函数,可回归锁定),此处只做等价调用。
|
|
2968
|
+
const picked = pickConsolidationTextPre(messages)
|
|
2969
|
+
userText = picked.userText
|
|
2970
|
+
assistantText = picked.assistantText
|
|
1620
2971
|
if (!userText.trim() || !assistantText.trim()) { why('empty text user=' + userText.length + ' asst=' + assistantText.length); return }
|
|
1621
2972
|
const combined = userText + '\n' + assistantText
|
|
1622
2973
|
if (combined.length < minChars) { why('too short combined=' + combined.length + ' min=' + minChars); return }
|
|
1623
|
-
|
|
2974
|
+
runtime.lastConsolidateAt = Date.now()
|
|
1624
2975
|
this._autoCallCount++
|
|
1625
2976
|
diag('consolidate subagent start count=' + this._autoCallCount + '/' + dailyMax + ' inputChars=' + Math.min(combined.length, 6000))
|
|
2977
|
+
// M8 啮合 P0③:本轮记入记忆中枢 episodic(累积段)→ 达 minSegments 巩固成 episode →
|
|
2978
|
+
// crossFeed 举一反三(success episode→procedure 观察;未决→fact 候选)。memoryHubEnabled
|
|
2979
|
+
// 门;全 fail-closed 静默,不阻断自动沉淀主流程。注意:本方法内作用域是引擎实例(this)。
|
|
2980
|
+
try {
|
|
2981
|
+
const hub = this._memoryHub
|
|
2982
|
+
if (this.config.memoryHubEnabled === true && hub && hub.stores && hub.stores.episodic) {
|
|
2983
|
+
const ar = hub.stores.episodic.append({
|
|
2984
|
+
sessionRef: String(runtime.sessionId || 'unknown').slice(0, 48),
|
|
2985
|
+
userText: userText.slice(0, 200),
|
|
2986
|
+
assistantText: assistantText.slice(0, 200),
|
|
2987
|
+
kind: 'user', eventSeq: Number(turn) || 0,
|
|
2988
|
+
})
|
|
2989
|
+
if (ar && ar.ok) {
|
|
2990
|
+
// 注意:episodic store 的 consolidate() 在段数不足 minSegments 时丢弃缓冲
|
|
2991
|
+
// (current=null),因此不能每次 append 都调——本地计数攒够再巩固。
|
|
2992
|
+
const minSegs = Math.max(1, Number(this.config.episodicMinSegments) || 2)
|
|
2993
|
+
this._hubEpBuffer = (this._hubEpBuffer || 0) + 1
|
|
2994
|
+
if (this._hubEpBuffer >= minSegs) {
|
|
2995
|
+
this._hubEpBuffer = 0
|
|
2996
|
+
const cr = hub.stores.episodic.consolidate()
|
|
2997
|
+
if (cr && cr.ok && cr.episode) {
|
|
2998
|
+
const cf = hub.crossFeed(cr.episode.sessionRef)
|
|
2999
|
+
diag('hub episode consolidated: ' + String(cr.episode.episodeId).slice(0, 20) + ' crossFeed=' + ((cf && cf.fed && cf.fed.length) || 0))
|
|
3000
|
+
} else {
|
|
3001
|
+
diag('hub episodic consolidate: ' + String(cr && cr.reason || 'unknown'))
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
// M9: success evidence——本轮 substantive 且记忆被 read/cite → procedure
|
|
3005
|
+
// addEvidence(success) 驱动晋升。修复 createSuccessEvidencePre 全仓零调用。
|
|
3006
|
+
try {
|
|
3007
|
+
const procs = hub.stores.procedures
|
|
3008
|
+
const ch = this._contextHost
|
|
3009
|
+
if (procs && ch && typeof ch.recentEvidenceForSuccess === 'function') {
|
|
3010
|
+
const cited = ch.recentEvidenceForSuccess(5 * 60 * 1000)
|
|
3011
|
+
if (cited.length) {
|
|
3012
|
+
const sr = String(runtime.sessionId || '').slice(0, 48)
|
|
3013
|
+
const allProcs = procs.query()
|
|
3014
|
+
const successEvs = []
|
|
3015
|
+
for (const e of cited) {
|
|
3016
|
+
for (const p of allProcs) {
|
|
3017
|
+
if (!(p.sourceMemoryIds || []).includes(e.memoryId)) continue
|
|
3018
|
+
procs.addEvidence(p.procedureId, { kind: 'success', sessionRef: sr })
|
|
3019
|
+
}
|
|
3020
|
+
const r = createSuccessEvidencePre({
|
|
3021
|
+
sessionId: runtime.sessionId || '', eventSeq: e.eventSeq || 0,
|
|
3022
|
+
nativeSeq: e.nativeSeq, contextVersion: e.contextVersion || 0,
|
|
3023
|
+
workspaceKey: e.workspaceKey || '', ts: Date.now(),
|
|
3024
|
+
memoryId: e.memoryId, anchorId: e.anchorId, scope: e.scope,
|
|
3025
|
+
sourceRef: e.sourceRef, sourceEpoch: e.sourceEpoch,
|
|
3026
|
+
sourceVersion: e.sourceVersion, fileDigest: e.fileDigest,
|
|
3027
|
+
recordDigest: e.recordDigest,
|
|
3028
|
+
})
|
|
3029
|
+
if (r.ok) successEvs.push(r.evidence)
|
|
3030
|
+
}
|
|
3031
|
+
if (successEvs.length) {
|
|
3032
|
+
ch.appendEvidence(successEvs)
|
|
3033
|
+
diag('hub success evidence: +' + successEvs.length)
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
} catch (_) {}
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
} catch (e) { diag('hub episodic hook error: ' + String(e && e.message || e).slice(0, 80)) }
|
|
1626
3041
|
} catch (e) { diag('consolidate pre-flight error: ' + (e && (e.stack || e.message) || e)); return }
|
|
1627
|
-
|
|
3042
|
+
runtime.consolidating = (async () => {
|
|
1628
3043
|
try {
|
|
1629
3044
|
await this.refresh(agent)
|
|
1630
3045
|
const p = await this.resolvePaths(agent)
|
|
@@ -1664,8 +3079,7 @@ class MemoryEngine {
|
|
|
1664
3079
|
if (!text) {
|
|
1665
3080
|
diag('consolidate: subagent returned empty text (queued for retry)')
|
|
1666
3081
|
// subagent 失败(返回空):入重试队列,由后台轮询兜底重试
|
|
1667
|
-
if (
|
|
1668
|
-
if (this._pendingConsolidations.length < 5) this._pendingConsolidations.push({ turn, agent })
|
|
3082
|
+
if (runtime.pendingConsolidations.length < 5) runtime.pendingConsolidations.push({ turn, agent })
|
|
1669
3083
|
return
|
|
1670
3084
|
}
|
|
1671
3085
|
if (text.includes('(无)')) return
|
|
@@ -1732,10 +3146,10 @@ class MemoryEngine {
|
|
|
1732
3146
|
} catch (e) {
|
|
1733
3147
|
console.error('[dsh-auto-memory] consolidateTurn failed', e && e.message ? e.message : e)
|
|
1734
3148
|
} finally {
|
|
1735
|
-
|
|
3149
|
+
runtime.consolidating = undefined
|
|
1736
3150
|
}
|
|
1737
3151
|
})()
|
|
1738
|
-
await
|
|
3152
|
+
await runtime.consolidating
|
|
1739
3153
|
}
|
|
1740
3154
|
|
|
1741
3155
|
/** AI 主动固化(做梦式):读最近日志 → 发散提炼 → 项目笔记/用户级 MEMORY.md 带日期标题。 */
|
|
@@ -1850,6 +3264,9 @@ class MemoryEngine {
|
|
|
1850
3264
|
const p = await this.resolvePaths(agent)
|
|
1851
3265
|
const cutoff = new Date()
|
|
1852
3266
|
cutoff.setDate(cutoff.getDate() - days)
|
|
3267
|
+
// 归档截止取「日期零点」而非当前时刻:否则凌晨 0:00-日界窗口内,当天(按日界归属)日志
|
|
3268
|
+
// 会因 log.date(零点) < cutoff(含时分秒)被误判为旧日志而归档掉(2026-08-24 凌晨窗口实测)。
|
|
3269
|
+
cutoff.setHours(0, 0, 0, 0)
|
|
1853
3270
|
const logs = await this.listDailyLogs(p.projectDir, 365)
|
|
1854
3271
|
const oldLogs = logs.filter((log) => {
|
|
1855
3272
|
const m = DATE_RE.exec(log.date)
|
|
@@ -1895,7 +3312,7 @@ class MemoryEngine {
|
|
|
1895
3312
|
try {
|
|
1896
3313
|
const text = await this.readTextSafe(path.join(p.projectDir, log.name))
|
|
1897
3314
|
if (text) { await this.writeFull(path.join(archiveDir, log.name), text); archived.push(log.name) }
|
|
1898
|
-
} catch (e) {}
|
|
3315
|
+
} catch (e) { console.error('[dsh-auto-memory] maintain archive failed for ' + log.name + ':', e && e.message ? e.message : e) }
|
|
1899
3316
|
}
|
|
1900
3317
|
// 3) 蒸馏结果写项目笔记;无 AI 时原样归档段保底
|
|
1901
3318
|
let noteMsg = ''
|
|
@@ -1956,6 +3373,7 @@ class MemoryEngine {
|
|
|
1956
3373
|
// 时间检测:暂离状态 / 待展示的自动总结 / 相关配置
|
|
1957
3374
|
away: !!this.state.away,
|
|
1958
3375
|
awayMinutes: Math.max(Number(this.config.awayMinutes) || 60, 1),
|
|
3376
|
+
autoPopupEnabled: this.config.autoPopupEnabled !== false,
|
|
1959
3377
|
autoSummaryTimes: Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : [],
|
|
1960
3378
|
pendingSummary: this.state.pendingSummary || null,
|
|
1961
3379
|
autoStats: this.autoStats,
|
|
@@ -2442,7 +3860,7 @@ function sanitizeForWrite(text, opts) {
|
|
|
2442
3860
|
if (b64line) return { ok: false, reason: 'base64', clean: '' }
|
|
2443
3861
|
if (raw.length > maxEntry) {
|
|
2444
3862
|
// 超长: 截断并标记(防止整篇文档吸入; 正常条目极少超过)
|
|
2445
|
-
return { ok: true, clean: raw.slice(0, maxEntry), truncated: true }
|
|
3863
|
+
return { ok: true, clean: sanitizeReservedSyntax(raw.slice(0, maxEntry)), truncated: true }
|
|
2446
3864
|
}
|
|
2447
3865
|
// 连续行重复(同一段一模一样的行连续 ≥3 次 → 疑似退化 writer 循环; 空行打断连续, 避免把不同段落里相同的短行误判为循环)
|
|
2448
3866
|
var seq = 0, prev = '', repeated = false
|
|
@@ -2452,7 +3870,14 @@ function sanitizeForWrite(text, opts) {
|
|
|
2452
3870
|
if (t === prev) { seq++; if (seq >= 3) { repeated = true; break } } else { prev = t; seq = 1 }
|
|
2453
3871
|
}
|
|
2454
3872
|
if (repeated) return { ok: false, reason: 'duplicate-lines' }
|
|
2455
|
-
return { ok: true, clean: raw }
|
|
3873
|
+
return { ok: true, clean: sanitizeReservedSyntax(raw) }
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
/** M3b-4 保留语法卫生:字面 '<!-- memory:' 与记忆 anchor 保留语法冲突(写入后 parseAnchors 判 orphan-content,
|
|
3877
|
+
* 开启 memoryAnchorEnabled 时文件锁死不可写)。写入前统一改写为豁免形式 '<!--memory:'(冒号后无空格),
|
|
3878
|
+
* 不构成保留子串,内容语义不变(文档/示例可读)。 */
|
|
3879
|
+
function sanitizeReservedSyntax(text) {
|
|
3880
|
+
return String(text || '').replace(/<!-- memory:/g, '<!--memory:')
|
|
2456
3881
|
}
|
|
2457
3882
|
/** 写闸门拦截原因 → 中文说明(供三个写入工具返回信息)。 */
|
|
2458
3883
|
var WRITE_GATE_REASON = { empty: '内容为空', mojibake: '疑似乱码/错误编码往返', stutter: '疑似复读退化', 'duplicate-lines': '疑似重复内容块', 'raw-json': '疑似外部画像 raw JSON envelope', base64: '疑似 base64 编码残骸行' }
|
|
@@ -2555,7 +3980,7 @@ function extractSessionMessages(agent) {
|
|
|
2555
3980
|
const ev = events[i]
|
|
2556
3981
|
const msg = messageOfEvent(ev)
|
|
2557
3982
|
if (!msg || !msg.role || !Array.isArray(msg.content)) continue
|
|
2558
|
-
out.push({ role: msg.role, text: textOfContent(msg.content), sourceKind: msg.source && msg.source.kind })
|
|
3983
|
+
out.push({ role: msg.role, text: textOfContent(msg.content), sourceKind: msg.source && msg.source.kind, eventType: ev.type })
|
|
2559
3984
|
}
|
|
2560
3985
|
return out
|
|
2561
3986
|
} catch (e) { return [] }
|
|
@@ -2621,15 +4046,369 @@ export function apply(ctx, config) {
|
|
|
2621
4046
|
}
|
|
2622
4047
|
} catch (e) {}
|
|
2623
4048
|
const engine = new MemoryEngine()
|
|
4049
|
+
// M4-3:Shadow Retrieval Host 接线(三开关全开时才构造状态/IO;默认关闭零留存)
|
|
4050
|
+
engine._shadowHost = createShadowHost({ engine })
|
|
4051
|
+
// M5-3:Context Bridge Host 接线(assoc+contextBridge 双门;默认关闭零构造/零 IO)
|
|
4052
|
+
engine._contextHost = createContextHost({ engine })
|
|
4053
|
+
// M6-3:Activation Inbox Host 接线(assoc+activationInbox 双门;默认关闭)
|
|
4054
|
+
engine._activationHost = createActivationHost({ engine })
|
|
4055
|
+
// M7-8:Host Index Sync Orchestrator(四门全开才启用;默认关闭零 IO;修复 M7-8 Phase E blocker)
|
|
4056
|
+
engine._indexSyncHost = createIndexSyncHostPre({ engine })
|
|
4057
|
+
// M8:记忆中枢(Memory Hub)编排器 —— 三层记忆(episodic/semantic/procedural)。
|
|
4058
|
+
// 惰性:默认关时三层 store 仍构造(纯内存零 IO),但消费链不启动;
|
|
4059
|
+
// 开启后经 memoryHubEnabled 门消费 judgement-shadow + 提供 overview 端点。
|
|
4060
|
+
// 2026-08-28 啮合 P0①:三店持久化 io(~/.dsh/memory/hub/,原子 tmp+rename)+启动 restore
|
|
4061
|
+
// (此前 io 缺省 no-op → 重启清零)。落盘为快照整体写,数据量有界(episodes≤retention/facts/过程中)。
|
|
4062
|
+
{
|
|
4063
|
+
const hubDir = path.join(dshHome(), 'memory', 'hub')
|
|
4064
|
+
const hubIo = (name) => {
|
|
4065
|
+
const f = path.join(hubDir, name)
|
|
4066
|
+
return {
|
|
4067
|
+
save(data) {
|
|
4068
|
+
try { mkdirSync(hubDir, { recursive: true }); const tmp = f + '.tmp'; writeFileSync(tmp, JSON.stringify(data), 'utf8'); renameSync(tmp, f) } catch (_) {}
|
|
4069
|
+
},
|
|
4070
|
+
load() { try { return JSON.parse(readFileSync(f, 'utf8')) } catch (_) { return null } },
|
|
4071
|
+
clear() { try { rmSync(f, { force: true }) } catch (_) {} },
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
engine._memoryHub = createMemoryHubPre({
|
|
4075
|
+
stores: {
|
|
4076
|
+
episodic: createEpisodicStorePre({ config: { minSegments: Number(engine.config.episodicMinSegments) || 2, retention: Number(engine.config.episodicRetention) || 256 }, io: hubIo('episodes.json') }),
|
|
4077
|
+
facts: createFactStorePre({ io: hubIo('facts.json') }),
|
|
4078
|
+
procedures: createProcedureStorePre({
|
|
4079
|
+
// 2026-08-30 修复:gates 用 getter 活读 engine.config——挂载(apply 同步)时
|
|
4080
|
+
// engine.config 尚未 loadConfig,静态读会冻结 DEFAULT(3/2),设置页改门槛永不生效
|
|
4081
|
+
gates: {
|
|
4082
|
+
// 0 值合法(验证用);仅非有限负值回退默认——不可用 || 0 会 falsy 穿透
|
|
4083
|
+
get minSessionDiversity() { const v = Number(engine.config.procedureMinSessions); return (Number.isFinite(v) && v >= 0) ? v : 3 },
|
|
4084
|
+
get minSuccessCount() { const v = Number(engine.config.procedureMinSuccess); return (Number.isFinite(v) && v >= 0) ? v : 2 },
|
|
4085
|
+
get maxCorrectionRate() { return Number(engine.config.procedureCorrectionCap) || 0.3 },
|
|
4086
|
+
get maxContradictions() { return 0 },
|
|
4087
|
+
get highRiskRequiresApproval() { return engine.config.procedureHighRiskApproval !== false },
|
|
4088
|
+
},
|
|
4089
|
+
activeLevel: engine.config.procedureActiveLevel || 'checklist',
|
|
4090
|
+
io: hubIo('procedures.json'),
|
|
4091
|
+
}),
|
|
4092
|
+
},
|
|
4093
|
+
})
|
|
4094
|
+
try {
|
|
4095
|
+
// restore 逐条校验,坏记录跳过(fail closed 幂等恢复);无文件/损坏 → 空启动
|
|
4096
|
+
const eData = hubIo('episodes.json').load()
|
|
4097
|
+
if (eData) engine._memoryHub.stores.episodic.restore(eData)
|
|
4098
|
+
const fData = hubIo('facts.json').load()
|
|
4099
|
+
if (fData) engine._memoryHub.stores.facts.restore(fData)
|
|
4100
|
+
const pData = hubIo('procedures.json').load()
|
|
4101
|
+
if (pData) engine._memoryHub.stores.procedures.restore(pData)
|
|
4102
|
+
} catch (_) {}
|
|
4103
|
+
}
|
|
4104
|
+
// ---------- M10 存储管理编排器(2026-08-30 P3) ----------
|
|
4105
|
+
// 原语(docStore.replace / rebuildSidecar / activationHost.purgeMemory / factStore.revokeBySource)
|
|
4106
|
+
// 此前零调用方,此处组装成「扫描 → 修复 → 删除三联动」三个动作,经 loopback 端点与
|
|
4107
|
+
// 设置页「存储管理」消费。docStore 与 hub 均为懒加载 → 用 getter 活读,apply 同步阶段安全。
|
|
4108
|
+
engine._storageManager = createStorageManagerPre({
|
|
4109
|
+
get docStore() { return engine.docStore },
|
|
4110
|
+
io: { sidecarDir: path.join(dshHome(), 'memory', 'index', 'files'), readFileSync },
|
|
4111
|
+
pathsOf: () => (engine.state && engine.state.ws ? {
|
|
4112
|
+
workspaceKey: canonicalize(engine.state.ws),
|
|
4113
|
+
userMemoryPath: engine.state.userDir ? path.join(engine.state.userDir, 'MEMORY.md') : undefined,
|
|
4114
|
+
workspaceMemoryPath: engine.state.notesPath,
|
|
4115
|
+
todayLogPath: engine.state.logPath,
|
|
4116
|
+
} : null),
|
|
4117
|
+
activationHostOf: () => engine._activationHost || null,
|
|
4118
|
+
factStoreOf: () => (engine._memoryHub && engine._memoryHub.stores ? engine._memoryHub.stores.facts : null),
|
|
4119
|
+
})
|
|
4120
|
+
|
|
4121
|
+
// ---------- M8 啮合 P0②/P1④:judgement 文件队列喂数 + fact 治理式写回 ----------
|
|
4122
|
+
// 喂数:60s 轮询 semantic/judgement-shadow.jsonl(Python 建议行)增量 → heading 富化 →
|
|
4123
|
+
// hub.ingestJudgement。memoryHubEnabled 门;行无 subject/predicate(memoryId 级建议),
|
|
4124
|
+
// 富化用 sidecar 语料记录的 heading/text;不可解析的行跳过。js-decide-shadow.jsonl 不喂
|
|
4125
|
+
// (JS 判定行无候选载荷,JS 侧记忆供给走 episodic/crossFeed 直接入店)。
|
|
4126
|
+
// 写回:P1④ 主闭环——hub.facts 中 confirmed/未过期/未撤销/置信≥0.6 的事实,每日限额
|
|
4127
|
+
// 治理式写入 notesPath/userFile(autoConsolidate 同款 appendText 原子事务)→ 进 M7 语料。
|
|
4128
|
+
{
|
|
4129
|
+
const hubFeedState = { shadow: null } // 文件指纹 {size,mtimeMs,count}
|
|
4130
|
+
const hubFlushState = { date: '', count: 0, flushed: {} } // flushed[factId]=true;持久化 hub/flush-state.json
|
|
4131
|
+
const hubFlushFile = () => path.join(dshHome(), 'memory', 'hub', 'flush-state.json')
|
|
4132
|
+
const hubFlushLoad = () => {
|
|
4133
|
+
try { const d = JSON.parse(readFileSync(hubFlushFile(), 'utf8')); if (d && typeof d === 'object') { hubFlushState.date = String(d.date || ''); hubFlushState.count = Number(d.count) || 0; hubFlushState.flushed = d.flushed || {} } } catch (_) {}
|
|
4134
|
+
}
|
|
4135
|
+
const hubFlushSave = () => {
|
|
4136
|
+
try { mkdirSync(path.dirname(hubFlushFile()), { recursive: true }); writeFileSync(hubFlushFile(), JSON.stringify({ date: hubFlushState.date, count: hubFlushState.count, flushed: hubFlushState.flushed }), 'utf8') } catch (_) {}
|
|
4137
|
+
}
|
|
4138
|
+
hubFlushLoad()
|
|
4139
|
+
// 语料查询器(heading 富化用):与 context-host 同源 sidecar 目录,懒建缓存
|
|
4140
|
+
let hubCorpusCache = { key: '', snap: null }
|
|
4141
|
+
const hubCorpusLookup = async (memoryId) => {
|
|
4142
|
+
try {
|
|
4143
|
+
const rt = engine.currentRuntime()
|
|
4144
|
+
const p = await engine.resolvePaths(rt && rt.agent)
|
|
4145
|
+
const key = canonicalize(p.notesPath || p.ws || '')
|
|
4146
|
+
if (!key) return null
|
|
4147
|
+
if (hubCorpusCache.key !== key || !hubCorpusCache.snap) {
|
|
4148
|
+
const cat = buildSourceCatalog({ workspaceKey: p.ws || '', userMemoryPath: p.userFile, workspaceMemoryPath: p.notesPath, todayLogPath: p.logPath })
|
|
4149
|
+
if (!engine._hubCorpusRegistry) engine._hubCorpusRegistry = new CorpusRegistry({ sidecarDir: path.join(dshHome(), 'memory', 'index', 'files') })
|
|
4150
|
+
const res = engine._hubCorpusRegistry.get(cat)
|
|
4151
|
+
if (!(res && res.ok)) return null
|
|
4152
|
+
hubCorpusCache = { key, snap: res.snapshot }
|
|
4153
|
+
}
|
|
4154
|
+
return (hubCorpusCache.snap.records || []).find((r) => r.memoryId === memoryId) || null
|
|
4155
|
+
} catch (_) { return null }
|
|
4156
|
+
}
|
|
4157
|
+
const hubFeedTick = () => {
|
|
4158
|
+
try {
|
|
4159
|
+
if (engine.config.memoryHubEnabled !== true) return
|
|
4160
|
+
const hub = engine._memoryHub
|
|
4161
|
+
if (!hub || !hub.ingestJudgementRows) return
|
|
4162
|
+
const f = path.join(dshHome(), 'memory', 'semantic', 'judgement-shadow.jsonl')
|
|
4163
|
+
let st
|
|
4164
|
+
try { st = statSync(f) } catch (_) { return }
|
|
4165
|
+
const prev = hubFeedState.shadow
|
|
4166
|
+
if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs) return
|
|
4167
|
+
const lines = readFileSync(f, 'utf8').split('\n').filter((l) => l.trim())
|
|
4168
|
+
const start = prev && lines.length >= prev.count ? prev.count : lines.length // 文件被截断则只取尾部
|
|
4169
|
+
const newRows = []
|
|
4170
|
+
for (let i = start; i < lines.length; i++) {
|
|
4171
|
+
try { const d = JSON.parse(lines[i]); if (d && d.kindCandidate) newRows.push(d) } catch (_) {}
|
|
4172
|
+
}
|
|
4173
|
+
hubFeedState.shadow = { size: st.size, mtimeMs: st.mtimeMs, count: lines.length }
|
|
4174
|
+
if (!newRows.length) return
|
|
4175
|
+
// 异步富化+喂送(不阻塞 tick);单行失败静默
|
|
4176
|
+
void (async () => {
|
|
4177
|
+
let fed = 0
|
|
4178
|
+
for (const row of newRows) {
|
|
4179
|
+
try {
|
|
4180
|
+
const src = Array.isArray(row.sourceIds) ? row.sourceIds[0] : null
|
|
4181
|
+
if (src) {
|
|
4182
|
+
const rec = await hubCorpusLookup(src)
|
|
4183
|
+
if (rec) {
|
|
4184
|
+
row.subject = String(rec.heading || rec.text || '').split('\n')[0].slice(0, 30)
|
|
4185
|
+
row.predicate = '记录要点'
|
|
4186
|
+
row.object = String(rec.text || '').slice(0, 120)
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
const r = hub.ingestJudgement(row)
|
|
4190
|
+
if (r && r.consumed) fed++
|
|
4191
|
+
} catch (_) {}
|
|
4192
|
+
}
|
|
4193
|
+
if (fed) diag('hub feed: +' + fed + '/' + newRows.length + ' rows consumed')
|
|
4194
|
+
})()
|
|
4195
|
+
} catch (_) {}
|
|
4196
|
+
}
|
|
4197
|
+
const hubFlushTick = async () => {
|
|
4198
|
+
try {
|
|
4199
|
+
if (engine.config.memoryHubEnabled !== true) return
|
|
4200
|
+
const hub = engine._memoryHub
|
|
4201
|
+
if (!hub || !hub.stores.facts) return
|
|
4202
|
+
const today = engine.memToday()
|
|
4203
|
+
if (hubFlushState.date !== today) { hubFlushState.date = today; hubFlushState.count = 0 }
|
|
4204
|
+
const DAILY_MAX = 8
|
|
4205
|
+
if (hubFlushState.count >= DAILY_MAX) return
|
|
4206
|
+
const facts = hub.stores.facts.query ? hub.stores.facts.query() : []
|
|
4207
|
+
const now = Date.now()
|
|
4208
|
+
let p = null
|
|
4209
|
+
for (const fact of facts) {
|
|
4210
|
+
if (hubFlushState.count >= DAILY_MAX) break
|
|
4211
|
+
if (!fact || fact.revoked || hubFlushState.flushed[fact.factId]) continue
|
|
4212
|
+
if (fact.ttl && now >= (fact.confirmedAt || 0) + fact.ttl) continue
|
|
4213
|
+
if (typeof fact.confidence === 'number' && fact.confidence < 0.6) continue
|
|
4214
|
+
const subj = String(fact.subject || '').trim()
|
|
4215
|
+
if (!subj || subj.startsWith('mem_')) continue // 无富化的 memoryId 主语不入正文
|
|
4216
|
+
try {
|
|
4217
|
+
if (!p) p = await engine.resolvePaths(engine.currentRuntime().agent)
|
|
4218
|
+
const target = fact.scope === 'User' ? p.userFile : p.notesPath
|
|
4219
|
+
if (!target) continue
|
|
4220
|
+
const cur = await engine.readTextSafe(target)
|
|
4221
|
+
if (cur && cur.includes(subj)) { hubFlushState.flushed[fact.factId] = true; continue } // 已在正文,只记标记
|
|
4222
|
+
const body = '\n## ' + subj + '(M8 固化)\n- ' + String(fact.predicate || '要点') + (fact.object ? ':' + String(fact.object) : '') + '\n- 来源:记忆中枢治理固化' + (typeof fact.confidence === 'number' ? '(confidence=' + fact.confidence.toFixed(2) + ')' : '')
|
|
4223
|
+
const written = await engine.appendText(target, body)
|
|
4224
|
+
try { if (engine.state) { if (fact.scope === 'User') engine.state.userText = written; else engine.state.notesText = written } } catch (_) {}
|
|
4225
|
+
hubFlushState.flushed[fact.factId] = true
|
|
4226
|
+
hubFlushState.count++
|
|
4227
|
+
diag('hub flush: fact ' + String(fact.factId).slice(0, 16) + ' → ' + (fact.scope === 'User' ? 'user' : 'notes'))
|
|
4228
|
+
} catch (_) {}
|
|
4229
|
+
}
|
|
4230
|
+
hubFlushSave()
|
|
4231
|
+
} catch (_) {}
|
|
4232
|
+
}
|
|
4233
|
+
const hubFeedTimer = setInterval(hubFeedTick, 60 * 1000)
|
|
4234
|
+
const hubFlushTimer = setInterval(() => { void hubFlushTick() }, 30 * 60 * 1000)
|
|
4235
|
+
// unref:定时器不阻止进程退出(测试 settle 不经过 apply 的 disposer 链会挂住)
|
|
4236
|
+
hubFeedTimer.unref(); hubFlushTimer.unref()
|
|
4237
|
+
const hubBootTimer = setTimeout(() => { hubFeedTick(); void hubFlushTick() }, 90 * 1000)
|
|
4238
|
+
hubBootTimer.unref()
|
|
4239
|
+
if (!engine._hubFeedDisposers) engine._hubFeedDisposers = []
|
|
4240
|
+
engine._hubFeedDisposers.push(() => { clearInterval(hubFeedTimer); clearInterval(hubFlushTimer) })
|
|
4241
|
+
}
|
|
4242
|
+
// C2 内置语义引擎宿主(2026-08-26 用户裁定:C2=默认主路径)。懒加载 e5-small q8;
|
|
4243
|
+
// 只做检索排序,激活决策仍属两车道策略。下载器落位=发行包布局 lib/models。
|
|
4244
|
+
{
|
|
4245
|
+
const pluginDir = path.dirname(fileURLToPath(import.meta.url))
|
|
4246
|
+
engine._jsSemantic = createJsSemanticEnginePre({ pluginDir })
|
|
4247
|
+
engine._jsDownload = createSemanticDownloaderPre({ modelsRoot: path.join(pluginDir, 'models') })
|
|
4248
|
+
// 资产探测(semantic-status / 档位解析 / 引导卡共用):发行包 models/ 优先,其次开发树
|
|
4249
|
+
engine.semanticAssetProbe = async () => {
|
|
4250
|
+
const fsMod = await import('node:fs')
|
|
4251
|
+
const cands = [
|
|
4252
|
+
path.join(pluginDir, 'models', 'multilingual-e5-small', 'onnx', 'model_quantized.onnx'),
|
|
4253
|
+
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'models', 'multilingual-e5-small', 'onnx', 'model_quantized.onnx'),
|
|
4254
|
+
]
|
|
4255
|
+
const modelOnnx = cands.find((c) => fsMod.existsSync(c)) || cands[0]
|
|
4256
|
+
const peerCands = [
|
|
4257
|
+
path.join(pluginDir, 'node_modules', '@huggingface', 'transformers'),
|
|
4258
|
+
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'node_modules', '@huggingface', 'transformers'),
|
|
4259
|
+
]
|
|
4260
|
+
const peerDir = peerCands.find((c) => fsMod.existsSync(c)) || peerCands[0]
|
|
4261
|
+
const assetPresent = fsMod.existsSync(modelOnnx)
|
|
4262
|
+
return {
|
|
4263
|
+
assetPresent,
|
|
4264
|
+
peerPresent: fsMod.existsSync(peerDir),
|
|
4265
|
+
ready: Boolean(assetPresent && fsMod.existsSync(peerDir)),
|
|
4266
|
+
assetBytes: assetPresent ? fsMod.statSync(modelOnnx).size : 0,
|
|
4267
|
+
assetPath: modelOnnx,
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
// 档位解析:lexical→C1 强制保底;其余(auto/js/python)在资产就绪时启用 C2 臂
|
|
4271
|
+
// (python 模式下 C2 只改善 envelope.refs/candidateHit,sink 检索仍归 sidecar)。
|
|
4272
|
+
engine.resolveSemanticTier = async () => {
|
|
4273
|
+
const mode = String(engine.config.semanticEngineMode || 'auto')
|
|
4274
|
+
// 2026-08-27 修正:区分引擎模式(修复「怎么切都显示 C2」)。
|
|
4275
|
+
// lexical→c1 强制;python→c3(高级档);auto/js→资产就绪 c2 否则 c1。
|
|
4276
|
+
if (mode === 'lexical') return 'c1'
|
|
4277
|
+
if (mode === 'python') return 'c3'
|
|
4278
|
+
try { return (await engine.semanticAssetProbe()).ready ? 'c2' : 'c1' } catch (_) { return 'c1' }
|
|
4279
|
+
}
|
|
4280
|
+
// context-host refs 选择钩子:C2 就绪时返回 {scores:Map};任何失败回退词法序。
|
|
4281
|
+
engine._jsSemanticRank = async (corpusSnap, queryText) => {
|
|
4282
|
+
try {
|
|
4283
|
+
if ((await engine.resolveSemanticTier()) !== 'c2') return null
|
|
4284
|
+
return await engine._jsSemantic.rank(corpusSnap, queryText)
|
|
4285
|
+
} catch (_) { return null }
|
|
4286
|
+
}
|
|
4287
|
+
// JS 端判定核(2026-08-27):读策略工件(懒加载+缓存),对 C2 检索结果做 fv2 决策。
|
|
4288
|
+
// 完全独立于 Python——JS 端默认闭环(C2 检索 + JS 判定 + M6 投递)的核心。
|
|
4289
|
+
engine._jsDecideCtx = null
|
|
4290
|
+
engine._jsDecideErr = ''
|
|
4291
|
+
engine.ensureJsDecide = () => {
|
|
4292
|
+
try {
|
|
4293
|
+
if (engine._jsDecideCtx) return engine._jsDecideCtx
|
|
4294
|
+
// 策略工件多路径探测:JS 侧 lib/policies 优先(纯 JS 部署必达,npm 包 JS-only 也含);
|
|
4295
|
+
// fallback python/policies(兼容旧部署)。纯 JS 用户系统无 Python,必须能从 JS 侧读到。
|
|
4296
|
+
const pluginDir = path.dirname(fileURLToPath(import.meta.url))
|
|
4297
|
+
const jsPolicyDir = path.join(pluginDir, 'policies')
|
|
4298
|
+
const pyPolicyDir = path.join(pluginRootDir(), 'python', 'policies')
|
|
4299
|
+
const jsIntent = path.join(jsPolicyDir, 'recall_intent_lr_v1.json')
|
|
4300
|
+
const jsAct = path.join(jsPolicyDir, 'activation_policy_v2.json')
|
|
4301
|
+
const intentPath = existsSync(jsIntent) ? jsIntent : path.join(pyPolicyDir, 'recall_intent_lr_v1.json')
|
|
4302
|
+
const actPath = existsSync(jsAct) ? jsAct : path.join(pyPolicyDir, 'activation_policy_v2.json')
|
|
4303
|
+
engine._jsDecideCtx = loadAndVerifyPolicy(intentPath, actPath)
|
|
4304
|
+
return engine._jsDecideCtx
|
|
4305
|
+
} catch (e) {
|
|
4306
|
+
engine._jsDecideErr = String(e && e.message || e).slice(0, 160)
|
|
4307
|
+
return null
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
// JS 发射门(2026-08-27):读 embedding-config.json 的 activationEmitMode(与 Python worker 同源)。
|
|
4311
|
+
// shadow=只记录不注入;canary-explicit/active=注入。JS 判定 emit 前必须过此门。
|
|
4312
|
+
// 5 秒缓存避免每次判定都读盘(设置页 semantic-emit 写后 5 秒内生效)。
|
|
4313
|
+
engine._jsEmitCache = { at: 0, mode: '' }
|
|
4314
|
+
engine.jsEmitMode = () => {
|
|
4315
|
+
const now = Date.now()
|
|
4316
|
+
if (now - engine._jsEmitCache.at < 5000) return engine._jsEmitCache.mode || 'shadow'
|
|
4317
|
+
try {
|
|
4318
|
+
const raw = JSON.parse(readFileSync(path.join(dshHome(), 'memory', 'semantic', 'embedding-config.json'), 'utf8'))
|
|
4319
|
+
const em = String((raw && raw.activationEmitMode) || 'shadow')
|
|
4320
|
+
const mode = ['shadow', 'canary-explicit', 'active'].includes(em) ? em : 'shadow'
|
|
4321
|
+
engine._jsEmitCache = { at: now, mode }
|
|
4322
|
+
return mode
|
|
4323
|
+
} catch (_) { return 'shadow' }
|
|
4324
|
+
}
|
|
4325
|
+
// JS 判定入口:输入 C2 排名结果 + envelope 上下文,输出 fv2 决策。
|
|
4326
|
+
// 返回 { ok, decision, lane, reasonCodes, features } 或 { ok:false }。
|
|
4327
|
+
engine._jsDecide = async (queryText, rankRes, envelope) => {
|
|
4328
|
+
try {
|
|
4329
|
+
const ctx = engine.ensureJsDecide()
|
|
4330
|
+
if (!ctx) return { ok: false, reason: 'policy-unavailable: ' + engine._jsDecideErr }
|
|
4331
|
+
// 从 rankRes 构建特征:candidateHit=refs 与语义候选重叠
|
|
4332
|
+
const refIds = new Set((envelope && envelope.memoryRefs || []).map((r) => r.memoryId))
|
|
4333
|
+
const scores = rankRes && rankRes.scores
|
|
4334
|
+
// 2026-08-28 孪生对齐(浏览器实调发现):Python fv2 的候选池=稠密 top-8(SHADOW_TOP_K),
|
|
4335
|
+
// 按 D6 融合序重排,margin=融合第1/2名的稠密分差(worker_semantic_v1.py dense_search
|
|
4336
|
+
// top_k=8 + hybrid_rank + candidates[0/1]['score'])。JS 旧实现=全量裸稠密分 top1-top2,
|
|
4337
|
+
// 在致密语料(105条同项目记忆)中恒 <deltaExp → emit 永不触发,与 held-out 校准分布脱节。
|
|
4338
|
+
let candIds = []
|
|
4339
|
+
let denseTop = 0
|
|
4340
|
+
let margin = 0
|
|
4341
|
+
let nCand = 0
|
|
4342
|
+
if (scores && scores.size) {
|
|
4343
|
+
const lexMap = (rankRes._lex instanceof Map) ? rankRes._lex : new Map()
|
|
4344
|
+
const denseSorted = [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
4345
|
+
const fusedOrder = fuseD6Pre(denseSorted.map(([id, d]) => ({ memoryId: id, dense: d, lex: lexMap.get(id) || 0 })))
|
|
4346
|
+
const ordered = (fusedOrder.length ? fusedOrder : denseSorted.map(([id]) => ({ memoryId: id })))
|
|
4347
|
+
.map((f) => ({ memoryId: f.memoryId, dense: scores.get(f.memoryId) || 0 }))
|
|
4348
|
+
candIds = ordered.map((c) => c.memoryId)
|
|
4349
|
+
denseTop = ordered.length ? ordered[0].dense : 0
|
|
4350
|
+
margin = ordered.length > 1 ? Math.max(0, denseTop - ordered[1].dense) : (ordered.length ? denseTop : 0)
|
|
4351
|
+
nCand = ordered.length
|
|
4352
|
+
}
|
|
4353
|
+
const candidateHit = [...refIds].some((id) => candIds.includes(id))
|
|
4354
|
+
const topRec = candIds.length ? ((rankRes._records || []).find((r) => r.memoryId === candIds[0]) || null) : null
|
|
4355
|
+
const containment = topRec ? lexicalContainment(queryText, topRec.text || '') : 0
|
|
4356
|
+
const tl = String(queryText || '').toLowerCase()
|
|
4357
|
+
const mark = (['?', '?', '什么', '如何', '怎么', '哪些', '哪个', '为什么', '多少', '吗', '呢', '是不是', '有没有', '之前', '上次', '当时'].some((k) => tl.includes(k))) ? 1 : 0
|
|
4358
|
+
const features = {
|
|
4359
|
+
text: String(queryText || '').slice(0, 2000),
|
|
4360
|
+
denseTop, margin, containment, mark,
|
|
4361
|
+
nCand, candidateHit,
|
|
4362
|
+
hardGates: {},
|
|
4363
|
+
requiresRelayFlag: false, piiClass: 'unknown',
|
|
4364
|
+
}
|
|
4365
|
+
// 2026-08-28 e5 校准增量:JS 档用配置覆盖 deltaExp(克隆 policy,冻结决策核不动)。
|
|
4366
|
+
// bge-m3 校准的 0.03 对 e5 的压缩分布过严(live 实测 margin 0-0.0284 全被拦)。
|
|
4367
|
+
let policy = ctx.policy
|
|
4368
|
+
const deltaOverride = Number(engine.config.jsDecideDeltaExp)
|
|
4369
|
+
if (Number.isFinite(deltaOverride) && deltaOverride >= 0 && policy && policy.thresholds) {
|
|
4370
|
+
policy = Object.assign({}, policy, { thresholds: Object.assign({}, policy.thresholds, { deltaExp: deltaOverride }) })
|
|
4371
|
+
}
|
|
4372
|
+
const out = decideActivationV2(features, ctx.head, policy)
|
|
4373
|
+
return { ok: true, decision: out.decision, lane: out.lane, reasonCodes: out.reasonCodes, features: out.features, _margin: margin, _denseTop: denseTop, _candN: nCand, _hit: candidateHit }
|
|
4374
|
+
} catch (e) {
|
|
4375
|
+
return { ok: false, reason: String(e && e.message || e).slice(0, 160) }
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
try { engine._activationHost.initCapability(ctx) } catch (_) {}
|
|
4380
|
+
// M7-0/M7-1:engine 级共享 SidecarClient(对象构造零副作用;进程仅在显式启用路径上 lazy spawn,
|
|
4381
|
+
// 默认关闭=零 Python process、零协议 IO、零 semantic 目录)
|
|
4382
|
+
engine._pythonSidecar = createPythonSidecarClientPre({
|
|
4383
|
+
command: () => String(engine.config.pythonBackendExecutable || '').trim() || 'python',
|
|
4384
|
+
scriptPath: () => String(engine.config.pythonBackendWorkerPath || '').trim() || defaultWorkerScriptPathPre(),
|
|
4385
|
+
dshHome: () => {
|
|
4386
|
+
const env = process.env.DSH_HOME
|
|
4387
|
+
if (env && env.trim()) return env.trim()
|
|
4388
|
+
try { return path.join(homedir(), '.dsh') } catch (_) { return '' }
|
|
4389
|
+
},
|
|
4390
|
+
})
|
|
4391
|
+
engine.__homedirFn = homedir
|
|
2624
4392
|
const sessionQuery = ctx.get('sessionQuery')
|
|
2625
4393
|
engine._sessionQuery = sessionQuery
|
|
2626
4394
|
engine._subagents = ctx.get('subagents') || undefined
|
|
2627
4395
|
// 时间检测用的服务(软获取,缺失时定时兜底自动降级)
|
|
2628
4396
|
engine._sessionsSvc = ctx.get('sessions') || undefined
|
|
2629
4397
|
engine._agentSvc = ctx.get('agent') || undefined
|
|
4398
|
+
// 模型目录(设置页「总结/问候默认模型」抽屉数据源);软获取,缺失时抽屉回退手动输入
|
|
4399
|
+
engine._llm = ctx.get('llm') || undefined
|
|
2630
4400
|
|
|
2631
4401
|
// 生命周期刷新
|
|
2632
|
-
const refreshAll = (agent) => {
|
|
4402
|
+
const refreshAll = (agent) => {
|
|
4403
|
+
void engine.refresh(agent).then(() => {
|
|
4404
|
+
// M4-3:refresh 完成后同步捕获 paths 快照供 Shadow 调度(§7.2 禁止异步 tick 内裸调 resolvePaths)
|
|
4405
|
+
try { if (engine._shadowHost && agent && engine.state.ws) engine._shadowHost.capturePaths(engine.runtimeFor(agent).key, engine.state) } catch (_) {}
|
|
4406
|
+
// M5-3:同一 paths 快照共享给 Context Bridge(仅相对投影,不落盘路径)
|
|
4407
|
+
try { if (engine._contextHost && agent && engine.state.ws) engine._contextHost.capturePaths(engine.runtimeFor(agent).key, engine.state) } catch (_) {}
|
|
4408
|
+
// M6-3:同一 paths 快照共享给 Activation Inbox(corpus 查询用)
|
|
4409
|
+
try { if (engine._activationHost && agent && engine.state.ws) engine._activationHost.capturePaths(engine.runtimeFor(agent).key, engine.state) } catch (_) {}
|
|
4410
|
+
}).catch(() => {})
|
|
4411
|
+
}
|
|
2633
4412
|
refreshAll()
|
|
2634
4413
|
// 自动检查更新:host 启动时查一次 npm registry(结果缓存 12 小时,设置页打开直接读缓存显示)
|
|
2635
4414
|
void engine.checkUpdate(false)
|
|
@@ -2638,23 +4417,41 @@ export function apply(ctx, config) {
|
|
|
2638
4417
|
const noticesTimer = setInterval(noticesRefresh, 3600 * 1000)
|
|
2639
4418
|
void engine.fetchNotices(false).then((l) => { engine._noticesCache = l }).catch(() => {})
|
|
2640
4419
|
ctx.on('agent/session-start', (payload) => {
|
|
2641
|
-
|
|
2642
|
-
//
|
|
2643
|
-
|
|
4420
|
+
// M0/M1: 建立该 agent/session 的 runtime(WeakMap/Map 登记),之后所有状态按 session 隔离。
|
|
4421
|
+
// 审查修复轮2:生命周期入口同样走严格身份——无 session 身份的对象不得经 runtimeFor 伪造匿名 runtime,
|
|
4422
|
+
// 也不得进入 _lastAgent(subagent parent 需要 session)。
|
|
4423
|
+
try {
|
|
4424
|
+
if (payload && payload.agent && engine.hasReliableSessionIdentity(payload.agent)) {
|
|
4425
|
+
const rt = engine.runtimeFor(payload.agent)
|
|
4426
|
+
engine._lastAgent = payload.agent
|
|
4427
|
+
// M2.4: 对已存在的 session.events 按原生 seq 补放(live feed 之后从游标续接去重)
|
|
4428
|
+
engine.seedRuntimeFromSession(payload.agent)
|
|
4429
|
+
engine.ingestAgentLifecycle(payload.agent, 'agent/session-start', { payload: { source: boundedStr(payload && payload.source) } })
|
|
4430
|
+
}
|
|
4431
|
+
} catch (e) {}
|
|
4432
|
+
// 审查修复轮3:refreshAll 同样受严格身份守卫 —— 匿名对象不得把 cwd 写进 default runtime state
|
|
4433
|
+
const ssa = payload && payload.agent
|
|
4434
|
+
if (!ssa || engine.hasReliableSessionIdentity(ssa)) refreshAll(ssa)
|
|
2644
4435
|
})
|
|
2645
4436
|
ctx.on('agent/turn-stopping', (payload) => {
|
|
2646
|
-
refreshAll
|
|
2647
|
-
|
|
2648
|
-
|
|
4437
|
+
// 审查修复轮3:refreshAll 只对可靠身份 agent 执行,匿名对象不得污染 default runtime state
|
|
4438
|
+
const evtAgent = payload && payload.agent
|
|
4439
|
+
if (!evtAgent || engine.hasReliableSessionIdentity(evtAgent)) refreshAll(evtAgent)
|
|
2649
4440
|
// 每轮自动沉淀:取本轮消息 → subagent 判断/提炼 → 写今日日志([自动沉淀])+升格长期记忆
|
|
2650
4441
|
try {
|
|
2651
|
-
const agent =
|
|
2652
|
-
|
|
2653
|
-
|
|
4442
|
+
const agent = evtAgent
|
|
4443
|
+
// 审查修复轮3:有效性判断必须是严格 session 身份 —— !!agent.session 会放行无身份对象
|
|
4444
|
+
const hasAgent = !!(agent && engine.hasReliableSessionIdentity(agent))
|
|
4445
|
+
if (hasAgent) {
|
|
4446
|
+
// 最后活动时间写入该 agent 自己的 runtime(不再落到 currentRuntime()/default)
|
|
4447
|
+
try { engine.runtimeFor(agent).lastActiveAt = Date.now() } catch (e) {}
|
|
4448
|
+
try { engine.ingestAgentLifecycle(agent, 'agent/turn-stopping', { turn: payload && payload.turn, payload: { turn: payload && payload.turn } }) } catch (e) {}
|
|
4449
|
+
}
|
|
4450
|
+
diag('turn-stopping fired: turn=' + JSON.stringify(payload && payload.turn) + ' hasAgent=' + hasAgent + ' payloadKeys=' + (payload ? Object.keys(payload).join(',') : 'null'))
|
|
2654
4451
|
if (hasAgent) {
|
|
2655
4452
|
// 延迟到 turn-stopping 收尾完成后再启动 subagent,避免与 DSH 会话收尾竞争导致进程级崩溃
|
|
2656
4453
|
setTimeout(() => {
|
|
2657
|
-
void engine.consolidateTurn(payload.turn, agent).catch((e) => console.error('[dsh-auto-memory] consolidateTurn unhandled', e && (e.stack || e.message) || e))
|
|
4454
|
+
void engine.withAgent(agent, () => engine.consolidateTurn(payload.turn, agent)).catch((e) => console.error('[dsh-auto-memory] consolidateTurn unhandled', e && (e.stack || e.message) || e))
|
|
2658
4455
|
}, 600)
|
|
2659
4456
|
}
|
|
2660
4457
|
} catch (e) { diag('turn-stopping handler error: ' + (e && e.message)) }
|
|
@@ -2664,20 +4461,45 @@ export function apply(ctx, config) {
|
|
|
2664
4461
|
// 首轮(loadedAt=0)必定 await 完 → 模型从第一个 token 起就看到记忆;之后每 15s 跟进轮间新写入。
|
|
2665
4462
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
2666
4463
|
try {
|
|
2667
|
-
const agent =
|
|
2668
|
-
//
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
4464
|
+
const agent = payload && payload.agent // M1: 精确取当前 agent 的 runtime,不猜
|
|
4465
|
+
// 审查修复轮2:无 session 身份的 agent 不登记 runtime、不进 stateFor/refresh,直接放行
|
|
4466
|
+
if (agent && engine.hasReliableSessionIdentity(agent)) {
|
|
4467
|
+
// 首步即登记 runtime(重启恢复的会话也可能不触发 session-start,这里补登记)
|
|
4468
|
+
const rt = engine.runtimeFor(agent)
|
|
4469
|
+
// M7.5:pre-step 边界先冲刷 CoT 缓冲——保证本轮 context_push 的 window 携带最新思维链
|
|
4470
|
+
try { engine.flushReasoningBuffer(rt) } catch (_) {}
|
|
4471
|
+
engine.ingestAgentLifecycle(agent, 'agent/pre-step', { turn: payload && payload.turn, step: payload && payload.step, payload: { turn: payload && payload.turn, step: payload && payload.step } })
|
|
4472
|
+
// M6-3:pre-step 时序(§8)——校验 cursor/index/TTL 后 claim packet,等待渲染面消费
|
|
4473
|
+
try { if (engine._activationHost) engine._activationHost.onPreStep(agent) } catch (_) {}
|
|
4474
|
+
// 只刷新顶层会话:子代理(自动沉淀/固化的 subagent)session 无 cwd,刷新会把 state 切到错误工作区
|
|
4475
|
+
let skip = false
|
|
4476
|
+
try { if (agent.session && agent.session.header && agent.session.header.parentSession) skip = true } catch (e) {}
|
|
4477
|
+
const st = engine.stateFor(agent)
|
|
4478
|
+
if (!skip && (!st.loadedAt || Date.now() - st.loadedAt > 15000)) {
|
|
4479
|
+
await engine.refresh(agent)
|
|
4480
|
+
}
|
|
2676
4481
|
}
|
|
2677
4482
|
} catch (e) {}
|
|
2678
4483
|
return next()
|
|
2679
4484
|
})
|
|
2680
4485
|
|
|
4486
|
+
// M0/M1: agent/session 销毁时清理对应 runtime(abort 挂起任务 + 从 store 移除;M2 同步清空 ring/pending)
|
|
4487
|
+
ctx.on('agent/disposed', (payload) => {
|
|
4488
|
+
try { if (payload && payload.agent) engine.disposeAgent(payload.agent) } catch (e) {}
|
|
4489
|
+
})
|
|
4490
|
+
ctx.on('session/disposed', (session) => {
|
|
4491
|
+
try { engine.runtimes.disposeSession(session) } catch (e) {}
|
|
4492
|
+
})
|
|
4493
|
+
|
|
4494
|
+
// M2.1: 结构化事件入口 —— session/event(post-commit append feed)+ frozen tools/result(执行级最终观察点)。
|
|
4495
|
+
// 只观察、有界最小投影;无可靠 owner 的事件被丢弃留痕,绝不落入 default runtime。
|
|
4496
|
+
ctx.on('session/event', (session, event) => {
|
|
4497
|
+
try { engine.observeSessionEvent(session, event) } catch (e) {}
|
|
4498
|
+
})
|
|
4499
|
+
ctx.on('tools/result', (exec, result) => {
|
|
4500
|
+
try { engine.observeToolResult(exec, result) } catch (e) {}
|
|
4501
|
+
})
|
|
4502
|
+
|
|
2681
4503
|
// ---------- 系统提示词注入 ----------
|
|
2682
4504
|
// 动态记忆 → ctx.systemPrompt.context():渲染为 user-role 快照追加在历史尾部,内容不变不重复注入(dsh-agent-loop project() 去重)。
|
|
2683
4505
|
// system prompt 不再包含动态内容 → 字节级稳定 → DeepSeek 前缀缓存全程命中(自动沉淀/跨天/切模型都不再击穿前缀)。
|
|
@@ -2688,10 +4510,63 @@ export function apply(ctx, config) {
|
|
|
2688
4510
|
try {
|
|
2689
4511
|
const agent = context && context.agent
|
|
2690
4512
|
if (!agent) return ''
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
4513
|
+
// M0/M1: 绑定到该 agent 的 runtime 再读取(state 是 per-session getter,避免落到 default runtime 读到空)
|
|
4514
|
+
return engine.withAgent(agent, () => {
|
|
4515
|
+
const st = engine.stateFor(agent)
|
|
4516
|
+
if (!st.loadedAt || Date.now() - st.loadedAt > 15000) {
|
|
4517
|
+
void engine.refresh(agent)
|
|
4518
|
+
}
|
|
4519
|
+
// 2026-08-27 频率控制(在调用方做,不破坏 renderMemoryDynamic 契约):
|
|
4520
|
+
// 指纹只基于"日志段"(recentLogs)——日志是每轮微变源。日志变化且距上次注入不足
|
|
4521
|
+
// snapshotMinGapRounds 轮 → 暂缓;反思/用户/笔记/欢迎等结构性变化始终立即注入。
|
|
4522
|
+
// 内容未变则正常返回(由 project 去重)。状态 per-agent(挂 runtime.state)防 A/B 串线。
|
|
4523
|
+
const snap = engine.renderMemoryDynamic(context)
|
|
4524
|
+
const gap = Math.max(0, Number(engine.config.snapshotMinGapRounds) || 5)
|
|
4525
|
+
if (gap > 0 && snap) {
|
|
4526
|
+
const st = engine.stateFor(agent)
|
|
4527
|
+
// 2026-08-27 压缩检测:上下文压缩/截断常伴随 contextVersion 重置(归零/倒退)。
|
|
4528
|
+
// 开启 snapshotReinjectOnCompact 时,压缩后绕过间隔立即重注入(快照被清掉后必须重建)。
|
|
4529
|
+
try {
|
|
4530
|
+
const rt = engine.runtimeFor(agent)
|
|
4531
|
+
const cv = (rt && rt.contextVersion) || 0
|
|
4532
|
+
if (engine.config.snapshotReinjectOnCompact !== false && st._snapCv !== undefined && cv < st._snapCv) {
|
|
4533
|
+
st._snapLogFp = '' // 强制重注入:清掉指纹,下个分支当"日志变化"处理
|
|
4534
|
+
st._snapPendingSnap = null
|
|
4535
|
+
st._snapPendingLogFp = null
|
|
4536
|
+
}
|
|
4537
|
+
st._snapCv = cv
|
|
4538
|
+
} catch (_) {}
|
|
4539
|
+
// 日志段指纹:从快照中截出"最近 N 天工作日志"段(含标题行到下一个 [ 段)
|
|
4540
|
+
const logMatch = snap.match(/\[最近 \d+ 天工作日志[^\]]*\][\s\S]*?(?=\n\[|\n<memory_system>|\n$)/)
|
|
4541
|
+
const logFp = createHash('sha256').update(logMatch ? logMatch[0] : '').digest('hex').slice(0, 16)
|
|
4542
|
+
if (logFp !== (st._snapLogFp || '')) {
|
|
4543
|
+
// 日志变化:距上次注入不足间隔 → 暂缓
|
|
4544
|
+
st._snapRound = (st._snapRound || 0) + 1
|
|
4545
|
+
if (st._snapFp === undefined) {
|
|
4546
|
+
st._snapFp = snap
|
|
4547
|
+
st._snapLogFp = logFp
|
|
4548
|
+
st._snapLastRound = st._snapRound
|
|
4549
|
+
} else if (st._snapRound - (st._snapLastRound || 0) < gap) {
|
|
4550
|
+
st._snapPendingSnap = snap
|
|
4551
|
+
st._snapPendingLogFp = logFp
|
|
4552
|
+
return engine.renderReflectionRequest()
|
|
4553
|
+
} else {
|
|
4554
|
+
st._snapFp = snap
|
|
4555
|
+
st._snapLogFp = logFp
|
|
4556
|
+
st._snapLastRound = st._snapRound
|
|
4557
|
+
}
|
|
4558
|
+
} else if (st._snapPendingSnap && logFp === st._snapPendingLogFp) {
|
|
4559
|
+
// 日志已稳定到间隔:放行暂缓版本
|
|
4560
|
+
st._snapFp = st._snapPendingSnap
|
|
4561
|
+
st._snapLogFp = logFp
|
|
4562
|
+
st._snapLastRound = st._snapRound
|
|
4563
|
+
st._snapPendingSnap = null
|
|
4564
|
+
st._snapPendingLogFp = null
|
|
4565
|
+
return snap + engine.renderReflectionRequest()
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
return snap + engine.renderReflectionRequest()
|
|
4569
|
+
})
|
|
2695
4570
|
} catch (e) { return '' }
|
|
2696
4571
|
},
|
|
2697
4572
|
})
|
|
@@ -2703,6 +4578,20 @@ export function apply(ctx, config) {
|
|
|
2703
4578
|
try { return engine.renderMemoryStatic() } catch (e) { return '' }
|
|
2704
4579
|
},
|
|
2705
4580
|
})
|
|
4581
|
+
// M6-3:专用动态 Reference Tail context surface('dynamic-context' capability)。
|
|
4582
|
+
// 仅渲染已 claimed 的 packet;返回非空=实际进入下一请求 messages → 同步 markDelivered + seen。
|
|
4583
|
+
// systemPrompt.section 永不承载该动态内容。
|
|
4584
|
+
const disposeTailSurface = ctx.systemPrompt.context({
|
|
4585
|
+
name: 'dsh:m6-reference-tail-pre',
|
|
4586
|
+
order: SECTION_ORDER + 1,
|
|
4587
|
+
text: (context) => {
|
|
4588
|
+
try {
|
|
4589
|
+
const agent = context && context.agent
|
|
4590
|
+
if (!agent || !engine._activationHost) return ''
|
|
4591
|
+
return engine.withAgent(agent, () => engine._activationHost.renderTailFor(agent)) || ''
|
|
4592
|
+
} catch (e) { return '' }
|
|
4593
|
+
},
|
|
4594
|
+
})
|
|
2706
4595
|
|
|
2707
4596
|
// ---------- 工具 ----------
|
|
2708
4597
|
const tools = [
|
|
@@ -2896,6 +4785,340 @@ export function apply(ctx, config) {
|
|
|
2896
4785
|
|
|
2897
4786
|
// ---------- 路由 ----------
|
|
2898
4787
|
const routes = [
|
|
4788
|
+
{
|
|
4789
|
+
kind: 'exact',
|
|
4790
|
+
path: API['semantic-status'],
|
|
4791
|
+
handler: async (req, res) => {
|
|
4792
|
+
// M7.5 C2 资产检测(只读;loopback):供首启向导/设置页判断语义引擎就绪状态
|
|
4793
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4794
|
+
try {
|
|
4795
|
+
const probe = await engine.semanticAssetProbe()
|
|
4796
|
+
const pyCands = [
|
|
4797
|
+
path.join(pluginRootDir(), 'python', 'bench', 'models-xenova-bge-m3-int8', 'onnx', 'model_int8.onnx'),
|
|
4798
|
+
]
|
|
4799
|
+
const fsMod = await import('node:fs')
|
|
4800
|
+
const pyOnnx = pyCands.find((c) => fsMod.existsSync(c)) || pyCands[0]
|
|
4801
|
+
// 发射开关读数(memory/semantic/embedding-config.json 的
|
|
4802
|
+
// activationEmitMode;worker 同源读取,缺省 shadow=fail closed)
|
|
4803
|
+
let activationEmitMode = 'shadow'
|
|
4804
|
+
try {
|
|
4805
|
+
const emRaw = JSON.parse(fsMod.readFileSync(path.join(dshHome(), 'memory', 'semantic', 'embedding-config.json'), 'utf8'))
|
|
4806
|
+
const em = String((emRaw && emRaw.activationEmitMode) || 'shadow')
|
|
4807
|
+
if (['shadow', 'canary-explicit', 'active'].includes(em)) activationEmitMode = em
|
|
4808
|
+
} catch (_) {}
|
|
4809
|
+
const resolvedTier = await engine.resolveSemanticTier()
|
|
4810
|
+
return writeJson(res, 200, {
|
|
4811
|
+
engineDefault: 'js',
|
|
4812
|
+
activationEmitMode,
|
|
4813
|
+
resolvedTier,
|
|
4814
|
+
jsSemantic: engine._jsSemantic ? engine._jsSemantic.status() : null,
|
|
4815
|
+
download: engine._jsDownload ? engine._jsDownload.state() : null,
|
|
4816
|
+
manifestBytes: E5_SMALL_Q8_MANIFEST_V1.totalBytes,
|
|
4817
|
+
assetPresent: probe.assetPresent,
|
|
4818
|
+
peerPresent: probe.peerPresent,
|
|
4819
|
+
ready: probe.ready,
|
|
4820
|
+
assetBytes: probe.assetBytes,
|
|
4821
|
+
assetPath: probe.assetPath,
|
|
4822
|
+
pythonInt8Present: fsMod.existsSync(pyOnnx),
|
|
4823
|
+
pythonInt8Bytes: fsMod.existsSync(pyOnnx) ? fsMod.statSync(pyOnnx).size : 0,
|
|
4824
|
+
tuning: {
|
|
4825
|
+
tauHi: null, deltaExp: null, deltaPro: null, // 由策略 JSON 权威;设置页只读展示
|
|
4826
|
+
},
|
|
4827
|
+
note: 'C3 python int8 tier optional; fp32 suspended per 2026-08-26 ruling',
|
|
4828
|
+
})
|
|
4829
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
4830
|
+
},
|
|
4831
|
+
},
|
|
4832
|
+
{
|
|
4833
|
+
kind: 'exact',
|
|
4834
|
+
path: API['semantic-download'],
|
|
4835
|
+
handler: async (req, res) => {
|
|
4836
|
+
// C2 资产下载器(POST start/cancel;loopback-only)。GET 不支持——进度走 semantic-status。
|
|
4837
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4838
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
4839
|
+
const body = await readJsonBody(req)
|
|
4840
|
+
const action = String((body && body.action) || '')
|
|
4841
|
+
if (action === 'start') {
|
|
4842
|
+
const r = engine._jsDownload.start(body && body.mirror)
|
|
4843
|
+
return writeJson(res, r.ok ? 200 : 400, Object.assign({ action: 'start' }, r))
|
|
4844
|
+
}
|
|
4845
|
+
if (action === 'cancel') {
|
|
4846
|
+
const r = engine._jsDownload.cancel()
|
|
4847
|
+
return writeJson(res, r.ok ? 200 : 409, Object.assign({ action: 'cancel' }, r))
|
|
4848
|
+
}
|
|
4849
|
+
return writeJson(res, 400, { error: 'invalid action' })
|
|
4850
|
+
},
|
|
4851
|
+
},
|
|
4852
|
+
{
|
|
4853
|
+
kind: 'exact',
|
|
4854
|
+
path: API['semantic-emit'],
|
|
4855
|
+
handler: async (req, res) => {
|
|
4856
|
+
// 唤起注入模式(POST {mode:shadow|canary-explicit|active};loopback-only)。
|
|
4857
|
+
// 写入 embedding-config.json 的 activationEmitMode,JS/Python 双轨同源读取。
|
|
4858
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4859
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
4860
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
4861
|
+
const mode = String((body && body.mode) || '')
|
|
4862
|
+
if (!['shadow', 'canary-explicit', 'active'].includes(mode)) return writeJson(res, 400, { error: 'invalid mode' })
|
|
4863
|
+
try {
|
|
4864
|
+
const fsMod = await import('node:fs')
|
|
4865
|
+
const pathMod = await import('node:path')
|
|
4866
|
+
const cfgPath = pathMod.join(dshHome(), 'memory', 'semantic', 'embedding-config.json')
|
|
4867
|
+
let cfg = {}
|
|
4868
|
+
try { cfg = JSON.parse(fsMod.readFileSync(cfgPath, 'utf8')) } catch (_) {}
|
|
4869
|
+
if (!cfg || typeof cfg !== 'object') cfg = {}
|
|
4870
|
+
cfg.activationEmitMode = mode
|
|
4871
|
+
fsMod.mkdirSync(pathMod.dirname(cfgPath), { recursive: true })
|
|
4872
|
+
fsMod.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), 'utf8')
|
|
4873
|
+
return writeJson(res, 200, { ok: true, mode })
|
|
4874
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
4875
|
+
},
|
|
4876
|
+
},
|
|
4877
|
+
{
|
|
4878
|
+
kind: 'exact',
|
|
4879
|
+
path: API['shadow-recent'],
|
|
4880
|
+
handler: async (req, res) => {
|
|
4881
|
+
// G-02 前置(只读;loopback):返回最近 shadow 决策行的脱敏投影,供「语料精修」面板渲染 A/P/S/H/E
|
|
4882
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4883
|
+
try {
|
|
4884
|
+
const fsMod = await import('node:fs')
|
|
4885
|
+
const pathMod = await import('node:path')
|
|
4886
|
+
const file = pathMod.join(dshHome(), 'memory', 'semantic', 'activation-shadow-v2.jsonl')
|
|
4887
|
+
if (!fsMod.existsSync(file)) return writeJson(res, 200, { rows: [] })
|
|
4888
|
+
const all = fsMod.readFileSync(file, 'utf8').split('\n').filter(Boolean)
|
|
4889
|
+
const tail = all.slice(-24).map((l) => {
|
|
4890
|
+
try {
|
|
4891
|
+
const r = JSON.parse(l)
|
|
4892
|
+
const f = r.features || {}
|
|
4893
|
+
return {
|
|
4894
|
+
observationId: r.observationId, decision: r.decision,
|
|
4895
|
+
reasonCodes: r.reasonCodes || [], lane: (f && f.lane) || r.lane || null,
|
|
4896
|
+
intentProb: (f && f.intentProb) != null ? f.intentProb : null,
|
|
4897
|
+
margin: (f && f.margin) != null ? f.margin : null,
|
|
4898
|
+
candidateHit: r.candidateHit != null ? r.candidateHit : null,
|
|
4899
|
+
maxLexRaw: r.maxLexRaw != null ? r.maxLexRaw : null,
|
|
4900
|
+
memoryRefCount: Array.isArray(r.candidateProvenance) ? r.candidateProvenance.length : null,
|
|
4901
|
+
// G-02 v1 增补:候选锚点前 3(8 字符前缀,脱敏——完整 id 不出端点)
|
|
4902
|
+
anchors: (Array.isArray(r.candidateProvenance) ? r.candidateProvenance : [])
|
|
4903
|
+
.slice(0, 3).map((p) => (p && p.memoryId ? String(p.memoryId).slice(0, 12) : null))
|
|
4904
|
+
.filter(Boolean),
|
|
4905
|
+
queryChars: r.queryChars || null, ts: r.ts || null,
|
|
4906
|
+
}
|
|
4907
|
+
} catch (e) { return { observationId: 'parse-error' } }
|
|
4908
|
+
})
|
|
4909
|
+
// G-02 v2(2026-08-30):决策↔投递时间线——把每条决策行与其真实投递结果关联。
|
|
4910
|
+
// 投递面=证据事件里的 seen 簇(5s 内聚合成一次投递)∪ activation host 的 render
|
|
4911
|
+
// 事件(volatile,含 skill 标志)。关联是启发式(时间窗+memoryId 交集),仅用于
|
|
4912
|
+
// 可视化,不参与任何决策。完整 memoryId 只在服务端做交集,不出端点(v1 隐私口径)。
|
|
4913
|
+
try {
|
|
4914
|
+
const fullIdsByRow = all.slice(-24).map((l) => {
|
|
4915
|
+
try { return (JSON.parse(l).candidateProvenance || []).map((p) => p && p.memoryId).filter(Boolean) } catch (_) { return [] }
|
|
4916
|
+
})
|
|
4917
|
+
const evDir2 = pathMod.join(dshHome(), 'memory', 'evidence', 'events')
|
|
4918
|
+
const seenClusters = []
|
|
4919
|
+
if (fsMod.existsSync(evDir2)) {
|
|
4920
|
+
const raw = []
|
|
4921
|
+
for (const f of fsMod.readdirSync(evDir2).filter((x) => x.endsWith('.jsonl')).slice(-2)) {
|
|
4922
|
+
const lines = fsMod.readFileSync(pathMod.join(evDir2, f), 'utf8').split('\n').filter(Boolean)
|
|
4923
|
+
for (const ln of lines.slice(-400)) {
|
|
4924
|
+
try { const r = JSON.parse(ln); if (r.kind === 'seen' && r.memoryId) raw.push({ at: Number((r.event && r.event.ts) || r.ts) || 0, memoryId: r.memoryId }) } catch (_) {}
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
raw.sort((a, b) => a.at - b.at)
|
|
4928
|
+
for (const s of raw) {
|
|
4929
|
+
const last = seenClusters[seenClusters.length - 1]
|
|
4930
|
+
if (last && s.at - last.at <= 5000) last.memoryIds.push(s.memoryId)
|
|
4931
|
+
else seenClusters.push({ at: s.at, memoryIds: [s.memoryId] })
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
let renderEvents = []
|
|
4935
|
+
try {
|
|
4936
|
+
const dv = engine._activationHost && engine._activationHost.debugView && engine._activationHost.debugView()
|
|
4937
|
+
renderEvents = ((dv && dv.recentEvents) || []).filter((e) => e && e.kind === 'render')
|
|
4938
|
+
} catch (_) {}
|
|
4939
|
+
tail.forEach((row, i) => {
|
|
4940
|
+
const ids = fullIdsByRow[i] || []
|
|
4941
|
+
let delivery = null
|
|
4942
|
+
// 只有关 deliverable 决策(emit/prefetch)才存在投递;取决策后最近的 seen 簇(≤300s)
|
|
4943
|
+
if (row.ts && ids.length && (row.decision === 'emit' || row.decision === 'prefetch')) {
|
|
4944
|
+
const tsMs = row.ts * 1000
|
|
4945
|
+
let best = null
|
|
4946
|
+
for (const c of seenClusters) {
|
|
4947
|
+
if (c.at < tsMs || c.at - tsMs > 300000) continue
|
|
4948
|
+
if (!c.memoryIds.some((m) => ids.includes(m))) continue
|
|
4949
|
+
if (!best || c.at < best.at) best = c
|
|
4950
|
+
}
|
|
4951
|
+
if (best) {
|
|
4952
|
+
const re = renderEvents.filter((e) => Math.abs(e.at - best.at) <= 15000).slice(-1)[0]
|
|
4953
|
+
delivery = { at: best.at, count: best.memoryIds.length, skill: !!(re && re.skill) }
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
row.delivery = delivery
|
|
4957
|
+
})
|
|
4958
|
+
} catch (_) {}
|
|
4959
|
+
return writeJson(res, 200, { rows: tail })
|
|
4960
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message) }) }
|
|
4961
|
+
},
|
|
4962
|
+
},
|
|
4963
|
+
{
|
|
4964
|
+
kind: 'exact',
|
|
4965
|
+
path: API['review-feedback'],
|
|
4966
|
+
handler: async (req, res) => {
|
|
4967
|
+
// G-02 前置(append-only 审批队列;loopback):用户 A/P/S/H/E 判定落盘,不直接改任何策略。
|
|
4968
|
+
// G-02 v2(2026-08-30)新增 GET:队列投影 + 判定×决策行联查汇总 + 政策提示(纯描述,
|
|
4969
|
+
// 不改参数——政策演进仍走离线 replay/审批流程)。
|
|
4970
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4971
|
+
try {
|
|
4972
|
+
const fsMod = await import('node:fs')
|
|
4973
|
+
const pathMod = await import('node:path')
|
|
4974
|
+
const dir = pathMod.join(dshHome(), 'memory', 'semantic')
|
|
4975
|
+
const file = pathMod.join(dir, 'review-queue.jsonl')
|
|
4976
|
+
if ((req.method || 'GET') === 'GET') {
|
|
4977
|
+
let entries = []
|
|
4978
|
+
if (fsMod.existsSync(file)) {
|
|
4979
|
+
entries = fsMod.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-100)
|
|
4980
|
+
.map((l) => { try { return JSON.parse(l) } catch (_) { return null } }).filter(Boolean)
|
|
4981
|
+
}
|
|
4982
|
+
// 联查 shadow 决策行(observationId → 行摘要)
|
|
4983
|
+
const shadowFile = pathMod.join(dir, 'activation-shadow-v2.jsonl')
|
|
4984
|
+
const byObs = new Map()
|
|
4985
|
+
if (fsMod.existsSync(shadowFile)) {
|
|
4986
|
+
const lines = fsMod.readFileSync(shadowFile, 'utf8').split('\n').filter(Boolean)
|
|
4987
|
+
for (const ln of lines.slice(-600)) {
|
|
4988
|
+
try { const r = JSON.parse(ln); if (r.observationId && !byObs.has(r.observationId)) byObs.set(r.observationId, r) } catch (_) {}
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
const joined = entries.map((e) => {
|
|
4992
|
+
const r = byObs.get(e.observationId)
|
|
4993
|
+
const f = (r && r.features) || {}
|
|
4994
|
+
return {
|
|
4995
|
+
at: e.at, choice: e.choice, observationId: e.observationId,
|
|
4996
|
+
decision: r ? r.decision : null, lane: (f && f.lane) || null,
|
|
4997
|
+
intentProb: (f && f.intentProb) != null ? f.intentProb : null,
|
|
4998
|
+
topReasons: r && Array.isArray(r.reasonCodes) ? r.reasonCodes.slice(0, 3) : [],
|
|
4999
|
+
}
|
|
5000
|
+
})
|
|
5001
|
+
const byChoice = {}
|
|
5002
|
+
for (const j of joined) byChoice[j.choice] = (byChoice[j.choice] || 0) + 1
|
|
5003
|
+
const hints = []
|
|
5004
|
+
for (const ch of ['H', 'S', 'E', 'A', 'P']) {
|
|
5005
|
+
const group = joined.filter((j) => j.choice === ch)
|
|
5006
|
+
if (!group.length) continue
|
|
5007
|
+
const rc = {}
|
|
5008
|
+
let hiConfidence = 0
|
|
5009
|
+
for (const g of group) { for (const x of g.topReasons) rc[x] = (rc[x] || 0) + 1; if (g.decision === 'emit' && (g.intentProb || 0) >= 0.6) hiConfidence++ }
|
|
5010
|
+
const topRc = Object.entries(rc).sort((a, b) => b[1] - a[1]).slice(0, 3).map((x) => x[0]).join(',')
|
|
5011
|
+
if (ch === 'H' && group.some((g) => g.decision === 'emit')) hints.push('H 有害判定落在 emit 决策×' + group.filter((g) => g.decision === 'emit').length + '——建议复核对应 reasonCodes 的精确率: ' + topRc)
|
|
5012
|
+
if (ch === 'S' && hiConfidence > 0) hints.push('S 应抑制判定命中高置信 emit(intentProb≥0.6)×' + hiConfidence + '——建议复核 explicit 车道 margin 阈值')
|
|
5013
|
+
if (ch === 'A' && group.length >= 2) hints.push('A 该激活×' + group.length + '(常见原因码: ' + (topRc || '—') + ')——可作晋升采纳候选')
|
|
5014
|
+
}
|
|
5015
|
+
return writeJson(res, 200, { queue: joined.slice(-50), byChoice, hints })
|
|
5016
|
+
}
|
|
5017
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5018
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
5019
|
+
const choice = String((body && body.choice) || '').toUpperCase()
|
|
5020
|
+
if (!['A', 'P', 'S', 'H', 'E'].includes(choice)) return writeJson(res, 400, { error: 'invalid choice' })
|
|
5021
|
+
fsMod.mkdirSync(dir, { recursive: true })
|
|
5022
|
+
const rec = { schemaVersion: 1, at: Date.now(),
|
|
5023
|
+
observationId: String((body && body.observationId) || '').slice(0, 64),
|
|
5024
|
+
choice, targetMemoryId: String((body && body.targetMemoryId) || '').slice(0, 64) || null,
|
|
5025
|
+
source: 'client-refine-panel' }
|
|
5026
|
+
fsMod.appendFileSync(file, JSON.stringify(rec) + '\n', 'utf8')
|
|
5027
|
+
return writeJson(res, 200, { ok: true, queued: true })
|
|
5028
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message) }) }
|
|
5029
|
+
},
|
|
5030
|
+
},
|
|
5031
|
+
{
|
|
5032
|
+
kind: 'exact',
|
|
5033
|
+
path: API['memory-hub'],
|
|
5034
|
+
handler: async (req, res) => {
|
|
5035
|
+
// M8 记忆中枢(loopback):GET 返回三层记忆 overview(episodic/facts/procedures);
|
|
5036
|
+
// POST {action} 触发编排(consolidate=巩固会话 / feed=喂 judgement 行 / render=渲染 checklists)。
|
|
5037
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5038
|
+
try {
|
|
5039
|
+
const hub = engine._memoryHub
|
|
5040
|
+
if (!hub) return writeJson(res, 200, { error: 'hub-unavailable' })
|
|
5041
|
+
if ((req.method || 'GET') === 'POST') {
|
|
5042
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
5043
|
+
const action = String((body && body.action) || '')
|
|
5044
|
+
if (action === 'consolidate') return writeJson(res, 200, hub.consolidateEpisodes())
|
|
5045
|
+
if (action === 'feed') return writeJson(res, 200, hub.ingestJudgementRows(Array.isArray(body.rows) ? body.rows : []))
|
|
5046
|
+
if (action === 'render') return writeJson(res, 200, { checklists: hub.renderChecklists() })
|
|
5047
|
+
if (action === 'crossfeed') return writeJson(res, 200, hub.crossFeed(String((body && body.sessionRef) || '')))
|
|
5048
|
+
// M9 审批动作(2026-08-30,G-02 同款 loopback+append-only 精神):用户在 hubTab
|
|
5049
|
+
// 对技能做晋升/激活/弃用/置顶。晋升走 store 的门槛判定(调门槛=设置页参数),
|
|
5050
|
+
// 不绕过任何 gate;每次动作进 diagnose 审计。
|
|
5051
|
+
const procs = hub.stores && hub.stores.procedures
|
|
5052
|
+
const pid = String((body && body.procedureId) || '')
|
|
5053
|
+
if (['promote', 'activate', 'deprecate', 'pin'].includes(action)) {
|
|
5054
|
+
if (!procs) return writeJson(res, 200, { ok: false, reason: 'no-procedure-store' })
|
|
5055
|
+
if (!pid) return writeJson(res, 400, { error: 'procedureId required' })
|
|
5056
|
+
let r
|
|
5057
|
+
if (action === 'promote') r = procs.promote(pid)
|
|
5058
|
+
else if (action === 'activate') r = procs.activate(pid)
|
|
5059
|
+
else if (action === 'deprecate') r = procs.deprecate(pid, 'user-disabled')
|
|
5060
|
+
else r = procs.setPinned(pid, (body && body.v) !== false)
|
|
5061
|
+
try { diag('hub review: ' + action + ' ' + pid.slice(0, 20) + ' → ' + JSON.stringify(r.ok !== undefined ? r.ok : r.decision || r.reason || '')) } catch (_) {}
|
|
5062
|
+
return writeJson(res, 200, r)
|
|
5063
|
+
}
|
|
5064
|
+
return writeJson(res, 400, { error: 'unknown action' })
|
|
5065
|
+
}
|
|
5066
|
+
return writeJson(res, 200, hub.overview())
|
|
5067
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message) }) }
|
|
5068
|
+
},
|
|
5069
|
+
},
|
|
5070
|
+
{
|
|
5071
|
+
kind: 'exact',
|
|
5072
|
+
path: API['storage-manage'],
|
|
5073
|
+
handler: async (req, res) => {
|
|
5074
|
+
// M10 存储管理(loopback):GET=语料健康扫描(逐源 sidecar↔正文 digest 比对);
|
|
5075
|
+
// POST {action:'scan'|'repair'|'delete'} —— repair 只重建 sidecar(不动正文),
|
|
5076
|
+
// delete 走「正文原子删除 + 在途激活包清理 + 派生事实撤销」三联动。
|
|
5077
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5078
|
+
try {
|
|
5079
|
+
const sm = engine._storageManager
|
|
5080
|
+
if (!sm) return writeJson(res, 200, { error: 'storage-manager-unavailable' })
|
|
5081
|
+
if ((req.method || 'GET') === 'POST') {
|
|
5082
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
5083
|
+
const action = String((body && body.action) || '')
|
|
5084
|
+
if (action === 'scan') return writeJson(res, 200, sm.scanHealth())
|
|
5085
|
+
if (action === 'repair') return writeJson(res, 200, await sm.repair(Array.isArray(body.items) ? body.items : null))
|
|
5086
|
+
if (action === 'delete') {
|
|
5087
|
+
const filePath = String((body && body.filePath) || '')
|
|
5088
|
+
const memoryId = String((body && body.memoryId) || '')
|
|
5089
|
+
if (!filePath || !memoryId) return writeJson(res, 400, { error: 'filePath and memoryId required' })
|
|
5090
|
+
// 路径白名单:只允许当前工作区三源之一,防止外部路径穿越
|
|
5091
|
+
const sc = sm.scanHealth()
|
|
5092
|
+
const allowed = sc.ok ? sc.sources.some((s) => s.file === filePath) : false
|
|
5093
|
+
if (!allowed) return writeJson(res, 403, { error: 'path-not-in-corpus' })
|
|
5094
|
+
const r = await sm.deleteMemory({ filePath, memoryId, expectedDigest: body && body.expectedDigest })
|
|
5095
|
+
try { diag('storage-manage delete: ' + memoryId.slice(0, 20) + ' → ' + JSON.stringify(r.ok) + ' revoked=' + ((r.cascade && r.cascade.revoked && r.cascade.revoked.revoked) || 0)) } catch (_) {}
|
|
5096
|
+
return writeJson(res, 200, r)
|
|
5097
|
+
}
|
|
5098
|
+
return writeJson(res, 400, { error: 'unknown action' })
|
|
5099
|
+
}
|
|
5100
|
+
return writeJson(res, 200, { ...sm.scanHealth(), audit: sm.auditLog().slice(-8) })
|
|
5101
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message) }) }
|
|
5102
|
+
},
|
|
5103
|
+
},
|
|
5104
|
+
{
|
|
5105
|
+
kind: 'exact',
|
|
5106
|
+
path: API['activation-inbox'],
|
|
5107
|
+
handler: async (req, res) => {
|
|
5108
|
+
// M6-3:fake activation 注入/状态(仅 loopback;activationInboxEnabled∧assoc 双门在 host 内再校验)
|
|
5109
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5110
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5111
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
5112
|
+
try {
|
|
5113
|
+
const host = engine._activationHost
|
|
5114
|
+
if (!host) return writeJson(res, 200, { error: 'host-unavailable' })
|
|
5115
|
+
const action = String((body && body.action) || '')
|
|
5116
|
+
if (action === 'inject') return writeJson(res, 200, host.injectActivation(body.request))
|
|
5117
|
+
if (action === 'status') return writeJson(res, 200, { activationInbox: host.debugView() })
|
|
5118
|
+
return writeJson(res, 400, { error: 'unknown action' })
|
|
5119
|
+
} catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
5120
|
+
},
|
|
5121
|
+
},
|
|
2899
5122
|
{
|
|
2900
5123
|
kind: 'exact',
|
|
2901
5124
|
path: API.state,
|
|
@@ -3120,7 +5343,11 @@ export function apply(ctx, config) {
|
|
|
3120
5343
|
if (!body || typeof body !== 'object') return writeJson(res, 400, { error: 'invalid body' })
|
|
3121
5344
|
const allowed = Object.keys(DEFAULT_CONFIG)
|
|
3122
5345
|
const patch = {}
|
|
3123
|
-
for (const key of allowed) if (body[key] !== undefined)
|
|
5346
|
+
for (const key of allowed) if (body[key] !== undefined) {
|
|
5347
|
+
// semanticEngineMode 枚举门:非法值直接丢弃(fail-closed,不落盘)
|
|
5348
|
+
if (key === 'semanticEngineMode' && !['auto', 'lexical', 'js', 'python'].includes(body[key])) continue
|
|
5349
|
+
patch[key] = body[key]
|
|
5350
|
+
}
|
|
3124
5351
|
const saved = await engine.saveConfig(patch)
|
|
3125
5352
|
writeJson(res, 200, { config: saved.config, migrated: saved.migrated || '' })
|
|
3126
5353
|
return
|
|
@@ -3296,13 +5523,46 @@ export function apply(ctx, config) {
|
|
|
3296
5523
|
} catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
3297
5524
|
},
|
|
3298
5525
|
},
|
|
5526
|
+
{
|
|
5527
|
+
// 设置页「总结/问候默认模型」抽屉数据源:枚举 llm 服务全部 provider/model(只读目录)
|
|
5528
|
+
kind: 'exact',
|
|
5529
|
+
path: API.models,
|
|
5530
|
+
handler: async (req, res) => {
|
|
5531
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
5532
|
+
if ((req.method || 'GET') !== 'GET') return writeJson(res, 405, { error: 'method not allowed' })
|
|
5533
|
+
try {
|
|
5534
|
+
const llm = engine._llm
|
|
5535
|
+
if (!llm || typeof llm.listProviders !== 'function' || typeof llm.listModels !== 'function') {
|
|
5536
|
+
return writeJson(res, 200, { providers: [], failures: [], unsupported: true })
|
|
5537
|
+
}
|
|
5538
|
+
const providers = []
|
|
5539
|
+
const failures = []
|
|
5540
|
+
await Promise.all(llm.listProviders().map(async (provider) => {
|
|
5541
|
+
const pid = provider && provider.id !== undefined ? String(provider.id) : String(provider)
|
|
5542
|
+
const pname = provider && provider.name ? String(provider.name) : pid
|
|
5543
|
+
try {
|
|
5544
|
+
const models = await llm.listModels(pid)
|
|
5545
|
+
providers.push({
|
|
5546
|
+
id: pid,
|
|
5547
|
+
name: pname,
|
|
5548
|
+
models: (models || []).map((m2) => ({ id: m2 && m2.id !== undefined ? String(m2.id) : String(m2), name: m2 && m2.name ? String(m2.name) : undefined })),
|
|
5549
|
+
})
|
|
5550
|
+
} catch (e2) {
|
|
5551
|
+
failures.push({ id: pid, name: pname, message: String(e2 && e2.message ? e2.message : e2) })
|
|
5552
|
+
}
|
|
5553
|
+
}))
|
|
5554
|
+
providers.sort((a, b) => a.id.localeCompare(b.id))
|
|
5555
|
+
writeJson(res, 200, { providers, failures })
|
|
5556
|
+
} catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
5557
|
+
},
|
|
5558
|
+
},
|
|
3299
5559
|
]
|
|
3300
5560
|
|
|
3301
5561
|
// ---------- 后台轮询兜底(对标外部助手的心跳轮询):每 5 分钟重试失败的自动沉淀 + 心跳文件 ----------
|
|
3302
5562
|
// 心跳:每次轮询把存活状态写入 ~/.dsh/memory/polling-heartbeat.json(可随时查看 LastWriteTime 确认轮询活着)
|
|
3303
5563
|
const writeHeartbeat = async () => {
|
|
3304
5564
|
try {
|
|
3305
|
-
const q = engine.
|
|
5565
|
+
const q = engine.runtimes.values().reduce((sum, rt) => sum + rt.pendingConsolidations.length, 0)
|
|
3306
5566
|
const hb = path.join(dshHome(), 'memory', 'polling-heartbeat.json')
|
|
3307
5567
|
await mkdir(path.dirname(hb), { recursive: true })
|
|
3308
5568
|
await writeFile(hb, JSON.stringify({
|
|
@@ -3318,10 +5578,11 @@ export function apply(ctx, config) {
|
|
|
3318
5578
|
const retryTimer = setInterval(() => {
|
|
3319
5579
|
void (async () => {
|
|
3320
5580
|
try {
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
5581
|
+
for (const rt of engine.runtimes.values()) {
|
|
5582
|
+
if (rt.pendingConsolidations.length && !rt.consolidating) {
|
|
5583
|
+
const item = rt.pendingConsolidations.shift()
|
|
5584
|
+
if (item && item.agent) void engine.withAgent(item.agent, () => engine.consolidateTurn(item.turn, item.agent))
|
|
5585
|
+
}
|
|
3325
5586
|
}
|
|
3326
5587
|
} catch (e) {}
|
|
3327
5588
|
})()
|
|
@@ -3332,8 +5593,28 @@ export function apply(ctx, config) {
|
|
|
3332
5593
|
|
|
3333
5594
|
// ---------- 注册与清理 ----------
|
|
3334
5595
|
const disposers = []
|
|
3335
|
-
disposers.push(disposeContext, disposeSection)
|
|
3336
|
-
|
|
5596
|
+
disposers.push(disposeContext, disposeSection, () => {
|
|
5597
|
+
try { if (engine._shadowHost) engine._shadowHost.disposeAll('plugin disposed') } catch (e) {}
|
|
5598
|
+
try { if (engine._contextHost) engine._contextHost.disposeAll('plugin disposed') } catch (e) {}
|
|
5599
|
+
try { if (engine._activationHost) engine._activationHost.disposeAll('plugin disposed') } catch (e) {}
|
|
5600
|
+
try { if (engine._indexSyncHost) engine._indexSyncHost.dispose('plugin disposed') } catch (e) {}
|
|
5601
|
+
try { if (engine._pythonSidecar) engine._pythonSidecar.dispose('plugin disposed') } catch (e) {}
|
|
5602
|
+
// M8 hub:清定时器 + 落盘 dispose(各店 dispose 时 io.save 兜底)
|
|
5603
|
+
try { if (engine._hubFeedDisposers) for (const d of engine._hubFeedDisposers) { try { d() } catch (_) {} } } catch (e) {}
|
|
5604
|
+
try { if (engine._memoryHub) engine._memoryHub.dispose('plugin disposed') } catch (e) {}
|
|
5605
|
+
try { engine.runtimes.disposeAll() } catch (e) {}
|
|
5606
|
+
})
|
|
5607
|
+
for (const tool of tools) {
|
|
5608
|
+
// M0/M1: 工具执行绑定到其 agent 的 runtime(exec.agent 精确取 runtime),this.state/autoStats 读写不串线
|
|
5609
|
+
const rawExec = tool.execute
|
|
5610
|
+
if (typeof rawExec === 'function') {
|
|
5611
|
+
tool.execute = async (args, exec) => {
|
|
5612
|
+
const agent = exec && exec.agent
|
|
5613
|
+
return agent ? engine.withAgent(agent, () => rawExec(args, exec)) : rawExec(args, exec)
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
disposers.push(ctx.tools.register(tool))
|
|
5617
|
+
}
|
|
3337
5618
|
for (const route of routes) disposers.push(ctx.webServer.register(route))
|
|
3338
5619
|
ctx.effect(() => () => {
|
|
3339
5620
|
clearInterval(retryTimer)
|