dsh-memory-palace 1.8.0-alpha.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/lib/client.js +2 -2
- package/lib/common/logger.mjs +150 -0
- package/lib/distill.mjs +25 -24
- package/lib/hybrid/subagent.mjs +36 -13
- package/lib/index.js +21 -6
- package/lib/projection.mjs +5 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
把 WorkBuddy 的文件式记忆系统移植进 [DeepSeek Harness](https://www.deepseek.com/harness/) —— 为 Harness 提供**跨会话持久化、人类可直接编辑的 Markdown 记忆**。
|
|
4
4
|
|
|
5
|
-
[](./LICENSE) [](https://awesome-dsh-plugin.com) [](https://www.npmjs.com/package/dsh-memory-palace) [](./LICENSE) [](https://awesome-dsh-plugin.com) [](https://www.npmjs.com/package/dsh-memory-palace) [](https://github.com/deepseek-ai/deepseek-harness) [](#安装)
|
|
6
6
|
|
|
7
7
|
## 特性
|
|
8
8
|
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
dsh plugin --profile web add github:lovezi0/dsh-memory-palace
|
|
82
|
-
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.
|
|
82
|
+
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.8.0
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
方式二:clone 后本地安装(开发 / 修改源码场景)
|
|
@@ -124,6 +124,11 @@ dsh plugin --profile web remove dsh-memory-palace
|
|
|
124
124
|
## 版本历史
|
|
125
125
|
|
|
126
126
|
- **1.8.0**
|
|
127
|
+
- dsh peer [0.1.7 ... 0.2.0]
|
|
128
|
+
- npm publish
|
|
129
|
+
- **1.8.0-alpha.4**
|
|
130
|
+
- 💥移除 stderr log 输出
|
|
131
|
+
- 💪新增不同 profiles 场景下 log文件 *详情见 [DEVELOPMENT.md](./DEVELOPMENT.md#日志落盘)*
|
|
127
132
|
- **1.8.0-alpha.2**
|
|
128
133
|
- 🐛修复多会话并发导致写侧串台 @sunligh91
|
|
129
134
|
- 🐛删除会话分桶后遗留的三个全局状态死字段
|
package/lib/client.js
CHANGED
|
@@ -69,7 +69,7 @@ window.__ModuleLoader__.load({
|
|
|
69
69
|
notifyProjectDone: "项目记忆蒸馏完成",
|
|
70
70
|
notifyFail: "记忆蒸馏失败",
|
|
71
71
|
distillDebugLogLabel: "蒸馏调试日志",
|
|
72
|
-
distillDebugLogHint: "
|
|
72
|
+
distillDebugLogHint: "开启后把本插件全部诊断日志落盘到 profile 目录下的 .memory-palace/logs/<会话 id>/(不再输出控制台)。info.log 收失败/跳过留痕与子代理终态台账;debug.log 收详单,配合 distillLogLevel=debug 时另含 LLM 原始响应。关闭时零输出,仅排障用。",
|
|
73
73
|
dev: "开发",
|
|
74
74
|
devDesc: "蒸馏调试日志与超时",
|
|
75
75
|
distillTimeoutLabel: "蒸馏超时(秒)",
|
|
@@ -143,7 +143,7 @@ window.__ModuleLoader__.load({
|
|
|
143
143
|
notifyProjectDone: "Project memory distilled",
|
|
144
144
|
notifyFail: "Memory distillation failed",
|
|
145
145
|
distillDebugLogLabel: "Distill debug log",
|
|
146
|
-
distillDebugLogHint: "When on,
|
|
146
|
+
distillDebugLogHint: "When on, writes all plugin diagnostics to .memory-palace/logs/<session-id>/ under the profile directory (no console output). info.log holds failure/skip traces and the subagent ledger; debug.log holds verbose detail plus the raw LLM response when distillLogLevel=debug. Off means zero output; troubleshooting only.",
|
|
147
147
|
dev: "Developer",
|
|
148
148
|
devDesc: "Distill debug log and timeout",
|
|
149
149
|
distillTimeoutLabel: "Distill timeout (s)",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// dsh-memory-palace v1.8.0-alpha.4:统一日志落盘模块。
|
|
2
|
+
//
|
|
3
|
+
// 背景:插件原先所有诊断输出走 console.error → dsh 服务端 stderr。但 Desktop 下 stderr 只进
|
|
4
|
+
// host 进程内存(apps/desktop/src/host-process.ts:204 存末 64KB),仅崩溃时随报告落盘,正常运行
|
|
5
|
+
// 时完全不可见;而插件其余落盘点只有 Markdown 记忆文件 —— 于是子代理的失败/跳过、蒸馏链路的
|
|
6
|
+
// 跳过原因全都无痕可查。
|
|
7
|
+
//
|
|
8
|
+
// 落点(与 dsh 原生 @deepseek-ai/dsh-plugin-manager 同构):
|
|
9
|
+
// <profileContext.dir>/.memory-palace/logs/<session-id>/<level>.log
|
|
10
|
+
// 其中 profileContext.dir = $DSH_HOME/profiles/<profile>。取径依据:宿主 CLI 与 Desktop 都经
|
|
11
|
+
// 同一个 runProfile 启动(apps/cli/src/profile-boot.ts:298 provide、apps/desktop-host/src/index.ts:25
|
|
12
|
+
// runProfile({profile:'desktop'})),故 ctx.get('profileContext').dir 两端都可用。
|
|
13
|
+
//
|
|
14
|
+
// 门控:统一受 distillDebugLog 控制 —— false(默认)时零输出(既不写终端也不写文件)。
|
|
15
|
+
// 分级:按【行级别】分流 —— warn/info → info.log;debug → debug.log。
|
|
16
|
+
//
|
|
17
|
+
// 失败安全:调用方都位于 turn 结算 / pre-step 钩子栈上,抛错会污染整个 turn(agent/pre-step 抛错
|
|
18
|
+
// 毁 turn 是已知宿主契约),故所有 fs 错误一律吞掉;写入经独立 promise 链串行(不与记忆落盘锁
|
|
19
|
+
// 共用 —— 日志 IO 不得拖慢记忆写入,且二者语义无关)。
|
|
20
|
+
import { appendFile, mkdir, stat } from "node:fs/promises";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
/** 单文件大小上限(字节);超出后停止追加(不删旧内容)。 */
|
|
24
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
25
|
+
/**
|
|
26
|
+
* 拿不到会话身份时的兜底目录名。
|
|
27
|
+
* ⚠️ 必须与 index.mjs 既有的 `__nosession__` 约定一致(`sid || "__nosession__"`),
|
|
28
|
+
* 否则会出现两套"无身份"目录并存,排查时难以判断哪份是哪条路径落的。
|
|
29
|
+
*/
|
|
30
|
+
const NO_SESSION = "__nosession__";
|
|
31
|
+
|
|
32
|
+
/** 本地时间戳(禁用 toISOString:UTC 会跨日错位,见 AGENTS.md §⑤)。 */
|
|
33
|
+
function stamp() {
|
|
34
|
+
const d = new Date();
|
|
35
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
36
|
+
return (
|
|
37
|
+
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
|
|
38
|
+
`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** JSON 化额外字段;循环引用等异常一律降级为字符串(日志失败不得影响主流程)。 */
|
|
43
|
+
function safeJson(value) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.stringify(value);
|
|
46
|
+
} catch {
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{ ctx: object, getConfig: () => object }} deps
|
|
53
|
+
* @returns {{ for: (sessionId?: string, scope?: string) => object }}
|
|
54
|
+
*/
|
|
55
|
+
export function createLogger({ ctx, getConfig }) {
|
|
56
|
+
let chain = Promise.resolve(); // 独立写链(全局串行)
|
|
57
|
+
const capped = new Set(); // 已达大小上限的文件(停止追加)
|
|
58
|
+
const prepared = new Set(); // 已确保存在的目录
|
|
59
|
+
|
|
60
|
+
// 落盘根 = profile 目录 + 私有尾目录。profileContext 缺失(测试 mock / 非 dsh 启动的宿主)时
|
|
61
|
+
// 返回 null → 放弃落盘,绝不猜路径(process.cwd() 在 CLI 下是用户目录;import.meta.url 在
|
|
62
|
+
// 本地软链安装下解析到开发仓库路径,二者都不可靠)。
|
|
63
|
+
function logRoot() {
|
|
64
|
+
const pc = typeof ctx?.get === "function" ? ctx.get("profileContext") : undefined;
|
|
65
|
+
const dir = pc?.dir;
|
|
66
|
+
return typeof dir === "string" && dir ? join(dir, ".memory-palace", "logs") : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fileOf(root, sessionId, level) {
|
|
70
|
+
const box = typeof sessionId === "string" && sessionId ? sessionId : NO_SESSION;
|
|
71
|
+
return join(root, box, level === "debug" ? "debug.log" : "info.log");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function put(file, text) {
|
|
75
|
+
if (capped.has(file)) return;
|
|
76
|
+
const dir = dirname(file);
|
|
77
|
+
if (!prepared.has(dir)) {
|
|
78
|
+
// 0700/0600:与原生 plugin-manager 同款收紧(日志含 cwd / sessionId / 路径等本机信息)。
|
|
79
|
+
// Windows 下 mode 不生效但不报错(plugin-manager 同样如此);Linux/macOS 上生效。
|
|
80
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
81
|
+
prepared.add(dir);
|
|
82
|
+
}
|
|
83
|
+
const info = await stat(file).catch(() => null);
|
|
84
|
+
if (info !== null && info.size >= MAX_FILE_BYTES) {
|
|
85
|
+
capped.add(file);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// mode 仅在文件首次创建时生效(已存在则忽略),故对新老文件都安全。
|
|
89
|
+
await appendFile(file, text, { encoding: "utf8", mode: 0o600 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 入链写入(fire-and-forget):调用点同步返回,异常全部吞掉。 */
|
|
93
|
+
function enqueue(file, text) {
|
|
94
|
+
chain = chain.then(() => put(file, text)).catch(() => {});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 门控 + 取径 + 入链;任一环节不可用即静默返回。 */
|
|
98
|
+
function emit(sessionId, level, scope, why, extra) {
|
|
99
|
+
let cfg = null;
|
|
100
|
+
try {
|
|
101
|
+
cfg = getConfig();
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!cfg?.distillDebugLog) return;
|
|
106
|
+
const root = logRoot();
|
|
107
|
+
if (!root) return;
|
|
108
|
+
const tail = extra === undefined ? "" : ` ${safeJson(extra)}`;
|
|
109
|
+
enqueue(fileOf(root, sessionId, level), `${stamp()} [${level}] ${scope} · ${why}${tail}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
/** 等待已入链的写入全部落盘(测试用;生产侧 fire-and-forget,无需等待)。 */
|
|
114
|
+
flush: () => chain,
|
|
115
|
+
/**
|
|
116
|
+
* 绑定会话与模块名,返回该作用域的日志器。
|
|
117
|
+
* @param {string|undefined} sessionId 会话 id(缺省落 __nosession__/)
|
|
118
|
+
* @param {string} scope 模块名(subagent / distill / projection / plugin)
|
|
119
|
+
*/
|
|
120
|
+
for(sessionId, scope) {
|
|
121
|
+
const name = scope || "plugin";
|
|
122
|
+
return {
|
|
123
|
+
/** 失败/跳过留痕(warn 级 → info.log)。 */
|
|
124
|
+
dbgFail: (why, extra) => emit(sessionId, "warn", name, why, extra),
|
|
125
|
+
/** 详单(debug 级 → debug.log)。 */
|
|
126
|
+
dbg: (why, extra) => emit(sessionId, "debug", name, why, extra),
|
|
127
|
+
/** 信息行(info 级 → info.log),如子代理终态台账。 */
|
|
128
|
+
info: (why, extra) => emit(sessionId, "info", name, why, extra),
|
|
129
|
+
/** LLM 原始响应等文本块(debug 级 → debug.log);调用点须已过双门控。 */
|
|
130
|
+
raw(text) {
|
|
131
|
+
let cfg = null;
|
|
132
|
+
try {
|
|
133
|
+
cfg = getConfig();
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!cfg?.distillDebugLog) return;
|
|
138
|
+
const root = logRoot();
|
|
139
|
+
if (!root) return;
|
|
140
|
+
enqueue(
|
|
141
|
+
fileOf(root, sessionId, "debug"),
|
|
142
|
+
`${stamp()} [debug] ${name} · llm raw text begin >>>\n` +
|
|
143
|
+
`--------------------------->\n${text}\n<----------------------------\n` +
|
|
144
|
+
`${stamp()} [debug] ${name} · llm raw text end <<<\n`,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/lib/distill.mjs
CHANGED
|
@@ -13,14 +13,18 @@ import { runWithRetry, RETRY_CONSTANTS } from "./common/retry.mjs";
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {{ ctx: object, getConfig: () => object, paths: object, records: object }} deps
|
|
15
15
|
*/
|
|
16
|
-
export function createDistill({ ctx, getConfig, paths, records }) {
|
|
16
|
+
export function createDistill({ ctx, getConfig, paths, records, logger }) {
|
|
17
17
|
const cfg = () => getConfig();
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
// 日志(v1.8.0-alpha.4):原 stderr 输出改为经统一 logger 落盘
|
|
20
|
+
// (`<profileDir>/.memory-palace/logs/<sessionId>/{info,debug}.log`,见 common/logger.mjs)。
|
|
21
|
+
// 按【会话】取作用域日志器——工厂级闭包会让并发会话的日志落到同一目录;logger 未注入时降级为 no-op。
|
|
22
|
+
// dbgFail=失败/跳过留痕(warn 级 → info.log);dbg=详单(debug 级 → debug.log);
|
|
23
|
+
// 两者统一受 distillDebugLog 门控:关闭时(默认)本插件零输出(既不写终端也不写文件)。
|
|
24
|
+
// 隐私红线:只打计数/字符数/错误 message+stack 首行等元数据,绝不打印对话或 MEMORY.md 文本
|
|
25
|
+
// (唯一例外:distillLogLevel=debug 时的 LLM 原始响应,经 raw() 显式落 debug.log)。
|
|
26
|
+
const noop = () => {};
|
|
27
|
+
const logFor = (session) => (typeof logger?.for === "function" ? logger.for(session?.id, "distill") : null);
|
|
24
28
|
|
|
25
29
|
// 模型解析(v1.2.3 修复):summaryModel 存的是注册表 models[].id 原样(client 下拉 value = m.id),
|
|
26
30
|
// pi-ai getModel(provider, id) 按 model.id 全等匹配(pi-ai models.js: getModels(provider).find(m => m.id === id)),
|
|
@@ -46,6 +50,9 @@ export function createDistill({ ctx, getConfig, paths, records }) {
|
|
|
46
50
|
return null;
|
|
47
51
|
}
|
|
48
52
|
async function resolveModel(summaryModel, session) {
|
|
53
|
+
const log = logFor(session);
|
|
54
|
+
const dbg = log ? log.dbg : noop;
|
|
55
|
+
const dbgFail = log ? log.dbgFail : noop;
|
|
49
56
|
const sm = (summaryModel || "").trim();
|
|
50
57
|
if (sm) {
|
|
51
58
|
const hit = await findModelInRegistry(sm);
|
|
@@ -97,6 +104,9 @@ export function createDistill({ ctx, getConfig, paths, records }) {
|
|
|
97
104
|
// v1.8.0:唯一调用方 = 按钮「蒸馏会话」(全量 fromSeq=0);原 smart 模式自动摘要链路已删除。
|
|
98
105
|
// 返回 { ok, summary, durableCount };失败 { ok:false }(调用方各自决定降级策略)。
|
|
99
106
|
async function distillSessionCore(session, dirs, fromSeq, opts) {
|
|
107
|
+
const log = logFor(session);
|
|
108
|
+
const dbg = log ? log.dbg : noop;
|
|
109
|
+
const dbgFail = log ? log.dbgFail : noop;
|
|
100
110
|
const allowDelete = !!(opts && opts.allowDelete);
|
|
101
111
|
// 回喂存量记忆(v1.4.0 特性3):feedbackEnabled 开启即回喂项目级 + 用户级 MEMORY.md 全文
|
|
102
112
|
// (逐行编号、无截断)。delete 由 allowDelete 决定(手动按钮传 true)。
|
|
@@ -185,17 +195,11 @@ export function createDistill({ ctx, getConfig, paths, records }) {
|
|
|
185
195
|
const { finish, text, chunks, deltaChars, tFirstChunk } = result;
|
|
186
196
|
dbg("session finish", { kind: finish?.kind, attempts: result.attempts });
|
|
187
197
|
dbg("session stream done", { chunks, deltaChars, firstChunkMs: tFirstChunk });
|
|
188
|
-
// 本地调试(受 distillDebugLog + distillLogLevel==="debug"
|
|
189
|
-
//
|
|
190
|
-
if (cfg().
|
|
191
|
-
console.error("[memory-palace][debug] session llm raw text begin >>>");
|
|
192
|
-
console.error("--------------------------->");
|
|
193
|
-
console.error(text);
|
|
194
|
-
console.error("<----------------------------");
|
|
195
|
-
console.error("[memory-palace][debug] session llm raw text end <<<");
|
|
196
|
-
}
|
|
198
|
+
// 本地调试(受 distillDebugLog + distillLogLevel==="debug" 双门控):LLM 原始响应写入 debug.log,
|
|
199
|
+
// 用分隔符整段包裹,便于一眼框住起止、不与其它 dbg 行混淆。
|
|
200
|
+
if (cfg().distillLogLevel === "debug") log?.raw(text);
|
|
197
201
|
if (finish && finish.kind === "max-tokens") {
|
|
198
|
-
|
|
202
|
+
dbgFail("session llm finish max-tokens (attempting partial JSON)", {});
|
|
199
203
|
}
|
|
200
204
|
if (!text) {
|
|
201
205
|
dbgFail("empty llm text");
|
|
@@ -279,6 +283,9 @@ export function createDistill({ ctx, getConfig, paths, records }) {
|
|
|
279
283
|
// 流程:读主目标 MEMORY.md → DISTILL_PROMPT 蒸馏 → 写 memory-cover.md(同目录,保证 rename 原子)
|
|
280
284
|
// → 完整性检查 → 备份 MEMORY.md.{时间戳} → rename 覆盖 → cover 随 rename 消失。
|
|
281
285
|
async function distillProjectMemory(cwd, session) {
|
|
286
|
+
const log = logFor(session);
|
|
287
|
+
const dbg = log ? log.dbg : noop;
|
|
288
|
+
const dbgFail = log ? log.dbgFail : noop;
|
|
282
289
|
const c = cfg();
|
|
283
290
|
if (!c.enabled) return { ok: false, message: "memory-palace 已停用。" };
|
|
284
291
|
const dirs = paths.writeDirs(cwd);
|
|
@@ -338,14 +345,8 @@ export function createDistill({ ctx, getConfig, paths, records }) {
|
|
|
338
345
|
return { ok: false, message: `蒸馏${kindZh}(重试 ${e?.attempts || 0} 次后仍失败):${cls.message || ""}` };
|
|
339
346
|
}
|
|
340
347
|
const { finish, text, chunks, deltaChars, gotFirstChunk, tFirstChunk } = result;
|
|
341
|
-
// 本地调试(受 distillDebugLog + distillLogLevel==="debug"
|
|
342
|
-
if (cfg().
|
|
343
|
-
console.error("[memory-palace][debug] project llm raw text begin >>>");
|
|
344
|
-
console.error("--------------------------->");
|
|
345
|
-
console.error(text);
|
|
346
|
-
console.error("<----------------------------");
|
|
347
|
-
console.error("[memory-palace][debug] project llm raw text end <<<");
|
|
348
|
-
}
|
|
348
|
+
// 本地调试(受 distillDebugLog + distillLogLevel==="debug" 双门控,与会话蒸馏路径一致):原始响应写入 debug.log。
|
|
349
|
+
if (cfg().distillLogLevel === "debug") log?.raw(text);
|
|
349
350
|
if (finish && finish.kind === "max-tokens") {
|
|
350
351
|
dbg("project finish", { kind: "max-tokens", attempts: result.attempts });
|
|
351
352
|
} else {
|
package/lib/hybrid/subagent.mjs
CHANGED
|
@@ -52,19 +52,29 @@ async function resolveModel(cfg, ctx, session) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// 单次流式调用(带 tools)。finish.kind 为 error/aborted 时抛出由上层降级;tool-calls/stop 正常返回。
|
|
55
|
+
// v1.8.0-alpha.4:附带 LLM 流统计(字段与 distill.mjs 的 streamOnce 对齐)供 dbg("stream turn") 落盘。
|
|
56
|
+
// 起因:子代理链路此前看不到任何 LLM 行为——耗时只能靠相邻两条日志的时间戳手动相减,
|
|
57
|
+
// 无法判断慢在网关排队还是模型生成。
|
|
55
58
|
async function streamTurn(ctx, params, timeoutMs) {
|
|
56
59
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
60
|
+
const t0 = Date.now();
|
|
61
|
+
let chunks = 0;
|
|
62
|
+
let deltaChars = 0;
|
|
63
|
+
let tFirstChunk = null;
|
|
57
64
|
const res = ctx.llm.stream({ ...params, signal });
|
|
58
65
|
const asm = new BlockAssembler();
|
|
59
66
|
for await (const ch of res) {
|
|
60
67
|
signal.throwIfAborted();
|
|
68
|
+
chunks++;
|
|
69
|
+
deltaChars += ch.text?.length || 0;
|
|
70
|
+
if (tFirstChunk === null) tFirstChunk = Date.now() - t0;
|
|
61
71
|
asm.push(ch);
|
|
62
72
|
}
|
|
63
73
|
const finish = asm.finish;
|
|
64
74
|
if (finish.kind === "error" || finish.kind === "aborted") {
|
|
65
75
|
throw new Error(finish.failure?.message || finish.kind);
|
|
66
76
|
}
|
|
67
|
-
return { finish, asm };
|
|
77
|
+
return { finish, asm, stats: { chunks, deltaChars, firstChunkMs: tFirstChunk, elapsedMs: Date.now() - t0 } };
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
// 把本轮 surface 事件投影为消息数组(seq >= fromSeq;与 distillSessionCore 同源逻辑)。
|
|
@@ -236,17 +246,21 @@ function applyLogOps(logFiles, dateStr, ops) {
|
|
|
236
246
|
// 记忆子 agent 主入口。每轮必跑(定案:不走闸门);plan 模式拦截由调用方(index.mjs _settle)负责。
|
|
237
247
|
// 返回 { ok, mode }:mode = "noop"(无重点收尾)| "written"(有落盘)| "no-tool-support";
|
|
238
248
|
// ok=false 表示整体失败(不推进断点、下一次 turn/end 自动补蒸;不降级为任何轻量写入)。
|
|
239
|
-
export async function runMemorySubagent({ ctx, getConfig, paths, state, session, dirs, isError, getSeq, setSeq }) {
|
|
249
|
+
export async function runMemorySubagent({ ctx, getConfig, paths, state, session, dirs, isError, getSeq, setSeq, logger }) {
|
|
240
250
|
const cfg = getConfig();
|
|
241
251
|
// 增量断点按会话取用:调用方(index.mjs)注入 getSeq/setSeq(按 sessionId 分桶)。
|
|
242
252
|
// 未注入时回落全局 state.lastSummarizedSeq,保持旧行为与向后兼容。
|
|
243
253
|
const readSeq = typeof getSeq === "function" ? getSeq : () => state.lastSummarizedSeq;
|
|
244
254
|
const writeSeq = typeof setSeq === "function" ? setSeq : (v) => { state.lastSummarizedSeq = v; };
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
// 日志(v1.8.0-alpha.4):原 stderr 输出改为经统一 logger 落盘
|
|
256
|
+
// (`<profileDir>/.memory-palace/logs/<sessionId>/{info,debug}.log`,见 common/logger.mjs)。
|
|
257
|
+
// dbgFail=失败/跳过留痕(warn 级 → info.log);dbg=详单(debug 级 → debug.log)。
|
|
258
|
+
// 两者统一受 distillDebugLog 门控:关闭时(默认)本插件零输出(既不写终端也不写文件)。
|
|
259
|
+
// 隐私红线:只打计数/字符数/错误 message 等元数据,绝不打印对话或日志正文文本。
|
|
260
|
+
// logger 未注入时(如直接调本函数的单测)降级为 no-op,绝不让日志拖垮主流程。
|
|
261
|
+
const logSink = typeof logger?.for === "function" ? logger.for(session?.id, "subagent") : null;
|
|
262
|
+
const dbgFail = logSink ? logSink.dbgFail : () => {};
|
|
263
|
+
const dbg = logSink ? logSink.dbg : () => {};
|
|
250
264
|
if (!cfg.enabled || !session || !dirs.length) {
|
|
251
265
|
dbgFail("disabled or no session/dirs", { enabled: !!cfg.enabled, hasSession: !!session, dirs: dirs.length });
|
|
252
266
|
return { ok: false, mode: "disabled" };
|
|
@@ -346,21 +360,30 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
|
|
|
346
360
|
let unknownHits = 0;
|
|
347
361
|
let retries = 0;
|
|
348
362
|
const unknownNames = new Set();
|
|
363
|
+
// v1.8.0-alpha.4:随返回值带上统计量,供调用方(index.mjs _settle)写终态台账行。
|
|
364
|
+
// 这些量原本算完即弃,却是排障刚需(本轮跑没跑、成没成、写了几条、误调几次)。
|
|
365
|
+
const stats = (round) => ({
|
|
366
|
+
round, appliedWrites, toolAttempts, unknownHits, retries, unknownNames: [...unknownNames],
|
|
367
|
+
});
|
|
349
368
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
350
|
-
const { finish, asm } = await streamTurn(ctx, { provider, model, system, messages, tools }, timeoutMs);
|
|
369
|
+
const { finish, asm, stats: llmStats } = await streamTurn(ctx, { provider, model, system, messages, tools }, timeoutMs);
|
|
370
|
+
// LLM 流统计(debug 级 → debug.log):每轮一次,直接看得到耗时与流进度。
|
|
371
|
+
// ⚠️ 必须解构成 `llmStats` 而非 `stats`:本循环体内已有 `const stats = (round) => …`(结算统计量
|
|
372
|
+
// 函数),同名解构会把它遮蔽,导致后面的 `stats(round)` 抛 "stats is not a function"。
|
|
373
|
+
dbg("stream turn", { round, ...llmStats });
|
|
351
374
|
if (finish.kind === "stop") {
|
|
352
375
|
// v1.6.3 stop 三态判定(修复:误调未知工具后模型直接收尾 → 日志丢失且断点被无脑推进)。
|
|
353
376
|
// ① 有落盘 → 正常完成,推进断点。
|
|
354
377
|
if (appliedWrites > 0) {
|
|
355
378
|
writeSeq(session.seq);
|
|
356
379
|
dbg("done", { round, mode: "written", appliedWrites });
|
|
357
|
-
return { ok: true, mode: "written" };
|
|
380
|
+
return { ok: true, mode: "written", stats: stats(round) };
|
|
358
381
|
}
|
|
359
382
|
// ② 从未调用任何工具 → 模型判定本轮无实质内容(noop),推进断点。
|
|
360
383
|
if (toolAttempts === 0) {
|
|
361
384
|
writeSeq(session.seq);
|
|
362
385
|
dbg("done", { round, mode: "noop" });
|
|
363
|
-
return { ok: true, mode: "noop" };
|
|
386
|
+
return { ok: true, mode: "noop", stats: stats(round) };
|
|
364
387
|
}
|
|
365
388
|
// ③ 命中过未知工具且零写入 → 回告工具清单 + 催促重试(预算 MAX_RETRY 次)。
|
|
366
389
|
if (unknownHits > 0 && retries < MAX_RETRY) {
|
|
@@ -379,13 +402,13 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
|
|
|
379
402
|
}
|
|
380
403
|
// ④ 重试预算耗尽 / 已知工具用错后自主放弃 → 零写入,不推进断点(下一轮 turn/end 补蒸)。
|
|
381
404
|
dbgFail("no write applied", { round, toolAttempts, unknownHits, retries });
|
|
382
|
-
return { ok: false, mode: "no-write" };
|
|
405
|
+
return { ok: false, mode: "no-write", stats: stats(round) };
|
|
383
406
|
}
|
|
384
407
|
if (finish.kind !== "tool-calls") {
|
|
385
408
|
// max-tokens 等异常收尾:返回失败,断点不推进——下一次 turn/end 子代理自动补蒸。
|
|
386
409
|
// v1.6.0 定案:不降级为轻量原文写入(无格式原文会破坏日志章节化结构),宁缺勿滥。
|
|
387
410
|
dbgFail(`finish ${finish.kind}`, { round });
|
|
388
|
-
return { ok: false, mode: `finish-${finish.kind}
|
|
411
|
+
return { ok: false, mode: `finish-${finish.kind}`, stats: stats(round) };
|
|
389
412
|
}
|
|
390
413
|
messages.push(asm.message({ kind: "model", provider, model }));
|
|
391
414
|
const calls = asm.blocks().filter((b) => b.type === "tool-call");
|
|
@@ -411,7 +434,7 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
|
|
|
411
434
|
}
|
|
412
435
|
}
|
|
413
436
|
dbgFail("max-rounds exceeded", { rounds: MAX_ROUNDS });
|
|
414
|
-
return { ok: false, mode: "max-rounds" };
|
|
437
|
+
return { ok: false, mode: "max-rounds", stats: stats(MAX_ROUNDS) };
|
|
415
438
|
} catch (e) {
|
|
416
439
|
if (e?.name === "TimeoutError" || /timeout|abort/i.test(String(e?.message))) {
|
|
417
440
|
dbgFail("timeout", { timeoutMs });
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import Schema from "@deepseek-ai/schemastery";
|
|
|
30
30
|
// settingsNamespace,设置节注册改用 SettingsProvider 实例方法 installSection(经 ctx.inject(["settings"]))。
|
|
31
31
|
import { extractText, extractToolErrorText } from "./common/text.mjs";
|
|
32
32
|
import { createPaths } from "./common/paths.mjs";
|
|
33
|
+
import { createLogger } from "./common/logger.mjs";
|
|
33
34
|
import { planModeOf } from "./common/planmode.mjs";
|
|
34
35
|
import { createRecords } from "./common/records.mjs";
|
|
35
36
|
import { createDistill } from "./distill.mjs";
|
|
@@ -79,9 +80,9 @@ export const Config = Schema.object({
|
|
|
79
80
|
// `silentPresets`(会话预设级静默)决定,不再有独立的写入闸门。
|
|
80
81
|
summaryModel: Schema.string().default("").description("记忆插件当前使用的模型(手动「蒸馏会话」/「蒸馏项目记忆」与记忆子代理共用)。留空=复用当前会话 provider/model;也可填 provider/model(如 deepseek/deepseek-chat)固定廉价模型省 token。").volatile(),
|
|
81
82
|
summaryTimeoutMs: Schema.number().default(60000).description("蒸馏 LLM 调用的超时(毫秒),超时视为失败并降级;覆盖手动「蒸馏会话」/「蒸馏项目记忆」与记忆子代理三条链路;默认 60000(60s)。").volatile(),
|
|
82
|
-
distillDebugLog: Schema.boolean().default(false).description("
|
|
83
|
-
// ---- v1.4.1
|
|
84
|
-
distillLogLevel: Schema.string().default("info").description("
|
|
83
|
+
distillDebugLog: Schema.boolean().default(false).description("调试开关:开启后把本插件**全部诊断日志落盘**(v1.8.0-alpha.4 起不再输出 stderr)。落点 = `<profile 目录>/.memory-palace/logs/<会话 id>/`(如 `$DSH_HOME/profiles/web/.memory-palace/logs/<sid>/`),按级别分文件:`info.log` 收失败/跳过留痕与子代理终态台账,`debug.log` 收详单(`distillLogLevel=debug` 时另含 LLM 原始响应)。关闭(默认)时零输出。单文件上限 1 MB、不自动清理。仅排障用").volatile(),
|
|
84
|
+
// ---- v1.4.1:蒸馏日志级别(平铺键,不进 UI;默认 info;distillDebugLog=true 时生效) ----
|
|
85
|
+
distillLogLevel: Schema.string().default("info").description("日志级别:`info`=只落盘元数据诊断(默认,不含 LLM 原始响应);`debug`=额外把 LLM 原始响应文本落盘到 `debug.log`(分隔符包裹),仅限受信本地排障开启。无需 UI 配置,经 profile 的 cordis.patch.yml 在 memory-palace 条目 config 下设置 distillLogLevel 键(dsh 0.1.7 起 settings.yaml 已被宿主移除、导入 profile patch)。").volatile(),
|
|
85
86
|
// ---- v1.4.0:手动「蒸馏会话」最终输出软预算(v1.4.1 起语义变更:prompt 软约束,思考不受限;实际硬上限由模型自身 maxTokens 决定) ----
|
|
86
87
|
summaryMaxTokens: Schema.number().default(2000).description("手动「蒸馏会话」LLM 的最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 2000;可调大以容纳更多 durable 事实。").volatile(),
|
|
87
88
|
projectMaxTokens: Schema.number().default(8000).description("手动「蒸馏项目记忆」LLM 的最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 8000。").volatile(),
|
|
@@ -248,7 +249,11 @@ export function apply(ctx, config) {
|
|
|
248
249
|
// ---- 装配:路径解析 / 记录读写 / 蒸馏 / 工具 / route ----
|
|
249
250
|
const paths = createPaths(() => source(), () => state.activeCwd);
|
|
250
251
|
const records = createRecords({ getConfig: () => source(), paths });
|
|
251
|
-
|
|
252
|
+
// v1.8.0-alpha.4:统一日志落盘器——原 stderr 输出(含原「无条件」的失败留痕)全部改为落盘,
|
|
253
|
+
// 落点 `<profileDir>/.memory-palace/logs/<sessionId>/{info,debug}.log`,统一受 distillDebugLog 门控。
|
|
254
|
+
// 取径与分级依据见 src/common/logger.mjs 文件头。
|
|
255
|
+
const logger = createLogger({ ctx, getConfig: () => source() });
|
|
256
|
+
const distill = createDistill({ ctx, getConfig: () => source(), paths, records, logger });
|
|
252
257
|
registerTools({ ctx, getConfig: () => source(), paths, state });
|
|
253
258
|
registerApi({ ctx, paths, distill, getSettingsFace });
|
|
254
259
|
// v1.8.0:记忆子代理模块(原 hybrid)。必须【无条件注册】——apply() 同步段执行时 settings 服务
|
|
@@ -261,7 +266,7 @@ export function apply(ctx, config) {
|
|
|
261
266
|
// v1.7.0 特性1:E 投影通道(记忆正文常驻注入)。与 hybrid 同理,无条件注册——
|
|
262
267
|
// 其内部读热配置,无副作用(enabled=false 或命中静默预设时 buildProjections 返回空)。
|
|
263
268
|
// v1.7.2:把 isSessionSilent 透传下去——投影按**会话**判定,故必须知道 agent 所属会话的预设。
|
|
264
|
-
registerProjection({ ctx, getConfig: () => source(), paths, isSilent: isSessionSilent });
|
|
269
|
+
registerProjection({ ctx, getConfig: () => source(), paths, isSilent: isSessionSilent, logger });
|
|
265
270
|
|
|
266
271
|
// ---------- v1.7.2:静默会话(enabled=false 或命中静默预设)的工具 schema 隐身 ----------
|
|
267
272
|
// 背景:`complete: true` 只收缩 sections、**不裁剪 tools**——记忆工具的 schema 仍会经 toolProvider
|
|
@@ -309,7 +314,8 @@ export function apply(ctx, config) {
|
|
|
309
314
|
} catch (error) {
|
|
310
315
|
// 绝不冒泡:restrict 抛错(如某工具因异常未注册命中 unknown-tool)时降级为"不隐身",
|
|
311
316
|
// 由 execute 的 enabled 兜底保证功能仍停用,只是没省掉 schema token。
|
|
312
|
-
|
|
317
|
+
// v1.8.0-alpha.4:原 stderr 输出改为落盘(warn 级 → info.log),受 distillDebugLog 门控。
|
|
318
|
+
logger.for(sessionId, "plugin").dbgFail("tool-hide skipped", { message: error?.message || String(error) });
|
|
313
319
|
}
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -510,10 +516,19 @@ export function apply(ctx, config) {
|
|
|
510
516
|
session,
|
|
511
517
|
dirs,
|
|
512
518
|
isError: effectiveIsError,
|
|
519
|
+
logger,
|
|
513
520
|
// 断点按会话分桶:全局单值在并发会话下会被互相重置,导致子代理静默 noop、日志不落盘。
|
|
514
521
|
getSeq: () => state.seqBySession.get(sid) ?? session?.firstLiveSeq ?? 0,
|
|
515
522
|
setSeq: (v) => state.seqBySession.set(sid, v),
|
|
516
523
|
})
|
|
524
|
+
.then((r) => {
|
|
525
|
+
// v1.8.0-alpha.4 终态台账:每轮结算留一行(info 级 → info.log),把子代理结果与其已算好的
|
|
526
|
+
// 统计量落盘。原先这些量算完即弃,导致「本轮跑没跑、成没成、写了几条」无从查证。
|
|
527
|
+
// 写入本身同样受 distillDebugLog 门控(emit 内部判定),失败静默、绝不影响主流程。
|
|
528
|
+
logger.for(sid, "subagent").info(`done ok=${!!r?.ok} mode=${r?.mode ?? "?"}`, {
|
|
529
|
+
...(r?.stats ?? {}), cwd: cwd ?? "", isError: effectiveIsError,
|
|
530
|
+
});
|
|
531
|
+
})
|
|
517
532
|
.catch(() => {});
|
|
518
533
|
}
|
|
519
534
|
|
package/lib/projection.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export function buildProjections({ getConfig, paths, session, isSilent }) {
|
|
|
152
152
|
* 注册 E 投影钩子。
|
|
153
153
|
* @param {{ ctx: object, getConfig: () => object, paths: object, isSilent?: (session?: object) => boolean }} deps
|
|
154
154
|
*/
|
|
155
|
-
export function registerProjection({ ctx, getConfig, paths, isSilent }) {
|
|
155
|
+
export function registerProjection({ ctx, getConfig, paths, isSilent, logger }) {
|
|
156
156
|
ctx.on("agent/pre-step", async ({ agent, messages, step, signal }, next) => {
|
|
157
157
|
// 先让瀑布流继续,拿到宿主与其他插件合成后的决策——投影要折叠进它的 messages。
|
|
158
158
|
const decision = await next();
|
|
@@ -178,9 +178,10 @@ export function registerProjection({ ctx, getConfig, paths, isSilent }) {
|
|
|
178
178
|
} catch (error) {
|
|
179
179
|
// 🔴 绝不冒泡:pre-step 抛错会让整个 turn 失败(宿主契约,见文件头)。
|
|
180
180
|
// 契约失效时表现为"记忆正文不注入",这行 warn 是唯一排查线索。
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
// v1.8.0-alpha.4:原 stderr 输出改为落盘(warn 级 → info.log),受 distillDebugLog 门控。
|
|
182
|
+
logger?.for?.(agent?.session?.id, "projection")?.dbgFail(`projection skipped (step ${step})`, {
|
|
183
|
+
message: error?.message || String(error),
|
|
184
|
+
});
|
|
184
185
|
return decision;
|
|
185
186
|
}
|
|
186
187
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-palace",
|
|
3
|
-
"version": "1.8.0
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "把 WorkBuddy 的文件式记忆系统移植进 DeepSeek Harness:用户级 + 工作区的人类可读 Markdown 记忆(~/.deepseek-harness/MEMORY.md 与 <cwd>/.deepseek-harness/memory/)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@deepseek-ai/cordis": "*",
|
|
45
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
|
|
46
|
-
"@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
|
|
47
|
-
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1 || ^0.1.7-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1 || ^0.1.7-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1 || ^0.1.7-alpha.1",
|
|
48
48
|
"@deepseek-ai/schemastery": "^3.18.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
51
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "node scripts/build.mjs",
|