@furongjun1999/dsh-memory 0.7.3 → 0.7.5
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 +631 -629
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
- package/docs/eval//345/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -0
- package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +45 -43
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +80 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +76 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +205 -18
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- package/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/sleep.py +223 -103
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/tokens.py +8 -1
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +217 -17
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +5 -5
package/src/hooks.ts
CHANGED
|
@@ -24,10 +24,29 @@
|
|
|
24
24
|
* `delegationDepth=0` 真实在写);**仍未观测**的是 `origin='subagent'` /
|
|
25
25
|
* `delegationDepth>0` / `form='relay'` 的真实出现(见 installMemoryHooks 内注释)。
|
|
26
26
|
*
|
|
27
|
+
* contextWindow(滑动窗口,2026-10-06):**短期记忆 = 运行态事件窗口**。
|
|
28
|
+
* 写侧把每条 user/assistant 消息(经既有 sanitize 的原文)追加进灵枢的
|
|
29
|
+
* `_recent.jsonl` 滚动窗口(`cg(op=recent, action=add)`);注入侧在每次
|
|
30
|
+
* system-prompt/assemble 时注入独立的「【本会话近期对话】」块
|
|
31
|
+
* (`cg(op=session, action=recall)` 的 recent 段)——宿主压缩(retained 置空
|
|
32
|
+
* 后重新投影)时该块随既有块一起自然重现,即「上下文满后早期对话的接续锚」。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ **两轨关系(勿混)**:
|
|
35
|
+
* · 知识面轨:`opts.userMessage` / `opts.assistantMessage` 管「消息沉淀成
|
|
36
|
+
* 记忆节点」(role:user/assistant,进检索正排);使用者 2026-09-27 的
|
|
37
|
+
* `userMessage=false` 决策关的是**这一轨**(消息不自动进知识面)。
|
|
38
|
+
* · 窗口轨:`opts.contextWindow`(enabled/turns)管「消息进运行态窗口」
|
|
39
|
+
* (`_recent.jsonl`:滚动淘汰、**不占知识层、不进检索正排、不是知识节点**)。
|
|
40
|
+
* 两轨**独立开关、互不替代**:知识面关掉时窗口照常工作(这正是本机制存在的
|
|
41
|
+
* 意义——压缩后的续接锚不依赖自动记忆的写入开关)。
|
|
42
|
+
*
|
|
27
43
|
* autoRecall:通过 system-prompt/assemble 事件(waterfall,异步允许)在每次
|
|
28
44
|
* 模型请求组装 system prompt 时自动注入灵枢最近记忆
|
|
29
45
|
* (`stg(op=timeline)`,最近记忆节点时间线),让记忆"自动可用"而不只依赖
|
|
30
46
|
* Agent 主动调用 recall/think 工具。失败静默(不影响请求)。
|
|
47
|
+
* contextWindow 的注入面是**独立第二块**(`lingshu:session-window`)——既有块
|
|
48
|
+
* (timeline / `lingshu:auto-recall`)的行为一字不动;第二块的注入门控 =
|
|
49
|
+
* 注入面总开关 `opts.autoRecall` × 本机制开关 `contextWindow.enabled`。
|
|
31
50
|
* ⚠️ 该注入块的**稳定性**决定宿主是否新追加快照:内容没变时也必须照旧 push
|
|
32
51
|
* (宿主按渲染后的整段文本去重);跳过 push 反而会各追加一份「有块/无块」的快照
|
|
33
52
|
* —— 详见 installMemoryHooks 里的长注释。
|
|
@@ -51,6 +70,9 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
|
51
70
|
import type { MdcgClient } from './lib/mdcg_client.js'
|
|
52
71
|
import { escapePromptBraces, renderUntrustedMemoryBlock } from './lib/prompt_safety.js'
|
|
53
72
|
import { HookAuditRecorder, kindOf, type HookWriteRole } from './lib/hook_audit.js'
|
|
73
|
+
// 运行期会话状态单点(B 治本批):观测在 hooks 面,写归因注入在工具面
|
|
74
|
+
// (src/tools.ts)——两处共用同一状态,见 lib/session_state.ts 头注。
|
|
75
|
+
import { currentSession, noteSession, UNASSIGNED_SESSION } from './lib/session_state.js'
|
|
54
76
|
|
|
55
77
|
/** 自动记忆开关。 */
|
|
56
78
|
export interface MemoryHooksOptions {
|
|
@@ -73,6 +95,18 @@ export interface MemoryHooksOptions {
|
|
|
73
95
|
* apply 探针同目录同惯例),供测试与定制注入;审计自身失败静默降级,
|
|
74
96
|
* 绝不冒泡进记忆路径。不改任何既有选项语义。 */
|
|
75
97
|
auditPath?: string
|
|
98
|
+
/** **短期会话窗口**(滑动窗口,2026-10-06):enabled(缺省 true)控制整条
|
|
99
|
+
* 机制(写侧 + 注入侧同时静默);turns(缺省 10)= 窗口取数条数
|
|
100
|
+
* (`session_recall` 的 `recent_limit`,语义是**条**不是轮)。
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ **两轨关系(勿混,头注有详述)**:`userMessage` / `assistantMessage`
|
|
103
|
+
* 管**知识面**(消息沉淀成记忆节点,进检索正排);本组管**运行态窗口**
|
|
104
|
+
* (`_recent.jsonl`:滚动淘汰、不占知识层、不进正排、不是知识节点)。
|
|
105
|
+
* 两轨独立开关、互不替代——知识面开关关掉时窗口照常工作。
|
|
106
|
+
*
|
|
107
|
+
* 可选(缺省视为 `{ enabled: true, turns: 10 }`):既有调用方不传本项时
|
|
108
|
+
* 行为与缺省一致,不改变任何既有选项语义。 */
|
|
109
|
+
contextWindow?: { enabled: boolean; turns: number }
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
/** 从 ContentBlock[] 提取纯文本。 */
|
|
@@ -320,6 +354,57 @@ function formatTimelineDecayed(payload: unknown): string {
|
|
|
320
354
|
return out.join('\n').slice(0, RECALL_MAX_CHARS)
|
|
321
355
|
}
|
|
322
356
|
|
|
357
|
+
// ---------------------------------------------------------------- 会话窗口渲染
|
|
358
|
+
// 「短期会话窗口」(contextWindow)的取数/渲染常量。与上方 RECALL_* 同款纪律:
|
|
359
|
+
// 常量写死在此处(而非 config schema——未知键会被 schema 剥离)。
|
|
360
|
+
/** 窗口块取数的整包 token 预算(服务端 `session_recall` 的 budget_tokens)。
|
|
361
|
+
*
|
|
362
|
+
* ⚠️ 这是**整包**预算,不是 recent 段的独立预算:notes/goals/tasks/self_state
|
|
363
|
+
* 与 recent 共享(服务端裁剪循环交替丢 recent / notes 尾部,md_cg/mdcos.py:
|
|
364
|
+
* 3351-3362)。取 600 是**有意保守**——本块定位是「存在性锚点 / 接续提示」,
|
|
365
|
+
* 宁可少注入几条,也不挤占宿主上下文。实测(空库 + 10 条窗口条目):
|
|
366
|
+
* budget_tokens=600 → recent 段 8 条;1200 → 10 条。 */
|
|
367
|
+
const WINDOW_BUDGET_TOKENS = 600
|
|
368
|
+
/** 单条窗口条目预览上限(字符)。 */
|
|
369
|
+
const WINDOW_ITEM_CHARS = 120
|
|
370
|
+
/** 窗口块总长上限(字符)。 */
|
|
371
|
+
const WINDOW_MAX_CHARS = 800
|
|
372
|
+
|
|
373
|
+
/** 会话窗口载荷 → 注入文本(限幅沿 RECALL 分级渲染的风格:单条 ≤120 字、
|
|
374
|
+
* 整块 ≤800 字)。
|
|
375
|
+
*
|
|
376
|
+
* `cg(op=session, action=recall)` 的 `recent` 段 = `{role, text, t}` 列表,
|
|
377
|
+
* 按**新→旧**排列(服务端 `recent_events` 的 newest_first)。渲染取**旧→新**
|
|
378
|
+
* (对话流水的自然阅读序),但**裁剪保最新**:先按服务端序(新→旧)逐条
|
|
379
|
+
* 试放入上限(放不下就**停在更旧的条目上**,整条不放入),最后整体反转
|
|
380
|
+
* ——总长受限时丢掉的是**最旧**条目(近因优先),且**不切条目中间**
|
|
381
|
+
* (逐条整放/整弃;最后才 slice 是错的——那会把最新一条切掉半截)。
|
|
382
|
+
*
|
|
383
|
+
* 空载荷 / 无 recent 段 / 全空条目 → 返回空串(调用方据此**不 push** 第二块)。 */
|
|
384
|
+
function formatSessionWindow(payload: unknown, limit: number): string {
|
|
385
|
+
const items = (payload && typeof payload === 'object'
|
|
386
|
+
&& Array.isArray((payload as { recent?: unknown }).recent))
|
|
387
|
+
? (payload as { recent: Array<Record<string, unknown>> }).recent
|
|
388
|
+
: []
|
|
389
|
+
const rows: string[] = []
|
|
390
|
+
const max = Math.max(1, Math.floor(limit) || 10)
|
|
391
|
+
let used = 0
|
|
392
|
+
for (const it of items.slice(0, max)) {
|
|
393
|
+
const role = String(it['role'] ?? '').trim() || 'user'
|
|
394
|
+
const preview = String(it['text'] ?? '').replace(/\s+/g, ' ').trim()
|
|
395
|
+
if (!preview) continue
|
|
396
|
+
const body = preview.length > WINDOW_ITEM_CHARS
|
|
397
|
+
? preview.slice(0, WINDOW_ITEM_CHARS) + '…'
|
|
398
|
+
: preview
|
|
399
|
+
const row = `[${role}] ${body}`
|
|
400
|
+
const next = used === 0 ? row.length : used + 1 + row.length
|
|
401
|
+
if (used > 0 && next > WINDOW_MAX_CHARS) break
|
|
402
|
+
rows.push(row)
|
|
403
|
+
used = next
|
|
404
|
+
}
|
|
405
|
+
return rows.reverse().join('\n')
|
|
406
|
+
}
|
|
407
|
+
|
|
323
408
|
/** 取宿主会话标识(只用于**归因/隔离**,不参与任何权限判断)。
|
|
324
409
|
*
|
|
325
410
|
* 动机:记忆写入必须带会话身份才能区分不同会话;读取默认只看本会话(防串台),
|
|
@@ -334,16 +419,8 @@ function sessionIdOf(raw: unknown): string {
|
|
|
334
419
|
return typeof v === 'string' ? v.trim() : ''
|
|
335
420
|
}
|
|
336
421
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
340
|
-
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
341
|
-
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
342
|
-
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
343
|
-
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
344
|
-
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
345
|
-
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
346
|
-
const UNASSIGNED_SESSION = 'unassigned'
|
|
422
|
+
// UNASSIGNED_SESSION(会话归属未知时的显式占位常量)单点已移至
|
|
423
|
+
// lib/session_state.ts(B 治本批:本文件与工具面共用同一常量与同一会话状态)。
|
|
347
424
|
|
|
348
425
|
/** H1 **会话级**判据:这条 session 是否「子代理/委派子会话」。
|
|
349
426
|
*
|
|
@@ -415,8 +492,80 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
415
492
|
* 绝不冒泡进记忆路径、不改任何写入/过滤判定。 */
|
|
416
493
|
const audit = new HookAuditRecorder(opts.auditPath)
|
|
417
494
|
|
|
418
|
-
|
|
419
|
-
|
|
495
|
+
// ── 短期会话窗口(contextWindow):与知识面写入**独立成轨**(见文件头)──
|
|
496
|
+
// 缺省开启({ enabled: true, turns: 10 });enabled=false → 写侧与注入侧
|
|
497
|
+
// **同时静默**。turns = 窗口取数条数(session_recall 的 recent_limit 语义是
|
|
498
|
+
// **条**不是轮);clamp 到 1~50(与既有 recallLimit 同款纪律,防配置失手)。
|
|
499
|
+
const cw = opts.contextWindow ?? { enabled: true, turns: 10 }
|
|
500
|
+
const cwEnabled = cw.enabled !== false
|
|
501
|
+
const cwTurns = Math.max(1, Math.min(50, Math.floor(cw.turns || 10)))
|
|
502
|
+
|
|
503
|
+
/** 窗口条目判定(contextWindow 写侧):这条事件是否值得进「近期对话」窗口。
|
|
504
|
+
* 返回 null = 不写。
|
|
505
|
+
*
|
|
506
|
+
* 过滤面与知识面**同源**(复用同一组谓词函数,故两处口径不会各自漂移):
|
|
507
|
+
* · user/message:`form==='relay'`(H1 消息级)与 `kind!=='user'`(插件注入 /
|
|
508
|
+
* 系统上下文)不写——与自动记忆同一判定;有文本才写;
|
|
509
|
+
* · assistant/message:有文本即写;
|
|
510
|
+
* · 其它事件类型(tool/result 等):一律不写(窗口是**对话**记录)。
|
|
511
|
+
*
|
|
512
|
+
* ⚠️ 两处**有意不同门**(这是设计,不是遗漏):本判定**不看**
|
|
513
|
+
* `opts.userMessage` / `opts.assistantMessage`——那两个开关管知识面(消息沉淀
|
|
514
|
+
* 成记忆节点),本机制由 `contextWindow.enabled` 管(运行态窗口)。若把窗口写
|
|
515
|
+
* 也挂到那两个开关上,使用者既有的 `userMessage=false` 就会连带关掉窗口,
|
|
516
|
+
* 「知识面关、窗口开」的独立轨道即不成立(两轨关系见文件头)。
|
|
517
|
+
*
|
|
518
|
+
* ⚠️ 子代理会话(H1 会话级)由调用点**更早**拦回(在取 sid 之前),不在此重判
|
|
519
|
+
* ——与自动记忆同口径:委派指令不进真人窗口。 */
|
|
520
|
+
const windowEntry = (event: SessionEvent): { role: 'user' | 'assistant'; text: string } | null => {
|
|
521
|
+
if (event.type === 'user/message') {
|
|
522
|
+
if (isRelayedMessage(event.data.source)) return null
|
|
523
|
+
if (event.data.source?.kind !== 'user') return null
|
|
524
|
+
const text = extractText(event.data.content)
|
|
525
|
+
return text ? { role: 'user', text } : null
|
|
526
|
+
}
|
|
527
|
+
if (event.type === 'assistant/message') {
|
|
528
|
+
const text = extractText(event.data.message.content)
|
|
529
|
+
return text ? { role: 'assistant', text } : null
|
|
530
|
+
}
|
|
531
|
+
return null
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** 窗口写入(fire-and-forget):失败只记 warn,**绝不炸会话流**(沿 memorize
|
|
535
|
+
* 的 catch 风格)。桥未就绪静默跳过(窗口是运行态面,不阻塞对话;「未就绪」
|
|
536
|
+
* 的告警已由 memorize 路径负责,不在此重复刷屏)。
|
|
537
|
+
*
|
|
538
|
+
* ⚠️ 同步抛出也必须被吞(catch 两段):真实部署下 graph 是 MdcgClient 全量
|
|
539
|
+
* 实现;但桥替换实现 / 降级替身缺该方法时,抛错同样不得越过会话流边界。 */
|
|
540
|
+
const noteRecent = (role: 'user' | 'assistant', text: string,
|
|
541
|
+
meta: Record<string, unknown>): void => {
|
|
542
|
+
if (!graph.isReady()) return
|
|
543
|
+
try {
|
|
544
|
+
void graph.recentAdd(role, text, meta, ['dsh', 'recent-window'])
|
|
545
|
+
.catch((err: Error) => ctx.logger.warn(`dsh-memory: 短期窗口写入失败: ${err.message}`))
|
|
546
|
+
} catch (err) {
|
|
547
|
+
ctx.logger.warn(`dsh-memory: 短期窗口写入失败: ${(err as Error).message}`)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** 本实例是否曾观测到会话(B 治本批)。
|
|
552
|
+
*
|
|
553
|
+
* 会话状态本体是**进程级单点**(lib/session_state.ts;hooks 面观测、工具面
|
|
554
|
+
* src/tools.ts 的写归因注入共用);而「本实例有没有观测过」是**实例级**事实:
|
|
555
|
+
* 多实例并存时(测试;或宿主重装钩子),不得让**别的实例**的观测替本实例决定
|
|
556
|
+
* 召回过滤——否则新装的钩子在尚未观测到会话时就按别的实例的会话去读
|
|
557
|
+
* (读错会话,正是 P45 会话隔离要防的形态)。真机单实例下两者等价:首个
|
|
558
|
+
* session/event 之前模块状态为空,之后回落值恒为同一单点值。
|
|
559
|
+
*
|
|
560
|
+
* ⚠️ 这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**:契约给的形态是
|
|
561
|
+
* 下方回落处直接 `|| currentSession()`(无本门);但字面形态与硬边界
|
|
562
|
+
* 「test/session-attribution.test.ts 逐字未动且全绿」互斥——该守卫 ② 以
|
|
563
|
+
* 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B。裁定
|
|
564
|
+
* 接受本门,两条理由:① 保留原闭包变量「新实例 = 干净状态」的**有意**语义
|
|
565
|
+
* (新装钩子未观测时不加召回过滤);② 冻结守卫零误伤。真机单实例与字面
|
|
566
|
+
* **逐位等价**(差异窗口「本实例未观测 ∧ 模块单点非空」单实例下不可达;
|
|
567
|
+
* HMR 重载时新实例回落 '' 属更保守行为)。 */
|
|
568
|
+
let observedSession = false
|
|
420
569
|
|
|
421
570
|
/** 记忆沉淀(fire-and-forget)。认知图未就绪则跳过并告警(不退回 AEIS)。
|
|
422
571
|
* `role`=null 表示**读预热**(user-recall)——审计只统计写入路径,
|
|
@@ -471,7 +620,13 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
471
620
|
// 想读**所有**会话做了什么:别走自动召回(它会串台),显式调
|
|
472
621
|
// `stg(op=timeline, session="*")`,返回项带 session 归属。
|
|
473
622
|
const hostCtx = (_ctx as unknown) as { agent?: { session?: unknown } } | undefined
|
|
474
|
-
|
|
623
|
+
// 回落 = **本实例观测门 × 模块单点值**(裁定项 dwfq-7b3a555e-1,见上方
|
|
624
|
+
// observedSession 注释):本实例尚未观测 → 回落 ''(保持「新实例 =
|
|
625
|
+
// 干净状态」,与 test/session-attribution.test.ts ② 的「未观测 → 不加
|
|
626
|
+
// 过滤」相容);已观测 → 取 lib/session_state.ts 的进程级单点值。
|
|
627
|
+
// 真机单实例下两者逐位等价(差异窗口不可达)。
|
|
628
|
+
const sid = sessionIdOf(hostCtx?.agent?.session)
|
|
629
|
+
|| (observedSession ? currentSession() : '')
|
|
475
630
|
const text = formatTimelineDecayed(
|
|
476
631
|
await graph.timeline(recallLimit, sid ? { session: sid } : {}))
|
|
477
632
|
if (text) {
|
|
@@ -490,6 +645,29 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
490
645
|
`【灵枢最近记忆】\n${text.slice(0, RECALL_MAX_CHARS)}`)),
|
|
491
646
|
})
|
|
492
647
|
}
|
|
648
|
+
// ── 独立第二块:「【本会话近期对话】」(contextWindow 注入面)──
|
|
649
|
+
// 与上方 timeline 块**块名/取数/开关各自独立**(既有块一字未动)。
|
|
650
|
+
// 稳定性口径与既有块同等:**每步都 push**(内容随新轮增长属预期——
|
|
651
|
+
// 宿主对内容变化追加快照的既有行为不变;窗口不变时两块逐字节相同)。
|
|
652
|
+
// fail-soft:取数/渲染失败 → 静默、不 push 第二块,绝不抛——且因
|
|
653
|
+
// 上方既有块已先 push,本块的失败**不影响**既有块(反之亦然)。
|
|
654
|
+
// 门控 = 注入面总开关 autoRecall(本 handler 的注册条件)× 本机制开关
|
|
655
|
+
// contextWindow.enabled。
|
|
656
|
+
if (cwEnabled) {
|
|
657
|
+
try {
|
|
658
|
+
const win = await graph.sessionRecall(sid, cwTurns, WINDOW_BUDGET_TOKENS)
|
|
659
|
+
const winText = formatSessionWindow(win, cwTurns)
|
|
660
|
+
if (winText) {
|
|
661
|
+
// 注入边界同规(文件头硬约束):不可信内容边界 + `{{` 转义,
|
|
662
|
+
// 都只改注入副本——窗口原文在库内保真。
|
|
663
|
+
assembly.contexts.push({
|
|
664
|
+
name: 'lingshu:session-window',
|
|
665
|
+
text: escapePromptBraces(renderUntrustedMemoryBlock(
|
|
666
|
+
`【本会话近期对话】\n${winText}`)),
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
} catch { /* 静默:窗口取数失败不影响请求,也不影响既有块 */ }
|
|
670
|
+
}
|
|
493
671
|
}
|
|
494
672
|
}
|
|
495
673
|
catch { /* 静默:召回失败不影响请求 */ }
|
|
@@ -499,8 +677,9 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
499
677
|
|
|
500
678
|
ctx.on('session/event', (session, event: SessionEvent) => {
|
|
501
679
|
// H1(2026-09-30)**会话级**判据:子代理/委派子会话的自动记忆**整条会话**拦掉。
|
|
502
|
-
// 位置在取 sid
|
|
503
|
-
//
|
|
680
|
+
// 位置在取 sid **之前**——子代理会话不得污染会话状态(lib/session_state.ts
|
|
681
|
+
// 单点),否则顶层会话的自动召回会拿子代理的 session 去读(读错会话)。
|
|
682
|
+
// 字段缺失即不拦,见 isSubagentSession。
|
|
504
683
|
if (isSubagentSession(session)) {
|
|
505
684
|
ctx.logger.info('dsh-memory: 子代理会话的自动记忆被拦(H1:header.origin/delegationDepth)')
|
|
506
685
|
audit.filtered('subagent')
|
|
@@ -508,9 +687,14 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
508
687
|
}
|
|
509
688
|
// 会话归属(P45):记忆写入必须带会话身份,用来区分不同会话的记忆。
|
|
510
689
|
// 空串 = 宿主未给出会话标识 → **显式标注 unassigned**(H2③:不落内核的进程级
|
|
511
|
-
// 随机 sess_*,也不编造宿主会话 id——见
|
|
690
|
+
// 随机 sess_*,也不编造宿主会话 id——见 lib/session_state.ts 的常量注释)。
|
|
512
691
|
const sid = sessionIdOf(session)
|
|
513
|
-
|
|
692
|
+
// 观测即记录(B 治本批):注入单点在 lib/session_state.ts——工具面
|
|
693
|
+
// (src/tools.ts 的写归因转发)读同一个状态;位置不动(H1 子代理闸之后)。
|
|
694
|
+
if (sid) {
|
|
695
|
+
noteSession(sid)
|
|
696
|
+
observedSession = true
|
|
697
|
+
}
|
|
514
698
|
const sessionTag = sid ? { session: sid } : { session: UNASSIGNED_SESSION }
|
|
515
699
|
if (event.type === 'user/message' && opts.userMessage) {
|
|
516
700
|
// 落盘审计(issue #56):source.kind 分布——先于两级判据记录**完整**输入分布
|
|
@@ -572,5 +756,21 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
|
|
|
572
756
|
...sessionTag,
|
|
573
757
|
}))
|
|
574
758
|
}
|
|
759
|
+
|
|
760
|
+
// ── 短期窗口写侧(contextWindow):与上方知识面写入**独立成轨** ──
|
|
761
|
+
// 位置在既有分支链**之外**:不受 opts.userMessage / opts.assistantMessage
|
|
762
|
+
// 门控(那两个开关管知识面;本机制由 contextWindow.enabled 管——两轨关系
|
|
763
|
+
// 见文件头与 windowEntry 注释)。
|
|
764
|
+
// 过滤面与知识面同源:H1 会话级(子代理整条会话)已在函数首拦回;此处经
|
|
765
|
+
// windowEntry 复用同一组谓词(relay / kind),再经**同一个** sanitize 脱敏
|
|
766
|
+
// ——纯凭据消息(sanitize 返回 null)同样不写(不把明文凭据引进窗口)。
|
|
767
|
+
// 失败只记 warn(noteRecent),绝不冒泡进会话流。
|
|
768
|
+
if (cwEnabled) {
|
|
769
|
+
const entry = windowEntry(event)
|
|
770
|
+
if (entry) {
|
|
771
|
+
const safe = sanitize(entry.text)
|
|
772
|
+
if (safe !== null) noteRecent(entry.role, safe, { ...sessionTag })
|
|
773
|
+
}
|
|
774
|
+
}
|
|
575
775
|
})
|
|
576
776
|
}
|
package/src/index.ts
CHANGED
|
@@ -169,8 +169,29 @@ export const Config: z<Config> = z.object({
|
|
|
169
169
|
autoRecall: z.boolean().default(true),
|
|
170
170
|
autoRecallLimit: z.number().default(4),
|
|
171
171
|
desensitize: z.boolean().default(true),
|
|
172
|
+
/** **短期会话窗口**(滑动窗口,2026-10-06)——「短期保留近 N 条对话记录」
|
|
173
|
+
* 的机制实现(长期记忆仍走灵枢的显式调用)。
|
|
174
|
+
*
|
|
175
|
+
* enabled(缺省 true)控制**整条机制**(写侧 + 注入侧同时静默);
|
|
176
|
+
* turns(缺省 10)=窗口取数条数(服务端 `session_recall` 的
|
|
177
|
+
* `recent_limit`,语义是**条**不是轮)。
|
|
178
|
+
*
|
|
179
|
+
* ⚠️ **两轨关系(勿混,机制头注见 src/hooks.ts / mdcg_client.ts)**:
|
|
180
|
+
* · 知识面轨:`userMessage` / `assistantMessage` 管「消息沉淀成记忆
|
|
181
|
+
* 节点」(role:user/assistant,进检索正排)。使用者 2026-09-27 的
|
|
182
|
+
* `userMessage=false` 决策关的是**这一轨**——消息不自动进知识面;
|
|
183
|
+
* · 窗口轨:本组管「消息进**运行态窗口**」(`_recent.jsonl`:滚动淘汰、
|
|
184
|
+
* 不占知识层、不进检索正排、**不是知识节点**)。
|
|
185
|
+
* 两轨独立开关、互不替代:知识面关掉时窗口照常工作(这正是本机制存在
|
|
186
|
+
* 的意义——上下文压缩后的续接锚不依赖自动记忆的写入开关)。 */
|
|
187
|
+
contextWindow: z
|
|
188
|
+
.object({
|
|
189
|
+
enabled: z.boolean().default(true),
|
|
190
|
+
turns: z.number().default(10),
|
|
191
|
+
})
|
|
192
|
+
.default({ enabled: true, turns: 10 }),
|
|
172
193
|
})
|
|
173
|
-
.default({ userMessage: true, assistantMessage: false, toolResult: false, importance: 0.6, autoRecall: true, autoRecallLimit: 4, desensitize: true }),
|
|
194
|
+
.default({ userMessage: true, assistantMessage: false, toolResult: false, importance: 0.6, autoRecall: true, autoRecallLimit: 4, desensitize: true, contextWindow: { enabled: true, turns: 10 } }),
|
|
174
195
|
toolCallTimeoutMs: z.number().default(60_000),
|
|
175
196
|
maxRetryDelayMs: z.number().default(30_000),
|
|
176
197
|
failOnStartupError: z.boolean().default(false),
|
package/src/lib/mdcg_client.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* 外部裁决回填 → MdcgClient.verify() → MCP cg(op=verify)
|
|
20
20
|
* 最近记忆时间线 → MdcgClient.timeline() → MCP stg(op=timeline)
|
|
21
21
|
* 近期事件窗口 → MdcgClient.recent() → MCP cg(op=recent)
|
|
22
|
+
* 窗口追加事件 → MdcgClient.recentAdd() → MCP cg(op=recent, action=add)
|
|
23
|
+
* 会话续接包 → MdcgClient.sessionRecall() → MCP cg(op=session, action=recall)
|
|
22
24
|
* 身份读取 → MdcgClient.identity() → MCP cg(op=identity)
|
|
23
25
|
* 白箱能力验证 → MdcgClient.whitebox() → MCP cg(op=whitebox)
|
|
24
26
|
* 服务信息 → MdcgClient.serviceInfo()→ MCP cg(op=info)
|
|
@@ -307,6 +309,52 @@ export class MdcgClient {
|
|
|
307
309
|
return this.cg({ op: 'recent', limit })
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
/** 追加一条**近期事件窗口**记录(`cg(op=recent, action=add)` →
|
|
313
|
+
* `md_cg` 的 `remember_event(role, text, tags, meta, window)`)。
|
|
314
|
+
*
|
|
315
|
+
* 这是「滑动窗口」写侧:事件落 `_recent.jsonl`(**运行态面**),按窗口滚动
|
|
316
|
+
* 淘汰(服务端缺省 200 条,`mdcg.py:91 DEFAULT_RECENT_WINDOW`)——它**不是
|
|
317
|
+
* 知识节点**:不占 knowledge 层、不进检索正排,与 `remember()` 的知识面沉淀
|
|
318
|
+
* 是两条独立的轨道(见 src/hooks.ts 文件头的 contextWindow 头注)。
|
|
319
|
+
*
|
|
320
|
+
* ⚠️ **不注入 as_unit**(与 `write()` 同款理由的反面):本调用不写任何层的
|
|
321
|
+
* 节点(服务端 `remember_event` 不做层白名单校验),收窄单元无收益且可能压低
|
|
322
|
+
* 事件密级(缺省 internal);`meta.session` 由调用方显式给出(沿本文件
|
|
323
|
+
* `remember()` 的 sessionTag 口径)。
|
|
324
|
+
*
|
|
325
|
+
* ⚠️ 服务端对 `meta` 做 `setdefault`(tenant/session/harness/unit,见
|
|
326
|
+
* `mdcos.py:5057 remember_event`):调用方已写的键**不被覆盖**。 */
|
|
327
|
+
recentAdd(role: string, text: string,
|
|
328
|
+
meta: Record<string, unknown> = {},
|
|
329
|
+
tags: string[] = []): Promise<unknown> {
|
|
330
|
+
return this.cg({ op: 'recent', action: 'add', role, text, meta, tags })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** **会话续接包**(`cg(op=session, action=recall)` → `md_cg` 的
|
|
334
|
+
* `session_recall`):一次取回 notes / goals / tasks / **recent 事件窗口** /
|
|
335
|
+
* unresolved / self_state,按 `budget_tokens` 整包裁剪(服务端
|
|
336
|
+
* `mdcos.py:3351-3362`:交替丢 recent / notes 尾部)。
|
|
337
|
+
*
|
|
338
|
+
* 本插件消费其中的 `recent` 段——滑动窗口的**注入面**(见 src/hooks.ts)。
|
|
339
|
+
* `recent_limit` 语义 = 近期事件**条数**(非「轮数」);服务端读取时会按
|
|
340
|
+
* `max(1, recent_limit or 10)` 归一(`mdcos.py:3309`)。
|
|
341
|
+
*
|
|
342
|
+
* ⚠️ 两条服务端事实(决定注入侧的可达性,勿据本方法名臆测):
|
|
343
|
+
* ① `recent` 段取 `recent_events(limit=recent_limit)` —— **不按 session
|
|
344
|
+
* 过滤**(近期事件是运行态滚动窗口,会话归属只写在每条事件的 meta 里);
|
|
345
|
+
* ② budget 是**整包**预算:库内 notes/tasks/self_state 占位越多,同样
|
|
346
|
+
* budget 下 recent 段被裁得越短(实测:空库 + 10 条窗口条目,
|
|
347
|
+
* budget_tokens=600 → recent 8 条;1200 → 10 条)。
|
|
348
|
+
*
|
|
349
|
+
* 只读调用,不注入 as_unit(同其它读路径:读无副作用,收窄只会压低 owner
|
|
350
|
+
* 的 private 读能力)。 */
|
|
351
|
+
sessionRecall(session: string, recentLimit: number, budgetTokens: number): Promise<unknown> {
|
|
352
|
+
return this.cg({
|
|
353
|
+
op: 'session', action: 'recall',
|
|
354
|
+
session, recent_limit: recentLimit, budget_tokens: budgetTokens,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
310
358
|
/** 最近记忆**时间线**(AEIS `timeline` 的对应物):`stg(op=timeline)` →
|
|
311
359
|
* `{count, limit, items:[{id, layer, start, end, preview}]}`,按时间倒序。
|
|
312
360
|
*
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session_state.ts —— 插件侧「运行期会话状态」单点(观测 → 写归因注入)
|
|
3
|
+
*
|
|
4
|
+
* 为什么有这个模块(B 治本批,2026-10-06)
|
|
5
|
+
* ----------------------------------------
|
|
6
|
+
* DSH 单进程多会话:宿主的会话标识只经 `session/event` 到达插件。此前它只被
|
|
7
|
+
* 用在 hooks 面(自动记忆的 remember / read 带 extra.session);**agent 直调
|
|
8
|
+
* 工具面**(src/tools.ts 的 execute → bridge.callTool)转发时**不带会话**——
|
|
9
|
+
* 而部署侧 `MDCG_SESSION` env 一旦取消/为空,`md_cg/mcp_server.py` 的
|
|
10
|
+
* `_declared_session`(:3823-3850)就以**请求声明**为归因来源
|
|
11
|
+
* (优先级:env > 请求声明 > 进程身份),不传即落回进程身份——md_cg 的
|
|
12
|
+
* `Principal.__init__` 会为假值 session 生成**进程级随机** `sess_<hex>`
|
|
13
|
+
* (md_cg/security.py:117):归属在审计上既读不出是谁、跨进程也对不上,
|
|
14
|
+
* 是静默的归属丢失。
|
|
15
|
+
*
|
|
16
|
+
* 本模块把「观测会话」与「写归因注入」收成**单点**:
|
|
17
|
+
* · hooks 面:每收到 session/event 就 `noteSession(sid)`——调用点仍在 H1
|
|
18
|
+
* 子代理闸**之后**(子代理会话不得污染会话状态的位置不变量保持);
|
|
19
|
+
* · 工具面:写归因调用注入 `currentSession() || UNASSIGNED_SESSION`
|
|
20
|
+
* (判据矩阵见 `attributeSession`,只注入写面;读面一律不注入)。
|
|
21
|
+
*
|
|
22
|
+
* 模块级状态(`lastSession`)是**进程级**的:同一进程内多个会话共享它,
|
|
23
|
+
* 「最近一次观测」即当前活动会话——与 hooks 面的既有口径一致(见
|
|
24
|
+
* test/session-attribution.test.ts ④「取值每步稳定」)。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
|
|
28
|
+
* B 治本批迁入本单点——注释要点保真搬迁)。
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
31
|
+
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
32
|
+
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
33
|
+
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
34
|
+
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
35
|
+
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
36
|
+
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
37
|
+
export const UNASSIGNED_SESSION = 'unassigned'
|
|
38
|
+
|
|
39
|
+
/** 最近一次观测到的宿主会话标识(空串 = 未观测到会话)。
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ 本状态是**模块级(进程级)单点**:hooks 面观测写入(src/hooks.ts 的
|
|
42
|
+
* session/event),工具面读它做写归因注入(src/tools.ts 的转发面)——两处必须
|
|
43
|
+
* 同源,否则注入的会话与写入的归属对不上(B 治本批的目的即在此)。
|
|
44
|
+
*
|
|
45
|
+
* 「本实例是否观测过」的**实例级门不在本模块**,在 src/hooks.ts 的
|
|
46
|
+
* installMemoryHooks 内(局部 `observedSession`):即「本实例观测到会话之后」
|
|
47
|
+
* 才用本单点值回落召回过滤。这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**
|
|
48
|
+
* ——契约建议的形态是 hooks 侧字面 `|| currentSession()`,但字面形态与硬边界
|
|
49
|
+
* 「test/session-attribution.test.ts 逐字未动且全绿」互斥(该守卫 ② 以
|
|
50
|
+
* 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B)。保留
|
|
51
|
+
* 实例门的两条理由:
|
|
52
|
+
* ① 「新实例 = 干净状态」是原闭包变量 `lastSession` 的**有意属性**(新装的钩子
|
|
53
|
+
* 在观测到会话前,自动召回不加过滤)——实例隔离语义在测试面被真实保留;
|
|
54
|
+
* ② 与既有冻结守卫 ② 相容(守卫逐字未动且全绿是本批硬边界)。
|
|
55
|
+
* 真机单实例下两者**逐位等价**:唯一差异窗口是「本实例未观测 ∧ 模块单点非空」,
|
|
56
|
+
* 而模块值只由本实例的 hooks 观测写入,单实例下该窗口不可达;HMR 重载场景下新
|
|
57
|
+
* 实例回落 '' 而非上一会话值,属更保守的防御行为(已由 owner 裁定接受)。 */
|
|
58
|
+
let lastSession = ''
|
|
59
|
+
|
|
60
|
+
/** 记录一次会话观测(H2③ 观测面单点)。
|
|
61
|
+
*
|
|
62
|
+
* trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
|
|
63
|
+
* 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
|
|
64
|
+
export function noteSession(sid: string): void {
|
|
65
|
+
const s = sid.trim()
|
|
66
|
+
if (s) lastSession = s
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
|
|
70
|
+
export function currentSession(): string {
|
|
71
|
+
return lastSession
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 写归因面判据:本次调用是否属于「要注入运行期会话」的写归因调用。
|
|
75
|
+
*
|
|
76
|
+
* **恰两条命中路径**(不得放宽、不得收窄出契约外):
|
|
77
|
+
* ① `mdcg_remember` —— md_cg 细粒度写入口(插件侧自动记忆/落图的写通道);
|
|
78
|
+
* ② `cg` 且 `op === 'write'` —— 认知图基元的带审核写路径。
|
|
79
|
+
*
|
|
80
|
+
* 为什么**只**这两个面(判据出处:md_cg/mcp_server.py):
|
|
81
|
+
* · **读面一律不注入**——`mdcg_recall` / `mdcg_search` / `mdcg_get` / `stg`
|
|
82
|
+
* 以及 `cg` 的其它 op,其 `session` 是**视图过滤**:服务端 schema 描述原文
|
|
83
|
+
* 「会话归属过滤(frontmatter.session;…缺省不过滤)」
|
|
84
|
+
* (md_cg/mcp_server.py:218-219 / :251-252 / :908-912 / :1074-1077)。
|
|
85
|
+
* 注入会把文档化的「缺省跨会话」翻转成「本会话视图」——那是功能收窄,
|
|
86
|
+
* 不是本批目标(读面要跨会话视图请显式 `stg(op=timeline, session="*")`)。
|
|
87
|
+
* · **op 特化语义不动**——`cg` 的 `sustain`/`session` 等 op 的 `session` 是
|
|
88
|
+
* 特化语义(resume/note 的**目标会话**),注入即污染其目标参数。
|
|
89
|
+
* · 归因与授权正交(issue #35 定稿):`call_tool` 的请求级 session **只做
|
|
90
|
+
* 归因**(写入归属/`_attribution` 取它),**不**改 `principal.session`
|
|
91
|
+
* (md_cg/mcp_server.py:3261-3264)——绑定档(private/secret)的读授权
|
|
92
|
+
* 锚定连接级身份,调用方自报的会话不构成看他人 private 的授权。
|
|
93
|
+
* 绑定档可见性判定 `MdCGSecure._readable`(md_cg/mdcos.py:5009-5053)
|
|
94
|
+
* 的会话绑定分支恒用 `nsess == self.principal.session`(:5051-5052),
|
|
95
|
+
* can_admin(设计者)豁免(:5045)——故本注入对绑定档无回归。
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ 禁止把本判据放宽成「所有带 session 参数的工具」:那会把上面两类语义
|
|
98
|
+
* (读面视图过滤 / op 特化目标)一并改写,属越权改契约。
|
|
99
|
+
*/
|
|
100
|
+
function isWriteAttributionCall(toolName: string, args: Record<string, unknown>): boolean {
|
|
101
|
+
if (toolName === 'mdcg_remember') return true
|
|
102
|
+
return toolName === 'cg' && args['op'] === 'write'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 请求是否已显式声明会话(string 且 trim 后非空 → 保留调用方声明,绝不覆盖)。 */
|
|
106
|
+
function hasDeclaredSession(args: Record<string, unknown>): boolean {
|
|
107
|
+
const v = args['session']
|
|
108
|
+
return typeof v === 'string' && v.trim() !== ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
|
|
112
|
+
*
|
|
113
|
+
* 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
|
|
114
|
+
* (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
|
|
115
|
+
* 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
|
|
116
|
+
* 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
|
|
117
|
+
*
|
|
118
|
+
* 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
|
|
119
|
+
* 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
|
|
120
|
+
*
|
|
121
|
+
* 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
|
|
122
|
+
* 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
|
|
123
|
+
export function attributeSession(toolName: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
124
|
+
if (!isWriteAttributionCall(toolName, args)) return args
|
|
125
|
+
if (hasDeclaredSession(args)) return args
|
|
126
|
+
return { ...args, session: currentSession() || UNASSIGNED_SESSION }
|
|
127
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import { defineTool, type ParameterPropertySpec, type ParameterSchemaSpec, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
|
|
10
10
|
import type { LingshuBridge, McpTool } from './bridge.ts'
|
|
11
|
+
// B 治本批(2026-10-06):写归因注入单点(运行期会话 → 写归因调用)。
|
|
12
|
+
// 运行时导入写 `.js`(本仓口径:type-only 才写 `.ts`,tsconfig 未开
|
|
13
|
+
// allowImportingTsExtensions —— 值导入写 `.ts` 会 TS5097 编译失败)。
|
|
14
|
+
import { attributeSession } from './lib/session_state.js'
|
|
11
15
|
|
|
12
16
|
/** 默认暴露的核心工具集合:**记忆面已基元化**,只注册 `cg` / `stg` 两个认知基元。
|
|
13
17
|
*
|
|
@@ -186,7 +190,14 @@ export async function registerLingshuTools(
|
|
|
186
190
|
// 此前完全忽略取消,取消后写操作(remember/relate/ingest 等)仍可能产生副作用
|
|
187
191
|
async execute(args: Record<string, unknown>, exec: { signal: AbortSignal }) {
|
|
188
192
|
if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
|
|
189
|
-
|
|
193
|
+
// B 治本批(2026-10-06):agent 直调工具的转发面把**运行期会话**注入
|
|
194
|
+
// **写归因调用**——env(MDCG_SESSION)取消后,请求声明即归因唯一来源
|
|
195
|
+
// (md_cg/mcp_server.py 的 _declared_session:env > 请求声明 > 进程身份)。
|
|
196
|
+
// 判据单点在 lib/session_state.ts 的 attributeSession:只注入
|
|
197
|
+
// mdcg_remember 与 cg(op=write) 两个写面;读面(视图过滤)/ op 特化语义
|
|
198
|
+
// 一律不动。命中且未显式声明时返回新对象,否则原样透传。
|
|
199
|
+
const forwarded = attributeSession(tool.name, args as Record<string, unknown>)
|
|
200
|
+
const result = await bridge.callTool(tool.name, forwarded, exec.signal)
|
|
190
201
|
if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
|
|
191
202
|
if (result.isError) {
|
|
192
203
|
throw new Error(extractText(result.content) || `灵枢 ${tool.name} 执行失败`)
|
package/zcode/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 本文件由 `scripts/render_discipline.py` 从真源
|
|
4
4
|
> `docs/工作纪律_认知图条目_v1.1.json` 渲染生成,**请勿手改**;改真源后重跑渲染。
|
|
5
|
-
> 真源指纹(SHA256 前16位):
|
|
5
|
+
> 真源指纹(SHA256 前16位):9486668b0dadac39
|
|
6
6
|
|
|
7
7
|
## 概述
|
|
8
8
|
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
### 16. 任务收尾归档(记忆闭环)
|
|
129
129
|
|
|
130
130
|
- **触发**:任务执行完成|修改落地后|交付后
|
|
131
|
-
- **动作**:任务收尾→提炼核心修改(内容/原因/位置/验证结论四要素, 不写中间过程/试错/调试/重复确认)→按 CCG 六要素成文(# 功能名/# 生效条件/# 子功能/# 执行/# 验证方式/# 不适用条件 六行缺一不可, 与正文四要素并置)→写入灵枢记忆(认知图/MCP memory)——text 类缺要素即被写入闸门拒(REJECT, 当场返回缺失清单, 不是重试无用), 按清单补齐后重写即可→读回确认(写入后发起一次读取查询确认写入成功且可检索)→标注关联条目+更新 subgraph/depends_on
|
|
131
|
+
- **动作**:任务收尾→提炼核心修改(内容/原因/位置/验证结论四要素, 不写中间过程/试错/调试/重复确认)→按 CCG 六要素成文(# 功能名/# 生效条件/# 子功能/# 执行/# 验证方式/# 不适用条件 六行缺一不可, 与正文四要素并置)→写入灵枢记忆(认知图/MCP memory)——text 类缺要素即被写入闸门拒(REJECT, 当场返回缺失清单, 不是重试无用), 按清单补齐后重写即可→读回确认(写入后发起一次读取查询确认写入成功且可检索)→标注关联条目+更新 subgraph/depends_on; 长会话每10轮→同款间歇归档(写入+读回确认一次), 不等收尾
|
|
132
132
|
- **不适用**:情感交互|闲聊|纯查询无改动
|
|
133
133
|
- **声明**:按工作纪律第16条: 任务收尾归档——每次任务执行完只提炼核心修改(内容/原因/位置/验证结论)并按 CCG 六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆(text 类六要素缺失会被写入闸门当场拒绝并返回缺失清单——补齐后重写即可, 不必等读回确认才发现), 写入后发起一次读取查询确认写入成功且可检索, 禁写中间过程/试错/调试等无效信息, 与第2条形成「查记忆→执行→写记忆→读回确认」闭环。
|
|
134
134
|
|
|
@@ -142,9 +142,9 @@
|
|
|
142
142
|
### 18. 工作区索引优先
|
|
143
143
|
|
|
144
144
|
- **触发**:查找工作区文件|需要了解工作区结构|跨目录检索定位|长会话续接/上下文压缩后查找工作区文件
|
|
145
|
-
- **动作**:定位工作区文件→先读 WORKSPACE_INDEX.md(仓根)→有则按表中职责/关键入口直接定位→无则先 python scripts/workspace_index.py --write 生成再读→守卫报陈化先重生成→不以重复全盘浏览代替; 新增顶层目录/根级文件须在脚本 DIR_ROLES/ROOT_FILES 登记后重生成; 上下文压缩/长会话续接后, 先 cg route『工作区索引』重建纪律视野(压缩会丢弃未留下执行痕迹的纪律, 只剩被声明过的条目——须主动召回)
|
|
145
|
+
- **动作**:定位工作区文件→先读 WORKSPACE_INDEX.md(仓根)→有则按表中职责/关键入口直接定位→无则先 python scripts/workspace_index.py --write 生成再读→守卫报陈化先重生成→不以重复全盘浏览代替; 新增顶层目录/根级文件须在脚本 DIR_ROLES/ROOT_FILES 登记后重生成; 上下文压缩/长会话续接后, 先 cg route『工作区索引』重建纪律视野(压缩会丢弃未留下执行痕迹的纪律, 只剩被声明过的条目——须主动召回); 压缩/续接后三步=route『工作区索引』→输出声明→回取本会话近10轮窗口重建上下文(cg(op=recent)/session_recall/会话 md 镜像)
|
|
146
146
|
- **不适用**:已明确路径的单文件操作|纯会话内对话/问答(无文件查找)|仓外路径/系统路径(git 追踪面之外)
|
|
147
|
-
- **声明**:按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。
|
|
147
|
+
- **声明**:按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。 压缩续接后并回取本会话窗口(最近10轮)重建上下文。
|
|
148
148
|
|
|
149
149
|
## 声明出口(`response.direct` 原文 · 未输出即未执行)
|
|
150
150
|
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
| 15 | 按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 另: 公开面路径写相对路径——本机绝对路径以中性占位代(仓内 `<仓根>`/家目录 `~/`/临时 `%TEMP%`), 门禁 check_local_paths。 |
|
|
168
168
|
| 16 | 按工作纪律第16条: 任务收尾归档——每次任务执行完只提炼核心修改(内容/原因/位置/验证结论)并按 CCG 六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆(text 类六要素缺失会被写入闸门当场拒绝并返回缺失清单——补齐后重写即可, 不必等读回确认才发现), 写入后发起一次读取查询确认写入成功且可检索, 禁写中间过程/试错/调试等无效信息, 与第2条形成「查记忆→执行→写记忆→读回确认」闭环。 |
|
|
169
169
|
| 17 | 按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前, zcode端可优先使用zcode工作流处理), 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。 |
|
|
170
|
-
| 18 | 按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。 |
|
|
170
|
+
| 18 | 按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。 压缩续接后并回取本会话窗口(最近10轮)重建上下文。 |
|
|
171
171
|
|
|
172
172
|
## 记忆接口速查(`cg` / `stg`)
|
|
173
173
|
|