本文定义 baton 的稳定内核:少数核心概念 + 少数不变量 + 一条流水线 + 一份扩展契约。判据只有一条——新增一个 harness 默认只改
harness/<harness>/+harness/registry+harness/ids(+ 或许harness/adapter.ts中的新 capability 接口),不触碰 session / store-reduce / projection / chat-tui。改动若渗进内核,通常说明"有个概念还没一等化"(见 §7)。内核并非冻结:当一个特性被多个 harness 共同印证,它也会演进——但改内核比改 adapter 贵一个量级,门槛见 §6。内核之外的设计(产品定位、存储路径、外部会话纳管、@ 引用、里程碑)见
design.md;输入 / 输出 / 审批三轴的展开见user-input-lifecycle.md、harness-output-lifecycle.md、approval-lifecycle.md;Adapter 契约的完整条款见harness-interaction-design.md。
从最高层看,Baton 内核先收束为三个协作域。这是判断概念归属、依赖方向与新能力落点的 指导模型,不等同于当前物理目录,也不要求代码立即按三域重排:
| 域 | 负责什么 | 不负责什么 |
|---|---|---|
| Input | 抽象进入 Baton 的刺激;以 source 区分 user、monitor、external event 等来源,各来源再以 kind / mode 表达 prompt、control、interaction resolution、steer 等真实语义 |
不感知 Harness wire,不决定排队、重试或 Turn 生命周期 |
| Controller | 连接两侧并拥有 Baton 的协调状态:接收 Input,完成 admission、queue、Attempt、Turn / Interaction 收口、Event 持久化,并将 Projection 反馈给页面 / 用户 | 不实现 Harness 方言,也不把某个 UI 的展示形状下沉为核心规则 |
| Harness | 抽象执行侧:HarnessTarget、Adapter、Capability、原生 HarnessSession,以及 Harness 操作和输出事实的归一 | 不拥有用户输入生命周期,不直接向页面投影 |
依赖与数据流保持单向可解释:Input → Controller → Harness 表达控制,
Harness → Controller → Projection → 页面 / 用户 表达感知;Input 与 Harness 不直接依赖,
只有 Controller 同时理解两侧契约。store、event、queue、projection 等是支撑这条协调链路的
内核机制,不再各自升级成与三域平级的问题域。
当前主要来源是 user;未来出现 monitor 输入或外部事件时,先作为 Input 的新 source / 子类型;
只有其 owner、生命周期或不变量确实独立时,才提升为新的平级域。三域视角先作为演进方向约束
概念归属,代码结构按真实改动压力渐进收敛。
在三域之下,当前由以下内核对象承载具体语义。它们是域内对象,不再与 Input / Controller / Harness 平铺为同一层概念;每个对象仍绑定一条不能被 harness 差异侵蚀的不变量。
| 概念 | 语义 | 绑定的不变量 |
|---|---|---|
| BatonSession | 用户拥有的持久逻辑历史、跨 harness 的唯一时间线,也是 Plugin 数据与执行 owner | 身份锚点:历史与 Plugin 数据跟随 session;项目只按发起 cwd 组织 Session(跨项目 fork = 同一段逻辑历史落到另一 cwd + 全新 HarnessSession,Plugin Binding 不隐式复制) |
| Event(信封) | 最小 append-only 事实:稳定 eventId + 单一 scope + 必填 source + 归一 payload + 原始 wire raw;归属、来源与执行坐标正交,归因字段由可信宿主入口填写 |
事件流是感知的唯一真相源;UI / 崩溃恢复 / resume 全是它的 reduce/投影,无旁路通道 |
| Turn | 一段有始有终的 harness 活动(带 stopReason) | "谁发起"是属性(driven / observed),不是存在条件;每个被 admit 的 turn 恰好收口一次 |
| Interaction | Baton 持有的持久待决交互;kind 区分 permission / question / hook trust,requester 指明谁在等待 |
identity 与 opened/resolved 生命周期由 Controller 统一签发和收口;Adapter 只提交 kind-specific draft 并等待结果 |
| Delivery Attempt | Controller 域内向 Harness 投递一轮已 admit Input 的持久执行记录 | 先持久化 prepared 再 dispatch;accepted 只确认 Adapter 接受投递责任,Harness 终态才给出最终 outcome;无法证明是否接收或结束时保持 uncertain,不盲目重投 |
| Context delivery | Controller 域内把有 owner/key 的 ContextSource 组装成 Snapshot,并向某个 HarnessSession 交付 | Snapshot 说明准备送什么;只有 transport 接受后落下的 DeliveryReceipt 才推进该 HarnessSession 的 ContextEpoch,不能用 Board 已更新或本地已组装代替 |
| HarnessTarget | Baton 配置、调度与状态查询侧的一份具体 Harness 目标;可在不创建 HarnessSession 的前提下做只读 capability/catalog probe | 实例坐标与协议类型分离:Target ID 只经显式 resolver 解析,未知值 fail closed;Adapter 工厂接收完整 Target;Controller processing / queue、HarnessBinding、原生 session、同步水位、偏好 / 授权和 Target-scoped 投影状态均按 harnessTargetId 隔离,不按 Harness 名称混用;发现不借 Adapter.open 制造隐形 session |
| Adapter + Capability | harness 方言的唯一居所:小核心 HarnessAdapter + 可选能力 descriptor |
差异表达为"能力有无",type-guard 发现、契约测试钉住;内核永不 if harness=== |
| Projection | 纯函数:event reduce → chat-tui State | 只产展示数据;chat-tui 消费 State 不消费领域语义;未变返回同引用(快照一致) |
HarnessSession 不在此表——它是某 HarnessTarget 启动出的原生执行状态、内核的实现细节:
baton 优先用 harnessSessionId 加速恢复,但它缺失只降级、不能阻止 BatonSession 续聊。
每次 create/resume 使用不可变 HarnessLaunchSnapshot 记录当时的 target、cwd、model 和 effort;
快照解释既有执行,后续配置变化不能回写它。
ID 规则:Baton 签发的 event / interaction / context snapshot / context epoch / session /
turn / message / tool call / delivery attempt 等对象使用带前缀 ULID(ev_ / ix_ / ctx_ /
ctxe_ / bs_ / hs_ / t_ / m_ / tc_ / att_),从第一天起稳定、可外部引用;
HarnessTarget、PluginInstance 等配置对象使用
各自作用域内的稳定配置 ID。fork 复制的 turn / interaction / message / tool call 等领域对象
与源共享对象 ID(git-branch 语义);Event envelope 因进入新的 session scope 而重新签发
eventId,保证一个 event id 只属于一个权威 ledger。跨会话引用领域对象以
bs_ + 对象 ID 消歧(why 见 resume-fork.md)。
Baton 的并发边界按故障与所有权划分,不按页面区域划分:
Baton host process
└── main JS thread / event loop
├── stdin、键盘路由、焦点
├── chat-tui + React/OpenTUI render
├── Controller、SessionStore、Plugin Manager
└── IPC / async subprocess coordination
child processes
├── Plugin Runner × active Binding
└── Harness process × Adapter-owned execution
终端只有一条 stdin 和一个当前键盘焦点。composer、timeline、activity、footer、sidecar 是 surface 粒度的订阅和渲染边界:它们只订阅所需 ChatStore slice,避免无关 state 触发重绘; 但它们不各自拥有 TTY、焦点或 JS event loop。输入先由 chat-tui 在 host 主线程翻译成 intent, 再路由给 Baton。sidecar 不监听文本输入,也不能移动 composer 的 buffer。
进程和未来 Worker 的编排归 Baton,不归 chat-tui。chat-tui 是可复用视图层,只维护焦点、 输入缓冲、surface selector 和 render;它不能知道 Plugin、Harness、Session 锁或进程恢复。 Baton 当前应用层不为 surface 创建 Worker:跨 Worker 复制完整 React / ChatStore 状态会增加 一致性协议,却不能改变终端单焦点事实。OpenTUI 或依赖内部使用的 native thread 是实现细节, 不属于 Baton 契约。
host 主线程上的代码必须满足两条纪律:
- render、键盘 handler 和同步 getter 只读取内存快照,不做文件、Git、网络、Package import 或 Plugin 回调;
- I/O 使用 async API;可能执行三方代码、同步子进程或不可控模块初始化的工作进入独立进程。
因此 Marketplace Plugin 按活动 Binding 进入独立 Runner。同步死循环只阻塞该 Runner;
Supervisor 的 deadline 到期后终止进程,Manager 撤销 Binding。Harness 的进程或 SDK 生命周期
由对应 Adapter 持有;Git 等短命工具由所属进程使用异步 subprocess,并显式设置 timeout、
取消和输出上限。详细 Plugin 协议见 plugin.md。
Worker thread 只在未来出现可信、CPU 密集、可结构化传输、可取消的 Baton core 计算时引入, 例如大型纯投影。届时仍由 Baton 定义请求、deadline 和关闭协议;不能让某个 surface 私自创建 线程。三方 Plugin 默认继续用进程,因为 Worker 不是权限沙箱,且进程退出与资源回收边界更清楚。
关闭顺序由 owner 反向执行:停止接收新 intent → cancel/close Harness → 关闭 Plugin Binding 与 Runner → flush/release Session → destroy renderer。子进程意外退出必须转换成 Baton 可观察的 失败并释放注册;不能让一个失联 Promise 永久占住队列。
这是一套多进程、每进程单 JS event loop 的应用模型。0.2.0 的不兼容点是 Plugin 公共回调 Promise 化和三方 Package 进程化,不把“surface 独立重渲染”误称为“surface 独立线程”。
内核的正确性压在这三条上;违反任意一条,加 harness 就会渗进核心。
-
单通道真相:一切经
event → append → broadcast → reduce → projection。live 与 resume 是同一条 reduce 路径。不允许第二条投影通道(per-turn 回调曾是第二通道,导致 observed turn 的回复"只持久化、不投影",重开会话才可见)。自愈也走这条:合成的终态事件重新进appendEvent,不直接改 state。由tests/harness-initiated-turn.test.ts的参数化契约测试钉住。 -
终态封闭 + 悲观兜底:内部状态是封闭词表,adapter 在边界把 harness 的开放 / UNSTABLE 字符串归一进来;未知一律保守(未知终态 →
failed不是completed;未知 verdict → 不 finalize)。"悲观、绝不失声"是感知面的承重原则。 -
核心无 harness 分支:harness 差异只以 capability 有无出现在内核视野里。渲染层与存储层不出现 harness 分支;harness 私有形态留在信封
raw。归一是"最大公约数 + raw 保真":形状统一,粒度差异不掩盖。
内核只有一条流水线,双向流动。observed turn、stall 自愈、审批闭环都是它的特例,不是另起的机制。
开发次序:两个边界的形态先钉死,中间处理慢慢打磨。 先定死 Input 域的入站形态(当前 user 输入按 kind 区分 prompt / interaction resolution / control,未来可增加 monitor / external event source)和 Harness 域的 I/O 形态(harness→baton:归一 Event 或 Interaction draft;baton→harness:capability 操作与 Interaction resolution)。这两个边界一旦稳定,baton 的中间处理(Controller 调度、queue、reduce、projection)就能渐进重构而不惊动边界契约——接入方(chat-tui)与 harness(adapter)不被中间打磨波及。这也是内核纪律钉在边界(§5 扩展契约、§3 不变量)、而演进(§6)主要作用于中间与概念提升的原因。
两点要害:入站归一箭头标注的 driven + observed——Adapter → event 路径同时承载用户驱动与 harness 自发两种 turn,独立于是否有待决 Input(单通道真相,不变量 #1);Input 经 composer+queue 被调度成 turn,而 Interaction 在浮层被 resolve,就地解开等待方,不进入输入队列(见 §7)。
控制(出站) chat-tui intent
→ Controller(拥有 Input 生命周期,调度 driven turn)
→ Delivery Attempt(prepared → dispatching → accepted)
→ Adapter(sendTurn 归一 new turn / steer,并映射 cancel / approve)
→ harness wire
感知(入站) harness wire
→ Adapter 归一(→ 封闭词表,未知 fail-closed,保留 raw)
→ 宿主可信入口盖 source:harness + Harness + HarnessTarget
→ Event append → broadcast
→ reduce → Projection 快照
→ chat-tui 渲染
Turn 生命周期(内核心跳):
admit(Controller,driven turn):出队即落user_message(source:user)+state_update(running, source:baton)——用户输入是 BatonSession 的事实,不等 harness 冷启动;driven turn 全局串行、finalize 推进队列。observe(adapter,observed turn):harness 自发。adapter 在终态后的同一消息流上检测到新活动,铸新 turnId、以state_update(running, source:harness)开界、idle 收界;controller 只划界记账、投影,不进队列(它已在跑,调度它无意义、阻塞用户输入更是倒置)。全局串行约定据此收窄为:driven turn 全局串行,observed turn 与其正交。terminal(恰好一次):adapter 在任何退出路径(正常 / wire error / 子进程退出 / transport close)都必须报告或合成一次state_update(idle);错误路径先发_baton_error_update。重复 / 迟到的物理终态允许存在,controller 按 baton turn id 幂等 finalize。setup(harness 冷启动,turn 之外的活动窗口):HarnessBinding创建 → open 完成之间,adapter 可能阻塞征询用户(hook trust / 登录确认)、拉模型目录、失败退出。setup 不自成 turn——其间打开的 Interaction 一律归属触发冷启动的 driven turn(Controller 按交互生命周期统一补归属,不按 kind 特判);setup 期间 adapter 自行启动的资源(子进程、探测 query)由 adapter 负责清理——open 未返回 ref 前 controller 无从 close,失败路径不清理即泄漏。finalize:落 turn-summary、推进队列(仅 driven)。
自愈旁支(harness 静默悬挂时):stall 在事件流上被观测(L1,_baton_stall_notice)→ 若 adapter 声明 Reconcilable 则探权威快照(L2)→ 用修复事件结算被丢的 item 级终态 → 合成终态重新进同一条流水线。silence 是观察不是判决,权威探测应能 clear / refine 而非直接判死。
人工审批闭环(Interaction 的一个 kind):Adapter 提交 permission draft → Controller 签发 ix_、append interaction.opened(state → requires_action)→ 用户在 TUI 决策 → Controller append interaction.resolved 并解开 Adapter await → Adapter 回传 Harness。自动 reviewer 未向 Baton 打开 Interaction 时,ApprovalReview 是独立审计事实,不伪造 opened/resolved 配对。declined 是一等终态;委托状态对当前活跃 harness 可见。
上下文接力旁支:ContextSource(kind + owner + key) → 组装并持久化
ContextSnapshot → 通过 syncContext、sendTurn side-channel 或 prompt prepend 交付 →
transport 接受后持久化 ContextDeliveryReceipt → 从 Receipt 重放该 HarnessSession 的
ContextEpoch。meta.syncedSeq 只是兼容缓存;存在 Snapshot 但没有 Receipt 时水位不前进,
下次仍需补投。当前首个 source kind 是 BatonSession 的 session_history,Board / Plugin /
Resource 等来源在真实接入时增加 kind,不预造注册表。
HarnessAdapter 是内核唯一面向 harness 的接口(完整条款见 harness-interaction-design.md):
interface HarnessAdapter {
readonly harness: string;
readonly capabilities: AdapterCapabilities; // 可展示的能力 descriptor
open(opts, sink: EventSink): Promise<HarnessSessionRef>;
sendTurn(ref, input: PromptInput): Promise<SendTurnReceipt>; // adapter 决定 new_turn / steer / rejected
cancel(ref): Promise<void>;
close(ref): Promise<void>;
}MUST:
- 实现小核心
HarnessAdapter;把 wire 方言归一成 Event 草稿并保raw;adapter 不能自填source、Harness 或 HarnessTarget,宿主在接入边界按绑定关系统一补齐;未知终态按不变量 #2 保守收口。 sendTurnthrow 只表示 Adapter 尚未接受投递责任;accepted 后的任何失败都必须经事件流 给出 Harness 终态。Delivery Attempt 是 Controller 的记账,不进入 Adapter 输入契约。- 需要外部参与者时向宿主提交 typed
InteractionDraft并等待 resolution;不得自签interactionId,也不得自行 emitinteraction.opened/resolved。 - 可选能力(
Reconcilable/SessionConfigurable/NativeSessionCheckpointable/ …)声明即必须实现,由契约测试保证;不声明 = 优雅降级, 绝不是核心分支。 - 经
harness/registry(Harness 定义 + adapter 工厂)+harness/ids(无 SDK 身份目录:id + aliases)注册。
MUST NOT(默认边界;确需突破时走 §6 的演进门槛,不在此私自扩核心):
- 为单个 harness 的方言给 BatonSession / Turn / Event 核心加字段或分支;
- 开第二条投影通道;
- 让 harness 字符串越过 adapter 边界(封闭词表在此收口);
- 静默持有审批授权(必须产生可见、带 id 的回执)。
自检:新增 harness 的 diff 只落在 harness/<harness>/ + harness/registry.ts + harness/ids.ts(+ 或许 harness/adapter.ts 中的新 capability 接口)。一旦落进 session/、store/reduce、projection 语义或 chat-tui,先自问:"这是这一家的方言,还是 ≥2 家的共性?"——前者归 adapter/raw,后者才按 §6 慎重提升内核。
内核不是冻结的。BatonSession / Turn / Event 也会演进——但内核是所有 harness 与全部投影 / 存储的共同约束,改它比改一个 adapter 贵一个量级,因此要很慎重,有明确的门槛与方向。
判据:默认下沉,共性才上浮。
- 默认:单个 harness 的特性留在 adapter +
raw,或表达为一个 optional capability。一家有、别家没有的东西不进内核——否则内核长出只服务一家的字段,就退化成"harness 分支的联合体",§3 不变量 #3 名存实亡。 - 提升触发:同一特性在 ≥2 个 harness 上独立出现,说明它是这个问题域的普遍形状、而非某家方言——此时才把它归一进内核。cross-harness 证据是门槛,单家便利不是。
- 加法优先、语义封闭:优先新增事件类型 / Turn 属性 / capability,尽量不改既有
payload的既定含义。确需改变信封契约时递增 envelope version,明确迁移或不兼容边界,不能让两种语义共用同一版本。v3 以eventId + scope取代顶层batonSessionId,明确不兼容 v2 信封。能用 optional capability 表达的,就不进核心必选。
两个演进方向:
- capability 毕业:一个可选能力(如
Reconcilable)若被所有活跃 harness 支持、且成为交互刚需,可从"可选"升为"核心约定"。代价是新 harness 从此必须实现它、接入门槛随之抬高——所以非刚需不升。 - 概念提升:一个反复在投影 / 存储层打补丁的隐式概念,被确认为跨 harness 的普遍需求后,提升为一等内核概念。§7 列出各轴的一等概念,就是这条路径的落点。
每次内核改动回答三问:① 这是 ≥2 家的共性,还是一家的方言?② 能否用 optional capability 而非核心字段表达?③ 持久协议是保持兼容,还是以新 envelope version 明确切断?三问没有明确答案,就先留在 adapter 层。
内核在每条轴上都要求一个"一等"的承载对象:隐式或泄漏的概念会让局部修复反复打补丁、扩展被迫改核心。五条轴的一等概念与其绑定规则——
-
输入轴 · Input family——Input 域进入 Controller 的信号统一收束为
Input;先通过source区分 user / monitor / external event,再在 user source 内通过kind区分 prompt(模型可见内容,mode再区分 submit / steer)、interaction resolution(回答一个已打开交互,就地解阻)与 control(无模型可见内容,命令 Turn 生命周期,如interrupt)。其中 prompt Input 是一等持久概念(身份即其 messageId),消费状态可查,统一 draft / queued / admitted / steer / recall;缺了它,"Esc + 第二条待决意图"这类时序本质不可判定(见user-input-lifecycle.mdS3)。三类 variant 共享 Input→Controller 的入口契约,但保留各自生命周期,不强行共用 queue 或 Attempt。 -
交互轴 · Interaction——任何需要外部参与者给出结果后才能继续的阻塞协作,都使用同一个持久对象:
interactionId + requester + kind-specific payload。kind当前为 permission / question / hook_trust,后续 Plugin 授权或 elicitation 继续增加 kind,而不是再造 Request/Fact 名词。Controller 是 lifecycle owner:先interaction.opened,后且仅后一个interaction.resolved;cancel / timeout / recovery 也是 resolution。Event.source回答谁报告事实,Interaction.requester回答谁在等待,二者正交。自动 reviewer 没有向 Baton 打开 Interaction 时,ApprovalReview保持独立审计 Event。详见harness-interaction-design.md§3.5。 -
输出轴 · 封闭终态词表——harness 的开放 / UNSTABLE 终态在 adapter 边界经统一原语收口到内部闭集,未知一律保守回落(不变量 #2)。闭合值进入事件流后 reduce / 投影不再面对未知;原始值留在
raw。反面参照StopReason:有意保持开放(forward-compat 元数据)——turn 靠idle无条件收口、不依赖 reason 字符串,故无需封闭。判据是"未知会不会导致失声",不是"凡开放皆封闭"。 -
上下文轴 · ContextSource family——不同来源收束为同一个
ContextSource判别联合,以kind表达来源类型、owner + key表达稳定身份;来源组装结果是不可变 Snapshot,目标已知基线是从 DeliveryReceipt 重放出的 ContextEpoch。Board 更新、Snapshot 生成和 Harness 接受是三个事实,不能共用一个synced布尔值。当前只实现session_history,新来源优先增加 kind;只有来源自己的读取契约和生命周期真正独立时才增加子类 / adapter。 -
展示轴 · outcome 与 tone 双轴——展示态分两根正交轴:lifecycle/outcome(completed / failed / declined)与 tone/severity(warning…)。二者混进单一 union,会让"跑了但需留痕"与真实结果争用一个状态位、共用一个颜色 token,枚举随特性膨胀。此轴在 chat-tui 侧,是纯展示取舍。
design.md— 内核之外的完整设计(定位、问题域、架构、存储、纳管、@、里程碑)harness-interaction-design.md— Adapter 契约完整条款(生命周期 / 能力 descriptor / admission)user-input-lifecycle.md/harness-output-lifecycle.md/approval-lifecycle.md— 输入 / 输出 / 审批三轴展开resume-fork.md— resume/fork 语义(fork = 同一段逻辑历史的复制)、会话锁与 crash recovery