Skip to content

Cell - Ta Host ​

ta serve 的服务端,作用相当于 Codex 的 app-server:pi 只有 stdio 上的 RPC 模式(一个进程一个会话、一个控制方),本组件补上多会话、多客户端、断线补发、远程审批、幂等回执和会话锁。命令与事件的语义和 pi RPC 模式一致。

组件 ​

组件作用
TaServerImpl@Rpc(TaServer),会话与 pi 的 RPC 命令,各命令照 pi rpc-mode 的做法转给 AgentSession / AgentSessionRuntime(pi 只导出绑在 stdio 上的 runRpcMode,所以这里逐条对应;升级 pi 时对照它的 rpc-mode 检查)
TaWorkspaceServerImpl / TaTerminalServerImpl / TaSettingsServerImpl / TaPackageServerImpl / TaAuthServerImpl / RelayBridge同一条 WebSocket 上的其他服务,各自转给下面的组件;TaAccess 拒绝 HTTP 形式的调用(POST /api/<服务> 返回 404)
TaSettingsServerImpl.setLanguageTa 页面告诉服务端它的语言(zh-CN、en-US),写进进程的 TA_LANGUAGE,服务端的 pi 包(ta-agents、ta-cron、ta-computer-use、/bug)按它说话;最后设置的客户端为准
ExtensionProvidersImplpi 包用 pi.registerProvider 注册的模型服务(比如 @celljs/ta-agents 的外部 Agent),要等扩展加载后才在共享的模型运行时里。还没有会话时,TaAuthServerImpl.listProviders、设置的模型列表、带 model 的 openSession 会先在家目录下加载一次用户的扩展(不信任项目资源),这样不会因为没登录模型服务而要求登录,新对话也能选到它们的模型。实测比不加载多约 7 MB,只在第一次调用时发生
TaAuthServerImpl@Rpc(TaAuthServer):在服务端跑 pi 的 /login 流程(OAuth 的登录页或设备码,或 API Key),把要打开的页面、要用户填的内容只发给发起登录的连接(TaClient.onAuthEvent),用户的回答用 answer 送回;logout 删掉存下的凭据。凭据写进服务端 pi 的 auth.json,所有会话共用,不会发回客户端
RelayBridge@Rpc(TaRelayServer):读 agent 目录的 relay.json,有配对的设备时连上 Relay,并替每台设备开一条到本机 /api/ws 的连接(见 @celljs/ta-relay);pair / unpair 与 ta pair 相同,没给 Relay 时用 https://relay.cellbang.com
TaWorkingDirectorysetWorkingDirectory 设的工作目录,记在 agent 目录的 ta.json(workingDirectory),跨重启保留;已经打开的会话不受影响
TaSessionRegistry打开的会话;超过 maxSessions 拒绝,空闲(无客户端、无运行、无待答对话框)超过 idleTimeoutMs 关闭,会话文件保留
TaSessionFactory像 pi 命令那样装配会话:项目信任、设置、扩展、主题;所有会话共用一个模型运行时。可 rebind 替换
TaSession一个打开的会话:事件日志、回执、已连接的客户端;newSession/fork/switchSession 后 id 不变,并发 session_replaced;navigateTree(pi 的 /tree)在同一会话里换分支后也发
RemoteUiContextpi 扩展的 ctx.ui:对话框广播给所有客户端,第一个回答生效,超时按扩展给的默认值
EventLog / ClientChannel每个会话按字节保留最近的事件供重连补发;每个客户端每 30ms 合并成一帧,等客户端应答后再发下一帧,落后过多则断开让其重连
TaAccesstoken 校验(WebSocket 握手与 HTTP)、监听本机时校验 Host 防 DNS rebinding、写 serve.json;token 与端口记进 serve-state.json,下次启动沿用
WebExtensionCatalog(WebExtensionCatalogImpl)listWebExtensions / readWebExtension / removeWebExtension:按 pi 加载包的方式(packages 设置与 extensions/ 目录,项目的只在受信任时)找出包里的 Web 扩展(package.json 的 pi.web,或 web/ 下的模块);只读声明过、在包内、不超过 2 MB 的文件
包(listPackages / installPackage / removePackage)列出 pi 加载的所有包(不论有没有 Web 扩展),带 resources(extensions、web、skills、prompts、themes);installPackage 与 ta install 相同(project 即 -l,只在受信任的项目);removePackage 与 removeWebExtension 相同,按包的 id
包的服务端(callPackageHost)调用内置包 pi.host 返回的对象上的方法(参数、返回值都是 JSON),供它的 Web 扩展使用;dispose、constructor、_ 开头的和对象自带的方法不能调用
开关(setPackageEnabled)Ta 自带的 pi 包(TaPackages)按名字开关,以 origin: 'builtin' 列出,只能开关、不能移除,开关存在 agent 目录的 ta.json(disabledPackages)。其他包按 listPackages 的 id 开关,和 ta config 一样逐个资源写进 pi 的设置(PackageSwitch:extensions/ 里的写 +路径 / -路径 到 extensions 等列表,packages 里的写进该条目的过滤),所以终端里的 ta 也认;pi 什么都不加载、只有 Web 扩展的包记在 ta.json 的 disabledWeb。enabled:pi 加载它的任一资源时为开(只有 Web 扩展的看 disabledWeb),关掉的包的 Web 扩展不加载。会话 reload 后生效
TaSettingsStore(TaSettingsStoreImpl)getSettings / updateSettings:ta 的设置(TA_SETTINGS 列出的键)就是 pi 的 settings.json,用户层在 agent 目录、项目层在 <cwd>/.ta/;只改给出的键(null 删除,空对象随之删掉),其余内容原样保留,整文件经临时文件写入、同一时间只写一次,写时持有与 pi 相同的文件锁(<文件>.lock 目录,超过 10 秒视为失效),所以终端里的 ta 同时改设置不会互相覆盖(pi 在锁内重读文件、只写回它改过的键);读不了的文件不覆盖。项目层:pi 会询问信任的项目(有扩展、技能等)未被信任时拒绝写入,没有需要询问的内容时写入即记为信任(否则 pi 不读)。写入后让相关会话的 SettingsManager 重新加载(模型与思考强度对新会话生效)
ExtensionDataStore(ExtensionDataStoreImpl)getExtensionData / setExtensionData:包存在服务器上的小数据(Web 扩展的 pi.storage),每个包一个 <agentDir>/extension-data/<包名>.json(权限 0600),同一文件串行写入;值为 null 删除该键;每个包上限 4 MB。带 cwd 时是该目录的数据(pi.storage.directory(cwd)),存在 extension-data/directories/<目录名>-<哈希>/<包名>.json,哈希含执行环境,远程执行时同名目录互不影响;变更通知带上 cwd
ClientToolBroker客户端工具(clientTools):每个是 pi 的一个工具,调用时广播 client_tool_request,等第一个客户端用 respondClientTool 回答;run 中止或会话关闭时以失败结束
DefaultExecEnvironmentFactory按目录给执行环境:ssh://<host>/<path> 在那台主机上(RemoteHosts),本机路径在本机,配了 ta.exec.url 时在 ta exec-server 上(见 @celljs/ta-exec)。返回的环境的 cwd 是环境里的路径,路径都按它解析。也可 rebind 自己的 ExecEnvironmentFactory
RemoteHosts / TaRemoteServerImpl@Rpc(TaRemoteServer):ssh:// 目录的主机,每台一个 SshHost(用系统 ssh 和它的配置,首次自动装 Node.js 和服务器),第一次调用时连接、断线后下次调用重连。listHosts(~/.ssh/config 里没有通配符、ssh 能用的 Host,和 ta.json 的 remoteHosts)、addHost / removeHost(记下或忘掉一台主机)、resolveLocation(不带路径的位置换成家目录)、watch / answer(ssh 要密码、口令、确认新主机时问所有页面,第一个回答生效)、disconnect;连接步骤和连上断开都推给 TaClient.onRemoteEvent
PortForwardsforwardPort:远端目录所在主机的端口在本机 127.0.0.1 上打开(尽量同一个端口号),每个连接经执行环境的 connect 转发;本机目录原样返回
PiDirectories远端目录(以及 ta.exec.url 时的所有目录)交给 pi 的本机影子目录 <agent 目录>/remote/ssh/<host>/<path>:pi 的会话文件按它命名,项目设置写在它下面;会话信息里再换回位置。远端项目的 AGENTS.md 等由 RemoteContextFiles 读来交给 pi(见 ta-remote-ssh.md)

每个会话加载的扩展与 ta 命令相同:pi 命令自带的 codemode、tool search、MCP(pi 的 SDK 不自动加载它们;执行环境不在本机时 stdio MCP 服务在执行环境里启动),Ta 的内置扩展,开启着的内置包的 pi 扩展,以及 AgentExtension、AgentTool 组件(@celljs/ai-coding-agent)。

配置 ​

yaml
ta:
  host:
    maxSessions: 32
    idleTimeoutMs: 600000
    eventBufferBytes: 524288      # 每个会话可补发的事件量
    clientBacklogBytes: 8388608   # 每个客户端允许积压的量
  exec:
    url: ''                       # ta exec-server 的地址(ta serve --exec);空:本机
    token: ''                     # ta serve 会把 TA_EXEC_TOKEN 填到这里
  remote:
    ssh: ssh                      # ssh:// 目录用的 ssh 命令,~/.ssh/config 照常生效
    sshArgs: []                   # 加在每个主机前,如 ["-F", "/path/config"]
    nodeMirror: ''                # 主机没有 Node.js 18+ 时从哪下载;空:https://nodejs.org/dist

装了 @celljs/ta-web 时同一端口的 / 是 Web 客户端:GET 页面文件不需要 token(不含数据),/api 下的调用都要 token,监听本机时两者都校验 Host。

会话存储(云端) ​

ta serve 在本机用 pi 的会话文件,靠会话锁保证同一时间只有一个进程写。云端可能有多个 ta serve 副本,任何一个都可能接手会话,这时会话放在 TaSessionStorage 里:

  • 租约 + epoch:acquire 拿到会话时 epoch 加一;每次 write 都带上租约,epoch 不是当前的就拒绝(StaleLeaseError),什么也不写。被别的副本接手(takeOver,或租约过期后)的旧持有者即使还不知道,也写不进去。过期时间只决定别人什么时候可以不经接手直接拿走。
  • FileSessionStorage:目录里的 <id>.jsonl(pi 的格式)和 <id>.lease.json,用锁目录让每次操作原子。可在本机或共享文件系统上用,也是云端实现(数据库的条件更新、KV 的比较并交换)要满足的行为的参照。
  • StoredSession:pi 的 SessionManager(内存模式)加上写穿到存储:pi 每加一条记录就按顺序写出去,和 pi 写文件一样,第一条用户或模型消息之前什么也不写;按租约的三分之一续租。租约丢了以后,pi 再加记录会失败(和写文件失败一样),onLost 通知宿主停掉会话。

具体用哪种云存储还没定,所以 TaSessionFactory 还没有接入它:pi 的 AgentSessionRuntime 在新对话、分叉、切换会话时仍自己按文件创建 SessionManager,接入时要在这几处换成 StoredSession。

已知限制 ​

  • 没有可用的模型(模型服务没登录、没选模型)时,pi 的报错会提到终端里的 /login 和它的文档路径;这里改成 CELL_TA_NOT_SIGNED_IN,提示先用 TaAuthServer 登录。
  • 去重表、事件日志和待答对话框都在内存里:服务重启后对话框作废(扩展拿到默认值),客户端应先 getMessages 核对再决定是否重发。
  • 远程执行时,pi 的资源(AGENTS.md、项目 .ta/ 设置与 Skills)仍从宿主上同名路径读取;执行机上的项目资源要靠自定义 ResourceLoader(尚未实现)。