fix(subagents): enforce UTF-8 snapshot budgets - #238
Conversation
535806d to
3eb2568
Compare
somewan820
left a comment
There was a problem hiding this comment.
P1 Must-Fix:extensions/subagents/src/manager.ts:427-435 的 persistExactResult() 在 settle() 中执行,未处理 writer 错误;生产 persistResultArtifact() 可因权限、symlink、磁盘满或 collision 抛错,导致 settle() 在 notify()/onSettled() 前失败,waiter/result delivery 可能永远收不到终态。请让 artifact cache fail-soft,或保证 settlement 先终态通知再处理缓存失败,并补充 failure-path tests。
P1 Must-Fix:exact-result artifact 没有 retention/aggregate size limit,长期运行会持续积累唯一结果并耗尽 cache volume。请加入 bounded cleanup/retention 及写入/清理失败恢复。
P2 Should-Fix:enforceSnapshotBudget() 将有损 projection 写回 live entry.snapshot,transcript omits 后 takeover UI 无 rehydrate 路径,历史会静默消失。执行 snapshot 与 UI/model projection 应分离,或提供显式 rehydrate/omission 状态。P3:snapshot parsers 当前无生产调用方。静态审查,未运行测试。
3a0cc46 to
51b6a22
Compare
|
感谢审查,已在最新提交 中处理这些问题。 P1:artifact persistence 不再阻断 settlement settle() 现在先完成 terminal status、retained result、projection 更新、 notify() 、 onSettled() 、waiter 唤醒及并发槽释放,再将 artifact persistence 调度为异步的可选恢复工作。因此权限错误、symlink、磁盘满、collision、lock contention 或 writer throw 都不会阻止终态通知,也不会让 waiter/result delivery 永久等待。 延迟 writer 会捕获 cache/filesystem 异常,严格验证 ref 只能是 { version: 1, digest } ,验证 digest 与 exact result 内容匹配,并使用 run generation guard 防止旧 run 的延迟 writer 把 artifact 附加到重启后的新 run。失败时不会回滚 terminal status 或 finalText 。 新增/更新的 manager failure‑path tests 验证了 writer throw、wrong digest 和非法 ref 场景下: waitFor() 返回、终态可从 per‑id listener 观察、 onSettled() 被调用,且后续仍可 spawn。 P1:artifact cache bounded retention cache 现在有硬性 retention 上限:最多 64 个 artifact 文件、最多 64 MiB payload;自定义 maxFiles / maxBytes 不能超过 hard cap。retention eviction 和 stale metadata cleanup 在 cache lock 内完成。超大写入在触碰 cache 前拒绝,不留下 partial artifact 或 .tmp ,后续正常写入仍可继续。 锁获取使用有界重试;ownership 不确定、PID reuse、lock contention、symlink、unknown/recovery entry 或平台无法提供安全 no‑follow 操作时均 fail‑closed。release cleanup 失败也不会把已成功发布的 ref 变成失败。 P2:canonical snapshot 与 projection 分离 entry.snapshot 保持 canonical event‑folding state, entry.projection 是独立的 aggregate‑bounded read model;projection 不再写回 canonical snapshot。takeover 继续使用 view.getFull?.(id) ?? view.get(id) 。 projection rebuild 失败时会清除旧 projection,读取方回退到最新 canonical state,避免 settled 条目继续显示旧的 running 。exact‑result recovery 只使用经过校验的 artifact 或明确完整的 canonical retained finalText ,不会把 projection 当作 recovery source。新增 finalTextTruncated 标记:当 artifact 不可用且 retained text 被 1 MiB cap 截断时, subagent_result 会明确报告 exact result unavailable,而不是返回截断前缀。 P3:snapshot projection 已有生产调用方 manager 会在 entry 注册和 lifecycle 更新时调用 snapshot projection;canonical snapshot 仍供 event folding 和 takeover rehydration 使用,model‑facing 工具使用 bounded projection。 验证 已执行: text node --test --experimental-strip-types tests/extensions/subagents/manager.test.ts tests/extensions/subagents/result-artifact.test.ts tests/extensions/subagents/snapshot.test.ts tests/extensions/subagents/takeover.test.ts tests/extensions/subagents/index.test.ts 结果:88 tests,74 passed,0 failed,14 skipped。 另已执行并通过: text bun run typecheck |
tt-a1i
left a comment
There was a problem hiding this comment.
复审 exact head b3d7d4665f160e0e3eb8fcaba73c27f536025885。目标是用UTF-8字节预算限制子代理展示快照,同时通过canonical state/受保护artifact保留恢复能力。旧review中的settlement被缓存异常阻塞、缓存无上限、展示投影覆盖canonical state三项都有实质改进,不重复计入。
Standards
独立人工审查无额外架构要求;现有自动守卫确认1项:新增subagent_result没有完成child分类,也未纳入现有tool surface,违反已有工具归属契约。请在现有表中补齐,不要新增一层权限系统。
Spec
3项已复现P2:
- Darwin使用/dev/fd/目录句柄拼子路径,macOS上的cache读写失败。
- 超过16KiB的单行结果无法通过line offset/limit恢复;limit=1仍只有截断提示。
- truncateUtf8Head把四字节字符的3字节前缀解码为替换字符,却误判为完整UTF-8前缀。
详见行内。它们直接影响本PR承诺的可恢复性、兼容性和UTF-8正确性;无需扩展成新存储框架。
验证
Node24/macOS exact checkout:check因index.ts/manager.ts格式失败;完整test失败,明确包含child drift guard和Mac artifact错误。独立重跑guard仍报subagent_result未分类;Mac artifact专项仍报ENOENT /dev/fd/.../cache。独立pageResultText复现20KiB单行只返回60-byte提示;truncateUtf8Head("😀",3)返回替换字符,4096字节真实预算同样复现。GitHub Node22/24 CI也为失败。不把其他并行运行出现的setup超时直接归因本PR;未做实际Pi UI或发布验收。
请修复这些边界并跑通check/test后再复审。本轮未修改源码、提交或合并。
| }); | ||
|
|
||
| pi.registerTool({ | ||
| name: "subagent_result", |
There was a problem hiding this comment.
[P2] 将新工具纳入现有归属和 child 权限分类
新增 subagent_result 后,extensions/shared/child-session.ts 的 child-safe/excluded 列表都没有它,OPENPI_TOOL_SURFACE.subagents 也未登记。当前现有 every registered package tool is classified ... 测试稳定失败,明确报该工具未分类。请按真实用途加入已有 parent-only/child-safe 分类及 subagents 工具可见性表,验证关闭能力和子会话边界,不应让新工具绕开已建立的登记契约。
| function descriptorRelativePath(fd: number, fallback: string) { | ||
| if (process.platform === "linux") return `/proc/self/fd/${fd}`; | ||
| if ( | ||
| process.platform === "darwin" || |
There was a problem hiding this comment.
[P2] Darwin 的 /dev/fd 不能当作 Linux 的目录遍历入口
本机 macOS 实测目录fd本身可fstat,/dev/fd/<fd>也显示directory,但访问/dev/fd/<fd>/package.json即ENOENT;它不像Linux /proc/self/fd/<fd>支持继续拼接子路径。因此这里声明支持darwin后,openCheckedDirectory中的mkdir/read始终失败,artifact专项稳定报ENOENT mkdir /dev/fd/14/cache,不是测试并行噪声。请验证Mac可用、具有同等安全边界的实现;如果决定暂不支持,需在I/O前明确fail-closed并披露精确恢复的退化,不能仅跳过测试或声称兼容性未变。未推断其他BSD行为。
| truncated: false, | ||
| }; | ||
| } | ||
| const suffix = `\n[page truncated at ${maxBytes} bytes; reduce the requested range]`; |
There was a problem hiding this comment.
[P2] 为超长单行保留可继续读取的恢复路径
当前工具只有行offset/limit,超出字节预算后使用的truncateHead不会截取首条超长行的部分内容。真实输入20KiB单行+尾标记,offset0/limit200与offset0/limit1都只返回60字节的截断提示;offset1已经EOF。用户按提示reduce range也无法取得正文,违反exact recovery用途。请提供最小行内继续读取方式或等价受保护恢复路径,同时保持页面字节上限,并补单行超限的完整恢复测试。
| let end = maxBytes; | ||
| while (end > 0) { | ||
| const candidate = bytes.subarray(0, end).toString("utf8"); | ||
| if (byteLength(candidate) === end) return candidate; |
There was a problem hiding this comment.
[P2] 解码后字节数相同不代表 UTF-8 边界完整
truncateUtf8Head("😀", 3)实际返回U+FFFD替换字符:四字节emoji的前三字节解码成一个三字节replacement,恰好满足byteLength(candidate) === end,因此提前返回了并非原文前缀的字符串。真实4096预算下,4093个ASCII字符后接emoji也会出现同样损坏。现测试只有三字节中文,漏掉了此情形。请按codepoint边界回退或采用严格解码验证,并覆盖四字节字符各切割位置;不要让有界裁剪改写原文内容。
|
当前 head b3d7d46 已复审,Changes Requested。旧的缓存失败阻塞settlement、无缓存上限、canonical/projection混用已有实质修复;新确认4项P2:工具分类遗漏、Mac缓存路径不可用、超长单行无法恢复、四字节UTF-8截断产生替换字符。check/test及必需CI当前均非绿色,详见 #238 (review) 。建议在现有边界内逐项修,不增加新框架。本轮未改源码或合并。 |
Keep canonical settlement separate from bounded projection, persist artifacts fail-soft, and recover through subagent_result(id, offset, limit) instead of cache pathnames.
Keep exact-result paging, UTF-8 truncation, Darwin cache paths, and child-tool classification within the existing subagent surface.
ba54ec5 to
781775e
Compare
tt-a1i
left a comment
There was a problem hiding this comment.
复审 exact head 781775e9acf419d34bff904c1e1353229251202e。目标是给 Direct Subagent snapshot 建立聚合 UTF-8 硬上限,同时保留 exact result 的恢复能力;价值是避免长 Session 的模型/UI read model 持续膨胀。当前提交已经修好工具分类、四字节 UTF-8 截断和超长单行分页,但仍有跨平台恢复、fail-open 和生命周期问题。
Standards
2 项:projection rebuild 失败会回退到完整 canonical snapshot,硬资源上限 fail-open;artifact persistence 用未受 Session Scope 管理的裸 setTimeout,shutdown 不取消/等待,且测试把 onSettled 内断言异常吞掉了。
Spec
2 项:exact artifact 现在仅支持 Linux,macOS 上超过 1 MiB 的终态结果不可恢复;Issue #180 要求的旧版/超大 persisted snapshot restore 边界仍没有实现,当前 projection 只存在于进程内 manager。另有 line-offset 转 byte cursor 时文本提示的 cursor 不是绝对值。
验证:macOS exact checkout bun run check 通过;manager/snapshot/result-artifact 专项 47 passed、14 个 Linux-only skipped;独立分页复现得到文本 next 38,结构化 absolute nextByteOffset=45。GitHub Node 22/24 仍各失败 2 项,因此必需 gate 未通过。建议先缩小并闭合现有边界,不要继续扩充存储框架。
| const RESULT_CACHE_RECOVERY_NAME = | ||
| /^\.retention-lock\.recovery\.[0-9a-f-]{36}$/iu; | ||
| const RESULT_ARTIFACT_TEMP_NAME = /^\.[a-f0-9]{64}\.[0-9a-f-]{36}\.tmp$/u; | ||
| const SUPPORTED_DESCRIPTOR_PLATFORMS = new Set(["linux"]); |
There was a problem hiding this comment.
[P1] 这不是修复 Darwin cache,而是把 exact-result artifact 限定为 Linux。macOS 上 persist 会抛出 unsupported,read 会变成 miss;manager 又只保留 1 MiB finalText,所以更大的 settled result 会被 subagent_result 明确判为 exact unavailable。Issue #180 要求 exact result artifacts 保持可恢复,PR body 也未披露 Linux-only。最小处理可以明确收窄 PR/Issue 范围并保留 #180 未完成;若仍声称 Fixes #180,则需要经验证的 Pi-native/平台安全恢复机制。
| return true; | ||
| } catch { | ||
| // Never expose a projection from an older lifecycle state after a | ||
| // failed rebuild; readers fall back to the canonical snapshot. |
There was a problem hiding this comment.
[P1] 投影重建失败时清空所有 bounded projection,而 view.list/get 随后用 entry.projection ?? entry.snapshot 回退到完整 canonical snapshot。资源上限自身一旦失败,模型/UI 就收到未受 aggregate budget 限制的数据,属于 fail-open。请保留最后一个安全 projection 或返回明确的有界失败状态,不能把 canonical full snapshot 当普通 reader fallback。
| // Artifact persistence is optional recovery work. Schedule it only | ||
| // after settlement has notified waiters and hooks, so a slow or broken | ||
| // writer can never hold the lifecycle path or a concurrency slot. | ||
| setTimeout(() => { |
There was a problem hiding this comment.
[P2] 这个裸 setTimeout 不属于 manager Scope;disposeAll() 不取消也不等待它,所以 Session 关闭后仍可能写 cache、修改 entry 并触发 notify。它也保证 onSettled/delivery clone 发生在 artifact 关联之前。当前“persisted before settlement”测试在 onSettled 里抛出的断言会被生产代码 catch 掉,因此是假阳性。请跟踪并在 teardown 取消/有界等待该任务,同时用不会被生产 catch 吞掉的外部观测验证顺序。
| ); | ||
| } | ||
|
|
||
| const noticeFor = (next: number) => `\n[page truncated; next ${next}]`; |
There was a problem hiding this comment.
[P2] line-offset 页面截断时,这个 notice 使用 page-local end,但返回的 nextByteOffset 是加上 pageStartByte 的绝对 cursor。复现 prefix\n + 长行、offset:1,maxBytes:64:正文提示 next 38,details 返回 nextByteOffset:45。Pi 提供给模型的可靠正文不能指向另一个位置;请让提示使用 absolute cursor,或删掉正文中的数值并只暴露一个可消费的权威 cursor。
概要
验证
bun test tests/extensions/subagents/snapshot.test.ts tests/extensions/subagents/manager.test.ts tests/extensions/subagents/index.test.tsbun run lintbun run typecheckFixes #180