Joye Dev

Back

Voyager 业务线学习:Agent 图片资产归一化、任务门禁与画布交付

这条业务线解决的是一个跨运行时的用户流程:用户把图片上传到 Agent chat,然后要求生成参考图、去背、编辑,或把图片放到画布上。聊天附件最初只是 chat-scoped 的 R2 对象,图片任务和 Design 节点却都要求 durable workspace asset id;Design 节点最终保存的…

source_automation: voyager-merged-pr run_date: 2026-08-04 anchor_pr_number: 5520 pr_number: 5520 pr_title: “Support chat uploads in image tools via attachment promotion” pr_url: https://github.com/adastralab-ai/voyager/pull/5520 author: “Jason Tan / banchichen” merged_at: “2026-08-04T04:48:17Z” modules:

  • backend/workers/agent/src/tools/attachmentAssets.ts
  • backend/workers/agent/src/tools/imageTarget.ts
  • backend/workers/agent/src/tools/insertImageNodeTool.ts
  • backend/workers/agent/src/tools/updateImageNodeTool.ts
  • backend/workers/agent/src/tools/imageTaskTool.ts
  • backend/workers/agent/src/tools/imageDelivery.ts
  • backend/go/internal/image/service.go files_changed: 7 changed_files:
  • backend/workers/agent/src/tools/imageTarget.ts
  • backend/workers/agent/src/tools/index.ts
  • backend/workers/agent/src/tools/insertImageNodeTool.test.ts
  • backend/workers/agent/src/tools/insertImageNodeTool.ts
  • backend/workers/agent/src/tools/toolDefs.ts
  • backend/workers/agent/src/tools/updateImageNodeTool.test.ts
  • backend/workers/agent/src/tools/updateImageNodeTool.ts learning_tags:
  • agent-runtime
  • image-task
  • attachment-promotion
  • workspace-asset
  • canvas-delivery
  • side-effect-ordering
  • idempotency
  • concurrent-deduplication
  • api-error-contract
  • billing-safety
  • user-edit-preservation
  • test-strategy business_line: “Agent 图片输入资产归一化、任务门禁与画布交付可靠性” related_prs: [5422, 5506, 5507, 5519, 5530] related_prior_prs: [5422, 5506, 5507, 5519, 5530] line_stage: “统一 runtime delivery seam (#5422) -> remove_background 接入 attachment promotion (#5506) -> source asset 终态错误 (#5507) -> delivery outcome 暴露给模型 (#5530) -> reference asset 在 task.Submit 前校验 (#5519) -> insert/update image node 的 upload promotion 与副作用顺序 (#5520)” open_questions:
  • “attachmentAssetStore.set 位于 uploadAsset 之后,跨重启没有原子提交证明;upload 成功但 set 失败时,重试仍可能铸造第二个 workspace asset。”
  • “deliveredPages 与 page delta 都是 turn-local / append-only;同一 turn 的并发 insert/update 仍可能在读写之间同时通过 delivered-once 检查。”
  • “insert/update 对虚构、删除或跨 workspace 的 asset id 仍可能把 Go getAsset 错误送入捕获路径;#5520 没有统一 placement 侧的 terminal asset_not_found 协议。”
  • “chat upload 只接受 PNG、JPEG、WebP;SVG、GIF、AVIF 或附件丢失时的模型提示和产品降级仍需要真实用户流程验证。”
  • “#5519 的 asset_not_found 映射依赖 Go 返回 code 102 且保持裸 APIError;未来新增同路径的 102 资源错误时,Agent 可能把它误判为可纠正的图片 id。”
  • “当前验证主要是 worker 单测、Go contract test 和一次手工 upload placement;尚无 R2 -> uploadAsset -> Go task -> result -> browser canvas 的部署级端到端回归。” feishu_doc_url: null github_repository: joyehuang/ai-agent-field-notes github_path: outputs/voyager-daily-pr-study/2026-08-04-pr-5520-agent-image-asset-promotion-delivery.md voyager_merge_commit: 81faf2802550022b3f96a860101da3d364f8d466 voyager_origin_main_snapshot: 81faf2802550022b3f96a860101da3d364f8d466

业务线概览#

这条业务线解决的是一个跨运行时的用户流程:用户把图片上传到 Agent chat,然后要求生成参考图、去背、编辑,或把图片放到画布上。聊天附件最初只是 chat-scoped 的 R2 对象,图片任务和 Design 节点却都要求 durable workspace asset id;Design 节点最终保存的是 fill.assetId,不能直接解析 /mnt/attachments 下的 id。

因此问题不只是“工具参数接受另一种 id”。如果原始 upload id 直接送到 Go API,会得到 asset not found;如果在拒绝请求前先提升附件或创建 canvas,又会留下已付费但无交付、孤儿 asset 或空白 page。图片任务还要处理 resumable task、credits、reference asset、slot、undo 和模型对交付结果的认知。

本次锚点 #5520 补上的是最后一段输入与画布边界:insert_image_node 和 update_image_node 也复用 turn-level attachment promoter,并把“查找目标、提升资产、检查媒体、创建 canvas、写 delta”拆成可观察的顺序。前置/并行 PR 则把同一条线的服务端预检、任务终态和 runtime delivery seam 补齐。

核心结论:外部 id 在边界处归一化,所有不可逆副作用都放到最后一个拒绝点之后;提交和交付分别返回结构化终态,并用 turn-local 账本避免同一产物被重复放置。 这让“图片任务成功”与“图片已经在用户画布上”成为两个可验证的事实,而不是一个默认假设。

今日锚点#

项目内容
PR#5520 Support chat uploads in image tools via attachment promotion
作者Jason Tan / banchichen
合并时间2026-08-04 14:48:17(Australia/Melbourne,04:48:17Z)
Voyager merge commit81faf2802550022b3f96a860101da3d364f8d466
改动范围Agent worker 的 target resolution、insert/update image node、共享 promoter wiring、两组行为测试
选择理由#5520 是 Melbourne 当天尚未作为锚点学习、且位于当前 origin/main 顶部的跨边界工程 PR。它能用同一批真实模块自然串起 #5506 的 attachment promotion、#5507/#5519 的错误与计费门禁、#5422/#5530 的 canvas delivery;不是把相似标题拼成一组。

代码事实与推断分开看:代码可以证明 upload id 被提升、媒体和 target rejection 在 canvas 创建前发生、reference asset 在 task.Submit 前解析、delivery outcome 进入 tool output;它还不能证明跨重启的 asset promotion 原子性、跨 tab 的去重或真实部署链路已经成立。

演进时间线#

阶段PR 与真实代码变化改变的层新边界
统一图片交付 seam#5422,2026-07-30,merge 3a83e6c37a9df42a70bbfa3ac545baf9f5860d0dAgent worker imageDelivery / imageTarget / image toolswithImageDelivery 在任务前占 slot、任务成功后填 slot;source image 在页面上时采用原节点,否则新增节点。写入只 patch fill,保留用户移动、crop、flip 和 adjustments。
去背接受 chat upload#5506,2026-08-02,merge 4bc0f81895418f228d75b565e4e211bb0d53c3f2remove_background tool / tool registry去背在构造 idempotency key 和 slot 之前调用共享 promoteAttachment;同一上传因此使用 durable asset id,并能复用已有 resumable task。
缺失 source 变成可纠正终态#5507,2026-08-03,merge 5d8c482006df6b21535c76653f516115946e3446Go API adapter / imageTaskToolGo code 102 被映射为 source_asset_not_found,不再 throw;模型得到“不要重发同一个 id”的结果,不创建 task、不轮询。PR 明确记录了 reference id 仍会因 wrapped error 变成 500。
交付结果进入模型上下文#5530,2026-08-03,merge 1550afd96d58f0dd8cfb7a77332b9d623563d8a2imageDelivery / imageTaskTool / output schemaplaceImageInSlot 返回 placed、failed 或 withdrawn;失败时 tool output 指向同一个 assetId 的补放置动作,避免模型把“任务完成”误说成“画布已更新”。
reference 在计费前解析#5519,2026-08-04,merge 269e0788f9c9322dbe49ba7792b7c40ce5509973Go image service / contract test / Agent result unionsubmitImageTask 和 resubmitTask 在 task.Submit 前解析 extra asset refs,保留裸 *APIError 让 404/102 通过 middleware;未知 reference 不再创建 task、debit、failed/refund 再返回 500。
今日锚点:placement 输入归一化#5520,2026-08-04,merge 81faf2802550022b3f96a860101da3d364f8d466insert/update image node / target lifecycle / shared promoterresolveImageTarget 改为无副作用的 ready / creatable / unavailable;promotion 和媒体检查通过后才创建 canvas。update path 在 upload promotion 后重读节点,并把已放置的 promoted asset 写入跨工具共享的 deliveredPages。

这条时间线的关键不是功能数量,而是责任边界逐层收敛:#5422 先统一“谁负责把结果写到画布”,#5506/#5520 再把 chat-scoped 输入变成 page 可解析的 workspace asset,#5507/#5519 把错误从异常和付费副作用中分离,#5530 最后让模型看到交付是否真的发生。

当前架构与数据流#

flowchart LR
  U["Chat upload"] --> A["attachmentsById + attachment R2"]
  A --> P["turn-level promoteAttachment"]
  P -->|store hit| ID["workspace assetId"]
  P -->|R2 read + uploadAsset| ID
  ID --> T["image_generation / edit / remove_background"]
  ID --> N["insert_image_node / update_image_node"]
  T --> S["Go submitImageTask"]
  S --> V["source + entitlement + reference preflight"]
  V --> Q["task row + credit debit + queue publish"]
  Q --> W["task worker / result asset"]
  W --> D["withImageDelivery"]
  D --> O["placed / failed / withdrawn"]
  N --> R["resolve target -> media check -> page delta"]
  R --> L["Design fill.assetId + deliveredPages"]
  O --> M["model-facing tool output"]
plaintext

1. 输入归一化与 turn 级共享状态#

buildAgentTools 在 backend/workers/agent/src/tools/index.ts:219-246 为一轮 Agent tool calls 创建一个 ImageDelivery context 和一个 createAttachmentPromoter。同一个 promoter 被传给 image_generation、cutout_generation、remove_background、insert_image_node 和 update_image_node(index.ts:356-403)。

attachmentAssets.ts:80-117 先从模型可能传入的 /mnt/attachments/、extension-less stem 或裸 id 中解析 chat attachment。已有 attachmentAssetStore 命中时直接复用;否则从 workspace/chat/attachment key 读 R2,调用 Go uploadAsset,再把 attachment id 映射到 workspace asset id。inFlight Map 让同一 turn 内并发 tool call 共用一个 promotion promise;跨 turn 的去重由 store 承担。

这个函数有一个重要的类型约定:raw id 没有对应 attachment 时返回 undefined,调用方把它解释为“本来就是 workspace asset id”。因此 workspace asset 的常见路径不产生上传 I/O,而 upload 的下游每一层都只使用 promoted id。

2. image task 路径:可恢复执行与 billing-safe preflight#

image_generation 的 resolveImageRefs(imageGenerationTool.ts:124-209)在调用形状判定后提升 refs,并保持数组顺序;runGeneration 和 runSourceOp 用 request 内容生成 idempotency key,然后交给 withImageDelivery。

任务提交进入 Go backend/go/internal/image/service.go:80-162:

  1. workspace membership、source asset、upscale room 和 entitlement 先校验;
  2. #5519 新增的 spec.Decode 与 resolveExtraSourceRefs 在 task.Submit 前执行;
  3. 只有这些拒绝点都通过,task.Submit 才同时创建 task row 和 credit debit;
  4. post-commit publish closure 只负责签 result/source URL、组 queue envelope 和发布任务,不再承担可能返回客户端 404 的 reference lookup。

Agent 侧 goApi.ts:662-737 把 billing gate、resolution gate、asset-not-found 都变成 SubmitImageTaskResult。runResumableImageTask(imageTaskTool.ts:156-253)遇到非 submitted 的 terminal gate 时直接返回失败结果,不写 pending record、不 poll;真正的 poll/SDK transient error 才 throw 并保留可重挂接的 task record。

3. runtime delivery 与 canvas 写入#

withImageDelivery(imageDelivery.ts:307-345)先尝试 hold source node 或 reserve slot,再运行任务;结果成功且有 assetId 时执行 placeImageInSlot,否则把 slot 标成 failed。#5530 扩展它的返回值,把 placement outcome 与 result asset 一起进入 tool output。

对于 #5520 的显式 placement:

  1. resolveImageTarget 只读取页面或返回 fileId,不创建任何东西;
  2. insert_image_node 调用 promoter,再 getAsset 检查类型;只有 source 确认是 image 后才调用 createImageTarget 和 submitPageDelta;
  3. update_image_node 先检查 page/node/no-op,再 promotion;upload promotion 会触发第二次 page read,避免用户在上传期间改动的 crop、flip 或 adjustments 被旧 snapshot 覆盖;
  4. promoted asset 写入 deliveredPages,跨 page 和跨 image tool 的重复交付 guard 使用同一个 id;手工传入的 workspace asset 不写入该 map,所以重复使用已有资产仍是用户明确要求的行为。

关键代码#

1. #5520 把“找目标”和“创建目标”拆开#

来源:PR #5520 diff,backend/workers/agent/src/tools/imageTarget.ts:73-122。

export type ImageTargetResolution =
  | { kind: "ready"; target: ImageTarget }
  | { kind: "creatable"; fileId: string }
  | { kind: "unavailable"; message: string };

// resolveImageTarget only reads; createImageTarget is called later.
plaintext

这是一个副作用顺序的显式类型。没有 file 时直接返回 unavailable;有 file 但没有 page 时只返回 creatable。这样 insert_image_node 可以先完成 promotion 和 getAsset 类型检查,再创建 canvas,避免“source 不是图片但已经创建空 page”以及“没有可放置目标却已经铸造 workspace asset”。

2. #5520 的 insert path 让每个拒绝点先于副作用#

来源:PR #5520 diff,backend/workers/agent/src/tools/insertImageNodeTool.ts:50-101。

const resolution = await resolveImageTarget(go, delivery.context, pageId, abortSignal);
if (resolution.kind === "unavailable") return noPlacement(resolution.message);

const assetId = (await promoteAttachment(sourceId, abortSignal)) ?? sourceId;
const asset = await getAsset(go, { assetId, signal: abortSignal });
if (asset.type.toLowerCase() !== "image") return noPlacement("not an image");

const target = resolution.kind === "ready"
  ? resolution.target
  : await createImageTarget(go, delivery.context, resolution.fileId, abortSignal);
plaintext

代码事实是顺序而不是注释:无 file、非 image source 都不会触发 addPage;后续节点的 fill、deliveredPages 和 delta 全部使用同一个 promoted assetId。这个顺序也把“模型传入一个合法但不可放置的 id”与“系统可以创建画布”分开。

3. #5520 的 update path 保留用户在上传期间的编辑#

来源:PR #5520 diff,backend/workers/agent/src/tools/updateImageNodeTool.ts:69-115,177-183。

if (promotedAssetId) {
  loaded = await loadImageNode(go, pageId, nodeId, abortSignal);
}

const deliveredTo = assetId
  ? delivery.context.deliveredPages.get(assetId)
  : undefined;
if (deliveredTo !== undefined && deliveredTo !== pageId) return noPlacement(...);

await submitPageDelta(go, { pageId, signal: abortSignal }, delta);
if (promotedAssetId) delivery.context.deliveredPages.set(promotedAssetId, pageId);
plaintext

patchNode 携带整个 fill,所以第二次读取不是无意义的重复请求:它把上传期间用户刚刚设置的 crop、flip 和 adjustments 重新带入 delta。另一方面,workspace asset id 不会触发第二次读取,因为它没有 promotion 的异步窗口;这是把额外 I/O 限定在真正存在竞态的路径。

4. #5519 把 reference lookup 移出 post-commit closure#

来源:PR #5519 diff,backend/go/internal/image/service.go:129-182,257-276。

input, err := spec.Decode(inputBytes)
if err != nil {
  return uuid.Nil, errors.Wrap(err, "decode task input")
}
extras, err := s.resolveExtraSourceRefs(ctx, spec, workspaceID, input)
if err != nil {
  return uuid.Nil, err
}

t, err := s.taskService.Submit(ctx, task.SubmitInput{
  InputData: task.JSONData(inputBytes),
  Publish: func(pubCtx context.Context, publish task.PublishInput) error {
    plan, err := s.buildPublishPlan(pubCtx, spec, publish.TaskID, srcAsset, input, extras)
    ...
  },
})
plaintext

resolveExtraSourceRefs 对 Asset 类型的 reference 返回裸 assetService.GetByID 错误(service.go:263-276),因为 Gin middleware 按具体 *bizerrors.APIError 类型映射 404/102;如果在这里 wrap,错误会再次变成 500。contract test submit_test.go:237-345 不只断言 HTTP 404/102,还直接检查 unknown reference 时 task count 和 task_debit 都为零,retry 场景则保持原 task/debit 各一条。

5. #5507 把错误 id 变成终态,而不是可重试异常#

来源:PR #5507 diff,backend/workers/agent/src/goApi.ts:662-734 与 imageTaskTool.ts:166-214。

if (result.error.code === GO_API_ERROR_CODE.notFound) {
  return { status: "asset_not_found" };
}

if (submitResult.status === "asset_not_found") {
  return {
    status: "failed",
    reason: "error",
    errorCode: "asset_not_found",
    message: "Do not re-send the same ids; use an earlier result or upload path.",
  };
}
plaintext

#5507 当时只覆盖 source id;#5519 让同一 gate 也能覆盖 reference id。关键设计是 terminal result 不进入 pending store、不进入 wait/poll,也不会把模型的错误输入伪装成 server fault。

6. #5530 让“任务成功”和“画布已更新”分离#

来源:PR #5530 diff,backend/workers/agent/src/tools/imageDelivery.ts:184-259、toolDefs.ts:274-301。

return slot.kind === "adopted"
  ? { status: "failed", pageId: slot.pageId, nodeId: slot.nodeId }
  : { status: "failed" };

return { ...result, delivery: outcome };
plaintext

schema 对 placed 使用必须存在的 pageId/nodeId,对 failed/withdrawn 保留可选位置。这样模型在 delivery failed 时能继续使用同一个 assetId 补放置;withdrawn 则不会把用户主动删除或替换的 node 复原。#5530 没有新增测试文件,主要验证证据来自真实运行手册与当前 schema/runtime diff;因此这部分的部署级证明仍是开放项。

工程取舍#

边界与复用#

  • chat attachment 与 workspace asset 分层。 attachment promoter 是唯一把 chat-scoped R2 对象铸造成 page/task 可消费 asset 的 seam;insert/update/remove_background/image refs 复用它,不各自实现 upload、路径解析和 dedupe。
  • runtime 负责交付,模型负责意图。 #5422 让 image task 的结果统一通过 slot;#5520 的显式 insert/update 仍是用户明确要求的写入动作,不能让自动 slot 与模型 tool call 双写同一 image。
  • turn-local 账本不冒充持久事实。 deliveredPages 只记录本 turn 由 Agent 交付的 asset,避免把用户早已拥有的 workspace asset 错当成一次性产物;它不替代 page revision 或数据库唯一约束。

副作用顺序与兼容性#

  • #5520 的顺序是 resolve target -> promote -> getAsset/type check -> create canvas -> page delta。这是用一次额外的 target resolution 分支换取“不留空 page/不提前铸造 asset”。
  • #5519 在 task.Submit 前解析 reference,保持同一个 task.Submit 事务负责 task row + credit debit;旧的 post-commit closure 仍只负责 publish,减少错误和回滚之间的歧义。
  • 输入字段仍叫 assetId,调用方只在函数内部绑定为 sourceId/replacementId;没有为 attachment id 引入第二套 schema,也没有做旧新双写。
  • delivery 在成功 output 中是 optional,兼容已有 transcript/tool output;当前报告依赖的是同一 release 内的 worker/schema 一起变更。

性能与并发#

  • workspace asset id 走 attachmentFor 的 Map miss 后直接返回 undefined,不做 R2 或 Go 请求;只有 chat upload 才付出 R2 read、uploadAsset 和 store set。
  • 同 turn 的同一 upload 在 inFlight Map 中共享 promise;跨 turn 的 attachmentAssetStore 复用已提升 id。#5520 update path 只在 promotion 存在时重读页面。
  • claimedSlots 避免多个 image delivery 根据同一 snapshot 选择相同位置,但 deliveredPages 的读、page delta 写和 map set 不是原子事务;它是降低重复的局部账本,不是并发一致性方案。

可靠性与测试策略#

PR已有代码证据仍未被证明
#5422imageDelivery 的 source/adopted slot、fill-only patch、撤回 reservation 和 undo reverse delta真实多 tool 并发、page revision 冲突和跨浏览器同步。
#5506/#5520#5506 接入 remove_background;#5520 新增 insertImageNodeTool.test.ts 和大量 update path 测试,覆盖 promotion、no-file、非 image、重读和 delivered-once#5520 的测试主要 stub Go API;只有 insert upload 有手工真实 R2/asset placement 验证,update upload 与 rejection 不是部署级测试。
#5507/#5519worker gate 测试;#5519 Go contract test 断言 404/102、task/debit 计数和 retry 不重复扣费Go deployed contract、middleware wrapping 变化、未知 reference 在实际 Agent tool 中的全链路。
#5530schema union 强制 placed 带 page/node,并将 delivery outcome 写进 model output没有在该 PR 中新增测试文件;真实 503、slot 回填失败、模型补投递和 replay 组合仍需运行时验证。

和最近学习记录的关系#

最近的 #5507 学习报告 已把“错误 source id 不应 throw”作为图片任务的终态边界,并明确记录 unknown reference 仍会被 wrapped APIError 变成 500。今天不是重复 #5507:#5519 在 Go 侧把 reference lookup 前移,证明未知 reference 不创建 task/debit;#5520 又把同一个资产归一化 seam 扩展到最后两个直接读 asset 的 placement tools。

同一份 #5507 报告把 #5422、#5461、#5506 作为背景链。今天只保留与当前 anchor 有代码关系的 #5422/#5506,并加入 #5530 的 delivery outcome 视角:前者解释为什么 image result 不能有第二个 writer,后者解释为什么“result asset 存在”仍不等于“用户画布已经更新”。

上一条并行的 #5503 学习报告 研究的是 DO/WebSocket 的连接恢复预算,不是本次图片资产线的前置。两者共享 Agent turn 的可靠性关注点,但没有把 chat reconnect PR 拼进本报告;本次新增证据集中在资产 id、task submit、page delta 和交付结果之间的边界。

我会怎么吸收#

  1. 先画资产生命周期,再设计工具参数。 明确 chat-scoped object、workspace asset、task result asset 和 page fill.assetId 的所有权与可解析范围,避免用一个字符串掩盖多个存储域。
  2. 把不可逆操作放在拒绝点之后。 用 ready/creatable/unavailable 或显式阶段类型表达“现在可以创建什么”,让代码审查能直接检查 upload、debit、task row、canvas 和 delta 的顺序。
  3. 把同一 turn 的共享资源放在一个 context。 promoter、in-flight dedupe、claimed slots、delivered pages 必须由同一 turn owner 创建和传递;否则每个 tool 各自“正确”仍会互相重复。
  4. 让终态返回可驱动下一步。 对“错误输入”“任务失败”“交付失败”“用户撤回”分别给出结构化 result,避免统一 throw 触发错误重试或让模型声称未发生的副作用。
  5. 测试副作用的顺序,而不是只测函数返回值。 #5520 的测试断言 promote/addPage/getAsset/delta 的调用关系;#5519 的 contract test 继续查 task 和 debit 行,这比只看 404 更能证明 billing-safe。

边界、风险与未解问题#

已验证的代码事实#

  • #5520 的 merge commit 和 #5422、#5506、#5507、#5519、#5530 的 merge commit 都是当前 origin/main 的祖先。
  • #5520 将一个 turn-level promoter 传给五类图片相关 tool;insert/update 的 raw upload id 在 Go getAsset 和 page fill 前都已转换为 promoted asset id。
  • resolveImageTarget 的 resolution 阶段没有 addPage;非 image asset 在 getAsset 检查处被拒绝,之后才可能创建 canvas。
  • #5519 的 Go contract test 检查 unknown reference 为 HTTP 404/code 102,初次 submit 的 task/debit 为零;deleted reference retry 保持原 task/debit 各一条。
  • #5530 的 output schema 对 placed 要求 pageId/nodeId;delivery failed/withdrawn 的 tool output 携带不同的补救语义。

合理推断#

  • 同一 turn 内复用 promoter 能显著降低同一上传被两个 tool 同时提升为两个 asset 的概率;这依赖 inFlight Map 的进程/turn 生命周期,并不等价于持久化唯一约束。
  • 将 reference lookup 移出 post-commit closure 应消除“先 debit、再 publish 失败、再 refund”的一类已知浪费,但不能保证所有 Go 任务错误都在 debit 前发生;queue publish、signed URL 和 task service 仍有自己的失败语义。
  • #5520 的 canvas/asset 顺序应降低孤儿副作用,但 page delta 没有 base revision,所以用户在最后一次 read 之后继续编辑时仍可能发生 stale write。

仍待确认#

  1. 模拟 worker 在 uploadAsset 成功后、attachmentAssetStore.set 前重启,确认重试是否产生重复 workspace asset;必要时把 attachment promotion 做成可重试的持久幂等协议。
  2. 用两个并发 image tools、两个 browser tab 和两个 Agent turn 复现 deliveredPages 的读-写竞态,决定是否需要 Go/page-delta 侧的 asset delivery reservation 或版本条件写。
  3. 为 insert/update 的 invented/deleted/foreign asset 设计与 #5507/#5519 对称的 terminal asset_not_found result,避免 Sentry 把模型输入错误统计为 server fault。
  4. 在真实 Agent worker + Go + R2 + browser 中覆盖 upload promotion、reference 404、task resume、slot fill 失败、用户删除/移动节点和 model follow-up 补投递。
  5. 评估 PNG/JPEG/WebP 限制、附件过期和 upload path 解析在真实多格式上传中的产品提示;不要只以 unit test 的 supported media set 作为用户契约。

候选说明#

候选窗口按 Australia/Melbourne 执行:先查 2026-08-04,当天的工程候选包括 #5520、#5519、#5533、#5389 和 #5479;#5520 与 #5519 直接共享 image tool / asset boundary,#5520 改动更集中地覆盖 attachment promotion、canvas creation 和 placement tests,因此作为 anchor。#5533 是 prompt/eval continuity,#5389 是 queued follow-up,#5479 是 Go request-context infra,均不能自然串入这条图片资产交付线。

再看 2026-08-03,#5507 和 #5503 已在学习日志中作为锚点完成,不能重复;#5507 被作为本次前置背景,因为 #5519 正好兑现它留下的 reference 资产问题。#5422 位于最近 30 天内,是 #5520 的 runtime delivery 基础;没有为了补数量扩大到更早、无直接代码 seam 的 PR。

GitHub 文档#

  • 报告文件:outputs/voyager-daily-pr-study/2026-08-04-pr-5520-agent-image-asset-promotion-delivery.md
  • 索引文件:outputs/voyager-daily-pr-study/index.md
  • 今日锚点:Voyager PR #5520
  • 代码 merge commit:81faf2802550022b3f96a860101da3d364f8d466
  • 目标仓库:joyehuang/ai-agent-field-notes

🗂️ 这是知识库中的🔬 研究。

内容可能仍在补充或修订中。

← Back