第 22 讲:终端持久会话与 LSP

DeepSeek Harness 源码解析 · 第 22 讲

DeepSeek Harness 源码解析系列 第 22 讲:终端持久会话与 LSP 基于 DeepSeek Harness 源码 · 2026-09-06 💡 本讲一句话: 模型要"持续跟一个 shell/REPL 对话"和"问语言服务器定义在哪",本质都是 管理一个长命进程 ——前者难在"命令到底跑完没有"(就绪证据状态机),后者难在"取消停不下来怎么办"(串行队列 + 有界拆除)。本讲拆 terminal 和 lsp 两个能力家族,看它们如何用同一套纪律(owner 隔离、fail loud、恰好一次清理)驯服长命进程。 一、终端服务接缝:owner-scoped 的 PTY 注册表 packages/terminal 家族分三层: 接缝包 dsh-terminal ( ctx.terminals ,拥有 id、发布、授权、等待清理)、 后端包 dsh-terminal-bash (PTY 机制本身)和 工具包 dsh-tool-terminal (六个模型可见工具)。接缝包的模块注释把分工写死:「Backends own terminal mechanics while this service owns ids, publication, authorization, and awaited cleanup.」——后端管"怎么跟 PTY 说话",服务管"谁能碰哪个会话、什么时候必须清干净"。 先看服务的状态字段和 backend 注册( packages/terminal/terminal/src/index.ts ,第 104-137 行): 📄 packages/terminal/terminal/src/index.ts (第 104-137 行) export class TerminalSessionService extends Service { // PTY 注册表服务:继承 Cordis Service,消费者经 ctx.terminals / ctx.get('terminals') 拿到 private readonly backends = new Map<string, TerminalBackend>() // backend type → 实现;一个类型恰好对应一个 provider(重名直接拒绝) private readonly sessions = new Map<TerminalSessionId, SessionRecord>() // 已发布会话表:id → 记录(owner/name/type/后端会话/在途 send/close 栅栏) private readonly reservedNames = new Map<Agent, Set<string>>() // 按 owner 的名字预留集:防止同一 owner 并发 spawn 同名会话双双成功 private readonly pendingSpawns = new Map<Agent, Set<PendingSpawn>>() // 在途(未发布)spawn:owner 清理必须等它们落定,否则后端进程会成孤儿 private readonly ownerCleanups = new Map<Agent, () => Promise<void> | void>() // 按 owner 的 detach 钩子:agent 被 dispose 时它名下所有 PTY 一起清 private readonly disposedOwners = new WeakSet<Agent>() // 已判死的 owner——用 WeakSet 不阻止 agent 本身被 GC private nextId = 0 // 单调递增 id 计数器:pty-1、pty-2……id 是服务铸造的,不是后端给的 private disposing = false // 服务级拆除标志:置位后所有新 spawn 一律拒绝(SERVICE_DISPOSING) constructor(ctx: Context) { super(ctx, 'terminals') // 注册为 Cordis Service,键固定 'terminals'——工具包按…

阅读全文:DeepSeek Harness 源码解析 第 22 讲