Steer and follow-up image delivery
steer 与 follow-up 的图片投递
Images submitted while an agent is running did not reliably reach the model context or retain their intended browser placement (#3186), for three addressed reasons and one deferred agent-loop race. First, a steer or follow-up spliced into a live driver latched no wake: the live driver was expected to claim it, but a turn that finished or failed between the splice and the claim exited without re-checking, stranding th
English
Problem
Images submitted while an agent is running did not reliably reach the model context or retain their intended browser placement (#3186), for three addressed reasons and one deferred agent-loop race.
First, a steer or follow-up spliced into a live driver latched no wake: the live driver was expected to claim it, but a turn that finished or failed between the splice and the claim exited without re-checking, stranding the accepted message until an unrelated waking send. Image admission widens this window because the Host awaits attachment normalization before agent.steer()/agent.followup() runs.
Second, continuable-subagent follow-ups rejected images in the Client (SUBAGENT_IMAGE_UNSUPPORTED) before any RPC, and stripped image parts from the text-only call. The Host route had no admission at all, and its wire content was ContentBlock[], so lifting the Client rejection alone would have let a browser cite any attachmentId it never uploaded.
Third, the browser queue projection reduced a queued image to the text [image] even though the durable reference was already present and readable through the session attachment authorization.
Fourth, every local submission echo rendered at the Chat flow tail while the browser serialized image bytes. A direct steer therefore appeared as an ordinary chat message during the pre-admission wait, then moved to the pending-steering position when the Host queue snapshot arrived. Busy Queue sends had the same transition into QueueDock.
Decision
Host-side subagent image admission. SubagentPromptRequest.content is now upload-shaped PromptContentPart[] (updating the wire contract in Web subagent conversations). dsh-attachment owns the shared upload vocabulary and the admitPromptContent() conversion used by both the Session prompt endpoint and SubagentRuntime.prompt; Session Controller's shared request types retain a structurally identical Client wire declaration so the generated Client Cordis catalog contains the complete prompt-part fields, with a compile-time equality test preventing drift. The subagent route admits and persists image batches through ctx.attachments before followup(), and the continuation manager refuses delivery inside the per-child lock when the child's agent.options route resolves to a model without image input (MODEL_DOES_NOT_SUPPORT_IMAGES, surfaced as subagent/attachment-invalid with the same reason vocabulary as the Session route). A child without a fixed options route, or a deployment without the LLM registry, delivers and relies on the LLM layer's text-only projection. The Client forwards image parts unchanged and the SUBAGENT_IMAGE_UNSUPPORTED copy is gone.
Queue presentation. The queue mirror's text preview excludes image blocks, and the queue dock renders each durable image part as a thumbnail resolved through ctx.uiConversation.imageUrl — the same session-authorized read the transcript uses. Editing queued image messages stays refused (#3072).
Stable optimistic placement. Session derives a PendingSubmission placement synchronously from its running state and the requested delivery mode: transcript for an idle send, queued for a busy Queue send, and steering for a busy Steer send. The captured placement remains stable while serialization is in flight. Chat renders transcript and steering echoes on their respective surfaces, while QueueDock renders queued echoes with browser-owned image previews. The existing rpcId correlation suppresses the local echo in the same render that introduces the Host queue occurrence or durable user node. If the turn closes while images serialize and the Host places a requested steer in the next-turn queue, the later move from steering to QueueDock reflects the authoritative delivery decision.
Alternatives considered
Keep the wire content ContentBlock[] and admit refs on the Host. Rejected: a reference-shaped wire lets a Client fabricate attachmentId citations; an upload-shaped wire makes Host admission the only way an attachment reference can exist in a child message.
Check child image capability in SubagentRuntime.prompt. Rejected: the route may address a cold child whose agent does not exist yet; the continuation manager sees the live or freshly materialized agent in both arms and inside the per-child delivery lock, so the check cannot race a concurrent delivery.
Testing
Host tests cover mode: 'steer' image admission; subagent control tests cover ordered admission, batch refusal, non-canonical base64, and the capability refusal mapping; continuation tests cover refusal without a partial message, capable delivery, and the routeless deferral. Client tests cover unstripped forwarding, the catalog-visible upload declaration, queue thumbnails (load, failure placeholder, unmount), the image-free preview, Session-owned placement derivation and capture, local steering presentation, queued echo presentation, and rpcId handoff on both surfaces.
Deferred
A steer or follow-up inserted after a running driver's final inbox check and before it becomes idle can remain pending until another waking send starts the driver. Image admission performs asynchronous work before insertion, so image submissions can reach this timing window more often. This change leaves the agent-loop lifecycle unchanged; the wake race requires a separate lifecycle change and review.
Consequences
Slow image serialization leaves optimistic messages on their selected transcript, QueueDock, or pending-steering surface until the Host handoff. The subagent package depends on dsh-attachment and reads ctx.llm optionally. Images persisted by a batch whose delivery is later refused stay as unreachable content-addressed objects under the existing retention rules. Queue thumbnails add one authorized attachment read per queued image, shared with the transcript cache. The deferred closing-turn race can leave an accepted message pending as described above.
中文
Problem
agent 运行期间提交的图片没有可靠进入模型上下文,也没有保持预期的浏览器显示位置(#3186)。本次处理了其中三个原因,延后处理一个 agent-loop 竞态。
第一,splice 进在线 driver 的 steer 或 follow-up 不会锁存唤醒:预期由在线 driver 自行认领,但轮次在 splice 与认领之间正常结束或失败时,退出路径不再复查,已接受的消息就滞留到下一次无关的唤醒发送。图片准入放大了这个窗口,因为 Host 在执行 agent.steer()/agent.followup() 之前要先等待附件规范化完成。
第二,可继续子代理的 follow-up 在客户端就拒绝图片(SUBAGENT_IMAGE_UNSUPPORTED),并把图片部分从纯文本调用中剥掉。Host 路由完全没有准入,wire 内容又是 ContentBlock[],单独放开客户端拒绝会允许浏览器引用任何它从未上传过的 attachmentId。
第三,浏览器队列投影把已排队的图片折叠成文本 [image],尽管持久化引用已经存在,并且可以通过会话附件授权读取。
第四,浏览器序列化图片字节期间,所有本地提交回显都位于 Chat 消息流末尾。直接 steer 会在准入前等待阶段显示为普通聊天消息,Host queue snapshot 到达后才移到 pending-steering 位置。繁忙时 Queue 发送也会发生同类跳动,最终进入 QueueDock。
Decision
Host 侧子代理图片准入。 SubagentPromptRequest.content 改为上传形态的 PromptContentPart[](同步更新 Web 子代理会话 的 wire 契约)。dsh-attachment 负责共享上传词汇,以及 Session prompt 端点与 SubagentRuntime.prompt 共用的 admitPromptContent() 转换;Session Controller 的共享请求类型保留结构相同的 Client wire 声明,使生成的 Client Cordis 目录包含完整的 prompt part 字段,并用编译期等价测试防止两处定义偏离。子代理路由在 followup() 之前经 ctx.attachments 完成整批图片的准入与持久化;continuation 管理器在逐子级锁内,当子级 agent.options 路由解析到不接受图片输入的模型时拒绝投递(MODEL_DOES_NOT_SUPPORT_IMAGES,以与 Session 路由一致的 subagent/attachment-invalid 词汇表上抛)。子级没有固定 options 路由,或部署未挂载 LLM 注册表时照常投递,交给 LLM 层的纯文本投影。客户端原样转发图片部分,SUBAGENT_IMAGE_UNSUPPORTED 文案删除。
队列展示。 队列镜像的文本预览不再包含图片块,queue dock 把每个持久化图片部分渲染为缩略图,经 ctx.uiConversation.imageUrl 解析,与会话记录使用同一个会话授权读取。已排队图片消息的编辑仍然拒绝(#3072)。
稳定的乐观显示位置。 Session 根据运行状态和请求的投递模式同步推导 PendingSubmission 位置:空闲发送是 transcript,繁忙时 Queue 发送是 queued,繁忙时 Steer 发送是 steering。该位置在序列化期间保持不变。Chat 分别在 transcript 与 steering 区域渲染对应回显,QueueDock 用浏览器持有的图片预览渲染 queued 回显。现有 rpcId 关联会在 Host queue occurrence 或持久化 user node 出现的同一次渲染中隐藏本地回显。如果图片序列化期间轮次关闭,Host 把请求的 steer 放入 next-turn queue,消息随后从 steering 移到 QueueDock,反映实际投递决定。
Alternatives considered
wire 内容保持 ContentBlock[],由 Host 准入引用。 拒绝:引用形态的 wire 允许客户端伪造 attachmentId;上传形态的 wire 使 Host 准入成为子级消息里附件引用的唯一来源。
在 SubagentRuntime.prompt 里做子级图片能力检查。 拒绝:该路由可能寻址冷的子级,其 agent 尚不存在;continuation 管理器在两条分支里都拿得到在线或刚物化的 agent,并且处于逐子级投递锁内,检查不会与并发投递竞态。
Testing
Host 测试覆盖 mode: 'steer' 的图片准入;subagent control 测试覆盖有序准入、整批拒绝、非规范 base64 与能力拒绝映射;continuation 测试覆盖拒绝时不留半条消息、能力通过时投递、无路由时的顺延。客户端测试覆盖不剥离的转发、目录可见的上传声明、队列缩略图(加载、失败占位、卸载)、无图片占位的预览、Session 负责的位置推导与捕获、steering 本地显示、queued 回显显示,以及两个区域的 rpcId 交接。
Deferred
如果 steer 或 follow-up 在运行中 driver 最后一次检查 inbox 之后、转为 idle 之前插入,消息可能保持 pending,直到另一条唤醒消息重新启动 driver。图片准入会在插入前执行异步工作,因此图片提交更容易落入这个时序窗口。本次变更不修改 agent-loop 生命周期;该唤醒竞态需要单独的生命周期变更与审查。
Consequences
图片序列化较慢时,乐观消息停留在选定的 transcript、QueueDock 或 pending-steering 区域,直到与 Host 状态交接。subagent 包依赖 dsh-attachment,并可选读取 ctx.llm。整批持久化后投递被拒绝的图片按现有保留规则保持为不可达的内容寻址对象。队列缩略图对每张排队图片增加一次授权附件读取,与会话记录缓存共享。上述延后处理的轮次收尾竞态可能使已接受的消息保持 pending。