dsh-layered-memory 0.8.9 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +10 -0
- package/README.md +6 -0
- package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
- package/dist/client.js +87 -6
- package/dist/contract.d.ts +19 -0
- package/dist/hooks/recall.js +4 -4
- package/dist/index.js +1 -1
- package/dist/stats.js +49 -5
- package/dist/store/session-modes.d.ts +7 -0
- package/dist/store/session-modes.js +28 -2
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +23 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -159,6 +159,16 @@ trajectory view):
|
|
|
159
159
|
- Each session's choice is persisted by sessionId to `session-modes.json`, surviving
|
|
160
160
|
restarts/session restore; stacks with the global switches (global is the master gate);
|
|
161
161
|
L2/L3 are fully family-isolated — content never leaks across families.
|
|
162
|
+
- **Write-only sessions (#38)**: a three-state "injection" switch inside the popover
|
|
163
|
+
(follow global / on / off) — set to "off" for a **write-only session**: capture and
|
|
164
|
+
distillation continue as usual (conversation still settles into L0→L1→L2/L3), but
|
|
165
|
+
nothing is injected into this session (recall injection, the persona/navigation
|
|
166
|
+
stable section and the tools guide all stop; `memory_search` and the other read
|
|
167
|
+
tools return a write-only notice). The pill face changes to `Memory · Write-only`;
|
|
168
|
+
the override persists per session, and switching back to "follow global" clears it
|
|
169
|
+
to the settings-page recall toggle. Ideal for debug/eval/sensitive sessions that
|
|
170
|
+
should absorb without interference. Orthogonal to the off mode: off remains full
|
|
171
|
+
stealth (capture off too), while write-only keeps the "in" and gates the "out".
|
|
162
172
|
|
|
163
173
|
## UI Preview
|
|
164
174
|
|
package/README.md
CHANGED
|
@@ -124,6 +124,12 @@ L1/L2/L3 层级过滤)、层级 × 时间窗口表格(调用数 / 输出与
|
|
|
124
124
|
打开期间自适应轮询(忙 2s / 静 5s),关闭即停;
|
|
125
125
|
- 每会话的选择按 sessionId 持久化到 `session-modes.json`,重启/恢复会话不丢;
|
|
126
126
|
与全局开关叠加(全局是总闸);L2/L3 完全分类,分类内容不渗透。
|
|
127
|
+
- **只写不读(#38)**:悬浮板内「注入」三态开关(跟随全局 / 开 / 关)——设为「关」
|
|
128
|
+
即**只写会话**:捕获与蒸馏照常(对话照常沉淀为 L0→L1→L2/L3),但不向本会话注入
|
|
129
|
+
任何记忆(召回注入、画像/导航稳定区、工具指南一并停止;`memory_search` 等读工具
|
|
130
|
+
返回只写提示)。pill 面文换作 `记忆·只写` 提示状态;覆盖按会话持久化,切回
|
|
131
|
+
「跟随全局」即清除、跟随设置页召回开关;适合调试/评测/敏感会话「只吸收不干扰」。
|
|
132
|
+
与 off 档正交:off 仍是完全隐身(连捕获都关),只写保留「进」关「出」。
|
|
127
133
|
|
|
128
134
|
## 界面预览
|
|
129
135
|
|
|
Binary file
|
package/dist/client.js
CHANGED
|
@@ -3066,7 +3066,13 @@ var __defProp = Object.defineProperty;
|
|
|
3066
3066
|
if (rc.enabled === false) {
|
|
3067
3067
|
rcVal = "停用";
|
|
3068
3068
|
rcLabel = "召回命中";
|
|
3069
|
-
|
|
3069
|
+
const reasonText = {
|
|
3070
|
+
deploy: "部署未启用",
|
|
3071
|
+
global: "全局开关关闭",
|
|
3072
|
+
session: "会话只写",
|
|
3073
|
+
mode: "档位关闭"
|
|
3074
|
+
};
|
|
3075
|
+
rcTitle = rc.reason ? "召回已停用(" + (reasonText[rc.reason] ?? rc.reason) + ")" : "召回已停用(开关关闭 / 档位关闭 / 部署未启用)";
|
|
3070
3076
|
} else {
|
|
3071
3077
|
rcVal = (rc.hitTurns || 0) + "/" + (rc.injectedTurns || 0);
|
|
3072
3078
|
rcLabel = "召回命中 · " + (rc.totalHits || 0) + " 条";
|
|
@@ -3383,6 +3389,36 @@ var __defProp = Object.defineProperty;
|
|
|
3383
3389
|
}
|
|
3384
3390
|
),
|
|
3385
3391
|
props.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 10, whiteSpace: "nowrap" }, children: props.error }) : null,
|
|
3392
|
+
props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3393
|
+
"div",
|
|
3394
|
+
{
|
|
3395
|
+
style: {
|
|
3396
|
+
borderTop: "1px solid var(--dsh-mem-border)",
|
|
3397
|
+
marginTop: 10,
|
|
3398
|
+
paddingTop: 8,
|
|
3399
|
+
display: "flex",
|
|
3400
|
+
justifyContent: "space-between",
|
|
3401
|
+
alignItems: "center",
|
|
3402
|
+
gap: 8
|
|
3403
|
+
},
|
|
3404
|
+
children: [
|
|
3405
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "注入" }),
|
|
3406
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3407
|
+
Segmented,
|
|
3408
|
+
{
|
|
3409
|
+
value: props.recall === null ? "follow" : props.recall ? "on" : "off",
|
|
3410
|
+
disabled: props.mode === "off",
|
|
3411
|
+
options: [
|
|
3412
|
+
{ key: "follow", label: "跟随全局", title: "清除本会话覆盖,跟随全局召回开关" },
|
|
3413
|
+
{ key: "on", label: "开", title: "本会话强制注入记忆" },
|
|
3414
|
+
{ key: "off", label: "关", title: "只写:记忆照常沉淀,但不注入本会话" }
|
|
3415
|
+
],
|
|
3416
|
+
onChange: (key) => props.onCommitRecall(key === "on" ? true : key === "off" ? false : null)
|
|
3417
|
+
}
|
|
3418
|
+
)
|
|
3419
|
+
]
|
|
3420
|
+
}
|
|
3421
|
+
) : null,
|
|
3386
3422
|
props.rpc && props.sessionId ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId }) : null
|
|
3387
3423
|
]
|
|
3388
3424
|
}
|
|
@@ -3397,6 +3433,8 @@ var __defProp = Object.defineProperty;
|
|
|
3397
3433
|
const rpc = props.rpc;
|
|
3398
3434
|
const sessionId = props.sessionId || props.session && props.session.sessionId;
|
|
3399
3435
|
const [mode, setMode] = (0, import_react17.useState)(null);
|
|
3436
|
+
const [recall, setRecall] = (0, import_react17.useState)(null);
|
|
3437
|
+
const [recallResolved, setRecallResolved] = (0, import_react17.useState)(true);
|
|
3400
3438
|
const [error, setError] = (0, import_react17.useState)(null);
|
|
3401
3439
|
const [open, setOpen] = (0, import_react17.useState)(false);
|
|
3402
3440
|
const wrapRef = (0, import_react17.useRef)(null);
|
|
@@ -3407,8 +3445,11 @@ var __defProp = Object.defineProperty;
|
|
|
3407
3445
|
setError(null);
|
|
3408
3446
|
rpc("dsh-memory/session-mode-get", { sessionId }).then((r) => {
|
|
3409
3447
|
if (token !== seqRef.current) return;
|
|
3410
|
-
if (r && r.ok && r.value)
|
|
3411
|
-
|
|
3448
|
+
if (r && r.ok && r.value) {
|
|
3449
|
+
setMode(r.value.mode);
|
|
3450
|
+
setRecall(r.value.recall);
|
|
3451
|
+
setRecallResolved(r.value.recallResolved);
|
|
3452
|
+
} else setError(r && !r.ok ? r.error.message : "RPC error");
|
|
3412
3453
|
}).catch((e) => {
|
|
3413
3454
|
if (token !== seqRef.current) return;
|
|
3414
3455
|
setError(String(e && e.message || e));
|
|
@@ -3444,25 +3485,54 @@ var __defProp = Object.defineProperty;
|
|
|
3444
3485
|
};
|
|
3445
3486
|
}, [open]);
|
|
3446
3487
|
const commit = (next) => {
|
|
3447
|
-
if (!rpc || next === mode) return;
|
|
3488
|
+
if (!rpc || !sessionId || mode === null || next === mode) return;
|
|
3448
3489
|
const prev = mode;
|
|
3490
|
+
const token = seqRef.current;
|
|
3449
3491
|
setMode(next);
|
|
3450
3492
|
setError(null);
|
|
3451
3493
|
rpc("dsh-memory/session-mode-set", { sessionId, mode: next }).then((r) => {
|
|
3494
|
+
if (token !== seqRef.current) return;
|
|
3452
3495
|
if (!r || !r.ok) {
|
|
3453
3496
|
setMode(prev);
|
|
3454
3497
|
setError(r && r.error ? "档位写入失败:" + r.error.message : "档位写入失败");
|
|
3455
3498
|
}
|
|
3456
3499
|
}).catch((e) => {
|
|
3500
|
+
if (token !== seqRef.current) return;
|
|
3457
3501
|
setMode(prev);
|
|
3458
3502
|
setError("档位写入失败:" + String(e && e.message || e));
|
|
3459
3503
|
});
|
|
3460
3504
|
};
|
|
3505
|
+
const commitRecall = (next) => {
|
|
3506
|
+
if (!rpc || !sessionId || mode === null || next === recall) return;
|
|
3507
|
+
const prevRecall = recall;
|
|
3508
|
+
const prevResolved = recallResolved;
|
|
3509
|
+
const token = seqRef.current;
|
|
3510
|
+
setRecall(next);
|
|
3511
|
+
if (next !== null) setRecallResolved(next);
|
|
3512
|
+
setError(null);
|
|
3513
|
+
rpc("dsh-memory/session-mode-set", { sessionId, mode, recall: next }).then((r) => {
|
|
3514
|
+
if (token !== seqRef.current) return;
|
|
3515
|
+
if (!r || !r.ok) {
|
|
3516
|
+
setRecall(prevRecall);
|
|
3517
|
+
setRecallResolved(prevResolved);
|
|
3518
|
+
setError(r && r.error ? "注入设置失败:" + r.error.message : "注入设置失败");
|
|
3519
|
+
} else {
|
|
3520
|
+
setRecall(r.value.recall);
|
|
3521
|
+
setRecallResolved(r.value.recallResolved);
|
|
3522
|
+
}
|
|
3523
|
+
}).catch((e) => {
|
|
3524
|
+
if (token !== seqRef.current) return;
|
|
3525
|
+
setRecall(prevRecall);
|
|
3526
|
+
setRecallResolved(prevResolved);
|
|
3527
|
+
setError("注入设置失败:" + String(e && e.message || e));
|
|
3528
|
+
});
|
|
3529
|
+
};
|
|
3461
3530
|
if (!sessionId || !rpc) return null;
|
|
3462
3531
|
const info = modeInfo(mode);
|
|
3463
3532
|
const loaded = mode !== null;
|
|
3464
3533
|
const isOff = loaded && mode === "off";
|
|
3465
3534
|
const isFlow = loaded && !isOff;
|
|
3535
|
+
const faceLabel = !loaded ? error ? "⚠" : "…" : isOff ? info.label : !recallResolved ? "只写" : info.label;
|
|
3466
3536
|
ensureThemeStyle();
|
|
3467
3537
|
const pillStyle = {
|
|
3468
3538
|
display: "inline-flex",
|
|
@@ -3497,11 +3567,22 @@ var __defProp = Object.defineProperty;
|
|
|
3497
3567
|
style: pillStyle,
|
|
3498
3568
|
children: [
|
|
3499
3569
|
"记忆 · ",
|
|
3500
|
-
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children:
|
|
3570
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: faceLabel })
|
|
3501
3571
|
]
|
|
3502
3572
|
}
|
|
3503
3573
|
),
|
|
3504
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3574
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3575
|
+
ModeSlider,
|
|
3576
|
+
{
|
|
3577
|
+
mode: mode || "auto",
|
|
3578
|
+
onCommit: commit,
|
|
3579
|
+
recall: loaded ? recall : void 0,
|
|
3580
|
+
onCommitRecall: commitRecall,
|
|
3581
|
+
error,
|
|
3582
|
+
rpc,
|
|
3583
|
+
sessionId
|
|
3584
|
+
}
|
|
3585
|
+
) : null
|
|
3505
3586
|
] });
|
|
3506
3587
|
}
|
|
3507
3588
|
|
package/dist/contract.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface MemoryLiveSettings {
|
|
|
75
75
|
* 跟随(静态层链 → 全局解析逐级兜底)。写入经 settings-set 逐层校验(头行必须显式)。 */
|
|
76
76
|
distillLayerChains: Record<LayerRouteKey, DistillChainEntry[]>;
|
|
77
77
|
}
|
|
78
|
+
/** 召回停用原因(session-stats recall.enabled=false 时带出;短路序第一个为假的因子)。 */
|
|
79
|
+
export type RecallDisabledReason = 'deploy' | 'global' | 'session' | 'mode';
|
|
78
80
|
/** 单会话召回统计(悬浮卡信息区数据源;口径见 recall.ts 注释)。 */
|
|
79
81
|
export interface RecallSessionStats {
|
|
80
82
|
/** 发生过召回检索的轮次数(含零命中、全量压制与超时)。 */
|
|
@@ -332,14 +334,27 @@ export interface SessionModeGetResponse {
|
|
|
332
334
|
sessionId: string;
|
|
333
335
|
mode: MemoryMode;
|
|
334
336
|
defaultMode: MemoryMode;
|
|
337
|
+
/** 会话级注入覆盖(#38 只写不读):null = 未覆盖(跟随全局)——线上 JSON 用 null
|
|
338
|
+
* 不用 undefined(序列化丢包)。 */
|
|
339
|
+
recall: boolean | null;
|
|
340
|
+
/** host 解析后的注入生效值(会话覆盖 ?? 全局开关):pill 面文直接消费,
|
|
341
|
+
* client 无需另知全局开关。 */
|
|
342
|
+
recallResolved: boolean;
|
|
335
343
|
}
|
|
336
344
|
export interface SessionModeSetRequest {
|
|
337
345
|
sessionId: string;
|
|
338
346
|
mode: MemoryMode;
|
|
347
|
+
/** 会话级注入覆盖:布尔 = 设置覆盖;显式 null = 清除(跟随全局);缺省 = 不动
|
|
348
|
+
* (旧 client 纯切档兼容,覆盖不丢)。mode 与 recall 可独立设置。 */
|
|
349
|
+
recall?: boolean | null;
|
|
339
350
|
}
|
|
340
351
|
export interface SessionModeSetResponse {
|
|
341
352
|
sessionId: string;
|
|
342
353
|
mode: MemoryMode;
|
|
354
|
+
/** 设置后的覆盖态(null = 跟随全局)。 */
|
|
355
|
+
recall: boolean | null;
|
|
356
|
+
/** 设置后的注入生效值(client 面文直接消费;清除覆盖后由 host 告知解析结果)。 */
|
|
357
|
+
recallResolved: boolean;
|
|
343
358
|
}
|
|
344
359
|
/** dsh-memory/session-stats(悬浮卡信息区;热路径端点)。 */
|
|
345
360
|
export interface SessionStatsRequest {
|
|
@@ -360,8 +375,12 @@ export type SessionStatsResponse = {
|
|
|
360
375
|
sessionId: string;
|
|
361
376
|
mode: MemoryMode;
|
|
362
377
|
defaultMode: MemoryMode;
|
|
378
|
+
/** 注入统计 + 生效位;enabled=false 时 reason 带短路序第一个为假因子
|
|
379
|
+
* (deploy 部署上限 / global 全局开关 / session 会话只写 / mode 档位关闭),
|
|
380
|
+
* 旧宿主缺省该字段时 client 回退本地枚举文案。 */
|
|
363
381
|
recall: {
|
|
364
382
|
enabled: boolean;
|
|
383
|
+
reason?: RecallDisabledReason;
|
|
365
384
|
} & RecallSessionStats;
|
|
366
385
|
/** 记忆上下文占用账本(未注入过的会话为 null)。 */
|
|
367
386
|
memoryOccupancy: MemoryOccupancy | null;
|
package/dist/hooks/recall.js
CHANGED
|
@@ -132,7 +132,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
132
132
|
try {
|
|
133
133
|
const s = live.get();
|
|
134
134
|
const mode = modes.get(payload.agent.id);
|
|
135
|
-
|
|
135
|
+
// 三级读闸:主闸 → off 档(完全隐身)→ 注入开关(#38:会话覆盖 ?? 全局)
|
|
136
|
+
if (!s.enabled || mode === 'off' || !modes.resolvedRecall(payload.agent.id, s.recall))
|
|
136
137
|
return decision;
|
|
137
138
|
// 只在有新的用户来源消息的步骤注入(轮首 claim 或 steering 插话);纯工具步透传
|
|
138
139
|
const hasNewUserMessage = decision.messages.some((m) => m.source?.kind === 'user');
|
|
@@ -222,10 +223,9 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
222
223
|
* 供旧会话回填估算(RecallHooks.estimateProfileTokens)复用同一口径。
|
|
223
224
|
*/ const composeStableText = (agentId) => {
|
|
224
225
|
const s = live.get();
|
|
225
|
-
if (!s.enabled || !s.recall)
|
|
226
|
-
return '';
|
|
227
226
|
const mode = modes.get(agentId);
|
|
228
|
-
|
|
227
|
+
// 与 pre-step 同款三级读闸(#38):主闸 → off 档 → 注入开关;空串即物理离场
|
|
228
|
+
if (!s.enabled || mode === 'off' || !modes.resolvedRecall(agentId, s.recall))
|
|
229
229
|
return '';
|
|
230
230
|
// auto 档:两族按类别归组(画像/导航各一个标签,域内 <domain> 分块);纯档:单族原格式
|
|
231
231
|
const body = mode === 'auto'
|
package/dist/index.js
CHANGED
|
@@ -299,7 +299,7 @@ export async function apply(ctx, config) {
|
|
|
299
299
|
}
|
|
300
300
|
const recall = registerRecall(ctx, config, stores, logger, live, modes, dataDir);
|
|
301
301
|
runner.setAfterRun(recall.invalidateProfile);
|
|
302
|
-
registerMemoryTools(ctx, config, stores, logger, modes);
|
|
302
|
+
registerMemoryTools(ctx, config, stores, logger, modes, live);
|
|
303
303
|
registerMemoryRpc(ctx, config, stores, logger, {
|
|
304
304
|
degraded: () => !storageOk || db.isDegraded(),
|
|
305
305
|
pending: () => runner.pendingCount,
|
package/dist/stats.js
CHANGED
|
@@ -165,7 +165,16 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
165
165
|
throw new Error('档位存储未初始化');
|
|
166
166
|
const p = (payload ?? {});
|
|
167
167
|
const sessionId = expectSessionId(p.sessionId);
|
|
168
|
-
|
|
168
|
+
// 注入解析权威在 host:recall 是原始覆盖(null=跟随全局),recallResolved 是生效值
|
|
169
|
+
const s = live?.get();
|
|
170
|
+
const globalRecall = s?.recall ?? true;
|
|
171
|
+
const v = {
|
|
172
|
+
sessionId,
|
|
173
|
+
mode: modes.get(sessionId),
|
|
174
|
+
defaultMode: modes.default,
|
|
175
|
+
recall: modes.getRecall(sessionId) ?? null,
|
|
176
|
+
recallResolved: modes.resolvedRecall(sessionId, globalRecall),
|
|
177
|
+
};
|
|
169
178
|
return v;
|
|
170
179
|
}
|
|
171
180
|
case 'dsh-memory/session-mode-set': {
|
|
@@ -177,9 +186,27 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
177
186
|
if (typeof p.mode !== 'string' || !allowed.includes(p.mode)) {
|
|
178
187
|
throw new Error(`非法档位: ${String(p.mode)}(允许 ${allowed.join('/')})`);
|
|
179
188
|
}
|
|
189
|
+
// 注入覆盖可选同车(#38):布尔 = 设置覆盖;显式 null = 清除覆盖(跟随全局);
|
|
190
|
+
// 缺省(undefined)= 仅切档、覆盖保持不动(旧 client 永不传 recall,行为不变)。
|
|
191
|
+
// 校验前置:非法 recall 在任何写穿发生前拒绝(不做部分提交)
|
|
192
|
+
if (p.recall !== undefined && typeof p.recall !== 'boolean' && p.recall !== null) {
|
|
193
|
+
throw new Error(`非法注入覆盖: ${String(p.recall)}(允许 true/false/null)`);
|
|
194
|
+
}
|
|
180
195
|
modes.set(sessionId, p.mode);
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
if (typeof p.recall === 'boolean') {
|
|
197
|
+
modes.setRecall(sessionId, p.recall);
|
|
198
|
+
}
|
|
199
|
+
else if (p.recall === null) {
|
|
200
|
+
modes.setRecall(sessionId, undefined);
|
|
201
|
+
}
|
|
202
|
+
deps.logger.info(`[memory] 会话档位设置 session=${sessionId} mode=${p.mode} recall=${JSON.stringify(modes.getRecall(sessionId) ?? null)}`);
|
|
203
|
+
const s = live?.get();
|
|
204
|
+
const v = {
|
|
205
|
+
sessionId,
|
|
206
|
+
mode: p.mode,
|
|
207
|
+
recall: modes.getRecall(sessionId) ?? null,
|
|
208
|
+
recallResolved: modes.resolvedRecall(sessionId, s?.recall ?? true),
|
|
209
|
+
};
|
|
183
210
|
return v;
|
|
184
211
|
}
|
|
185
212
|
// ── 会话级统计(悬浮卡信息区;热路径端点,见 SessionInfoSource 的零 I/O 硬规则) ──
|
|
@@ -192,7 +219,20 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
192
219
|
const caps = sessionInfo.capabilities();
|
|
193
220
|
const l0Count = await sessionInfo.l0Count(sessionId);
|
|
194
221
|
const s = live?.get();
|
|
195
|
-
|
|
222
|
+
// 注入生效四因子短路序(#38):部署上限 → 全局开关 → 会话覆盖 → 档位;
|
|
223
|
+
// disabled 时 reason 带第一个为假因子(悬浮卡停用文案的数据源)
|
|
224
|
+
const globalRecall = s?.recall ?? true;
|
|
225
|
+
const sessionRecall = modes ? modes.resolvedRecall(sessionId, globalRecall) : true;
|
|
226
|
+
let recallReason;
|
|
227
|
+
if (!cfg.recall.enabled)
|
|
228
|
+
recallReason = 'deploy';
|
|
229
|
+
else if (!globalRecall)
|
|
230
|
+
recallReason = 'global';
|
|
231
|
+
else if (!sessionRecall)
|
|
232
|
+
recallReason = 'session';
|
|
233
|
+
else if (mode === 'off')
|
|
234
|
+
recallReason = 'mode';
|
|
235
|
+
const recallOn = recallReason === undefined;
|
|
196
236
|
const view = sessionInfo.runnerView(sessionId, mode);
|
|
197
237
|
// lastDistillAt 统一转 ISO(与 global.lastExtractAt 口径一致,client 直接 fmtAgo)
|
|
198
238
|
const distillView = { ...view, lastDistillAt: view.lastDistillAt ? new Date(view.lastDistillAt).toISOString() : null };
|
|
@@ -220,7 +260,11 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
220
260
|
sessionId,
|
|
221
261
|
mode,
|
|
222
262
|
defaultMode: modes?.default ?? cfg.family,
|
|
223
|
-
recall: {
|
|
263
|
+
recall: {
|
|
264
|
+
enabled: recallOn,
|
|
265
|
+
...(recallReason ? { reason: recallReason } : {}),
|
|
266
|
+
...(sessionInfo.recallStats(sessionId) ?? emptyRecallStats()),
|
|
267
|
+
},
|
|
224
268
|
memoryOccupancy: sessionInfo.memoryOccupancy(sessionId),
|
|
225
269
|
occupancyBackfill: sessionInfo.profileEstimate
|
|
226
270
|
? {
|
|
@@ -17,6 +17,13 @@ export declare class SessionModeStore {
|
|
|
17
17
|
get default(): MemoryMode;
|
|
18
18
|
/** 同步读取:未设置过的会话返回默认档。 */
|
|
19
19
|
get(sessionId: string): MemoryMode;
|
|
20
|
+
/** 会话级注入覆盖原始值(#38):undefined = 未覆盖,跟随全局。 */
|
|
21
|
+
getRecall(sessionId: string): boolean | undefined;
|
|
22
|
+
/** 解析后的注入开关:会话覆盖 ?? 全局运行时开关(部署级 cfg.recall.enabled
|
|
23
|
+
* 与主闸 s.enabled 不经此处,仍按既有硬门生效——覆盖打不穿部署上限)。 */
|
|
24
|
+
resolvedRecall(sessionId: string, globalRecall: boolean): boolean;
|
|
25
|
+
/** 设置会话级注入覆盖(#38;undefined = 清除覆盖跟随全局。写穿持久化)。 */
|
|
26
|
+
setRecall(sessionId: string, recall: boolean | undefined): void;
|
|
20
27
|
/** 注册档位切换回调(同步调用;回调异常只记日志不阻断写穿)。 */
|
|
21
28
|
setModeChangeHandler(cb: (sessionId: string, oldMode: MemoryMode, newMode: MemoryMode) => void): void;
|
|
22
29
|
/** 设置会话档位(写穿持久化;持久化失败保持内存态生效)。 */
|
|
@@ -42,7 +42,12 @@ export class SessionModeStore {
|
|
|
42
42
|
continue;
|
|
43
43
|
if (now - (entry.updatedAt ?? 0) > PRUNE_MS)
|
|
44
44
|
continue;
|
|
45
|
-
this.entries.set(sid, {
|
|
45
|
+
this.entries.set(sid, {
|
|
46
|
+
mode: entry.mode,
|
|
47
|
+
// 非布尔视为损坏丢弃(= 跟随全局);旧文件无此键同款兼容
|
|
48
|
+
recall: typeof entry.recall === 'boolean' ? entry.recall : undefined,
|
|
49
|
+
updatedAt: entry.updatedAt ?? now,
|
|
50
|
+
});
|
|
46
51
|
count++;
|
|
47
52
|
}
|
|
48
53
|
if (count > 0)
|
|
@@ -55,6 +60,25 @@ export class SessionModeStore {
|
|
|
55
60
|
get(sessionId) {
|
|
56
61
|
return this.entries.get(sessionId)?.mode ?? this.loaded;
|
|
57
62
|
}
|
|
63
|
+
/** 会话级注入覆盖原始值(#38):undefined = 未覆盖,跟随全局。 */
|
|
64
|
+
getRecall(sessionId) {
|
|
65
|
+
return this.entries.get(sessionId)?.recall;
|
|
66
|
+
}
|
|
67
|
+
/** 解析后的注入开关:会话覆盖 ?? 全局运行时开关(部署级 cfg.recall.enabled
|
|
68
|
+
* 与主闸 s.enabled 不经此处,仍按既有硬门生效——覆盖打不穿部署上限)。 */
|
|
69
|
+
resolvedRecall(sessionId, globalRecall) {
|
|
70
|
+
return this.entries.get(sessionId)?.recall ?? globalRecall;
|
|
71
|
+
}
|
|
72
|
+
/** 设置会话级注入覆盖(#38;undefined = 清除覆盖跟随全局。写穿持久化)。 */
|
|
73
|
+
setRecall(sessionId, recall) {
|
|
74
|
+
const entry = this.entries.get(sessionId);
|
|
75
|
+
this.entries.set(sessionId, {
|
|
76
|
+
mode: entry?.mode ?? this.loaded,
|
|
77
|
+
recall,
|
|
78
|
+
updatedAt: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
this.writeChain = this.writeChain.then(() => this.persist());
|
|
81
|
+
}
|
|
58
82
|
/** 注册档位切换回调(同步调用;回调异常只记日志不阻断写穿)。 */
|
|
59
83
|
setModeChangeHandler(cb) {
|
|
60
84
|
this.onModeChange = cb;
|
|
@@ -62,7 +86,9 @@ export class SessionModeStore {
|
|
|
62
86
|
/** 设置会话档位(写穿持久化;持久化失败保持内存态生效)。 */
|
|
63
87
|
set(sessionId, mode) {
|
|
64
88
|
const old = this.get(sessionId);
|
|
65
|
-
|
|
89
|
+
// 已有注入覆盖跨切档保留(#38:档位与注入正交,切档不动覆盖)
|
|
90
|
+
const recall = this.entries.get(sessionId)?.recall;
|
|
91
|
+
this.entries.set(sessionId, { mode, recall, updatedAt: Date.now() });
|
|
66
92
|
this.writeChain = this.writeChain.then(() => this.persist());
|
|
67
93
|
if (old !== mode && this.onModeChange) {
|
|
68
94
|
try {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* 会话档位联动:execute 的 exec.agent 即发起调用的 agent(agent.id === sessionId),
|
|
5
5
|
* memory_search 按会话档位过滤族(auto 不过滤,纯档只查本族);off 档下三工具统一
|
|
6
6
|
* 返回提示(本会话已对记忆系统隐身)。conversation_search 检索范围保持全库。
|
|
7
|
+
* 只写会话(#38:注入覆盖=关)同款拒读,notice 区分文案——写入走捕获钩子不经工具,
|
|
8
|
+
* 拒读不影响"只写"语义。
|
|
7
9
|
*/
|
|
8
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
11
|
import type { MemoryConfig } from '../config.js';
|
|
12
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
10
13
|
import type { L0Store } from '../store/l0.js';
|
|
11
14
|
import type { L1Store } from '../store/l1.js';
|
|
12
15
|
import type { PersonaStore } from '../store/persona.js';
|
|
@@ -18,4 +21,4 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
|
|
|
18
21
|
l1: L1Store;
|
|
19
22
|
scenes: Record<MemoryFamily, SceneStore>;
|
|
20
23
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
21
|
-
}, logger: MemoryLogger, modes: SessionModeStore): void;
|
|
24
|
+
}, logger: MemoryLogger, modes: SessionModeStore, live: LiveSettingsHandle): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
2
|
const OFF_NOTICE = '本会话的记忆档位为"关闭":该会话对记忆系统完全隐身,不读取也不写入记忆。';
|
|
3
|
-
|
|
3
|
+
const WRITE_ONLY_NOTICE = '本会话为只写模式:记忆照常沉淀,但不读取。';
|
|
4
|
+
const GLOBAL_OFF_NOTICE = '记忆注入已全局停用:本会话不读取记忆(沉淀照常)。';
|
|
5
|
+
export function registerMemoryTools(ctx, cfg, stores, logger, modes, live) {
|
|
4
6
|
if (!cfg.tools)
|
|
5
7
|
return;
|
|
6
8
|
/**
|
|
7
|
-
* 调用会话的检索族(auto → undefined 不过滤;off → null 表示整体禁用)。
|
|
9
|
+
* 调用会话的检索族(auto → undefined 不过滤;off/只写 → null 表示整体禁用)。
|
|
8
10
|
* fail-open:exec.agent 缺失(宿主调用路径未带 agent 标识)按全族检索放行——
|
|
9
11
|
* 档位隔离依赖宿主正确传递 exec.agent.id,缺失只告警一次不拒绝工具调用。
|
|
10
12
|
*/
|
|
@@ -20,8 +22,24 @@ export function registerMemoryTools(ctx, cfg, stores, logger, modes) {
|
|
|
20
22
|
const mode = modes.get(agentId);
|
|
21
23
|
if (mode === 'off')
|
|
22
24
|
return null;
|
|
25
|
+
// 只写会话拒读(#38,T2 裁决):与注入同属读维度,不拒则"不注入"从工具路径漏风
|
|
26
|
+
if (!modes.resolvedRecall(agentId, live.get().recall))
|
|
27
|
+
return null;
|
|
23
28
|
return mode === 'auto' ? undefined : mode;
|
|
24
29
|
};
|
|
30
|
+
/** 拒读时的归因文案(familyOfCaller 判 null 后重查内存 Map,成本可忽略):
|
|
31
|
+
* off 完全隐身 / 会话只写覆盖 / 全局召回关——三种停用各说各话,不谎报只写。 */
|
|
32
|
+
const blockNoticeOf = (agentId) => {
|
|
33
|
+
if (agentId !== undefined) {
|
|
34
|
+
if (modes.get(agentId) === 'off')
|
|
35
|
+
return OFF_NOTICE;
|
|
36
|
+
if (modes.getRecall(agentId) === false)
|
|
37
|
+
return WRITE_ONLY_NOTICE;
|
|
38
|
+
if (!modes.resolvedRecall(agentId, live.get().recall))
|
|
39
|
+
return GLOBAL_OFF_NOTICE;
|
|
40
|
+
}
|
|
41
|
+
return OFF_NOTICE;
|
|
42
|
+
};
|
|
25
43
|
// ── memory_search: L1 结构化记忆 ──
|
|
26
44
|
ctx.tools.register(defineTool({
|
|
27
45
|
name: 'memory_search',
|
|
@@ -59,7 +77,7 @@ export function registerMemoryTools(ctx, cfg, stores, logger, modes) {
|
|
|
59
77
|
execute: async (args, exec) => {
|
|
60
78
|
const family = familyOfCaller(exec.agent?.id);
|
|
61
79
|
if (family === null)
|
|
62
|
-
return { items: [], notice:
|
|
80
|
+
return { items: [], notice: blockNoticeOf(exec.agent?.id) };
|
|
63
81
|
const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
|
|
64
82
|
const hits = await stores.l1.search(args.query, limit, { type: args.type || undefined, family: family ?? undefined });
|
|
65
83
|
return {
|
|
@@ -107,7 +125,7 @@ export function registerMemoryTools(ctx, cfg, stores, logger, modes) {
|
|
|
107
125
|
},
|
|
108
126
|
execute: async (args, exec) => {
|
|
109
127
|
if (familyOfCaller(exec.agent?.id) === null)
|
|
110
|
-
return { items: [], notice:
|
|
128
|
+
return { items: [], notice: blockNoticeOf(exec.agent?.id) };
|
|
111
129
|
const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
|
|
112
130
|
const records = await stores.l0.search(args.query, limit);
|
|
113
131
|
return {
|
|
@@ -141,7 +159,7 @@ export function registerMemoryTools(ctx, cfg, stores, logger, modes) {
|
|
|
141
159
|
},
|
|
142
160
|
execute: async (args, exec) => {
|
|
143
161
|
if (familyOfCaller(exec.agent?.id) === null)
|
|
144
|
-
return { content:
|
|
162
|
+
return { content: blockNoticeOf(exec.agent?.id) };
|
|
145
163
|
const p = args.path.trim();
|
|
146
164
|
let content;
|
|
147
165
|
if (p === 'persona.md' || p === 'persona-chat.md' || p === 'persona' || p === 'persona-chat') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|