/** wire 帧(SSE data 载荷)。`lagged`(慢消费丢帧标记)与 `task_settled`(中途终态信号,复审 #3: * 三条 run 腿的 onTaskNotification 观察者顺手 publish——订阅期间子代 settle 不再只剩 heartbeat 永挂, * 路由收到即写帧收流)是本总线自己的帧(非 core 事件)。`task_settled` 自 core 1.373 起携可选 * `seq`(settle 铸的 stop-cycle 世代号,TaskNotificationPayload.seq 透传)——流上实时拿到「第 N 世代 * 终结」,与 meta 帧的 seq(durable 终态跨实例 serve 时在场)成套。 */ export type SubagentTailFrame = Record & { type: string; }; /** core forward 事件 → wire 投影(sync 主流五分支同源;不认识的帧类型=null 不发——tail 面只承诺 * content 六类,新 core 帧类型先经这里显式收编再出 wire)。 * ⚠️ **本函数是无状态逐帧投影**:`reasoning_end`(S-310 的推理权威段)不在这里 —— 它要按句柄攒段, * 住在 {@link SubagentTailBus.onForwardFrame}(publish 侧是唯一知道句柄的地方)。 */ export declare function projectTailFrame(e: Record & { type?: string; }): SubagentTailFrame | null; /** * 宿主腿的 **content 供给形**([ref] / [ref] 定谳)——本面能不能拿到内容帧,由**宿主 run 的 spec** * `forwardSubagentEvents` 决定,而不是由订阅者决定: * * · `"on"` = 宿主 run 提交时带了 `forwardSubagentEvents:true` ⇒ core 的 forward 门 * (`prepare-task` 的 `spec.forwardSubagentEvents === true` 分支)放行子代的 * text_delta/reasoning_delta/tool_start/tool_end ⇒ 本面有内容帧; * · `"progress_only"` = 宿主 run 没带 ⇒ 那道门**只**放 task_progress ⇒ 本面恒只有 rollup 帧。 * * 🔴 为什么必须上 wire([ref]「不许静默 fail-open」在本接缝的落实):`progress_only` 形下本端点 * **一切正常**——200、meta 说 running、心跳照发、task_progress 照来——就是永远没有内容帧。消费端 * (cli engineSubagentTail 明文「task_progress 不重复消费」)看到的是一个永远空的转录面,与「子代此刻 * 恰好没说话」在 wire 上**不可区分**;而订阅方在查看态**没有任何办法**把它打开(那是 run 提交时的 * 一次性表态)。[ref] cli 一手取证的指纹(建连成功 / 75s 零内容帧 / 同期 fleet token 在涨)与本形 * 逐条吻合。所以 meta 帧如实声明供给形,让「空」有诚实的来路。 * * 缺席(`undefined` ⇒ meta 出 `"unknown"`)= **还不知道**:子代还没产出第一帧,或宿主腿在别的副本 * (跨实例 serve 的 durable 行)——不猜、不折默认值。 */ export type TailContentMode = "on" | "progress_only"; export declare class SubagentTailBus { private readonly subs; /** * a\* 句柄 → 该句柄**自己的宿主腿**的供给形。 * * 🔴 键必须是**句柄**,不是「查看时用的那条 run」(codex 复审 high,2026-08-14 采纳): * session-scoped 子代(`backgroundScope:"session"`)活得比派它的那条 run 久,而订阅方寻址用的是 * 「派它的 run ?? 当前活跃 run」——同一会话里 A 腿(带 forwardSubagentEvents)派的子代,完全可能 * 被经**后起的 B 腿**(没带)去看。按 run/会话键答题在这一档会**确定性谎报**(A=on 被答成 * progress_only ⇒ 客户端提前放弃真内容;反向 ⇒ 永久空等)。按句柄登记则天然正确:登记发生在 * **产出这一帧的那条闭包**里,帧上的 bgAgentId 与它的宿主腿是同一件事,不经任何寻址推断。 * * 时间轴与降级:子代产出第一帧之前 = 无登记 ⇒ 路由出 `"unknown"`(诚实:还不知道);宿主腿在别的 * 副本 ⇒ 本副本永远收不到它的帧 ⇒ 同样 `"unknown"`(而不是拿本地同会话的无关腿冒充);容量逐出 * 同理只会把 `"on"/"progress_only"` 退成 `"unknown"`,不会退成错答案。 * ⚠️ `progress_only` 形下 task_progress **照来**,所以那一档同样在子代首拍 tick 就登记上了—— * 不需要"先有内容帧"才能学到"没有内容帧"。 */ private readonly modeByHandle; /** * 句柄 → 该句柄这条 tail 流上**在攒的推理段**(S-310)。 * * 键必须是句柄,理由与 {@link modeByHandle} 同源再加一条硬的:两个并发子代的 `reasoning_delta` 在 * **同一个** forward sink 上交错到达,一只累积器会把两份思考拼成一段、再盖上其中一个的身份键。 * * 生命期 = **有人在听的那段时间**:攒段住在 `hasSubscribers` 快门之内(tail 是 live-only 面,没人听 * 就不投影也不攒),最后一个订阅者走掉或句柄终态({@link forgetHandle})时整条删掉 ⇒ 本表恒被 * 订阅者数量所界,不需要第二道容量闸。 */ private readonly segByHandle; /** 三条 run 腿的 forward 回调里逐帧调(在 `hasSubscribers` 快门**之前**——没人在听时也要学, * 否则晚开的 tail 永远读不到供给形)。同值重复写是 no-op(热路径只付一次 Map.get)。 */ noteHandleContentMode(bgAgentId: string, mode: TailContentMode): void; /** * 🔴 **三条 run 腿的 forward 回调唯一入口**(S-310 收口)——登记供给形 → 段界收段 → 逐帧投影 → publish。 * * 此前这四步以**同一段六行代码**抄在三条腿上(sync 流腿、verify-resume 腿、resume-stream 腿), * 差别为零;S-310 要给 tail 面补 `reasoning_end` 时,那就是第四、第五、第六份拷贝。收成一扇门之后 * 腿上只剩「句柄在不在」这一句判断,三腿不可能再分家(`test/subagent-tail-stream.test.ts` G18-G20)。 * * 顺序即语义:权威段**先于**触发它的结构帧上 wire(消费端「整段替换已攒 delta」的替换点不与卡片交错), * 与账本腿「文本行先于结构行」逐字同构。 */ onForwardFrame(bgAgentId: string, mode: TailContentMode, e: Record & { type?: string; }): void; /** * 句柄终态时**撤销**登记(三条腿的 `onTaskNotification` 观察者调,与 `task_settled` 帧同处)。 * * 🔴 为什么必须撤(codex 复审 round2 high,2026-08-14 采纳):后台子代可以用**原 a\* 句柄**进入下一个 * 复活周期(`POST /v1/runs/:id/subagents/:target/resume` → core `registerBackgroundAgent({ id: * reviveRow.handle })`),而新周期的宿主腿是**另一条 run**,它的 forwardSubagentEvents 完全可能相反。 * 不撤的话,新周期首帧之前 meta 会把**上一周期**的供给形当成本周期的答案:上周期 on / 本周期 * progress_only ⇒ 客户端等一个永远不来的内容;反向 ⇒ 提前放弃真内容。撤到 `"unknown"` 是唯一诚实的 * 中间态(新周期首帧一到自然重新登记)。 * 非 a\* 句柄(bash / monitor 那些从未登记过的句柄)进来是 no-op。 * * 残余(如实记,不假装没有):撤销之后若还有**同周期的迟到帧**进来,会按那条腿的模式重新登记一次 * ——值仍是「刚结束那个周期」的真话,只是万一紧接着发生复活、且新周期首帧未到,那一瞬读到的是旧周期 * 的答案。core 今天把终态通知排在最后一条 forward 之后(fleet-bus 同注),复活又是人手动作,这条窗 * 实际上不可达;真要关死需要给句柄加墓碑态,代价大于一位诊断标签的收益,故不做。 */ forgetHandle(bgAgentId: string): void; /** 路由读(按 tail 的 target 句柄)。缺席 = 不知道,路由出 `"unknown"`。 */ handleContentMode(bgAgentId: string): TailContentMode | undefined; /** 两条 run 腿的 fwdInternals 调用点:帧已投影(projectTailFrame),null 不进来。 */ publish(bgAgentId: string, frame: SubagentTailFrame): void; /** 有无订阅者(publish 侧的免投影快门:没人听就不投影)。 */ hasSubscribers(bgAgentId: string): boolean; subscribe(bgAgentId: string): AsyncIterableIterator; } /** 进程单例(defaultTaskRegistry 同款姿势):bus 语义本就是进程内 replica-local,两条 run 腿与路由 * 直接 import,不穿长参数列表;测试自 new 独立实例。 */ export declare const defaultSubagentTailBus: SubagentTailBus; //# sourceMappingURL=subagent-tail-bus.d.ts.map