Repository Wiki
deepseek-ai/deepseek-harness

Shell、PTY 与终端执行

本页聚焦仓库中已核实的持久 PTY 会话注册与生命周期实现:TerminalSessionService 为 Agent 管理会话身份、访问控制、后端注册、交互发送与销毁。Shell 的命令解析、具体 PTY 进程启动以及终端 UI 的实现不在本页作未经验证的描述。

目的与范围

适合需要接入终端后端、定位会话权限错误或排查创建/关闭竞态的工程师。这里讨论的是终端服务的协调层,而非 shell 命令语义、沙箱策略或某个后端的输入输出缓冲算法;这些具体机制应另见相应 Shell、子进程或终端后端专题。服务的职责边界在模块注释中明确:后端负责 terminal mechanics,服务负责 ID、发布、授权和等待清理完成。实现说明。

概述

TerminalSessionService 是基于 @deepseek-ai/cordis 的 Service,在 Context 中以 terminals 暴露。它以 backend type 选择具体实现,以精确 Agent 对象身份界定会话所有权;会话保留在进程内 Map 中,而不是在该实现里写入持久数据库。后端 spawn 成功且 owner 仍存活后才发布会话;之后可发送、读取、发送信号、列出及关闭。服务及存储、发布条件。

架构

Loading diagram...

Source: index.ts

后端注册采用 ctx.effect,scope 销毁时撤销对应的后端贡献;服务的 effect 则执行 disposeAll()。SessionRecord 保存 owner、可选名称、后端类型、后端会话以及活跃发送/关闭中的操作,使授权和生命周期栅栏集中在一个注册表中。注册逻辑、记录结构。

核心流程:创建、使用和关闭

创建与发布

spawn(owner, request, signal?) 先拒绝已销毁服务或已取消请求,并为 owner 安装清理回调;再检查所请求的后端类型和非空名称。reserveName 对同一 owner的已有会话和未完成创建同时查重;reserveSpawn 建立可被 owner/服务销毁打断的待创建记录。传给后端的 signal 是调用方 signal 和内部 signal 的组合(调用方提供 signal 时)。ID 按 pty-${++nextId} 分配;ID 分配并不等于发布。只有 backend.spawn 返回、外部取消检查通过、服务未销毁且 owner 仍为注册中的同一对象,才将记录放入 sessions 并返回含 MOTD 的快照。创建路径、预留逻辑。

失败时,如果已生成会话但尚未发布,服务尝试 session.close('PTY spawn rolled back');失败清理会作为聚合异常暴露(未因调用方取消而跳过抛出原取消错误)。finally 释放待创建预留和名称;若清理本身失败,待创建记录保留供销毁流程收集该失败。回滚及预留、待创建记录。

Loading diagram...

Source: index.ts

交互、读取与信号

startSend 通过 expectOwned 授权、拒绝关闭中的会话和同一会话的第二个活跃发送,然后把请求交给后端并在 operation.done 完成或拒绝时清空活跃标记。read 将滚屏读取请求委托给后端;signal 将允许的信号委托给后端。因此本层管理发送互斥,但具体文本写入、滚屏截断和操作完成的语义由后端会话决定。交互方法。

关闭与销毁

kill 将后端 close(reason) 的 Promise 存入 record.closing:并发的第二次调用等待同一关闭操作并返回 false;首次成功关闭才删除注册记录并返回 true。关闭失败会清空栅栏并将错误抛给调用方,使后续重试仍可进行。owner context 销毁会取消其未发布的 spawn,等待其结束后关闭已发布会话;服务销毁则对所有 owner 执行同样的流程,并最终清空后端、名称和待创建注册表。关闭、销毁。

Loading diagram...

Source: index.ts Source: index.ts

用法示例(仓库实现摘录)

以下是服务内部的真实调用路径,不是未经验证的外部调用示例。

注册后端并绑定作用域

typescript
1registerBackend(backend: TerminalBackend): () => void { 2 if (backend.type.length === 0) throw new Error('pty backend type must be non-empty') 3 if (this.backends.has(backend.type)) { 4 throw new TerminalError(`a PTY backend named "${backend.type}" is already registered`, 'DUPLICATE_BACKEND') 5 } 6 const dispose = this.ctx.effect(() => { 7 this.backends.set(backend.type, backend) 8 return () => { 9 if (this.backends.get(backend.type) === backend) this.backends.delete(backend.type) 10 } 11 }, 'pty.registerBackend()') 12 return () => void dispose() 13}

Source: index.ts

这里特意只在 map 当前值仍是同一对象时删除,以免撤销旧的 contribution 时误删别的注册项;重复 type 在 effect 创建前就被拒绝。

交互操作的单会话互斥

typescript
1startSend(owner: Agent, id: TerminalSessionId, request: TerminalSendRequest): TerminalSendOperation { 2 const record = this.expectOwned(owner, id) 3 if (record.closing !== undefined) throw new Error(`PTY session ${id} is closing`) 4 if (record.active !== undefined) throw new TerminalError(`PTY session ${id} already has an active send`, 'SEND_ACTIVE') 5 const operation = record.session.startSend(request) 6 record.active = operation 7 void operation.done.then( 8 () => { record.active = undefined }, 9 () => { record.active = undefined }, 10 ) 11 return operation 12}

Source: index.ts

这是每个 SessionRecord 上的互斥,而不是全局串行化;活跃标记在成功或失败后均释放。

等待后端真正关闭后移除记录

typescript
1async kill(owner: Agent, id: TerminalSessionId, reason: string = 'model request'): Promise<boolean> { 2 const record = this.expectOwned(owner, id) 3 if (record.closing !== undefined) { 4 await record.closing 5 return false 6 } 7 const closing = record.session.close(reason) 8 record.closing = closing 9 try { 10 await closing 11 this.sessions.delete(id) 12 return true 13 } catch (error) { 14 record.closing = undefined 15 throw error 16 } 17}

Source: index.ts

注册表在关闭完成前仍保留记录,避免已请求关闭却被当作不存在;后端关闭失败时也保留记录以便显式重试。

配置与状态

本次核实的服务实现没有读取环境变量或配置文件;它接受后端注册与每次调用的请求参数。不要将下列项误认为全局配置键。声明与创建入口。

项类型默认值作用
request.type后端类型字符串无;必须匹配已注册 type选择 backends 中的实现
request.name可选字符串未命名按 owner 去重;空字符串拒绝
request.cwd可选值,具体定义见类型契约未传入原样传到后端 spawn spec
signal可选 AbortSignal未传入在后端创建阶段与内部 owner 销毁 signal 合并
kill 的 reasonstring'model request'向后端 close 传递诊断原因

会话 ID 为进程内递增计数生成的 pty-N 字符串;TerminalSessionId 函数只是 TypeScript 品牌转换,不负责生成或验证 ID。ID 与计数、分配、使用。

API 参考

以下签名均来自 TerminalSessionService 实现;请求/结果类型的字段详情未在本页读取类型定义,除服务实际访问的字段外不推断其他成员。公开方法。

方法参数和返回值行为与主要失败
registerBackend(backend: TerminalBackend): () => voidbackend 的 type 必须非空且唯一;返回撤销注册的函数空 type 抛 Error;重复 type 抛 TerminalError('DUPLICATE_BACKEND')
listBackends(): string[]无参数;返回按注册顺序排列的新数组仅列出当前注册的 type
spawn(owner: Agent, request: TerminalSpawnRequest, signal?: AbortSignal): Promise<TerminalSpawnResult>owner 为精确 Agent 对象;请求选择 type,可带 name/cwd;可取消创建后再发布;无后端、重复名称、owner 不存活、服务销毁或取消均可失败;后端及回滚也可失败
hasOwnerActivity(owner: Agent): boolean查询指定 owner未发布的 pending spawn 或已发布会话任一存在即返回 true
startSend(owner: Agent, id: TerminalSessionId, request: TerminalSendRequest): TerminalSendOperation指定 owner、ID 和后端发送请求关闭中或存在活跃发送时拒绝;后端返回 live operation
read(owner: Agent, id: TerminalSessionId, request: TerminalReadRequest = {}): TerminalReadResult可省略读请求后端提供带分页元信息的滚屏结果
signal(owner: Agent, id: TerminalSessionId, signal: TerminalSignal): Promise<TerminalSignalResult>交给后端的信号返回后端信号结果;本服务先检查所有权
kill(owner: Agent, id: TerminalSessionId, reason: string = 'model request'): Promise<boolean>可选关闭原因本次完成关闭返回 true,等待已有关闭返回 false;关闭失败则抛出
list(owner: Agent): TerminalSessionSnapshot[]精确 owner按发布顺序返回其会话的新快照

快照携带 sessionId、可选 name、type、后端可选 pid 以及实时 status();仅 spawn 的快照额外包含后端 motd。read、signal、kill、startSend 都使用 expectOwned:不存在时为 NO_SESSION,owner 对象不同时为 FOREIGN_SESSION。快照与授权。

失败、边界与并发

  • 错误分类: TerminalErrorCode 明确列出 DUPLICATE_BACKEND、DUPLICATE_NAME、FOREIGN_SESSION、NO_BACKEND、NO_SESSION、OWNER_NOT_LIVE、SEND_ACTIVE、SERVICE_DISPOSING。空 backend type、空会话名和关闭中调用 startSend 则直接抛普通 Error,不能把所有失败都当作 TerminalError 处理。错误类型、验证点、创建验证、发送验证。
  • 所有权不是字符串匹配: isLiveOwner 要求 agent registry 中的 get(owner.id) === owner;expectOwned 比较 record.owner !== owner。同 ID 的另一个对象不能接管终端。存活检查、访问检查。
  • 创建与清理竞态: 待创建 spawn 在调用后端之前预留,并受内部取消信号约束;owner 销毁先 abort 并 Promise.all 等待它们结算,再关闭已发布会话。hasOwnerActivity 同时检查两类记录,从创建至关闭无“未发布即空闲”的检测间隙。创建预留、活动检查、清理顺序。
  • 聚合清理失败: closeRecords 用 Promise.allSettled 等待每个关闭,收集失败并抛 AggregateError;abortAndClose 即使取消未发布创建失败,也仍尝试关闭已发布会话。全局 teardown 在 finally 中清空注册表并运行 owner 清理函数。批量关闭。

运行与扩展提示

该协调层没有数据库持久化逻辑:服务重建后不会从 sessions Map 恢复会话。按 owner 列出或检查活动时,会遍历当前会话;这是进程内表的线性检查,不应误读为数据库查询。状态和遍历、列表、名称查重。若增加后端,实现者应遵循 TerminalBackend/TerminalBackendSession 契约:服务调用其 spawn、startSend、read、signal、close、status,并依赖关闭 Promise 表示清理已完成;详细类型定义和具体 shell 后端行为须以其实现为准。服务调用点、操作委托。

相关链接

Sources

(1 files)