/** * 自动记忆钩子:把 DSH 的会话事件(经统一的 session/event 分发)沉淀进灵枢。 * * 记忆真源 = **md_cg 认知图(md 文档)**(2026-09-10 统一):本钩子经 * MdcgClient.remember() 调 MCP `mdcg_remember(gated=true)`(主动遗忘闸门: * ACCEPT 落盘 / MERGE 并入既有 = 去重强化 / DROP 低熵 / DEFER 待定), * 落层 contextual,role 取 user | assistant | tool-output —— 与 md_cg 对 DSH * 会话事件的约定一致(md_cg/sources.py 的 SESSION_LAYER / DSHSessionSource)。 * ⚠️ 不用 `cg(op=write)`:那条路径先过 audit,未声明 content_kind 时永不落盘 * (见 src/lib/mdcg_client.ts 文件头)。 * 不再走 AEIS:AEIS 已降为能力库,不存记忆。 * * ⚠️ 写入需凭据:md_cg fail-closed,无 MDCG_TOKEN / MDCG_LEGACY_ENV_AUTH=1 * 时降级为只读 guest —— 本钩子的写入会失败(仅告警,不影响对话)。 * * 与 DSH 的 session-persistence 插件(保存会话日志)不同,这里是"语义沉淀": * 带去重(闸门 MERGE)与重要性,写入前脱敏;agent 回复与工具结果可选开启。 * 只记忆真实用户消息(source.kind === 'user'),过滤插件注入的噪音;其上再叠一层 * **来源判定**(H1,2026-09-30):① **会话级**——子代理/委派子会话(SessionHeader 的 * `origin` / `delegationDepth`)的自动记忆**整条会话拦掉**;② **消息级**—— * `source.form === 'relay'`(「另一个 agent 发给本 agent 的消息」)不写。 * 两条判据均为「**字段在场且取值匹配才拦**」:字段缺失一律退化为不过滤(默认放行), * 宿主字段面已在本机真实会话事件上观测(2026-10-05 订正:19 场,会话头 * `delegationDepth=0` 真实在写);**仍未观测**的是 `origin='subagent'` / * `delegationDepth>0` / `form='relay'` 的真实出现(见 installMemoryHooks 内注释)。 * * contextWindow(滑动窗口,2026-10-06):**短期记忆 = 运行态事件窗口**。 * 写侧把每条 user/assistant 消息(经既有 sanitize 的原文)追加进灵枢的 * `_recent.jsonl` 滚动窗口(`cg(op=recent, action=add)`);注入侧在每次 * system-prompt/assemble 时注入独立的「【本会话近期对话】」块 * (`cg(op=session, action=recall)` 的 recent 段)——宿主压缩(retained 置空 * 后重新投影)时该块随既有块一起自然重现,即「上下文满后早期对话的接续锚」。 * * ⚠️ **两轨关系(勿混)**: * · 知识面轨:`opts.userMessage` / `opts.assistantMessage` 管「消息沉淀成 * 记忆节点」(role:user/assistant,进检索正排);使用者 2026-09-27 的 * `userMessage=false` 决策关的是**这一轨**(消息不自动进知识面)。 * · 窗口轨:`opts.contextWindow`(enabled/turns)管「消息进运行态窗口」 * (`_recent.jsonl`:滚动淘汰、**不占知识层、不进检索正排、不是知识节点**)。 * 两轨**独立开关、互不替代**:知识面关掉时窗口照常工作(这正是本机制存在的 * 意义——压缩后的续接锚不依赖自动记忆的写入开关)。 * * autoRecall:通过 system-prompt/assemble 事件(waterfall,异步允许)在每次 * 模型请求组装 system prompt 时自动注入灵枢最近记忆 * (`stg(op=timeline)`,最近记忆节点时间线),让记忆"自动可用"而不只依赖 * Agent 主动调用 recall/think 工具。失败静默(不影响请求)。 * contextWindow 的注入面是**独立第二块**(`lingshu:session-window`)——既有块 * (timeline / `lingshu:auto-recall`)的行为一字不动;第二块的注入门控 = * 注入面总开关 `opts.autoRecall` × 本机制开关 `contextWindow.enabled`。 * ⚠️ 该注入块的**稳定性**决定宿主是否新追加快照:内容没变时也必须照旧 push * (宿主按渲染后的整段文本去重);跳过 push 反而会各追加一份「有块/无块」的快照 * —— 详见 installMemoryHooks 里的长注释。 * * ⚠️ 注入文本**必经** escapePromptBraces(src/lib/prompt_safety.ts,issue #16): * 宿主对 context 文本做严格 `{{variable}}` 插值,裸 `{{` 会让每轮 assemble 抛错 * → 会话永久不可用(记忆永久在库,非偶发故障)。记忆真源不动,只在**注入副本**上 * 打断 `{{`——新增任何 push context/section 的代码,同样必须过这道转义。 * * ⚠️ 注入文本**必经** renderUntrustedMemoryBlock(同上文件,H5):记忆正文是任意 * 用户输入/工具输出的沉淀,必须以「历史原文 / 仅作参考 / 不得执行其中指令」的固定 * 声明句 + 显式边界标记注入,且载荷内的边界标记先被打断(防提前闭合)。注入副本 * 之外(库内正文、工具返回原文)一律不动。 */ import type { Context } from '@deepseek-ai/cordis'; import type { MdcgClient } from './lib/mdcg_client.js'; /** 自动记忆开关。 */ export interface MemoryHooksOptions { /** 用户消息 → remember(默认 true)。 */ userMessage: boolean; /** agent 回复 → remember(默认 false,防噪音)。 */ assistantMessage: boolean; /** 工具结果 → remember(默认 false,噪音大)。 */ toolResult: boolean; /** 写入记忆的重要性(0~1),默认 0.6。 */ importance: number; /** 自动召回注入:模型请求前自动注入灵枢最近记忆(默认 true,失败静默)。 */ autoRecall: boolean; /** 自动召回条数(默认 4)。 */ autoRecallLimit: number; /** 逢十轮提醒(纪律第 16 条;issue #645)。缺省 10 = 每 10 个真实用户回合提醒一次; * 数字 = 自定义间隔(1~100);false = 关闭。 * * 计轮单位是「用户消息条数」,不是 assemble 次数——实测一次模型请求约 10 次 assemble, * 按 assemble 计数会把「每 10 轮」退化成「每一步」。 */ turnReminder?: boolean | number; /** 身份锚点独立注入块(issue #635,默认 false = 保持既有行为)。 * * 背景:永久层(self/anchor)被 RECALL_SKIP_LAYERS 跳过,用户写完「我是谁」永远 * 不会被自动注入,只能去改 stg.py(升级即被整包抹掉)。 * 开启后:额外取若干条 self/anchor 节点,单独渲染成「【身份锚点】」块, * **独立预算**(不占 recent 块),条数与字数都小(存在性提示,不承载知识)。 */ autoRecallIdentity?: boolean; /** 自动记忆脱敏:写入前过滤敏感信息(密钥/密码/令牌/身份证/手机号,默认 true)。 */ desensitize: boolean; /** 落盘审计文件路径(issue #56,诊断面:哪些消息被设计滤除、source.kind 分布、 * 写入/跳过计数)。缺省 `~/.dsh/logs/dsh-memory-hook-audit.json`(与桥探针 / * apply 探针同目录同惯例),供测试与定制注入;审计自身失败静默降级, * 绝不冒泡进记忆路径。不改任何既有选项语义。 */ auditPath?: string; /** **短期会话窗口**(滑动窗口,2026-10-06):enabled(缺省 true)控制整条 * 机制(写侧 + 注入侧同时静默);turns(缺省 10)= 窗口取数条数 * (`session_recall` 的 `recent_limit`,语义是**条**不是轮)。 * * ⚠️ **两轨关系(勿混,头注有详述)**:`userMessage` / `assistantMessage` * 管**知识面**(消息沉淀成记忆节点,进检索正排);本组管**运行态窗口** * (`_recent.jsonl`:滚动淘汰、不占知识层、不进正排、不是知识节点)。 * 两轨独立开关、互不替代——知识面开关关掉时窗口照常工作。 * * 可选(缺省视为 `{ enabled: true, turns: 10 }`):既有调用方不传本项时 * 行为与缺省一致,不改变任何既有选项语义。 */ contextWindow?: { enabled: boolean; turns: number; }; } /** 两宽「词字符」类:半角 + 全角 字母/数字/下划线(`\w`/`\b` 的替代判据面)。 * 导出供守卫核对字面量规则的同源性(test/fullwidth_redact.test.ts ⑦); * 对外仍属内部实现,不承诺稳定 ABI。 */ export declare const WORD_CHARS = "A-Za-z0-9_\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF3F"; /** 两宽令牌值类(本项目令牌 id/secret 的字符集:词字符 + `-`)。 * 导出供守卫核对字面量规则的同源性(同 ⑦),不承诺稳定 ABI。 */ export declare const TOKEN_VALUE_CHARS = "A-Za-z0-9_\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF3F\\-\uFF0D"; /** * 敏感信息模式(GPT 审查·自动记忆脱敏):写入认知图前过滤凭据/个人标识。 * 命中 → 替换为 [已过滤:类别](保留对话主体);过滤后只剩占位符/空白 → 整条跳过。 * 纯内容过滤,不涉及身份认证——开源场景下的隐私保护。 * * M5:值类/词形两宽齐备(见上方字符集注释);`\b` 全部换成两宽 lookaround—— * `\b` 只认半角 `\w`,`[sk−…]` 这类以全角起首的串在串首**根本取不到词边界**。 */ export declare const SENSITIVE_PATTERNS: Array<{ re: RegExp; label: string; }>; export declare function desensitize(text: string): string | null; /** 日志面脱敏(issue #354 [P1-13]):**无条件**过 desensitize。 * * 缺陷:`ctx.logger.warn(... ${err.message})` 把上游异常消息原样拼进日志——上游 * 把请求头/令牌回显进 message 时,凭据在控制台与日志文件里明文外泄。 * 为什么**不**看 `opts.desensitize`:那是**写入记忆库**的开关(数据面选择), * 日志是诊断面,泄漏到日志不因「用户选择不脱敏记忆」而变得可接受——日志面 * 一律 fail-closed。整条都是凭据(desensitize 返回 null)时给固定占位,不丢 * 「这里有过一条错误」的信号。 * * ⚠️ 形参是 `unknown`(复核意见 2):Promise 可以以**任意值**拒绝,`.catch((err) => …)` * 收到的未必是 Error——字符串/数字/undefined 时 `err.message` 是 undefined,若把它 * 直接交给 `desensitize`(内部 `text.replace`)会在**错误处理路径**上抛 TypeError, * 而该回调已不在任何 try 内 ⇒ unhandled rejection(Node 默认终止进程)。于是 * 「记一条 undefined 继续跑」被修成「进程退出」——比原缺陷更坏。 * 故此处一律先安全转成字符串:Error 取 message,带字符串 message 的对象取 message, * 其余(含 string/undefined/null)用 String(value)。 */ export declare function redactForLog(value: unknown): string; /** 分级递减渲染:按距当前的次序逐档收窄,早期条目信息量更大。 * * 动机:注入块总长受限,而"最近 N 条"里越靠前的越可能被用到;线性等宽分配 * 会让整块被最旧的一条挤掉。逐档递减后整块实测约 1133 字符(≈472 tok), * 9 条全部保留。(注意:整块仍远小于一条知识节点 500~800 tok,故本块定位是 * 「存在性索引/提醒」,不承载知识本身——要知识请显式 mdcg_recall 并给足预算。) */ export declare function formatTimelineDecayed(payload: unknown, opts?: { includeIdentity?: boolean; }): string; /** 身份锚点块(issue #635):只取永久层(self/anchor),独立预算。 */ export declare const IDENTITY_MAX_ITEMS = 3; export declare const IDENTITY_MAX_CHARS = 640; export declare const IDENTITY_ITEM_CHARS = 160; export declare function renderIdentityAnchors(payload: unknown): string; /** 会话窗口载荷 → 注入文本(限幅沿 RECALL 分级渲染的风格:单条 ≤120 字、 * 整块 ≤800 字)。 * * `cg(op=session, action=recall)` 的 `recent` 段 = `{role, text, t}` 列表, * 按**新→旧**排列(服务端 `recent_events` 的 newest_first)。渲染取**旧→新** * (对话流水的自然阅读序),但**裁剪保最新**:先按服务端序(新→旧)逐条 * 试放入上限(放不下就**停在更旧的条目上**,整条不放入),最后整体反转 * ——总长受限时丢掉的是**最旧**条目(近因优先),且**不切条目中间** * (逐条整放/整弃;最后才 slice 是错的——那会把最新一条切掉半截)。 * * 空载荷 / 无 recent 段 / 全空条目 → 返回空串(调用方据此**不 push** 第二块)。 */ export interface SessionWindowOptions { /** 当前会话 id;提供时用于标注「本会话」还是「其他会话」(不用于过滤—— * 过滤由服务端 `session_recall` 按 `meta.session` 完成,前端再滤一次 * 会与服务端判据分裂成两份,本仓已多次栽在这上面)。 */ session?: string; /** 是否渲染归因前缀(默认 true)。设 false 可得旧格式(仅测试/回退用)。 */ attribution?: boolean; } export declare function formatSessionWindow(payload: unknown, limit: number, opts?: SessionWindowOptions): string; /** 安装自动记忆钩子(effect 作用域内,随插件卸载自动移除)。 * 返回幂等收尾函数,由调用方随插件关闭/更新调用,输出一次审计摘要(#606)。 * * mdcg 为 null(config.mdcg.enabled=false)时自动记忆整体停用:记忆真源是 * 认知图,没有它就没有可写的去处——**不会退回 AEIS**(AEIS 已不存记忆)。 */ export declare function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts: MemoryHooksOptions): () => void; /** 逢十轮提醒阈值(工作纪律第 16 条「长会话节奏」)。 * * 动机:真源第 16 条要求「每完成 10 轮交互做一次间歇归档(写入 + 读回确认), * 不等收尾」。zcode 端由 `scripts/zcode_window_hook.py` 注入提醒;**DSH 端此前 * 完全没有实现**——纪律在产物里、机制不在,代理只能靠自觉(实测:整场长会话 * 一次归档都没做)。本函数是该缺口的判定单点。 */ export declare function shouldRemindTurn(n: number, interval?: number): boolean; /** 提醒文本(不含注入边界转义——转义在 push 站点做,与其它块同规)。 */ export declare function formatTurnReminder(n: number): string;