dsh-session-handoff
会话自动衔接:新建会话时,自动把「上一场会话」的尾部对话注入为一段交接材料,使新会话不必从头解释上下文。
快速开始
1) 装依赖(在目标 profile 的 package.json 加 link 依赖):
"dsh-session-handoff": "link:E:/alice/self-plugins/dsh-session-handoff"
2) 挂组合:
- id: dsh-session-handoff
name: dsh-session-handoff
3) 30 秒验证:新建会话 → 首轮上下文里应出现上一场的尾部对话(仅在存在新鲜对话时注入)
解决的问题
一场会话卡住/中断/上下文过载后,新开一场就丢失了「刚在做什么、卡在哪、下一步是什么」。手工复述既费时又容易漏。本插件把这件事变成自动的、可核查的一行注入。
它做什么
- 监听
session/created({ global: true }),只对真正的新会话生效:子代理会话、fork 子会话、重启时恢复的历史会话一律跳过。 - 取该会话的 scope key(
carrierKeyOf(this)——事件的this是 routing-only 载体,没有 Context API),找到同工作目录、窗口内最近的一场有真实对话的会话。 - 摘录它尾部的若干条 user/assistant 文本(不调模型、不做摘要生成,成本为 0 且可复现),渲染成一段交接块。
- 交接块经
systemPrompt.context落为动态上下文快照:全局注册一次,text按本次装配的AssembleContext.scope查表返回——未命中的会话拿到空串,而「空文本不贡献任何内容」是PromptContext的既定语义 ⇒ 只影响被衔接的那一场会话。
交接块首行显式声明它是历史材料、不是新指令——衔接内容来自上一场会话,绝不能变成可执行的命令。
它不做什么
| 不做 | 归属 |
|---|---|
| fork(全量继承历史) | sessions.fork |
| 生成式摘要(调模型压缩) | 模型侧 or 人工 |
| 改系统提示词 / 工具面 | section / 工具注册 |
| 跨会话记忆 | dsh-agent-memory |
配置
| 字段 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关 |
maxAgeMinutes |
720 |
新鲜度窗口:上一场会话最后活动早于此则不衔接 |
maxChars |
3000 |
交接块字符预算 |
messageChars |
400 |
单条消息字符上限 |
maxMessages |
8 |
最多引用多少条消息 |
sameWorkspaceOnly |
true |
只衔接同一工作目录的会话 |
maxCandidates |
3 |
最多向前探查几个候选(每个都要读日志,故设上限) |
order |
850 |
动态上下文排序位 |
观测(机制自证)
每次注入/跳过/出错都落一行侧车:
tail -f "$DSH_HOME/session-handoff-trace.jsonl"
stage 取值:injected(注入成功,带来源会话/字符数/消息数)、skip(无合格对话)、filtered(被判据跳过,带 reason)、error / candidate-error(读日志失败)。
判据(可证伪):开一场新会话后,该文件应出现一行 "stage":"injected",且其中的 session 等于新会话 id;未产生注入时会看到 filtered/skip 并附原因——不会静默什么都不写。
已知边界
- 不作为的注入:交接文本进入模型历史(动态上下文快照默认进历史),因此只摘录 user/assistant 文本,不摘录工具结果与文件内容。
- 只认「最近一场」:不跨多场会话合并,不猜测话题相关性;判据是时间 + 工作目录,不含语义相似度。
- 读日志有成本:候选逐个
observeSession(读完整事件流)。maxCandidates就是这个成本的闸门;projectionMode: 'none'已关闭投影计算。 sessionQuery是硬依赖:写在inject里(不是ctx.get()探测)。用探测会与加载顺序赛跑,失败时插件静默变成空转(实测:装了、mounted、却零轨迹)。- 「新会话」按内容判,不按创建时间判:判据是该会话自身还没有真实对话(见下「实践修订记录」)。
测试
pnpm build && pnpm test # 10 个用例,含 4 条尸体样本(子代理/fork/过期/无对话/异目录/自身)
纯决策层(src/handoff.ts)与 IO 壳层(src/index.ts)分层:判据与渲染可离线验证,壳层只做搬运。
实践修订记录
两处判据都是被真实运行打回来才改对的,留档以免重犯:
2026-09-30 ·
ctx.get('sessionQuery')探测 → 静态inject首版用ctx.get()探测服务,探测失败就打一条 warn 后返回。结果:插件mounted、built: true、进程也重启了,却零轨迹——外部完全看不出「没加载」与「加载了但空转」的区别。改为inject = ['systemPrompt', 'sessionQuery']声明式依赖,并加一行启动自报stage: 'ready'。教训:机制必须自证,否则「装了」和「在工作」在观测上是同一张脸。2026-09-30 · 按
createdAt判「新会话」 → 按「自身有无真实对话」判 首版用「创建时间在 N 分钟内」识别新会话。实测:GUI 点「新会话」复用的是既有空白会话的 id(21:28 建的空会话,22:05 点「新会话」时仍用它),于是被误判为「恢复的老会话」而拒绝注入(轨迹里留下"reason":"not-new")。改为内容判据:extractMessages(session.snapshotEvents()).length === 0才算新会话。判据要贴语义(「还没有对话」),不要贴代理量(「创建时间新」)。
相关文档
- ⚠ 本插件暂无
semantic.md(未建语义文档)——契约以 README 各节与源码为准;补文档是待办。 - 中心仓:alice-digital-life
No comments yet. Be the first to write one.