Skip to content

fix(subagents): enforce UTF-8 snapshot budgets - #238

Open
smf-h wants to merge 4 commits into
openpi-dev:mainfrom
smf-h:fix/subagents-snapshot-180
Open

fix(subagents): enforce UTF-8 snapshot budgets#238
smf-h wants to merge 4 commits into
openpi-dev:mainfrom
smf-h:fix/subagents-snapshot-180

Conversation

@smf-h

@smf-h smf-h commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

概要

  • 为 subagent snapshot 增加严格的 UTF-8 字节级预算。
  • 对多个 agent 进行公平裁剪,并保留生命周期身份、终态状态、错误信息和恢复所需引用。
  • 在展示投影前持久化精确 terminal result,读取时优先使用 artifact 内容。
  • 保留 raw active tool map,确保 ToolUpdate / ToolEnd 生命周期事件仍可关联。
  • 对 malformed、oversized 和非法 snapshot 输入 fail safely。
  • 增加 UTF-8 边界、公平裁剪、幂等投影、artifact precedence 和 tool lifecycle 回归测试。

验证

  • bun test tests/extensions/subagents/snapshot.test.ts tests/extensions/subagents/manager.test.ts tests/extensions/subagents/index.test.ts
  • bun run lint
  • bun run typecheck

Fixes #180

@smf-h
smf-h force-pushed the fix/subagents-snapshot-180 branch from 535806d to 3eb2568 Compare August 28, 2026 18:07

@somewan820 somewan820 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 当前无生产调用方。静态审查,未运行测试。

@github-actions github-actions Bot added the area:subagents Subagent delegation, skills, or tests label Aug 30, 2026
@smf-h
smf-h force-pushed the fix/subagents-snapshot-180 branch from 3a0cc46 to 51b6a22 Compare August 30, 2026 08:34
@smf-h
smf-h requested a review from somewan820 August 30, 2026 08:37
@smf-h

smf-h commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

感谢审查,已在最新提交   中处理这些问题。

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
bun run lint
 
14 个 skipped 测试依赖 POSIX descriptor‑relative/no‑follow 文件系统能力;当前环境为 Windows,因此按预期跳过。Windows fail‑closed 测试已通过。POSIX lock recovery、并发 retention 和 symlink cache 行为未能在当前 Windows 环境实际执行,未修改 skip 条件或削弱安全行为。

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 exact head b3d7d4665f160e0e3eb8fcaba73c27f536025885。目标是用UTF-8字节预算限制子代理展示快照,同时通过canonical state/受保护artifact保留恢复能力。旧review中的settlement被缓存异常阻塞、缓存无上限、展示投影覆盖canonical state三项都有实质改进,不重复计入。

Standards

独立人工审查无额外架构要求;现有自动守卫确认1项:新增subagent_result没有完成child分类,也未纳入现有tool surface,违反已有工具归属契约。请在现有表中补齐,不要新增一层权限系统。

Spec

3项已复现P2:

  1. Darwin使用/dev/fd/目录句柄拼子路径,macOS上的cache读写失败。
  2. 超过16KiB的单行结果无法通过line offset/limit恢复;limit=1仍只有截断提示。
  3. 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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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" ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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]`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 为超长单行保留可继续读取的恢复路径

当前工具只有行offset/limit,超出字节预算后使用的truncateHead不会截取首条超长行的部分内容。真实输入20KiB单行+尾标记,offset0/limit200与offset0/limit1都只返回60字节的截断提示;offset1已经EOF。用户按提示reduce range也无法取得正文,违反exact recovery用途。请提供最小行内继续读取方式或等价受保护恢复路径,同时保持页面字节上限,并补单行超限的完整恢复测试。

Comment thread extensions/subagents/src/snapshot.ts Outdated
let end = maxBytes;
while (end > 0) {
const candidate = bytes.subarray(0, end).toString("utf8");
if (byteLength(candidate) === end) return candidate;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 解码后字节数相同不代表 UTF-8 边界完整

truncateUtf8Head("😀", 3)实际返回U+FFFD替换字符:四字节emoji的前三字节解码成一个三字节replacement,恰好满足byteLength(candidate) === end,因此提前返回了并非原文前缀的字符串。真实4096预算下,4093个ASCII字符后接emoji也会出现同样损坏。现测试只有三字节中文,漏掉了此情形。请按codepoint边界回退或采用严格解码验证,并覆盖四字节字符各切割位置;不要让有界裁剪改写原文内容。

@tt-a1i

tt-a1i commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

当前 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.
@smf-h
smf-h force-pushed the fix/subagents-snapshot-180 branch from ba54ec5 to 781775e Compare September 1, 2026 04:34

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 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"]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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}]`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:subagents Subagent delegation, skills, or tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

subagents: 聚合 snapshot 缺少统一 byte 上限

3 participants